C# WebSocket入門:リアルタイム通信の実装方法とエラー対処をサンプルコードで解説

はじめに

C#でリアルタイム通信を実装したい場合、代表的な選択肢のひとつがWebSocketです。通常のHTTP通信では、基本的にクライアントがリクエストを送り、サーバーがレスポンスを返します。一方、WebSocketでは一度接続を確立すると、クライアントとサーバーの間で双方向にメッセージをやり取りできます。

そのため、チャットアプリ、通知機能、リアルタイムダッシュボード、オンラインゲーム、IoTデータの監視など、「サーバー側で発生した変化をすぐに画面へ反映したい」場面でよく使われます。

この記事では、C# WebSocketの基本から、ASP.NET Coreを使ったWebSocketサーバーの作り方、ClientWebSocketを使ったC#クライアントの実装、ブラウザJavaScriptからの接続方法、複数クライアント対応、よくあるエラーの対処法まで、サンプルコード付きで解説します。

1. C#でWebSocketを学ぶ前に知っておきたい基礎知識

1-1. WebSocketとは?HTTP通信との違い

WebSocketは、クライアントとサーバーの間に双方向の通信経路を維持するためのプロトコルです。ASP.NET Coreの公式ドキュメントでも、WebSocketはTCP接続上で双方向の永続的な通信チャネルを実現するプロトコルとして説明されています。

通常のHTTP通信では、次のような流れになります。

クライアント → サーバー:リクエスト
サーバー → クライアント:レスポンス

この通信は、基本的に1回のリクエストと1回のレスポンスで完結します。サーバーからクライアントへ自発的にデータを送ることはできないため、リアルタイム性が必要な場合は、クライアントが定期的にサーバーへ問い合わせるポーリング処理が必要になります。

一方、WebSocketでは最初にHTTPで接続を開始し、その後WebSocket接続へアップグレードします。接続が確立されると、次のように双方から自由にメッセージを送れます。

クライアント ⇄ サーバー

この「接続を維持したまま双方向通信できる」という点が、WebSocketの大きな特徴です。

1-2. WebSocketがリアルタイム通信に向いている理由

WebSocketがリアルタイム通信に向いている理由は、主に次の3つです。

1つ目は、サーバーからクライアントへ即時にメッセージを送れることです。HTTPのようにクライアントからのリクエストを待つ必要がないため、チャットの新着メッセージや通知をすぐに届けられます。

2つ目は、通信のたびにHTTPヘッダーを大量に送る必要がないことです。接続確立後は軽量なフレームでメッセージをやり取りするため、短いメッセージを頻繁に送る用途に向いています。

3つ目は、1つの接続を継続して使えることです。株価、センサー値、ログ、ゲーム状態など、頻繁に更新されるデータをサーバーから継続的に配信できます。

1-3. C#でWebSocketを使う主な用途

C#でWebSocketを使う主な用途には、次のようなものがあります。

チャットアプリでは、あるユーザーが送信したメッセージを、接続中の他のユーザーへ即時に配信できます。管理画面やダッシュボードでは、売上、アクセス数、サーバー負荷、ジョブの進行状況などをリアルタイムに更新できます。

通知機能では、注文完了、承認依頼、アラート、障害検知などを画面へ即時表示できます。オンラインゲームや共同編集ツールでは、ユーザー操作や状態変更を複数クライアント間で同期できます。

C#はサーバーサイド、デスクトップアプリ、Unity、バックグラウンドサービスなど幅広い環境で使えるため、WebSocketとの相性も良いです。

1-4. SignalRとの違い:WebSocketを直接使うべきケース

C#でリアルタイム通信を実装する場合、WebSocketを直接使う方法のほかに、ASP.NET Core SignalRを使う方法もあります。

SignalRはWebSocketを含む複数の通信方式を抽象化したリアルタイム通信ライブラリです。接続管理、再接続、グループ配信、スケールアウトなどの機能を持っているため、一般的なチャットや通知機能ではSignalRの方が実装しやすい場合があります。

一方で、WebSocketを直接使うべきケースもあります。たとえば、独自プロトコルを実装したい場合、外部システムとWebSocket仕様で直接通信する必要がある場合、メッセージ形式や接続管理を細かく制御したい場合、SignalRに依存しない軽量な実装にしたい場合です。

つまり、アプリケーション開発を素早く進めたいならSignalR、WebSocketプロトコルを直接扱いたいならC#のWebSocket APIを選ぶと考えるとわかりやすいです。

2. C#でWebSocketを実装する方法の全体像

2-1. ASP.NET CoreでWebSocketサーバーを作る方法

C#でWebSocketサーバーを作る場合、ASP.NET Coreを使うのが一般的です。ASP.NET CoreにはWebSocketを扱うためのミドルウェアが用意されており、UseWebSockets()でWebSocketを有効化し、AcceptWebSocketAsync()で接続を受け付けます。Microsoftの公式ドキュメントでも、ASP.NET CoreでWebSocketを使う方法としてUseWebSocketsやWebSocketリクエストの受け付けが紹介されています。

基本的な流れは次のとおりです。

1. ASP.NET Coreプロジェクトを作成する
2. Program.csでWebSocketを有効化する
3. /ws などのエンドポイントを用意する
4. WebSocketリクエストかどうか確認する
5. AcceptWebSocketAsyncで接続を受け付ける
6. ReceiveAsyncとSendAsyncで送受信する
7. CloseAsyncで安全に切断する

2-2. ClientWebSocketでC#クライアントを作る方法

