我正处于编写新的Windows服务应用程序的设计阶段,该应用程序接受长连接的TCP / IP连接(即,这不像HTTP,其中有很多短连接,而是客户端连接并保持连接数小时或者几天甚至几周。)
我正在寻找设计网络架构的最佳方法的想法。我将需要为该服务启动至少一个线程。我正在考虑使用Asynch API(BeginRecieve等),因为我不知道在任何给定时间(可能是数百个)我将连接多少客户端。我绝对不想为每个连接启动一个线程。
数据将主要从我的服务器流向客户端,但有时会从客户端发送一些命令。这主要是一个监控应用程序,我的服务器定期向客户端发送状态数据。
有关尽可能扩展的最佳方法的任何建议吗?基本工作流程感谢。
编辑:要清楚,我正在寻找基于.net的解决方案(如果可能,请使用C#,但任何.net语言都可以使用)
BOUNTY注意:要获得赏金,我希望不仅仅是一个简单的答案。我需要一个解决方案的工作示例,作为指向我可以下载的内容的指针或在线的简短示例。它必须是基于.net和Windows(任何.net语言都可以接受)
编辑:我要感谢所有给出好答案的人。不幸的是,我只能接受一个,我选择接受更为人熟知的Begin / End方法。 Esac的解决方案可能会更好,但它仍然足够新,我不确定它将如何运作。我赞成了我认为很好的所有答案,我希望我能为你们做更多的事情。再次感谢。
答案 0 :(得分:91)
我把它写成了一个管理服务器所有连接的类。
我只是使用一个列表来保存所有客户端连接,但如果你需要更快的查找更大的列表,你可以随意编写它。
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;
}
我只想注意异常处理代码看起来很糟糕,但其原因是我在其中有异常抑制代码,因此如果设置了config选项,则会抑制任何异常并返回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都不会延迟。
接受回调通过调用end receive完成读取数据套接字。这将填充begin 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
我包含的using System.Net.Sockets;
,因为当我们不包括在内时,我总是感到恼火。
BeginAccept
我希望它有用,它可能不是最干净的代码,但它有效。代码也有一些细微差别,你应该对改变感到厌倦。例如,一次只能调用一个ReceiveCallback
。曾经有一个非常烦人的.net漏洞,这是多年前的事情,所以我不记得细节了。
此外,在ReceiveCallback
代码中,我们处理从套接字接收的任何内容,然后排队下一次接收。这意味着对于单个套接字,我们实际上在任何时间点都只有{{1}}一次,并且我们不需要使用线程同步。但是,如果您重新排序此项以在提取数据后立即调用下一个接收,这可能会快一些,您需要确保正确同步线程。
另外,我删除了很多代码,但留下了正在发生的事情的本质。这应该是你设计的一个良好开端。如果您对此有任何疑问,请发表评论。
答案 1 :(得分:81)
在C#中进行网络操作的方法有很多种。它们都使用不同的机制,因此具有高并发性的主要性能问题。开始*操作是其中之一,许多人经常误以为是更快/最快的网络方式。
为了解决这些问题,他们引入了* 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
}
}
答案 2 :(得分:45)
答案 3 :(得分:22)
答案 4 :(得分:11)
我已经在我的一些解决方案中运行了这样的服务器。以下是对.net:Get Closer to the Wire with High-Performance Sockets in .NET
中不同方法的详细解释最近我一直在寻找改进代码的方法,并将研究这个问题:“Socket Performance Enhancements in Version 3.5”特别包含“供使用异步网络I / O以实现最高性能的应用程序使用”
“这些增强功能的主要特点是避免在高容量异步套接字I / O期间重复分配和同步对象.Socket类当前为异步套接字I / O实现的Begin / End设计模式要求为每个异步套接字操作分配一个System.IAsyncResult对象。“
如果您点击链接,您可以继续阅读。我个人将在明天测试他们的示例代码,以便根据我的情况对其进行基准测试。
编辑: Here您可以使用新的3.5 SocketAsyncEventArgs找到客户端和服务器的工作代码,这样您就可以在几分钟内测试它并通过代码。这是一种简单的方法,但却是开始实施更大规模的基础。两年前在MSDN杂志上发表的this文章也很有趣。
答案 5 :(得分:9)
您是否考虑过仅使用WCF网络TCP绑定和发布/订阅模式? WCF允许您[主要]关注您的域而不是管道..
有很多WCF样本&amp;即使是IDesign下载部分提供的发布/订阅框架也可能有用:http://www.idesign.net
答案 6 :(得分:8)
我想知道一件事:
我绝对不想开始 每个连接的线程。
为什么?至少从Windows 2000开始,Windows可以处理应用程序中的数百个线程。我已经完成了它,如果线程不需要同步,它就非常容易使用。特别是考虑到你正在进行大量的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();
}
}
}
答案 7 :(得分:5)
如果能够正确获取所有细节,使用.NET的集成Async IO(BeginRead
等)是个好主意。正确设置套接字/文件句柄后,它将使用操作系统的底层IOCP实现,允许您的操作在不使用任何线程的情况下完成(或者,在最坏的情况下,使用我相信来自内核的IO线程池的线程.NET的线程池,有助于缓解线程池拥塞。)
主要问题是确保以非阻塞模式打开套接字/文件。大多数默认的便利功能(如File.OpenRead
)都没有这样做,因此您需要自己编写。
另一个主要问题是错误处理 - 在编写异步I / O代码时正确处理错误比在同步代码中执行错误要困难得多。即使您可能没有直接使用线程,也很容易遇到竞争条件和死锁,所以您需要注意这一点。
如果可能,您应该尝试使用便利库来简化可扩展异步IO的过程。
Microsoft的Concurrency Coordination Runtime是.NET库的一个示例,旨在减轻执行此类编程的难度。它看起来很棒,但由于我没有使用它,我无法评论它的扩展程度。
对于需要进行异步网络或磁盘I / O的个人项目,我使用了一组过去一年中构建的.NET并发/ IO工具,称为Squared.Task。它的灵感来自imvu.task和twisted之类的库,我在存储库中包含了一些working examples来执行网络I / O.我也在我编写的一些应用程序中使用它 - 最大的公开发布的应用程序NDexer(用于无线程磁盘I / O)。该库是根据我对imvu.task的经验编写的,并且有一套相当全面的单元测试,因此我强烈建议你试一试。如果您有任何问题,我很乐意为您提供帮助。
在我看来,基于我使用异步/无线IO而不是线程的经验,只要您准备好处理学习曲线,就可以在.NET平台上进行有价值的尝试。它允许您避免Thread对象的成本所带来的可伸缩性麻烦,并且在许多情况下,您可以通过仔细使用Futures / Promises等并发原语来完全避免使用锁和互斥锁。
答案 8 :(得分:2)
我使用了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;
}
}
答案 9 :(得分:2)
您可以在C10k problem page找到很好的技术概述。
答案 10 :(得分:1)
我会使用SEDA或轻量级线程库(erlang或更新的linux see NTPL scalability on the server side)。如果您的通信不是,那么异步编码非常麻烦:)
答案 11 :(得分:1)
让人们复制粘贴接受的答案,你可以重写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);
}
}
你甚至可以删除第一个catch,因为它的内容是相同的,但它是一个模板方法,你应该使用typed异常来更好地处理异常并理解导致错误的原因,所以只需用一些有用的实现这些catch代码
答案 12 :(得分:1)
我会使用.Net 3.5中添加的AcceptAsync / ConnectAsync / ReceiveAsync / SendAsync方法。我做了一个基准测试,他们的速度提高了大约35%(响应时间和比特率),100个用户不断发送和接收数据。
答案 13 :(得分:1)
嗯,.NET套接字似乎提供了select() - 这对于处理输入是最好的。对于输出,我有一个侦听工作队列的套接字编写器线程池,接受套接字描述符/对象作为工作项的一部分,因此每个套接字不需要一个线程。
答案 14 :(得分:1)
您可以尝试使用名为ACE(自适应通信环境)的框架,它是网络服务器的通用C ++框架。它是一款非常坚固,成熟的产品,旨在支持高可靠性,大批量应用,直至电信级。
该框架处理各种各样的并发模型,并且可能有一个适合您开箱即用的应用程序。这应该使系统更容易调试,因为大多数讨厌的并发问题已经解决了。这里的权衡是框架是用C ++编写的,并不是最热情和蓬松的代码库。另一方面,您可以获得经过测试的工业级网络基础架构和开箱即用的高度可扩展架构。
答案 15 :(得分:0)
我建议您阅读有关ACE
的这些书籍获取有关允许您创建高效服务器的模式的想法。
尽管ACE是用C ++实现的,但本书涵盖了许多可用于任何编程语言的有用模式。
答案 16 :(得分:-1)
要清楚,我正在寻找基于.net的解决方案(如果可能,请使用C#,但任何.net语言都可以使用)
如果纯粹使用.NET,您将无法获得最高级别的可伸缩性。 GC暂停可能会妨碍延迟。
我将需要为该服务启动至少一个线程。我正在考虑使用Asynch API(BeginRecieve等),因为我不知道在任何给定时间(可能是数百个)我将连接多少客户端。我绝对不想为每个连接启动一个线程。
Overlapped IO通常被认为是Windows最快的网络通信API。我不知道这是否与您的Asynch API相同。不要使用select,因为每次调用都需要检查每个打开的套接字而不是在活动套接字上有回调。
答案 17 :(得分:-1)
您可以使用Push Framework开源框架进行高性能服务器开发。它建立在IOCP之上,适用于推送场景和消息广播。