github.com/simonmittag/ws@v1.1.0-rc.5.0.20210419231947-82b846128245/README.md (about) 1 # ws 2 3 [![GoDoc][godoc-image]][godoc-url] 4 [![CI][ci-badge]][ci-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/gobwas/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/simonmittag/ws" 56 "github.com/simonmittag/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/simonmittag/ws" 92 "github.com/simonmittag/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/simonmittag/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/simonmittag/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/gobwas/httphead" 294 "github.com/simonmittag/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 # Compression 355 356 There is a `ws/wsflate` package to support [Permessage-Deflate Compression 357 Extension][rfc-pmce]. 358 359 It provides minimalistic I/O wrappers to be used in conjunction with any 360 deflate implementation (for example, the standard library's 361 [compress/flate][compress/flate]. 362 363 It is also compatible with `wsutil`'s reader and writer by providing 364 `wsflate.MessageState` type, which implements `wsutil.SendExtension` and 365 `wsutil.RecvExtension` interfaces. 366 367 ```go 368 package main 369 370 import ( 371 "bytes" 372 "log" 373 "net" 374 375 "github.com/simonmittag/ws" 376 "github.com/simonmittag/ws/wsflate" 377 ) 378 379 func main() { 380 ln, err := net.Listen("tcp", "localhost:8080") 381 if err != nil { 382 // handle error 383 } 384 e := wsflate.Extension{ 385 // We are using default parameters here since we use 386 // wsflate.{Compress,Decompress}Frame helpers below in the code. 387 // This assumes that we use standard compress/flate package as flate 388 // implementation. 389 Parameters: wsflate.DefaultParameters, 390 } 391 u := ws.Upgrader{ 392 Negotiate: e.Negotiate, 393 } 394 for { 395 conn, err := ln.Accept() 396 if err != nil { 397 log.Fatal(err) 398 } 399 400 // Reset extension after previous upgrades. 401 e.Reset() 402 403 _, err = u.Upgrade(conn) 404 if err != nil { 405 log.Printf("upgrade error: %s", err) 406 continue 407 } 408 if _, ok := e.Accepted(); !ok { 409 log.Printf("didn't negotiate compression for %s", conn.RemoteAddr()) 410 conn.Close() 411 continue 412 } 413 414 go func() { 415 defer conn.Close() 416 for { 417 frame, err := ws.ReadFrame(conn) 418 if err != nil { 419 // Handle error. 420 return 421 } 422 423 frame = ws.UnmaskFrameInPlace(frame) 424 425 if wsflate.IsCompressed(frame.Header) { 426 // Note that even after successful negotiation of 427 // compression extension, both sides are able to send 428 // non-compressed messages. 429 frame, err = wsflate.DecompressFrame(frame) 430 if err != nil { 431 // Handle error. 432 return 433 } 434 } 435 436 // Do something with frame... 437 438 ack := ws.NewTextFrame([]byte("this is an acknowledgement")) 439 440 // Compress response unconditionally. 441 ack, err = wsflate.CompressFrame(ack) 442 if err != nil { 443 // Handle error. 444 return 445 } 446 if err = ws.WriteFrame(conn, ack); err != nil { 447 // Handle error. 448 return 449 } 450 } 451 }() 452 } 453 } 454 ``` 455 456 457 [rfc-url]: https://tools.ietf.org/html/rfc6455 458 [rfc-pmce]: https://tools.ietf.org/html/rfc7692#section-7 459 [godoc-image]: https://godoc.org/github.com/gobwas/ws?status.svg 460 [godoc-url]: https://godoc.org/github.com/gobwas/ws 461 [compress/flate]: https://golang.org/pkg/compress/flate/ 462 [ci-badge]: https://github.com/gobwas/ws/workflows/CI/badge.svg 463 [ci-url]: https://github.com/gobwas/ws/actions?query=workflow%3ACI