Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

Wireshark + C#으로 확인하는 PSH flag와 Nagle 알고리듬

근래의 환경에서, Nagle 알고리즘을 명시적으로 끄는 작업이 필요할까요? (정말 몰라서 묻는 겁니다. ^^)

예를 들어, TCP 소켓에서 다음과 같이 1 바이트를 보내면,

byte[] buf = new byte[1];
client.Send(buf, 0, buf.Length, SocketFlags.None);

Socket.NoDelay 속성의 기본값은 false이므로 Nagle 알고리듬을 사용하는 상태입니다. 따라서 우리가 알고 있는 지식이라면 1 바이트를 곧바로 보내지 않고 약간의 시간 차를 둬야 하는데, 실제로는 곧바로 전송이 됩니다.

그리고 이때의 패킷을 wireshark로 보면,

..client_ip... ..server_ip... TCP 55 1318 → 15000 [PSH, ACK] Seq=1 Ack=1 Win=262656 Len=1

그러한 역할을 하도록 PSH 플래그가 설정돼 있습니다. PSH가 어떤 것인지에 대해서는 아래의 글에서 잘 설명하고 있습니다.

TCP Flags: PSH 그리고 URG
; https://techlog.gurucat.net/314

즉, PSH 플래그가 설정된 패킷은 버퍼가 어느 정도 채워졌는지 고려할 필요 없이 곧바로 송신하는 역할을 담당합니다. 게다가 수신 측에서도 PSH가 설정된 패킷을 받으면 수신 버퍼의 상태를 고려하지 않고 곧바로 ACK 신호를 처리해 응용 프로그램의 recv 호출에 반응할 수 있게 합니다.

(딱히 이력을 찾을 수 없는데 원래부터 그랬던 것인지는 알 수 없으나) 어쨌든 적어도 Windows 10 환경에서 현재 TCP 소켓을 테스트해 보면, Socket.send 시 무조건 마지막 패킷에 PSH 플래그를 붙여 전송합니다. 즉, 1 바이트를 send하면 1바이트를 보내는 TCP 패킷에 PSH가 설정돼 위에서 테스트했던 것처럼 바로 전송이 되고, 20,000 바이트를 send하는 경우에는 다음과 같이 패킷 분할이 발생할 텐데,

4  3.304470 ..client_ip... ..server_ip... TCP 14654 2820 → 15000 [ACK] Seq=1 Ack=1 Win=262656 Len=14600
5  3.308772 ..server_ip... ..client_ip... TCP 60 15000 → 2820 [ACK] Seq=1 Ack=2921 Win=131328 Len=0
6  3.308772 ..server_ip... ..client_ip... TCP 60 15000 → 2820 [ACK] Seq=1 Ack=5841 Win=131328 Len=0
7  3.308820 ..client_ip... ..server_ip... TCP 5454 2820 → 15000 [PSH, ACK] Seq=14601 Ack=1 Win=262656 Len=5400
8  3.310578 ..server_ip... ..client_ip... TCP 60 15000 → 2820 [ACK] Seq=1 Ack=8761 Win=131328 Len=0
9  3.310578 ..server_ip... ..client_ip... TCP 60 15000 → 2820 [ACK] Seq=1 Ack=11681 Win=131328 Len=0
10 3.310578 ..server_ip... ..client_ip... TCP 60 15000 → 2820 [ACK] Seq=1 Ack=14601 Win=131328 Len=0
11 3.314316 ..server_ip... ..client_ip... TCP 60 15000 → 2820 [ACK] Seq=1 Ack=17521 Win=131328 Len=0
12 3.314316 ..server_ip... ..client_ip... TCP 60 15000 → 2820 [ACK] Seq=1 Ack=20001 Win=131328 Len=0

(send로 넘겨준 앞 부분의) 첫 패킷은 PSH 플래그가 없지만, 두 번째 (send에 넘겨줬던 마지막) 패킷에는 PSH를 붙여 전송합니다.




