WebSocket メモ

## WebSocket とは

- クライアントとリモートホストの間で双方向通信を可能とするプロトコル。
- セキュリティモデルはウェブブラウザでよく用いられるオリジンをベースとしたモデル。
- `XMLHttpRequest` やロングポーリングに頼らずにサーバとの双方向通信を必要とするブラウザアプリケーションのためのメカニズムを提供する。
- クライアント・サーバ間の双方向のトラフィックのためにTCPコネクションを一つだけ張る。

## プロトコルの概要

プロトコルは**ハンドシェイク**と**データ転送**の2つの部分からなる。

### ハンドシェイク

#### クライアント -> サーバ

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

#### サーバ -> クライアント

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

ハンドシェイクが成功すると、クライアントとサーバは1つ以上のフレームからなる**メッセージ**をお互いに送出し合うことになる。

## Node.js でシンプルな WebSocket 通信

Node.js では、[ws](https://www.npmjs.com/package/ws) というWebSocketライブラリを用いてシンプルな WebSocket 通信ができる（[Socket.IO](https://socket.io/) でも可能）。

### サーバ側のコード

```javascript
const WebSocket = require('ws')

const server = new WebSocket.Server({ port: 3000 })

server.on('connection', ws => {
  ws.send('connected!')

  ws.on('message', message => {
    console.log('received: %s', message)
  })
})
```

### クライアント側のコード

```javascript
const ws = require('ws')

const client = new ws('ws://localhost:3000')

client.on('open', () => {
  client.send('hello')
})

client.on('message', message => {
  console.log(message)
})
```

`node server.js` でWebSocketサーバを立ち上げた後 `node client.js` を実行すると、サーバ側では

> received: hello

と出力され、クライアント側では

> connected!

と出力される。

## WebSocket でチャットアプリを作る

[blessed](https://github.com/chjj/blessed#textbox-from-textarea) というクールな CUI を作れるライブラリを使って、超シンプルなチャットアプリ（もどき）を作ってみる。

### サーバ側

```javascript
const WebSocket = require('ws')

const server = new WebSocket.Server({ port: 3000 })

const sockets = new Map()

const broadcast = message => {
  console.log(message)
  server.clients.forEach(client => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(message)
    }
  })
}

server.on('connection', socket => {
  socket.on('message', message => {

    if (sockets.has(socket)) {
      const name = sockets.get(socket)
      broadcast(`${name}: ${message}`)
    } else {
      sockets.set(socket, message)
      broadcast(`${message} entered the room.`)
    }
  })

  socket.on('close', () => {
    const name = sockets.get(socket)
    if (name) {
      sockets.delete(socket)
      broadcast(`${name} left the room.`)
    }
  })
})
```

### クライアント側

```javascript
const ws = require('ws')
const blessed = require('blessed')

const screen = blessed.screen({ smartCSR: true })
screen.title = 'chat'

const chatList = blessed.list({ border: { type: 'line' } })

const textbox = blessed.textbox({
  height: '10%',
  bottom: 0,
  input: true,
  inputOnFocus: true,
  border: {
    type: 'line'
  }
})

const client = new ws('ws://localhost:3000')

client.on('open', async () => {
  textbox.setText('(Please enter your name)')
  textbox.focus()
  screen.render()
})

client.on('message', message => {
  chatList.insertLine(0, message)
  screen.render()
})

textbox.key(['enter'], () => {
  client.send(textbox.getText())
  textbox.clearValue()
  chatList.render()
  textbox.focus()
})


const EXIT_COMMANDS = ['escape', 'C-c']
const exit = () => process.exit(0)
screen.key(EXIT_COMMANDS, exit)
chatList.key(EXIT_COMMANDS, exit)
textbox.key(EXIT_COMMANDS, exit)


screen.append(chatList)
screen.append(textbox)
screen.render()
```

### 実際の動作

![chat.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/519262/2f87ec14-e659-8a5c-f668-fd0033a4031c.gif)

それっぽくは動く。

## 所感

- blessed の API がよくわからない
- ブラウザを UI としたチャットアプリを作ってみたい
- 随時更新する

## 出典

- [RFC 6455 - The WebSocket Protocol](https://tools.ietf.org/html/rfc6455)
- [HTML Standard: 9.3 Web sockets](https://html.spec.whatwg.org/multipage/web-sockets.html#network)
- [websockets/ws](https://github.com/websockets/ws)
- [Socket.IO](https://socket.io/)
- [chjj/blessed](https://github.com/chjj/blessed#textbox-from-textarea)