TCP에서는 연결과정보다 중요한 것이 종료 과정이다. 종료 과정에서는 예상치 못한 일이 발생할 수 있기 때문이다.
일방적인 연결종료의 문제점
close 함수호출은 완전 종료를 의미한다. 완전 종료라는 것은 데이터를 전송하는 것은 물론 수신하는 것조차 더 이상 불가능한 상황을 의미한다. 상대방의 상태에 상관없이 일방적인 종료의 형태를 띤다. 때문에 한쪽에서의 일방적인 close 함수호출은 경우에 따라 좋지 못하다.
위 그림은 양방향으로 통신하고 있는 두 호스트의 상황을 나타낸 그림이다.
Host A가 마지막 데이터를 전송하고 나서 close함수의 호출을 통해 연결을 종료하였다. 그래서 그 이후부터는 Host A는 Host B가 전송하는 데이터를 수신하지 못한다(데이터 수신과 관련된 함수의 호출 자체가 불가능). 결국에 Host B가 전송한, Host A가 반드시 수신해야 할 데이터라 할지라도 그냥 소멸되고 만다.
이러한 문제를 해결하기 위해 데이터의 송수신에 사용되는 스트림의 일부만 종료하는 Half-close방식을 사용한다. → 전송을 가능하지만 수신은 불가능한 상황, 혹은 그 반대.
소켓과 스트림(Socket & Stream)
소켓을 통해 두 호스트가 연결되면, 상호 간에 데이터의 송수신이 가능한 상태가 되는데, 이러한 상태를 가리켜 스트림이 형성된 상태라고 한다. 소켓의 스트림은 한쪽 방향으로만 데이터의 이동이 가능하기 때문에 양방향 통신을 위해서는 아래의 그림과 같이 두 개의 스트림이 필요하다.
따라서 두 호스트간에 소켓이 연결되면, 각 호스트 별로 입력 스트림과 출력 스트림이 형성된다(한 호스트의 입력 스트림은 다른 호스트의 출력 스트림으로 이어진다). Half-close란, 이 두 스트림을 모두 끊어버리는 것이 아니라, 이 중 하나의 스트림만 끊는 것이다. 일반적으로 Half-close라 하면, 입력 스트림만 종료하는 것을 의미한다. close함수호출은 두 스트림을 동시에 끊어버린다.
Shut-down 함수와 그 필요성
Half-close에 사용되는 함수로, 스트림의 일부를 종료한다.
#include <sys/socket.h>
int shutdown(int sock, int howto);
//성공 시 0, 실패 시 -1 return
- sock : 종료할 소켓의 FD
- howto : 종료방법에 대한 정보
- SHUT_RD : 입력 스트림 종료
- 더 이상 데이터를 수신할 수 없는 상태
- 데이터가 입력 버퍼에 전달되더라도 지워져 버린다.
- SHUT_WR : 출력 스트림 종료
- 더 이상 데이터를 전송할 수 없는 상태
- 출력 버퍼에 아직 전송되지 못한 상태로 남아있는 데이터가 존재하면, 해당 데이터는 목적지로 전송된다.
- SHUT_RDWR : 입출력 스트림 종료
- SHUT_RD : 입력 스트림 종료
출력 스트림을 종료하면 상대 호스트로 EOF도 전송되고, 입력 스트림은 여전히 살아있어 데이터의 수신이 가능하다.
Half-close 기반 파일 전송 프로그램
파일 전송 서버, 클라이언트의 데이터 흐름을 정리하면 다음과 같다.
Server.c
...
while(1)//accept 함수호출을 통해서 연결된 clinet에게 파일 데이터를 전송
{
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);
read(clnt_sd, buf, BUF_SIZE);//입력 스트림을 통한 데이터 수신(사용 가능)
printf("Msg from client: %s\n", buf);
fclose(fp);
close(clnt_sd);
close(serv_sd);
파일 전송 후에 출력 스트림(SHUT_WR)에 대한 Half-close를 진행하고 있다. 이로써 클라이언트에게는 EOF가 전송되고, 이를 통해 클라이언트는 파일 전송이 완료되었음을 인식할 수 있다.
출력 스트림만 닫았기 때문에, read함수를 이용해 입력 스트림을 통한 데이터의 수신은 여전히 가능하다.
Client.c
...
//EOF가 전송될 때까지 데이터를 수신
while((read_cnt=read(sd, buf, BUF_SIZE))!=0)
fwrite((void*)buf, 1, read_cnt, fp);
puts("Received file data\n");
//서버로 msg를 전송. 서버의 입력 스트림이 닫히지 않았다면, 이 msg를 수신할 수 있다.
write(sd, "Thank you", 10);
fclose(fp);
close(sd);
'네트워크 프로그래밍' 카테고리의 다른 글
[Socket] 소켓의 다양한 옵션 (1) | 2023.10.17 |
---|---|
[Socket] 도메인 이름과 인터넷 주소 (0) | 2023.10.17 |
[Socket] TCP기반 서버/클라이언트 (0) | 2022.07.07 |
[Socket] 주소체계와 데이터 정렬 (0) | 2022.07.07 |
[Socket] 소켓의 타입과 프로토콜의 설정 (0) | 2022.07.07 |