.NET における TCP ソケット通信の基本設計と実装パターン

伝送層プロトコルの特性比較

ネットワークアプリケーション間のデータ交換には主に二つの伝送層プロトコルが利用されます。

TCP(Transmission Control Protocol)

接続志向の信頼性重視プロトコルです。シーケンス番号と確認応答(ACK)によりデータの完全性と順番を保証し、輻輳制御機能も備えています。ファイル転送やリモート操作など、欠落なく正確な通信が要求される場面向きの設計ですが、オーバーヘッドがあるため単発のストリーミング送信には適さないとされています。

UDP(User Datagram Protocol)

非接続型の軽量プロトコルです。ヘッダ情報の最小化と再送処理の省略により、低遅延でのデータ搬送が可能です。代わりに信頼性は担保されず、パケットの消失や順序入れ替わりが発生する可能性があります。リアルタイム音声・映像配信やゲームのステート同期など、速度優先の通信基盤として広く採用されています。

ソケット通信のアーキテクチャ

ソケット(Socket)は、OSのネットワークスタックに対して抽象化されたインターフェースであり、ホスト上のプロセス間通信を実現するエンドポイントです。典型的なクライアント/サーバーモデルでは、サーバー側が特定のポートに対して「バインド」を行い「リスン」状態で待機します。クライアントは対象のIPアドレスとポートを指定して接続ハンドシェイクを実施し、完了すると双方向のバイトストリーム通信が可能になります。IPアドレスは物理ネットワーク内のマシンの所在を特定し、ポート番号はそのマシン内で実際にデータを扱うアプリケーションプロセスを区別する役割を担います。

C# によるクライアント/サーバー実装例

以下のコードは、.NET の `System.Net.Sockets` 名前空間を使用したWinFormsベースの実装パターンです。元の構造をリファクタリングし、エンコーディングをUTF-8に変更、バッファ処理の最適化、およびスレッド管理をモジュール化しています。UI スレッドブロックを防ぐために `Task` と `Invoke` による安全な跨ぎ呼び出しを維持しつつ、変数名と制御フローを刷新しています。

クライアント実装

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace TcpChatClient
{
    public partial class ConnectionPanel : Form
    {
        private Socket _currentSession;
        private static readonly Encoding _charset = Encoding.UTF8;
        private bool _sessionActive;

        public ConnectionPanel()
        {
            InitializeComponent();
            PopulateLocalEndpoints();
        }

        private void PopulateLocalEndpoints()
        {
            var hostInfo = Dns.GetHostEntry(Dns.GetHostName());
            foreach (var addr in hostInfo.AddressList)
            {
                if (addr.AddressFamily == AddressFamily.InterNetwork)
                    addressSelector.Items.Add(addr);
            }
            addressSelector.Items.Add(IPAddress.Loopback);
            addressSelector.SelectedIndex = 0;
        }

        private async void InitiateConnection(object sender, EventArgs e)
        {
            if (_currentSession != null || _sessionActive) return;

            try
            {
                _currentSession = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                var targetIp = IPAddress.Parse(addressSelector.SelectedItem.ToString());
                var targetPort = Convert.ToInt32(portSelector.Value);
                var endpoint = new IPEndPoint(targetIp, targetPort);

                await Task.Run(() => _currentSession.Connect(endpoint));
                _sessionActive = true;
                ShowNotification("接続確立完了");
                _ = Task.Run(MonitorDataStream);
            }
            catch (SocketException ex)
            {
                ShowNotification($"接続拒否: {ex.SocketErrorCode}");
            }
        }

        private async void TerminateSession(object sender, EventArgs e)
        {
            if (_currentSession?.Connected ?? false)
            {
                _currentSession.Shutdown(SocketShutdown.Both);
                _currentSession.Close();
            }
            _sessionActive = false;
            ShowNotification("切断しました");
        }

        private async void DispatchMessage(object sender, EventArgs e)
        {
            if (!(_currentSession?.Connected ?? false)) return;

            var payload = _charset.GetBytes(messageInput.Text);
            try
            {
                await Task.Run(() => _currentSession.Send(payload));
                messageInput.Clear();
            }
            catch (Exception err)
            {
                ShowNotification($"送信エラー: {err.Message}");
            }
        }

        private void MonitorDataStream()
        {
            var readBuffer = new byte[4096];
            while (_sessionActive && _currentSession?.Connected == true)
            {
                if (_currentSession.Poll(500, SelectMode.SelectRead))
                {
                    int receivedBytes = 0;
                    try
                    {
                        receivedBytes = _currentSession.Receive(readBuffer);
                    }
                    catch
                    {
                        break;
                    }

                    if (receivedBytes > 0)
                    {
                        var decodedText = _charset.GetString(readBuffer, 0, receivedBytes);
                        this.Invoke(new Action(() => logDisplay.AppendText($"[{DateTime.Now:HH:mm:ss}] {decodedText}\r\n")));
                    }
                    else
                    {
                        break;
                    }
                }
                Task.Delay(100).Wait();
            }

            this.Invoke(new Action(() => ShowNotification("ピアとの接続が切断されました")));
            _sessionActive = false;
        }
    }
}

