C#ソケット通信入門:TCP/UDPの基本から接続・送受信エラー対策までサンプルコードで解説

はじめに

C#でソケット通信を実装したいとき、多くの人が最初につまずくのは「TCPとUDPの違い」「サーバーとクライアントの役割」「Connect、Accept、Send、Receiveの流れ」「接続できない・受信できない原因」です。

C#では、低レベルなSocketクラスのほかに、TCPを扱いやすくしたTcpClientTcpListener、UDPを扱いやすくしたUdpClientが用意されています。TcpClientはTCPネットワークサービスのクライアント接続、TcpListenerはTCPクライアントからの接続待ち受け、UdpClientはUDPネットワークサービスを扱うためのクラスです。

この記事では、「C# ソケット」で検索している初心者から実務で通信処理を実装したい人までを対象に、TCP/UDPの基礎、接続・送受信の実装、エラー対策、非同期処理、安全な運用までをサンプルコード付きで解説します。

1. C#のソケット通信とは?検索ユーザーが最初に知りたい基礎知識

1-1. ソケット通信の仕組みを初心者向けにわかりやすく解説

ソケット通信とは、ネットワーク上のプログラム同士がデータを送受信するための仕組みです。たとえば、PC Aで動いているC#アプリと、PC Bで動いているC#アプリが直接メッセージをやり取りする場合、それぞれのアプリは「IPアドレス」と「ポート番号」を使って相手を指定します。

イメージとしては、IPアドレスが「建物の住所」、ポート番号が「部屋番号」です。サーバー側アプリは特定のポート番号で待ち受け、クライアント側アプリはそのIPアドレスとポート番号に接続します。

C#のソケット通信では、主にSystem.NetSystem.Net.Sockets名前空間を使います。TCPならTcpListenerで待ち受け、TcpClientで接続し、データ本体はNetworkStreamを通じて読み書きするのが基本です。UDPならUdpClientでデータグラムを送受信します。

1-2. C#でソケット通信を使う主な用途

C#ソケット通信は、HTTPよりも低いレイヤーで通信を制御したい場面で使われます。代表的な用途は次のとおりです。

  • 独自プロトコルのクライアント・サーバー通信

  • チャットアプリやメッセージング機能

  • IoT機器、センサー、計測機器との通信

  • ゲームのリアルタイム通信

  • LAN内ツールのPC間通信

  • 既存システムや組み込み機器とのTCP/UDP連携

  • 監視ツール、ログ収集、制御コマンド送信

Web APIのように標準化された通信ならHTTP/HTTPSが向いています。一方、データ形式や通信タイミングを細かく制御したい場合、C#のソケット通信が選択肢になります。

1-3. SocketクラスとTcpClient・TcpListener・UdpClientの違い

C#でソケット通信を実装するときは、まず「どのクラスを使うか」を決めます。

クラス主な用途特徴
SocketTCP/UDPを低レベルに制御細かい制御ができるが実装量が増える
TcpListenerTCPサーバー側クライアント接続を待ち受ける
TcpClientTCPクライアント側、またはサーバーが受け入れた接続NetworkStreamで読み書きできる
UdpClientUDP送受信接続なしで軽量にデータを送れる

初心者には、まずTcpListenerTcpClientUdpClientがおすすめです。Socketクラスは柔軟ですが、接続管理、バッファ処理、例外処理を自分で細かく設計する必要があります。SocketにはAcceptConnectSendReceiveBindなどネットワーク通信の基本操作が用意されています。

1-4. ソケット通信で使われるIPアドレス・ポート番号・エンドポイントとは

ソケット通信では、次の用語を理解しておく必要があります。

IPアドレスは、ネットワーク上の機器を識別する住所です。ローカルPC自身を指す場合は127.0.0.1localhostを使います。LAN内の別PCと通信する場合は、192.168.x.xのようなプライベートIPアドレスを使うことが多いです。

ポート番号は、同じPC内でどのアプリケーションに通信を届けるかを識別する番号です。たとえばC#のTCPサーバーを5000番ポートで待ち受ける場合、クライアントは「サーバーのIPアドレス + 5000番ポート」に接続します。

エンドポイントは、IPアドレスとポート番号を組み合わせた接続先情報です。C#ではIPEndPointクラスで表現します。

C#
using System.Net;

IPAddress ipAddress = IPAddress.Parse("127.0.0.1");
int port = 5000;

IPEndPoint endpoint = new IPEndPoint(ipAddress, port);
Console.WriteLine(endpoint); // 127.0.0.1:5000

2. TCPとUDPの違いを理解する

2-1. TCP通信の特徴:接続型・信頼性・順序保証

TCPは、通信前にサーバーとクライアントが接続を確立する「接続型」のプロトコルです。データが届いたか、順番が正しいかを管理しながら通信します。

TCPの主な特徴は次のとおりです。

  • 接続を確立してから送受信する

  • データの到達性が高い

  • 送信した順序で受信される

  • データが連続したストリームとして扱われる

  • 信頼性が高い分、UDPより処理は重くなりやすい

