【C#】TCP/IPでチャットアプリを作成する方法を解説【前編】

C#

はじめに

初心者向けにC#でのTCP/IPを使用したチャットアプリの作成方法を解説します。
例外処理や細かい部分で考慮が足りない部分があるかもしれませんが、TCP/IPの大まかなやり取りの流れを掴んでいただければと思います。

この記事はC#の基本的な文法を理解されている方を対象としています。

完成形は以下の様になります。

少し長くなってしまうので前編後編に記事を分けています。
前編ではサーバーアプリケーションの作成までを解説します。

解説で使用しているソースコード

記事内にソースコードを全て掲載していますが、ソリューション毎ダウンロードしたいという方は以下Githubからダウンロード出来ます。

アプリケーションの構成

コードを記載する前に、ざっくりとした構成をまとめます。
サーバーアプリ1つに対して、クライアントアプリはN個とします。
また、クライアントはデータのやり取りを必ずサーバーアプリを介して行います。

やり取りするデータのフォーマットも定義します。
データ長は5120byteで、全ての領域をメッセージ部とします。
今回のチャットアプリでは文字列のやり取りしかしないため、メッセージ部しかありませんが、仮に機能を拡張して、ファイルの受け渡し、あて先を指定等出来るようにする場合は上記通信データの領域にデータを割り当てることになります。
ちょっとした独自のプロトコルですね。

サーバー側処理の大まかな流れをシーケンス図で記載しました。
あまりシーケンス図を描くのは得意ではないので細かい点は目をつぶってください。

アプリケーションの作成

サーバーアプリケーションプロジェクトの作成

VisualStudio2022で解説します。
VisualStudio2019でもほぼ同様に進められると思います。

まず、Windowsフォームアプリケーション(.NET Framework)プロジェクトを作成します。
プロジェクト名は「ServerApp」、ソリューション名は「ChatApp」とします。

チャットアプリケーションコモンプロジェクトの作成

サーバーアプリケーションとクライアントアプリケーション共通で使用するデータをDLLにまとめるため、チャットアプリケーションコモンプロジェクトを作成します。

ソリューションエクスプローラーを右クリック>追加>新しいプロジェクトを選択します。
クラスライブラリ(.NET Framework)を選択し、プロジェクト名に「ChatAppCommon」を設定します。

サーバーアプリケーションの作成

作成したServerAppプロジェクトのForm1を以下の様に変更します。

コントロール名プロパティ名説明
FormNameServerFormアプリケーションメインフォーム
 TextServerフォーム左上に表示される文字列を設定
gbSettingsText設定設定グループボックス
lblPortText監視ポート番号(0-65535)監視ポート番号のラベル
txtPort   
btnStartText開始開始ボタン
 ClickbtnStart_Clickクリックイベント
btnEndText終了終了ボタン
 ClickbtnEnd_Clickクリックイベント
 Enabledfalse 
gpConnectingClientText接続中クライアント接続中クライアントグループボックス
lbConnectingClient  接続中クライアントリストボックス
gpConnectionLogTextログロググループボックス
lbLog  ログリストボックス
ssStatus  ステータスストリップ
sslblStatusText状態:ステータスストリップラベル

ソースコード

まずはServerForm.csとChatAppCommon.csの全体のコードを記載します。
すべてコピペで動作すると思います。

System.ServiceModelの参照が足りないというエラーになるかもしれませんが、
その場合はソリューションエクスプローラー>ServerApp>参照右クリック>参照の追加>アセンブリ>System.ServiceModelを追加してください。
ServerAppプロジェクトにChatAppCommonへの参照も必要です。

ServerForm.cs

ChatAppCommon.cs

コードの解説

処理の要所を解説します。

ポート番号のチェック

ポート番号が空または数値ではないまたは0-65535以外の場合はエラーとしています。

サーバーの作成~ポート監視待機

 

