본문 바로가기
공부 기록

[Network] 소켓 통신 방법과 순서

by 타태 2022. 6. 20.

이 책을 읽는 스터디를 진행하며 정리한 내용입니다.

 

2022.06.11 - [Back-End/Java] - [JAVA] Socket with JDK

 

[JAVA] Socket with JDK

2022.04.16 - [실전 공부] - [JAVA x Apache POI] 전략 패턴과 리플렉션을 활용하여 컬럼 자동 생성 엑셀 다운로드 구현하기 [JAVA x Apache POI] 전략 패턴과 리플렉션을 활용하여 컬럼 자동 생성 엑셀 다운로

ktae23.tistory.com

 

1. 접속

1-1

소켓을 만들고 나면 애플리케이션(브라우저)은 connect를 호출하고 프로토콜 스택은 클라이언트 소켓을 이용해 서버 소켓에 접속을 시도합니다.
하지만 소켓을 만든 직후에는 아무런 제어 정보가 기록되어 있지 않기 때문에 통신 상대가 누구인지 알 수 없습니다.
이 상태에서는 송신 의뢰가 와도 데이터를 어디로 보내면 좋을 지 알 수 없기 때문에 서버의 IP 주소나 포트 번호를 프로토콜 스택에 알리는 동작이 필요합니다.

이를 위해 connect에 디스크립터, 서버의 IP와 포트 번호를 전달하면 해당 명령은 이를 프로토콜 스택의 TCP 담당 부분으로 전달합니다.
명령을 전달 받은 TCP 담당 부분은 전달받은 해당 서버의 TCP 담당 부분과 제어 정보를 주고받고 데이터 송수신을 위한 절차를 진행합니다.

TCP 헤더를 만들어 이를 IP 담당 부분에 건네어 송신 요청을 하면 IP 담당 부분이 패킷 송신을 실행하고 네트워크를 통해 패킷이 서버로 전달 됩니다.
TCP 헤더를 만들 때 송신처와 수신처의 포트 번호를 지정할 수 있고, TCP 헤더 중 컨트롤 비트인 SYN을 1로 ACK을 0으로 작성하여 전송합니다. 

그러면 또 다시 서버의 IP 담당 부분은 이를 TCP 담당 부분으로 전달하고, TCP 헤더를 조사하여 수신처 포트에 해당하고 접속 대기 중인 소켓을 찾아 필요한 제어 정보를 프로토콜 스택에 기록합니다.
이 과정이 끝나면 TCP 담당 부분은 준비가 되었다는 응답을 클라이언트 소켓으로 보냅니다.

이때 클라이언트에서 요청을 보낼 때처럼 송신처와 수신처의 포트 번호를 지정하고, SYN을 1로, 그리고 데이터가 올바르게 도착했다는 의미로 ACK를 1로 작성합니다.

 

1-2

클라이언트는 서버로부터 받은 응답이 IP를 거쳐 TCP에 도착했을 때 TCP 헤더를 조사합니다.
헤더에서 ACK 값이 1이면 전송에 성공한 것이고 SYN이 1이면 접속에 성공한 것이기 때문에 서버의 IP 주소와 포트 번호 등의 제어 정보와 접속 완료를 나타내는 제어 정보를 프로토콜 스택에 기록합니다.

그리고 마지막으로 클라이언트 역시 서버로부터 패킷을 정상적으로 전달 받았음을 알리기 위해 ACK을 1로 작성한 TCP헤더를 작성하여 반송합니다.
이로써 소켓은 데이터를 주고 받을 수 있는 상태가 되며 클라이언트와 서버 소켓은 데이터 송수신 때 사용할 목적으로 데이터 임시 저장을 위해 위한 버퍼 메모리 영역을 확보합니다.

이처럼 데이터를 주고 받을 준비가 끝난 상태를 커넥션이라고 합니다.

커넥션은 close를 호출하여 연결을 끊기 전까지 계속 존재합니다.

커넥션이 이루어지면 프로토콜 스택의 접속 동작이 끝나기 때문에 connect의 실행이 끝나며 제어권은 다시 애플리케이션으로 넘어갑니다.

 


2. 송/수신

2-1

connect가 끝난 후 애플리케이션으로 제어권이 돌아오면 애플리케이션은 write를 호출하여 송신 데이터를 프로토콜 스택으로 전달합니다.
데이터를 전달 받은 프로토콜 스택은 이 데이터를 곧바로 서버로 전달하지 않고 자신의 송신용 버퍼 메모리 영역에 저장하고 다음 데이터를 기다립니다.

