C# - TCP KeepAlive의 서버 측 구현
지난 이야기에서는,
C# - TCP KeepAlive에 새로 추가된 Retry 옵션
; https://www.sysnet.pe.kr/2/0/13531
클라이언트로 예제를 사용했는데요, 사실 현업으로 따지자면 서버 측에서의 구현이 더 일반적일 수 있습니다. 왜냐하면, 서버는 다수의 클라이언트와 연결을 맺는 것이므로 쓸데없는 연결이 남게 되면 자칫 자원 고갈의 문제로 이어질 수 있기 때문입니다. 따라서 끊긴 클라이언트를 가능하면 빠르게 인지하는 것이 서버 입장에서는 더 필요할 수밖에 없습니다.
일단, (클라이언트와는 달리) 서버는 그나마 자유롭게 고를 수 있으므로 Windows Server 2019 이상으로 가정한다면 TCP 레벨에 추가된 3가지 옵션(TcpKeepAliveTime, TcpKeepAliveInterval, TcpKeepAliveRetryCount)을 걱정 없이 사용할 수 있을 것입니다.
적용 방법은 간단한데요, Server 소켓에만 적용해 주면 이후 Accept한 클라이언트 소켓에 서버의 설정이 상속되므로 다음과 같이 간단하게 KeepAlive 설정을 할 수 있습니다.
int port = 18500;
Socket listenSocket = new Socket(AddressFamily.InterNetwork,
SocketType.Stream,
ProtocolType.Tcp);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) == false
|| Environment.OSVersion.Version < new Version(10, 0, 15063))
{
throw new NotSupportedException();
}
listenSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
listenSocket.SetSocketOption(SocketOptionLevel.Tcp, (SocketOptionName)3, 1);
listenSocket.SetSocketOption(SocketOptionLevel.Tcp, (SocketOptionName)17, 3);
listenSocket.SetSocketOption(SocketOptionLevel.Tcp, (SocketOptionName)16, 10);
이후, 실제로 (VM을 Pause 시킨 순간의) 연결 끊김을 알아내기 위해 Ping 코드를 추가해 다음과 같이 테스트 서버를 완성할 수 있습니다.
IPEndPoint ep = new IPEndPoint(IPAddress.Any, port);
listenSocket.Bind(ep);
listenSocket.Listen(5);
while (true)
{
Socket clientSocket = listenSocket.Accept();
Log($"{clientSocket} connected.");
bool pingEnabled = true;
Task.Run(() =>
{
bool connected = true;
IPEndPoint? ep = clientSocket.RemoteEndPoint as IPEndPoint;
if (ep == null)
{
return;
}
while (pingEnabled)
{
Ping ping = new Ping();
PingOptions options = new PingOptions();
options.DontFragment = true;
string data = "test";
byte[] buffer = ASCIIEncoding.ASCII.GetBytes(data);
int timeout = 300;
PingReply reply = ping.Send(ep.Address, timeout, buffer, options);
bool replied = reply.Status == IPStatus.Success;
if (connected != replied)
{
connected = replied;
Log($"Status changed to {reply.Status}");
}
Thread.Sleep(32);
}
});
Task.Run(() =>
{
Log("SendData");
clientSocket.Send(new byte[4] { 1, 2, 3, 4 });
Log("Wait for receiving");
try
{
clientSocket.Receive(new byte[4]);
Log($"Received");
}
catch (Exception e)
{
Log($"Exception thrown: {e.Message}");
}
pingEnabled = false;
});
}
이에 대응하는 클라이언트 코드는 기본으로만 만들면 됩니다.
using System.Net.Sockets;
namespace ConsoleApp2;
internal class Program
{
static void Main(string[] args)
{
string host = "192.168.0.22";
int port = 18500;
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.Connect(host, port);
Log("Connected");
int received = socket.Receive(new byte[4]);
Log("Received");
try
{
socket.Receive(new byte[4]);
Log("Received");
}
catch (Exception ex)
{
Log($"Exception thrown: {ex.Message}");
}
socket.Close();
}
private static void Log(string text)
{
Console.WriteLine($"[{DateTime.Now:mm ss fff}] {text}");
}
}
이후, 서버와 클라이언트를 실행하고 나서 클라이언트가 실행된 VM을 Pause 시키면 서버에는 다음과 같은 로그가 남게 됩니다.
[02 05 573] Status changed to TimedOut
[02 36 791] Exception thrown: 연결된 구성원으로부터 응답이 없어 연결하지 못했거나, 호스트로부터 응답이 없어 연결이 끊어졌습니다.
약 31초 만에 끊겼으니 KeepAlive에 설정한 대로 동작했습니다. 테스트를 위해 살짝 parameter를 바꿔보면,
SocketOptionName.KeepAlive == 1
SocketOptionName.TcpKeepAliveInterval == 1
SocketOptionName.TcpKeepAliveRetryCount == 10
이제 실행 결과는 10여 초 만에 끊기도록 나옵니다.
[17 34 240] Status changed to TimedOut
[17 44 580] Exception thrown: 연결된 구성원으로부터 응답이 없어 연결하지 못했거나, 호스트로부터 응답이 없어 연결이 끊어졌습니다.
참고로, Accept 이후 얻게 된 클라이언트 소켓에서 기본값을 재정의하는 것도 가능합니다.
Socket clientSocket = listenSocket.Accept();
clientSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
clientSocket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, 1);
clientSocket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, 3);
clientSocket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount, 10);
현실적으로는, 저렇게 클라이언트마다 재정의하는 것이 필요한 상황은 거의 없을 것입니다.
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]