2013/11/11

C# Tips2 TCP/IP通信 サーバー編

こんにちは、Kです。今日はC#で非同期のTCP/IP通信を刷るプログラムを考えてみましょう。
1. IPアドレスの変換
文字列からIPAddressクラスに変換するのに、最初次のようなコードを書いていましたが、
string[] ipaddress_strings = host.Split('.');
byte[] apaddress_bytes = new byte[4];
for(int i = 0; i < ipaddress_strings.Length; i++) {
   apaddress_bytes[i] = ipaddress_strings.Parse(ipr[i]);
}
var ipaddress = new System.Net.IPAddress(apaddress_bytes);
IPAddressクラスをリファレンスで探したところ、「Parse〔IP アドレス文字列を IPAddress インスタンスに変換します。〕」というメソッドがあることが分かりました。
そういえばC#では、文字列からインスタンスを生成するのにParseという名前のメソッドを使うのが一般的でしたね。逆はToStringです。
ローカルのホスト名をDns.GetHostName();で取得できますが、これで取得するとIPv6のものまで含まれてしまい、IPv4で指定した通信には使えません。そのため、ipa.AddressFamily == AddressFamily.InterNetworkと比較することにより、次のように取得しています。
foreach(IPAddress ipa in Dns.GetHostAddresses(host)) {
   if(ipa.AddressFamily == AddressFamily.InterNetwork) {
      ipaddress = ipa;
      break;
   }
}
ただし、これでは最初のIPアドレスしか取得していないことに注意が必要です。


2. 非同期通信

通常、同期通信では、サーバーがクライアントを受け入れて、送受信を繰り返した後、クライアントとの接続を絶って次の要求を受け入れます。ところがチャットでは、クライアントは常に接続しており、複数のクライアントが接続しっぱなしでサーバーと通信をします。同期通信では、送受信の間、1つのクライアントとしか通信できませんから、実現するにはクライアントの数だけスレッドを回す必要があります。
非同期操作をするにはBeginAcceptを使った方法があります。サンプルはhttp://sabulinsprog.seesaa.net/article/130980311.htmlを参照。
ところが、C#にはasync/await機構があり、これを利用することでまるで同期通信のプログラムを書いているかのように非同期通信のプログラムを書けます。使い方は簡単で、async/awaitを付け、メソッドの末尾にAsyncを付けるだけです。以下はサーバーの受信サンプルです。複数のクライアント要求に対応しています。
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Collections.Generic;

class Server {
   /* public */
   public delegate void RecieveCallback(string message); 
   public event RecieveCallback onReceive;
   
   /* private */
   enum State { START, STOP };
   IPAddress ipaddress;
   int port = 60000;

   TcpListener listenner;

   /* public */
   public Server() {
      try {
         string host = Dns.GetHostName();
         foreach(IPAddress ipa in Dns.GetHostAddresses(host)) {
            if(ipa.AddressFamily == AddressFamily.InterNetwork) {
               ipaddress = ipa;
               Console.WriteLine("起動 : {0}", ipa);
               break;
            }
         }
      }
      catch(Exception) {
         throw new IPAddressException();
      }
   }

   public void connect() {
      listenner = new TcpListener(ipaddress, port);
      listenner.Start();

      // スレッドの開始
      accept();
   }

   public void disconnect() {
      listenner.Stop();
   }

   public void send(byte[] data) {

   }

   /* private */
   async System.Threading.Tasks.Task accept() {
      while(true) {
         Console.WriteLine("待機");

         TcpClient client = await listenner.AcceptTcpClientAsync();

         acceptClient(client);
      }
   }

   async System.Threading.Tasks.Task acceptClient(TcpClient client) {
      Console.WriteLine("要求");
      var ns = client.GetStream();
      var ms = new System.IO.MemoryStream();
      byte[] result_bytes = new byte[256];

      do {
         int result_size = await ns.ReadAsync(result_bytes, 0, result_bytes.Length);

         if(result_size == 0) {
            Console.WriteLine("切断");
            client.Close();
            return;
         }

         ms.Write(result_bytes, 0, result_size);
      } while(ns.DataAvailable);

      string message = System.Text.Encoding.UTF8.GetString(ms.ToArray());
      ms.Close();

      onReceive(message);

      acceptClient(client);
   }