ポイント
  1. 接続情報(ポート番号)の有効チェック。
  2. サーバーを作成して指定のポートの監視を開始。
  3. 非同期で接続待機のループを開始。
  1. 接続情報(ポート番号)の有効チェック。
    ポート番号のチェックを行い、有効なポート番号の場合のみ後続の処理を実行します。

  2. サーバーを作成して指定のポートの監視を開始。
    ポートを監視するためにTcpListnerを作成します。
    作成時に監視するIPアドレスとポート番号をコンストラクタに渡します。

    監視するポートが既に使用されているポートの場合、TcpListner.Start()で例外となり、SocketException.SocketErrorCodeにAddressAlreadyInUseが設定されるため、「指定したポートは他のシステムに使用されています。」エラーメッセージを表示します。

  3. 非同期で接続待機のループを開始。
    ポートの監視が正常に開始できたら、接続受け入れを非同期で待機するループ処理を開始します。

    AsyncAcceptWaitLoopメソッドにasyncを指定しているため、AsyncAcceptWaitLoopメソッドは非同期で実行されます。
    接続受け入れを非同期で待機するのは、同期で待機すると接続を受け入れるまでアプリケーションが固まってしまうためです。

    また、開始ボタンを無効、終了ボタンを有効に設定します。
    (開始しているときに開始ボタンを押せないようにするためです。)

    TcpListnerExとなっているのは、TcpListnerのActiveプロパティをpublicで参照できるようにするためにラップしたクラスを使用しているからです。
    使用方法はTcpListnerと同じです。
    TcpListnerExはChatAppCommon.csに定義しています。

非同期接続待機ループ処理

ポイント
  1. 非同期で接続を待機する。
  2. サーバーの監視が停止されるまで、ずっと接続要求を受け入れ続ける。

  1. 非同期で接続を待機する。
    非同期で接続を待機するには、TcpListner.BeginAcceptTcpClientメソッドを使用します。

    サーバーが監視中の間は非同期で接続を待機し続けます。
    非同期メソッドなので、メソッドのシンタックスをasyncとし、戻り値としてvoidではなくTaskを返します。

    ここでの戻り値は使用しないため、呼び出し元のbtnStart_Clickでは「_」(破棄)として受け取っていますいます。

    TcpListner.BeginAcceptTcpClientメソッドの引数には、
    ・接続要求時コールバックデリゲート(接続要求があった時に実行される処理)
    ・コールバックに渡されるユーザー定義オブジェクト(自由に設定出来るオブジェクト)
    を指定します。

    コールバックデリゲートには「public delegate void AsyncCallback(IAsyncResult ar);」型のデリゲートを指定します。
    つまり、戻り値がvoidで引数にIAsyncResultを受け取るメソッドを指定する必要があります。

    ユーザー定義型オブジェクトにはコールバック関数の引数に渡すオブジェクトを指定しますが、ここでは特に不要なのでnullを渡しています。
    AsyncWaitHandle.WaitOne(-1)は接続要求があるまでずっと待機し続ける処理です。
    待機しない場合、接続要求を受けていないのにBeginAcceptTcpClientメソッドが延々と実行され続けることになってしまいます。
    PCのスペックにもよりますが、数分でメモリリークしてしまいますので注意しましょう。
  2. サーバーの監視が停止されるまで、ずっと接続要求を受け入れ続ける。
    アプリケーションで終了がクリックされるまで、whileで接続待機処理をループさせます。
    こうすることで、何度でもクライアントからの接続要求を受け入れる事が可能です。

非同期接続受け入れ処理

接続要求を受けたときに非同期で実行されます。
クライアントアプリケーションでTcpClient.Connectメソッドが実行された時という事ですね。