注意点は、TCPには「メッセージの区切り」がないことです。送信側が1回Writeしても、受信側の1回Readで必ず同じ単位で受け取れるとは限りません。そのため、実務では「改行区切り」「固定長」「先頭にデータ長を付ける」など、アプリ側でメッセージ境界を設計します。

2-2. UDP通信の特徴:非接続型・高速性・軽量性

UDPは、接続を確立せずにデータを送る「非接続型」のプロトコルです。TCPのような到達確認や順序保証はありませんが、その分軽量で高速です。C#ではUdpClientを使うとUDPネットワークサービスを簡単に扱えます。

UDPの主な特徴は次のとおりです。

  • 接続確立なしで送信できる

  • 低遅延で軽量

  • パケットが失われる可能性がある

  • 到着順が入れ替わる可能性がある

  • 1回の送信が1つのデータグラムとして扱われる

UDPは、多少の欠損よりも速度を重視する用途に向いています。たとえばリアルタイムゲーム、音声・映像配信、センサー値の定期送信、LAN内ブロードキャストなどです。

2-3. TCPとUDPの使い分け方

TCPとUDPは、どちらが優れているというより、用途が異なります。

判断項目TCPUDP
信頼性高い低い
順序保証ありなし
通信速度UDPより重め軽量・高速
接続必要不要
向いている用途チャット、ファイル送信、業務通信ゲーム、配信、ブロードキャスト、センサー値

データを確実に届けたいならTCP、多少失われてもリアルタイム性を重視するならUDPが基本です。

2-4. C#でTCP/UDPを選ぶときの判断基準

C#でソケット通信を実装する場合、最初は次の基準で選ぶと失敗しにくくなります。

  • ログイン、注文、制御コマンド、ファイル転送など「失ってはいけないデータ」ならTCP

  • 位置情報、状態通知、センサー値など「次のデータで上書きされてもよい情報」ならUDP

  • サーバーとクライアントが1対1または複数接続で会話するならTCP

  • LAN内の複数端末へ一斉通知したいならUDPブロードキャスト

  • 初心者が学習するなら、まずTCPのTcpListenerTcpClientから始める

3. C#でTCPソケット通信を実装する基本手順

3-1. TCPサーバー側の処理フロー

TCPサーバー側の基本的な流れは次のとおりです。

  1. 待ち受けるIPアドレスとポート番号を決める

  2. TcpListenerを作成する

  3. Start()で待ち受けを開始する

  4. AcceptTcpClient()でクライアント接続を受け入れる

  5. GetStream()NetworkStreamを取得する

  6. Readで受信、Writeで送信する

  7. 通信が終わったらCloseまたはDisposeで解放する

TcpListenerはTCPクライアントからの接続をリッスンするクラスで、受け入れた接続はTcpClientとして扱えます。

3-2. TCPクライアント側の処理フロー

TCPクライアント側の基本的な流れは次のとおりです。

  1. 接続先のIPアドレスまたはホスト名を決める

  2. 接続先ポート番号を決める

  3. TcpClientを作成する

  4. Connectまたはコンストラクターで接続する

  5. GetStream()NetworkStreamを取得する

  6. Writeで送信、Readで受信する

  7. 通信が終わったらDisposeする

TcpClientでは、GetStream()で取得したNetworkStreamReadWriteを使ってデータを送受信します。

3-3. TcpListenerを使ったTCPサーバーのサンプルコード

次のコードは、127.0.0.1:5000で待ち受けるシンプルなTCPサーバーです。クライアントから文字列を受け取り、応答を返します。

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

int port = 5000;
TcpListener? listener = null;