サーバー実装

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace TcpChatServer
{
    public partial class ServerControlPanel : Form
    {
        private Socket _listener;
        private readonly List<Socket> _activePeers = new List<Socket>();
        private CancellationTokenSource _shutdownToken;
        private static readonly Encoding _charset = Encoding.UTF8;

        public ServerControlPanel()
        {
            InitializeComponent();
            PopulateLocalEndpoints();
        }

        private void PopulateLocalEndpoints()
        {
            var hostInfo = Dns.GetHostEntry(Dns.GetHostName());
            foreach (var addr in hostInfo.AddressList)
            {
                if (addr.AddressFamily == AddressFamily.InterNetwork)
                    serverAddressSelector.Items.Add(addr);
            }
            serverAddressSelector.Items.Add(IPAddress.Any);
            serverAddressSelector.SelectedIndex = 0;
        }

        private async void LaunchServer(object sender, EventArgs e)
        {
            if (_listener != null) return;

            try
            {
                _listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                var bindAddress = IPAddress.Parse(serverAddressSelector.SelectedItem.ToString());
                var bindPort = Convert.ToInt32(bindPortSelector.Value);
                var localEndpoint = new IPEndPoint(bindAddress, bindPort);

                _listener.Bind(localEndpoint);
                _listener.Listen(128);
                _shutdownToken = new CancellationTokenSource();

                ShowNotification("サービス開始完了");
                _ = Task.Run(AcceptIncomingPeers, _shutdownToken.Token);
                _ = Task.Run(BroadcastInboundPayloads, _shutdownToken.Token);
            }
            catch (Exception ex)
            {
                ShowNotification($"起動失敗: {ex.Message}");
            }
        }

        private void AcceptIncomingPeers(CancellationToken token)
        {
            try
            {
                while (!token.IsCancellationRequested)
                {
                    var peerSocket = _listener.Accept();
                    lock (_activePeers) _activePeers.Add(peerSocket);

                    var peerInfo = (IPEndPoint)peerSocket.RemoteEndPoint;
                    UpdateClientList(peerInfo.Address.ToString(), peerInfo.Port);
                    HandlePeerJoinEvent(peerSocket, peerInfo);
                }
            }
            catch (OperationCanceledException) { }
            catch (Exception err) { ShowNotification(err.Message); }
        }

        private void BroadcastInboundPayloads(CancellationToken token)
        {
            var buffer = new byte[4096];
            while (!token.IsCancellationRequested)
            {
                lock (_activePeers)
                {
                    for (int i = _activePeers.Count - 1; i >= 0; i--)
                    {
                        var client = _activePeers[i];
                        if (client.Poll(1, SelectMode.SelectRead))
                        {
                            if (client.Available == 0)
                            {
                                CleanupDisconnectedPeer(i, client);
                                continue;
                            }

                            try
                            {
                                int bytesIn = client.Receive(buffer);
                                if (bytesIn > 0)
                                {
                                    var msg = _charset.GetString(buffer, 0, bytesIn);
                                    var remote = (IPEndPoint)client.RemoteEndPoint;
                                    LogOutboundActivity(remote.Address.ToString(), msg);
                                }
                            }
                            catch { CleanupDisconnectedPeer(i, client); }
                        }
                    }
                }
                Task.Delay(50).Wait();
            }
        }

        private void CleanupDisconnectedPeer(int index, Socket socket)
        {
            lock (_activePeers)
            {
                _activePeers.RemoveAt(index);
            }
            socket.Close();
            this.Invoke(new Action(() => RemoveFromGrid(index)));
        }

        private void DispatchBroadcastOrDirect(object sender, EventArgs e)
        {
            if (_activePeers.Count == 0) return;

            var payload = _charset.GetBytes(outgoingText.Text);
            bool isBroadcast = wideTransmissionCheckbox.Checked;
            int selectedIndex = peerGrid.CurrentRow?.Index ?? -1;

            if (isBroadcast)
            {
                lock (_activePeers)
                {
                    foreach (var peer in _activePeers)
                    {
                        try { peer.Send(payload); } catch { }
                    }
                }
            }
            else if (selectedIndex >= 0)
            {
                Socket target;
                lock (_activePeers) target = _activePeers[selectedIndex];
                try { target.Send(payload); } catch { }
            }
        }

        private void StopService(object sender, EventArgs e)
        {
            _shutdownToken?.Cancel();
            lock (_activePeers) _activePeers.ForEach(p => p.Close());
            _activePeers.Clear();
            _listener?.Close();
            _listener = null;
            this.Invoke(new Action(() => ClearGrid()));
            ShowNotification("シャットダウン実行済み");
        }
    }
}

実装上のポイント

ソケット通信では、受信データが複数回の分割読み取りや、単一の読み取りで複数のメッセージが混在する「タイディング(message boundary)」問題が発生します。今回の実装ではデバッグ用途のため連続文字列をそのまま出力していますが、生産環境ではヘッダーサイズプレフィックス方式やJSON/XMLのタグ区切りを導入する必要があります。また、`Poll` メソッドによるステータス確認は同期的なウェイトを伴うため、高負荷時には `TaskCompletionSource` と `SocketAsyncEventArgs` を併用した非同期I/Oパターンへの移行が推奨されます。接続管理においては、サーバー側でクライアントリストのスレッドセーフなロック処理を明示的に記述しており、並列受信ループによるデータ競合を回避する構成となっています。

タグ: csharp System.Net.Sockets TCP WinForms network-io

5月13日 04:29 投稿