Я написал нечто похожее на это в прошлом. Из моего исследования, проведенного несколько лет назад, я понял, что лучше всего написать собственную реализацию сокетов, используя асинхронные сокеты. Это означало, что клиенты, которые на самом деле ничего не делали, требовали относительно небольших ресурсов. Все, что происходит, обрабатывается пулом потоков .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
только один раз в любой момент времени, и нам не нужно использовать синхронизацию потоков. Однако, если вы измените порядок для вызова следующего приема сразу после извлечения данных, что может быть немного быстрее, вам необходимо убедиться, что вы правильно синхронизировали потоки.
Кроме того, я много взломал свой код, но оставил суть происходящего на месте. Это должно стать хорошим началом для вашего дизайна. Оставьте комментарий, если у вас есть еще вопросы по этому поводу.