try
{
listener = new TcpListener(IPAddress.Loopback, port);
listener.Start();

Console.WriteLine($"TCPサーバーを開始しました。ポート: {port}");

while (true)
{
Console.WriteLine("クライアント接続待ち...");
using TcpClient client = listener.AcceptTcpClient();
Console.WriteLine("クライアントが接続しました。");

using NetworkStream stream = client.GetStream();

byte[] buffer = new byte[1024];
int bytesRead = stream.Read(buffer, 0, buffer.Length);

if (bytesRead == 0)
{
Console.WriteLine("クライアントが切断しました。");
continue;
}

string message = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine($"受信: {message}");

string response = $"サーバーで受信しました: {message}";
byte[] responseBytes = Encoding.UTF8.GetBytes(response);

stream.Write(responseBytes, 0, responseBytes.Length);
Console.WriteLine("応答を送信しました。");
}
}
catch (SocketException ex)
{
Console.WriteLine($"SocketException: {ex.SocketErrorCode} / {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"エラー: {ex.Message}");
}
finally
{
listener?.Stop();
}

3-4. TcpClientを使ったTCPクライアントのサンプルコード

次のコードは、上記のTCPサーバーに接続するクライアントです。

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

string server = "127.0.0.1";
int port = 5000;

try
{
using TcpClient client = new TcpClient();
client.Connect(server, port);

using NetworkStream stream = client.GetStream();

string message = "こんにちは、C#ソケット通信";
byte[] sendBytes = Encoding.UTF8.GetBytes(message);

stream.Write(sendBytes, 0, sendBytes.Length);
Console.WriteLine($"送信: {message}");

byte[] buffer = new byte[1024];
int bytesRead = stream.Read(buffer, 0, buffer.Length);

string response = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine($"受信: {response}");
}
catch (SocketException ex)
{
Console.WriteLine($"接続または通信エラー: {ex.SocketErrorCode} / {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"エラー: {ex.Message}");
}

動作確認は、先にサーバーを起動し、次にクライアントを起動します。同じPC内で試す場合は127.0.0.1またはlocalhostを使います。

3-5. TCP通信で文字列データを送受信する方法

文字列を送る場合は、送信時にEncoding.UTF8.GetBytes()でバイト配列に変換し、受信時にEncoding.UTF8.GetString()で文字列に戻します。

ただし、TCPでは1回のReadで1メッセージ全体が受け取れるとは限りません。実務では、次のように改行区切りで送受信すると扱いやすくなります。

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

using TcpClient client = new TcpClient("127.0.0.1", 5000);
using NetworkStream stream = client.GetStream();
using StreamWriter writer = new StreamWriter(stream, Encoding.UTF8) { AutoFlush = true };
using StreamReader reader = new StreamReader(stream, Encoding.UTF8);

await writer.WriteLineAsync("1行メッセージです");
string? response = await reader.ReadLineAsync();

Console.WriteLine($"応答: {response}");

改行区切りにする場合、送信側はWriteLine、受信側はReadLineを使います。バイナリデータや大きなデータを扱う場合は、「先頭4バイトにデータ長を入れる」などの長さ付きプロトコルを設計するのが一般的です。

4. C#でUDPソケット通信を実装する基本手順

4-1. UDP通信の処理フロー

UDP通信はTCPと違い、接続確立を必須としません。送信側は宛先IPアドレスとポート番号を指定してデータを送ります。受信側は指定したポートで待ち受け、届いたデータグラムを受信します。

UDP受信側の流れは次のとおりです。

  1. 受信用ポート番号を決める

  2. UdpClientを作成してポートにバインドする

  3. ReceiveまたはReceiveAsyncで待ち受ける

  4. 送信元エンドポイントとデータを取得する

  5. 必要に応じて送信元に返信する

UDP送信側の流れは次のとおりです。

  1. 送信先IPアドレスとポート番号を決める

  2. UdpClientを作成する

  3. 文字列などをバイト配列に変換する

  4. SendまたはSendAsyncで送信する

4-2. UdpClientを使ったUDP送信のサンプルコード

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

string serverIp = "127.0.0.1";
int port = 5001;

using UdpClient udpClient = new UdpClient();

string message = "UDPで送信するメッセージ";
byte[] data = Encoding.UTF8.GetBytes(message);

IPEndPoint endpoint = new IPEndPoint(IPAddress.Parse(serverIp), port);

udpClient.Send(data, data.Length, endpoint);

Console.WriteLine($"UDP送信: {message}");

4-3. UdpClientを使ったUDP受信のサンプルコード

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

int port = 5001;

using UdpClient udpClient = new UdpClient(port);

Console.WriteLine($"UDP受信待ち。ポート: {port}");

while (true)
{
IPEndPoint remoteEndPoint = new IPEndPoint(IPAddress.Any, 0);

byte[] receivedBytes = udpClient.Receive(ref remoteEndPoint);
string message = Encoding.UTF8.GetString(receivedBytes);

Console.WriteLine($"受信元: {remoteEndPoint}");
Console.WriteLine($"受信データ: {message}");
}

先に受信側を起動し、次に送信側を起動すると、受信側コンソールにメッセージが表示されます。

4-4. UDP通信で複数クライアントとやり取りする方法

UDPでは、受信時に送信元のIPEndPointを取得できます。そのため、複数クライアントから届いたデータに対して、送信元ごとに返信できます。

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

int port = 5001;

using UdpClient server = new UdpClient(port);
Console.WriteLine($"UDPサーバー開始: {port}");

while (true)
{
IPEndPoint remote = new IPEndPoint(IPAddress.Any, 0);
byte[] requestBytes = server.Receive(ref remote);

string request = Encoding.UTF8.GetString(requestBytes);
Console.WriteLine($"{remote} から受信: {request}");

string response = $"受信しました: {request}";
byte[] responseBytes = Encoding.UTF8.GetBytes(response);

server.Send(responseBytes, responseBytes.Length, remote);
}

TCPのように「接続中クライアント一覧」を持たなくても、UDPでは送信元エンドポイントを見て返信できます。ただし、相手が受信可能な状態かどうか、途中でパケットが失われていないかはアプリ側で考慮する必要があります。

4-5. UDP通信で注意すべきパケットロスと順序ズレ

UDPでは、送信したデータが必ず届くとは限りません。また、複数のパケットを連続送信した場合に、到着順が入れ替わる可能性もあります。

対策としては、用途に応じて次のような仕組みをアプリ側で追加します。

  • シーケンス番号を付ける

  • タイムスタンプを付ける

  • 古いデータを破棄する

  • 必要なデータだけ再送要求する

  • 一定時間応答がなければタイムアウト扱いにする

リアルタイム性が重要なゲームやセンサー通信では、古いパケットを再送するより「最新データを優先する」設計のほうが自然な場合もあります。

5. ソケット通信で接続・送受信を安定させる実装ポイント

5-1. Connect・Accept・Receive・Sendの基本動作

C#ソケット通信でよく使う基本操作は次のとおりです。

操作役割
Connectクライアントがサーバーへ接続する
Accept / AcceptTcpClientサーバーがクライアント接続を受け入れる
Receive / Readデータを受信する
Send / Writeデータを送信する

同期メソッドの多くは、処理が完了するまでブロックします。たとえばAcceptTcpClient()はクライアントが接続するまで待ち続けます。Read()もデータが届くまで待機します。

このブロック動作を理解していないと、「アプリが固まった」「次の処理に進まない」と感じる原因になります。GUIアプリや複数クライアント対応では、非同期処理や別スレッド化を検討します。

5-2. バッファサイズの決め方とデータ欠損を防ぐ方法

受信バッファは、1回の読み取りで一時的にデータを入れる領域です。

C#
byte[] buffer = new byte[4096];
int bytesRead = stream.Read(buffer, 0, buffer.Length);

バッファサイズを大きくすれば必ず安全というわけではありません。TCPではデータが分割されて届く可能性があるため、「何バイト読めたか」を必ず確認し、必要なサイズに達するまで繰り返し読む設計が重要です。

固定長データを受信する場合の例です。

C#
static int ReadExact(NetworkStream stream, byte[] buffer, int size)
{
int totalRead = 0;

while (totalRead < size)
{
int read = stream.Read(buffer, totalRead, size - totalRead);

if (read == 0)
{
throw new IOException("接続が切断されました。");
}

totalRead += read;
}

return totalRead;
}

TCPでは「1回送ったものが1回で読める」と考えないことが重要です。

5-3. 文字コードを指定して文字化けを防ぐ方法

C#で文字列を送受信するときは、送信側と受信側で同じ文字コードを使います。日本語を扱うならUTF-8を指定するのが基本です。

C#
string text = "日本語メッセージ";
byte[] bytes = Encoding.UTF8.GetBytes(text);

string restored = Encoding.UTF8.GetString(bytes);

送信側がUTF-8、受信側がShift_JISやASCIIとして解釈すると文字化けします。特に外部機器や既存システムと連携する場合は、仕様書で文字コードを確認しましょう。

5-4. タイムアウトを設定して処理停止を防ぐ方法

同期通信では、接続先が応答しないとReadWriteで待ち続けることがあります。TcpClientではReceiveTimeoutSendTimeoutを設定できます。NetworkStreamにも読み書きのタイムアウト関連プロパティがあります。

C#
using TcpClient client = new TcpClient();

client.ReceiveTimeout = 5000; // 5秒
client.SendTimeout = 5000;

client.Connect("127.0.0.1", 5000);

using NetworkStream stream = client.GetStream();
stream.ReadTimeout = 5000;
stream.WriteTimeout = 5000;

非同期処理では、CancellationTokenSourceを使ってタイムアウトやキャンセルを制御する方法がよく使われます。

C#
using CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));

