私は、長時間実行接続のためにTCP/IP接続を受け入れる新しいWindowsサービスアプリケーションを作成する設計段階にあります(つまり、これはHTTPのように短い接続が多いHTTPではなく、クライアントが接続して数時間または数日間接続したままにするか、数週間)。
ネットワークアーキテクチャを設計する最適な方法のアイデアを探しています。サービス用に少なくとも1つのスレッドを開始する必要があります。 Asynch API(BeginRecieveなど)を使用することを検討しています。これは、一度に接続するクライアントの数(おそらく数百)がわからないためです。接続ごとにスレッドを開始したくないことは間違いありません。
データは主にサーバーからクライアントに流出しますが、クライアントからコマンドが送信される場合があります。これは主に、サーバーがクライアントに定期的にステータスデータを送信する監視アプリケーションです。
これを可能な限りスケーラブルにする最良の方法に関する提案はありますか?基本的なワークフロー?ありがとう。
編集:明確にするために、.netベースのソリューションを探しています(可能な場合はC#ですが、.net言語はすべて機能します)
報奨金:報奨金を受け取るには、単なる答え以上のものを期待しています。ダウンロードできるものへのポインタとして、またはインラインで短い例を使用して、ソリューションの実用的な例を必要とします。また、.netおよびWindowsベースである必要があります(どの.net言語でもかまいません)
編集:良い答えをくれたみんなに感謝したい。残念ながら、1つしか受け入れられず、よく知られているBegin/Endメソッドを受け入れることにしました。 Esacのソリューションはもっと良いかもしれませんが、それはまだ十分に新しいので、どのように機能するかはわかりません。
私は良いと思ったすべての答えに賛成しました。皆さんのためにもっとできることを望みます。再度、感謝します。
私は過去にこれに似たものを書きました。数年前の私の調査から、非同期ソケットを使用して、独自のソケット実装を書くことが最善策であることが示されました。つまり、実際には何もしていないクライアントは、比較的少ないリソースしか必要としませんでした。発生するものはすべて、.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つは、一度に1つのBeginAccept
のみを呼び出すことです。以前は非常に迷惑な.netバグがありましたが、これは何年も前のことなので、詳細を思い出せません。
また、ReceiveCallback
コードでは、次の受信をキューに入れる前に、ソケットから受信したものをすべて処理します。これは、単一のソケットに対して、実際にReceiveCallback
にいるのはいつでも一度だけであり、スレッド同期を使用する必要がないことを意味します。ただし、データをプルした後すぐに次の受信を呼び出すためにこれを並べ替える場合は、少し速くなる可能性があるため、スレッドを適切に同期する必要があります。
また、コードの多くをハックしましたが、何が起こっているかの本質を残しました。これはあなたがデザインしている人にとって良いスタートになるはずです。これに関して他に質問がある場合は、コメントを残してください。
C#でネットワーク操作を行う方法は多数あります。それらはすべて、ボンネットの下で異なるメカニズムを使用するため、高い同時実行性で大きなパフォーマンスの問題が発生します。 Begin *操作はこれらの1つであり、多くの人々はしばしば、より高速で最も高速なネットワーキング方法であると誤解しています。
これらの問題を解決するために、彼らは* Asyncメソッドセットを導入しました。MSDNから http://msdn.Microsoft.com/en-us/library/system.net.sockets.socketasynceventargs.aspx
SocketAsyncEventArgsクラスは、System.Net.Sockets .. ::。Socketクラスの一連の拡張機能の一部であり、特殊な高性能ソケットアプリケーションで使用できる代替の非同期パターンを提供します。 。このクラスは、特に高いパフォーマンスを必要とするネットワークサーバーアプリケーション用に設計されました。アプリケーションは、拡張非同期パターンを排他的に使用することも、ターゲットのホットエリアでのみ使用することもできます(たとえば、大量のデータを受信する場合)。
これらの機能強化の主な機能は、大量の非同期ソケットI/O中のオブジェクトの割り当てと同期の繰り返しを回避することです。 System.Net.Sockets .. ::。Socketクラスによって現在実装されているBegin/Endデザインパターンでは、非同期ソケット操作ごとにSystem .. ::。IAsyncResultオブジェクトを割り当てる必要があります。
カバーの下で、* Async APIはIO完了ポートを使用します。これは、ネットワーク操作を実行する最も速い方法です。 http://msdn.Microsoft.com/en-us/ magazine/cc302334.aspx
そして、あなたを助けるために、* Async APIを使って書いたtelnetサーバーのソースコードを含めています。関連する部分のみを含めています。また、データをインラインで処理する代わりに、別のスレッドで処理されるロックフリー(待機フリー)キューにプッシュすることを選択します。空の場合に新しいオブジェクトを作成する単純なプールである対応するPoolクラス、および不確定性を受け取っていない限り実際には必要ない自己拡張バッファであるBufferクラスは含まれていないことに注意してくださいデータ量。さらに情報が必要な場合は、お気軽にPMをお送りください。
public class Telnet
{
private readonly Pool<SocketAsyncEventArgs> m_EventArgsPool;
private Socket m_ListenSocket;
/// <summary>
/// This event fires when a connection has been established.
/// </summary>
public event EventHandler<SocketAsyncEventArgs> Connected;
/// <summary>
/// This event fires when a connection has been shutdown.
/// </summary>
public event EventHandler<SocketAsyncEventArgs> Disconnected;
/// <summary>
/// This event fires when data is received on the socket.
/// </summary>
public event EventHandler<SocketAsyncEventArgs> DataReceived;
/// <summary>
/// This event fires when data is finished sending on the socket.
/// </summary>
public event EventHandler<SocketAsyncEventArgs> DataSent;
/// <summary>
/// This event fires when a line has been received.
/// </summary>
public event EventHandler<LineReceivedEventArgs> LineReceived;
/// <summary>
/// Specifies the port to listen on.
/// </summary>
[DefaultValue(23)]
public int ListenPort { get; set; }
/// <summary>
/// Constructor for Telnet class.
/// </summary>
public Telnet()
{
m_EventArgsPool = new Pool<SocketAsyncEventArgs>();
ListenPort = 23;
}
/// <summary>
/// Starts the telnet server listening and accepting data.
/// </summary>
public void Start()
{
IPEndPoint endpoint = new IPEndPoint(0, ListenPort);
m_ListenSocket = new Socket(endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
m_ListenSocket.Bind(endpoint);
m_ListenSocket.Listen(100);
//
// Post Accept
//
StartAccept(null);
}
/// <summary>
/// Not Yet Implemented. Should shutdown all connections gracefully.
/// </summary>
public void Stop()
{
//throw (new NotImplementedException());
}
//
// ACCEPT
//
/// <summary>
/// Posts a requests for Accepting a connection. If it is being called from the completion of
/// an AcceptAsync call, then the AcceptSocket is cleared since it will create a new one for
/// the new user.
/// </summary>
/// <param name="e">null if posted from startup, otherwise a <b>SocketAsyncEventArgs</b> for reuse.</param>
private void StartAccept(SocketAsyncEventArgs e)
{
if (e == null)
{
e = m_EventArgsPool.Pop();
e.Completed += Accept_Completed;
}
else
{
e.AcceptSocket = null;
}
if (m_ListenSocket.AcceptAsync(e) == false)
{
Accept_Completed(this, e);
}
}
/// <summary>
/// Completion callback routine for the AcceptAsync post. This will verify that the Accept occured
/// and then setup a Receive chain to begin receiving data.
/// </summary>
/// <param name="sender">object which posted the AcceptAsync</param>
/// <param name="e">Information about the Accept call.</param>
private void Accept_Completed(object sender, SocketAsyncEventArgs e)
{
//
// Socket Options
//
e.AcceptSocket.NoDelay = true;
//
// Create and setup a new connection object for this user
//
Connection connection = new Connection(this, e.AcceptSocket);
//
// Tell the client that we will be echo'ing data sent
//
DisableEcho(connection);
//
// Post the first receive
//
SocketAsyncEventArgs args = m_EventArgsPool.Pop();
args.UserToken = connection;
//
// Connect Event
//
if (Connected != null)
{
Connected(this, args);
}
args.Completed += Receive_Completed;
PostReceive(args);
//
// Post another accept
//
StartAccept(e);
}
//
// RECEIVE
//
/// <summary>
/// Post an asynchronous receive on the socket.
/// </summary>
/// <param name="e">Used to store information about the Receive call.</param>
private void PostReceive(SocketAsyncEventArgs e)
{
Connection connection = e.UserToken as Connection;
if (connection != null)
{
connection.ReceiveBuffer.EnsureCapacity(64);
e.SetBuffer(connection.ReceiveBuffer.DataBuffer, connection.ReceiveBuffer.Count, connection.ReceiveBuffer.Remaining);
if (connection.Socket.ReceiveAsync(e) == false)
{
Receive_Completed(this, e);
}
}
}
/// <summary>
/// Receive completion callback. Should verify the connection, and then notify any event listeners
/// that data has been received. For now it is always expected that the data will be handled by the
/// listeners and thus the buffer is cleared after every call.
/// </summary>
/// <param name="sender">object which posted the ReceiveAsync</param>
/// <param name="e">Information about the Receive call.</param>
private void Receive_Completed(object sender, SocketAsyncEventArgs e)
{
Connection connection = e.UserToken as Connection;
if (e.BytesTransferred == 0 || e.SocketError != SocketError.Success || connection == null)
{
Disconnect(e);
return;
}
connection.ReceiveBuffer.UpdateCount(e.BytesTransferred);
OnDataReceived(e);
HandleCommand(e);
Echo(e);
OnLineReceived(connection);
PostReceive(e);
}
/// <summary>
/// Handles Event of Data being Received.
/// </summary>
/// <param name="e">Information about the received data.</param>
protected void OnDataReceived(SocketAsyncEventArgs e)
{
if (DataReceived != null)
{
DataReceived(this, e);
}
}
/// <summary>
/// Handles Event of a Line being Received.
/// </summary>
/// <param name="connection">User connection.</param>
protected void OnLineReceived(Connection connection)
{
if (LineReceived != null)
{
int index = 0;
int start = 0;
while ((index = connection.ReceiveBuffer.IndexOf('\n', index)) != -1)
{
string s = connection.ReceiveBuffer.GetString(start, index - start - 1);
s = s.Backspace();
LineReceivedEventArgs args = new LineReceivedEventArgs(connection, s);
Delegate[] delegates = LineReceived.GetInvocationList();
foreach (Delegate d in delegates)
{
d.DynamicInvoke(new object[] { this, args });
if (args.Handled == true)
{
break;
}
}
if (args.Handled == false)
{
connection.CommandBuffer.Enqueue(s);
}
start = index;
index++;
}
if (start > 0)
{
connection.ReceiveBuffer.Reset(0, start + 1);
}
}
}
//
// SEND
//
/// <summary>
/// Overloaded. Sends a string over the telnet socket.
/// </summary>
/// <param name="connection">Connection to send data on.</param>
/// <param name="s">Data to send.</param>
/// <returns>true if the data was sent successfully.</returns>
public bool Send(Connection connection, string s)
{
if (String.IsNullOrEmpty(s) == false)
{
return Send(connection, Encoding.Default.GetBytes(s));
}
return false;
}
/// <summary>
/// Overloaded. Sends an array of data to the client.
/// </summary>
/// <param name="connection">Connection to send data on.</param>
/// <param name="data">Data to send.</param>
/// <returns>true if the data was sent successfully.</returns>
public bool Send(Connection connection, byte[] data)
{
return Send(connection, data, 0, data.Length);
}
public bool Send(Connection connection, char c)
{
return Send(connection, new byte[] { (byte)c }, 0, 1);
}
/// <summary>
/// Sends an array of data to the client.
/// </summary>
/// <param name="connection">Connection to send data on.</param>
/// <param name="data">Data to send.</param>
/// <param name="offset">Starting offset of date in the buffer.</param>
/// <param name="length">Amount of data in bytes to send.</param>
/// <returns></returns>
public bool Send(Connection connection, byte[] data, int offset, int length)
{
bool status = true;
if (connection.Socket == null || connection.Socket.Connected == false)
{
return false;
}
SocketAsyncEventArgs args = m_EventArgsPool.Pop();
args.UserToken = connection;
args.Completed += Send_Completed;
args.SetBuffer(data, offset, length);
try
{
if (connection.Socket.SendAsync(args) == false)
{
Send_Completed(this, args);
}
}
catch (ObjectDisposedException)
{
//
// return the SocketAsyncEventArgs back to the pool and return as the
// socket has been shutdown and disposed of
//
m_EventArgsPool.Push(args);
status = false;
}
return status;
}
/// <summary>
/// Sends a command telling the client that the server WILL echo data.
/// </summary>
/// <param name="connection">Connection to disable echo on.</param>
public void DisableEcho(Connection connection)
{
byte[] b = new byte[] { 255, 251, 1 };
Send(connection, b);
}
/// <summary>
/// Completion callback for SendAsync.
/// </summary>
/// <param name="sender">object which initiated the SendAsync</param>
/// <param name="e">Information about the SendAsync call.</param>
private void Send_Completed(object sender, SocketAsyncEventArgs e)
{
e.Completed -= Send_Completed;
m_EventArgsPool.Push(e);
}
/// <summary>
/// Handles a Telnet command.
/// </summary>
/// <param name="e">Information about the data received.</param>
private void HandleCommand(SocketAsyncEventArgs e)
{
Connection c = e.UserToken as Connection;
if (c == null || e.BytesTransferred < 3)
{
return;
}
for (int i = 0; i < e.BytesTransferred; i += 3)
{
if (e.BytesTransferred - i < 3)
{
break;
}
if (e.Buffer[i] == (int)TelnetCommand.IAC)
{
TelnetCommand command = (TelnetCommand)e.Buffer[i + 1];
TelnetOption option = (TelnetOption)e.Buffer[i + 2];
switch (command)
{
case TelnetCommand.DO:
if (option == TelnetOption.Echo)
{
// ECHO
}
break;
case TelnetCommand.WILL:
if (option == TelnetOption.Echo)
{
// ECHO
}
break;
}
c.ReceiveBuffer.Remove(i, 3);
}
}
}
/// <summary>
/// Echoes data back to the client.
/// </summary>
/// <param name="e">Information about the received data to be echoed.</param>
private void Echo(SocketAsyncEventArgs e)
{
Connection connection = e.UserToken as Connection;
if (connection == null)
{
return;
}
//
// backspacing would cause the cursor to proceed beyond the beginning of the input line
// so prevent this
//
string bs = connection.ReceiveBuffer.ToString();
if (bs.CountAfterBackspace() < 0)
{
return;
}
//
// find the starting offset (first non-backspace character)
//
int i = 0;
for (i = 0; i < connection.ReceiveBuffer.Count; i++)
{
if (connection.ReceiveBuffer[i] != '\b')
{
break;
}
}
string s = Encoding.Default.GetString(e.Buffer, Math.Max(e.Offset, i), e.BytesTransferred);
if (connection.Secure)
{
s = s.ReplaceNot("\r\n\b".ToCharArray(), '*');
}
s = s.Replace("\b", "\b \b");
Send(connection, s);
}
//
// DISCONNECT
//
/// <summary>
/// Disconnects a socket.
/// </summary>
/// <remarks>
/// It is expected that this disconnect is always posted by a failed receive call. Calling the public
/// version of this method will cause the next posted receive to fail and this will cleanup properly.
/// It is not advised to call this method directly.
/// </remarks>
/// <param name="e">Information about the socket to be disconnected.</param>
private void Disconnect(SocketAsyncEventArgs e)
{
Connection connection = e.UserToken as Connection;
if (connection == null)
{
throw (new ArgumentNullException("e.UserToken"));
}
try
{
connection.Socket.Shutdown(SocketShutdown.Both);
}
catch
{
}
connection.Socket.Close();
if (Disconnected != null)
{
Disconnected(this, e);
}
e.Completed -= Receive_Completed;
m_EventArgsPool.Push(e);
}
/// <summary>
/// Marks a specific connection for graceful shutdown. The next receive or send to be posted
/// will fail and close the connection.
/// </summary>
/// <param name="connection"></param>
public void Disconnect(Connection connection)
{
try
{
connection.Socket.Shutdown(SocketShutdown.Both);
}
catch (Exception)
{
}
}
/// <summary>
/// Telnet command codes.
/// </summary>
internal enum TelnetCommand
{
SE = 240,
NOP = 241,
DM = 242,
BRK = 243,
IP = 244,
AO = 245,
AYT = 246,
EC = 247,
EL = 248,
GA = 249,
SB = 250,
WILL = 251,
WONT = 252,
DO = 253,
DONT = 254,
IAC = 255
}
/// <summary>
/// Telnet command options.
/// </summary>
internal enum TelnetOption
{
Echo = 1,
SuppressGoAhead = 3,
Status = 5,
TimingMark = 6,
TerminalType = 24,
WindowSize = 31,
TerminalSpeed = 32,
RemoteFlowControl = 33,
LineMode = 34,
EnvironmentVariables = 36
}
}
以前はCoversantのChris Mullinsによって書かれた.NETを使用したスケーラブルなTCP/IPについての非常に良い議論がありましたが、残念ながら彼のブログは以前の場所から消えたようです。このスレッドに登場する彼の: C++ vs. C#:スケーラブルなIOCPサーバーの開発 )
何よりもまず、両方ともBegin/End
およびAsync
クラスのSocket
メソッドは、IO Completion Ports(IOCP))を使用してスケーラビリティを提供します。正しく使用します。以下を参照してください)、実際にソリューションを実装するために選択する2つの方法のどちらよりもスケーラビリティに優れています。
クリス・マリンズの投稿は、Begin/End
、私が個人的に経験したものです。 Chrisはこれに基づいて、2GBのメモリを搭載した32ビットマシンで最大10,000の同時クライアント接続を、そして十分なメモリを備えた64ビットプラットフォームで100,000にまで拡張したソリューションをまとめました。私自身のこの手法での経験から(この種の負荷に近いところはどこにもありません)、これらの指標値を疑う理由はありません。
内部でIOCPを使用するメカニズムを使用する理由は、IOに実際のデータがあるまでスレッドをウェイクアップしない非常に低レベルのWindowsスレッドプールを使用するためです。読み込もうとしているチャネル(IOCPはファイルIOにも使用できます)。これの利点は、Windowsがそれを見つけるためだけにスレッドに切り替える必要がないことです。とにかくまだデータがないため、サーバーが必要とするコンテキスト切り替えの数を最小限に抑えます。
コンテキストスイッチは、「接続ごとのスレッド」メカニズムを確実に強制終了しますが、これは数十の接続のみを処理する場合に実行可能なソリューションです。しかし、このメカニズムは想像力の「拡張性」が伸びないことによるものです。
メモリ
何よりもまず、実装が単純すぎる場合、IOCPは.NETでメモリの問題を容易に引き起こす可能性があることを理解することが重要です。すべてのIOCP BeginReceive
呼び出しは、読み取り先のバッファーの「固定」になります。これが問題である理由の適切な説明については、 ユンジンのウェブログ:OutOfMemoryExceptionとPinning を参照してください。
幸い、この問題は回避できますが、多少のトレードオフが必要です。推奨される解決策は、大きなbyte[]
少なくとも90KB程度のアプリケーション起動時(またはそれに近い)のバッファー(.NET 2の時点で、必要なサイズは後のバージョンで大きくなる可能性があります)。これを行う理由は、大きなメモリの割り当てが自動的に非圧縮メモリセグメント(ラージオブジェクトヒープ)になり、自動的に固定されるためです。起動時に1つの大きなバッファを割り当てることにより、この移動できないメモリのブロックが邪魔にならず、断片化を引き起こさない比較的「低いアドレス」にあることを確認します。
次に、オフセットを使用して、この1つの大きなバッファーを、データを読み取る必要のある接続ごとに個別の領域に分割できます。これがトレードオフの出番です。このバッファーは事前に割り当てる必要があるため、接続ごとに必要なバッファースペースの量と、スケーリングする接続数に設定する上限を決定する必要があります(または、抽象化を実装できます)必要に応じて追加の固定バッファを割り当てることができます)。
最も単純な解決策は、このバッファ内の一意のオフセットですべての接続に単一バイトを割り当てることです。次に、1バイトを読み取るためにBeginReceive
呼び出しを行い、取得したコールバックの結果として残りの読み取りを実行できます。
処理中
作成したBegin
呼び出しからコールバックを取得する場合、コールバックのコードが低レベルIOCPスレッドで実行されることを認識することが非常に重要です。このコールバックでの長時間の操作を避けるのは絶対にessentialです。これらのスレッドを複雑な処理に使用すると、「接続ごとのスレッド」を使用するのと同じくらい効果的にスケーラビリティが低下します。
推奨される解決策は、コールバックを使用して、他のスレッドで実行される着信データを処理するワークアイテムをキューに入れることのみです。 IOCPスレッドができるだけ早くそのプールに戻ることができるように、コールバック内で潜在的にブロックする操作を避けてください。 .NET 4.0では、Task
を生成し、クライアントソケットへの参照と、BeginReceive
呼び出しによって既に読み取られた最初のバイトのコピーを作成することをお勧めします。 。次に、このタスクは、処理中のリクエストを表すソケットからすべてのデータを読み取り、実行してから、新しいBeginReceive
呼び出しを行って、もう一度IOCPのソケットをキューに入れます。 .NET 4.0より前では、ThreadPoolを使用するか、独自のスレッド化ワークキュー実装を作成できます。
基本的に、このソリューションにKevinのサンプルコードを使用することをお勧めしますが、次の警告が追加されています。
BeginReceive
に渡すバッファーが既に「固定」されていることを確認してくださいBeginReceive
に渡すコールバックが、着信データの実際の処理を処理するタスクをキューに入れるだけであることを確認してくださいそれを行うと、Chrisの結果を数十万の潜在的な数十の同時クライアントにスケールアップできることを疑う余地はありません(適切なハードウェアと独自の処理コードの効率的な実装を考えると;)
あなたはすでに上記のコードサンプルを通して答えの大部分を手に入れました。非同期IO操作は絶対にここに行く方法です。非同期IOはWin32が内部的にスケーリングするように設計されている方法です。完了ポートを使用してソケットを完了ポートにバインドし、完了ポートの完了を待機するスレッドプールを使用することにより達成されます。一般的な常識は、完了をCPU(コア)あたり2〜4スレッド待機することです。 WindowsパフォーマンスチームのRick Vicikによる記事:
上記の記事はほとんどネイティブのWindows APIを扱っていますが、スケーラビリティとパフォーマンスを把握しようとする人は必読です。彼らは物事の管理された側にもいくつかのブリーフを持っています。
2番目に行う必要があるのは、オンラインで利用できる 。NETアプリケーションのパフォーマンスとスケーラビリティの向上 の本を確認することです。第5章で、スレッド、非同期呼び出し、およびロックの使用に関する適切かつ有効なアドバイスを見つけることができます。しかし、実際の宝石は、第17章にあり、スレッドプールのチューニングに関する実用的なガイダンスなどがあります。この章の推奨事項に従ってmaxIothreads/maxWorkerThreadsを調整するまで、私のアプリにはいくつかの深刻な問題がありました。
純粋なTCPサーバーなので、私の次のポイントは偽物です。ただし、自分が追い詰められてWebRequestクラスとその派生物を使用する場合 ServicePointManager 。これは、パフォーマンスを台無しにすることを目的とする1つの目的を持つ構成クラスです。人工的に課されたServicePointからサーバーを解放してください。 .ConnectionLimitまたはアプリケーションはスケーリングしません(デフォルト値は何ですか?)また、httpリクエストでExpect100Continueヘッダーを送信するデフォルトポリシーを再検討することもできます。
コアソケット管理APIについては、送信側ではかなり簡単ですが、受信側では非常に複雑です。高スループットとスケールを実現するには、受信用にバッファがポストされていないため、ソケットがフロー制御されていないことを確認する必要があります。理想的には、パフォーマンスを向上させるために、3〜4個のバッファを先にポストし、1つが戻ったらすぐに新しいバッファをポストする必要があります(beforeバッファを戻します)そのため、ソケットは常にネットワークからのデータを保管する場所を確保します。おそらくすぐにこれを達成できない理由がわかるでしょう。
BeginRead/BeginWrite APIの操作が完了し、本格的な作業を開始すると、トラフィックのセキュリティが必要であることがわかります。 NTLM/Kerberos認証およびトラフィック暗号化、または少なくともトラフィック改ざん保護。これを行う方法は、組み込みのSystem.Net.Security.NegotiateStream(または異なるドメインにまたがる必要がある場合はSslStream)を使用することです。これは、ストレートソケット非同期操作に依存する代わりに、AuthenticatedStream非同期操作に依存することを意味します。ソケットを取得するとすぐに(クライアントへの接続またはサーバーからの受け入れから)、BeginAuthenticateAsClientまたはBeginAuthenticateAsServerを呼び出して、ソケットでストリームを作成し、認証のために送信します。認証が完了した後(少なくともネイティブInitiateSecurityContext/AcceptSecurityContext狂気からあなたの金庫...)、認証されたストリームのRemoteIdentityプロパティをチェックし、製品がサポートする必要のあるACL検証を行うことで認証を行います。その後、BeginWriteを使用してメッセージを送信し、BeginReadでそれらを受信します。これは、AuthenticateStreamクラスがこれをサポートしていないため、複数の受信バッファーをポストできないということを以前に話していた問題です。 BeginRead操作は、フレーム全体を受信するまですべてのIOを内部で管理します。 AuthenticatedStreamクラスによる処理は非常に良好であり、問題はないはずです。つまり、4〜5%のCPUでGBネットワークを飽和させることができます。AuthenticatedStreamクラスは、プロトコル固有のフレームサイズの制限も課します。 (SSLの場合は16k、Kerberosの場合は12k)。
これにより、正しい軌道に乗ることができます。ここにコードを投稿するつもりはありません。 MSDNの完璧な例 があります。私はこのような多くのプロジェクトを行っており、問題なく接続している約1000人のユーザーに拡張することができました。さらに、レジストリキーを変更して、カーネルがより多くのソケットハンドルを使用できるようにする必要があります。そして、serverOS、つまりXPまたはVista(つまりクライアントOS)ではなくW2K3 、それは大きな違いになります。
ところで、サーバーまたはファイルでデータベース操作があるかどうかを確認してくださいIOまた、非同期フレーバーを使用するか、すぐにスレッドプールを空にします。SQLServer接続の場合は、接続文字列に「非同期処理= true」を追加します。
私のソリューションのいくつかでこのようなサーバーを実行しています。 .NETでさまざまな方法を実行するための非常に詳細な説明を次に示します。 。NET で高性能ソケットを使用してワイヤに近づける
最近、コードを改善する方法を探しており、これについて検討しています:「 Socket Performance Enhancements in Version 3.5 」は、具体的には「非同期ネットワークを使用するアプリケーションで使用するために」含まれています最高のパフォーマンスを達成するためのI/O」。
「これらの機能強化の主な機能は、大量の非同期ソケットI/O中にオブジェクトの繰り返し割り当てと同期を回避することです。非同期ソケットI/OのSocketクラスによって現在実装されているBegin/Endデザインパターンにはシステムが必要です。 IAsyncResultオブジェクトは、各非同期ソケット操作に割り当てられます。」
リンクをたどれば読み続けることができます。私は個人的に明日、彼らのサンプルコードをテストして、私が持っているものと比較してベンチマークを行うつもりです。
編集:ここ では、新しい3.5 SocketAsyncEventArgsを使用してクライアントとサーバーの両方の作業コードを検索できるため、テストできます数分以内にコードを実行します。これは単純なアプローチですが、より大規模な実装を開始するための基礎となります。また、ほぼ2年前のMSDN Magazineの this の記事は興味深い読み物でした。
WCFネットTCPバインディングとパブリッシュ/サブスクライブパターンを使用することだけを検討しましたか?.
多くのWCFサンプルがあり、IDesignのダウンロードセクションにあるパブリッシュ/サブスクライブフレームワークも有用です: http://www.idesign.net
私は一つのことについて疑問に思っています:
接続ごとにスレッドを開始したくないことは間違いありません。
何故ですか? Windowsは、少なくともWindows 2000以降、アプリケーションで数百のスレッドを処理できました。私はそれをやったことがあります。スレッドを同期する必要がない場合、作業は非常に簡単です。特に、大量のI/Oを実行しているため(CPUに縛られておらず、ディスク通信またはネットワーク通信で多くのスレッドがブロックされます)、この制限は理解できません。
マルチスレッド方式をテストしましたが、何か不足していることがわかりましたか?また、各スレッドにデータベース接続を使用する予定ですか(データベースサーバーを強制終了するため、これは悪い考えですが、3層設計で簡単に解決できます)。数百人ではなく数千人のクライアントがいることを心配していますか? (32以上のGBがRAM-再びあれば、CPUに縛られていないことを考えると、スレッドの切り替え時間はまったく関係ないはずです。 )
コードを次に示します。これがどのように実行されるかを確認するには、 http://mdpopescu.blogspot.com/2009/05/multi-threaded-server.html に移動して、画像をクリックします。
サーバークラス:
public class Server
{
private static readonly TcpListener listener = new TcpListener(IPAddress.Any, 9999);
public Server()
{
listener.Start();
Console.WriteLine("Started.");
while (true)
{
Console.WriteLine("Waiting for connection...");
var client = listener.AcceptTcpClient();
Console.WriteLine("Connected!");
// each connection has its own thread
new Thread(ServeData).Start(client);
}
}
private static void ServeData(object clientSocket)
{
Console.WriteLine("Started thread " + Thread.CurrentThread.ManagedThreadId);
var rnd = new Random();
try
{
var client = (TcpClient) clientSocket;
var stream = client.GetStream();
while (true)
{
if (rnd.NextDouble() < 0.1)
{
var msg = Encoding.ASCII.GetBytes("Status update from thread " + Thread.CurrentThread.ManagedThreadId);
stream.Write(msg, 0, msg.Length);
Console.WriteLine("Status update from thread " + Thread.CurrentThread.ManagedThreadId);
}
// wait until the next update - I made the wait time so small 'cause I was bored :)
Thread.Sleep(new TimeSpan(0, 0, rnd.Next(1, 5)));
}
}
catch (SocketException e)
{
Console.WriteLine("Socket exception in thread {0}: {1}", Thread.CurrentThread.ManagedThreadId, e);
}
}
}
サーバーメインプログラム:
namespace ManyThreadsServer
{
internal class Program
{
private static void Main(string[] args)
{
new Server();
}
}
}
クライアントクラス:
public class Client
{
public Client()
{
var client = new TcpClient();
client.Connect(IPAddress.Loopback, 9999);
var msg = new byte[1024];
var stream = client.GetStream();
try
{
while (true)
{
int i;
while ((i = stream.Read(msg, 0, msg.Length)) != 0)
{
var data = Encoding.ASCII.GetString(msg, 0, i);
Console.WriteLine("Received: {0}", data);
}
}
}
catch (SocketException e)
{
Console.WriteLine("Socket exception in thread {0}: {1}", Thread.CurrentThread.ManagedThreadId, e);
}
}
}
クライアントのメインプログラム:
using System;
using System.Threading;
namespace ManyThreadsClient
{
internal class Program
{
private static void Main(string[] args)
{
// first argument is the number of threads
for (var i = 0; i < Int32.Parse(args[0]); i++)
new Thread(RunClient).Start();
}
private static void RunClient()
{
new Client();
}
}
}
.NETの統合された非同期IO(BeginRead
など)を使用することは、すべての詳細を正しく取得できる場合は良い考えです。ソケット/ファイルハンドルを適切に設定すると、 OSの基盤となるIOCP実装を使用して、スレッドを使用せずに操作を完了できるようにします(最悪の場合、.NETの代わりにカーネルのIOスレッドプールからのスレッドを使用します)スレッドプール。スレッドプールの混雑を緩和するのに役立ちます。)
主な落とし穴は、ソケット/ファイルを非ブロックモードで確実に開くことです。デフォルトの便利な関数のほとんど(File.OpenRead
)これを行わないでください。したがって、独自に記述する必要があります。
その他の主な懸念事項の1つはエラー処理です。非同期I/Oコードを記述する際のエラーの適切な処理は、同期コードで行うよりもはるかに困難です。また、スレッドを直接使用していない場合でも、競合状態やデッドロックに陥るのは非常に簡単です。そのため、これに注意する必要があります。
可能であれば、便利なライブラリを使用して、スケーラブルな非同期IOを実行するプロセスを簡単にする必要があります。
Microsoftの Concurrency Coordination Runtime は、この種のプログラミングの難しさを緩和するために設計された.NETライブラリの一例です。見た目は素晴らしいのですが、使っていないので、どれだけうまく拡張できるかについてはコメントできません。
非同期ネットワークまたはディスクI/Oを実行する必要がある個人プロジェクトでは、過去1年間に作成した一連の.NET同時実行性/ IOツールを使用します。これは Squared.Task と呼ばれます。 imvu.task や twisted のようなライブラリに触発され、ネットワークI/Oを行うリポジトリに 実例 を含めました。また、私が書いたいくつかのアプリケーションでそれを使用しました-最も一般に公開されているものは NDexer (スレッドレスディスクI/Oに使用します)です。ライブラリはimvu.taskでの私の経験に基づいて作成されており、かなり包括的な単体テストのセットを持っているため、試してみることを強くお勧めします。問題がある場合は、サポートを提供させていただきます。
私の意見では、スレッドの代わりに非同期/スレッドレスIOを使用した私の経験に基づいて、学習曲線に対処する準備ができている限り、.NETプラットフォームで価値のある努力です。 Threadオブジェクトのコストによって課されるスケーラビリティの面倒を回避できます。多くの場合、Futures/Promisesのような並行処理プリミティブを慎重に使用することで、ロックとミューテックスの使用を完全に回避できます。
私はKevinのソリューションを使用しましたが、彼はそのソリューションにはメッセージの再アセンブリのためのコードがないと言います。開発者は、このコードを使用してメッセージの再アセンブリを行うことができます。
private static void ReceiveCallback(IAsyncResult asyncResult )
{
ClientInfo cInfo = (ClientInfo)asyncResult.AsyncState;
cInfo.BytesReceived += cInfo.Soket.EndReceive(asyncResult);
if (cInfo.RcvBuffer == null)
{
// First 2 byte is lenght
if (cInfo.BytesReceived >= 2)
{
//this calculation depends on format which your client use for lenght info
byte[] len = new byte[ 2 ] ;
len[0] = cInfo.LengthBuffer[1];
len[1] = cInfo.LengthBuffer[0];
UInt16 length = BitConverter.ToUInt16( len , 0);
// buffering and nulling is very important
cInfo.RcvBuffer = new byte[length];
cInfo.BytesReceived = 0;
}
}
else
{
if (cInfo.BytesReceived == cInfo.RcvBuffer.Length)
{
//Put your code here, use bytes comes from "cInfo.RcvBuffer"
//Send Response but don't use async send , otherwise your code will not work ( RcvBuffer will be null prematurely and it will ruin your code)
int sendLenghts = cInfo.Soket.Send( sendBack, sendBack.Length, SocketFlags.None);
// buffering and nulling is very important
//Important , set RcvBuffer to null because code will decide to get data or 2 bte lenght according to RcvBuffer's value(null or initialized)
cInfo.RcvBuffer = null;
cInfo.BytesReceived = 0;
}
}
ContinueReading(cInfo);
}
private static void ContinueReading(ClientInfo cInfo)
{
try
{
if (cInfo.RcvBuffer != null)
{
cInfo.Soket.BeginReceive(cInfo.RcvBuffer, cInfo.BytesReceived, cInfo.RcvBuffer.Length - cInfo.BytesReceived, SocketFlags.None, ReceiveCallback, cInfo);
}
else
{
cInfo.Soket.BeginReceive(cInfo.LengthBuffer, cInfo.BytesReceived, cInfo.LengthBuffer.Length - cInfo.BytesReceived, SocketFlags.None, ReceiveCallback, cInfo);
}
}
catch (SocketException se)
{
//Handle exception and Close socket here, use your own code
return;
}
catch (Exception ex)
{
//Handle exception and Close socket here, use your own code
return;
}
}
class ClientInfo
{
private const int BUFSIZE = 1024 ; // Max size of buffer , depends on solution
private const int BUFLENSIZE = 2; // lenght of lenght , depends on solution
public int BytesReceived = 0 ;
public byte[] RcvBuffer { get; set; }
public byte[] LengthBuffer { get; set; }
public Socket Soket { get; set; }
public ClientInfo(Socket clntSock)
{
Soket = clntSock;
RcvBuffer = null;
LengthBuffer = new byte[ BUFLENSIZE ];
}
}
public static void AcceptCallback(IAsyncResult asyncResult)
{
Socket servSock = (Socket)asyncResult.AsyncState;
Socket clntSock = null;
try
{
clntSock = servSock.EndAccept(asyncResult);
ClientInfo cInfo = new ClientInfo(clntSock);
Receive( cInfo );
}
catch (SocketException se)
{
clntSock.Close();
}
}
private static void Receive(ClientInfo cInfo )
{
try
{
if (cInfo.RcvBuffer == null)
{
cInfo.Soket.BeginReceive(cInfo.LengthBuffer, 0, 2, SocketFlags.None, ReceiveCallback, cInfo);
}
else
{
cInfo.Soket.BeginReceive(cInfo.RcvBuffer, 0, cInfo.BytesReceived, SocketFlags.None, ReceiveCallback, cInfo);
}
}
catch (SocketException se)
{
return;
}
catch (Exception ex)
{
return;
}
}
C10k問題ページ でテクニックの概要を見つけることができます。
.Net 3.5で追加されたAcceptAsync/ConnectAsync/ReceiveAsync/SendAsyncメソッドを使用します。ベンチマークを行ったところ、約35%高速(応答時間とビットレート)になり、100人のユーザーが常にデータを送受信しています。
ネットワークサーバー用の汎用C++フレームワークであるACE(Adaptive Communications Environment)と呼ばれるフレームワークを使用してみてください。非常に堅実で成熟した製品であり、電話会社グレードまでの高信頼性、大容量アプリケーションをサポートするように設計されています。
このフレームワークは、非常に広範囲の同時実行モデルに対応しており、おそらくすぐに使用できるアプリケーションに適したものを持っています。これにより、厄介な並行性の問題のほとんどがすでに整理されているため、システムのデバッグが容易になります。ここでのトレードオフは、フレームワークがC++で書かれており、最も温かくふわふわしたコードベースではないことです。一方、テスト済みの産業グレードのネットワークインフラストラクチャと、すぐに使用できる拡張性の高いアーキテクチャを手に入れることができます。
[〜#〜] seda [〜#〜] または軽量スレッドライブラリ(erlang以降のLinux サーバー側のNTPLスケーラビリティを参照 )を使用します。あなたのコミュニケーションがそうでない場合、非同期コーディングは非常に面倒です:)
さて、.NETソケットは select() を提供するようです-これは入力を処理するのに最適です。出力には、ワークキューでリッスンするソケットライタースレッドのプールがあり、ワークアイテムの一部としてソケット記述子/オブジェクトを受け入れるので、ソケットごとにスレッドは必要ありません。
受け入れられた回答を貼り付ける人々に、acceptCallbackメソッドを書き換えて、_serverSocket.BeginAccept(new AsyncCallback(acceptCallback)、_serverSocket)のすべての呼び出しを削除できます。そして、この方法でfinally {}句に入れます:
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);
}
catch (SocketException e)
{
if (conn.socket != null)
{
conn.socket.Close();
lock (_sockets)
{
_sockets.Remove(conn);
}
}
}
catch (Exception e)
{
if (conn.socket != null)
{
conn.socket.Close();
lock (_sockets)
{
_sockets.Remove(conn);
}
}
}
finally
{
//Queue the next accept, think this should be here, stop attacks based on killing the waiting listeners
_serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
}
}
内容は同じですが、テンプレートメソッドであるため、最初のキャッチを削除することもできます。例外をより適切に処理し、エラーの原因を理解するには、型付き例外を使用する必要があります。
これらの本をACEで読むことをお勧めします
パターンに関するアイデアを得て、効率的なサーバーを作成できるようにします。
ACEはC++で実装されていますが、本はあらゆるプログラミング言語で使用できる多くの有用なパターンをカバーしています。