5.TCP 기반 서버 클라이언트 - 2

# 앞전 포스팅에서 작성한 에코 클라이언트만 문제가 있다면?

 

- echo_server.c

while((str_len=read(clnt_sock,message,BUF_SIZE))!=0)  // 클라이언트로부터 수신한 문자열이 있을때에

write(clnt_sock,message,str_len);                                   // 그 문자열을 그대로 에코(문자열 끝 널문자는 제외!)

 

- echo_client.c

write(sock,message,strlen(message));       // 서버로 문자열(널문자 포함) 전송

str_len=read(sock,message,BUF_SIZE-1);  // 서버에서 에코한 문자열 수신

 

#.참고

0) TCP 소켓은 "데이터의 경계가 없다.

1)  클라이언트가 서버로 문자열을 write 함수 한번을 호출하여 전달

2) 서버가 이 문자열을 while문을 통해 read하여 읽고, write하여 다시 클라이언트로 전달

(반복문을 통해 구현했으므로, 서버가 클라이언트가 보낸 문자열을 모두 수신하고 다시 전달함이 보장됨)

3) 클라이언트는 서버가 에코한 문자열을 read 함수 "한번 호출"로 수신한다.

(TCP 소켓의 특성상, 서버가 에코한 문자열을 수신했음을 보장하지 못한다.)

=> 즉, 클라이언트에 문제가 있다.

 

#.해결책

클라이언트가 모든 문자열을 수신할 때까지, read 함수를 반복 호출하면 된다.

 

#.예제

 

#.어플리케이션 프로토콜의 정의

- 에코 클라이언트의 경우 자기가 서버에 송신한 데이터 크기이므로 수신할 데이터의 크기를 미리 파악이 가능했다. 미리 파악이 힘든 이외의 경우에는 어플리케이션 프로토콜의 정의를 한다.

 

#.어플리케이션 프로토콜

데이터 송수신 과정에서, 데이터의 끝을 파악할 수 있는 약속(프로토콜)을 정의해서 , 데이터의 끝을 표현하거나, 송수신될 데이터의 크기를 미리 알려줘서 그에 따른 대비가 가능하게 하는 것.

(즉, 목적에 맞는 프로그램의 구현에 따라서 정의하게 되는 약속)

 

실제 프로그램의 구현을 위해서는 자세하고 정확한 프로토콜이 정의되어야 한다. 그만큼 네트워크 프로그래밍에서는 어플리케이션 프로토콜의 정의가 중요하다. 프로토콜만 잘 정의하면, 구현은 큰 문제가 되지 않는다.

 

 

#.프로토콜 정의

1.계산기 서버, 클라이언트의 프로토콜 정의하기

- 클라이언트는 서버에 접속하자마자 피연산자의 개수정보를 1바이트 정수형태로 전달

- 클라이언트가 서버에 전달하는 정수 하나는 4바이트로 표현

- 정수를 전달한 다음에는 연산의 종류를 전달, 연산정보는 1바이트로 전달

- 문자 +, -, * 중 하나를 선택해서 전달

- 서버는 연산결과를 4바이트 정수의 형태로 클라이언트에 전달

- 연산결과를 얻은 클라이언트는 서버와의 연결을 종료

 

2. close 함수가 호출되면, 상대방에게 EOF가 전달된다.

3. 데이터의 송수신을 위한 메모리 공간은 데이터를 누적해서 송수신해야하기 때문에 배열을 기반으로 생성하는 것이 좋다.

4. 하나의 배열에 다양한 종류(타입)의 데이터를 저장해서 전송하려면, char형 배열을 이용하면 좋다.(포인터변환 활용)

5. TCP는 데이터의 경계가 존재하지 않기 때문에, 한번의 write 함수 호출을 통해서 묶어 보내도 되고,
여러번의 write 함수 호출을 통해 나눠 보내도 된다.

 

#.데이터 구조

 

#.서버 예제

 

#.클라이언트 예제

 

 

#.TCP 소켓에 존재하는 입출력버퍼

 

TCP 소켓의 데이터 송수신에는 경계가 없다. 따라서 서버가 한번의 write 함수호출을 통해서 40바이트를 전송해도 클라이언트는 네 번의 read 함수호출을 통해서 10바이트씩 데이터를 수신하는 것이 가능하다. 그런데 이러한 현상에 의문을 가질 수 있다. 서버는 데이터를 한 번에 40바이트를 전송했는데, 클라이언트가 이를 여유 있게 조금씩 수신하니 말이다. 클라이언트가 10바이트만 먼저 수신했다면, 서버가 보낸 나머지 30바이트는 어디서 대기하고 있는 것일가?

 

사실 write 함수가 호출되는 순간이 데이터가 전송되는 순간이 아니고, read 함수가 호출되는 순간이 데이터가 수신되는 순간이 아니다. 정확히 말하면 write 함수가 호출되는 순간 데이터는 출력버퍼로 이동하고, read 함수가 호출되는 순간 입력버퍼에 저장된 데이터를 읽어 들이게 된다.

 

- write 함수가 호출되는 순간, 데이터는 바로 전송되는 것이 아니라, 데이터는 출력버퍼로 이동한다.

- read 함수가 호출되는 순간, 데이터는 바로 수신되는 것이 아니라, 입력버퍼에 저장된 데이터를 읽는다.

 

#.입출력버퍼의 특징

1. TCP 소켓 각각에 별도로 존재한다.
2. 소켓 생성시 자동으로 생성된다.
3. 소켓을 닫아도 출력버퍼에 남아있는 데이터는 계속해서 전송이 이루어진다. -> 데이터 송신 보장한다.
4. 소켓을 닫으면 입력버퍼에 남아있는 데이터는 소멸되어버림 -> 데이터 수신을 보장하지 않는다.

 

"입력버퍼의 크기를 초과하는 분량의 데이터 전송은 발생하지 않는다" 왜냐하면 TCP가 데이터의 흐름까지 컨트롤하기 때문이다. TCP에는 '슬라이딩 윈도우(Sliding Window)'라는 프로토콜이 존재한다. 이 프로토콜의 역할을 대화로 표현하면 다음과 같다

  • 소켓a : 50바이트까지는 보내도 괜찮아

  • 소켓b : OK

  • 소켓a : 방금 20바이트 비웠으니까 60바이트까지 괜찮아

  • 소켓b : OK

이렇게 서로 대화를 주고받으면서 데이터를 송수신하기 때문에 버퍼가 차고 넘쳐서 데이터가 소멸되는 일이 TCP에서는 발생하지 않는다.

 

 

#.TCP의 내부 동작원리1 : 상대 소켓과의 연결

 

  • 상대 소켓과의 연결

  • 상대 소켓과의 데이터 송수신

  • 상대 소켓과의 연결종료

[shake1] 소켓a : 소켓b, 전달할 데이터가 있다! 연결하자!

[shake2] 소켓b : OK 준비 됨, 연결 하자!

[shake3] 소켓a : 좋아! 얼른 하자!

 

실제로 TCP 소켓은 연결설정 과정에서 총 세 번의 대화를 주고 받는다 이를 가리켜 Three-way handshaking이라 한다.