- Notifications
You must be signed in to change notification settings - Fork382
Tiny WebSocket library for Go.
License
gobwas/ws
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
RFC6455 WebSocket implementation in Go.
- Zero-copy upgrade
- No intermediate allocations during I/O
- Low-level API which allows to build your own logic of packet handling andbuffers reuse
- High-level wrappers and helpers around API in
wsutil
package, which allowto start fast without digging the protocol internals
Existing WebSocket implementations do not allow users to reuse I/O buffersbetween connections in clear way. This library aims to export efficientlow-level interface for working with the protocol without forcing only one wayit could be used.
By the way, if you want get the higher-level tools, you can usewsutil
package.
Library is tagged asv1*
so its API must not be broken during someimprovements or refactoring.
This implementation of RFC6455 passesAutobahn TestSuite and currently hasabout 78% coverage.
Example applications usingws
are developed in separate repositoryws-examples.
The higher-level example of WebSocket echo server:
package mainimport ("net/http""github.com/gobwas/ws""github.com/gobwas/ws/wsutil")funcmain() {http.ListenAndServe(":8080",http.HandlerFunc(func(w http.ResponseWriter,r*http.Request) {conn,_,_,err:=ws.UpgradeHTTP(r,w)iferr!=nil {// handle error}gofunc() {deferconn.Close()for {msg,op,err:=wsutil.ReadClientData(conn)iferr!=nil {// handle error}err=wsutil.WriteServerMessage(conn,op,msg)iferr!=nil {// handle error}}}()}))}
Lower-level, but still high-level example:
import ("net/http""io""github.com/gobwas/ws""github.com/gobwas/ws/wsutil")funcmain() {http.ListenAndServe(":8080",http.HandlerFunc(func(w http.ResponseWriter,r*http.Request) {conn,_,_,err:=ws.UpgradeHTTP(r,w)iferr!=nil {// handle error}gofunc() {deferconn.Close()var (state=ws.StateServerSidereader=wsutil.NewReader(conn,state)writer=wsutil.NewWriter(conn,state,ws.OpText))for {header,err:=reader.NextFrame()iferr!=nil {// handle error}// Reset writer to write frame with right operation code.writer.Reset(conn,state,header.OpCode)if_,err=io.Copy(writer,reader);err!=nil {// handle error}iferr=writer.Flush();err!=nil {// handle error}}}()}))}
We can apply the same pattern to read and write structured responses through a JSON encoder and decoder.:
...var (r=wsutil.NewReader(conn,ws.StateServerSide)w=wsutil.NewWriter(conn,ws.StateServerSide,ws.OpText)decoder=json.NewDecoder(r)encoder=json.NewEncoder(w))for {hdr,err=r.NextFrame()iferr!=nil {returnerr}ifhdr.OpCode==ws.OpClose {returnio.EOF}varreqRequestiferr:=decoder.Decode(&req);err!=nil {returnerr}varrespResponseiferr:=encoder.Encode(&resp);err!=nil {returnerr}iferr=w.Flush();err!=nil {returnerr}}...
The lower-level example withoutwsutil
:
package mainimport ("net""io""github.com/gobwas/ws")funcmain() {ln,err:=net.Listen("tcp","localhost:8080")iferr!=nil {log.Fatal(err)}for {conn,err:=ln.Accept()iferr!=nil {// handle error}_,err=ws.Upgrade(conn)iferr!=nil {// handle error}gofunc() {deferconn.Close()for {header,err:=ws.ReadHeader(conn)iferr!=nil {// handle error}payload:=make([]byte,header.Length)_,err=io.ReadFull(conn,payload)iferr!=nil {// handle error}ifheader.Masked {ws.Cipher(payload,header.Mask,0)}// Reset the Masked flag, server frames must not be masked as// RFC6455 says.header.Masked=falseiferr:=ws.WriteHeader(conn,header);err!=nil {// handle error}if_,err:=conn.Write(payload);err!=nil {// handle error}ifheader.OpCode==ws.OpClose {return}}}()}}
Zero-copy upgrade helps to avoid unnecessary allocations and copying whilehandling HTTP Upgrade request.
Processing of all non-websocket headers is made in place with use of registereduser callbacks whose arguments are only valid until callback returns.
The simple example looks like this:
package mainimport ("net""log""github.com/gobwas/ws")funcmain() {ln,err:=net.Listen("tcp","localhost:8080")iferr!=nil {log.Fatal(err)}u:= ws.Upgrader{OnHeader:func(key,value []byte) (errerror) {log.Printf("non-websocket header: %q=%q",key,value)return},}for {conn,err:=ln.Accept()iferr!=nil {// handle error}_,err=u.Upgrade(conn)iferr!=nil {// handle error}}}
Usage ofws.Upgrader
here brings ability to control incoming connections ontcp level and simply not to accept them by some logic.
Zero-copy upgrade is for high-load services which have to control manyresources such as connections buffers.
The real life example could be like this:
package mainimport ("fmt""io""log""net""net/http""runtime""github.com/gobwas/httphead""github.com/gobwas/ws")funcmain() {ln,err:=net.Listen("tcp","localhost:8080")iferr!=nil {// handle error}// Prepare handshake header writer from http.Header mapping.header:=ws.HandshakeHeaderHTTP(http.Header{"X-Go-Version": []string{runtime.Version()},})u:= ws.Upgrader{OnHost:func(host []byte)error {ifstring(host)=="github.com" {returnnil}returnws.RejectConnectionError(ws.RejectionStatus(403),ws.RejectionHeader(ws.HandshakeHeaderString("X-Want-Host: github.com\r\n",)),)},OnHeader:func(key,value []byte)error {ifstring(key)!="Cookie" {returnnil}ok:=httphead.ScanCookie(value,func(key,value []byte)bool {// Check session here or do some other stuff with cookies.// Maybe copy some values for future use.returntrue})ifok {returnnil}returnws.RejectConnectionError(ws.RejectionReason("bad cookie"),ws.RejectionStatus(400),)},OnBeforeUpgrade:func() (ws.HandshakeHeader,error) {returnheader,nil},}for {conn,err:=ln.Accept()iferr!=nil {log.Fatal(err)}_,err=u.Upgrade(conn)iferr!=nil {log.Printf("upgrade error: %s",err)}}}
There is aws/wsflate
package to supportPermessage-Deflate CompressionExtension.
It provides minimalistic I/O wrappers to be used in conjunction with anydeflate implementation (for example, the standard library'scompress/flate).
It is also compatible withwsutil
's reader and writer by providingwsflate.MessageState
type, which implementswsutil.SendExtension
andwsutil.RecvExtension
interfaces.
package mainimport ("bytes""log""net""github.com/gobwas/ws""github.com/gobwas/ws/wsflate")funcmain() {ln,err:=net.Listen("tcp","localhost:8080")iferr!=nil {// handle error}e:= wsflate.Extension{// We are using default parameters here since we use// wsflate.{Compress,Decompress}Frame helpers below in the code.// This assumes that we use standard compress/flate package as flate// implementation.Parameters:wsflate.DefaultParameters,}u:= ws.Upgrader{Negotiate:e.Negotiate,}for {conn,err:=ln.Accept()iferr!=nil {log.Fatal(err)}// Reset extension after previous upgrades.e.Reset()_,err=u.Upgrade(conn)iferr!=nil {log.Printf("upgrade error: %s",err)continue}if_,ok:=e.Accepted();!ok {log.Printf("didn't negotiate compression for %s",conn.RemoteAddr())conn.Close()continue}gofunc() {deferconn.Close()for {frame,err:=ws.ReadFrame(conn)iferr!=nil {// Handle error.return}frame=ws.UnmaskFrameInPlace(frame)ifwsflate.IsCompressed(frame.Header) {// Note that even after successful negotiation of// compression extension, both sides are able to send// non-compressed messages.frame,err=wsflate.DecompressFrame(frame)iferr!=nil {// Handle error.return}}// Do something with frame...ack:=ws.NewTextFrame([]byte("this is an acknowledgement"))// Compress response unconditionally.ack,err=wsflate.CompressFrame(ack)iferr!=nil {// Handle error.return}iferr=ws.WriteFrame(conn,ack);err!=nil {// Handle error.return}}}()}}
You can use compression withwsutil
package this way:
// Upgrade somehow and negotiate compression to get the conn...// Initialize flate reader. We are using nil as a source io.Reader because// we will Reset() it in the message i/o loop below.fr:=wsflate.NewReader(nil,func(r io.Reader) wsflate.Decompressor {returnflate.NewReader(r)})// Initialize flate writer. We are using nil as a destination io.Writer// because we will Reset() it in the message i/o loop below.fw:=wsflate.NewWriter(nil,func(w io.Writer) wsflate.Compressor {f,_:=flate.NewWriter(w,9)returnf})// Declare compression message state variable.//// It has two goals:// - Allow users to check whether received message is compressed or not.// - Help wsutil.Reader and wsutil.Writer to set/unset appropriate// WebSocket header bits while writing next frame to the wire (it// implements wsutil.RecvExtension and wsutil.SendExtension).varmsg wsflate.MessageState// Initialize WebSocket reader as previously.// Please note the use of Reader.Extensions field as well as// of ws.StateExtended flag.rd:=&wsutil.Reader{Source:conn,State:ws.StateServerSide|ws.StateExtended,Extensions: []wsutil.RecvExtension{&msg, },}// Initialize WebSocket writer with ws.StateExtended flag as well.wr:=wsutil.NewWriter(conn,ws.StateServerSide|ws.StateExtended,0)// Use the message state as wsutil.SendExtension.wr.SetExtensions(&msg)for {h,err:=rd.NextFrame()iferr!=nil {// handle error.}ifh.OpCode.IsControl() {// handle control frame.}if!msg.IsCompressed() {// handle uncompressed frame (skipped for the sake of example// simplicity).}// Reset the writer to echo same op code.wr.Reset(h.OpCode)// Reset both flate reader and writer to start the new round of i/o.fr.Reset(rd)fw.Reset(wr)// Copy whole message from reader to writer decompressing it and// compressing again.if_,err:=io.Copy(fw,fr);err!=nil {// handle error.}// Flush any remaining buffers from flate writer to WebSocket writer.iferr:=fw.Close();err!=nil {// handle error.}// Flush the whole WebSocket message to the wire.iferr:=wr.Flush();err!=nil {// handle error.}}
About
Tiny WebSocket library for Go.