LoginSignup
17
9

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-12-05

「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 が返ってきます。
UpgradeConnectionはクライアントからのリクエストと同様のパラメータであるため説明は省略します。
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 websocketbuffers にある通り、

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つをしています。

  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のハンドシェイクを実装できるのはありがたいですね。

参考

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

17
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
9