Eu escrevi algo semelhante a isso no passado. De minha pesquisa, anos atrás, mostrei que escrever sua própria implementação de soquete era a melhor aposta, usando os soquetes assíncronos. Isso significava que os clientes que realmente não estavam fazendo nada realmente exigiam relativamente poucos recursos. Tudo o que ocorre é tratado pelo pool de threads .net.
Eu escrevi como uma classe que gerencia todas as conexões para os servidores.
Simplesmente usei uma lista para armazenar todas as conexões do cliente, mas se você precisar de pesquisas mais rápidas para listas maiores, poderá escrevê-la como quiser.
private List<xConnection> _sockets;
Além disso, você precisa que o soquete esteja realmente ouvindo as conexões recebidas.
private System.Net.Sockets.Socket _serverSocket;
O método start realmente inicia o soquete do servidor e começa a escutar as conexões recebidas.
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;
}
Gostaria apenas de observar que o código de tratamento de exceções parece ruim, mas o motivo é que eu tinha um código de supressão de exceção lá para que quaisquer exceções fossem suprimidas e retornassem false
se uma opção de configuração fosse definida, mas eu queria removê-lo para amor de brevidade.
O _serverSocket.BeginAccept (novo AsyncCallback (acceptCallback)), _serverSocket) acima define essencialmente o soquete do servidor para chamar o método acceptCallback sempre que um usuário se conectar. Esse método é executado no pool de threads .Net, que manipula automaticamente a criação de threads de trabalho adicionais se você tiver muitas operações de bloqueio. Isso deve lidar de maneira ideal com qualquer carga no servidor.
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);
}
}
O código acima basicamente acabou de aceitar a conexão que entra, enfileira BeginReceive
que é um retorno de chamada que será executado quando o cliente envia dados e, em seguida, enfileira o próximo acceptCallback
que aceitará a próxima conexão do cliente que entrar.
A BeginReceive
chamada de método é o que diz ao soquete o que fazer quando recebe dados do cliente. Para BeginReceive
, você precisa fornecer uma matriz de bytes, que é onde ele copiará os dados quando o cliente enviar dados. O ReceiveCallback
método será chamado, e é assim que lidamos com o recebimento de dados.
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);
}
}
}
}
Edição: Neste padrão eu esqueci de mencionar que nesta área de código:
//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);
O que eu geralmente faria é no código que você quiser, é remontar pacotes em mensagens e, em seguida, criá-los como trabalhos no pool de threads. Dessa forma, o BeginReceive do próximo bloco do cliente não é atrasado enquanto qualquer código de processamento de mensagens estiver em execução.
O retorno de chamada de aceitação termina de ler o soquete de dados chamando o recebimento final. Isso preenche o buffer fornecido na função de recebimento inicial. Depois que você fizer o que quiser, onde deixei o comentário, chamamos o próximo BeginReceive
método que executará o retorno de chamada novamente se o cliente enviar mais dados. Agora, aqui está a parte realmente complicada: quando o cliente envia dados, seu retorno de chamada de recebimento pode ser chamado apenas com parte da mensagem. A remontagem pode se tornar muito, muito complicada. Eu usei meu próprio método e criei uma espécie de protocolo proprietário para fazer isso. Eu o deixei de fora, mas se você solicitar, eu posso adicioná-lo. Esse manipulador foi, na verdade, o código mais complicado que eu já escrevi.
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;
}
O método de envio acima, na verdade, usa uma Send
chamada síncrona , para mim, devido aos tamanhos das mensagens e à natureza multithread do meu aplicativo. Se você deseja enviar para todos os clientes, basta percorrer a lista _sockets.
A classe xConnection que você vê acima referenciada é basicamente um invólucro simples para um soquete incluir o buffer de bytes e, na minha implementação, alguns extras.
public class xConnection : xBase
{
public byte[] buffer;
public System.Net.Sockets.Socket socket;
}
Também para referência, aqui estão os que using
eu incluo, pois sempre fico irritado quando eles não estão incluídos.
using System.Net.Sockets;
Espero que seja útil, pode não ser o código mais limpo, mas funciona. Existem também algumas nuances no código que você deve estar cansado de mudar. Por um lado, tenha apenas uma BeginAccept
chamada por vez. Costumava haver um bug .net muito irritante em torno disso, que foi anos atrás, então não me lembro dos detalhes.
Além disso, no ReceiveCallback
código, processamos tudo o que é recebido do soquete antes de enfileirar o próximo recebimento. Isso significa que, para um soquete único, na verdade, estamos apenas ReceiveCallback
uma vez a qualquer momento e não precisamos usar a sincronização de threads. No entanto, se você reordenar isso para chamar a próxima recepção imediatamente após extrair os dados, o que pode ser um pouco mais rápido, será necessário sincronizar corretamente os encadeamentos.
Além disso, cortei muito do meu código, mas deixei a essência do que está acontecendo no lugar. Este deve ser um bom começo para o seu design. Deixe um comentário se tiver mais alguma dúvida sobre isso.