await client.ConnectAsync("127.0.0.1", 5000, cts.Token);

5-5. using・Close・Disposeでリソースを正しく解放する方法

ソケット通信では、通信が終わったらリソースを解放する必要があります。解放しないと、ポートが使用中のまま残ったり、接続数が増え続けたりする原因になります。

基本はusingを使います。

C#
using TcpClient client = new TcpClient("127.0.0.1", 5000);
using NetworkStream stream = client.GetStream();

// 通信処理

TcpClientUdpClientIDisposableを実装しているため、usingを使うとスコープ終了時に自動で破棄されます。TcpClientClose()は接続を閉じ、関連リソースを解放するために使われます。

6. C#ソケット通信でよくあるエラーと対策

6-1. 接続できない原因と確認ポイント

TCPクライアントから接続できない場合、次の点を確認します。

  • サーバーアプリが起動しているか

  • サーバーが正しいポートで待ち受けているか

  • クライアントの接続先IPアドレスが正しいか

  • 127.0.0.1に接続していないか

  • ファイアウォールでブロックされていないか

  • サーバー側がIPAddress.Loopbackだけで待ち受けていないか

  • ルーターやVPNの影響がないか

  • ポート番号が他のアプリと競合していないか

別PCから接続したい場合、サーバー側をIPAddress.Anyで待ち受ける必要があるケースがあります。

C#
TcpListener listener = new TcpListener(IPAddress.Any, 5000);
listener.Start();

IPAddress.LoopbackはローカルPC内からの接続確認には便利ですが、別PCからの接続を受け付けたい場合には適しません。

6-2. ポートが使用中の場合の対処法

「ポートが既に使用されています」というエラーが出る場合、同じポートを別のアプリが使用している可能性があります。

Windowsでは、次のコマンドで使用中ポートを確認できます。

Bash
netstat -ano | findstr :5000

表示されたPIDを確認し、タスクマネージャーや次のコマンドでプロセスを特定します。

Bash
tasklist | findstr <PID>

対策は次のいずれかです。

  • 既存プロセスを終了する

  • C#アプリ側のポート番号を変更する

  • サーバー終了時にStop()Dispose()で確実に解放する

  • デバッグ中に複数起動していないか確認する

