C#における低レベルネットワーク通信の実装:TCPとUDPのSocketプログラミング

Socketは、オペレーティングシステムが提供するネットワーク通信の基本的な抽象化インターフェースです。Unix系OSではファイル記述子として扱われるこの機構は、Windowsおよび.NET環境でも同様に、ネットワークI/Oをカプセル化した柔軟なアクセス手段として機能します。C#ではSystem.Net.Sockets名前空間を通じて、IPアドレス、ポート、プロトコルなどのネットワーク要素を直接制御可能なSocketクラスが提供されます。

通信モデルの違いを理解する

TCP(Transmission Control Protocol)は接続指向型で、信頼性・順序保証・フロー制御を内包します。これに対しUDP(User Datagram Protocol)は接続不要・軽量・非信頼型であり、アプリケーション側で再送やタイムアウト処理を実装する必要があります。選択は用途に応じて行うべきで、リアルタイム音声・ゲーム状態同期などではUDPの低遅延特性が優位ですが、ファイル転送やHTTP通信にはTCPが適しています。

基本的な通信フロー

  • TCPサーバー:ソケット作成 → ローカルエンドポイントへのバインド → Listen()による接続待機 → Accept()でクライアントとの接続確立 → 双方向データ交換 → クローズ
  • TCPクライアント:ソケット作成 → Connect()でサーバーへ接続要求 → 接続確立後の双方向通信 → クローズ
  • UDPエンドポイント:ソケット作成 → バインド(受信側)または未バインド(送信側)→ SendTo()/ReceiveFrom()で相手のアドレスを明示的に指定して通信

実装例:TCPサーバー(非同期対応設計)

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

public class TcpEchoServer
{
    private readonly Socket _listener;
    private readonly int _port = 8080;

    public TcpEchoServer()
    {
        _listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        var localEndpoint = new IPEndPoint(IPAddress.Any, _port);
        _listener.Bind(localEndpoint);
        _listener.Listen(5);
        Console.WriteLine($"TCP Echo Server started on port {_port}");
    }

    public async Task StartAsync()
    {
        while (true)
        {
            try
            {
                var clientSocket = await AcceptClientAsync();
                _ = HandleClientAsync(clientSocket); // デタッチされたタスク
            }
            catch (ObjectDisposedException) { break; }
            catch (Exception ex) { Console.WriteLine($"Accept error: {ex.Message}"); }
        }
    }

    private async Task<Socket> AcceptClientAsync()
    {
        var acceptTask = Task.Factory.FromAsync<Socket>(
            _listener.BeginAccept, _listener.EndAccept, null);
        return await acceptTask;
    }

    private async Task HandleClientAsync(Socket client)
    {
        var remoteEp = client.RemoteEndPoint;
        Console.WriteLine($"Client connected: {remoteEp}");

        try
        {
            var welcomeMsg = "Echo server ready. Send messages (empty to exit).";
            await SendAsync(client, welcomeMsg);

            var buffer = new byte[1024];
            while (true)
            {
                var received = await ReceiveAsync(client, buffer);
                if (received == 0) break;

                var message = Encoding.UTF8.GetString(buffer, 0, received).Trim();
                Console.WriteLine($"Received from {remoteEp}: \"{message}\"");

                if (string.IsNullOrEmpty(message)) break;

                await SendAsync(client, $"Echo: {message}");
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Client {remoteEp} error: {ex.Message}");
        }
        finally
        {
            client.Shutdown(SocketShutdown.Both);
            client.Close();
            Console.WriteLine($"Client {remoteEp} disconnected");
        }
    }

    private async Task SendAsync(Socket socket, string data)
    {
        var bytes = Encoding.UTF8.GetBytes(data + "\n");
        await Task.Factory.FromAsync(
            (cb, s) => socket.BeginSend(bytes, 0, bytes.Length, SocketFlags.None, cb, s),
            socket.EndSend, null);
    }

    private async Task<int> ReceiveAsync(Socket socket, byte[] buffer)
    {
        return await Task.Factory.FromAsync<int>(
            (cb, s) => socket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, cb, s),
            socket.EndReceive, null);
    }
}

実装例:TCPクライアント(簡易インタラクティブ版)

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

public class TcpEchoClient
{
    private readonly string _host;
    private readonly int _port;
    private Socket? _socket;

