なべひろBlog

プログラミングをメインに仕事に関するアレコレを発信しています。

C#でSystem.Net.SocketsのxxxAsyncを使ったTCPサーバを作ってみる

以前C#の非同期TCPサーバのサンプルを作った時はBeginxxxメソッドを使用したサンプルコードを作成しましたが、今回はxxxAsyncを使ったTCPサーバを作成してみます。
前回は
きっかけは調べ物をしていた時に偶然にxxxAsyncメソッドを見つけ、何が違うのか調べたらxxxAsyncの方が良いようなコメント(英文)が幾つか見つかった
ので近々に使用する事はないが、突然使う事になった時のために覚えておこうと思ったからです。
今回は参考になる日本語の情報も少なく間違えがあるかもしれませんので間違えがありましたらご指摘お願いします。
サンプルソースはこちらになります。
プログラムの全体像としては接続待ちのAcceptAsyncメソッドをWhileループで常に接続待ちとさせメソッドが実行されたら送受信するクライアント毎にインスタンスを生成し、そこでクライアントとのやり取りを行います。
TCPクライアントから0バイトのデータ受信または任意に切断したらSocketAsyncEventArgsを破棄して終了となります。
SocketAsyncEventArgs
このクラスを用いて非同期通信を行います。
インスタンスのイベントハンドラにイベント発生時のメソッドを定義したり送受信バッファの設定を行います。
1つのTCPクライアントに1つのSocketAsyncEventArgsインスタンスが割り当てられます。
終了時には破棄する必要がありますので相手から切断された時や自分で切断した時は破棄を忘れないでください。
AcceptAsync
TCPクライアントの接続待ちを非同期で行います。
非同期とは言え戻り値がfalseの場合、同期的にクライアント接続が行われるので戻り値によって少し処理を変える必要があります。
SocketAsyncEventArgs args = new();
args.Completed += new EventHandler<SocketAsyncEventArgs>>(ConnectCompleted);
if (Sock.AcceptAsync(args) == true)
{
    // 非同期処理が完了するまで待機
    AllDone.Wait();
}
else
{
    // 同期的に完了した場合はこちらで接続してきたクライアントに対する処理
}
非同期で処理された場合Completedイベントに加えられたイベントハンドラが実行されます。
上記サンプルでは非同期で処理が行こなわれたらManualResetEventSlimがSetされるまで待ちます。
イベントハンドラは
private void ConnectCompleted(object sender, SocketAsyncEventArgs e)
{
    // e.LastOperationでイベント種類が判別できる
    if (e.LastOperation == SocketAsyncOperation.Accept)
    {
        AllDone.Set();
        // 非同期に完了した場合はこちらで接続してきたクライアントに対する処理
    }
}
LastOperationを参照する事でどのようなイベントが発生したかを判断できます。
ReceiveAsync
非同期受信です。
これもAcceptAsyncと同様に戻り値がtrueの場合は非同期に、falseの場合は同期的に処理が行われた事を示します。
但し、非同期であれ同期的であれ同じ入れ物に受信データが入るので戻り値がtrueの場合、イベントハンドラでManualResetEventSlimがSetされるのを待つだけで大丈夫です。
// 受信データの入れ物
var receiveBuffer = new byte[1024];
// イベントハンドラの設定
args.Completed += new EventHandler<SocketAsyncEventArgs>(completed);
// 受信データの入れ物を定義 (SocketAsyncEventArgs)
args.SetBuffer(receiveBuffer, 0, bufferSize);
// イベントハンドラでSetされるManualResetEventSlimは念の為リセット
receiveDone.Reset();
// データ受信待ち
if (args.AcceptSocket.ReceiveAsync(args) == true)
{
    receiveDone.Wait();
}
データを受信すると「args.BytesTransferred」に受信したデータ数を表します。
これはBeginReceiveと同様ですが受信データ量が0ならクライアントが切断したと判断できます。
イベントハンドラは
private void completed(object sender, SocketAsyncEventArgs e)
{
    // e.LastOperationでイベント種類が判別できる
    switch (e.LastOperation)
    {
        // 受信イベント
        case SocketAsyncOperation.Receive:
            receiveDone.Set();
            break;
        // 送信イベント
        case SocketAsyncOperation.Send:
            sendsDone.Set();
            break;
    }
}
LastOperationでセットするManualResetEventSlimを変えてますが、上手くやれば1つでもいいかもしれません。
SendAsync
非同期送信です。
これも同様に戻り値がfalseなら同期的に処理が完了、trueなら非同期に処理が完了した事を示します。
// 予めsendBufferを用意して送信するデータを入れておく
args.SetBuffer(sendBuffer, 0, sendBuffer.Length);
// イベントハンドラでSetされるManualResetEventSlimは念の為リセット
sendsDone.Reset();
// データ送信待ち
if (args.AcceptSocket.SendAsync(args) == true)
{
    sendsDone.Wait();
}
これもReceiveAsyncと同じ考えで戻り値がfalseでもtrueでも特段処理は変わらすtrueならManualResetEventSlimがSetされるのを待つだけです。
ちなみに任意の切断は
args.AcceptSocket.Shutdown(SocketShutdown.Both);
args.AcceptSocket.Close();
で可能です。
関連記事