WebSocketとは ~gorilla / websocketでみてみる~

[「Develop fun!」を体現する Works Human Intelligence Advent Calendar 2020](https://qiita.com/advent-calendar/2020/whi)の6日目の記事になります。
２枚目の[「Develop fun!」を体現する Works Human Intelligence #2 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/whi-2)もあるので、そちらもお楽しみください！

# はじめに

## 内容
この記事では

- WebSocketとは
- WebSocket通信の確立
- gorilla/websocketでどのようにWebSocketを確立しているか

について書いています。

コードはGoですが、 `WebSocketとは` を中心に書いています。

## きっかけ
Goのweb toolkitで[gorilla/websocket](https://github.com/gorilla/websocket)というものがあります。🦍
`gorilla/websocket` の中にある `Upgrader.Upgrade` メソッドがHTTP通信からWebSocket通信に更新してくれる便利なものでした。

実装例としてはこのような形（[Go言語によるWebアプリケーション開発](https://github.com/oreilly-japan/go-programming-blueprints)より）

```
import "github.com/gorilla/websocket"

const (
	socketBufferSize  = 1024
	messageBufferSize = 256
)

var upgrader = &websocket.Upgrader{ReadBufferSize: socketBufferSize, WriteBufferSize: socketBufferSize}

func (r *room) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	socket, err := upgrader.Upgrade(w, req, nil)
}
```

「とても便利！」と思い、 `Upgrade` メソッドの中を見てみると（[server.go > Upgrade( )](https://github.com/gorilla/websocket/blob/master/server.go#L123))

```
func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error) {
	const badHandshake = "websocket: the client is not using the websocket protocol: "
	if !tokenListContainsValue(r.Header, "Connection", "upgrade") {
		return u.returnError(w, r, http.StatusBadRequest, badHandshake+"'upgrade' token not found in 'Connection' header")
	}
	.
	.
	.
	p = append(p, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "...)
	p = append(p, computeAcceptKey(challengeKey)...)
	.
	.
	.
```

と何かヘッダーをif文でチェックしていて、レスポンスらしきものを書き込んでいる。。。
が、WebSocketの仕組みをわからず、メソッド内部で何をしているかわからなかったので調べました。

# WebSocket

## WebSocketとは

webアプリケーションにおいて、
サーバーとクライアントの双方向通信を可能とするアプリケーション層の `プロトコル（通信規格）` 。

ネットワーク上でTCP接続がHTTPを利用しているように見えるように、HTTPのリクエストの形式をとります。

[RFC6455](https://www.rfc-editor.org/rfc/rfc6455.txt)で定義されています。

WebSocketを使うと、
`誰かがメッセージを送るとみんなのチャット画面に即座に表示される` みたいなことを実現できます。

## WebSocketのメリット
以下の特徴が挙げられます。

- コネクションを確立した後は、サーバーとクライアントどちらからでも通信が可能
- コネクションの確立が一度でいいので、通信のコストが低い

## URLスキーム
URIスキームとしては

- ws://~~
- wss://~~

の２種類があります。

## なぜWebSocketが必要？？
WebSocket以前ではHTTPでクライアントからサーバーへの一方向でよかったのですが、
リアルタイムで多くのクライアントが最新の情報を取得したくなってきました。（SNSのチャットなど）

### HTTPで打開策を探した
以下のような手法で打開策を探していたが限界がありました。

- 一定時間ごとにクライアントからサーバーへ新着がないかリクエストをする
- Comet（※）という手法の活用

※サーバーからクライアントに送信はできるが、

- 双方間通信ごとにTCPのハンドシェイク手続きを行う必要がある
- 長時間のコネクションの占有

などが問題としてありました。

## WebSocket通信の仕組み
WebSocketはハンドシェイクで通信を確立し、双方向通信を行います。

### 1.ハンドシェイク
`ハンドシェイク` とは、目的の情報のやり取り（通信）を行う前に、通信に必要なパラメータ等を決めるものです。
クライアントとサーバーからそれぞれハンドシェイクを送ります。

ハンドシェイクが成立したら見事WebSocket通信の確立です🤝

#### クライアントからのハンドシェイク
クライアントから以下のようなハンドシェイクを送ります。

```
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
```

([RFC6455](https://www.rfc-editor.org/rfc/rfc6455.txt) > 1.2.  Protocol Overviewより)

以下のヘッダーが必須で必要です。

- Host
- Upgrade
- Connection
- Sec-WebSocket-Key
- Origin
- Sec-WebSocket-Version

特徴的なパラメータは以下の通りです。

- Upgrade：HTTPの接続からwebsocketプロトコルにアップグレードする。
- Connection：この `upgrade` ヘッダーがその接続に限定的なもので、他の接続経由でプロキシが通信してはいけない。
- Sec-WebSocket-Version：サーバーに対してWebSocketのバージョンに対応できているか確認する。最新の13を指定。対応できない場合、WebSocket通信を中断する。
- Sec-Websocket-Key：WebSocketを行なっているクライアントと通信していることを証明するキー。

#### サーバーからのハンドシェイク

サーバーからはクライアントからのメッセージを受けて以下のようなハンドシェイクを送ります。

```
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
```

([RFC6455](https://www.rfc-editor.org/rfc/rfc6455.txt) > 1.2.  Protocol Overviewより)

HTTPのレスポンスと同じ形を取り、ステータスとしては `101` が返ってきます。
`Upgrade` と `Connection`はクライアントからのリクエストと同様のパラメータであるため説明は省略します。
`Sec-WebSocket-Accept` ヘッダーはサーバーでコネクションを承諾したかを示し、
リクエストヘッダーの `Sec-WebSocket-Key` を元に作成されます。

### 2.双方向通信
TCP上で `フレーム` という単位でデータを転送します。
`Payload Data` にデータを格納します。
なお、`Payload Data`に加えて2byte~14byteの情報が追加されるだけであるため、低コストな通信が可能です。

# gorilla/websocket
## 改めてgorilla/websocketとは
Go言語でWebSocketプロトコルを利用する際に使われるweb toolkitです。
https://github.com/gorilla/websocket

## websocket.Upgrader.upgraderについて
webSocket通信を行うためにハンドシェイクををこなっているのが、冒頭であった `Upgrader.upgrader` です。

使い方としては以下のような形。（再掲）

```
import "github.com/gorilla/websocket"

const (
	socketBufferSize  = 1024
	messageBufferSize = 256
)

var upgrader = &websocket.Upgrader{ReadBufferSize: socketBufferSize, WriteBufferSize: socketBufferSize}

func (r *room) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	socket, err := upgrader.Upgrade(w, req, nil)
}
```

このコードを基に今度こそ　`Upgrader.upgrader` を見ていきます。

### &websocket.Upgrader{...}について
まず`*websocket.Upgrader`の型を返しているこのコードでは、以下の２つを指定することができます。

- ReadBufferSize
- WriteBufferSize

それぞれ読み込みと書き込みのbyteのバッファーサイズを決めることができ、
`0` と指定した場合は HTTPサーバーのよって作成されたバッファーサイズの `4096` がデフォルトで設定されます。

[Package websocket](https://pkg.go.dev/github.com/gorilla/websocket)の ***buffers*** にある通り、
>Applications should tune the buffer sizes to balance memory use and performance. Increasing the buffer size uses more memory, but can reduce the number of system calls to read or write the network. In the case of writing, increasing the buffer size can reduce the number of frame headers written to the network.

バッファーサイズを大きくすれば、メモリはより多く使用されますが、
ネットワークでのシステムコール数を下げることができ、データ転送のフレームヘッダー数を減らすことができます。


### upgrader.Upgrade(w, req, nil)について
そして、このコードでHTTP通信をWebSocket通信にupgradeしています。

▼ソース箇所
[websocketパッケージ > server.go > Upgrade()](https://github.com/gorilla/websocket/blob/master/server.go#L123)

```
func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error) {
	const badHandshake = "websocket: the client is not using the websocket protocol: "
.....
```

#### ソースの流れ
`WebSocket通信の仕組み` で記載したクライアントからのヘッダーをチェックし、ハンドシェイクを返します。
大きく分けて以下の２つをしています。

1. リクエストヘッダーの値を確認
2. レスポンスを返す

##### リクエストヘッダーの値を確認
以下のリクエストヘッダーの値を確認しています。

- Connection：upgrade
- Upgrade：websocket
- Method：GET
- Sec-Websocket-Version：13
- Sec-Websocket-Extensions：含まれていないか（サポートしていないようです）
- Origin リクエストヘッダー：許可されているか
- Sec-Websocket-Key：含まれているか

コードを見てみると

```
	.
	.
	.
	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")
	}

	if _, ok := responseHeader["Sec-Websocket-Extensions"]; ok {
		return u.returnError(w, r, http.StatusInternalServerError, "websocket: application specific 'Sec-WebSocket-Extensions' headers are unsupported")
	}

	checkOrigin := u.CheckOrigin
	if checkOrigin == nil {
		checkOrigin = checkSameOrigin
	}
	if !checkOrigin(r) {
		return u.returnError(w, r, http.StatusForbidden, "websocket: request origin not allowed by Upgrader.CheckOrigin")
	}

	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")
	}
	.
	.
	.
```

と、上記で提示したそれぞれのリクエストヘッダーをチェックしエラーハンドリングをしています。

##### サーバーからのハンドシェイク
以下の内容を返します。

- HTTP Status 101
- Upgrade: websocket
- Connection: Upgrade
- Sec-WebSocket-Accept: リクエストヘッダーの `Sec-WebSocket-Key` を元に作成された値。

コードを見てみると

```
	.
	.
	.
	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"...)
	if c.subprotocol != "" {
		p = append(p, "Sec-WebSocket-Protocol: "...)
		p = append(p, c.subprotocol...)
		p = append(p, "\r\n"...)
	}
	if compress {
		p = append(p, "Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n"...)
	}
	.
	.
	.
```

とハンドシェイクのレスポンスを作成しています。

見事これが返れば、ハンドシェイクの成立です。🤝

# 終わりに
たった1行のコードでwebSocketのハンドシェイクを実装できるのはありがたいですね。

#参考
参考にさせて頂きありがとうございました。

- [RFC6455](https://www.rfc-editor.org/rfc/rfc6455.txt)
- [Gorilla WebSocket](https://github.com/gorilla/websocket)
- [go-programming-blueprints](https://github.com/oreilly-japan/go-programming-blueprints)
- [WebSocket](https://ja.wikipedia.org/wiki/WebSocket)
- [今さら聞けないWebSocket ~WebSocketとは~](https://qiita.com/chihiro/items/9d280704c6eff8603389)
- [WebSocketについて調べてみた。](https://qiita.com/south37/items/6f92d4268fe676347160)
- [Windows 8 と WebSocket プロトコル](https://docs.microsoft.com/ja-jp/archive/msdn-magazine/2012/december/windows-8-networking-windows-8-and-the-websocket-protocol)
- [WebSocket / WebRTCの技術紹介](https://www.slideshare.net/mawarimichi/websocketwebrtc)