성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] 그냥 RSS Reader 기능과 약간의 UI 편의성 때문에 사용...
[이종효] 오래된 소프트웨어는 보안 위협이 되기도 합니다. 혹시 어떤 기능...
[정성태] @Keystroke IEEE의 문서를 소개해 주시다니... +_...
[손민수 (Keystroke)] 괜히 듀얼채널 구성할 때 한번에 같은 제품 사라고 하는 것이 아...
[정성태] 전각(Full-width)/반각(Half-width) 기능을 토...
[정성태] Vector에 대한 내용은 없습니다. Vector가 닷넷 BCL...
[orion] 글 읽고 찾아보니 디자인 타임에는 InitializeCompon...
[orion] 연휴 전에 재현 프로젝트 올리자 생각해 놓고 여의치 않아서 못 ...
[정성태] 아래의 글에 정리했으니 참고하세요. C# - Typed D...
[정성태] 간단한 재현 프로젝트라도 있을까요? 저런 식으로 설명만 해...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>C# - UDP Datagram의 최대 크기</h1> <p> IP 프로토콜에서 datagram의 크기는 일반적으로 65,535 바이트까지 가능합니다. 그중에서 20바이트 IP 헤더와 8바이트 UDP 헤더를 제외하면 65,507을 데이터로 사용할 수 있습니다.<br /> <br /> 혹시, 그 65,507과 같은 크기를 구할 수 있을까요? Windows의 경우, <a target='tab' href='https://learn.microsoft.com/en-us/windows/win32/winsock/socket-options-and-ioctls-2'>getsockopt에 SO_MAX_MSG_SIZE</a> 옵션을 제공하므로 다음과 같이 구하는 것이 가능합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using System.Net.Sockets; using System.Net; using System.Text; internal class Program { public static int SOL_SOCKET { get { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { return 0xffff; } return 1; } } public const int SO_MAX_MSG_SIZE = 0x2003; // 8195 static void Main(string[] args) { using (UdpClient listener = new UdpClient(60900)) { byte[] buffer = new byte[4]; listener.Client.<a target='tab' href='https://www.sysnet.pe.kr/2/0/13226'>GetRawSocketOption</a>(<span style='color: blue; font-weight: bold'>SOL_SOCKET, SO_MAX_MSG_SIZE, buffer);</span> int result = BitConverter.ToInt32(buffer, 0); Console.WriteLine($"MSG_SIZE: {result}"); // 출력 결과: 65507 } } } </pre> <br /> 반면, 리눅스의 경우에는 이 값을 구하는 방법이 딱히 없습니다. 위의 C# 프로그램을 리눅스에서 실행하면 GetRawSocketOption 실행 시 "System.Net.Sockets.SocketException: 'Operation not supported'" 오류가 발생하는데요, 닷넷 BCL의 문제가 아닌 C/C++로 호출한 경우에도 이 옵션은 리눅스에서 제공하지 않는 것을 알 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > #include <cstdio> #include <sys/socket.h> int main() { int value = 0; socklen_t len = sizeof(value); int fd = socket(AF_INET, SOCK_DGRAM, 0); int result = getsockopt(fd, SOL_SOCKET, <span style='color: blue; font-weight: bold'>0x2003</span>, &value, &len); // 위의 코드는 사실 의미가 없는데, <a target='tab' href='https://www.sysnet.pe.kr/2/0/13226'>Windows와 Linux는 옵션 값이 다르기</a> 때문에 리눅스에서 SO_MAX_MSG_SIZE 옵션을 제공한다 해도 값이 0x2003이진 않을 것입니다. printf("%d, (%d)\n", value, result); // 출력 결과: 0, -1 return 0; } </pre> <br /> 어쨌든, 이 크기보다 큰 datagram을 보내려고 하면 예외가 발생합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using (UdpClient client = new UdpClient()) { // <a target='tab' href='https://www.sysnet.pe.kr/2/0/13227#windows_mtu'>만약 아래의 IP 주소를 127.0.0.1로 하면</a> 예외가 발생하지 않습니다. client.Connect(IPAddress.Parse("192.168.100.50"), 65000); client.Send(new byte[<span style='color: blue; font-weight: bold'>65507 + 1</span>]); } /* // Send에서 예외 발생 // Windows 환경의 오류 메시지 (<a target='tab' href='https://learn.microsoft.com/en-us/windows/win32/winsock/windows-sockets-error-codes-2'>WSAEMSGSIZE 10040</a>) Unhandled exception. System.Net.Sockets.SocketException (10040): A message sent on a datagram socket was larger than the internal message buffer or some other network limit, or the buffer used to receive a datagram into was smaller than the datagram itself. at System.Net.Sockets.UdpClient.Send(ReadOnlySpan`1 datagram) at Program.Main(String[] args) in C:\temp\ConsoleApp1\ConsoleApp1\Program.cs:line 39 // WSL/Linux 환경의 오류 메시지 Unhandled exception. System.Net.Sockets.SocketException (90): Message too long at System.Net.Sockets.UdpClient.Send(ReadOnlySpan`1 datagram) at Program.Main(String[] args) in C:\temp\ConsoleApp1\ConsoleApp1\Program.cs:line 38 */ </pre> <br /> <hr style='width: 50%' /><br /> <br /> 위와 같이 얻은 UDP Datagram의 크기는 엄밀히 로컬 PC에서만 유효한 것입니다. 일단, 패킷이 네트워크를 타고 흐르면 각각의 네트워크 장비가 정한 규격으로 인해 65,507 바이트를 정상적으로 전송하지 못할 수 있습니다. 이에 대해 검색해 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > What is the largest Safe UDP Packet Size on the Internet ; <a target='tab' href='https://stackoverflow.com/questions/1098897/what-is-the-largest-safe-udp-packet-size-on-the-internet'>https://stackoverflow.com/questions/1098897/what-is-the-largest-safe-udp-packet-size-on-the-internet</a> </pre> <br /> <a target='tab' href='https://www.rfc-editor.org/rfc/rfc1122'>RFC 1122 표준 문서</a>상으로는 "reassemble" 가능한 최소 크기를 576 바이트라고 명시했다고 합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 3.3.2 Reassembly The IP layer MUST implement reassembly of IP datagrams. We designate the largest datagram size that can be reassembled by EMTU_R ("Effective MTU to receive"); this is sometimes called the "reassembly buffer size". EMTU_R MUST be greater than or equal to 576, SHOULD be either configurable or indefinite, and SHOULD be greater than or equal to the MTU of the connected network(s). </pre> <br /> 따라서, 어쩌면 인터넷에는 실제로 최소 규격만을 만족하는 장비가 있을 가능성을 배제할 수는 없으므로 가능한 모든 환경에서 테스트하는 것이 권장됩니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> UDP datagram이 물론 프로토콜상으로는 메시지 단위로 한 번에 전송은 되지만, 하위 프로토콜로 전달되면서, 예를 들어 ethernet을 사용한다면 <a target='tab' href='https://www.sysnet.pe.kr/2/0/13227'>MTU 1500 바이트의 크기</a>로 쪼개져서 전달됩니다. 따라서 만약 1개의 ehternet 패킷으로 UDP를 보내고 싶다면 1500에서 IP header 20바이트, UDP header 8바이트를 뺀 1472 크기가 <a target='tab' href='https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-sendto'>sendto</a>에 전송할 수 있는 실제 데이터 크기가 됩니다.<br /> <br /> 재미있는 건, 실제로 UDP datagram을 MTU 크기까지만, 즉 1개의 패킷으로만 전송하도록 강제하는 옵션이 있다는 점입니다. <br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public const int IPPROTO_IP = 0; public static int IP_MTU_DISCOVER { get { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { return 71; } return 10; } } public static int IP_PMTUDISC_DO { get { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { return 1; } return 2; } } using (UdpClient client = new UdpClient()) { client.Connect(IPAddress.Parse("192.168.100.50"), 65000); byte[] buffer = BitConverter.GetBytes(IP_PMTUDISC_DO); <span style='color: blue; font-weight: bold'>client.Client.SetRawSocketOption(IPPROTO_IP, IP_MTU_DISCOVER, buffer);</span> <span style='color: blue; font-weight: bold'>int sentBytes = client.Send(new byte[1473]);</span> // 예외 발생 Console.WriteLine(sentBytes); } </pre> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > import socket import sys <span style='color: blue; font-weight: bold'>length = 1473</span> text = 'a' * length IP_MTU_DISCOVER = 10 # linux 10, windows 71 IP_PMTUDISC_DO = 2 # linux 2, windows 1 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) <span style='color: blue; font-weight: bold'>sock.setsockopt(socket.IPPROTO_IP, IP_MTU_DISCOVER, IP_PMTUDISC_DO)</span> endpoint = ('172.28.96.1', 60900) sent_bytes = sock.sendto(text.encode(), endpoint) /* 예외 발생 $ <span style='color: blue; font-weight: bold'>python test.py</span> Traceback (most recent call last): File "test.py", line 10, in <module> sent_bytes = sock.sendto(text.encode(), endpoint) OSError: [Errno 90] Message too long */ </pre> <br /> 위와 같이 IP_MTU_DISCOVER, IP_PMTUDISC_DO 옵션을 적용하면, 이제 UDP datagram의 크기는 반드시 1개의 패킷으로 전달되도록 강제가 됩니다. 따라서, 위의 코드를 수행하면 1473 바이트를 전송하므로 예외가 발생합니다.<br /> <br /> (참고로, IP_PMTUDISC_DO를 이용해 IP MTU 크기만큼 제약을 해도 SO_MAX_MSG_SIZE로 반환하는 값은 여전히 65507로 나옵니다.)<br /> <br /> <hr style='width: 50%' /><br /> <br /> 닷넷의 경우, 위의 옵션을 간단하게 Socket 타입의 DontFragment 옵션을 설정하는 것으로도 가능합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using (UdpClient client = new UdpClient()) { client.Connect(IPAddress.Parse("192.168.100.50"), 65000); <span style='color: blue; font-weight: bold'>client.DontFragment = true;</span> // 또는 client.Client.DontFragment = true; sentBytes = client.Send(new byte[<span style='color: blue; font-weight: bold'>1473</span>]); // 예외 발생 } </pre> <br /> DontFragment 속성의 소스 코드를 분석하면 결국 이렇게도 구현할 수 있는 것입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > client.Client.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.DontFragment, 1); </pre> <br /> 위의 소스 코드를 SetRawSocketOption으로는 이렇게 바꿀 수 있는데요,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // Windows에서만 동작 // <a target='tab' href='https://learn.microsoft.com/en-us/troubleshoot/windows/win32/header-library-requirement-socket-ipproto-ip#ipproto_ip-level-socket-options-in-ws2tcpiph'>https://learn.microsoft.com/en-us/troubleshoot/windows/win32/header-library-requirement-socket-ipproto-ip#ipproto_ip-level-socket-options-in-ws2tcpiph</a> byte[] buffer = BitConverter.GetBytes(1); client.Client.SetRawSocketOption(IPPROTO_IP, 14, buffer); </pre> <br /> 아쉽게도 Linux에서는 동작하지 않습니다. netinet/in.h 헤더 파일을 보면, <br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // <a target='tab' href='https://codebrowser.dev/glibc/glibc/sysdeps/unix/sysv/linux/bits/in.h.html'>https://codebrowser.dev/glibc/glibc/sysdeps/unix/sysv/linux/bits/in.h.html</a> #define IPV6_DONTFRAG 62 </pre> <br /> IPv6에 대한 옵션은 있어도 IPv4에 해당하는 IP_DONTFRAG나 IPV4_DONTFRAG 상수를 찾을 수가 없습니다. 단지 몇몇 문서를 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <a target='tab' href='https://manpages.ubuntu.com/manpages/xenial/en/man4/rawip.4freebsd.html'>https://manpages.ubuntu.com/manpages/xenial/en/man4/rawip.4freebsd.html</a> </pre> <br /> IP_DONTFRAG가 있는데, 아마도 리눅스 종류에 따라 다른 듯합니다.<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
1176
(왼쪽의 숫자를 입력해야 합니다.)