Я писав щось подібне до цього в минулому. З моїх досліджень років тому було показано, що написання власної реалізації сокета було найкращим варіантом, використовуючи асинхронні розетки. Це означало, що клієнти, які насправді нічого не робили, вимагали відносно невеликих ресурсів. Все, що відбувається, обробляється пулом потоків .net.
Я написав це як клас, який управляє всіма підключеннями до серверів.
Я просто використав список для утримання всіх клієнтських з'єднань, але якщо вам потрібні швидші пошуки для більш великих списків, ви можете написати його, як тільки захочете.
private List<xConnection> _sockets;
Також вам потрібна розетка, яка насправді прослуховує вхідні з'єднання.
private System.Net.Sockets.Socket _serverSocket;
Метод запуску фактично запускає серверний сокет і починає прослуховувати будь-які вхідні з'єднання.
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
якщо встановлено параметр config, але я хотів його видалити для стислість заради.
_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);
}
}
}
}
EDIT: У цій шаблоні я забув зазначити, що в цій області коду:
//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 наступного блоку від клієнта не затримується під час виконання будь-якого коду обробки повідомлень.
Зворотний виклик прийняття завершує зчитування розетки даних викликом кінцевого прийому. Це заповнює буфер, передбачений функцією початку прийому. Після того, як ви зробите все, що завгодно, де я залишив коментар, ми зателефонуємо на наступний 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
дзвінок, для мене це було чудово через розміри повідомлень та багатопотоковий характер моєї програми. Якщо ви хочете надіслати кожен клієнт, вам просто потрібно пройти через список _sockets.
Клас xConnection, на який ви бачите посилання вище, - це в основному проста обгортка для сокета, що включає байт-буфер, і в моїй реалізації деякі додаткові елементи.
public class xConnection : xBase
{
public byte[] buffer;
public System.Net.Sockets.Socket socket;
}
Також для довідкового опису є сюди, які using
я включаю, оскільки я завжди дратуюся, коли вони не включаються.
using System.Net.Sockets;
Я сподіваюся, що це корисно, можливо, це не найчистіший код, але він працює. Також у коді є деякі нюанси, які ви повинні бути змушені змінювати. Для одного лише мати єдиний BeginAccept
дзвінок у будь-який час. Раніше це було дуже дратівливою помилкою .net, що було років тому, тому я не пригадую деталей.
Крім того, в ReceiveCallback
коді ми обробляємо все, що надійшло з сокета, перш ніж черговий черговий прийом. Це означає, що для однієї розетки ми є насправді лише в ReceiveCallback
будь-який момент часу, і нам не потрібно використовувати синхронізацію потоків. Однак якщо ви упорядкуєте це для виклику наступного прийому відразу після витягування даних, що може бути трохи швидшим, вам потрібно буде переконатися, що ви правильно синхронізували потоки.
Крім того, я зламав багато свого коду, але залишив суть того, що відбувається на місці. Це має бути гарним початком для вашого дизайну. Залиште коментар, якщо у вас є питання щодо цього.