애플리케이션의 종류나 만드는 방법에 따라 데이터를 한 번에 송신 의뢰하는 경우도 있고 1바이트 또는 1행씩 나누는 경우도 있습니다.
송신 의뢰시 전달되는 데이터 길이는 전적으로 애플리케이션에 달려 있기 때문에 프로토콜 스택이 제어할 수 없습니다.

때문에 프로토콜 스택은 전달 된 데이터를 즉시 송신하지 않고 MTU(Maximum Transmission Unit : IP 헤더 + TCP헤더 + 데이터의 최대 길이)와 MSS(Maximum Segment Size : 데이터의 최대 길이)와 같은 패킷의 크기와 프로토콜 스택 내부 타이머(최대 대기 시간)를 이용한 전송 시점을 바탕으로 패킷의 전송 시점을 판단합니다.

하지만 이러한 판단가 절충에 대한 기준이 없기 때문에 비효율을 초래할 수 있습니다.
때문에 개발자가 "버퍼 메모리에 머물지 않고 즉시 전송"이란 옵션을 부여하여 애플리케이션단에서 제어할 수 있도록 하였습니다.

보통 HTTP 요청 메시지의 크기는 한 패킷에 담길 정도이지만 form 데이터일 경우 한 패킷에 들어가지 않을 수 있습니다.
이 경우 송신 버퍼에 저장된 데이터가 MSS의 길이를 초과하므로 다음 데이터를 기다리지 않고 즉시 전송 됩니다.
프로토콜 스택이 메시지를 전송 할때는 소켓에 기록된 제어 정보를 바탕으로 필요한 내요을 기록한 TCP 헤더를 패킷의 맨 앞에 부가합니다. 
그리고 이 패킷을 IP 담당 부분에 건네어주면 마찬가지로 IP 헤더를 추가하고 서버 소켓으로 송신합니다.

클라이언트가 서버로 전송하는 것을 예로 들었지만 반대로도 동일한 양방향 작업입니다.

 

2-2

TCP는 신뢰성 있는 통신을 보장하는 것이 특징인데요.
이를 가능하게 하는 것이 ACK 번호를 통한 시퀀스 번호 확인입니다.

TCP 담당 부분은 데이터를 조각으로 분할할 때 해당 조각이 통신 개시 시점으로부터 몇 번째 조각인지를 세어 TCP 헤더에 기록합니다.

TCP는 이 방법으로 통신 상대가 데이터를 받았는지 여부를 확인하는데, 송신한 시퀀스에 대응하는 ACK가 돌아오기 전까지 송신했던 패킷을 송신용 버퍼 메모리 영역에 보관해 두었다가 ACK가 돌아오지 않으면 패킷을 다시 전송합니다.
이 구조는 매우 강력하기 때문에 네트워크 오류로 인해 패킷이 누락될 경우 다른 곳에서 오류 회복 조치를 할 
필요가 없습니다.

수신측에서는 전달받은 패킷의 시퀀스 초기값으로부터 ACK 번호를 산출하여 TCP 헤더에 기록하여 데이터를 전달받았음을 알려줍니다.

이때 시퀀스 초기값은 악의적인 공격을 방어하기 위해 1이 아닌 난수를 바탕으로 산출한 초기 값이 할당 됩니다.
하지만 난수로 시작하면 초기값을 알 수 없기 때문에 접속 단계에서 SYN을 1로 보낼 때 시퀀스 번호의 초기 값을 함께 전송합니다. 

 

2-3

기본적인 내용들은 이와 같지만 실제 오류 검출과 회복이 되는 원리는 꽤 복잡하기 때문에 ACK 번호가 돌아오는 것을 기다리는 시간을 설정하게 됩니다.
이 대기 시간을 타임아웃 값이라고 합니다.
타임아웃 값이 길면 요청 시간 지연으로 인한 속도 저하가 발생하고 짧으면 정상적으로 전송이 되었어도 ACK가 돌아오기 전에 다시 데이터를 보내버리는 소모가 발생할 수 있습니다.
때문에 TCP는 항상 ACK가 돌아오는 시간을 기록해 두었다가 이를 기준으로 대기 시간을 동적으로 변경하는 방법을 취하고 있습니다.

 

2-4

