socket servidor multiples multiple clients clientes cliente c# multithreading tcp server

servidor - socket tcp c#



Lectura continua de mĂșltiples conexiones TCP en CF (1)

Este método de lectura continua de clientes conectados hace que cada conexión tenga un subproceso de trabajo que está esperando que BeginRead vuelva

No, no es así. De hecho, el uso de BeginRead() o una de las otras alternativas asíncronas para procesar E / S en un objeto Socket es el enfoque más escalable para usar.

¿Sería una solución tener un hilo que sondee la propiedad DataAvailable de cada conexión hasta que aparezca algo y luego haga que un hilo para leer / procesar sea una solución?

No. Esto sería horrible. El sondeo de un socket, a través de DataAvailable o Select() , es terriblemente ineficiente, lo que obliga a invertir una gran cantidad de tiempo de CPU simplemente verificando el estado del socket. El SO proporciona buenos mecanismos asincrónicos para manejar esto; una implementación de sondeo ignora eso y hace todo el trabajo en sí mismo.

¿O está creando todos estos hilos de trabajo no tan grandes como yo pienso?

No estás creando los hilos que crees que eres. Cuando utiliza las API asíncronas, hacen uso de una función en ventanas llamada Puertos de finalización de E / S. Un puerto de finalización de E / S está asociado con una operación de E / S, y un hilo puede esperar en un puerto. Pero un hilo puede manejar la espera en un gran número de operaciones, por lo que tener diez operaciones de lectura sobresalientes no hace que se creen diez hilos diferentes.

.NET administra un grupo de subprocesos para manejar estas operaciones, administradas como parte de la clase ThreadPool . Puede supervisar esa clase para ver el comportamiento del conjunto de IOCP (que es diferente del grupo de subprocesos de trabajo utilizado para QueueUserWorkItem() ).

.NET asignará nuevos objetos y subprocesos de IOCP según sea necesario para dar servicio a sus operaciones de E / S de red. Puede estar seguro de que lo hará de una manera razonable y eficiente.

En escalas muy grandes, la sobrecarga de la recolección de basura de los objetos asociados con las operaciones de lectura puede entrar en juego. En este caso, puede usar el método ReceiveAsync() , que le permite reutilizar su propio conjunto de objetos de estado para las operaciones, de modo que no esté constantemente creando y descartando objetos.

Otro problema que puede surgir es la fragmentación de la memoria, especialmente en el montón de objetos grandes (dependiendo del tamaño de los búferes que utilice). Cuando inicia una operación de lectura en un socket, el buffer debe ser fijado, evitando que .NET compacte el montón en el que reside.

Pero estos problemas no son razones para evitar el uso de las API asincrónicas (y, de hecho, el segundo problema ocurre independientemente). Son solo cosas que debes tener en cuenta. Usar la API asíncrona es, de hecho, la mejor manera de hacerlo.

Dicho esto, BeginReceive() es "vieja escuela". Funciona, pero puede ajustar una operación BeginReceive() en una Task (consulte Task.FromAsync() y TPL y Programación Asincrónica tradicional de .NET Framework ), o puede envolver todo el Socket en un objeto NetworkStream (que tiene ReadAsync() y métodos similares), lo que le permitirá escribir su código asíncrono de una manera más legible que no requiera el uso de métodos explícitos de devolución de llamada. Y para escenarios donde la E / S de red siempre culmina en cierta interacción con la interfaz de usuario, le permite usar async / await para hacerlo, de nuevo de una manera más legible y fácil de escribir.

Tengo un servidor TCP simple que puede escuchar y aceptar múltiples conexiones en un puerto. Luego continuamente espera que los datos se lean de sus conexiones. Utiliza una clase contenedora para un TcpClient llamado ConnectedClient por conveniencia y una lista (diccionario) de ConnectedClients para realizar un seguimiento de todas las conexiones. Básicamente es así:

/* this method waits to accept connections indefinitely until it receives the signal from the GUI thread to stop. When a connection is accepted, it adds the connection to the list and calls a method called ProcessClient, which returns almost immediately.*/ public void waitForConnections() { // this method has access to a TcpListener called listener that was started elsewhere try { while (!_abort) { TcpClient socketClient = listener.AcceptTcpClient(); //Connected client constructor takes the TcpClient as well as a callback that it uses to print status messages to the GUI if ConnectedClient client = new ConnectedClient(socketClient, onClientUpdate); clients.Add(client.id, client); ProcessClient(client); } } catch (Exception e) { onStatusUpdate("Exception Occurred: " + e.Message); } } /* This method doesn''t do much other than call BeginRead on the connection */ private void ProcessClient(ConnectedClient client) { try { // wrapper class contains an internal buffer for extracting data as well as a TcpClient NetworkStream stream = client.tcpClient.GetStream(); stream.BeginRead(client.buffer, 0, client.tcpClient.ReceiveBufferSize, new AsyncCallback(StreamReadCompleteCallback), client); } catch (Exception ex) { onStatusUpdate(ex.Message); } }

En mi función de devolución de llamada, StreamReadCompleteCallback, llamo a EndRead, verificando el valor de retorno de EndRead para detectar si la conexión se ha cerrado. Si el valor de retorno es mayor que cero, extraigo / proceso los datos de lectura y llamo a BeginRead nuevamente en el mismo cliente. Si el valor de retorno es cero, la conexión se ha cerrado y elimino la conexión (eliminar de la lista, cerrar el TcpClient, etc.).

private void StreamReadCompleteCallback(IAsyncResult ar) { ConnectedClient client = (ConnectedClient)ar.AsyncState; try { NetworkStream stream = client.tcpClient.GetStream(); int read = stream.EndRead(ar); if (read != 0) { // data extraction/light processing of received data client.Append(read); stream.BeginRead(client.buffer, 0, client.tcpClient.ReceiveBufferSize, new AsyncCallback(StreamReadCompleteCallback), client); } else { DisconnectClient(client); } } catch (Exception ex) { onStatusUpdate(ex.Message); } }

Todo esto funciona bien, puedo aceptar conexiones y leer desde múltiples dispositivos cliente, etc.

Mi pregunta es: este método de lectura continua de clientes conectados hace que cada conexión tenga un subproceso de trabajo que está esperando que BeginRead regrese.

Entonces, si tengo 10 conexiones, tengo 10 BeginReads en marcha.

Parece un desperdicio tener tantos hilos de trabajo sentados esperando a leer. ¿Hay alguna otra forma mejor de lograr esto? Eventualmente me quedo sin memoria para agregar conexiones si tengo una gran cantidad de conexiones activas.

¿Sería una solución tener un hilo que sondee la propiedad DataAvailable de cada conexión hasta que aparezca algo y luego haga que un hilo para leer / procesar sea una solución?

¿O está creando todos estos hilos de trabajo no tan grandes como yo pienso?