【C#】C#でWebSocket、ブラウザーとのやり取り。

# 前置き

仕事で少しWebSocketを使ったので備忘録代わりに。
C#でWebSocketのサーバー側、ブラウザー側をクライアントとして、通信ネタではよくある「チャット的なやつ」を作成しました。

「C#」「WebSocket」で検索すれば既に先人たちの記載はありますが（感謝）、Nugetで何かいい塩梅のものを取ってきたり、IISを絡めたり、といったことなし、素の.Netの範疇「とりあえず動くものを作った」が今回の内容になります。

![WebSocket説明2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138460/2aed02e6-3dff-2ac6-63b7-3542b5771800.gif)

GitHubにポチポチで動かせるお試しセット（exe+html)も置いてあります。
https://github.com/fu-foo/WebSocketCSharpSimpleSample/tree/main/Trial

# 環境

- （WebSocketサーバー側）.Net 4.8 + C#、コンソールアプリ。
開発環境:Microsoft Visual Studio 2022 (64 ビット) 
大したことはしていないので.Net4.8使える環境なら大丈夫でしょう。

- （WebSocketクライアント側）手打ちHTMLと素のJavaScript。
確認:Chrome:バージョン: 109.0.5414.120（Official Build） （64 ビット）で確認
こちらも大したことはしていないので、WebSocket未サポートのIE9以前とかでなければ大丈夫なのではと。

# 前提

- 極力WebSocket部分に注力、ということでコンソールアプリかつ例外処理等入っていません。（通信切れのような容易に考えられる程度のケースのみ配慮）
参考にする場合はエラー処理もろもろ配慮を各自お願いします。
- 私の人生でWeb界隈にほとんど時間を費やしてないこともあり、WebSocketクライアント側は「まぁこうやれば動くんだろうな。。。」程度なので、もしかしたらすごくイケてないかもしれません。
- ポート8080を使っていますが、ファイヤーウォールなど環境的な制限を受ける可能性があります。
- .Netに用意されているWebSocketのクラスが非同期仕様なので、async/await部分を理解してないと読みにくいかもしれないです。できればWebSocket本筋以外の論点は織り込みたくなかったのですが避けられないので事前にご確認を。
- 参考、ご使用はご自由にですが、何があっても責任はとれません。悪しからず。 

# 仕様

- WebSocketサーバー側
    - あくまでも「1対1」のやり取りのみ想定。（複数ブラウザーからの接続は考慮せず。）
    - コンソールアプリ（極力必要なWebSocketのところに注力して説明したかったので）
    - C# + .Net4.8（.Net4.8より前でも動くと思いますが、いつからWebSocketをサポートしたか調べてないので.Net4.8前提)
    - localhostのポート8080で受付。（直書きなので変更したい場合はご自身で。）
    - CTRL+Cで停止。
    - 1024バイトまで受信可能、受信したデータはUTF-8のテキストとしてコンソールに出力。
    - 入力→ENTERでWebSocketクライアント側に入力テキスト送信。（送信データもコンソールに出力。）
 - WebSocketクライアント側
     - HTML+JavaScriptべた書き。
     - HTMLを開いたときにWebSocket接続トライ。（localhostのポート8080へ。直書きなので変更したい場合はご自身で。）
         - 再接続したい時はF5更新してください。
     - 受信したデータを追記表示。（送信データも追記表示。）

# GitHub

まるっと、はこちらをCloneしてください。

https://github.com/fu-foo/WebSocketCSharpSimpleSample

# ソース全景

<details><summary>WebSocketServer:ソースコードを表示（折りたたみ）</summary><div>