그런데, 위의 상황을 가만히 보면 기존의 Nagle 알고리듬을 꺼야 했던 상황과 똑같습니다. 단지 다른 점이 있다면, Nagle은 송신 측에서 작은 데이터를 일정 시간 동안 대기해 버퍼가 찰 기회를 줬다가 전송하는 기능이므로, 이것을 끄게 되면 곧바로 패킷에 담아 전송하는 역할만 합니다. 즉, 이것은 전송에서만 유효하며 수신 측 입장에서 보면 1바이트가 왔다고 해서 곧바로 ACK를 하고 응용 프로그램에 올려 보낼 것인지는 사실 Nagle 알고리듬 자체와는 무관한 문제입니다.

물론, Nagle 알고리듬이 꺼지면 PSH 플래그를 붙여 처리할 수도 있을 것입니다. 실제로 텔넷 같은 프로그램이 Nagle을 껐을 것이고, 패킷 캡처를 했을 때 PSH 플래그가 설정되었다는 것에서 그 사실을 유추할 수 있습니다.

그러니까, 현재의 기능을 비교해 보면 굳이 Nagle 알고리듬을 끄든지/켜든지 달라지는 것이 없습니다.

Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
client.NoDelay = false; // 이 코드에 상관없이 send는 곧바로 해당 데이터를 서버로 바로 전송

분명히 예전 자료를 보면, telnet 등의 서비스를 구현할 때 키보드 입력에 대한 반응성을 향상시키기 위해 Nagle을 꺼야 하는 옵션이 사용되었다는 것을 감안할 때 아마도 이전에는 send 호출 시 마지막에 자동으로 붙였던 것은 아닌 듯합니다. (혹시 이에 대한 이력을 아시는 분은 덧글 부탁드립니다. ^^)




제가 찾아본 공식 문서로는, 원래 RFC793 문서에 PSH 플래그의 역할을 찾아볼 수 있습니다.

TRANSMISSION CONTROL PROTOCOL
DARPA INTERNET PROGRAM
PROTOCOL SPECIFICATION
; https://tools.ietf.org/html/rfc793

2.8.  Data Communication

  The data that flows on a connection may be thought of as a stream of
  octets.  The sending user indicates in each SEND call whether the data
  in that call (and any preceeding calls) should be immediately pushed
  through to the receiving user by the setting of the PUSH flag.

  A sending TCP is allowed to collect data from the sending user and to
  send that data in segments at its own convenience, until the push
  function is signaled, then it must send all unsent data.  When a
  receiving TCP sees the PUSH flag, it must not wait for more data from
  the sending TCP before passing the data to the receiving process.

  There is no necessary relationship between push functions and segment
  boundaries.  The data in any particular segment may be the result of a
  single SEND call, in whole or part, or of multiple SEND calls.

  The purpose of push function and the PUSH flag is to push data through
  from the sending user to the receiving user.  It does not provide a
  record service.

  There is a coupling between the push function and the use of buffers
  of data that cross the TCP/user interface.  Each time a PUSH flag is
  associated with data placed into the receiving user's buffer, the
  buffer is returned to the user for processing even if the buffer is
  not filled.  If data arrives that fills the user's buffer before a
  PUSH is seen, the data is passed to the user in buffer size units.

  TCP also provides a means to communicate to the receiver of data that
  at some point further along in the data stream than the receiver is
  currently reading there is urgent data.  TCP does not attempt to
  define what the user specifically does upon being notified of pending
  urgent data, but the general notion is that the receiving process will
  take action to process the urgent data quickly.

그리고 RFC1122 문서에는 이를 좀 더 부연 설명하는데, 여기에 보면 SEND 호출이 반드시 PUSH 플래그를 설정하도록 강제하는 것은 아님을 알 수 있습니다.

Requirements for Internet Hosts -- Communication Layers
; https://tools.ietf.org/html/rfc1122

