github.com/ezoic/ws@v1.0.4-0.20220713205711-5c1d69e074c5/README.md (about)

     1  # ws
     2  
     3  [![GoDoc][godoc-image]][godoc-url]
     4  [![Travis][travis-image]][travis-url]
     5  
     6  > [RFC6455][rfc-url] WebSocket implementation in Go.
     7  
     8  # Features
     9  
    10  - Zero-copy upgrade
    11  - No intermediate allocations during I/O
    12  - Low-level API which allows to build your own logic of packet handling and
    13    buffers reuse
    14  - High-level wrappers and helpers around API in `wsutil` package, which allow
    15    to start fast without digging the protocol internals
    16  
    17  # Documentation
    18  
    19  [GoDoc][godoc-url].
    20  
    21  # Why
    22  
    23  Existing WebSocket implementations do not allow users to reuse I/O buffers
    24  between connections in clear way. This library aims to export efficient
    25  low-level interface for working with the protocol without forcing only one way
    26  it could be used.
    27  
    28  By the way, if you want get the higher-level tools, you can use `wsutil`
    29  package.
    30  
    31  # Status
    32  
    33  Library is tagged as `v1*` so its API must not be broken during some
    34  improvements or refactoring.
    35  
    36  This implementation of RFC6455 passes [Autobahn Test
    37  Suite](https://github.com/crossbario/autobahn-testsuite) and currently has
    38  about 78% coverage.
    39  
    40  # Examples
    41  
    42  Example applications using `ws` are developed in separate repository
    43  [ws-examples](https://github.com/ezoic/ws-examples).
    44  
    45  # Usage
    46  
    47  The higher-level example of WebSocket echo server:
    48  
    49  ```go
    50  package main
    51  
    52  import (
    53  	"net/http"
    54  
    55  	"github.com/ezoic/ws"
    56  	"github.com/ezoic/ws/wsutil"
    57  )
    58  
    59  func main() {
    60  	http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    61  		conn, _, _, err := ws.UpgradeHTTP(r, w)
    62  		if err != nil {
    63  			// handle error
    64  		}
    65  		go func() {
    66  			defer conn.Close()
    67  
    68  			for {
    69  				msg, op, err := wsutil.ReadClientData(conn)
    70  				if err != nil {
    71  					// handle error
    72  				}
    73  				err = wsutil.WriteServerMessage(conn, op, msg)
    74  				if err != nil {
    75  					// handle error
    76  				}
    77  			}
    78  		}()
    79  	}))
    80  }
    81  ```
    82  
    83  Lower-level, but still high-level example:
    84  
    85  
    86  ```go
    87  import (
    88  	"net/http"
    89  	"io"
    90  
    91  	"github.com/ezoic/ws"
    92  	"github.com/ezoic/ws/wsutil"
    93  )
    94  
    95  func main() {
    96  	http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    97  		conn, _, _, err := ws.UpgradeHTTP(r, w)
    98  		if err != nil {
    99  			// handle error
   100  		}
   101  		go func() {
   102  			defer conn.Close()
   103  
   104  			var (
   105  				state  = ws.StateServerSide
   106  				reader = wsutil.NewReader(conn, state)
   107  				writer = wsutil.NewWriter(conn, state, ws.OpText)
   108  			)
   109  			for {
   110  				header, err := reader.NextFrame()
   111  				if err != nil {
   112  					// handle error
   113  				}
   114  
   115  				// Reset writer to write frame with right operation code.
   116  				writer.Reset(conn, state, header.OpCode)
   117  
   118  				if _, err = io.Copy(writer, reader); err != nil {
   119  					// handle error
   120  				}
   121  				if err = writer.Flush(); err != nil {
   122  					// handle error
   123  				}
   124  			}
   125  		}()
   126  	}))
   127  }
   128  ```
   129  
   130  We can apply the same pattern to read and write structured responses through a JSON encoder and decoder.:
   131  
   132  ```go
   133  	...
   134  	var (
   135  		r = wsutil.NewReader(conn, ws.StateServerSide)
   136  		w = wsutil.NewWriter(conn, ws.StateServerSide, ws.OpText)
   137  		decoder = json.NewDecoder(r)
   138  		encoder = json.NewEncoder(w)
   139  	)
   140  	for {
   141  		hdr, err = r.NextFrame()
   142  		if err != nil {
   143  			return err
   144  		}
   145  		if hdr.OpCode == ws.OpClose {
   146  			return io.EOF
   147  		}
   148  		var req Request
   149  		if err := decoder.Decode(&req); err != nil {
   150  			return err
   151  		}
   152  		var resp Response
   153  		if err := encoder.Encode(&resp); err != nil {
   154  			return err
   155  		}
   156  		if err = w.Flush(); err != nil {
   157  			return err
   158  		}
   159  	}
   160  	...
   161  ```
   162  
   163  The lower-level example without `wsutil`:
   164  
   165  ```go
   166  package main
   167  
   168  import (
   169  	"net"
   170  	"io"
   171  
   172  	"github.com/ezoic/ws"
   173  )
   174  
   175  func main() {
   176  	ln, err := net.Listen("tcp", "localhost:8080")
   177  	if err != nil {
   178  		log.Fatal(err)
   179  	}
   180  
   181  	for {
   182  		conn, err := ln.Accept()
   183  		if err != nil {
   184  			// handle error
   185  		}
   186  		_, err = ws.Upgrade(conn)
   187  		if err != nil {
   188  			// handle error
   189  		}
   190  
   191  		go func() {
   192  			defer conn.Close()
   193  
   194  			for {
   195  				header, err := ws.ReadHeader(conn)
   196  				if err != nil {
   197  					// handle error
   198  				}
   199  
   200  				payload := make([]byte, header.Length)
   201  				_, err = io.ReadFull(conn, payload)
   202  				if err != nil {
   203  					// handle error
   204  				}
   205  				if header.Masked {
   206  					ws.Cipher(payload, header.Mask, 0)
   207  				}
   208  
   209  				// Reset the Masked flag, server frames must not be masked as
   210  				// RFC6455 says.
   211  				header.Masked = false
   212  
   213  				if err := ws.WriteHeader(conn, header); err != nil {
   214  					// handle error
   215  				}
   216  				if _, err := conn.Write(payload); err != nil {
   217  					// handle error
   218  				}
   219  
   220  				if header.OpCode == ws.OpClose {
   221  					return
   222  				}
   223  			}
   224  		}()
   225  	}
   226  }
   227  ```
   228  
   229  # Zero-copy upgrade
   230  
   231  Zero-copy upgrade helps to avoid unnecessary allocations and copying while
   232  handling HTTP Upgrade request.
   233  
   234  Processing of all non-websocket headers is made in place with use of registered
   235  user callbacks whose arguments are only valid until callback returns.
   236  
   237  The simple example looks like this:
   238  
   239  ```go
   240  package main
   241  
   242  import (
   243  	"net"
   244  	"log"
   245  
   246  	"github.com/ezoic/ws"
   247  )
   248  
   249  func main() {
   250  	ln, err := net.Listen("tcp", "localhost:8080")
   251  	if err != nil {
   252  		log.Fatal(err)
   253  	}
   254  	u := ws.Upgrader{
   255  		OnHeader: func(key, value []byte) (err error) {
   256  			log.Printf("non-websocket header: %q=%q", key, value)
   257  			return
   258  		},
   259  	}
   260  	for {
   261  		conn, err := ln.Accept()
   262  		if err != nil {
   263  			// handle error
   264  		}
   265  
   266  		_, err = u.Upgrade(conn)
   267  		if err != nil {
   268  			// handle error
   269  		}
   270  	}
   271  }
   272  ```
   273  
   274  Usage of `ws.Upgrader` here brings ability to control incoming connections on
   275  tcp level and simply not to accept them by some logic.
   276  
   277  Zero-copy upgrade is for high-load services which have to control many
   278  resources such as connections buffers.
   279  
   280  The real life example could be like this:
   281  
   282  ```go
   283  package main
   284  
   285  import (
   286  	"fmt"
   287  	"io"
   288  	"log"
   289  	"net"
   290  	"net/http"
   291  	"runtime"
   292  
   293  	"github.com/ezoic/httphead"
   294  	"github.com/ezoic/ws"
   295  )
   296  
   297  func main() {
   298  	ln, err := net.Listen("tcp", "localhost:8080")
   299  	if err != nil {
   300  		// handle error
   301  	}
   302  
   303  	// Prepare handshake header writer from http.Header mapping.
   304  	header := ws.HandshakeHeaderHTTP(http.Header{
   305  		"X-Go-Version": []string{runtime.Version()},
   306  	})
   307  
   308  	u := ws.Upgrader{
   309  		OnHost: func(host []byte) error {
   310  			if string(host) == "github.com" {
   311  				return nil
   312  			}
   313  			return ws.RejectConnectionError(
   314  				ws.RejectionStatus(403),
   315  				ws.RejectionHeader(ws.HandshakeHeaderString(
   316  					"X-Want-Host: github.com\r\n",
   317  				)),
   318  			)
   319  		},
   320  		OnHeader: func(key, value []byte) error {
   321  			if string(key) != "Cookie" {
   322  				return nil
   323  			}
   324  			ok := httphead.ScanCookie(value, func(key, value []byte) bool {
   325  				// Check session here or do some other stuff with cookies.
   326  				// Maybe copy some values for future use.
   327  				return true
   328  			})
   329  			if ok {
   330  				return nil
   331  			}
   332  			return ws.RejectConnectionError(
   333  				ws.RejectionReason("bad cookie"),
   334  				ws.RejectionStatus(400),
   335  			)
   336  		},
   337  		OnBeforeUpgrade: func() (ws.HandshakeHeader, error) {
   338  			return header, nil
   339  		},
   340  	}
   341  	for {
   342  		conn, err := ln.Accept()
   343  		if err != nil {
   344  			log.Fatal(err)
   345  		}
   346  		_, err = u.Upgrade(conn)
   347  		if err != nil {
   348  			log.Printf("upgrade error: %s", err)
   349  		}
   350  	}
   351  }
   352  ```
   353  
   354  
   355  
   356  [rfc-url]: https://tools.ietf.org/html/rfc6455
   357  [godoc-image]: https://godoc.org/github.com/ezoic/ws?status.svg
   358  [godoc-url]: https://godoc.org/github.com/ezoic/ws
   359  [travis-image]: https://travis-ci.org/ezoic/ws.svg?branch=master
   360  [travis-url]: https://travis-ci.org/ezoic/ws