``` C#:Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
using System.ComponentModel;

namespace WebSocketServer
{
	internal class Program : IDisposable
	{
		// WebSocket
		private static WebSocket webSocket;

		// Main
		static void Main(string[] args)
		{
			Console.WriteLine("開始:CTRL+Cで停止してください");

			// WebSocket作成＋受信
			var nowait1 = CreateWebSocketServerAndReceive();

			// 送信
			var nowait2 = Send();

			// CTRL+Cを押すまで止めない用
			using (var manualResetEvent = new ManualResetEvent(false))
			{
				manualResetEvent.WaitOne();
			}
		}

		// Ctrl+C で止めるときにお行儀よく切断
		public async void Dispose()
		{
			if (webSocket != null)
			{
				Console.WriteLine("WebSocketサーバーからの切断");
				await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "WebSocketサーバーからの切断", CancellationToken.None);
				webSocket.Dispose();
			}
		}

		// WebSocket接続待ち作成
		static async Task CreateWebSocketServerAndReceive()
		{
			// ポート8080でWebSocket受付
			var httpListener = new HttpListener();
            // ※localhost指定ではlocalhostからのソケットしか受け付けない
            // 全て(+)で受け付ける場合は管理者権限で起動する必要あり
            httpListener.Prefixes.Add("http://localhost:8080/");
			//httpListener.Prefixes.Add("http://+:8080/");
			httpListener.Start();

			Console.WriteLine("WebSocket受付開始");

			while (true)
			{
				var httpListenerContext = await httpListener.GetContextAsync();
				
				// WebSocketではない場合はNG
				if (!httpListenerContext.Request.IsWebSocketRequest)
				{
					Console.WriteLine("WebSocketでの接続ではありません");
					httpListenerContext.Response.StatusCode = 400;	// bad request
					httpListenerContext.Response.Close();
					return;
				}
				// Accept
				var taskHttpListenerContext = await httpListenerContext.AcceptWebSocketAsync(null);

				// 今回は１接続のみ意識、既に接続があれば既存WebSocketいったん破棄。
				if (webSocket != null)
				{
					webSocket.Dispose();
				}
				// WebSocket作成
				webSocket = taskHttpListenerContext.WebSocket;

				Console.WriteLine("WebSocket接続");
				Console.Write(">");

				// 受信
				var nowait = Receive();
			}
		}

		// 受信
		static async Task Receive()
		{
			//情報取得待ちループ
			while (true)
			{
				// WebSocketが切れていれば受信ループはおしまい
				if (webSocket == null || (webSocket != null && webSocket.State != WebSocketState.Open))
				{
					Console.WriteLine("WebSocket接続がありません");
					Console.Write(">");
					return;
				}

				// 受信
				var buffer = new byte[1024];	// とりあえず1024バイト
				var byteArray = new ArraySegment<byte>(buffer);
				var ret = await webSocket.ReceiveAsync(byteArray, CancellationToken.None);

				//エンドポイントCloseの場合、処理を中断
				if (ret.MessageType == WebSocketMessageType.Close)
				{
					Console.WriteLine("クライアントから切断されました");
					Console.Write(">");
					await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "クライアントから切断されました", CancellationToken.None);
					return;
				}

				// 今回は簡易チャットなのでテキスト以外は扱わない
				if (ret.MessageType != WebSocketMessageType.Text)
				{
					Console.WriteLine("テキストデータではありません");
					Console.Write(">");
					await webSocket.CloseAsync(WebSocketCloseStatus.InvalidMessageType, "テキストデータではありません", CancellationToken.None);
					return;
				}

				// 受信内容取得
				int size = ret.Count;
				while (!ret.EndOfMessage)
				{
					if (buffer.Length <= size)
					{
						Console.WriteLine("バッファーオーバー");
						await webSocket.CloseAsync(WebSocketCloseStatus.InvalidPayloadData, "バッファーオーバー", CancellationToken.None);
						return;
					}
					byteArray = new ArraySegment<byte>(buffer, size, buffer.Length - size);
					ret = await webSocket.ReceiveAsync(byteArray, CancellationToken.None);

					size += ret.Count;
				}

				// 受信内容のバイト列を文字列に変換・出力
				var message = Encoding.UTF8.GetString(buffer, 0, size);
				Console.WriteLine($"\r\n[{DateTime.Now.ToString()}][受信]{message}");
				Console.Write(">");
			}
		}

		// 送信
		static async Task Send()
		{
			while (true)
			{
				// 入力待ち
				string line = Console.ReadLine();

				// 接続がなければ入力待ちに戻る
				if (webSocket == null || (webSocket != null && webSocket.State != WebSocketState.Open))
				{
					Console.WriteLine("WebSocket接続がありません");
					Console.Write(">");
					continue;
				}

				// 文字列
				var buffer = Encoding.UTF8.GetBytes(line);
				var byteArray = new ArraySegment<byte>(buffer);

				// 文字列→バイト列に変換して送信
				await webSocket.SendAsync(byteArray, WebSocketMessageType.Text, true, CancellationToken.None);
				Console.WriteLine($"[{DateTime.Now.ToString()}][送信]{line}");
				Console.Write(">");
			}
		}
	}
}

```
</div></details>