   public class IPAddressException : Exception {
      public override string ToString() {
         return "IPアドレスの変換あるいは取得に失敗しました。";
      }
   }
}
これにクライアントの登録を含めて、サーバーと送受信可能にしたものが次の通りです。
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Collections.Generic;

class Server {
   /* public */
   public delegate void RecieveCallback(string message); 
   public event RecieveCallback onReceive;
   
   /* private */
   enum State { START, STOP };
   IPAddress ipaddress;
   int port = 60000;
   int connected_number;
   System.Text.Encoding encoding;

   TcpListener listenner;
   List<ClientData> clients;

   /* public */
   public Server() {
      try {
         string host = Dns.GetHostName();
         foreach(IPAddress ipa in Dns.GetHostAddresses(host)) {
            if(ipa.AddressFamily == AddressFamily.InterNetwork) {
               ipaddress = ipa;
               Console.WriteLine("起動 : {0}", ipa);
               break;
            }
         }
      }
      catch(Exception) {
         throw new IPAddressException();
      }

      connected_number = 0;
      clients = new List<ClientData>();
      encoding = System.Text.Encoding.GetEncoding(932);
   }

   public void connect() {
      listenner = new TcpListener(ipaddress, port);
      listenner.Start();

      // スレッドの開始
      accept();
   }

   public void disconnect() {
      listenner.Stop();
   }

   public void sendByNo(int no, string message) {
      ClientData client_data = new ClientData();
      bool flag = false;

      // 番号からクライアントを探す
      foreach(ClientData cd in clients) {
         if(cd.no == no) {
            client_data = cd;
            flag = true;
         }
      }

      // 見付かったクライアントに対して送信
      if(flag) {
         send(ref client_data, message);
      }
      else {
         throw new CanotFindClientException();
      }
   }

   /* private */
   async System.Threading.Tasks.Task accept() {
      while(true) {
         Console.WriteLine("待機");

         TcpClient client = await listenner.AcceptTcpClientAsync();
         
         // クライアントの追加
         ClientData client_data = new ClientData();
         client_data.client = client;
         client_data.no = connected_number++;
         clients.Add(client_data);

         acceptClient(client, client_data);
      }
   }

   async System.Threading.Tasks.Task acceptClient(TcpClient client, ClientData client_data) {
      Console.WriteLine("要求({0})", client_data.no);
      var ns = client.GetStream();
      var ms = new System.IO.MemoryStream();
      byte[] result_bytes = new byte[16];

      do {
         int result_size = await ns.ReadAsync(result_bytes, 0, result_bytes.Length);

         if(result_size == 0) {
            Console.WriteLine("切断({0})", client_data.no);
            clients.Remove(client_data);
            client.Close();
            return;
         }

         ms.Write(result_bytes, 0, result_size);
      } while(ns.DataAvailable);

      string message = encoding.GetString(ms.ToArray());
      ms.Close();

      onReceive(message);

      acceptClient(client, client_data);
   }

   void send(ref ClientData client_data, string message) {
      TcpClient client = client_data.client;
      var ns = client.GetStream();
      byte[] message_byte = encoding.GetBytes(message);

      do {
         ns.Write(message_byte, 0, message_byte.Length);
      } while(ns.DataAvailable);
   }

   /* private */
   struct ClientData {
      public TcpClient client;
      public int no;
   }

   /* public */
   public class IPAddressException : Exception {
      public override string ToString() {
         return "※ IPアドレスの変換あるいは取得に失敗しました。";
      }
   }

   public class CanotFindClientException : Exception {
      public override string ToString() {
         return "※ 要求された番号のクライアントが存在しません。";
      }
   }
}
mainからは次のように使います。
using System;

class Program {
   public static void Main() {
      Server server;

      try {
         server = new Server();
         server.connect();
         server.onReceive += server_onReceive;
         while(true) {
            Console.WriteLine("送信先クライアント番号 : ");
            int no = int.Parse(Console.ReadLine());
            
            if(no == -1) {
               break;
            }

            Console.WriteLine("送信文 : ");
            try {
               server.sendByNo(no, Console.ReadLine());
            }
            catch(Server.CanotFindClientException e) {
               Console.WriteLine(e);
            }
         }
         server.disconnect();
      }
      catch(Server.IPAddressException e) {
         Console.WriteLine(e);
      }
   }

   static void server_onReceive(string message) {
      Console.WriteLine("★" + message);
   }
}

0 件のコメント:

コメントを投稿