C# - Windows 10/2019부터 추가된 SIO_TCP_INFO
우연히 다음의 pdf 자료를 읽게 되었는데,
TCP improvements in the Windows network stack
; https://datatracker.ietf.org/meeting/98/materials/slides-98-tcpm-tcp-improvements-in-windows-01
Windows 10 Creators 업데이트(빌드 15014+)부터 SIO_TCP_INFO 옵션이 I/O Control Code에 추가되었다고 합니다. ^^ 리눅스의 TCP_INFO API를 따온 거라고 하는데 소켓 단위로 상태 조회를 할 수 있는 기능입니다.
C#으로 간단하게 테스트 코드를 만들어 보면 대충 이렇습니다.
using System;
using System.IO;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Text;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
string ipAddr = "www.naver.com";
int port = 80;
using (Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
{
try
{
Console.WriteLine($"{DateTime.Now} Connecting...");
client.Connect(ipAddr, port);
Console.WriteLine($"{DateTime.Now} Connected.");
string request = @"GET / HTTP/1.0
HOST: www.naver.com
";
byte[] buffer = Encoding.UTF8.GetBytes(request);
Console.WriteLine($"Sent: {buffer.Length}");
client.Send(buffer);
MemoryStream ms = new MemoryStream();
while (true)
{
byte[] rcvBuffer = new byte[4096];
int nRecv = client.Receive(rcvBuffer);
if (nRecv == 0)
{
break;
}
ms.Write(rcvBuffer, 0, nRecv);
}
Console.WriteLine($"Receive: {ms.Length}");
DisplaySocketInfo(client);
client.Close();
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
Console.WriteLine($"{DateTime.Now} Failed.");
}
}
}
private static void DisplaySocketInfo(Socket client)
{
TCP_INFO_v0 info = new TCP_INFO_v0();
const int SIO_TCP_INFO = unchecked((int)0xd8000027);
byte[] version = new byte[4];
byte[] tcpInfo = new byte[Marshal.SizeOf(info)];
try
{
if (client.IOControl(SIO_TCP_INFO, version, tcpInfo) == tcpInfo.Length)
{
unsafe
{
fixed (byte* ptr = tcpInfo)
{
IntPtr ptrBuf = new IntPtr(ptr);
info = (TCP_INFO_v0)Marshal.PtrToStructure(ptrBuf, typeof(TCP_INFO_v0));
}
}
}
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
return;
}
Console.WriteLine($"State: {info.State}"); // 소켓 상태 출력
Console.WriteLine($"Connection: {info.ConnectionTimeMs}ms"); // 연결 시 걸린 시간 출력
Console.WriteLine($"Bytes-In: {info.BytesIn} bytes(s)"); // 소켓에서 Receive한 총 데이터
Console.WriteLine($"Bytes-Out: {info.BytesOut} bytes(s)"); // 소켓에서 Send한 총 데이터
}
}
}
public enum TCPSTATE
{
CLOSED,
LISTEN,
SYN_SENT,
SYN_RCVD,
ESTABLISHED,
FIN_WAIT_1,
FIN_WAIT_2,
CLOSE_WAIT,
CLOSING,
LAST_ACK,
TIME_WAIT,
MAX
};
[StructLayout(LayoutKind.Sequential)]
public struct TCP_INFO_v0
{
public TCPSTATE State;
public uint Mss;
public ulong ConnectionTimeMs;
[MarshalAs(UnmanagedType.U1)]
public bool TimestampsEnabled;
public uint RttUs;
public uint MinRttUs;
public uint BytesInFlight;
public uint Cwnd;
public uint SndWnd;
public uint RcvWnd;
public uint RcvBuf;
public ulong BytesOut;
public ulong BytesIn;
public uint BytesReordered;
public uint BytesRetrans;
public uint FastRetrans;
public uint DupAcksIn;
public uint TimeoutEpisodes;
public byte SynRetrans;
}
실행하면 결과는 이렇고!
2020-12-14 오전 1:16:42 Connecting...
2020-12-14 오전 1:16:42 Connected.
Sent: 39
Receive: 334
State: CLOSE_WAIT
Connection: 19ms
Bytes-In: 334 bytes(s)
Bytes-Out: 39 bytes(s)
참고로, 몇 가지 예외 상황을 살펴보면.
SIO_TCP_INFO 옵션을 지원하지 않는 환경(예를 들어 Windows Server 2016)에서 Ioctl 명령을 수행하면 (C++에서는 WSAEOPNOTSUPP(10045) 오류 코드 반환) 0x80004005 예외가 발생합니다.
System.Net.Sockets.SocketException (0x80004005): The attempted operation is not supported for the type of object referenced
at System.Net.Sockets.Socket.IOControl(Int32 ioControlCode, Byte[] optionInValue, Byte[] optionOutValue)
at ConsoleApp1.Program.Main(String[] args) in C:\ConsoleApp1\ConsoleApp1\Program.cs:line 27
또한, 이 오류는 소켓을 Connect 하기 전에 실행해도 동일하게 발생하는데요, 따라서 만약 connect 여부를 판단할 수 없는 소켓에 대해서도 SIO_TCP_INFO 명령을 실행해야 한다면 예외 처리를 하거나, WSAIoctl 함수를 DllImport로 직접 호출하면 됩니다.
[DllImport("Ws2_32.dll")]
unsafe static extern int WSAIoctl(IntPtr s, uint dwIoControlCode, byte* lpvInBuffer, int cbInBuffer,
byte* lpvOutBuffer, int cbOutBuffer, ref int lpcbBytesReturned, IntPtr lpOverlapped, IntPtr lpCompletionRoutine);
using (Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
{
IntPtr nativeSocketHandle = client.Handle;
DisplaySocketInfo(nativeSocketHandle);
}
private unsafe static void DisplaySocketInfo(Socket socket)
{
TCP_INFO_v0 info;
const uint SIO_TCP_INFO = 0xd8000027;
int outBufferLen = Marshal.SizeOf(typeof(TCP_INFO_v0));
int returnBufferLen = 0;
byte* version = stackalloc byte[4];
byte* tcpInfo = stackalloc byte[outBufferLen];
if (WSAIoctl(socket.Handle, SIO_TCP_INFO, version, 4,
tcpInfo, outBufferLen, ref returnBufferLen, IntPtr.Zero, IntPtr.Zero) != 0)
{
return;
}
if (returnBufferLen != outBufferLen)
{
return;
}
unsafe
{
IntPtr ptrBuf = new IntPtr(tcpInfo);
info = (TCP_INFO_v0)Marshal.PtrToStructure(ptrBuf, typeof(TCP_INFO_v0));
}
Console.WriteLine($"State: {info.State}");
Console.WriteLine($"Connection: {info.ConnectionTimeMs}ms");
Console.WriteLine($"Bytes-In: {info.BytesIn} bytes(s)");
Console.WriteLine($"Bytes-Out: {info.BytesOut} bytes(s)");
}
재미있는 것은, Socket을 Close한 이후에도 SIO_TCP_INFO 옵션을 사용할 수 없는데 단지 이번에는 Socket.IOControl의 오류가 아닌, 닫힌 Socket 인스턴스를 사용하기 때문에 예외가 발생합니다.
System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'System.Net.Sockets.Socket'.
at System.Net.Sockets.Socket.IOControl(Int32 ioControlCode, Byte[] optionInValue, Byte[] optionOutValue)
at ConsoleApp1.Program.DisplaySocketInfo(Socket client) in C:\ConsoleApp1\ConsoleApp1\Program.cs:line 116
따라서 Socket의 닫힘 여부를 알 수 없는 상황에서도 Win32 API를 호출하는 것으로 우회할 수 있습니다. (그러니까... 결국, 그냥 Win32 API를 직접 호출하는 것이 더 편리합니다. ^^;)
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]