ポイント

  1. TcpListner.EndAcceptTcpClientメソッドにIAsyncResultを渡して接続してきたクライアントを取得する。
  2. 取得したクライアントからのデータ受信を非同期で待機する。
  3. 接続中クライアントに、新規クライアントからの接続があった事を通知する。

  1. TcpListner.EndAcceptTcpClientメソッドにIAsyncResultを渡すことで、接続元との通信をするためのTcpClientを取得します。

    ここで取得したTcpClientからデータを受信又は送信することで、接続元とのデータのやり取りを行います。

  2. 取得したクライアントからのデータ受信を非同期で待機します。
    非同期での受信待機にはTcpClient.Client.BeginReceiveメソッドを使用します。

    BeginReceiveメソッドの引数には
    ・受信するデータのバッファ(byte[])
    ・受信するデータのバッファ内の読込み開始位置(int)
    ・バッファサイズ(int)
    ・送受信時の動作(SocketFlags)
    ・受信時コールバックデリゲート(受信時に実行される処理)
    ・コールバックに渡されるユーザー定義オブジェクト(自由に設定出来るオブジェクト)
    を指定します。

    掲載しているコードではユーザー定義オブジェクトにChatAppCommonに定義した通信データクラスを設定しています。
  3. 接続中クライアントに、新規クライアントからの接続があった事を通知する。

    これはメンバ変数の接続中のクライアントリストに対して接続元のIPアドレス、ポート番号を送信しています。

    注意が必要なのは、今回のアプリケーションは非同期処理で実行されているため、メンバ変数へのアクセスはスレッドセーフでなければなりません。

    そのため、非同期処理からのメンバ変数へのアクセス時は「lock」ステートメントを使用したり、通常のリストではなく、スレッドセーフなリストの「SynchronizedCollection」を使用しています。

    「SynchronizedCollection」はスレッドセーフなコレクションですが、それは追加(Add)や削除(Remove)操作がスレッドセーフなだけであり、列挙処理に関してはスレッドセーフではありません。
    なので、lock(SynchronizedCollection.SyncRoot)として列挙操作中は他のスレッドからの操作を待機させるようにしています。

    ここではSynchronizedCollectionやlockステートメントについての解説は省略します。別の記事で実際にSynchronizedCollectionのコードを見ながら解説したいと思います。

非同期データ受信処理

AcceptCallbackで実行したBeginReceiveのコールバック処理です。
クライアントアプリケーションからデータを受信したときに実行されます。

ポイント

  1. BeginReceiveの引数に指定したユーザー定義型からデータを取得する。
  2. 受信データサイズが0byteの場合、切断判定とする。
  3. 受信データを接続中クライアントに転送する。
  4. 受信処理後、再度受信待機する。

  1. BeginReceiveの引数に指定したユーザー定義型からデータを取得する。
    IAsyncResult.AsyncStateにはBeginReceiveメソッドの引数に渡したユーザー定義型のオブジェクトが設定されています。

    object型なので、渡したユーザー定義型にキャストして取得します。

    IAsyncResult.AsyncStateには、AcceptCallbackメソッドのBeginReceiveメソッドに設定した通信データクラスが設定されています。
  2. 受信データサイズが0byteの場合、切断判定とする。
    TcpClient.Client.EndReceiveメソッドは、IAsyncResultを渡すと受信したデータサイズを返します。
    受信したデータが0byteの場合、クライアントから切断されたと判定できます。

    ここでは、切断されたことを接続中クライアントに対して通知しています。

  3. 受信データを接続中クライアントに転送する。
    BeginReceiveメソッドのバッファ引数に渡した通信データクラスのバッファに受信したデータが設定されています。
    受信したデータを接続中クライアント全てに対して転送します。
  4. 受信処理後、再度受信待機する。
    再度TcpClient.Client.BeginReceiveを実行し、データの受信を待機します。
    これで切断されるまでデータ受信待機→データ受信→クライアントに転送→再度データ受信待機→…というループになります。

まとめ

サーバーアプリケーションの解説は以上となります。
思った以上にだらだらと長くなってしまいました。

非同期の処理と、TPC/IPの受信・送信について、なんとなくイメージを掴んでいただけたらと思います。

後半ではクライアントアプリケーションの解説を行います。

C#プログラミング
凡人プログラマーのブログ

コメント