kakasoo

[TCP/IP] 클라이언트, BUF_SIZE 초과 해결, Buffer에 대하여 본문

프로그래밍/네트워크

[TCP/IP] 클라이언트, BUF_SIZE 초과 해결, Buffer에 대하여

카카수(kakasoo) 2020. 7. 17. 17:10
반응형

문제는 클라이언트가 전송을 할 때, BUF_SIZE 값보다 큰 경우다.

전송을 하게 될 때, 다 전송 받은 상태인지, 사이즈가 커서 잘렸는지 알 수가 없지 않은가, 그렇지만 괜찮다.

echo_server와 client는 이미 "자신이 보낸 정보의 크기"를 알고 있는 경우에 해당하기 때문이다.

echo_client는 정보를 보내고, 자신이 돌려 받을 때에도 똑같은 크기가 돌아왔는지 확인하고, 그렇지 않다면 대기하면 된다.

echo를 괜히 만든 게 아니다, 우리가 만들려는 게 그저 서버와 클라이언트가 했던 말 따라 하는 프로그램을 작성하려던 게 아니고, 정보의 송수신이 문제 없이 이루어졌는지를, 매 순간 확인하기 위한 것이기 때문이다.

쓸 데 없이, 왜 했던 말 반복하는 서버-클라이언트를 만들었냐고 생각하지 말고, 더 깊게 공부해보도록 하자.

 

 

1
2
3
4
5
6
7
8
9
10
strLen = recv(hSocket, message, BUF_SIZE - 10); // 서버 측으로부터 에코된 게 있는지 확인 (문자열 길이를 반환)
 
int recv_cnt;
int recv_len = 0;
while(recv_len < str_len){
    recv_cnt = recv(hSocket, &message[recv_len], BUF_SIZE - 10);
    if (recv_cnt == -1)
        error_handling("read() error!");
    recv_len += recv_cnt;
}
cs

(1번과 3번 코드를 나란히 쓰면 안 된다, 비교를 위해 하나의 창에 모두 작성한 것.)

 

1번 줄이 기존의 코드였다, 해석하면, 소켓은 메세지를 받는다, 버퍼 사이즈 - 1까지를 최대 크기로 하여금.

이라는 뜻인데,

메세지가 만약 버퍼 사이즈 - 1보다 큰 경우라면, 메세지가 잘린다, 앞에 했던 말의 반복인데, 결국 이게 문제란 거다.

이걸 해결하기 위해서 만들어진 것이 3번부터 10번까지의 코드이다.

해석하면,

 

3 : int recv_cnt; // recv()의 반환값을 저장하기 위한 변수를 선언한다.

4 : int recv_len; // recv()의 반환값들을 누적하여 저장하기 위한 변수를 선언한다.

5 : while(recv_len < str_len) // 누적값이 str_len (자신이 전송했었던 값)보다 작다면, 수신이 다 되지 않은 거로 판단.

6 : recv_cnt = recv(hSocket, &message[recv_len], BUF_SIZE - 1, 0); // 소켓에서 전송을 받은 메세지를 수신한다.

7 : if (recv_cnt == -1) // 전송이 아무것도 되지 않는다면, 에러 메세지 호출

8 : recv_len += recv_cnt; // 전송받은 것을 누적.

이라는 의미가 되겠다.

(실제 코드가 정상 동작하는지는 확인하지 않았다, 소켓 프로그래밍은 디버깅이 너무 까다로운 지라.)

 


TCP의 이론적인 이야기

서버에서 버퍼 이상의 크기를 보내더라도 클라이언트는 이를 잘게 나눠서 받을 수 있다는 게 이제 이해될 것이다.

하지만 잘게 잘라서, 가령 한 조각을 보냈다고 치면 나머지 조각들은 어디에서 대기하는가?

사실 write, read, send, recv 함수들이 사용되는 순간, 바로 데이터의 전송과 수신이 이루어지는 게 아니다.

중간에 Buffer가 있다.

버퍼는 아래와 같은 특징들을 가진다.

  • TCP 소켓들은 각각 입출력 버퍼들을 가진다.
  • 이러한 입출력 버퍼는 소켓 생성 시에 자동으로 생성이 된다.
  • 소켓을 닫아도 출력 버퍼는 데이터를 마저 전송하고,
  • 소켓을 닫으면 입력 버퍼는 더 이상 데이터를 받지 않고 사라진다.

여기서 나오는 질문이, 버퍼까지 꽉 차면 어떡하냐는 것이다.

우리는 버퍼의 크기를 통해서, 데이터를 송수신하는 과정에서 데이터가 온전히 왔는지를 체크하고 있었다.