C#からWebSocketサーバーへ接続する場合は、System.Net.WebSockets.ClientWebSocketクラスを使います。ClientWebSocketはWebSocketサービスに接続するためのクライアントを提供するクラスです。

基本的な流れは次のようになります。

1. ClientWebSocketを作成する
2. ConnectAsyncでサーバーへ接続する
3. SendAsyncでメッセージを送信する
4. ReceiveAsyncで応答を受信する
5. CloseAsyncで切断する

サーバー側もクライアント側もC#で書けるため、コンソールアプリ、Windowsアプリ、バックグラウンドサービスなどからWebSocket通信を行えます。

2-3. ブラウザJavaScriptクライアントと接続する方法

WebSocketサーバーをC#で作り、クライアントをブラウザJavaScriptで実装する構成もよく使われます。

ブラウザには標準のWebSocket APIがあり、次のように接続できます。

JavaScript
const socket = new WebSocket("ws://localhost:5000/ws");

HTTPS環境では、WebSocketのURLはwss://を使います。

JavaScript
const socket = new WebSocket("wss://example.com/ws");

HTTPに対するHTTPSのように、WebSocketにも平文通信のws://と暗号化通信のwss://があります。実運用では基本的にwss://を使います。

2-4. websocket-sharpなど外部ライブラリを使う場合との違い

C#のWebSocket実装では、過去にwebsocket-sharpなどの外部ライブラリが使われることもありました。外部ライブラリは手軽に扱える一方で、メンテナンス状況、対応.NETバージョン、セキュリティ更新、ASP.NET Coreとの統合性を確認する必要があります。

現在のASP.NET CoreアプリでWebSocketサーバーを作るなら、まずは標準機能を使うのがおすすめです。標準APIであれば、System.Net.WebSocketsやASP.NET Coreのミドルウェアと自然に連携でき、依存ライブラリを増やさずに実装できます。

ただし、古い.NET Framework環境や特殊なクライアント要件がある場合は、外部ライブラリを検討するケースもあります。その場合でも、ライブラリの更新状況とライセンスは必ず確認しましょう。

3. 開発環境の準備

3-1. 必要な.NET SDKとVisual Studio/VS Codeの準備

C# WebSocketを学ぶには、.NET SDKとエディタが必要です。この記事のサンプルはASP.NET Coreの最小構成で書いているため、.NET 8以降のLTS版またはサポート中の.NET SDKを使うとよいでしょう。

エディタはVisual Studio、Visual Studio Code、JetBrains Riderなどが使えます。初心者であれば、ASP.NET Coreのテンプレートやデバッグ機能が使いやすいVisual Studioがおすすめです。軽量に進めたい場合はVS Codeでも問題ありません。

インストール後、ターミナルで次のコマンドを実行して.NET SDKが認識されているか確認します。

Bash
dotnet --version

バージョン番号が表示されれば準備完了です。

3-2. ASP.NET Coreプロジェクトの作成

まず、WebSocketサーバー用のASP.NET Coreプロジェクトを作成します。

Bash
dotnet new web -n CSharpWebSocketServer
cd CSharpWebSocketServer

dotnet new webは、最小構成のASP.NET Coreアプリを作成するテンプレートです。WebSocketの基本を学ぶには、このシンプルな構成が扱いやすいです。

作成後、次のコマンドで起動できます。

Bash
dotnet run

起動すると、次のようなURLが表示されます。

Now listening on: http://localhost:5000
Now listening on: https://localhost:5001

環境によってポート番号は異なります。表示されたURLを確認し、WebSocket接続時のURLに反映します。

3-3. WebSocket通信で使うURLとポートの考え方

WebSocketのURLは、HTTPとはスキームが異なります。

http://localhost:5000     → ws://localhost:5000/ws
https://localhost:5001 → wss://localhost:5001/ws

ローカル開発ではws://localhost:5000/wsのようなURLを使うことが多いです。本番環境ではHTTPS化されたドメイン上でwss://example.com/wsのように接続します。

WebSocketでは、接続先のパスも重要です。サーバー側で/wsというエンドポイントを用意した場合、クライアントも/wsへ接続する必要があります。

3-4. 動作確認に使えるツール

WebSocketの動作確認には、次のような方法があります。

ブラウザのJavaScriptコンソールを使うと、簡単にWebSocket接続を試せます。HTMLファイルを作成してボタン操作で送受信する方法も便利です。

また、WebSocketクライアント機能を持つAPIテストツールを使うと、URLを入力してメッセージ送信や受信ログを確認できます。サーバー側のログと組み合わせると、接続できない原因を調査しやすくなります。

C#クライアントの確認には、ClientWebSocketを使ったコンソールアプリを作成するとよいでしょう。サーバーとクライアントの両方を自分で実装することで、WebSocketの流れを理解しやすくなります。

4. C#でWebSocketサーバーを実装する手順

4-1. Program.csでWebSocketを有効化する

ASP.NET CoreでWebSocketを使うには、Program.csUseWebSockets()を呼び出します。

C#
using System.Net.WebSockets;
using System.Text;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var webSocketOptions = new WebSocketOptions
{
KeepAliveInterval = TimeSpan.FromSeconds(30)
};

app.UseWebSockets(webSocketOptions);

app.MapGet("/", () => "C# WebSocket server is running.");

app.Run();

KeepAliveIntervalは、接続維持のための間隔を指定する設定です。ネットワーク機器やプロキシによっては、一定時間通信がない接続が切断されることがあるため、実運用ではKeepAliveの考え方が重要になります。

