Let's learn the WebSocket protocol with a simple implementation of echo WebSocket server and client in Go.
WebSocket
WebSocket is a mechanism for low-cost, full-duplex communication on Web, which protocol was standardized as RFC 6455.
The following diagram, quoted by Wikipedia, describe a communication using WebSocket between client and server.
Let's see how to implement the above design, especially handshaking, using the code which works.
WebSocket in Go
WebSocket server and client can be implemented in various programming languages, but in this article, I use Go language because it is simple and easy to read.
In Go project, gorilla/websocket is widely used to implement WebSocket, and many samples using this library are available on the internet.
I'll explain what gorilla/websocket does in its internal implementation while reading RFC 6455.
WebSocket server
First, let's learn the WebSocket protocol with server implementation. The sample code is available on GitHub repository. I'll explain it line by line.
package main
import (
"fmt"
"log"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{}
func main() {
p := 12345
log.Printf("Starting websocket echo server on port %d", p)
http.HandleFunc("/", echo)
if err := http.ListenAndServe(fmt.Sprintf(":%d", p), nil); err != nil {
log.Panicf("Error while starting to listen: %#v", err)
}
}
func echo(w http.ResponseWriter, r *http.Request) {
// Start handshaking
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("Upgrading error: %#v\n", err)
return
}
defer c.Close()
// End
for {
mt, message, err := c.ReadMessage()
if err != nil {
log.Printf("Reading error: %#v\n", err)
break
}
log.Printf("recv: message %q", message)
if err := c.WriteMessage(mt, message); err != nil {
log.Printf("Writing error: %#v\n", err)
break
}
}
}
This article focuses on the handshake on the WebSocket protocol from WebSocket server perspective.
"Upgrade" an HTTP to a WebSocket
Here is a first line, excluding the import statement.
var upgrader = websocket.Upgrader{}
Upgrader
struct specifies parameters for upgrading an HTTP connection to a WebSocket connection. The naming itself, "Upgrader", is also meaningful.
WebSocket is designed to work over HTTP. To achieve compatibility with HTTP, the WebSocket handshake uses the HTTP Upgrade header in order to change from the HTTP protocol to the WebSocket protocol.
The Upgrade header can be used to upgrade an already established client/server connection to a different protocol. To use Upgrade
header, Connection: update
must be set.
RFC 6455 - 1.2 Protocol Overview illustrates an example of WebSocket handshake.
From client:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Then server will respond as follow:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
Fields of Upgrader struct is as follow:
type Upgrader struct {
HandshakeTimeout time.Duration
ReadBufferSize, WriteBufferSize int
Subprotocols []string
Error func(w http.ResponseWriter, r *http.Request, status int, reason error)
CheckOrigin func(r *http.Request) bool
EnableCompression bool
}
Important fields to understand WebSocket protocol are HandshakeTimeout
and Subprotocols
.
Handshake
HandshakeTimeout
specifies the duration for the handshake to complete.
The first step to exchange message bidirectionally is handshake where opens a new connection between client and server.
Subprotocols
Subprotocols is like a custom XML schema or doctype declaration.
Sec-WebSocket-Protocol: soap, wamp
For example, if you're using a subprotocol json
, all data is passed as JSON (of course, you needs extra code on the server to parse JSON data).
As a WebSocket server, it specifies the server's supported protocols. Therefore, when the server specifies supported protocol is only soap
, server will reject client's handshake with other subprotocols such as json.
Wait handshake from client
Here is a http handler which waits HTTP requests to URI path /
on port 12345.
func main() {
http.HandleFunc("/", echo)
if err := http.ListenAndServe(fmt.Sprintf(":%d", 12345), nil); err != nil {
log.Panicf("Error while starting to listen: %#v", err)
}
}
func echo(w http.ResponseWriter, r *http.Request) {
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("Upgrading error: %#v\n", err)
return
}
defer c.Close()
// ...(omit)
}
echo
function first performs WebSocket handshake procedure in response to a request from client. The detailed implementation is in Upgrader.Upgrade function.
c, err := upgrader.Upgrade(w, r, nil)
Upgrade
upgrades the HTTP server connection to the WebSocket protocol.
Handshake request validation
Upgrade
function begins with gorilla/websocket codes which validate client HTTP request.
if !tokenListContainsValue(r.Header, "Connection", "upgrade") {
return u.returnError(w, r, http.StatusBadRequest, badHandshake+"'upgrade' token not found in 'Connection' header")
}
if !tokenListContainsValue(r.Header, "Upgrade", "websocket") {
return u.returnError(w, r, http.StatusBadRequest, badHandshake+"'websocket' token not found in 'Upgrade' header")
}
if r.Method != "GET" {
return u.returnError(w, r, http.StatusMethodNotAllowed, badHandshake+"request method is not GET")
}
if !tokenListContainsValue(r.Header, "Sec-Websocket-Version", "13") {
return u.returnError(w, r, http.StatusBadRequest, "websocket: unsupported version: 13 not found in 'Sec-Websocket-Version' header")
}
The specification of handshake is as follow:
- The request MUST contain a Connection header field whose value MUST include the
upgrade
token.- A
Connection
header is often set tokeep-alive
orclose
in normal HTTP communication.
- A
- The request MUST contain a Upgrade header field whose value MUST include the
websocket
keyword.- A
Upgrade
header is used to upgrade an already established client/server connection to a different protocol.
- A
- The method of the request MUST be GET as described on RFC 6455 - page 16
-
Sec-Websocket-Version
is the WebSocket protocol version which is supported by server. Available versions are listed in IANA WebSocket Version Number Registry. RFC 6415 is registered as Version 13, so basically, only 13 should be supported.
Sec-Websocket-Key
There is also gorilla/websocket codes on the way in Upgrade
function.
challengeKey := r.Header.Get("Sec-Websocket-Key")
if challengeKey == "" {
return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'Sec-WebSocket-Key' header is missing or blank")
}
Sec-Websocket-Key
is used mainly for two purposes according to RFC 6455 and the discussion on stack overflow.
- Ensure that server understands WebSocket protocol
- Prevent clients accidentally requesting WebSocket upgrade not expecting it
See How decided a value set in Sec-WebSocket-Key/Accept header for more information.
Return HTTP status code 101 (Switching Protocols)
Upgrade
function ends with gorilla/websocket codes which write HTTP headers.
p = append(p, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "...)
p = append(p, computeAcceptKey(challengeKey)...)
p = append(p, "\r\n"...)
The HTTP 101 Switching Protocols response code indicates the protocol the server is switching to as requested by a client. This code includes in this response an Upgrade
response header field whose value is websocket
.
When the client receives this response, it understands that the communication protocol has been upgraded to WebSocket, and the first step, handshake, is completed.
Conclusion
This article explains WebSocket handshake protocol using gorilla/websocket server implementation.
Additional articles will be published on the WebSocket protocol from the client perspective and data frame processing when exchanging messages and etc.
-
Deep dive into WebSocket opening handshake protocol with Go
- It explains the WebSocket handshaking from the client perspective.
- How WebSocket protocol designs bidirectional messaging and implements in Go
Top comments (0)