4.2.2.2  Use of Push: RFC-793 Section 2.8

            When an application issues a series of SEND calls without
            setting the PUSH flag, the TCP MAY aggregate the data
            internally without sending it.  Similarly, when a series of
            segments is received without the PSH bit, a TCP MAY queue
            the data internally without passing it to the receiving
            application.

            The PSH bit is not a record marker and is independent of
            segment boundaries.  The transmitter SHOULD collapse
            successive PSH bits when it packetizes data, to send the
            largest possible segment.

            A TCP MAY implement PUSH flags on SEND calls.  If PUSH flags
            are not implemented, then the sending TCP: (1) must not
            buffer data indefinitely, and (2) MUST set the PSH bit in
            the last buffered segment (i.e., when there is no more
            queued data to be sent).

            The discussion in RFC-793 on pages 48, 50, and 74
            erroneously implies that a received PSH flag must be passed
            to the application layer.  Passing a received PSH flag to
            the application layer is now OPTIONAL.

            An application program is logically required to set the PUSH
            flag in a SEND call whenever it needs to force delivery of
            the data to avoid a communication deadlock.  However, a TCP
            SHOULD send a maximum-sized segment whenever possible, to
            improve performance (see Section 4.2.3.4).

            DISCUSSION:
                 When the PUSH flag is not implemented on SEND calls,
                 i.e., when the application/TCP interface uses a pure
                 streaming model, responsibility for aggregating any
                 tiny data fragments to form reasonable sized segments
                 is partially borne by the application layer.

                 Generally, an interactive application protocol must set
                 the PUSH flag at least in the last SEND call in each
                 command or response sequence.  A bulk transfer protocol
                 like FTP should set the PUSH flag on the last segment
                 of a file or when necessary to prevent buffer deadlock.

                 At the receiver, the PSH bit forces buffered data to be
                 delivered to the application (even if less than a full
                 buffer has been received). Conversely, the lack of a
                 PSH bit can be used to avoid unnecessary wakeup calls
                 to the application process; this can be an important
                 performance optimization for large timesharing hosts.
                 Passing the PSH bit to the receiving application allows
                 an analogous optimization within the application.

그러니까, send 호출에 대한 push flag를 설정하는 것은 TCP 구현 스택의 임의 재량에 속하는 것입니다. 실제로 과거의 윈도우 KB 자료를 보면,

Design issues - Sending small data segments over TCP with Winsock
; https://learn.microsoft.com/en-us/troubleshoot/windows/win32/data-segment-tcp-winsock

send 호출마다 PSH가 설정된다는 식의 언급은 전혀 찾아볼 수 없고 오직 Nagle 이야기만 하고 있습니다. 아마도 저 시절에는 send에 전달한 데이터의 마지막 패킷에 PSH를 붙이는 것은 구현하지 않았을 수 있습니다.




참고로, "TCP Flags: PSH 그리고 URG" 글에서 약간 혼란의 여지가 있는 내용이 있다면 PSH 플래그를 응용 프로그램(Application Layer)에서 제어할 수 있는 것처럼 설명하고 있습니다. 그리고 위에서 인용했던 RFC 문서에도 "When an application issues a series of SEND calls without setting the PUSH flag,"라고 언급하면서 응용 프로그램 레벨에서 PUSH 플래그를 다룰 수 있는 것처럼 설명하지만, 실제로는 제어할 수 없습니다. 제가 코드상으로 방법을 찾아본 바도 그렇고, (공식 문서는 아니지만) 아래의 글에서도 이에 대한 설명을 하고 있습니다.

Improve latency for TCP by not waiting for Push flag
; http://smallvoid.com/article/winnt-tcp-push-flag.html

The setting of the Push Flag is usually not controlled by the sending application, but by the sending TCP layer.


마지막으로, 혹시 시간 되시면 다음과 같은 글들도 읽어 보시길. ^^

Improve latency for TCP by not waiting for Push flag
; http://smallvoid.com/article/winnt-tcp-push-flag.html

recv - IgnorePushBitOnReceives
; https://learn.microsoft.com/en-us/previous-versions/windows/embedded/ms911740(v=msdn.10)

첨부 파일 문서 - TCPIP_2003.doc (Windows Server 2003 TCPIP 문서)
; https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=1727&boardid=331301885