6-3. ファイアウォールやセキュリティソフトによる通信ブロック

localhostでは動くのに別PCから接続できない場合、Windows Defenderファイアウォールやセキュリティソフトがブロックしていることがあります。

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

  • サーバーPCの受信規則で対象ポートが許可されているか

  • プライベートネットワークとして認識されているか

  • 会社や学校のネットワークポリシーで遮断されていないか

  • セキュリティソフトがアプリの通信を止めていないか

開発中は一時的に許可ルールを作り、動作確認後に必要最小限の範囲へ絞ることが重要です。

6-4. 送信できない・受信できないときの確認ポイント

送信できない、または受信できない場合は、コードだけでなく通信設計を確認します。

  • サーバーとクライアントの起動順は正しいか

  • TCPなのにUDPで待ち受けていないか

  • ポート番号が一致しているか

  • 文字コードが一致しているか

  • 受信側がReadReceiveを呼んでいるか

  • 送信後にすぐ接続を閉じていないか

  • TCPのメッセージ区切りを設計しているか

  • バッファサイズより大きなデータを1回で読もうとしていないか

特にTCPでは、「送信できたのに受信側で途中までしか読めない」という問題が起きやすいです。この場合は、データ長を付ける、区切り文字を使う、必要なバイト数までループで読むなどの対策を行います。

6-5. SocketExceptionの代表的な原因と対処法

SocketExceptionは、ソケット関連のネットワークエラーが発生したときにスローされる例外です。SocketクラスやDnsクラスでネットワークエラーが発生した場合にも使われます。

代表的な原因は次のとおりです。

原因よくある状況対策
接続拒否サーバーが起動していないサーバー起動、IP・ポート確認
タイムアウト相手が応答しないタイムアウト設定、ネットワーク確認
アドレス使用中同じポートを使用中ポート変更、既存プロセス終了
ホスト不明DNS解決できないホスト名確認、IP指定
接続リセット相手が強制切断再接続処理、例外処理

SocketExceptionを捕捉するときは、SocketErrorCodeをログに出すと原因調査がしやすくなります。

C#
catch (SocketException ex)
{
Console.WriteLine($"SocketError: {ex.SocketErrorCode}");
Console.WriteLine($"ErrorCode: {ex.ErrorCode}");
Console.WriteLine($"Message: {ex.Message}");
}

6-6. タイムアウト・切断・例外処理の実装例

次のコードは、TCPクライアントでタイムアウトと切断を考慮する例です。

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

try
{
using TcpClient client = new TcpClient();
client.ReceiveTimeout = 5000;
client.SendTimeout = 5000;

client.Connect("127.0.0.1", 5000);

using NetworkStream stream = client.GetStream();
stream.ReadTimeout = 5000;
stream.WriteTimeout = 5000;

byte[] sendBytes = Encoding.UTF8.GetBytes("ping");
stream.Write(sendBytes, 0, sendBytes.Length);

byte[] buffer = new byte[1024];
int read = stream.Read(buffer, 0, buffer.Length);

if (read == 0)
{
Console.WriteLine("相手が正常に切断しました。");
return;
}

string response = Encoding.UTF8.GetString(buffer, 0, read);
Console.WriteLine($"受信: {response}");
}
catch (IOException ex)
{
Console.WriteLine($"入出力エラーまたはタイムアウト: {ex.Message}");
}
catch (SocketException ex)
{
Console.WriteLine($"ソケットエラー: {ex.SocketErrorCode} / {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"予期しないエラー: {ex.Message}");
}

Readの戻り値が0の場合、相手側が正常に接続を閉じたことを意味する場面があります。切断をエラーとして扱うのか、通常終了として扱うのかはアプリの仕様に合わせて決めます。

7. 非同期ソケット通信の基本

7-1. 同期処理と非同期処理の違い

同期処理は、通信処理が終わるまで次の処理に進みません。シンプルで理解しやすい一方、受信待ち中にアプリが止まったように見えることがあります。

非同期処理は、通信の完了を待っている間もスレッドを占有しにくく、複数クライアント対応やGUIアプリで有利です。C#ではasync/awaitを使うことで、非同期コードを比較的読みやすく書けます。

NetworkStreamでは、同期のReadWriteに加えて、非同期I/O用のReadAsyncWriteAsyncを利用できます。Microsoft Learnでも、非同期I/OにはTaskまたはValueTaskベースのReadAsyncWriteAsyncの使用が案内されています。

7-2. async/awaitを使うメリット

async/awaitを使うメリットは次のとおりです。

  • 接続待ちや受信待ちでスレッドを占有しにくい

  • 複数クライアントを同時に扱いやすい

  • GUIアプリで画面が固まりにくい

  • タイムアウトやキャンセル処理を組み込みやすい

  • 同期コードに近い読みやすさで書ける

特にサーバー側で複数クライアントを処理する場合、AcceptTcpClientAsyncReadAsyncを組み合わせると実装しやすくなります。

7-3. NetworkStreamで非同期送受信するサンプルコード

次のコードは、非同期TCPサーバーの簡易例です。

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

int port = 5000;

TcpListener listener = new TcpListener(IPAddress.Any, port);
listener.Start();

Console.WriteLine($"非同期TCPサーバー開始: {port}");

while (true)
{
TcpClient client = await listener.AcceptTcpClientAsync();
_ = HandleClientAsync(client);
}

static async Task HandleClientAsync(TcpClient client)
{
using (client)
using (NetworkStream stream = client.GetStream())
{
byte[] buffer = new byte[4096];

try
{
while (true)
{
int read = await stream.ReadAsync(buffer, 0, buffer.Length);

if (read == 0)
{
Console.WriteLine("クライアントが切断しました。");
break;
}

string message = Encoding.UTF8.GetString(buffer, 0, read);
Console.WriteLine($"受信: {message}");

string response = $"echo: {message}";
byte[] responseBytes = Encoding.UTF8.GetBytes(response);

await stream.WriteAsync(responseBytes, 0, responseBytes.Length);
}
}
catch (IOException ex)
{
Console.WriteLine($"通信エラー: {ex.Message}");
}
catch (SocketException ex)
{
Console.WriteLine($"ソケットエラー: {ex.SocketErrorCode}");
}
}
}

このサンプルでは、クライアント接続ごとにHandleClientAsyncを開始しています。実務では、同時接続数の上限、キャンセル処理、ログ、認証、入力検証などを追加します。

7-4. 複数クライアント対応で非同期処理が必要になるケース

同期処理で複数クライアントを扱うと、1つのクライアントの受信待ちで他のクライアント処理が止まることがあります。たとえばチャットサーバー、リアルタイム監視、複数端末からの計測データ収集では、同時に複数接続を処理する必要があります。

このような場合は、次の設計を検討します。

  • クライアントごとに非同期タスクを分ける

  • 接続一覧をスレッドセーフに管理する

  • 切断時に一覧から削除する

  • 送信失敗したクライアントを整理する

  • サーバー停止時にCancellationTokenで全処理を止める

7-5. 非同期通信で注意すべきキャンセル処理と例外処理

非同期処理では、サーバー停止や画面終了時に通信待ちをキャンセルできるようにしておくと安全です。

C#
using System.Net.Sockets;

using TcpClient client = new TcpClient();

using CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(5));

