中堅プログラマーの備忘録

忘れっぽくなってきたので備忘録として・・・

【c#.net】asyn/awaitについてTcpClientクラスを使って非同期処理を検証する

1.概要

C#5.0から追加になった非同期処理【async/await】について
使いにくい【TcpClient】クラスをあえて使って検証してみます。

様々な機能を持ったアプリケーションでは非同期処理はマストかと思います。
応答待ちでデッドロックなんて目も当てられません・・・

2.基本的な使い方

まずは【async/await】の基本的な動作について確認します。
【Form】に【Button】を貼り付けて
【Button】をクリックすると【HeavyProcess】という関数が非同期で実行されます。

bool flg = false;

private async void button1_Click(object sender, EventArgs e)
{
    if (flg == true)
    {
        Console.WriteLine("Processing");
        return;
    }
    flg = true;
    Console.WriteLine("Start");
    //Task<string> task = Task.Run<string>(new Func<string>(HeavyProcess)); ←引数なしの場合
    Task<string> task = Task.Run(() => HeavyProcess(5000));
    string result = await task; // まつけどスレッドは抜ける
    // HeavyProcessの処理が完了したら以下から処理が再開する
    if (result == "OK") // 正常
    {
        Console.WriteLine("End");
        flg = false;
    }
}

private string HeavyProcess(int time)
{
    System.Threading.Thread.Sleep(time);
    return "OK";
}

動作結果としては以下のログが出力されます。

Start
Processing ←  処理中にボタンをクリックした
Processing
Processing
Processing
Processing
Processing
End

重い処理を行っても、【Form】はデットロックされず
【Button】の操作が可能になっています。
そして処理が完了したという結果もしっかり取得出来ています。

3.TcpClientクラスを非同期にする

単純なTCP通信を使って検証してみます。
【ClientAsync】というクラスを作って、この中でTCP/IP通信を行います。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace TcpClientTest
{
    public partial class Form1 : Form
    {
        ClientAsyncKai clientAsync;
        public Form1()
        {
            InitializeComponent();
        }

        private async void button1_Click(object sender, EventArgs e)
        {
            clientAsync = new ClientAsync();

            Task<string> task = Task.Run(() => clientAsyncKai.Connect("送信データ", "127.0.0.1", 60000));
            string result = await task; // まつけどスレッドは抜ける
            if (result == "ERR_1")
            {

            }
            else if (result == "ERR_2")
            {

            }
            else if (result == "TIMEOUT")
            {

            }
            else // 正常
            {
                    
            }
        }
    }
}


using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

namespace TcpClientTest
{
    class ClientAsync
    {
        private TcpClient client;

        public ClientAsync()
        {
            client = new TcpClient();
            client.SendTimeout = 3000;
            client.ReceiveTimeout = 3000;
        }

        public string Connect(string sendMsg, string IPaddress, int port)
        {
            string resMsg="";
            try
            {
                // サーバーへの接続部分
                // 接続タイムアウトは下記のように自作する
                // これを入れないと接続タイムアウトまで約20秒程度かかってしまう
                int timeout = 3000;
                Task task = client.ConnectAsync(IPaddress, port);
                if (!task.Wait(timeout))
                {
                    client.Close();
                    throw new SocketException(10060);
                }

                // ソケット上でデータを送受信するためのメソッドを準備
                NetworkStream ns = client.GetStream();
                ns.ReadTimeout = 3000;
                ns.WriteTimeout = 3000;

                // データの送信
                var enc = Encoding.UTF8;
                byte[] sendBytes = enc.GetBytes(sendMsg);
                ns.Write(sendBytes, 0, sendBytes.Length);

                // データの受信
                System.IO.MemoryStream ms = new System.IO.MemoryStream();
                byte[] resBytes = new byte[256];
                int resSize = 0;
                do
                {
                    resSize = ns.Read(resBytes, 0, resBytes.Length);
                    if (resSize == 0)
                    {
                        Console.WriteLine("切断された");
                        break;
                    }
                    ms.Write(resBytes, 0, resSize);
                } while (ns.DataAvailable || resBytes[resSize - 1] != '\n');
                resMsg = enc.GetString(ms.GetBuffer(), 0, (int)ms.Length);
                ms.Close();

                resMsg = resMsg.TrimEnd('\n');
                // 閉じる
                ns.Close();
                client.Close();
            }
            catch (SocketException ex)
            {
                // 接続タイムアウト
                return "TIMEOUT";
            }
            catch (AggregateException ex)
            {
                if (ex.InnerException is SocketException)
                {
                    // 接続失敗拒否
                    return "ERR_1";
                }
            }
            catch (Exception ex)
            {
                // その他のエラー
                return "ERR_2";
            }

            return resMsg;
        }
    }
}

4.まとめ

【TcpClient】クラスは接続のタイムアウトを設定することが出来ません。
ですので自分である程度カスタムする必要があります。
クライアントからサーバーへ接続できない場合
タイムアウトまで約20秒程待つなんてありえないですね。。。


とりあえず【async/await】は使い方さえわかれば
使う場面が増えるのではないでしょうか?