4-2. WebSocket接続を受け付けるエンドポイントを作る

次に、/wsというエンドポイントでWebSocket接続を受け付けます。

C#
app.Map("/ws", async context =>
{
if (!context.WebSockets.IsWebSocketRequest)
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
await context.Response.WriteAsync("WebSocket request expected.");
return;
}

using var webSocket = await context.WebSockets.AcceptWebSocketAsync();

await HandleWebSocketAsync(webSocket, context.RequestAborted);
});

context.WebSockets.IsWebSocketRequestで、リクエストがWebSocket接続要求かどうかを確認します。通常のHTTPアクセスだった場合は、400 Bad Requestを返しています。

WebSocketリクエストであれば、AcceptWebSocketAsync()で接続を受け付けます。

4-3. ReceiveAsyncでクライアントからメッセージを受信する

WebSocketでメッセージを受信するには、ReceiveAsync()を使います。

C#
var buffer = new byte[4 * 1024];

WebSocketReceiveResult result =
await webSocket.ReceiveAsync(
new ArraySegment<byte>(buffer),
CancellationToken.None);

ただし、実際のWebSocketメッセージは複数フレームに分割されることがあります。そのため、EndOfMessagetrueになるまで読み取る実装にしておくと安全です。

C#
using var ms = new MemoryStream();
WebSocketReceiveResult result;

do
{
result = await webSocket.ReceiveAsync(
new ArraySegment<byte>(buffer),
CancellationToken.None);

ms.Write(buffer, 0, result.Count);
}
while (!result.EndOfMessage);

テキストメッセージの場合は、UTF-8文字列として取り出します。

C#
var message = Encoding.UTF8.GetString(ms.ToArray());

4-4. SendAsyncでクライアントへメッセージを送信する

サーバーからクライアントへメッセージを送るには、SendAsync()を使います。

C#
var response = $"Server received: {message}";
var bytes = Encoding.UTF8.GetBytes(response);

await webSocket.SendAsync(
new ArraySegment<byte>(bytes),
WebSocketMessageType.Text,
endOfMessage: true,
cancellationToken: CancellationToken.None);

第3引数のendOfMessagetrueにすると、この送信で1つのメッセージが完了することを示します。短いテキストメッセージを送る場合は、基本的にtrueで問題ありません。

4-5. 接続終了時にCloseAsyncで安全に切断する

クライアントから切断要求が送られてくると、MessageTypeCloseになります。この場合は、サーバー側もCloseAsync()を呼び出して正常に接続を閉じます。

C#
if (result.MessageType == WebSocketMessageType.Close)
{
await webSocket.CloseAsync(
WebSocketCloseStatus.NormalClosure,
"Closed by client",
CancellationToken.None);

return;
}

WebSocketでは、接続が突然切断されることもあります。たとえば、ブラウザを閉じた、ネットワークが切れた、サーバーが再起動したなどのケースです。そのため、送受信処理は例外処理と組み合わせて実装することが重要です。

4-6. サーバー側の完成サンプルコード

以下は、ASP.NET Coreで作るC# WebSocketサーバーの完成サンプルです。クライアントから受信したメッセージを、Server received: ...という形で返すエコーサーバーです。

C#
using System.Net.WebSockets;
using System.Text;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var webSocketOptions = new WebSocketOptions
{
KeepAliveInterval = TimeSpan.FromSeconds(30)
};

app.UseWebSockets(webSocketOptions);

app.MapGet("/", () => "C# WebSocket server is running. Connect to /ws.");

app.Map("/ws", async context =>
{
if (!context.WebSockets.IsWebSocketRequest)
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
await context.Response.WriteAsync("WebSocket request expected.");
return;
}

using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
await HandleWebSocketAsync(webSocket, context.RequestAborted);
});

app.Run();

static async Task HandleWebSocketAsync(
WebSocket webSocket,
CancellationToken cancellationToken)
{
var buffer = new byte[4 * 1024];

try
{
while (webSocket.State == WebSocketState.Open &&
!cancellationToken.IsCancellationRequested)
{
using var ms = new MemoryStream();
WebSocketReceiveResult result;

do
{
result = await webSocket.ReceiveAsync(
new ArraySegment<byte>(buffer),
cancellationToken);

if (result.MessageType == WebSocketMessageType.Close)
{
await webSocket.CloseAsync(
WebSocketCloseStatus.NormalClosure,
"Closed by client",
CancellationToken.None);

return;
}

ms.Write(buffer, 0, result.Count);
}
while (!result.EndOfMessage);

if (result.MessageType == WebSocketMessageType.Text)
{
var message = Encoding.UTF8.GetString(ms.ToArray());
Console.WriteLine($"Received: {message}");

var response = $"Server received: {message}";
var responseBytes = Encoding.UTF8.GetBytes(response);

await webSocket.SendAsync(
new ArraySegment<byte>(responseBytes),
WebSocketMessageType.Text,
endOfMessage: true,
cancellationToken);
}
}
}
catch (OperationCanceledException)
{
Console.WriteLine("WebSocket operation was canceled.");
}
catch (WebSocketException ex)
{
Console.WriteLine($"WebSocket error: {ex.Message}");
}
}

起動するには、プロジェクトフォルダで次のコマンドを実行します。

Bash
dotnet run

表示されたURLを確認し、ws://localhost:ポート番号/wsへ接続します。

5. C#でWebSocketクライアントを実装する手順

5-1. ClientWebSocketの基本的な使い方

C#でWebSocketクライアントを作るには、ClientWebSocketを使います。ClientWebSocketSystem.Net.WebSockets名前空間に含まれています。

