.NETでTCPを使って通信する(TcpClientとTcpListenerを使う)

.NETで通信するプログラムに興味が出てきたので調べたりしています。ネットワークプログラミングと言うんですかね。まずはやっぱりTCPかなと思うので、TCPを使って通信する簡単なクライアントとサーバのサンプルコードを書いてみました。

.NETでTCPを使って通信するにはTcpClientとTcpListenerを使います。

より細かく制御できるSocketを使う方法もあるようです。いい勉強になりそうだとは思うのですがなかなか難しそうなのでソケットはまた別で試したいと思います。というフラグ立て。

クライアント

クライアントもサーバもHTTPをイメージしてリクエストとレスポンスのメッセージをやりとりする動きにしました。まずはクライアントの動きです。

クライアントの動き

  1. サーバに接続
  2. サーバにリクエストを送信する
  3. サーバからレスポンスを受信する

クライアントのサンプルコード

上記の動きをするクライアントのサンプルコードです。クライアント側ではTcpClientを使います。エンドポイントとはIPアドレスとポート番号のセットのことです。

// クライアント
public class Client<TRequest, TResponse> {
    // 接続先のエンドポイント
    private readonly IPEndPoint _endpoint;

    public Client(IPEndPoint endpoint) {
        _endpoint = endpoint;
    }

    // サーバにリクエストを送信してレスポンスを受信する
    public async Task<TResponse> Send(TRequest request) {
        using (var client = new TcpClient()) {
            // 1. サーバに接続
            await client.ConnectAsync(_endpoint.Address, _endpoint.Port);

            using (var stream = client.GetStream()) {
                // 2. サーバにリクエストを送信する
                Console.WriteLine($"Client send: {request}");
                stream.WriteObject(request);

                // 3. サーバからレスポンスを受信する
                var response = stream.ReadObject<TResponse>();
                Console.WriteLine($"Client received: {response}");

                return response;
            }
        }
    }
}

Stream.WriteObject、Stream.ReadObjectは自作した拡張メソッドです。WriteObjectはオブジェクトをバイト配列に変換してストリームに書き込みます。ReadObjectはストリームから読み込んだバイト配列をオブジェクトとして取得します。

サーバ

続いてサーバです。クライアントと比べるとちょっと複雑です。

サーバの動き

  1. クライアントからの接続を待つ
  2. クライアントからの接続を受け入れる
  3. クライアントからリクエストを受信する
  4. リクエストを処理してレスポンスを作る
  5. クライアントにレスポンスを送信する

3から5は複数のクライアントからのリクエストを並列に処理できるようにしています。

サーバのサンプルコード

サーバではまずTcpListenerを使ってクライアントからの接続を待ちます。クライアントから接続されると今度はTcpClientを使ってメッセージを読み書きすると言った感じになります。

// サーバ
public class Server<TRequest, TResponse> {
    // 接続を待つエンドポイント
    private readonly IPEndPoint _endpoint;

    // リクエストからレスポンスを作成する処理
    private readonly Func<TRequest, TResponse> _processor;

    // TCPリスナー
    private readonly TcpListener _listener;

    public Server(IPEndPoint endpoint, Func<TRequest, TResponse> processor) {
        _endpoint = endpoint;
        _processor = processor;
        _listener = new TcpListener(_endpoint);
    }

    // クライアントからリクエストを受信してレスポンスを送信する
    private void Receive(TcpClient client) {
        using (client)
        using (var stream = client.GetStream()) {
            // 3. クライアントからリクエストを受信する
            var request = stream.ReadObject<TRequest>();

            // 4. リクエストを処理してレスポンスを作る
            var response = _processor(request);

            // 5. クライアントにレスポンスを送信する
            stream.WriteObject(response);
        }
    }

    // 接続を待つ
    public async Task Listen() {
        Console.WriteLine($"Server listen:");
        // 1. クライアントからの接続を待つ
        _listener.Start();

        while (true) {
            // 2. クライアントからの接続を受け入れる
            var client = await _listener.AcceptTcpClientAsync();
            Console.WriteLine($"Server accepted:");

            var task = Task.Run(() => Receive(client));

            // Taskの管理やエラー処理は省略
        }
    }

    // 終了する
    public void Close() {
        _listener.Stop();
    }
}

なるべく簡単なサンプルにするためにエラー処理やTaskの管理など省いていますが、ベースとしてはこんな感じかなと。実際作り込むには色々考えることがありそうに思ったり。

クライアントとサーバを実行してみる

サーバを動かしてクライアントも実行してみます。

class Program {
    static void Main(string[] args) {
        // サーバが接続を待つエンドポイント
        // であり
        // クライアントが接続するサーバのエンドポイント
        var endpoint = new IPEndPoint(IPAddress.Loopback, 54321);

        // サーバ
        var server = new Server<Message, Message>(
            endpoint,
            // リクエストからレスポンスを作る処理
            request => new Message {
                Id = request.Id,
                // メッセージの文字列を逆順にする
                Content = new string(request.Content.Reverse().ToArray()),
            });
        // 接続を待機
        var task = Task.Run(() => server.Listen());

        // クライアント
        Task.WaitAll(
            // リクエストを送信してレスポンスを受信
            new Client<Message, Message>(endpoint).Send(new Message { Id = 10, Content = "あいうえお" }),
            new Client<Message, Message>(endpoint).Send(new Message { Id = 20, Content = "かきくけこ" }),
            new Client<Message, Message>(endpoint).Send(new Message { Id = 30, Content = "さしすせそ" })
        );

        // サーバを終了
        server.Close();
        // サーバの終了処理、Taskの管理、エラー処理あたりが微妙
    }
}

// 実行結果(実行するたびに変わる)
/*
Server listen:
Server accepted:
Server accepted:
Client send: { Id = 10, Content = "あいうえお" }
Client send: { Id = 30, Content = "さしすせそ" }
Server accepted:
Client send: { Id = 20, Content = "かきくけこ" }
Client received: { Id = 30, Content = "そせすしさ" }
Client received: { Id = 10, Content = "おえういあ" }
Client received: { Id = 20, Content = "こけくきか" }
*/

実行結果やデバッグ実行で分かりやすくするために、同じプロセス内でクライアントもサーバも動かしています。あとサーバ(リスナー)の終了の仕方とかTaskの管理とか微妙かもです。

ソース全体はこちらに。