Delphi 用 WebSocket 作った

# 結論を急ぎたい人は↓へ
**WebSocket のリポジトリ**
https://bitbucket.org/freeonterminate/websocket/

# Discord のボット作りたい

「Discord のボット作りたい！」その思いから僕の旅は始まった…

# Discord Gateway API は WebSocket を使う

Delphi にも WebSocket ぐらい作ってる人いるっしょ！見たことあるし！楽勝！楽勝！
あったあった

**DelphiWebsockets**
https://github.com/andremussche/DelphiWebsockets

ん？何か書いてあるぞ…

```:原文
Not active anymore
Unfortunately I don't have time to support this project anymore.
Also the websocket protocol has changed in the meantime, so it won't work with browser and other modern implementations.
```

```:翻訳
もうアクティブではありません
残念ながら、私はもうこのプロジェクトをサポートする時間がありません。
また、その間にwebsocketプロトコルも変更されたため、ブラウザやその他の最近の実装では機能しません。
```

ほげえ！
あ、でも代替手段が書いてある！

```
Please take a look at the free (but closed) 3rd party component:

http://www.esegece.com/websockets/download
http://www.esegece.com/download/sgcWebSockets_free.zip
```` 

よっしゃー
アクセスや！

![image.png](https://qiita-image-store.s3.amazonaws.com/0/12977/cfc29a3d-9381-ef52-d8fb-3008f58ac532.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/12977/5a7e4e2f-ad8e-c1fc-674f-8d82623aa190.png)

デスヨネー…
個人的使用であれば安いし（€199.00 ⇒ 約25000円）普通に買うんだけど…Discord Bot のソースはオープンにしたいんだよなあ。
これじゃあなあ…

# WebSocket を実装する

こうなったら自分で実装するしかあるまい…
幸い RFC 6455 は日本語訳がある！

**RFC 6455, The WebSocket Protocol (非公式日本語訳)**
https://triple-underscore.github.io/RFC6455-ja.html

WebSocket の詳細については上記の文書に記載されているので詳しくは書きませんが、実装時についての注意点をいくつか。

### 1. IdTCPClient を使う
WebSocket は HTTP を拡張した物ですが、ネゴシエーションを HTTP ヘッダで行います。
そのため通常の [THttpClient](http://docwiki.embarcadero.com/Libraries/Rio/ja/System.Net.HttpClient.THTTPClient) は使えず自分で HTTP を偽装しなくてはなりません。
そこで TCP Socket の出番ですが、Delphi RTL で用意されている [TSocket](http://docwiki.embarcadero.com/Libraries/Rio/ja/System.Net.Socket.TSocket) は SSL 対応されていないため、使えません。
そこで、[Indy](https://www.indyproject.org/) の TIdTCPClient を使います。
ただ、TIdTCPClient で SSL 通信するためには以下のライブラリが必要です。

| OS      | ライブラリ１    | ライブラリ２ |
|---------|-----------------|--------------|
| Windows | libeay32.dll    | ssleay32.dll |
| macOS   | libcrypto.dylib | libssl.dylib |
| iOS     | libcrypto.a     | libssl.a     |
| Android | libcrypto.so    | libssl.so    |
| Linux   | libcrypto.so    | libssl.so    |
※iOS は静的リンクの必要あり

これらの必要な[ライブラリはこのリンクから落とせます](https://indy.fulgan.com/SSL/)。
ただ、将来的に RTL の TSocket が SSL 対応されれば、そちらを使いたい（DLL が要らないため）ので、TIdTCPClient をラップして使っています。

### 2. ws, wss を http に偽装する
WebSocket のスキーム ```ws```, ```wss``` は本質的に ```http```, ```https```です。
上記の TIdTCPClient で接続する時は ```ws://``` や ```wss://``` を ```http://```, ```https://``` に置換します。

### 3. 終了時に Close コードを送出
WebSocket は終了時に Close コード ```8``` をコネクション相手に送る必要がありますが、これがまた難しい。
FormDestory ⇒ WebSocket.DisposeOf ⇒ SocketThread.Terminate ⇒ ```8``` の送出、と手順を踏みますが、Socket の都合上 Thread から ```8``` を送出するようになっています。
それによって、```8``` 送出より先に WebSocket のインスタンスだったり Form のインスタンスが削除されてしまいエラーになります。
この問題を解決するため終了処理がえらいめんどくさい事になっています。

### 4. コンソールでは TThead.Queue は使えない
GUI アプリで言うところのメインスレッドが無いため [TThread.Queue](http://docwiki.embarcadero.com/Libraries/Rio/ja/System.Classes.TThread.Queue) は使えません。
そのため、[CreateAnonymousThread](http://docwiki.embarcadero.com/Libraries/Rio/ja/System.Classes.TThread.CreateAnonymousThread) を使って遅延処理を実現しています。

### 5.手抜きポイント
複数フレーム対応は省略しています。
WebSocket は「フレーム」という単位で通信するのですが、送るデータの量によっては複数のフレームに分けて転送します。
分ける分けないは自由ですがフレームで保持できるデータ量を超えた場合は必ず分けないといけません。
…ですが、その分ける値というのは Unsigned Int64 で表せる ```18446744073709551615```= ```15 [EiB]``` です。
これ超える事ある！？！？ということで、送信時の複数フレームに対する対応はしていません。

他にも 126 文字以上になった時の文字列長データの並び順とかいくつかはまったポイントがありました。

# WebSocket の使い方

そんなこんなでできあがった WebSocket の簡単な使い方を紹介します。

```pascal:生成と破棄
// WebSocket の作成
procedure TfrmMain.FormCreate(Sender: TObject);
begin
  FWebSocket := TWebSocket.Create(Self);
  FWebSocket.OnError := WebSocketError; // エラーイベント
  FWebSocket.OnText := WebSocketText;   // 文字列受信イベント
end;

// WebSocket の破棄
procedure TfrmMain.FormDestroy(Sender: TObject);
begin
  FWebSocket.DisposeOf;
end;
```


```pascal:接続と通信
// 接続
procedure TfrmMain.Button1Click(Sender: TObject);
begin
  FWebSocket.Connect('wss://echo.websocket.org');
end;

// 送信
procedure TfrmMain.Button2Click(Sender: TObject);
begin
  FWebSocket.Send('Hello, WebSocket !');
end;

// 受信
procedure TfrmMain.WebSocketText(Sender: TObject; const iText: String);
begin
  Memo1.Lines.Add(iText);
end;
````

```pascal:エラー処理
// エラー処理（ログに出すだけ…）
procedure TfrmMain.WebSocketError(Sender: TObject; const iMessage: String);
begin
  Memo1.Lines.Add('ERROR: ' + iMessage);
end;
```

# まとめ
一度は自分で車輪を作ってみるのもいいよ！