この記事では、C#のソケット通信におけるリスナーの実装を説明します。
リスナーとは、なんらかのきっかけでアクション、つまりイベントを発生させるプログラムのことを指します。
ソケット通信におけるリスナーの役割と実装について紹介します。
サンプルプログラムは以下になります。
リスナーの起動
リスナーを起動させるのに必要なのはポート番号です。IPアドレスを住所とするならば、ポート番号は道をさします。国道1号線のような1の部分を変えることでいろんな道でIPアドレスまでたどり着くことができます。
ポート番号は、0~65535までの中から利用できますが、すでに他のソフトが使っていることがありますので、なんでもかんでも好きな数字を入力できるわけではありません。
ポートの予約されていて新たに設定できない番号です。(Wikipedia)
ポート番号の中で、私用ポート番号として割り当てられている49152~65535は使えると思いますので他のアプリとの競合を気を付けて利用してください。
以下にListnerの起動時のプログラムを記載します。
//モジュール変数
private Socket _listener_socket;
private IPEndPoint _socket_EP;
//サーバーのステータスの状況を確認するための列挙型
public enum EnumServerState
{
None,
Listening,
Stopped
}
//Listener用のSocketを作成
if (_listener_socket == null)
{
_listener_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
this.Status = EnumServerState.None;
}
//接続状態であるか確認する
if (this.Status != EnumServerState.None)
throw new ApplicationException("すでに待ち状態です。");
// IPアドレスとポート番号を指定して、ローカルエンドポイントを設定
_socket_EP = new IPEndPoint(IPAddress.Any, port);
// TCP/IPのソケットをローカルエンドポイントにバインド
_listener_socket.Bind(_socket_EP);
//Listenを開始する
_listener_socket.Listen(back_log);
this.Status = EnumServerState.Listening;
注目すべきは、_listener_socketがSocketクラスで宣言されています。これはクライアント側もSocketクラスで処理を行います。同じクラスを使うため、送受信処理は同じ処理を利用します。
SocketのListen関数を実行することでリスナーが起動します。
受け入れ待ち
リスナーは、BeginAccept関数を使いクライアントからの接続を待ちます。クライアントからの接続があるとコールバック関数が呼び出されます。(CallbackAccept関数)
注意点はコードバック関数が呼び出されると受け入れ完了となり、BeginAccept関数は処理完了となるので複数のクライアント接続を行う場合、再度BeginAccept関数を実行する必要があります。
BeginAcceptは、処理完了を待たず抜けてきますのでDoneCheckAll.WaitOne();でループを一時停止する。
//マニュアルリセットイベントのインスタンスを生成
public ManualResetEvent DoneCheckAll = new ManualResetEvent(false);
/// <summary>
/// 接続承認ループ
/// </summary>
public void AcceptLoop()
{
//キャンセルされたら抜ける
while (this.Cancel == false)
{
// シグナル状態にし、メインスレッドの処理を続行する
DoneCheckAll.Reset();
//承認開始クライアントからの接続があればCallbackAcceptが発生する
_listener_socket.BeginAccept(new AsyncCallback(CallbackAccept), _listener_socket);
// シグナル状態になるまで待つ
DoneCheckAll.WaitOne();
}
}
コールバック関数では、取得したクライアントのSocket情報を利用しデータの送受信を開始する。
受け入れが完了したらDoneCheckAll.Set();を実行します。これをするとDoneCheckAll.WaitOne();から抜けてループが再開し、再度BeginAcceptが実行されます。
以下に記載されているClientSocketクラスは存在しません。私が作った独自クラスです。
StartReceive関数は、socket関数の中のデータ受信待ち状態にするBeginReceive関数を実行しています。
/// <summary>
/// 接続要求がくるのでデータ受信待ち状態にする。
/// </summary>
/// <param name="ar"></param>
public void CallbackAccept(IAsyncResult ar)
{
// クライアント要求を処理するソケットを取得(_listener_socketと同じはず)
Socket server = (Socket)ar.AsyncState;
//リスナーが終了している場合もしくはすでに破棄されている場合は、nullが返るので終了する。
if (server == null || this.disposedValue == true)
{
this.Cancel = true; //ループから抜けるためにCancelする
DoneCheckAll.Set();
return;
};
if (this.Status == EnumServerState.None) return;
// シグナル状態にし、メインスレッドの処理を続行する
DoneCheckAll.Set();
//接続してきたクライアントのソケットを取得
Socket socket = server.EndAccept(ar);
//クライアントソケットオブジェクトを生成
ClientSocket client = new ClientSocket(socket);
//端末からデータ受信を開始する
client.StartReceive();
}
リスナーによるデータの受信、送信
リスナーによるデータの受信、送信に関してはクライアント起動時に作成したソケット通信用のsocketクラスを利用して行います。socketクラスでの送受信についてはクライアントと同じなのでここでは割愛します。
クライアント編でまとめて説明します。
リスナーの終了
Close関数を実行するとクライアントを強制的に遮断します。
ここでは、ステータスをNoneにしてリスナーは動いていないとします。
再度、リスナーを開始するときは、上記のリスナー起動の処理を実行しますがそこで新しくsocketクラスのインスタンスを作成していますのでCloseしたインスタンスは破棄しています。
ガベージコレクションの対象になるようにDispose関数を実行しています。
_listener_socket.Close();
this.Status = EnumServerState.None;
_listener_socket.Dispose();
_listener_socket = null;
まとめ
ここで紹介してるリスナーは、クライアントから受信したデータをそのほかのクライアントに送るという処理はしていません。
なのでグループチャットなどをしたい場合は、BeginRecive時にリスナー接続されている他のクライアントに取得したデータを送ってあげる必要があります。
サンプルプログラムでは1対他を想定していますが、グループチャットなどの多対多を想定する場合は回収が必要になりますね。
この記事が皆様のお役に立てたら幸いです。