[C#] 45. ネットワークソケット通信(Socket)を使い方


Study / C#    作成日付 : 2021/10/06 19:06:25   修正日付 : 2021/10/06 20:07:44

こんにちは。明月です。


この投稿はC#でネットワークソケット通信(Socket)を使い方に関する説明です。


プログラムとプログラム、そしてパソコンとパソコンでデータを送受信することを通信と言います。通信をもっと詳しく説明すると、伝送するパケット(データ)がパソコンのLANカードによってランケーブルに伝送します。ランケーブルに伝送したデータはDNSとルータなどを通って到達しようとPCのLANカードによって最終に目標したプログラムでパケット(データ)を読み込みます。端末と端末の間にデータを通信します。

この時、我々は各端末間にデータ変換や装置間のプロトコール、規約などに関して実装してないです。この通信規約に関してはすべてOS側で設定して(OSI7階層)、我々はその上で差し込んで使うという意味でSocket通信という言います。


link - OSI参照モデル


Socket通信規約は規則が決めています。 통신 규약은 규칙이 정해져 있습니다.

先に通信を待つ側のPCをサーバというし、Portを開いてクライアントの接続を待ちます。そして接続する側をクライアントと言うし、サーバのIPとPortに接続して通信が繋がります。

サーバとクライアント間の通信はSend、 Receiveのタイプでデータを送受信します。そして通信が終わったらClose関数で接続を切ります。

この規約を利用してC#でソケット通信を作成してみましょう。

先にServerを作成してWindowのTelnetプログラムを利用して接続を確認し、そして仕様に合わせてClientを作成します。

using System;
using System.Text;
using System.Net;
using System.Net.Sockets;

namespace Example
{
  class Program
  {
    // 実行関数
    static void Main(string[] args)
    {
      // Socket EndPoint 設定(サーバの場合はAnyで設定してポート番号だけ設定する。)
      var ipep = new IPEndPoint(IPAddress.Any, 10000);
      // ソケットインスタンス生成
      using (Socket server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
      {
        // サーバソケットにEndPoint設定
        server.Bind(ipep);
        // クライアントソケット待機バッファー
        server.Listen(20);
        // コンソールに出力
        Console.WriteLine($"Server Start... Listen port {ipep.Port}...");
        // クライアントから接続を待機
        using (var client = server.Accept())
        {
          // クライアントEndPoint情報を取得
          var ip = client.RemoteEndPoint as IPEndPoint;
          // コンソール出力 - 接続IPと接続時間
          Console.WriteLine($"Client : (From: {ip.Address.ToString()}:{ip.Port}, Connection time: {DateTime.Now})");
          // クライアントで接続メッセージをbyteで変換する送信
          client.Send(Encoding.ASCII.GetBytes("Welcome server!\r\n>"));
          // メッセージバッファー
          var sb = new StringBuilder();
          // 無限ループ
          while (true)
          {
            // 通信バイナリバッファー
            var binary = new Byte[1024];
            // クライアントからメッセージ待機
            client.Receive(binary);
            // クライアントから受け取ったメッセージをStringに変換
            var data = Encoding.ASCII.GetString(binary);
            // メッセージの空白(\0)を取り除く
            sb.Append(data.Trim('\0'));
            // メッセージの総内容が2文字以上だし、改行(\r\n)が発生すれば
            if (sb.Length > 2 && sb[sb.Length - 2] == '\r' && sb[sb.Length - 1] == '\n')
            {
              // メッセージバッファーの内容をStringに変換
              data = sb.ToString().Replace("\n", "").Replace("\r", "");
              // メッセージ内容が空白なら続けてメッセージ待機状況に
              if (String.IsNullOrWhiteSpace(data))
              {
                continue;
              }
              // メッセージ内容がexitなら無限ループ終了(つまり、サーバ終了)
              if ("EXIT".Equals(data, StringComparison.OrdinalIgnoreCase))
              {
                break;
              }
              // メッセージ内容をコンソールに表示
              Console.WriteLine("Message = " + data);
              // バッファー初期化
              sb.Length = 0;
              // メッセージにECHOを付ける
              var sendMsg = Encoding.ASCII.GetBytes("ECHO : " + data + "\r\n>");
              // クライアントでメッセージ送信
              client.Send(sendMsg);
            }
          }
          // コンソール出力 - 接続終了メッセージ
          Console.WriteLine($"Disconnected : (From: {ip.Address.ToString()}:{ip.Port}, Connection time: {DateTime.Now})");
        }
      }
      // 任意のキーを押してください
      Console.WriteLine("Press Any key...");
      Console.ReadLine();
    }
  }
}


上の内容はウィンドウtelnetプログラムで私が作ったサーバプログラムに接続する例を作成しました。

先にプログラムを説明すればSocketクラスでサーバのSocketサーバのインスタンスを生成しました。Bind関数を使って待機ポートを設定します。

Listenで同時接続待機設定をしてAccept関数を通ってクライアントの接続を待機します。

プログラム上ではAccept関数が呼び出したらClient接続が発生する時までプロセスが止まることになります。


そしてtelnetプログラムで接続をすることになればAccept関数を通ってクライアントSocketインスタンスをリターンするし、SendとReceive関数を通ってサーバとクライアントからお互いにメッセージを送受信することができます。


上の例は私がMain関数で実装したので、一つのクライアントだけ接続ができます。つまり、クライアントが二つ以上なら接続ができない状況です。

そうならマルチ接続ができるようにはスレッド機能を実装しなければならないです。

using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;

namespace Example
{
  class Program
  {
    // サーバ実行Taskメソッド
    static async Task RunServer(int port)
    {
      // Socket EndPoint 設定(サーバの場合はAnyで設定してポート番号だけ設定する。)
      var ipep = new IPEndPoint(IPAddress.Any, port);
      // ソケットインスタンス生成
      using (Socket server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
      {
        // サーバソケットにEndPoint設定
        server.Bind(ipep);
        // クライアントソケット待機バッファー
        server.Listen(20);
        // コンソールに出力
        Console.WriteLine($"Server Start... Listen port {ipep.Port}...");
        // server AcceptをTaskで並列処理(つまり、非同期を作成する。)
        var task = new Task(() =>
        {
          // 無限ループ
          while (true)
          {
            // クライアントからメッセージ待機
            var client = server.Accept();
            // 接続すればTaskで並列処理
            new Task(() =>
            {
              // クライアントEndPoint情報を取得
              var ip = client.RemoteEndPoint as IPEndPoint;
              // コンソール出力 - 接続IPと接続時間
              Console.WriteLine($"Client : (From: {ip.Address.ToString()}:{ip.Port}, Connection time: {DateTime.Now})");
              // クライアントで接続メッセージをbyteで変換する送信
              client.Send(Encoding.ASCII.GetBytes("Welcome server!\r\n>"));
              // メッセージバッファー
              var sb = new StringBuilder();
              // 終了すれば自動にclient終了
              using (client)
              {
                // 無限ループ
                while (true)
                {
                  // 通信バイナリバッファー
                  var binary = new Byte[1024];
                  // クライアントからメッセージ待機
                  client.Receive(binary);
                  // クライアントから受け取ったメッセージをStringに変換
                  var data = Encoding.ASCII.GetString(binary);
                  // メッセージの空白(\0)を取り除く
                  sb.Append(data.Trim('\0'));
                  // メッセージの総内容が2文字以上だし、改行(\r\n)が発生すれば
                  if (sb.Length > 2 && sb[sb.Length - 2] == '\r' && sb[sb.Length - 1] == '\n')
                  {
                    // メッセージバッファーの内容をStringに変換
                    data = sb.ToString().Replace("\n", "").Replace("\r", "");
                    // メッセージ内容が空白なら続けてメッセージ待機状況に
                    if (String.IsNullOrWhiteSpace(data))
                    {
                      continue;
                    }
                    // メッセージ内容がexitなら無限ループ終了(つまり、サーバ終了)
                    if ("EXIT".Equals(data, StringComparison.OrdinalIgnoreCase))
                    {
                      break;
                    }
                    // メッセージ内容をコンソールに表示
                    Console.WriteLine("Message = " + data);
                    // バッファー初期化
                    sb.Length = 0;
                    // メッセージにECHOを付ける
                    var sendMsg = Encoding.ASCII.GetBytes("ECHO : " + data + "\r\n>");
                    // クライアントでメッセージ送信
                    client.Send(sendMsg);
                  }
                }
                // コンソール出力 - 接続終了メッセージ
                Console.WriteLine($"Disconnected : (From: {ip.Address.ToString()}:{ip.Port}, Connection time: {DateTime.Now})");
              }
              // Task実行
            }).Start();
          }
        });
        // Task実行
        task.Start();
        // 待機
        await task;
      }
    }
    // 実行関数
    static void Main(string[] args)
    {
      // TaskでSocketサーバを作成(サーバが終了する時まで待機)
      RunServer(10000).Wait();
      // 任意のキーを押してください
      Console.WriteLine("Press Any key...");
      Console.ReadLine();
    }
  }
}


上の例は始めの例でTaskスレッドを利用してマルチ接続ができるように作成しました。

Accept関数はクライアントが接続する前にスレッドが止まる形なので、クライアントで接続すれば並列でまたTaskスレッドを作成してまたループに乗ってAcceptに待機状況になります。

ここまで簡単なサーバプログラムが作成されました。


上のプログラムの基盤でまたクライアントを作成しましょう。

using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;

namespace Example
{
  class Program
  {
    // 実行関数
    static void Main(string[] args)
    {
      // Socket EndPoint設定
      var ipep = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 10000);
      // ソケットインスタンス生成
      using (Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
      {
        // ソケット接続
        client.Connect(ipep);
        // 接続になればTaskで並列処理
        new Task(() =>
        {
          try
          {
            // 終了すれば自動にclient終了
            // 無限ループ
            while (true)
            {
              // 通信バイナリバッファー
              var binary = new Byte[1024];
              // サーバからメッセージ待機
              client.Receive(binary);
              // サーバから受け取ったメッセージをStringに変換
              var data = Encoding.ASCII.GetString(binary).Trim('\0');
              // メッセージ内容が空白ならずっとメッセージ待機状況に
              if (String.IsNullOrWhiteSpace(data))
              {
                continue;
              }
              // メッセージ内容をコンソールに表示
              Console.Write(data);
            }
          }
          catch (SocketException)
          {
            // 接続の切りが発生するとExceptionが発生
          }
          // Task実行
        }).Start();
        // ユーザからメッセージを受け取るための無限ループ
        while (true)
        {
          // コンソール入力を受け取る。
          var msg = Console.ReadLine();
          // クライアントで受け取ったメッセージをStringに変換
          client.Send(Encoding.ASCII.GetBytes(msg + "\r\n"));
          // メッセージ内容がexitなら無限ループ終了(つまり、クライアント終了)
          if ("EXIT".Equals(msg, StringComparison.OrdinalIgnoreCase))
          {
            break;
          }
        }
        // コンソール出力 - 接続終了メッセージ
        Console.WriteLine($"Disconnected");
      }
      // 任意のキーを押してください
      Console.WriteLine("Press Any key...");
      Console.ReadLine();
    }
  }
}


サーバプログラムの仕様に合わせてクライアントを作成しました。

サーバとSend、 Receive関数は似てますが、Bind、 Listen関数の代わりにConnect関数を使ってサーバにSocket接続します。

クライアントは普通の一つのサーバを接続するため、別に並列処理を作成する必要がありません。あればReceive関数だけTaskスレッドに作成して、Send, Receiveを分離しました。

仕様によりクライアントも様々にサーバを同時に接続することができますが、基本的に一つのサーバに接続をします。


ここまでC#でネットワークソケット通信(Socket)を使い方に関する説明でした。


ご不明なところや間違いところがあればコメントしてください。

#C#
最新投稿