try
{
await client.ConnectAsync("127.0.0.1", 5000, cts.Token);
Console.WriteLine("接続成功");
}
catch (OperationCanceledException)
{
Console.WriteLine("接続がタイムアウトまたはキャンセルされました。");
}
catch (SocketException ex)
{
Console.WriteLine($"ソケットエラー: {ex.SocketErrorCode}");
}

キャンセル、タイムアウト、相手切断、ネットワーク断はすべて起こり得ます。try-catchで握りつぶすのではなく、ログに残し、再接続するのか、ユーザーへ通知するのかを設計しましょう。

8. 実践で使えるC#ソケット通信サンプル

8-1. TCPチャットアプリの簡易サンプル

次のサンプルは、複数クライアントから受け取ったメッセージを全クライアントへ配信する簡易TCPチャットサーバーです。

C#
using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
using System.Text;

int port = 5002;
TcpListener listener = new TcpListener(IPAddress.Any, port);
ConcurrentDictionary<TcpClient, NetworkStream> clients = new();

listener.Start();
Console.WriteLine($"チャットサーバー開始: {port}");

while (true)
{
TcpClient client = await listener.AcceptTcpClientAsync();
NetworkStream stream = client.GetStream();

clients.TryAdd(client, stream);
Console.WriteLine("クライアント参加");

_ = Task.Run(async () =>
{
byte[] buffer = new byte[4096];

try
{
while (true)
{
int read = await stream.ReadAsync(buffer, 0, buffer.Length);

if (read == 0)
{
break;
}

string message = Encoding.UTF8.GetString(buffer, 0, read);
string broadcastText = $"[{DateTime.Now:HH:mm:ss}] {message}";
byte[] data = Encoding.UTF8.GetBytes(broadcastText);

foreach (var pair in clients.ToArray())
{
try
{
await pair.Value.WriteAsync(data, 0, data.Length);
}
catch
{
clients.TryRemove(pair.Key, out _);
pair.Key.Dispose();
}
}
}
}
finally
{
clients.TryRemove(client, out _);
client.Dispose();
Console.WriteLine("クライアント退出");
}
});
}

簡易チャットクライアントは次のように書けます。

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

using TcpClient client = new TcpClient("127.0.0.1", 5002);
using NetworkStream stream = client.GetStream();

_ = Task.Run(async () =>
{
byte[] buffer = new byte[4096];

while (true)
{
int read = await stream.ReadAsync(buffer, 0, buffer.Length);

if (read == 0)
{
break;
}

string message = Encoding.UTF8.GetString(buffer, 0, read);
Console.WriteLine(message);
}
});

while (true)
{
string? input = Console.ReadLine();

if (string.IsNullOrWhiteSpace(input))
{
continue;
}

byte[] data = Encoding.UTF8.GetBytes(input);
await stream.WriteAsync(data, 0, data.Length);
}

このサンプルは学習用です。実務では、メッセージ境界、ユーザー名、認証、切断処理、送信キュー、ログ、暗号化を追加してください。

