-
TCP/IP 소켓 프로그래밍 - 5장 : TCP 기반 서버/클라이언트 2네트워크 2024. 6. 17. 19:16
5장05-1 에코 클라이언트의 완벽 구현
에코 클라이언트와 에코 서버의 데이터 송수신 코드는 아래와 같다.
//에코 서버 while((str_len=read(clnt_sock, message, BUF_SIZE))!=0) write(clnt_sock, message, str_len); //에코 클라이언트 write(sock, message, strlen(message)); str_len = read(sock, message, BUF_SIZE-1);에코 클라이언트와 에코 서버의 코드의 가장 큰 차이점은,
수신하기를 기대하는 단위가 다르다는 것이다.
에코 서버는 주어지는 대로 읽은 만큼 수신하고 다시 그만큼 송신하고 있지만,
에코 클라이언트는 한 번에 문자열 데이터가 수신되기( read ) 되기를 원하고 있는 것이다.
해결책
클라이언트가 수신해야 할 데이터 크기를 미리 알고 있다면,
그 크기만큼을 수신할 때까지 read함수를 반복 호출하면 된다.
str_len = write(sock, message, strlen(message)); recv_len = 0; while(recv_len < str_len) { recv_cnt = read(sock, &message[recv_len], BUF_SIZE-1); if(recv_cnt==-1) error_handling("read() error!"); recv_len += recv_cnt; } message[recv_len] = 0;while문의 조건을 recv_len ≠ str_len으로 하지 않는 것은
예측 못한 무한 루프에 빠질 가능성을 최소화하기 위함이다.
수신 데이터의 크기를 알지 못할 때
이럴 때 필요한 것이 어플리케이션 프로토콜이다.
앞서 구현한 코드에서,
“ Q가 전달되면 연결을 종료한다 “와 같이 프로토콜을 정의해주면 된다.
소켓 프로그래밍을 할 때, 서버와 클라이언트가 한 쪽이 read 중이면 다른 쪽은 write를 하고 있어야 한다. 둘 다 read일 경우 아무일도 안일어날 수 가 있으니 이를 꼭 확인해주자.계산기 서버, 클라이언트 만들어보기 - 포인터를 이용한 형변환
책에서는 피연산자 개수, 피연산자, 연산자 모두를 한 배열에 담아서 보낸다.
이를 위해서는 한 버퍼안에서 포인터를 이용해 byte 크기를 정해주어야 한다.
이걸 몰라도 구현은 가능하기는 하지만.. (삽질로 극복 가능..)
위 그림과 같이 담아내기 위해서는 아래와 같이 클라이언트 코드를 작성해주어야 한다.
#define OPSZ 4 //operand의 크기를 4(byte)로 상수화시켜준다. ... char opmsg[BUF_SIZE]; //char형 배열에 정보를 담을 것이다. ... opmsg[0] = (char)opnd_cnt; //피연산자 개수 정보를 char형으로 형변환시켜 1바이트에 담는다. ... scanf("%d", (int*)&opmsg[i*OPSZ+1]); //char형 버퍼에 특정 위치에 int형 포인터로 정수를 담는다. ... scanf("%c", &opmsg[opnd_cnt*OPSZ+1]); //마지막으로 operator를 담는다. ... write(sock, opmsg, opnd_cnt*OPSZ+2); //write에서 읽어야 할 사이즈크기는 피연산자수x4 + 2와 같다.서버는 이를 바탕으로 아래와 같이 작성해주어야 한다.
char opinfo[BUF_SIZE]; //char형 배열을 선언해준다. ... read(clnt_sock, &opnd_cnt, 1); //1byte만큼에 담긴 피연산자개수 정보를 읽는다. ... while((opnd_cnt*OPSZ+1) > recv_len) //모두 다 받을 때까지 read연산을 계속한다. ... result = calculate(opnd_cnt, (int*)opinfo, opinfo[recv_len-1]); //calculate함수의 원형은 int calculate(int opnum, int opnds[], char op) 이다. //opinfo가 원래는 char형 배열이지만 int형 포인터를 사용해 캐스팅해주어 사용한다.
05-2 TCP의 이론적인 이야기
TCP 소켓에 존재하는 입출력 버퍼
write 함수가 호출되는 순간과 데이터가 전송되는 순간은 같지 않다.
read 함수가 호출되는 순간과 데이터가 수신되는 순간은 같지 않다.
정확히는 write함수가 호출되는 순간, 데이터는 출력버퍼로 이동을 하고
read함수가 호출되는 순간, 데이터는 입력버퍼에 저장된 데이터를 읽기 시작한다.