패킷의 전송과 수신은 동기 방식으로 핑퐁을 하게 됩니다.
하지만 이럴 경우 ACK 번호가 돌아오기를 기다리는 시간 동안 대기를 하게 됩니다.
이를 해결하기 위해 비동기 식으로 송신을 한 뒤 ACK 번호를 수신하는 윈도우 제어 방식이 고안되었습니다.

윈도 제어 방식을 사용할 경우 송신 측에서 데이터를 연속하여 보내기 때문에 ACK 번호를 계산하고 데이터 조각을 모아 애플리케이션으로 전달하는 속도보다 더 빠르게 수신용 버퍼 메모리가 차서 넘쳐버릴 수 있습니다.
이를 방지하기 위해 수신 측은 수신 버퍼 메모리의 수용 가능한 데이터 양을 TCP 헤더의 윈도 필드에 담아 송신 측에 전달합니다.

그럼 송신측은 이 값을 기준으로 송신한 데이터 길이를 계산하여 수신 측의 데이터 처리 역량을 가늠하여 오버플로우가 발생하지 않도록 송신을 조절하게 됩니다.

송신 측에서 전달받은 값으로부터 스스로 여유 메모리를 예측할 수 있기 때문에 윈도 필드를 매번 통지하지는 않습니다.

수신 측에서 데이터 처리를 끝내 버퍼 메모리에 여유가 생기면 송신 측으로 처리 가능한 데이터 길이를 통지하고 송신 측은 이를 기준으로 다시 데이터 송신을 재개합니다.
수신 측은 수신처리가 끝나면 ACK 번호를 산출하여 송신 측에 반송해야만 합니다. 하지만 윈도 제어 방식은 송신 측에서 데이터를 전송 가능한 만큼 미리 보내 두기 때문에 매번 ACK 번호를 반송하는 것은 비효율 적입니다.


때문에 수신측은 ACK 번호나 윈도를 통지할 때 패킷을 바로 보내지 않고 잠시 기다린 후에 다음 통지 동작이 일어나면 한 개의 패킷으로 묶어서 송신 측으로 전송합니다.
이때 중간에 수행 된 패킷 들에 대해서는 계산하지 않고 마지막 작업에 대한 정보만을 통지합니다.

모든 데이터를 수신 한 후에는 TCP 헤더의 내용과 데이터 조각들을 조사하여 데이터 누락이 없는지 검사하고 문제가 없으면 데이터 조각을 연결하여 데이터를 복원 한 뒤 애플리케이션이 지정한 메모리 영역으로 전달합니다.
그리고 애플리케이션으로 제어가 돌아가고 타이밍을 가늠하여 ACK 번호와 윈도를 반송합니다.

 


3. 종료

3-1

데이터 송/수신의 종료는 송신해야 하는 데이터를 모두 송신 완료했다고 판단 했을때 진행됩니다.
송신을 완료한 소켓은 연결 끊기 단계로 들어가는데 클라이언트와 서버 중 어디 부터 연결을 끊기 시작하는지는 애플리케이션에 따라 다릅니다.

웹의 경우 브라우저에서 요청을 보내 서버에서 응답 메시지를 마치는 시점에서 데이터 송신이 종료된 것이기 때문에 서버 측에서 연결 끊기 단계에 들어가게 됩니다.
애플리케이션과 상황에 따라 다르기 때문에 프로토콜 스택은 클라이언트와 서버 중 어디라도 먼저 연결 끊기 단계에 들어가도 괜찮습니다.

데이터 송신을 마쳐 연결 끊기 단계에 들어가는 측에서 Socket 라이브러리의 close를 호출합니다.
close가 호출 된 프로토콜 스택은 소켓에 연결 끊기 동작에 들어갔다는 정보를 기록하고 컨트롤 비트의 FIN 비트에 1을 설정한 TCP 헤더를 작성하여 IP 담당 부분으로 송신 의뢰를 합니다.

FIN에 1을 설정한 TCP 헤더를 받은 통신 상대는 자신의 소켓에 연결 끊기 동작에 들어갔음을 기록하고 이를 잘 받았다는 의미로 ACK 번호를 반송합니다.

이후 애플리케이션이 read를 호출하여 데이터를 가지러 옵니다.(FIN이 오기 전에 수행 될 경우 데이터 전달을 보류한 뒤 FIN이 온 뒤 재개합니다.)
그럼 데이터를 애플리케이션으로 넘기기 전에 통신 상대에게 수신 완료를 의미하는 ACK 번호를 반송하고 연결 끊기에 들어가면서 FIN을 전달하고 ACK을 돌려 받는 동작을 동일하게 수행합니다.