8-2. UDPブロードキャスト通信のサンプル

UDPブロードキャストを使うと、LAN内の複数端末へ一斉にメッセージを送信できます。

送信側です。

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

int port = 5003;

using UdpClient udp = new UdpClient();
udp.EnableBroadcast = true;

string message = "LAN内へのブロードキャスト通知";
byte[] data = Encoding.UTF8.GetBytes(message);

IPEndPoint broadcastEndPoint = new IPEndPoint(IPAddress.Broadcast, port);

await udp.SendAsync(data, data.Length, broadcastEndPoint);

Console.WriteLine("ブロードキャスト送信完了");

受信側です。

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

int port = 5003;

using UdpClient udp = new UdpClient(port);

Console.WriteLine($"UDPブロードキャスト受信待ち: {port}");

while (true)
{
UdpReceiveResult result = await udp.ReceiveAsync();

string message = Encoding.UTF8.GetString(result.Buffer);
Console.WriteLine($"{result.RemoteEndPoint} から受信: {message}");
}

ネットワーク環境によっては、ブロードキャストがルーターやファイアウォールで制限される場合があります。LAN内の探索や通知には便利ですが、インターネット越しの通信には向きません。

8-3. ローカル環境で動作確認する手順

C#ソケット通信を初めて試す場合は、まず同じPC内で確認するのが安全です。

  1. Visual Studioまたはdotnet new consoleでサーバー用プロジェクトを作成する

  2. サーバーコードを貼り付けて実行する

  3. 別のコンソールアプリとしてクライアント用プロジェクトを作成する

  4. 接続先を127.0.0.1、ポートをサーバーと同じ番号にする

  5. サーバーを先に起動する

  6. クライアントを起動する

  7. サーバー側とクライアント側のコンソール出力を確認する

ローカルで動かない場合、コード、ポート番号、起動順のどこかに問題がある可能性が高いです。別PC間の通信テストに進む前に、localhostで確実に成功させましょう。

8-4. 別PC間で通信テストする方法

別PC間でTCP通信を試す場合は、次の点を確認します。

  • サーバーPCとクライアントPCが同じLANにいるか

  • サーバーPCのIPアドレスを確認したか

  • サーバー側がIPAddress.Anyで待ち受けているか

  • クライアント側の接続先が127.0.0.1のままになっていないか

  • サーバーPCのファイアウォールで受信ポートが許可されているか

  • 使用ポートが他アプリと競合していないか

サーバーPCのIPアドレスは、Windowsなら次のコマンドで確認できます。

Bash
ipconfig

クライアント側では、たとえば次のようにサーバーPCのLAN内IPアドレスを指定します。

C#
using TcpClient client = new TcpClient("192.168.1.10", 5000);

127.0.0.1は自分自身を指すため、別PCのサーバーには接続できません。

8-5. Visual Studioでデバッグするときの確認ポイント

Visual StudioでC#ソケット通信をデバッグするときは、次の点を確認しましょう。

  • サーバーとクライアントを別プロセスで起動しているか

  • ブレークポイントで止めたままタイムアウトしていないか

  • 例外設定でSocketExceptionが中断対象になっていないか

  • コンソール出力にIP、ポート、受信バイト数を出しているか

  • Readの戻り値が0になっていないか

  • 送信データの文字コードが一致しているか

  • 複数起動によりポート競合していないか

通信処理では、ログ出力が非常に重要です。最低でも「接続開始」「接続成功」「受信バイト数」「送信バイト数」「切断」「例外内容」は出力しておくと、原因を追いやすくなります。

9. C#ソケット通信を安全に運用するための注意点

9-1. 平文通信のリスクと暗号化の必要性

ここまでのサンプルは学習用の平文通信です。平文通信では、ネットワーク上でデータを盗聴されると内容が読まれる可能性があります。ログイン情報、個人情報、業務データ、制御コマンドなどを扱う場合は、暗号化を検討する必要があります。

特に社外ネットワークやインターネット越しに通信する場合、独自TCP通信をそのまま公開するのは危険です。認証、暗号化、入力検証、アクセス制限、監査ログを組み合わせて設計しましょう。

9-2. SSL/TLSを使った安全な通信の考え方

C#でTCP通信を暗号化する場合は、NetworkStreamSslStreamでラップする方法があります。SslStreamはSSL/TLSを使うクライアント・サーバー通信のためのストリームで、サーバー認証や必要に応じたクライアント認証を行えます。

概念的には、次のような流れです。

C#
using System.Net.Security;
using System.Net.Sockets;

// TCP接続
TcpClient client = new TcpClient("example.com", 443);
NetworkStream networkStream = client.GetStream();

// TLS用ストリームでラップ
SslStream sslStream = new SslStream(networkStream);

// サーバー認証
await sslStream.AuthenticateAsClientAsync("example.com");

// 以降はsslStreamに対してRead/Writeする

実運用では、証明書の検証、ホスト名の一致、TLSバージョン、証明書更新、秘密鍵管理を正しく扱う必要があります。独自実装に不安がある場合は、HTTPSやgRPCなど既存の安全な通信基盤を使うことも検討してください。