コンソールアプリを作成するには、次のコマンドを実行します。

Bash
dotnet new console -n CSharpWebSocketClient
cd CSharpWebSocketClient

基本形は次のとおりです。

C#
using System.Net.WebSockets;

using var client = new ClientWebSocket();

usingを使うことで、処理終了時にWebSocketクライアントを破棄できます。

5-2. ConnectAsyncでサーバーへ接続する

サーバーへ接続するには、ConnectAsync()を使います。

C#
var uri = new Uri("ws://localhost:5000/ws");

await client.ConnectAsync(uri, CancellationToken.None);

Console.WriteLine("Connected.");

サーバー側のポート番号が異なる場合は、起動時に表示されたURLに合わせて変更してください。

HTTPSで起動している場合は、次のようにwss://を使います。

C#
var uri = new Uri("wss://localhost:5001/ws");

ローカルの開発証明書が信頼されていない場合、wss://localhost接続で証明書エラーが発生することがあります。その場合は、開発証明書を信頼する設定を行うか、学習用途では一時的にws://で確認します。

5-3. SendAsyncでメッセージを送信する

メッセージを送信するには、文字列をUTF-8のバイト配列に変換してSendAsync()に渡します。

C#
var message = "こんにちは、WebSocket!";
var bytes = Encoding.UTF8.GetBytes(message);

await client.SendAsync(
new ArraySegment<byte>(bytes),
WebSocketMessageType.Text,
endOfMessage: true,
cancellationToken: CancellationToken.None);

日本語を送る場合は、エンコーディングをUTF-8で統一することが重要です。サーバー側でもEncoding.UTF8.GetString()を使えば、文字化けを防ぎやすくなります。

5-4. ReceiveAsyncでサーバーからの応答を受信する

サーバーからの応答を受信するには、サーバー側と同じくReceiveAsync()を使います。

C#
var buffer = new byte[4 * 1024];

var result = await client.ReceiveAsync(
new ArraySegment<byte>(buffer),
CancellationToken.None);

var response = Encoding.UTF8.GetString(buffer, 0, result.Count);

Console.WriteLine(response);

ただし、メッセージが分割される可能性を考えると、EndOfMessageまで読み込む実装の方が安全です。

C#
using var ms = new MemoryStream();
WebSocketReceiveResult result;

do
{
result = await client.ReceiveAsync(
new ArraySegment<byte>(buffer),
CancellationToken.None);

if (result.MessageType == WebSocketMessageType.Close)
{
Console.WriteLine("Server closed the connection.");
return;
}

ms.Write(buffer, 0, result.Count);
}
while (!result.EndOfMessage);

var response = Encoding.UTF8.GetString(ms.ToArray());
Console.WriteLine(response);

5-5. クライアント側の完成サンプルコード

以下は、C#で作るWebSocketクライアントの完成サンプルです。

C#
using System.Net.WebSockets;
using System.Text;

using var client = new ClientWebSocket();

var uri = new Uri("ws://localhost:5000/ws");

