Я написал нечто похожее на это в прошлом. Из моего исследования, проведенного несколько лет назад, я понял, что лучше всего написать собственную реализацию сокетов, используя асинхронные сокеты. Это означало, что клиенты, которые на самом деле ничего не делали, требовали относительно небольших ресурсов. Все, что происходит, обрабатывается пулом потоков .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 (новый 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
я включаю, так как я всегда раздражаюсь, когда они не включены.
using System.Net.Sockets;
Я надеюсь, что это полезно, это может быть не самый чистый код, но это работает. Есть также некоторые нюансы в коде, которые вы должны быть утомлены при изменении. Для одного, только один BeginAccept
звонил в любое время. Раньше там была очень досадная ошибка .net, которая была много лет назад, поэтому я не вспоминаю подробности.
Кроме того, в ReceiveCallback
коде мы обрабатываем все, что получено из сокета, прежде чем ставить в очередь следующий прием. Это означает, что для одного сокета мы на самом деле только ReceiveCallback
один раз в любой момент времени, и нам не нужно использовать синхронизацию потоков. Однако, если вы измените этот порядок для вызова следующего приема сразу после извлечения данных, что может быть немного быстрее, вам необходимо убедиться, что вы правильно синхронизировали потоки.
Кроме того, я много взломал мой код, но оставил суть происходящего на месте. Это должно стать хорошим началом для вашего дизайна. Оставьте комментарий, если у вас есть еще вопросы по этому поводу.