非同期でソケット通信を行うためには、サーバの接続およびTCP/IPでの受信にスレッドを使います。TCP/IPでの送信については、送信要求があると即TCP/IPで送信するため、システムによって非同期にするか同期にするかが異なります。通常は、Windows Formでユーザインタフェースを作成します。この場合、Formと同じスレッドでソケット通信を行うと、Socket接続時および受信待ち時には、画面操作ができなくなります。このため、C#言語では、サーバ側ではSocketクラスのBeginAcceptメソッド、クライアント側ではBeginConnectメソッドを使用して、接続時の処理のために、Form処理がブロックされないように制御します。また、クライアントでサーバからのデータを受け取るときには、まずBeginReceiveメソッドを呼び出し、データの受信を開始します。Receiveメソッドではデータを受信しない限り処理が次へ進みませんが、BeginReceiveメソッドはすぐに終了し、処理をブロックしません。BeginReceiveを呼び出した後にデータを受信すると、パラメータで指定したコールバックメソッドが実行されます。このコールバックメソッドでEndReceiveメソッドを呼び出し、データを受信します。この場合、Windows Form処理と受信処理時のスレッドが異なるため、Socket通信で受信したデータをWindows Formに表示させるためには、同じスレッドにする必要があります。このために、C#言語では、Invokeメソッド(またはBeginInvokeメソッド)を使用します。次に、今回作成したサーバソフトとクライアントソフトを示します。
TCP/IPサーバソフト
サーバのポートは、「60001」とします。サーバを起動すると、別スレッドでBeginAccepメソッドを呼び出し、クライアントからの接続を待ちます。クライアントからの接続を受け取ると、OnConnectRequestメソッドに制御が移り、BeginReceiveメソッドを実行してソケット通信によるデータを受信します。
受信が完了すると、ReadCallbackメソッドに制御が移るので、EndReceiveメソッドを呼び出し、StateObjectクラスのbuffer変数に受け取ったデータをコピーします。このプログラム例では、サーバが受け取ったデータを折り返して送信する仕様としているため、BeginSendメソッドにより、受け取ったデータ(StateObjectクラスに保存}をクライアントに送信します。
クライアントからの切断は、ReadCallbackメソッドで、受信バイト数が「0」かそれ以下の場合、切断されたと判断します。相手から切断したときは、特にサーバ側でのClose処理は必要ありません。
サーバソフト(コンソールアプリ)
static void Main(string[] args) { try { Console.WriteLine("Main ThreadID:" + Thread.CurrentThread.ManagedThreadId); Program prog = new Program("60001"); prog.init(); } catch (Exception e) { } } private static ManualResetEvent SocketEvent = new ManualResetEvent(false); private IPEndPoint ipEndPoint; private Socket sock; private Thread tMain; Program(String port) { Console.WriteLine("Program ThreadID:" + Thread.CurrentThread.ManagedThreadId); IPAddress myIP = Dns.GetHostByName(Dns.GetHostName()).AddressList[0]; ipEndPoint = new IPEndPoint(myIP, Int32.Parse(port)); } void init() { Console.WriteLine("init ThreadID:" + Thread.CurrentThread.ManagedThreadId); sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); sock.Bind(ipEndPoint); sock.Listen(10); Console.WriteLine("サーバー起動中・・・"); tMain = new Thread(new ThreadStart(Round)); tMain.Start(); } void Round() { Console.WriteLine("Round ThreadID:" + Thread.CurrentThread.ManagedThreadId); while (true) { SocketEvent.Reset(); sock.BeginAccept(new AsyncCallback(OnConnectRequest), sock); SocketEvent.WaitOne(); } } void OnConnectRequest(IAsyncResult ar) { Console.WriteLine("OnConnectRequest ThreadID:" + Thread.CurrentThread.ManagedThreadId); SocketEvent.Set(); Socket listener = (Socket)ar.AsyncState; Socket handler = listener.EndAccept(ar); Console.WriteLine(handler.RemoteEndPoint.ToString() + " joined"); StateObject state = new StateObject(); state.workSocket = handler; handler.BeginReceive(state.buffer, 0, StateObject.BUFFER_SIZE, 0, new AsyncCallback(ReadCallback), state); } void ReadCallback(IAsyncResult ar) { Console.WriteLine("ReadCallback ThreadID:" + Thread.CurrentThread.ManagedThreadId); StateObject state = (StateObject)ar.AsyncState; Socket handler = state.workSocket; int ReadSize = handler.EndReceive(ar); if (ReadSize < 1) { Console.WriteLine(handler.RemoteEndPoint.ToString() + " disconnected"); return; } byte[] bb = new byte[ReadSize]; Array.Copy(state.buffer, bb, ReadSize); string msg = System.Text.Encoding.UTF8.GetString(bb); Console.WriteLine(msg); handler.BeginSend(bb, 0, bb.Length, 0, new AsyncCallback(WriteCallback), state); } void WriteCallback(IAsyncResult ar) { Console.WriteLine("WriteCallback ThreadID:" + Thread.CurrentThread.ManagedThreadId); StateObject state = (StateObject)ar.AsyncState; Socket handler = state.workSocket; handler.EndSend(ar); Console.WriteLine("送信完了"); handler.BeginReceive(state.buffer, 0, StateObject.BUFFER_SIZE, 0, new AsyncCallback(ReadCallback), state); } void disConnect() { Console.WriteLine("disConnect ThreadID:" + Thread.CurrentThread.ManagedThreadId); sock.Close(); }
サーバソフト(StateObjectクラス)
StateObjectクラスは、Socketのハンドラと使用する入出力バッファのクラス変数を持ちます。クライアントとの通信ハンドラがクライアントとの接続時に保存され、受信時のコールバック関数により受信したデータをバッファのクラス変数に保存します。
public Socket workSocket { get; set; } public const int BUFFER_SIZE = 1024; internal byte[] buffer = new byte[BUFFER_SIZE];
TCP/IPクライアントソフト
クライアントソフトを実行すると次のような画面が表示されます。クライアントソフトはWindows画面を表示するWindows Formクラスと、TCP/IP通信を行うTcpClientクラスで構成します。Connectボタンを押すとサーバに接続し、Sendボタンを押すとサーバに設定した値が送信され、折り返しサーバから同じデータが戻され、Visual Studioの出力画面にそのデータが表示されます。Closeボタンを押すとソケット通信が終了します。
クライアントソフト(Form)
Connectボタンを押しサーバと接続されると、TcpClientクラスでOnConnectedイベントが発生し、tClient_OnConnectedメソッドが呼ばれます。
サーバからデータを受信すると、TcpClientクラスでOnReceiveDataイベントが発生し、tClient_OnReceiveDataメソッドが呼ばれます。Invokeメソッドにより、スレッドをformと同じスレッドにして、DelegateによりReceiveDelegateメソッドを呼び出します。InvokeRequiredは、現在実行しているスレッドがformと同じスレッドかを判断し、同じでないなら「true」を返します。
closeボタンを押しサーバと切断されると、TcpClientクラスでOnDisconnectedイベントが発生し、tClient_OnDisconnectedメソッドが呼ばれます。Formのスレッドと現在のスレッドが異なると、Disconnectedメソッドを呼び出します。
//Socketクライアント TcpClient tClient = new TcpClient(); public Form1() { Debug.WriteLine("Form1" + " ThreadID:" + Thread.CurrentThread.ManagedThreadId); InitializeComponent(); //接続OKイベント tClient.OnConnected += new TcpClient.ConnectedEventHandler(tClient_OnConnected); //接続断イベント tClient.OnDisconnected += new TcpClient.DisconnectedEventHandler(tClient_OnDisconnected); //データ受信イベント tClient.OnReceiveData += new TcpClient.ReceiveEventHandler(tClient_OnReceiveData); } /** 接続断イベント **/ void tClient_OnDisconnected(object sender, EventArgs e) { Debug.WriteLine("tClient_OnDisconnected" + " ThreadID:" + Thread.CurrentThread.ManagedThreadId); if (this.InvokeRequired) this.Invoke(new DisconnectedDelegate(Disconnected), new object[] { sender, e }); } delegate void DisconnectedDelegate(object sender, EventArgs e); private void Disconnected(object sender, EventArgs e) { //接続断処理 Debug.WriteLine("Disconnected" + " ThreadID:" + Thread.CurrentThread.ManagedThreadId); } /** 接続OKイベント **/ void tClient_OnConnected(EventArgs e) { //接続OK処理 Debug.WriteLine("tClient_OnConnected" + " ThreadID:" + Thread.CurrentThread.ManagedThreadId); } /** 接続ボタンを押して接続処理 **/ private void btn_Connect_Click(object sender, EventArgs e) { Debug.WriteLine("btn_Connect_Click" + " ThreadID:" + Thread.CurrentThread.ManagedThreadId); try { //接続先ホスト名 string host = "192.168.0.100"; //接続先ポート int port = 60001; //接続処理 // Connect to the remote endpoint. tClient.Connect(host, port); } catch (Exception ex) { MessageBox.Show(ex.Message); } } /** データ受信イベント **/ void tClient_OnReceiveData(object sender, string e) { Debug.WriteLine("tClient_OnReceiveData" + " ThreadID:" + Thread.CurrentThread.ManagedThreadId); //別スレッドからくるのでInvokeを使用 if (this.InvokeRequired) this.Invoke(new ReceiveDelegate(ReceiveData), new object[] { sender, e }); } delegate void ReceiveDelegate(object sender, string e); //データ受信処理 private void ReceiveData(object sender, string e) { Debug.WriteLine("ReceiveData:" + e + " ThreadID:" + Thread.CurrentThread.ManagedThreadId); } private void btnSend_Click(object sender, EventArgs e) { //送信 tClient.Send(txbxSendStr.Text); } private void btnClose_Click(object sender, EventArgs e) { Debug.WriteLine("Form1_FormClosing" + " ThreadID:" + Thread.CurrentThread.ManagedThreadId); if (!tClient.IsClosed) tClient.Close(); }
クライアントソフト(TcpClientクラス)
TcpClientクラスは、c#のSocketクラスを使用して、非同期ソケット通信を行います。
Connectボタンを押されると、formクラスよりConnectメソッドを呼び出し、BeginConnectメソッドにより別スレッドでソケットの接続を行います。接続が完了すると、ConnectCallbackメソッドが呼び出され、ConnectCallbackメソッドで、FormクラスのtClient_OnReceiveDataメソッドにOnConnectedイベントで通知します。StartReceiveメソッドを呼び出し、ReceiveDataCallbackメソッドをパラメータにして、BeginReceiveメソッドを呼び出し、サーバからのデータを待ちます。
Sendボタンが押されると、formクラスよりSendメソッドを呼び出し、ソケットクラスのSendメソッドでデータを送信します。この送信時には、ロッククラスを用いてロックします。
サーバからデータを受信すると、Formクラスと別のスレッドでReceiveDataCallbackメソッドによりデータを受け取ります。受信したデータが\rや\nかをチェックして、tureならばOnReceiveDataイベントでFormクラスに通知します。なお、このプログラムでは、\rや\nを電文の終了としています。また、受信したデータは、MemoryStreamクラスに保存します。受信したデータを保存した後、再度BeginReceiveメソッドを呼び出し、次の受信データを待ちます。この時、ロッククラスを用いてロックします。
Closeボタンが押されると、formクラスよりCloseメソッドを呼び出し、Shutdownメソッドで生成したSocketを無効にし、CloseメソッドでSocketを閉じます。OnDisconnectedイベントにより、FormクラスのtClient_OnReceiveDataメソッドに通知します。
/** プライベート変数 **/ //Socket private Socket mySocket = null; //受信データ保存用 private MemoryStream myMs; //ロック用 private readonly object syncLock = new object(); //送受信文字列エンコード private Encoding enc = Encoding.UTF8; /** イベント **/ //データ受信イベント public delegate void ReceiveEventHandler(object sender, string e); public event ReceiveEventHandler OnReceiveData; //接続断イベント public delegate void DisconnectedEventHandler(object sender, EventArgs e); public event DisconnectedEventHandler OnDisconnected; //接続OKイベント public delegate void ConnectedEventHandler(EventArgs e); public event ConnectedEventHandler OnConnected; /** プロパティ **/ /// <summary> /// ソケットが閉じているか /// </summary> public bool IsClosed { get { return (mySocket == null); } } /// <summary> /// Dispose /// </summary> public virtual void Dispose() { //Socketを閉じる Close(); } /// <summary> /// コンストラクタ /// </summary> public TcpClient() { //Socket生成 mySocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); } public TcpClient(Socket sc) { mySocket = sc; } /// <summary> /// SocketClose /// </summary> public void Close() { Debug.WriteLine("Close" + " ThreadID:" + Thread.CurrentThread.ManagedThreadId); //Socketを無効 mySocket.Shutdown(SocketShutdown.Both); //Socketを閉じる mySocket.Close(); mySocket = null; //受信データStreamを閉じる if (myMs != null) { myMs.Close(); myMs = null; } //接続断イベント発生 OnDisconnected(this, new EventArgs()); } /// <summary> /// Hostに接続 /// </summary> /// <param name="host">接続先ホスト</param> /// <param name="port">ポート</param> public void Connect(string host, int port) { Debug.WriteLine("Connect" + " ThreadID:" + Thread.CurrentThread.ManagedThreadId); //IP作成 IPEndPoint ipEnd = new IPEndPoint(Dns.GetHostAddresses(host)[0], port); //ホストに接続 mySocket.Connect(ipEnd); // Connect to the remote endpoint. mySocket.BeginConnect(ipEnd, new AsyncCallback(ConnectCallback), mySocket); } private void ConnectCallback(IAsyncResult ar) { Debug.WriteLine("ConnectCallback" + " ThreadID:" + Thread.CurrentThread.ManagedThreadId); try { // Retrieve the socket from the state object. Socket client = (Socket)ar.AsyncState; // Complete the connection. client.EndConnect(ar); Console.WriteLine("Socket connected to {0}", client.RemoteEndPoint.ToString()); //接続OKイベント発生 OnConnected(new EventArgs()); //データ受信開始 StartReceive(); } catch (Exception e) { Console.WriteLine(e.ToString()); } } /// <summary> /// データ受信開始 /// </summary> public void StartReceive() { Debug.WriteLine("StartReceive" + " ThreadID:" + Thread.CurrentThread.ManagedThreadId); //受信バッファ byte[] rcvBuff = new byte[1024]; //受信データ初期化 myMs = new MemoryStream(); //非同期データ受信開始 mySocket.BeginReceive(rcvBuff, 0, rcvBuff.Length, SocketFlags.None, new AsyncCallback(ReceiveDataCallback), rcvBuff); } /// <summary> /// 非同期データ受信 /// </summary> /// <param name="ar"></param> private void ReceiveDataCallback(IAsyncResult ar) { Debug.WriteLine("ReceiveDataCallback" + " ThreadID:" + Thread.CurrentThread.ManagedThreadId); int len = -1; lock (syncLock) { if (IsClosed) return; //データ受信終了 len = mySocket.EndReceive(ar); } //切断された if (len <= 0) { Close(); return; } //受信データ取り出し byte[] rcvBuff = (byte[])ar.AsyncState; //受信データ保存 myMs.Write(rcvBuff, 0, len); if (myMs.Length >= 2) { //\r\nかチェック myMs.Seek(-2, SeekOrigin.End); if (myMs.ReadByte() == '\r' && myMs.ReadByte() == '\n') { //受信データを文字列に変換 string rsvStr = enc.GetString(myMs.ToArray()); //受信データ初期化 myMs.Close(); myMs = new MemoryStream(); //データ受信イベント発生 OnReceiveData(this, rsvStr); } else { //ストリーム位置を戻す myMs.Seek(0, SeekOrigin.End); } } lock (syncLock) { //非同期受信を再開始 if (!IsClosed) mySocket.BeginReceive(rcvBuff, 0, rcvBuff.Length, SocketFlags.None, new AsyncCallback(ReceiveDataCallback), rcvBuff); } } /// <summary> /// メッセージを送信する /// </summary> /// <param name="str"></param> public void Send(string str) { Debug.WriteLine("Send" + " ThreadID:" + Thread.CurrentThread.ManagedThreadId); if (!IsClosed) { //文字列をBYTE配列に変換 byte[] sendBytes = enc.GetBytes(str + "\r\n"); lock (syncLock) { //送信 mySocket.Send(sendBytes); } } }
非同期ソケット通信の実行結果
実行すると、送受信したデータが表示されるとともに、各メソッドごとに動作しているスレッドIDを表示します。次に、サバー側とクライアント側それぞれの実行結果を確認します。
サーバ側ソケット通信実行結果
サーバソフトの実行結果を見ると、起動時のスレッドのIDは「10」で、ここから別スレッド「11」を起動し、このスレッドでクライアントからの接続を待ちます。クライアントの接続があると受信待ち処理を実行し、データを受信するとReadCallbackメソッド(スレッドID:13)で処理を行います。ReadCallbackメソッドでは受信したデータを送信し、送信完了するとWriteCallbackメソッド(スレッドID:14)が呼び出されます。また、クライアントからの切断要求を受けて、ソケットが切断されています。
クライアント側ソケット通信実行結果
Formクラス(スレッドID:9)が起動し、Connectボタンを押すと別スレッド(スレッドID:11)で接続が完了します。
Sendボタンによりデータをサーバに送信すると、サーバからのデータをReceiveDataCallbackメソッド(スレッドID:11)で受け取ります。実行結果を見ると、Invokeメソッドにより、ReceiveDataメソッドでは、Formと同じスレッドID「9」となっています。
Closeボタンを押すと、クローズ処理(スレッドID:9)が行われます。ソケットがCloseするとサーバからは、バイト数「0」あるいはそれ以下のデータを受信します。ReceiveDataCallbackメソッド(スレッドID:11)で受け取り、判断して、tClient_OnDisconnectedメソッドを呼び出します。InvokeRequired変数によりスレッドを判断し、Formクラスと同じスレッド(スレッドID:9)のため、Invokeメソッドを呼び出していません。