    public TcpEchoClient(string host = "127.0.0.1", int port = 8080)
    {
        _host = host;
        _port = port;
    }

    public async Task RunAsync()
    {
        try
        {
            var ipHost = await Dns.GetHostAddressesAsync(_host);
            var endpoint = new IPEndPoint(ipHost[0], _port);
            _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            
            await ConnectAsync(endpoint);
            Console.WriteLine("Connected. Type messages (or 'quit' to exit):");

            while (true)
            {
                Console.Write("> ");
                var input = Console.ReadLine();
                if (input?.ToLower() == "quit") break;

                if (!string.IsNullOrWhiteSpace(input))
                {
                    await SendAsync(input);
                    var response = await ReceiveAsync();
                    Console.WriteLine($"← {response}");
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Connection failed: {ex.Message}");
        }
        finally
        {
            _socket?.Shutdown(SocketShutdown.Both);
            _socket?.Close();
        }
    }

    private async Task ConnectAsync(IPEndPoint endpoint)
    {
        await Task.Factory.FromAsync(
            (cb, s) => _socket!.BeginConnect(endpoint, cb, s),
            _socket!.EndConnect, null);
    }

    private async Task SendAsync(string message)
    {
        var data = Encoding.UTF8.GetBytes(message + "\n");
        await Task.Factory.FromAsync(
            (cb, s) => _socket!.BeginSend(data, 0, data.Length, SocketFlags.None, cb, s),
            _socket!.EndSend, null);
    }

    private async Task<string> ReceiveAsync()
    {
        var buffer = new byte[1024];
        var len = await Task.Factory.FromAsync<int>(
            (cb, s) => _socket!.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, cb, s),
            _socket!.EndReceive, null);
        return Encoding.UTF8.GetString(buffer, 0, len).Trim();
    }
}

UDP双方向通信の実装(シングルスレッド方式)

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

public class UdpEchoNode
{
    private readonly Socket _socket;
    private readonly IPEndPoint _localEndpoint;
    private EndPoint _remoteEndpoint;

    public UdpEchoNode(int port = 9050)
    {
        _localEndpoint = new IPEndPoint(IPAddress.Any, port);
        _socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
        _socket.Bind(_localEndpoint);
        _remoteEndpoint = new IPEndPoint(IPAddress.Any, 0);
        Console.WriteLine($"UDP node listening on {_localEndpoint}");
    }

    public async Task StartAsync()
    {
        // 初期挨拶受信
        var buffer = new byte[1024];
        var recv = await ReceiveFromAsync(buffer);
        Console.WriteLine($"First message from {_remoteEndpoint}: {Encoding.UTF8.GetString(buffer, 0, recv)}");

        // 応答送信
        var reply = "Hello! UDP echo node is active.";
        await SendToAsync(Encoding.UTF8.GetBytes(reply));

        // メッセージループ
        while (true)
        {
            recv = await ReceiveFromAsync(buffer);
            var msg = Encoding.UTF8.GetString(buffer, 0, recv).Trim();
            Console.WriteLine($"Received: \"{msg}\"");

            if (msg.Equals("quit", StringComparison.OrdinalIgnoreCase)) break;

            await SendToAsync(Encoding.UTF8.GetBytes($"Echo: {msg}"));
        }
    }

    private async Task<int> ReceiveFromAsync(byte[] buffer)
    {
        return await Task.Factory.FromAsync<int>(
            (cb, s) => _socket.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref _remoteEndpoint, cb, s),
            (ar) => _socket.EndReceiveFrom(ar, ref _remoteEndpoint), null);
    }

    private async Task SendToAsync(byte[] data)
    {
        await Task.Factory.FromAsync(
            (cb, s) => _socket.BeginSendTo(data, 0, data.Length, SocketFlags.None, _remoteEndpoint, cb, s),
            _socket.EndSendTo, null);
    }
}

上記の実装は、.NETの非同期I/Oパターン(APM)を用いてブロッキングを回避し、リソース効率を高めています。実際の運用では、async/awaitSocketAsyncEventArgsを組み合わせた高性能I/Oモデルや、TcpClient/UdpClientなどの高水準ラッパークラスも検討できますが、ネットワークの本質的理解には、低レベルSocket操作の習熟が不可欠です。

タグ: DotNet csharp socket TCP udp

6月1日 16:57 投稿