過去にこれに似たものを書いたことがあります。数年前の私の研究から、非同期ソケットを使用して、独自のソケット実装を作成することが最善の策であることを示しました。つまり、実際には何も実行していないクライアントは、比較的少ないリソースしか必要としませんでした。発生することはすべて、.netスレッドプールによって処理されます。
サーバーのすべての接続を管理するクラスとして作成しました。
私は単にリストを使用してすべてのクライアント接続を保持しましたが、より大きなリストをより高速に検索する必要がある場合は、好きなように記述できます。
private List<xConnection> _sockets;
また、着信接続を実際にリッスンするソケットも必要です。
private System.Net.Sockets.Socket _serverSocket;
startメソッドは実際にサーバーソケットを開始し、着信接続の待機を開始します。
public bool Start()
{
System.Net.IPHostEntry localhost = System.Net.Dns.GetHostEntry(System.Net.Dns.GetHostName());
System.Net.IPEndPoint serverEndPoint;
try
{
serverEndPoint = new System.Net.IPEndPoint(localhost.AddressList[0], _port);
}
catch (System.ArgumentOutOfRangeException e)
{
throw new ArgumentOutOfRangeException("Port number entered would seem to be invalid, should be between 1024 and 65000", e);
}
try
{
_serverSocket = new System.Net.Sockets.Socket(serverEndPoint.Address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
}
catch (System.Net.Sockets.SocketException e)
{
throw new ApplicationException("Could not create socket, check to make sure not duplicating port", e);
}
try
{
_serverSocket.Bind(serverEndPoint);
_serverSocket.Listen(_backlog);
}
catch (Exception e)
{
throw new ApplicationException("Error occured while binding socket, check inner exception", e);
}
try
{
//warning, only call this once, this is a bug in .net 2.0 that breaks if
// you're running multiple asynch accepts, this bug may be fixed, but
// it was a major pain in the ass previously, so make sure there is only one
//BeginAccept running
_serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
}
catch (Exception e)
{
throw new ApplicationException("Error occured starting listeners, check inner exception", e);
}
return true;
}
例外処理コードが悪いように見えることに注意したいのですが、その理由は、そこに例外抑制コードがあったため、構成false
オプションが設定されている場合に例外が抑制されて戻るが、それを削除したかったためです簡潔にするために。
上記の_serverSocket.BeginAccept(new AsyncCallback(acceptCallback))、_ serverSocket)は基本的に、ユーザーが接続するたびにacceptCallbackメソッドを呼び出すようにサーバーソケットを設定します。このメソッドは、.Netスレッドプールから実行されます。これは、多くのブロッキング操作がある場合、追加のワーカースレッドの作成を自動的に処理します。これにより、サーバーの負荷を最適に処理できます。
private void acceptCallback(IAsyncResult result)
{
xConnection conn = new xConnection();
try
{
//Finish accepting the connection
System.Net.Sockets.Socket s = (System.Net.Sockets.Socket)result.AsyncState;
conn = new xConnection();
conn.socket = s.EndAccept(result);
conn.buffer = new byte[_bufferSize];
lock (_sockets)
{
_sockets.Add(conn);
}
//Queue recieving of data from the connection
conn.socket.BeginReceive(conn.buffer, 0, conn.buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), conn);
//Queue the accept of the next incomming connection
_serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
}
catch (SocketException e)
{
if (conn.socket != null)
{
conn.socket.Close();
lock (_sockets)
{
_sockets.Remove(conn);
}
}
//Queue the next accept, think this should be here, stop attacks based on killing the waiting listeners
_serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
}
catch (Exception e)
{
if (conn.socket != null)
{
conn.socket.Close();
lock (_sockets)
{
_sockets.Remove(conn);
}
}
//Queue the next accept, think this should be here, stop attacks based on killing the waiting listeners
_serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
}
}
上記のコードは基本的に、入ってくる接続の受け入れを終了BeginReceive
し、クライアントがデータを送信するときに実行されるコールバックであるキューに入れ、次にacceptCallback
入ってくるクライアント接続を受け入れる次をキューに入れます。
BeginReceive
メソッド呼び出しは、クライアントからのデータを受信したときに何をすべきかソケットを告げるものです。のBeginReceive
場合、クライアントにデータを送信するときにデータをコピーするバイト配列を指定する必要があります。ReceiveCallback
この方法では、我々はデータを受信し処理する方法である、と呼ばれます。
private void ReceiveCallback(IAsyncResult result)
{
//get our connection from the callback
xConnection conn = (xConnection)result.AsyncState;
//catch any errors, we'd better not have any
try
{
//Grab our buffer and count the number of bytes receives
int bytesRead = conn.socket.EndReceive(result);
//make sure we've read something, if we haven't it supposadly means that the client disconnected
if (bytesRead > 0)
{
//put whatever you want to do when you receive data here
//Queue the next receive
conn.socket.BeginReceive(conn.buffer, 0, conn.buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), conn);
}
else
{
//Callback run but no data, close the connection
//supposadly means a disconnect
//and we still have to close the socket, even though we throw the event later
conn.socket.Close();
lock (_sockets)
{
_sockets.Remove(conn);
}
}
}
catch (SocketException e)
{
//Something went terribly wrong
//which shouldn't have happened
if (conn.socket != null)
{
conn.socket.Close();
lock (_sockets)
{
_sockets.Remove(conn);
}
}
}
}
編集:このパターンでは、コードのこの領域でそれを言及するのを忘れていました:
//put whatever you want to do when you receive data here
//Queue the next receive
conn.socket.BeginReceive(conn.buffer, 0, conn.buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), conn);
私が一般的に行うことは、コードが何であれ、パケットをメッセージに再構成してから、それらをスレッドプールのジョブとして作成することです。このようにして、クライアントからの次のブロックのBeginReceiveは、メッセージ処理コードが実行されている間は遅延されません。
Acceptコールバックは、end receiveを呼び出すことによってデータソケットの読み取りを終了します。これにより、受信開始機能で提供されるバッファーがいっぱいになります。コメントを残した場所で必要な操作を行うとBeginReceive
、クライアントがさらにデータを送信した場合にコールバックを再度実行する次のメソッドが呼び出されます。これが本当にトリッキーな部分です。クライアントがデータを送信するとき、受信コールバックはメッセージの一部でのみ呼び出される可能性があります。再組み立ては非常に複雑になる可能性があります。私は自分の方法を使用し、これを行うために独自のプロトコルを作成しました。省略しましたが、必要に応じて追加できます。このハンドラーは、私が書いたコードの中で最も複雑なものでした。
public bool Send(byte[] message, xConnection conn)
{
if (conn != null && conn.socket.Connected)
{
lock (conn.socket)
{
//we use a blocking mode send, no async on the outgoing
//since this is primarily a multithreaded application, shouldn't cause problems to send in blocking mode
conn.socket.Send(bytes, bytes.Length, SocketFlags.None);
}
}
else
return false;
return true;
}
上記のsendメソッドは実際には同期Send
呼び出しを使用しますが、メッセージサイズとアプリケーションのマルチスレッドの性質上、問題ありませんでした。すべてのクライアントに送信する場合は、_socketsリストをループするだけです。
上記で参照したxConnectionクラスは、基本的には、バイトバッファーを含めるためのソケットの単純なラッパーであり、私の実装ではいくつかの追加機能です。
public class xConnection : xBase
{
public byte[] buffer;
public System.Net.Sockets.Socket socket;
}
また、using
含まれていない場合は常に迷惑になるため、ここに含まれているsも参照します。
using System.Net.Sockets;
それがお役に立てば幸いです。最もクリーンなコードではないかもしれませんが、機能します。また、コードを変更することにうんざりするようなニュアンスもあります。1つは、一度にBeginAccept
呼び出されるのは1つだけです。これについては非常に煩わしい.netバグがありましたが、これは何年も前のことなので、詳細は思い出せません。
また、ReceiveCallback
コードでは、次の受信をキューに入れる前に、ソケットから受信したものを処理します。これは、単一のソケットの場合、実際に ReceiveCallback
はいつでも一度だけであり、スレッド同期を使用する必要がないことを意味します。ただし、データをプルした直後に次の受信を呼び出すようにこれを並べ替える場合、これは少し高速かもしれませんが、スレッドを適切に同期することを確認する必要があります。
また、私は自分のコードの多くをハッキングしましたが、何が起こっているかの本質をそのままにしました。これはあなたのデザインにとって良い出発点になるはずです。これに関して他に質問がある場合はコメントを残してください。