9-3. 入力データ検証と不正アクセス対策

ソケット通信では、相手が必ず正しいデータを送ってくるとは限りません。想定外のデータ、不正な長さ、巨大なペイロード、文字コード不正、連続接続などに備える必要があります。

対策例は次のとおりです。

  • 最大受信サイズを決める

  • メッセージ形式を厳密に検証する

  • 不正なデータは破棄する

  • 認証前の操作を制限する

  • 接続元IPを制限する

  • 一定時間無通信なら切断する

  • 連続エラーが多いクライアントを切断する

  • 例外内容をそのまま相手へ返さない

ソケット通信は自由度が高い反面、HTTPフレームワークが自動で行ってくれる保護を自分で設計する必要があります。

9-4. 通信ログを残して障害調査しやすくする方法

実務でC#ソケット通信を運用するなら、通信ログは必須です。最低限、次の情報を残すと障害調査がしやすくなります。

  • 接続日時

  • 切断日時

  • 接続元IPアドレスとポート

  • 送受信バイト数

  • 処理したコマンド名

  • エラー内容

  • タイムアウト発生有無

  • 処理時間

ただし、パスワードや個人情報をログにそのまま出すのは避けてください。ログは障害調査に役立つ一方、漏洩時のリスクにもなります。必要な情報だけを安全に記録しましょう。

10. C#ソケット通信に関するよくある質問

10-1. C#でSocketクラスとTcpClientはどちらを使うべき?

初心者や一般的なTCP通信なら、まずTcpClientTcpListenerを使うのがおすすめです。NetworkStreamで読み書きできるため、コードが比較的シンプルになります。

一方、細かいソケットオプションを制御したい、TCP以外も含めて低レベルに扱いたい、大量接続を高性能に処理したい場合はSocketクラスを検討します。Socketクラスは柔軟ですが、実装難易度は上がります。

10-2. TCPとUDPはどちらが初心者向け?

初心者にはTCPがおすすめです。理由は、接続、送信、受信、切断という流れが理解しやすく、信頼性も高いためです。

UDPはコード自体は短く書けますが、パケットロス、順序ズレ、再送、到達確認などを自分で考える必要があります。まずTCPでC#ソケット通信の基本を学び、その後にUDPを学ぶと理解しやすくなります。

10-3. localhostでは動くのに別PCから接続できない原因は?

よくある原因は、接続先に127.0.0.1を指定していることです。127.0.0.1はクライアントPC自身を指します。別PCのサーバーに接続する場合は、サーバーPCのLAN内IPアドレスを指定します。

また、サーバー側がIPAddress.Loopbackで待ち受けていると、外部PCから接続できない場合があります。別PCから接続するテストでは、サーバー側をIPAddress.Anyで待ち受けることを検討します。

C#
TcpListener listener = new TcpListener(IPAddress.Any, 5000);

加えて、Windowsファイアウォール、セキュリティソフト、ポート番号、ネットワーク種別も確認してください。

10-4. ポート番号は何番を使えばよい?

学習用や社内ツールでは、他のサービスと競合しにくい番号を選びます。たとえばサンプルでは500050015002のような番号を使っています。

ただし、既存サービスが使用しているポート番号は避けてください。実運用では、システム内で使用ポートを管理し、設定ファイルで変更できるようにしておくと安全です。ポート番号を固定でコードに埋め込むより、アプリ設定として外出しするのがおすすめです。

10-5. ソケット通信とHTTP通信の違いは?

HTTP通信は、TCPの上で動くアプリケーション層のプロトコルです。リクエストとレスポンス、ヘッダー、ステータスコードなどのルールが決まっており、Web APIやブラウザ通信で広く使われています。

一方、ソケット通信はより低レベルで、データ形式や送受信タイミングを自由に設計できます。その分、メッセージ区切り、エラー処理、認証、暗号化、再接続などを自分で実装する必要があります。

一般的なWebシステムならHTTP/HTTPSが適しています。独自プロトコル、リアルタイム通信、機器連携、LAN内ツールなどでは、C#ソケット通信が有効な選択肢になります。

まとめ

C#ソケット通信を理解するには、まずTCPとUDPの違いを押さえることが重要です。TCPは接続型で信頼性が高く、チャット、ファイル送信、業務通信などに向いています。UDPは非接続型で軽量・高速ですが、パケットロスや順序ズレを考慮する必要があります。

C#では、TCPサーバーにTcpListener、TCPクライアントにTcpClient、UDP通信にUdpClientを使うと、初心者でも比較的わかりやすく実装できます。より細かい制御が必要な場合はSocketクラスを使います。

実装時は、次のポイントを意識しましょう。

  • TCPでは1回のReadで1メッセージとは限らない

  • 文字列送受信では文字コードを統一する

  • タイムアウトを設定して待ち続けを防ぐ

  • usingDisposeでリソースを解放する

  • SocketExceptionの内容をログに残す

  • 複数クライアント対応では非同期処理を使う

  • 実運用では暗号化、認証、入力検証を行う

まずはlocalhostでTCPサーバーとTCPクライアントを動かし、次にUDP、非同期処理、別PC間通信へ進むと、C#ソケット通信の理解がスムーズになります。