더 쉽게 풀어 쓰면, 우리는 각각의 변수들을 만들어서, 변수의 크기를 비교하고 있었다, 버퍼는 알아서 동작하니까,

버퍼의 크기는 우리의 고려 대상이 아니었다.

근데 아예 버퍼 자체가 꽉 차면 어떻게 되는가?

 

답은, "상관없다." 가 되겠다.

TCP는 데이터의 흐름을 컨트롤한다, 그 와중에 슬라이딩 윈도우라는 프로토콜이 존재한다.

이 프로토콜은, 버퍼가 가득 차면 기다렸다가 비는 만큼을 전송하는 것, 즉 송수신 소켓 간에 서로 처리 가능한 크기를 계속 주고 받고 있다는 의미이다.

(ex. 얼마만큼 받을 수 있어? // 잠깐만, 지금 비어있는 게... 30이야, 일단 30만 전달해!)

 

Three way handshaking

TCP 소켓의 생성부터 소멸까지의 과정은, 연결 - 데이터 송수신 - 연결 종료의 세 단계로 이루어진다.

당연히 상대 소켓이 있어야 한다, 고로 이 세 단계는 모두 다시 이렇게 설명된다.

상대 소켓과 연결 - 상대 소켓과 데이터 송수신 - 상대 소켓과 연결 종료 라고.

즉, 세 단계 모두가 상대 소켓과의 메시지를 주고 받게 되는데, 이것을 Three way handshaking 이라고 말하는 것이다.

 

이 과정을 세부적으로 보면 SYN, SYN+ACK, ACK 라고 한다.

모든 데이터는 SEQ, ACK로 이루어져 있는데, SEQ는 다음 번에 자신이 받아야 할 정보를 의미한다.

만약 1000을 SEQ에 담아서 보냈다면, 나에게 1000 + 1의 값을 보내라고 요청하라는 뜻이다.

음, 다시 설명하겠다.

"만약 처음에 N의 값을 전달했고, 이 값이 정확히 전달이 되었다면, 나에게 N+1을 요청하라고 하라, 그럼 내가 N+1을 주겠다" 라는 뜻인데, 이렇게 전달하는 값이 SEQ에 담기고, 돌려주는 값이 ACK에 담긴다.

그래서 아래의 순서가 된다.

 

상대 소켓과 연결

(1)

SEQ : 1000 // 내가 1000을 줄테니, 나에게 1000 + 1의 값을 달라고 요청해봐.

ACK : - // 처음 시작이기 때문에 돌려줄 게 없다.

이 정보를 보내고,

 

(2)

SEQ : 2000 // 나는 2000을 줄 거고, ( 이 수는 임의의 수다. )

ACK : 1001 // 아까 1001을 달라고 말해달라 했었지?

라고 반환되며,

 

(3) == (1)과 같은 호스트이다, (2)는 다른 호스트이다.

SEQ : 1001 // 응, 맞아. 그러니까 내가 1001을 주는 걸 통해서 제대로 송수신된다는 것을 알 수 있겠지?

ACK : 2001 // 그리고 나도, 아까 너가 보낸 2000에 1을 더해서, 2001을 줄게.

다시 재 반환된다.

 

이렇게, 매 송수신마다 상대 값의 1을 더한 것을 주고 받음으로써, 정상적으로 데이터가 오감을 확인할 수 있다.

TCP는 이런 방식을 통해서 데이터의 무결성을 확보한다.

이런 Three way handshaking이 끝났다면, 데이터의 송수신 준비가 끝난 것으로 판단한다.

데이터의 송수신 준비가 끝났다면, 위의 과정과 유사하게, 이번에는 실제 데이터의 송수신을 하게 되는데,

방식은 약간 변해서, +1 값 뿐만 아니라 ACK 값을 포함하여 전달하게 된다.

 

상대 소켓과 데이터 송수신

(1)

SEQ : 1200

100 byte 크기의 data를 전송

 

(2)

SEQ : - // 이제부터 호스트 B는 값을 전달할 필요가 없다고 치자, 받는 측에 해당하기 때문이다.

ACK : 1301 // 1200 + 100 (== size of data) + 1

 

이런 방식을 통해서 전달하게 되면, 데이터의 송수신 순서도 알 수 있을 뿐더러, 데이터를 온전히 전달했는지도 볼 수 있다.

만약 중간에 숫자가 빠진 것이 존재한다면, 운영체제는 알아서 재전송을 명령한다.

 

상대 소켓과 연결 종료

연결과 같은 방식으로 이루어지나, 4단계를 거쳐서 four way handshaking 이라고 한다.

반응형