본문 바로가기

Java

[Network] TCP/IP와 소켓통신 Send/Recv 동작 원리( + Blocking VS NonBlocking)

728x90

 

테스트 실패 현상

서버(고객사 개발)와 클라이언트(필자가 개발) 프로그램이 있습니다. 서버는 150Hz로 클라이언트 측으로 데이터를 송신합니다. 클라이언트는 해당 데이터를 수신하여 처리합니다. 클라이언트가 1회 송신 요청 데이터만 보내면, 서버는 스트림 형태로 데이터를 계속 송신하고, 클라이언트가 중단 요청을 보내면 그때 정지합니다.

 

서버에서 정상적으로 데이터를 send하였고, 클라이언트가 정상 수신 받아 처리하는 과정에서 일정 시간 이후( 발생 시점은 불특정) 클라이언트 입장에서 데이터를 정상 수신하지 못함.

 

서버 입장에서 모니터링을 하면 클라이언트가 데이터를 빠르게 가져가지 못해서 발생하는 것으로 밝혀짐.

 

이럴 경우 뭐가 문제이고 클라이언트는 무엇을 해야할까?

 

 

TCP/IP와 소켓통신 Send/Recv 동작 원리

  1. 서버 어플리케이션은 소켓을 오픈하고 application 레벨에서 보내고자 하는 데이터를 저장할 memory를 할당한다.
  2. 할당한 메모리에 데이터를 담고 send 한다.
  3. send 함수에 대한 systemcall이 커널 레벨로 전달이 되면 TCP계층에서 해당 데이터를 버퍼에 copy 한다. (Buffer I/O라고 한다.)
  4. 여기서 Buffer에 쌓인 데이터를 Segment 단위로 나눈다. 그리고 이 데이터를 IP 계층으로 전달한다. (TCP 헤더 + Segment)
  5. IP계층에서는 수신 받은 데이터에 송수신 IP 데이터를 헤더에 담아서 보낸다. 여기서 IP헤더 + ( TCP헤더+Segment)를 패킷이라고 부른다. (cf. 택배 박스에 보내는 사람, 받는사람 쓰는 개념)
  6. 네트워크 인터페이스로 넘어가면서 패킷은 프레임이 된다. 여기서도 계층 단계별로 헤더가 추가된다. 추가된 헤더는 최종적으로 1,0으로 이루어진 전기 신호로 전송된다.
  7. 클라이언트에서 수신한다. 그리고 서버와는 반대로 IP계층, TCP계층을 거쳐 헤더를 하나씩 벗겨내고, 헤더에 담긴 정보의 어플리케이션에서 할당한 소켓으로 데이터를 전달한다.
  8. 여기서 TCP 버퍼에서 Segment 를 받으면 서버로 잘 받았다는 Ack를 보내고, 받은 세그먼트 다음 번호를 붙여서 보낸다. 여기에 TCP버퍼가 데이터를 더 받을 수 있는지를 나타내는 window size도 함께 보낸다.
  9. 서버는 Ack를 수신받고 window size를 체크한다. 데이터를 더 보낼 수 있으면 정상적으로 다음 segment를 전송한다. 반대로 보낼 수 없으면 보내지 않는다.
  10. 그래서 클라이언트 application은 window size가 유지되도록 빠르게 데이터를 땡겨가야 한다.

 

이 과정에서 TCP 전이 상태는 아래와 같다. 가운데 recv/send를 보면 될 듯 하다.

( 위 자료는 RFC문서번호 793 문서다. RFC (Request for Comments) 미국의 국제 인터넷 표준화기구인 IETF(Internet Engineering Task Force)에서 제공, 관리하는 문서인데 1981년에 작성 된 문서라 그런지 도식이 굉장히 초창기 에디터 느낌이다.)

 

 

TCP/IP와 소켓통신 Send/Recv 동작 원리를 몰라서 발생한 문제

다시 테스트 중에 발생한 문제로 돌아가보자. 

서버는 데이터를 150Hz로 보내는데 수신하는 입장에서 데이터를 받고 처리하고, 다음 데이터를 받는 구조로 처리하도록 구현했다. 당연히 서버가 send하는 속도가 훨씬 빠르므로 서버의 TCP버퍼는 full이 되고, 클라이언트 TCP 버퍼도 full이 되었을 것이다. 당연히 클라이언트의 TCP 버퍼의 window 사이즈는 다음 데이터를 받기에 턱도 없었을 것이다.

 

그리고 내가 한가지 더 눈치채지 못한 부분이 있었는데, 서버의 데이터 송신 방식이 Blocking이었다. 이렇게 되면 서버 어플리케이션이 send할때 TCP버퍼가 full이면 Block을 걸어버린다.

 

 

즉, 일정 시간이 클라이언트는 데이터를 못받고, 서버 어플리케이션을 모니터링하면 클라이언트가 못가져가서 Block되는 현상이 발견되었을 것이다.

 

나는 네트워크 문제라고 생각하고 계속 삽질을 열심히 하고 있었다. 이 이야기를 위 유튜브 출처 ‘널널한 개발자'님 채널에서 정확히 지적해주셔서 소름…

 

 

해결방법

1. 서버 Application을 Non-Blocking으로 바꾼다. 그렇게 되면 서버는 TCP 버퍼가 차던지 말던지 Application 입장에서는 send를 하게 되고, 버퍼는 데이터가 쌓이면 버려지거나 커넬에 의해 관리가 되었을 것이다. 클라이언트는 받는대로 가져가서 처리를 했을 것.

2. 반대로 클라이언트 Application에서 큐를 사용하여 recv()를 담당하는 Thread에서는 계속 데이터를 수신받아서 큐에 쌓고, 처리하는 Thread에서는 데이터를 계속 처리하는 구조로 갔으면 window size를 수신 가능한 상태로 유지하면서 데이터 수신과 처리가 가능했을 것이다. 아니면 프로세스를 별도로 구현하여 메시지 큐를 사용했어도 해결이 가능했을 것이다.

728x90