입출력 버퍼의 특징을 정리해보면,
- 입출력 버퍼는 TCP 소켓 각각에 별도로 존재한다.
- 입출력 버퍼는 소켓 생성시 자동으로 생성된다.
- 소켓을 닫아도 출력버퍼에 남아있는 데이터는 계속해서 전송이 이뤄진다.
- 소켓을 닫으면 입력버퍼에 남아있는 데이터는 소멸한다.
알아둘 점은, 입력 버퍼의 크기를 초과하는 양의 데이터 전송은 발생하지 않는다는 것이다.
TCP에는 ‘슬라이딩 윈도우’라는 프로토콜이 있어 데이터의 흐름까지 컨트롤 한다.
추가로 write함수가 반환되는 시점은 상대 호스트로 데이터의 전송이 완료되는 시점이 아니고,
전송할 데이터가 출력버퍼로 이동이 완료되는 시점이다.
그러나 TCP는 출력버퍼로 이동된 데이터의 전송을 보장하기 때문에 (특징 3번을 다시 보자.),
write함수는 데이터 전송이 완료되어야 반환 된다고 표현해도 무방하다.
TCP 내부 동작원리 - ( TCP의 Flow Control )
TCP 소켓의 생성부터 소멸까지 거치게 되는 일을 3가지로 나눠보면 아래와 같다.
- 상대 소켓과의 연결
- 상대 소켓과의 데이터 송수신
- 상대 소켓과의 연결종료
내부동작원리 1 : 상대소켓과의 연결

TCP 소켓의 연결과정은 그림과 같이 Three-way handshaking을 통해서 이루어진다.
A는 첫번째 syn으로 seq 1000을 보내고 ,
받게된 hostB는 잘 받았다면, ack으로 1001의 값을 보내주면 된다.
이때 B 역시도 패킷에 시퀀스 번호를 붙여서 보낼 수 있다.
ack 1001을 받은 A는 B가 잘 받은 것을 확인하고, seq 1001부터 보내주면 된다.
패킷에 번호를 부여해서 확인하는 절차 덕분에 손실된 데이터의 확인 및 재전송이 가능하고,
이것이 TCP가 신뢰성 있는(데이터가 손실없이 전송되는 것을 보장하는) 프로토콜이 되는 것이다.
내부동작원리 2 : 상대 소켓과의 데이터 송수신
Ack 메시지를 보낼 때, seq 번호 + 전송된 바이트 크기 + 1을 보내주면 된다.
A가 보낸 패킷에 대해 ack을 받지 못하면, time-out이 발생하게 되고,
이때, 패킷을 재전송하게 된다.

내부 동작 원리 3 : 상대 소켓과 연결 종료
소켓 A가 종료 메시지를 소켓 B에게 전달하면,
소켓 B는 해당 메시지를 수신했음을 소켓 A에게 알린다.
이어서, 소켓 B가 종료 메시지를 소켓 A에게 전달하면,
소켓 A는 해당 메시지를 수신했음을 소켓 B에게 알리고 종료하게 된다.

이 과정은 네 단계에 걸쳐서 진행되어서, Four-way handshaking이라고 부른다.
중간에 ack 1202가 중복되는 것은 Ack 메시지가 보내진 이후로
그에 대한 ack을 A가 보내지 않아서 재전송된 것이다.
위 내용은 TCP의 흐름제어를 설명한 내용이기도 하다.
'네트워크' 카테고리의 다른 글
TCP/IP 소켓 프로그래밍 - 8장 : 도메인 이름과 인터넷 주소 (0) 2024.06.18 TCP/IP 소켓 프로그래밍 - 7장 : 소켓의 우아한 연결종료 (0) 2024.06.18 TCP/IP 소켓 프로그래밍 - 6장 : UDP 기반 서버/클라이언트 (0) 2024.06.18 TCP/IP 소켓 프로그래밍 - 4장 : TCP 기반 서버/클라이언트 (2) 2024.06.16 TCP/IP 소켓 프로그래밍 - 3장 : 주소체계와 데이터 정렬 (1) 2024.06.16