TCP에서는 연결과정보다 중요한 것이 종료과정입니다. 이번 포스트에서 설명할 Half-close는 명확한 종료를 위해 반드시 알아야 할 사항입니다.
일방적 연결종료의 문제점
close 함수호출은 완전종료를 의미합니다. 완전종료라는 것은 데이터 전송은 물론이고 데이터 수신도 더 이상 불가능하게 합니다. 아래 그림을 보며 예시 상황을 하나 살펴보겠습니다.
위 그림은 양방향으로 통신하고 있는 두 호스트의 상황을 묘사한 것입니다. 호스트 A가 마지막 데이터를 전송하고 나서 연결을 종료했습니다. 그 이후부터 호스트 A는 호스트 B가 전송하는 데이터를 수신하지 못합니다(데이터 수신과 관련된 함수 호출 자체가 불가능합니다.). 때문에 호스트 B가 전송한 데이터는 그냥 소멸되고 맙니다. 이러한 문제의 해결을 위해 데이터 송수신에 사용되는 스트림의 일부만 종료하는 방법이 제공되고 있습니다. 이 말은 전송은 가능하지만 수신은 불가능한 상황 또는 수신은 가능하지만 전송은 불가능한 상황을 만들 수 있음을 뜻합니다.
소켓과 스트림
소켓을 통해 두 호스트가 연결되면 상호간에 데이터의 송수신이 가능한 상태가 됩니다. 이러한 상태를 가리켜 스트림이 형성된 상태라고 합니다. 스트림은 물의 흐름을 의미합니다. 그런데 물의 흐름은 한쪽 방향으로만 형성됩니다. 마찬가지로 소켓의 스트림 역시 한쪽 방향으로만 데이터의 이동이 가능하므로 양방향 통신을 위해서 두 개의 스트림이 필요합니다.
때문에 두 호스트 간에 소켓이 연결되면 각 호스트 별로 입력 스트림과 출력 스트림이 형성됩니다. 이번 포스트에서는 우아한 종료를 다뤄볼 것인데, 우아한 종료는 두 스트림을 한번에 모두 끊는 게 아니라 이 중 하나의 스트림만 끊는 것입니다. 물론 close 함수는 둘 다를 끊어버리기 때문에 우아한 종료와는 거리가 멉니다.
우아한 종료를 위한 shutdown 함수
다음 shutdown 함수가 스트림의 일부를 종료하는 데 사용되는 함수입니다. 이 함수에는 다음과 같은 인자들을 전달해줘야 합니다.
- int sock: 종료할 소켓의 파일 디스크립터 전달
- int howto: 종료방법에 대한 정보 전달
// 성공 시 0, 실패 시 -1 반환
#include <sys/socket.h>
int shutdown(int sock, int howto);
위의 종료방법에는 다음과 같은 것들을 전달할 수 있습니다.
- SHUT_RD: 입력 스트림 종료
- SHUT_WR: 출력 스트림 종료
- SHUT_RDWR: 입출력 스트림 종료
Half-close가 필요한 이유
Half-close가 필요한 이유에 대해서 아직 제대로 다루지 않았습니다. Half-close 없이 그냥 데이터를 주고받기에 충분한 만큼 연결을 유지하다가 종료하면 되는 게 아닌가 하는 의문이 들 수도 있습니다.
그런데 클라이언트가 서버에 접속하면 서버는 약속된 파일을 클라이언트에 전송하고 클라이언트는 파일을 잘 수신했다는 의미로 문자열 "Thank you"를 서버에게 전송하는 상황을 생각해보겠습니다. 파일을 전송하는 서버는 단순히 파일 데이터를 연속해서 전송하면 되지만 클라이언트는 언제까지 데이터를 수신해야 할지 알 수 없습니다.
이를 위해서 서버는 파일의 전송이 끝났음을 알리는 목적으로 EOF를 전송해야 합니다. 클라이언트는 EOF의 수신을 함수의 반환 값을 통해 확인이 가능하기 때문에 저장된 데이터와 중복될 일도 없습니다. 서버는 출력 스트림을 종료함으로써 상대 호스트로 EOF를 보낼 수 있습니다. 물론 close 함수호출을 통해 입력 스트림을 모두 종료해줘도 EOF는 전송되지만 이럴 경우 상대방이 전송하는 데이터를 더 이상 수신 못한다는 문제가 있습니다. 즉, close 함수호출을 통해 스트림을 종료하면 클라이언트가 마지막으로 보내는 문자열을 수신할 수 없습니다. 따라서 shutdown 함수 호출을 통해 서버의 출력 스트림만 Half-close 해야하는 것입니다. 이런 데이터 흐름을 정리하면 다음 그림과 같습니다.
Half-close 기반의 파일전송 프로그램
먼저 서버 프로그램을 소개하겠습니다. 이번 코드에선 코드 이해의 편의를 위해 오류처리에 대한 부분들을 생략했습니다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
int main(int argc, char* argv[]) {
int serv_sd, clnt_sd;
FILE* fp;
char buf[BUF_SIZE];
int read_cnt;
struct sockaddr_in serv_adr, clnt_adr;
socklen_t clnt_adr_sz;
if (argc != 2) {
printf("Usage: %s <port>\n", argv[0]);
exit(1);
}
fp = fopen("file_server.c", "rb");
serv_sd = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
bind(serv_sd, (struct sockaddr*)&serv_adr, sizeof(serv_adr));
listen(serv_sd, 5);
clnt_adr_sz = sizeof(clnt_adr);
clnt_sd = accept(serv_sd, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
while (1) {
read_cnt = fread((void*)buf, 1, BUF_SIZE, fp);
if (read_cnt < BUF_SIZE) {
write(clnt_sd, buf, read_cnt);
break;
}
write(clnt_sd, buf, BUF_SIZE);
}
shutdown(clnt_sd, SHUT_WR); // 출력 스트림 close, 클라이언트에게는 EOF가 전송
read(clnt_sd, buf, BUF_SIZE);
printf("Message from client: %s\n", buf);
fclose(fp);
close(clnt_sd);
close(serv_sd);
return 0;
}
곧바로 클라이언트 프로그램도 소개하겠습니다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
int main(int argc, char* argv[]) {
int sd;
FILE* fp;
char buf[BUF_SIZE];
int read_cnt;
struct sockaddr_in serv_adr;
if (argc != 3) {
printf("Usage: %ss <IP> <port>\n", argv[0]);
exit(1);
}
fp = fopen("receive.dat", "wb");
sd = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(argv[2]));
connect(sd, (struct sockaddr*)&serv_adr, sizeof(serv_adr));
while ((read_cnt = read(sd, buf, BUF_SIZE)) != 0) // EOF가 전송될 때까지 데이터 수신
fwrite((void*)buf, 1, read_cnt, fp);
puts("Received file data");
write(sd, "Thank you", 10);
fclose(fp);
close(sd);
return 0;
}
결과는 아래와 같습니다.
'네트워크 프로그래밍 > C' 카테고리의 다른 글
[네트워크 프로그래밍/C] IP 주소와 도메인 이름 사이의 변환 (0) | 2022.02.27 |
---|---|
[네트워크 프로그래밍/C] Domain Name System (0) | 2022.02.26 |
[네트워크 프로그래밍/C] UDP의 데이터 송수신 특성과 UDP에서의 connect 함수호출 (0) | 2022.02.26 |
[네트워크 프로그래밍/C] UDP 기반 서버, 클라이언트의 구현 (0) | 2022.02.25 |
[네트워크 프로그래밍/C] UDP에 대한 이해 (0) | 2022.02.25 |