.net - puerto - tcp en c#
Orden correcta para cerrar las conexiones TcpListener y TcpClient(de qué lado debe estar el cierre activo) (3)
¿Cuál de estos enfoques es el camino correcto y por qué? (Suponiendo que quiero que el cliente sea el lado ''activo'')
En la situación ideal, haría que su servidor envíe un paquete RequestDisconnect
con un determinado código de operación al cliente, que luego el cliente maneja cerrando la conexión al recibir ese paquete. De esa manera, no terminas con sockets obsoletos en el lado del servidor (ya que los sockets son recursos y los recursos son finitos, por lo que tener stales es algo malo).
Si el cliente ejecuta su secuencia de desconexión desechando el socket (o llamando a Close()
si está usando un TcpClient
, colocará el socket en el estado CLOSE_WAIT
en el servidor, lo que significa que la conexión está en proceso de ser cerrada). .
¿Hay una mejor manera de implementar el enfoque 3? Queremos que el cierre sea iniciado por el cliente (para que el cliente se quede con las TIME_WAITs), pero cuando el cliente se cierra, también queremos cerrar la conexión en el servidor.
Sí. Igual que el anterior, haga que el servidor envíe un paquete para solicitar al cliente que cierre su conexión al servidor.
Mi escenario es en realidad opuesto a un servidor web; Tengo un solo cliente que se conecta y desconecta de muchas máquinas remotas diferentes. Prefiero que el servidor tenga conexiones bloqueadas en TIME_WAIT en su lugar, para liberar recursos en mi cliente. En este caso, ¿debería hacer que el servidor realice el cierre activo y poner el modo de suspensión / cierre en mi cliente?
Sí, si es lo que busca, no dude en llamar a Dispose
en el servidor para deshacerse de los clientes.
En una nota al margen, es posible que desee considerar el uso de objetos Socket procesar en lugar del TcpClient
, ya que es extremadamente limitado. Si opera directamente en sockets, tiene SendAsync
y todos los otros métodos asíncronos para las operaciones de socket a su disposición. El uso de las llamadas Thread.Sleep
manuales es algo que evitaría, ya que estas operaciones son asíncronas por naturaleza. La desconexión después de haber escrito algo en una secuencia se debe hacer en la devolución de llamada de SendAsync
lugar de después de una Sleep
.
Leí esta respuesta en una pregunta anterior que dice:
Por lo tanto, el interlocutor que inicia la terminación, es decir, llama a close () primero, terminará en el estado TIME_WAIT. [...]
Sin embargo, puede ser un problema con muchos sockets en el estado TIME_WAIT en un servidor, ya que podría eventualmente evitar que se acepten nuevas conexiones. [...]
En su lugar, diseñe su protocolo de aplicación para que la terminación de la conexión siempre se inicie desde el lado del cliente . Si el cliente siempre sabe cuándo ha leído todos los datos restantes, puede iniciar la secuencia de terminación. Como ejemplo, un navegador sabe por el encabezado HTTP Content-Length cuando ha leído todos los datos y puede iniciar el cierre. (Sé que en HTTP 1.1 lo mantendrá abierto por un tiempo para una posible reutilización, y luego lo cerrará).
Me gustaría implementar esto usando TcpClient / TcpListener, pero no está claro cómo hacerlo funcionar correctamente.
Enfoque 1: ambos lados se cierran
Esta es la forma típica en que se ilustran los ejemplos de MSDN: ambos lados llaman a Close()
, no solo al cliente:
private static void AcceptLoop()
{
listener.BeginAcceptTcpClient(ar =>
{
var tcpClient = listener.EndAcceptTcpClient(ar);
ThreadPool.QueueUserWorkItem(delegate
{
var stream = tcpClient.GetStream();
ReadSomeData(stream);
WriteSomeData(stream);
tcpClient.Close(); <---- note
});
AcceptLoop();
}, null);
}
private static void ExecuteClient()
{
using (var client = new TcpClient())
{
client.Connect("localhost", 8012);
using (var stream = client.GetStream())
{
WriteSomeData(stream);
ReadSomeData(stream);
}
}
}
Después de ejecutar esto con 20 clientes, TCPView muestra muchos sockets tanto del cliente como del servidor atascados en TIME_WAIT
, que tardan bastante tiempo en desaparecer.
Enfoque 2: solo cierre cliente
Según las citas anteriores, eliminé las llamadas Close()
en mi oyente, y ahora solo confío en el cierre del cliente:
var tcpClient = listener.EndAcceptTcpClient(ar);
ThreadPool.QueueUserWorkItem(delegate
{
var stream = tcpClient.GetStream();
ReadSomeData(stream);
WriteSomeData(stream);
// tcpClient.Close(); <-- Let the client close
});
AcceptLoop();
Ahora ya no tengo TIME_WAIT
, pero sí me quedan zócalos en varias etapas de CLOSE_WAIT
, FIN_WAIT
, etc., que también tardan mucho tiempo en desaparecer.
Enfoque 3: dar tiempo al cliente para cerrar primero
Esta vez agregué un retraso antes de cerrar la conexión del servidor:
var tcpClient = listener.EndAcceptTcpClient(ar);
ThreadPool.QueueUserWorkItem(delegate
{
var stream = tcpClient.GetStream();
ReadSomeData(stream);
WriteSomeData(stream);
Thread.Sleep(100); // <-- Give the client the opportunity to close first
tcpClient.Close(); // <-- Now server closes
});
AcceptLoop();
Esto parece ser mejor: ahora solo hay sockets de cliente en TIME_WAIT
; Los sockets del servidor se han cerrado correctamente:
Esto parece estar de acuerdo con lo que dice el artículo previamente vinculado:
Por lo tanto, el interlocutor que inicia la terminación, es decir, llama a close () primero, terminará en el estado TIME_WAIT.
Preguntas:
- ¿Cuál de estos enfoques es el camino correcto y por qué? (Suponiendo que quiero que el cliente sea el lado ''activo'')
- ¿Hay una mejor manera de implementar el enfoque 3? Queremos que el cierre sea iniciado por el cliente (para que el cliente se quede con las TIME_WAITs), pero cuando el cliente se cierra, también queremos cerrar la conexión en el servidor.
- Mi escenario es en realidad opuesto a un servidor web; Tengo un solo cliente que se conecta y desconecta de muchas máquinas remotas diferentes. Prefiero que el servidor tenga conexiones bloqueadas en
TIME_WAIT
en su lugar, para liberar recursos en mi cliente. En este caso, ¿debería hacer que el servidor realice el cierre activo y poner el modo de suspensión / cierre en mi cliente?
El código completo para probarlo usted mismo está aquí:
https://gist.github.com/PaulStovell/a58cd48a5c6b14885cf3
Edición : otro recurso útil:
Para un servidor que establece conexiones salientes además de aceptar conexiones entrantes, la regla de oro es asegurarse siempre de que si se produce un TIME_WAIT que termine en el otro par y no en el servidor. La mejor manera de hacer esto es nunca iniciar un cierre activo desde el servidor, sin importar el motivo. Si su interlocutor se agota, cancele la conexión con un RST en lugar de cerrarlo. Si su interlocutor envía datos no válidos, cancele la conexión, etc. La idea es que si su servidor nunca inicia un cierre activo, nunca puede acumular sockets TIME_WAIT y, por lo tanto, nunca sufrirá los problemas de escalabilidad que causan. Aunque es fácil ver cómo puede abortar las conexiones cuando ocurren situaciones de error, ¿qué sucede con la terminación normal de la conexión? Lo ideal es que diseñe en su protocolo una forma en que el servidor le diga al cliente que debe desconectarse, en lugar de simplemente hacer que el servidor instigue un cierre activo. Entonces, si el servidor necesita terminar una conexión, el servidor envía un mensaje de "hemos terminado" a nivel de la aplicación que el cliente toma como una razón para cerrar la conexión. Si el cliente no puede cerrar la conexión en un tiempo razonable, el servidor cancela la conexión.
En el cliente, las cosas son un poco más complicadas, después de todo, alguien tiene que iniciar un cierre activo para terminar una conexión TCP limpiamente, y si es el cliente, ahí es donde terminará TIME_WAIT. Sin embargo, tener TIME_WAIT en el cliente tiene varias ventajas. Primero, si, por alguna razón, el cliente termina con problemas de conectividad debido a la acumulación de sockets en TIME_WAIT, es solo un cliente. Otros clientes no serán afectados. En segundo lugar, es ineficiente abrir y cerrar rápidamente las conexiones TCP al mismo servidor, por lo que tiene sentido más allá del problema de TIME_WAIT tratar de mantener las conexiones durante períodos de tiempo más largos en lugar de períodos de tiempo más cortos. No diseñe un protocolo mediante el cual un cliente se conecte al servidor cada minuto y lo haga abriendo una nueva conexión. En su lugar, use un diseño de conexión persistente y vuelva a conectarse solo cuando la conexión falla, si los enrutadores intermedios se niegan a mantener la conexión abierta sin flujo de datos, entonces puede implementar un ping de nivel de aplicación, usar TCP mantener vivo o simplemente aceptar que el enrutador está reiniciando su conexión ; Lo bueno es que no estás acumulando sockets TIME_WAIT. Si el trabajo que realiza en una conexión es naturalmente de corta duración, entonces considere alguna forma de diseño de "agrupación de conexiones" por el cual la conexión se mantiene abierta y se reutiliza. Finalmente, si es absolutamente necesario abrir y cerrar las conexiones rápidamente desde un cliente al mismo servidor, entonces quizás pueda diseñar una secuencia de cierre de nivel de aplicación que pueda usar y luego seguir esto con un cierre abortivo. Su cliente podría enviar un mensaje de "He terminado", su servidor podría enviar un mensaje de "adiós" y el cliente podría entonces interrumpir la conexión.
Así es como funciona el TCP, no puedes evitarlo. Puede establecer diferentes tiempos de espera para TIME_WAIT o FIN_WAIT en su servidor, pero eso es todo.
La razón de esto es que en TCP, un paquete puede llegar a un socket que ha cerrado hace mucho tiempo. Si ya tiene otro socket abierto en la misma IP y puerto, recibiría datos destinados a la sesión anterior, lo que confundiría el infierno. Especialmente dado que la mayoría de la gente considera que TCP es confiable :)
Si tanto su cliente como su servidor implementan TCP correctamente (por ejemplo, manejar el cierre limpio correctamente), no importa si el cliente o el servidor cerraron la conexión. Ya que parece que manejas ambos lados, no debería ser un problema.
Su problema parece ser con el cierre correcto en el servidor. Cuando un lado del zócalo se cierra, el otro lado Read
con una longitud de 0
; ese es su mensaje de que la comunicación ha finalizado. Lo más probable es que ignore esto en el código de su servidor, es un caso especial que dice "ahora puede desechar este zócalo de forma segura, hágalo ahora".
En su caso, un cierre del lado del servidor parece ser el mejor ajuste.
Pero en realidad, el TCP es bastante complicado. No ayuda que la mayoría de las muestras en Internet tengan fallas severas (especialmente las muestras para C # - no es demasiado difícil encontrar una buena muestra para C ++, por ejemplo) e ignoran muchas de las partes importantes del protocolo. Tengo una muestra simple de algo que podría funcionar para usted: https://github.com/Luaancz/Networking/tree/master/Networking%20Part%201 Todavía no es el TCP perfecto, pero es mucho mejor que el Ejemplos de MSDN, por ejemplo.
Paul, tú también has hecho una gran investigación. También he estado trabajando un poco en esta área. Un artículo que he encontrado muy útil sobre el tema de TIME_WAIT es este:
http://vincent.bernat.im/en/blog/2014-tcp-time-wait-state-linux.html
Tiene algunas preocupaciones específicas de Linux, pero todo el contenido de nivel TCP es universal.
En última instancia, ambos lados deberían cerrarse (es decir, completar el protocolo de enlace FIN / ACK), ya que no desea que los estados FIN_WAIT o CLOSE_WAIT permanezcan prolongados, esto es simplemente "mal" TCP. Evitaría usar RST para forzar el cierre de las conexiones, ya que es probable que cause problemas en otros lugares y simplemente se sienta como un mal cibernético.
Es cierto que el estado TIME_WAIT ocurrirá en el extremo que finaliza la conexión primero (es decir, envía el primer paquete FIN) y que debe optimizar para cerrar la conexión primero en el extremo que tendrá la menor rotación de conexiones.
En Windows, tendrá un poco más de 15,000 puertos TCP disponibles por IP de manera predeterminada, por lo que necesitará una conexión de conexión decente para lograrlo. La memoria para que los TCB rastreen los estados TIME_WAIT deben ser bastante aceptables.
https://support.microsoft.com/kb/929851
También es importante tener en cuenta que una conexión TCP puede estar medio cerrada. Es decir, un extremo puede optar por cerrar la conexión para enviar pero dejarla abierta para recibir. En .NET esto se hace así:
tcpClient.Client.Shutdown(SocketShutdown.Send);
http://msdn.microsoft.com/en-us/library/system.net.sockets.socket.shutdown.aspx
Encontré esto necesario al portar parte de la herramienta netcat de Linux a PowerShell:
http://www.powershellmagazine.com/2014/10/03/building-netcat-with-powershell/
Debo repetir el consejo de que si puedes mantener una conexión abierta e inactiva hasta que la necesites de nuevo, esto generalmente tiene un impacto masivo en la reducción de los TIME_WAIT.
Más allá de eso, intente medir cuando los TIME_WAIT se conviertan en un problema ... realmente se necesita mucha conexión para agotar los recursos TCP.
Espero que algo de esto sea de ayuda.