Delayed ACK and Nagle’s Algorithm
; https://noisy.network/2017/02/06/delayed-ack-and-nagles-algorithm/




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 1/17/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 31  32  33  34  35  36  37  38  39  40  41  42  43  [44]  45  ...
NoWriterDateCnt.TitleFile(s)
12530정성태2/4/202113042개발 환경 구성: 536. Wireshark + C#으로 확인하는 TCP 통신의 Receive Window
12529정성태2/4/202110140개발 환경 구성: 535. Wireshark + C#으로 확인하는 TCP 통신의 MIN RTO [1]
12528정성태2/1/20219532개발 환경 구성: 534. Wireshark + C#으로 확인하는 TCP 통신의 MSS(Maximum Segment Size) - 윈도우 환경
12527정성태2/1/20219759개발 환경 구성: 533. Wireshark + C#으로 확인하는 TCP 통신의 MSS(Maximum Segment Size) - 리눅스 환경파일 다운로드1
12526정성태2/1/20217606개발 환경 구성: 532. Azure Devops의 파이프라인 빌드 시 snk 파일 다루는 방법 - Secure file
12525정성태2/1/20217304개발 환경 구성: 531. Azure Devops - 파이프라인 실행 시 빌드 이벤트를 생략하는 방법
12524정성태1/31/20218343개발 환경 구성: 530. 기존 github 프로젝트를 Azure Devops의 빌드 Pipeline에 연결하는 방법 [1]
12523정성태1/31/20218373개발 환경 구성: 529. 기존 github 프로젝트를 Azure Devops의 Board에 연결하는 방법
12522정성태1/31/20219874개발 환경 구성: 528. 오라클 클라우드의 리눅스 VM - 9000 MTU Jumbo Frame 테스트
12521정성태1/31/20219886개발 환경 구성: 527. 이더넷(Ethernet) 환경의 TCP 통신에서 MSS(Maximum Segment Size) 확인 [1]
12520정성태1/30/20218406개발 환경 구성: 526. 오라클 클라우드의 VM에 ping ICMP 여는 방법
12519정성태1/30/20217518개발 환경 구성: 525. 오라클 클라우드의 VM을 외부에서 접근하기 위해 포트 여는 방법
12518정성태1/30/202124950Linux: 37. Ubuntu에 Wireshark 설치 [2]
12517정성태1/30/202112547Linux: 36. 윈도우 클라이언트에서 X2Go를 이용한 원격 리눅스의 GUI 접속 - 우분투 20.04
12516정성태1/29/20219214Windows: 188. Windows - TCP default template 설정 방법
12515정성태1/28/202110422웹: 41. Microsoft Edge - localhost에 대해 http 접근 시 무조건 https로 바뀌는 문제 [3]
12514정성태1/28/202110727.NET Framework: 1021. C# - 일렉트론 닷넷(Electron.NET) 소개 [1]파일 다운로드1
12513정성태1/28/20218738오류 유형: 698. electronize - User Profile 디렉터리에 공백 문자가 있는 경우 빌드가 실패하는 문제 [1]
12512정성태1/28/20218522오류 유형: 697. The program can't start because VCRUNTIME140.dll is missing from your computer. Try reinstalling the program to fix this problem.
12511정성태1/27/20218261Windows: 187. Windows - 도스 시절의 8.3 경로를 알아내는 방법
12510정성태1/27/20218662.NET Framework: 1020. .NET Core Kestrel 호스팅 - Razor 지원 추가 [1]파일 다운로드1
12509정성태1/27/20219609개발 환경 구성: 524. Jupyter Notebook에서 C#(F#, PowerShell) 언어 사용을 위한 환경 구성 [3]
12508정성태1/27/20218206개발 환경 구성: 523. Jupyter Notebook - Slide 플레이 버튼이 없는 경우
12507정성태1/26/20218329VS.NET IDE: 157. Visual Studio - Syntax Visualizer 메뉴가 없는 경우
12506정성태1/25/202111599.NET Framework: 1019. Microsoft.Tye 기본 사용법 소개 [1]
12505정성태1/23/20219364.NET Framework: 1018. .NET Core Kestrel 호스팅 - Web API 추가 [1]파일 다운로드1
... 31  32  33  34  35  36  37  38  39  40  41  42  43  [44]  45  ...