try
{
await client.ConnectAsync(uri, CancellationToken.None);
Console.WriteLine("Connected to WebSocket server.");

var message = "こんにちは、C# WebSocket!";
var sendBytes = Encoding.UTF8.GetBytes(message);

await client.SendAsync(
new ArraySegment<byte>(sendBytes),
WebSocketMessageType.Text,
endOfMessage: true,
cancellationToken: CancellationToken.None);

Console.WriteLine($"Sent: {message}");

var buffer = new byte[4 * 1024];
using var ms = new MemoryStream();

WebSocketReceiveResult result;

do
{
result = await client.ReceiveAsync(
new ArraySegment<byte>(buffer),
CancellationToken.None);

if (result.MessageType == WebSocketMessageType.Close)
{
Console.WriteLine("Server closed the connection.");
return;
}

ms.Write(buffer, 0, result.Count);
}
while (!result.EndOfMessage);

var response = Encoding.UTF8.GetString(ms.ToArray());
Console.WriteLine($"Received: {response}");

await client.CloseAsync(
WebSocketCloseStatus.NormalClosure,
"Client finished",
CancellationToken.None);

Console.WriteLine("Closed.");
}
catch (WebSocketException ex)
{
Console.WriteLine($"WebSocket error: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}

実行する前に、WebSocketサーバーを起動しておきます。

Bash
dotnet run

サーバー側とクライアント側でポート番号が一致していれば、クライアントからメッセージが送信され、サーバーから応答が返ってきます。

6. ブラウザからC# WebSocketサーバーへ接続する方法

6-1. JavaScriptのWebSocket APIで接続する

ブラウザからC# WebSocketサーバーへ接続するには、JavaScriptのWebSocketオブジェクトを使います。

JavaScript
const socket = new WebSocket("ws://localhost:5000/ws");

接続が成功すると、onopenイベントが発火します。

JavaScript
socket.onopen = () => {
console.log("WebSocket connected.");
};

サーバーからメッセージを受信すると、onmessageイベントが発火します。

JavaScript
socket.onmessage = (event) => {
console.log("Received:", event.data);
};

エラーや切断もイベントで扱えます。

JavaScript
socket.onerror = (error) => {
console.error("WebSocket error:", error);
};

socket.onclose = () => {
console.log("WebSocket closed.");
};

6-2. HTMLフォームからメッセージを送信する

HTMLの入力フォームからメッセージを送るには、テキストボックスとボタンを用意します。

HTML
<input id="messageInput" type="text" placeholder="メッセージを入力">
<button id="sendButton">送信</button>

ボタンがクリックされたときに、socket.send()でメッセージを送信します。

JavaScript
document.getElementById("sendButton").addEventListener("click", () => {
const input = document.getElementById("messageInput");
const message = input.value;

if (socket.readyState === WebSocket.OPEN && message) {
socket.send(message);
input.value = "";
}
});

readyStateを確認してから送信することで、接続前や切断後に送信してエラーになることを防げます。

6-3. サーバーからのメッセージを画面に表示する

サーバーから受信したメッセージを画面に表示するには、ログ表示用の要素を用意します。

HTML
<ul id="messages"></ul>

onmessage内で要素を追加します。

JavaScript
socket.onmessage = (event) => {
const messages = document.getElementById("messages");
const li = document.createElement("li");
li.textContent = event.data;
messages.appendChild(li);
};

これで、サーバーから返ってきたメッセージをブラウザ画面上で確認できます。

6-4. ブラウザ接続用のサンプルコード

以下は、C# WebSocketサーバーへ接続するHTMLサンプルです。index.htmlとして保存し、ブラウザで開いて使います。

HTML
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>C# WebSocket Client</title>
</head>
<body>
<h1>C# WebSocket ブラウザクライアント</h1>

<div>
<input id="messageInput" type="text" placeholder="メッセージを入力">
<button id="sendButton">送信</button>
</div>

<h2>受信メッセージ</h2>
<ul id="messages"></ul>

<script>
const socket = new WebSocket("ws://localhost:5000/ws");

socket.onopen = () => {
addMessage("接続しました。");
};

socket.onmessage = (event) => {
addMessage("受信: " + event.data);
};

socket.onerror = () => {
addMessage("エラーが発生しました。");
};

socket.onclose = () => {
addMessage("切断されました。");
};

document.getElementById("sendButton").addEventListener("click", () => {
const input = document.getElementById("messageInput");
const message = input.value;

if (!message) {
return;
}

if (socket.readyState === WebSocket.OPEN) {
socket.send(message);
addMessage("送信: " + message);
input.value = "";
} else {
addMessage("WebSocketが接続されていません。");
}
});

function addMessage(text) {
const messages = document.getElementById("messages");
const li = document.createElement("li");
li.textContent = text;
messages.appendChild(li);
}
</script>
</body>
</html>

サーバーのポート番号が5000以外の場合は、次の部分を変更してください。

JavaScript
const socket = new WebSocket("ws://localhost:5000/ws");

7. 複数クライアント対応とリアルタイム通信の実装例

7-1. 接続中のWebSocketクライアントを管理する

チャットアプリや通知機能では、複数のクライアントが同時に接続します。そのため、接続中のWebSocketを管理する仕組みが必要です。

単純な実装では、ConcurrentDictionaryを使って接続中のクライアントを保持できます。

C#
using System.Collections.Concurrent;
using System.Net.WebSockets;

var clients = new ConcurrentDictionary<string, WebSocket>();

接続時にIDを発行して登録します。

C#
var clientId = Guid.NewGuid().ToString();
clients.TryAdd(clientId, webSocket);

切断時には削除します。

C#
clients.TryRemove(clientId, out _);

ConcurrentDictionaryを使うことで、複数スレッドから安全に追加・削除しやすくなります。ただし、WebSocket自体への同時送信には注意が必要です。同じWebSocketに対して複数のSendAsyncが同時に走ると問題になることがあるため、実運用ではクライアントごとに送信用キューやロックを持たせる設計が安全です。

7-2. 全クライアントへメッセージをブロードキャストする

接続中の全クライアントへ同じメッセージを送る処理をブロードキャストと呼びます。

シンプルな例は次のとおりです。

C#
static async Task BroadcastAsync(
ConcurrentDictionary<string, WebSocket> clients,
string message,
CancellationToken cancellationToken)
{
var bytes = Encoding.UTF8.GetBytes(message);

foreach (var client in clients)
{
var socket = client.Value;

if (socket.State != WebSocketState.Open)
{
continue;
}

await socket.SendAsync(
new ArraySegment<byte>(bytes),
WebSocketMessageType.Text,
endOfMessage: true,
cancellationToken);
}
}

この例では、接続中のすべてのクライアントへ同じテキストを送信しています。

ただし、実運用では送信に失敗したクライアントを削除する、送信タイムアウトを設ける、遅いクライアントに引きずられないようにする、といった工夫が必要です。

7-3. チャットアプリの基本構成

C# WebSocketでチャットアプリを作る場合、基本構成は次のようになります。

ブラウザA ┐
ブラウザB ├── WebSocketサーバー ── メッセージ管理
ブラウザC ┘

1人のユーザーがメッセージを送信すると、サーバーがそのメッセージを受信し、接続中の全クライアントへブロードキャストします。

処理の流れは次のとおりです。

1. クライアントがWebSocket接続する
2. サーバーが接続をclientsに登録する
3. クライアントがメッセージを送信する
4. サーバーがReceiveAsyncで受信する
5. サーバーが全クライアントへSendAsyncで配信する
6. 切断時にclientsから削除する

簡易的なチャットであれば、送信者名と本文をJSON形式で送ると扱いやすいです。

JSON
{
"user": "taro",
"message": "こんにちは"
}

C#側ではSystem.Text.Jsonを使ってJSONをシリアライズ・デシリアライズできます。

7-4. ダッシュボードや通知機能への応用例

WebSocketはチャットだけでなく、リアルタイムダッシュボードや通知機能にも向いています。

たとえば、管理画面で売上件数や処理状況をリアルタイムに表示したい場合、サーバー側でデータ更新が発生したタイミングでWebSocket経由でブラウザへ通知します。ブラウザ側は受信したデータを画面に反映するだけでよいため、無駄なポーリングを減らせます。

通知機能では、次のようなイベントをWebSocketで配信できます。

・新しい注文が入った
・承認依頼が届いた
・バッチ処理が完了した
・エラーが発生した
・ユーザー宛てのメッセージが届いた

ただし、WebSocketは常時接続を前提にするため、すべてのAPIをWebSocketに置き換える必要はありません。ユーザー一覧の取得や詳細データの取得はREST API、リアルタイム通知はWebSocket、というように役割を分けると設計しやすくなります。

8. C# WebSocketでよくあるエラーと対処法

8-1. 接続できない場合の確認ポイント

C# WebSocketで接続できない場合は、まずURL、ポート、プロトコル、エンドポイントを確認します。

よくある原因は次のとおりです。

・サーバーが起動していない
・ポート番号が違う
・/ws などのパスが違う
・ws:// と wss:// を間違えている
・HTTPS証明書が信頼されていない
・プロキシやファイアウォールで遮断されている
・WebSocketリクエストではないURLに接続している

ローカルでは、ASP.NET Core起動時に表示されるURLを必ず確認してください。

Now listening on: http://localhost:5000

この場合、WebSocketのURLは次のようになります。

ws://localhost:5000/ws

HTTPS側に接続するなら、次のようになります。

wss://localhost:5001/ws

8-2. The remote party closed the WebSocket connectionエラーの原因

The remote party closed the WebSocket connectionというエラーは、接続相手がWebSocketを閉じたときに発生します。

主な原因は次のとおりです。

・ブラウザを閉じた
・クライアントアプリが終了した
・ネットワークが切断された
・サーバーが再起動した
・プロキシやロードバランサーが接続を切断した
・CloseAsyncを呼ばずに接続が終了した

このエラーは必ずしも異常ではありません。ユーザーがページを閉じた場合にも発生することがあります。

対処法としては、送受信処理をtry-catchで囲み、切断時に接続管理リストからWebSocketを削除することが重要です。

C#
try
{
// ReceiveAsync / SendAsync
}
catch (WebSocketException ex)
{
Console.WriteLine($"WebSocket closed or failed: {ex.Message}");
}
finally
{
clients.TryRemove(clientId, out _);
}

8-3. 送受信中にObjectDisposedExceptionが出る原因

ObjectDisposedExceptionは、すでに破棄されたWebSocketに対して送受信しようとした場合に発生します。

よくある原因は次のとおりです。

・usingスコープを抜けた後にSendAsyncしている
・切断済みのWebSocketをclientsに残している
・別スレッドでCloseAsyncまたはDisposeされた
・ブロードキャスト中にクライアントが切断された

対策として、送信前にStateを確認します。

C#
if (socket.State == WebSocketState.Open)
{
await socket.SendAsync(
new ArraySegment<byte>(bytes),
WebSocketMessageType.Text,
true,
CancellationToken.None);
}

ただし、Stateを確認した直後に切断される可能性もあります。そのため、最終的にはtry-catchで保護する必要があります。

C#
try
{
if (socket.State == WebSocketState.Open)
{
await socket.SendAsync(
new ArraySegment<byte>(bytes),
WebSocketMessageType.Text,
true,
CancellationToken.None);
}
}
catch (ObjectDisposedException)
{
// 切断済みとして扱う
}
catch (WebSocketException)
{
// 通信エラーとして扱う
}

8-4. WebSocketExceptionが発生する主なケース

WebSocketExceptionは、WebSocket通信中のさまざまな問題で発生します。

主なケースは次のとおりです。

・接続先URLが間違っている
・WebSocketハンドシェイクに失敗した
・通信中にネットワークが切れた
・相手側が異常終了した
・TLS証明書に問題がある
・プロキシがWebSocketを許可していない
・不正なフレームを受信した

対処するには、例外メッセージだけでなく、サーバー側ログ、クライアント側ログ、HTTPステータスコード、リバースプロキシのログをあわせて確認します。

特に本番環境では、アプリケーションサーバーだけでなく、Nginx、IIS、Azure App Service、ロードバランサーなどの設定も確認が必要です。

8-5. 文字化けやメッセージ欠落が起きる原因

日本語メッセージが文字化けする場合、送信側と受信側のエンコーディングが一致していない可能性があります。C#側では、基本的にUTF-8を使います。

送信側は次のようにします。

C#
var bytes = Encoding.UTF8.GetBytes(message);

受信側は次のようにします。

C#
var message = Encoding.UTF8.GetString(bytes);

また、メッセージ欠落の原因として、ReceiveAsync()を1回だけ呼んで処理を終えているケースがあります。WebSocketメッセージは分割されることがあるため、EndOfMessagetrueになるまで読み取る必要があります。

C#
do
{
result = await socket.ReceiveAsync(
new ArraySegment<byte>(buffer),
CancellationToken.None);

ms.Write(buffer, 0, result.Count);
}
while (!result.EndOfMessage);

長いメッセージやJSONを扱う場合は、この実装が特に重要です。

8-6. タイムアウトや切断が頻発する場合の対処法

WebSocketの切断が頻発する場合は、アプリケーションコードだけでなく、ネットワーク経路も確認します。

主な確認ポイントは次のとおりです。

・KeepAliveIntervalを設定しているか
・プロキシのタイムアウトが短すぎないか
・ロードバランサーがWebSocketをサポートしているか
・アイドル接続が途中で切られていないか
・サーバーのメモリやCPUが不足していないか
・クライアント側で再接続処理を実装しているか

ASP.NET Core側では、UseWebSockets()にオプションを渡してKeepAliveを設定できます。

C#
app.UseWebSockets(new WebSocketOptions
{
KeepAliveInterval = TimeSpan.FromSeconds(30)
});

また、ブラウザやC#クライアントでは、切断時に再接続する設計が必要です。ただし、全クライアントが一斉に再接続するとサーバーに負荷がかかるため、再接続間隔を少しずつ伸ばすバックオフ処理を入れると安全です。

9. 実運用で注意すべき設計ポイント

9-1. wss化とHTTPS対応

本番環境では、WebSocket通信は基本的にwss://を使います。ws://は暗号化されていないため、通信内容を保護できません。

開発環境: ws://localhost:5000/ws
本番環境: wss://example.com/ws

HTTPSサイトからws://へ接続しようとすると、ブラウザのセキュリティ制約でブロックされることがあります。HTTPSサイトでは、WebSocketもwss://に統一しましょう。

証明書は、アプリケーションサーバー、リバースプロキシ、クラウドサービスのどこで終端するのかを明確にします。NginxやIISの背後にASP.NET Coreアプリを配置する場合、WebSocketのアップグレードヘッダーが正しく転送される設定も必要です。

9-2. 認証・認可を組み込む方法

WebSocketでも認証・認可は重要です。誰でも接続できるWebSocketエンドポイントを公開すると、不正アクセスや過剰接続の原因になります。

認証方法としては、Cookie認証、Bearerトークン、クエリ文字列での一時トークン、接続直後の認証メッセージなどがあります。

ブラウザアプリで既存のログインセッションを使う場合は、Cookie認証と組み合わせることがあります。一方、外部クライアントやモバイルアプリでは、トークン認証を使うことが多いです。

たとえば、接続URLにトークンを付ける設計は次のようになります。

wss://example.com/ws?access_token=xxxxx

ただし、クエリ文字列はログに残る可能性があるため、短命のトークンを使う、ログに出さない、HTTPSを必須にするなどの対策が必要です。

認可では、ユーザーごとに受信できるメッセージを制御します。たとえば、管理者向け通知を一般ユーザーへ送らない、特定ルームの参加者だけにチャットを配信する、といった制御が必要です。

9-3. 接続数が増えた場合のスケーリング

WebSocketは常時接続を維持するため、REST APIとは異なるスケーリング設計が必要です。

接続数が少ないうちは、1台のサーバー内で接続リストを管理しても問題ありません。しかし、サーバーを複数台に増やすと、各クライアントが別々のサーバーへ接続されます。

クライアントA → サーバー1
クライアントB → サーバー2
クライアントC → サーバー3

この状態で全体にメッセージを配信するには、サーバー間でメッセージを共有する仕組みが必要です。代表的にはRedis Pub/Sub、メッセージキュー、クラウドのリアルタイム配信サービスなどを使います。

また、ロードバランサーでは、同じクライアントを同じサーバーへ振り分けるスティッキーセッションが必要になる場合があります。設計によっては、接続管理をサーバー内メモリに閉じず、外部ストアやメッセージ基盤と組み合わせる方が拡張しやすくなります。

9-4. Ping/PongやKeepAliveの考え方

WebSocketでは、接続が生きているかを確認するためにPing/PongやKeepAliveの仕組みを考慮します。

ASP.NET CoreのWebSocketOptionsでは、KeepAliveIntervalを設定できます。

C#
app.UseWebSockets(new WebSocketOptions
{
KeepAliveInterval = TimeSpan.FromSeconds(30)
});

ただし、KeepAliveだけでアプリケーションレベルの状態確認が十分とは限りません。たとえば、クライアントアプリが内部的に停止している、画面は開いているが処理が進んでいない、といったケースでは、アプリケーション独自のハートビートメッセージを設計することもあります。

例として、クライアントから定期的に次のようなJSONを送る方法があります。

JSON
{
"type": "ping"
}

サーバーは応答として次のように返します。

JSON
{
"type": "pong"
}

このような設計にしておくと、アプリケーションレベルで接続状態を把握しやすくなります。

9-5. ログ出力と監視のポイント

WebSocketは長時間接続されるため、通常のHTTP APIよりもログ設計が重要です。

最低限、次のような情報を記録すると障害調査がしやすくなります。

・接続開始時刻
・切断時刻
・ユーザーID
・接続元IP
・接続先エンドポイント
・切断理由
・例外内容
・送受信メッセージ数
・接続中クライアント数

ただし、メッセージ本文をすべてログに出すと、個人情報や機密情報が残る可能性があります。本番環境では、本文をマスクする、ログレベルを分ける、必要なメタデータだけ記録するなどの配慮が必要です。

監視では、現在の接続数、接続失敗数、異常切断数、送信失敗数、サーバーのCPU・メモリ使用率を確認します。接続数が急増した場合にアラートを出す仕組みも有効です。

9-6. 例外処理と再接続処理の実装方針

WebSocketはネットワークに依存するため、切断や例外を前提に設計します。

サーバー側では、例外が発生したら接続リストから削除し、リソースを解放します。

C#
finally
{
clients.TryRemove(clientId, out _);

if (webSocket.State != WebSocketState.Closed)
{
webSocket.Dispose();
}
}

クライアント側では、oncloseWebSocketExceptionを検知して再接続します。ブラウザであれば次のような考え方です。

JavaScript
let retryCount = 0;

function connect() {
const socket = new WebSocket("wss://example.com/ws");

socket.onopen = () => {
retryCount = 0;
};

socket.onclose = () => {
const delay = Math.min(1000 * Math.pow(2, retryCount), 30000);
retryCount++;

setTimeout(connect, delay);
};
}

connect();

再接続時には、未送信メッセージをどう扱うか、再接続後に最新状態をREST APIで再取得するか、サーバー側で再購読処理が必要かも検討します。

10. C# WebSocket実装でよくある質問

10-1. C#だけでWebSocketサーバーとクライアントを作れる?

はい、作れます。サーバー側はASP.NET CoreのWebSocket機能、クライアント側はClientWebSocketを使えば、C#だけでWebSocket通信を実装できます。

学習用であれば、ASP.NET CoreのWebSocketサーバーとC#コンソールアプリのクライアントを作る構成がわかりやすいです。さらにブラウザクライアントも追加すると、サーバー、C#クライアント、JavaScriptクライアントの違いを確認できます。

10-2. ASP.NET CoreなしでもWebSocketは使える?

クライアントとして接続するだけであれば、ASP.NET Coreは不要です。C#のコンソールアプリやデスクトップアプリからClientWebSocketを使ってWebSocketサーバーへ接続できます。

一方、WebSocketサーバーをC#で実装する場合は、ASP.NET Coreを使うのが一般的です。HTTPサーバー機能、ルーティング、認証、ログ、ホスティングなどと統合しやすいためです。

低レベルなソケット処理を自前で実装することも不可能ではありませんが、WebSocketのハンドシェイクやフレーム処理を正しく実装する必要があるため、通常はおすすめしません。

10-3. WebSocketとSignalRはどちらを選ぶべき?

多くの業務アプリでは、まずSignalRを検討するとよいでしょう。SignalRはリアルタイム通信を抽象化しており、接続管理、グループ配信、再接続などを扱いやすくしてくれます。

一方で、次のような場合はWebSocketを直接使う選択肢があります。

・外部サービスのWebSocket APIへ接続する
・独自プロトコルを実装したい
・SignalRクライアントを使えない環境と通信する
・通信形式を細かく制御したい
・学習目的でWebSocketの仕組みを理解したい

「アプリの機能を早く作りたい」ならSignalR、「WebSocketそのものを制御したい」なら直接WebSocket、という判断がしやすいです。

10-4. REST APIとWebSocketはどう使い分ける?

REST APIとWebSocketは、どちらか一方に統一するものではなく、用途で使い分けるのが基本です。

REST APIは、リソースの取得、作成、更新、削除に向いています。たとえば、ユーザー一覧の取得、商品詳細の取得、注文の作成などです。

WebSocketは、サーバーからクライアントへ即時通知したい処理に向いています。たとえば、新着通知、チャット、進捗更新、リアルタイムログ、状態変化の配信などです。

実際の設計では、初期データはREST APIで取得し、その後の差分更新をWebSocketで受け取る構成がよく使われます。

1. 画面表示時にREST APIで初期データを取得
2. WebSocketへ接続
3. サーバー側の更新をWebSocketで受信
4. 必要に応じてREST APIで詳細を再取得

このように分けると、実装が整理され、障害時のリカバリーもしやすくなります。

10-5. Unityやデスクトップアプリでも使える?

はい、C#を使うUnityやデスクトップアプリでもWebSocketは利用できます。

デスクトップアプリでは、ClientWebSocketを使ってサーバーへ接続できます。WPF、WinForms、MAUIなどのアプリで、通知受信やリアルタイム更新を実装できます。

Unityの場合は、使用するUnityバージョンや対象プラットフォームによって利用できるAPIが異なることがあります。PC向け、スマートフォン向け、WebGL向けでは制約が変わるため、UnityでWebSocketを使う場合は、標準APIで足りるか、Unity向けのWebSocketライブラリが必要かを確認しましょう。

特にゲームでは、低遅延通信や大量メッセージ処理が必要になることがあります。WebSocketで十分なケースもありますが、リアルタイム性が非常に高いゲームではUDPベースの通信方式を検討する場合もあります。

まとめ

C#でWebSocketを実装すると、クライアントとサーバーの間で双方向のリアルタイム通信を行えます。ASP.NET Coreを使えばWebSocketサーバーを作成でき、ClientWebSocketを使えばC#クライアントからWebSocketサーバーへ接続できます。ブラウザJavaScriptのWebSocket APIと組み合わせれば、Webアプリのリアルタイム通知やチャット機能も実装できます。

基本的な実装の流れは、サーバー側でUseWebSockets()を有効化し、AcceptWebSocketAsync()で接続を受け付け、ReceiveAsync()SendAsync()でメッセージを送受信するというものです。クライアント側では、ConnectAsync()で接続し、同じくSendAsync()ReceiveAsync()で通信します。

実運用では、wss://による暗号化、認証・認可、複数クライアント管理、ブロードキャスト、KeepAlive、ログ監視、例外処理、再接続処理が重要になります。また、すべてをWebSocketで実装するのではなく、初期データ取得や通常のCRUD処理はREST API、リアルタイム通知はWebSocketというように役割を分けると、保守しやすい設計になります。

C# WebSocketの基本を理解しておけば、チャット、通知、ダッシュボード、IoT監視、デスクトップアプリ、Unityアプリなど、さまざまなリアルタイム通信機能に応用できます。