- Notifications
You must be signed in to change notification settings - Fork385
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.
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Uh oh!
There was an error while loading.Please reload this page.