「Develop fun!」を体現する Works Human Intelligence Advent Calendar 2020の6日目の記事になります。
2枚目の「Develop fun!」を体現する Works Human Intelligence #2 Advent Calendar 2020もあるので、そちらもお楽しみください!
はじめに
内容
この記事では
- WebSocketとは
- WebSocket通信の確立
- gorilla/websocketでどのようにWebSocketを確立しているか
について書いています。
コードはGoですが、 WebSocketとは
を中心に書いています。
きっかけ
Goのweb toolkitでgorilla/websocketというものがあります。🦍
gorilla/websocket
の中にある Upgrader.Upgrade
メソッドがHTTP通信からWebSocket通信に更新してくれる便利なものでした。
実装例としてはこのような形(Go言語によるWebアプリケーション開発より)
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( ))
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で定義されています。
WebSocketを使うと、
誰かがメッセージを送るとみんなのチャット画面に即座に表示される
みたいなことを実現できます。
WebSocketのメリット
以下の特徴が挙げられます。
- コネクションを確立した後は、サーバーとクライアントどちらからでも通信が可能
- コネクションの確立が一度でいいので、通信のコストが低い
URLスキーム
URIスキームとしては
- ws://~~
- wss://~~
の2種類があります。
なぜ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 > 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 > 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
の型を返しているこのコードでは、以下の2つを指定することができます。
- ReadBufferSize
- WriteBufferSize
それぞれ読み込みと書き込みのbyteのバッファーサイズを決めることができ、
0
と指定した場合は HTTPサーバーのよって作成されたバッファーサイズの 4096
がデフォルトで設定されます。
Package 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()
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通信の仕組み
で記載したクライアントからのヘッダーをチェックし、ハンドシェイクを返します。
大きく分けて以下の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のハンドシェイクを実装できるのはありがたいですね。
#参考
参考にさせて頂きありがとうございました。