<details><summary>WebSocketClient:ソースコードを表示（折りたたみ）</summary><div>

``` HTML:WebSocketClient.html
<html>
	<body>
		<input type="text" name="text" id='textMessage' autocomplete='off'>
		<input type="button" value="送信" onclick="clickMessageSend()">
		<table id="message">
			<tr>
				<th>送受信メッセージ</th>
			</tr>
		</table>
	</body>

<script>

addMessage('再接続はF5更新で行ってください',0);

// 接続
addMessage('接続中...',0);
var connection = new WebSocket('ws://localhost:8080/');

//通信が接続された場合
connection.onopen = function(e) { 
	addMessage('接続しました',0);
}
 
//エラーが発生した場合
connection.onerror = function(error) {
	addMessage('接続できません',0);
}
 
//メッセージを受け取った場合
connection.onmessage = function(e) { 
	addMessage(e.data,1);
}
  
//通信が切断された場合
connection.onclose = function() { 
	addMessage('通信が切断されました',0);
}
// 送信
function clickMessageSend() {
	connection.send(document.getElementById('textMessage').value);
	addMessage(document.getElementById('textMessage').value,2);
}

// 送受信などの出力
function addMessage(message, type) {
	// エレメント作成
	let messagetable = document.getElementById('message');
	let child1 = document.createElement("tr");
	let child2 = document.createElement("td");
	// 日付作成
	var date_obj = new Date();
	let line = '[' + date_obj.toString() + ']';
	// 送受信作成
	if(type == '1') {line += '[→受信]'};
	if(type == '2') {line += '[←送信]'};
	// messagetableに追加
	child1.append(line+ message,child2);
	messagetable.append(child1);
}
	</script>
</html>
```

</div></details>

# ソース解説

## WebSocketサーバー

こんな実装です。細かいところはソースのコメントや出力メッセージで察してもらうこととして・・・。

- Main 
    - エントリーポイント。
    - 以下の処理を非同期で投げます。
        - WebSocket作成 + 受信待ち
        - 入力受付 + 送信
    - 最後にMainが終わってしまわない用の待ち。
- Dispose
    - ブチっと止める以外に終わる方法がないシロモノですが、終わる時にお行儀よくCloseはしてあげたいのでDispose内にCloseを実装。
- CreateWebSocketServerAndReceive
    - 接続待ち受け。
    - 接続できたら受信(Receive)開始
- Receive
    - 受信内容をコンソールに出力。
- Send
    - 入力待ち。
    - 入力内容をWebSocketクライアントに送信。

## WebSocketクライアント

こちらも細かいところはソースのコメントや出力メッセージで察してもらうこととして・・・。

- UI
    - 「送信テキスト入力」「送信ボタン」「送受信データを出力するテーブル」の３つのみ。
- JavaScript
    - とりあえず最初にWebSocket(ws://localhost:8080/)に接続。
    - 受信したら内容を「送受信データを出力するテーブル」に追記。
    - 「送信ボタン」を押したら「送信テキスト入力」の内容をWebSocket接続先に送信＋「送受信データを出力するテーブル」に追記。


# 謝辞

大変参考にさせていただきました。ありがとうございます。

- [C#でWebSocketのサンプルを動かしてみた](https://qiita.com/Zumwalt/items/53797b0156ebbdcdbfb1)
- [WebSocketについて調べてみた。](https://qiita.com/south37/items/6f92d4268fe676347160)