3-2

모든 소켓이 종료된 이후에도 오작동을 막기 위해 잠시 동안은 소켓을 말소하지 않고 잠시 대기합니다.
오작동을 일으키는 데는 여러 이유가 있지만, 모든 과정이 끝난 이후 바로 말소할 경우 상대로부터 FIN에 대한 ACK를 받지 못한 채로 끝날 수도 있습니다.
말소된 소켓이 사용했던 IP 주소와 포트 번호에 다른 애플리케이션이 할당되어 사용 중일 수 도 있습니다.
ACK를 받지 못한 통신 상대가 FIN을 다시 보낼 경우 할당된 다른 애플리케이션이 FIN을 받아 종료 단계를 수행할 수 도 있기 때문에 소켓을 바로 말소하지 않고 잠시 대기하게 됩니다.

 


 

소켓과 프로토콜 스택

프로토콜 스택

프로토콜 스택은 내부에 제어 정보를 기록하는 메모리 영역을 가지고 있으며, 여기에 통신 동작을 제어하기 위한 제어 정보를 기록합니다.
대표적인 정보는 통신 상대의 IP 주소는 무엇인가, 포트 번호는 몇 번인가, 통신 동작이 어떤 진행 상태에 있는가 하는 것입니다.
본래 소켓은 개념적인 것이어서 실체가 없으므로 굳이 말하자면 이 제어 정보가 소켓의 실체라고 할 수 있겠죠.
또는 제어 정보를 기록한 메모리 영역이 소켓의 실체라고 생각해도 좋습니다.

프로토콜 스택은 이 제어 정보를 참조하며 동작합니다. 
예를 들어 데이터를 송신할 때는 소켓에 기록되어 있는 IP주소나 포트 번호를 보고 그 IP 주소와 포트 번호를 대상으로 데이터를 송신합니다.
소켓에는 응답이 돌아오는지의 여부와 송신 동작 후의 경과 시간 등이 기록되어 있습니다. 
프로토콜 스택은 소켓에 기록 되어 있는 여러 제어 정보들을 참조하여 다음에 무엇을 해야 하는지를 판단합니다.

애플리케이션으로부터 HTTP 리퀘스트 메시지 송신 요청이 들어오면 프로토콜 스택은 소켓을 준비하기 시작합니다.
이때 프로토콜 스택이 최초로 하는 일은 소켓 한 개 분량의 메모리 영역을 확보하는 것입니다.
소켓의 제어 정보를 기록하는 메모리 영역은 처음부터 존재하는 것이 아니므로 먼저 그것을 확보해야 합니다.
이 때는 송/수신 동작이 시작되지 않았기 때문에 초기 상태임을 나타내는 제어 정보를 소켓의 메모리 영역에 기록하고 이 과정을 통해 소켓이 만들어집니다.

소켓이 만들어지면 소켓을 나타내는 디스크립터를 애플리케이션에 반환해줍니다.
이때 디스크립터는 프로토콜 스택 내부에 있는 여러 소켓 중 어떤 소켓인지를 가리키는 식별 정보입니다.
송/수신 측 정보와 통신 상태에 대한 정보는 모두 소켓에 있기 때문에 클라이언트가 디스크립터만 전송하면 나머지 정보는 모두 프로토콜 스택에서 알 수 있습니다.

 

제어 정보

제어 정보에는 크게 두 가지가 있는데, 하나는 클라이언트와 서버가 서로 연락을 절충하기 위해 주고받는 제어 정보로 TCP 헤더로 작성하여 전달합니다.
이는 접속 동작뿐 아니라 데이터를 송/수신하는 동작이나 연결을 끊는 동작도 포함하여 통신 동작 전체에서 어떤 정보가 필요한지를 검토하여 내용을 TCP 프로토콜의 사양으로 규정하고 있습니다.

다른 하나는 소켓에 기록하여 프로토콜 스택의 동작을 제어하기 위한 정보로 애플리케이션에서 통지된 정보, 통신 상대로부터 받은 정보 등이 수시로 기록됩니다. 
프로토콜 스택의 구현체에 따라 필요한 내용이 다르고 헤더를 통해 제어 정보를 주고받기 때문에 각 소켓의 프로토콜 스택에 기록된 제어 정보는 통신 상대가 알 수 없습니다. 

반응형

댓글