모든 내용은 [윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어] 를 기반으로 제 나름대로 이해하여 정리한 것입니다. 다소 부정확한 내용이 있을수도 있으니 이를 유념하고 봐주세요!
# TCP에서 "연결과정"에서는 큰 변수가 발생하지 않으나,
"종료과정"에서는 예상치 못한 일이 발생할 수 있기 때문에, 종료과정을 명확히 해야함
# close(리눅스) / closesocket(윈도우) => 완전 종료를 의미(데이터 송수신이 양쪽 다 불가능한 상황을 의미)
- 한 호스트의 일방적 연결종료는 의도치 않은 데이터 소멸을 야기할 수 있음
- 이를 위해 Half-close(스트림의 일부만 종료) 존재
# 소켓을 통해 두 호스트가 연결되면, 데이터 송수신이 가능한 상태가 됨(스트림이 형성된 상태가 됨)
- 스트림은 한쪽 방향의 흐름만 가능하므로, 양방향 통신을 위해서는 (입출력)스트림 두 개가 필요함
- Half-close는 이 두 개의 스트림 중 하나만 끊는 것임
- close/closesocket은 두 개의 스트림 모두를 끊는 것임
# 우아한 종료(Half-close)를 위한 shutdown 함수
#include <sys/socket.h>
int shutdown(int sock, int howto);
// -> 성공시 0, 실패시 -1 반환
- sock : 종료할 소켓의 파일 디스크립터 전달
- howto : 종료방법에 대한 정보 전달
1) SHUT_RD(입력 스트림 종료) : 데이터 수신 및 관련 함수 호출 불가능.
단, 데이터가 입력버퍼에 전달되더라도 삭제됨
2) SHUT_WR(출력 스트림 종료) : 데이터 송신 및 관련 함수 호출 불가능.
단, 출력버퍼에 남아있던 데이터는 목적지로 전송됨
3) SHUT_RDWR(입출력 스트림 종료) : 1, 2를 인자로 하여 shutdown 함수를 한번씩 호출한 것과 같음
# Half-close가 필요한 이유
- 클라이언트가 서버에 접속하면, 서버는 클라이언트에게 약속된 파일을 전송하고,
클라이언트는 파일을 다 수신하면, 잘 받았다는 의미로 서버에 데이터를 전송 하는 경우를 고려해보자.
1) 서버는 단순히 클라이언트에 데이터(파일)를 전송하기만 하면 되나,
2) 클라이언트는 데이터를 언제까지 수신해야 할지 알 도리가 없음
- 클라이언트가 무작정 입력함수 호출하면 => 블로킹에 빠질 수 있고
- 파일의 끝을 의미하는 문자를 약속한다고 해도 => 데이터 안에 그 문자가 있을 수도 있음
따라서,
=> 서버가 파일 전송이 끝났음을 알리는 목적으로 클라이언트에 "EOF"를 마지막에 전송하게 하자
=> 이러면 파일의 데이터와 중복될 일도 없다. 그렇다면 서버는 EOF를 어떻게 전달할 수 있을까
=> 단순하게 close를 호출하여 입출력 스트림 모두를 종료해도 EOF가 전달되지만,
이렇게 되면 클라이언트로부터 서버가 데이터를 수신할 수 없음
=> 따라서, shutdown 함수로 서버의 출력스트림만을 끊으면, 클라이언트로 EOF를 전달하면서도
클라이언트로부터 여전히 데이터 수신이 가능해짐
# Half-close 기반의 파일전송 프로그램
file_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char *message);
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) // 실행파일 경로/PORT번호 를 입력받아야 함
{
printf("Usage : %s <port>\n",argv[0]);
exit(EXIT_FAILURE);
}
fp=fopen("file_server.c","rb"); // 지금 이 소스파일 자체를 바이너리 읽기전용 형태로 엶
serv_sd=socket(PF_INET,SOCK_STREAM,0); // TCP 소켓 생성
/* 서버 주소정보 초기화 */
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));
/* 이 때, 비로소 진정한 서버소켓이 되며
연결요청 대기큐가 생성됨.
이 함수 호출 이후부터 클라이언트의 connect 함수 호출이 허용됨.
*/
listen(serv_sd, 5);
clnt_adr_sz=sizeof(clnt_adr);
clnt_sd=accept(serv_sd,(struct sockaddr*)&clnt_adr,&clnt_adr_sz);
// 연결요청 대기큐에서 대기중인(client 함수를 호출한) 클라이언트와의 연결을 수락
while(true)
{
read_cnt=fread((void*)buf,1,BUF_SIZE,fp);
/* 파일로부터 한 요소의 크기가 1바이트이고 요소의 개수가 BUF_SIZE 만큼인
배열만큼 데이터를 파일로부터 읽어들여서 buf에 저장 */
if(read_cnt<BUF_SIZE) // 방금 읽은 내용이 파일의 마지막 내용이라는 의미이므로
{
write(clnt_sd,buf,read_cnt); // 클라이언트로 송신하고 반복문 탈출
break;
}
write(clnt_sd,buf,BUF_SIZE); // 읽은 파일 내용을 클라이언트로 송신
}
/* 클라이언트로 모두 파일을 송신후 출력스트림 종료
이러면, 클라이언트는 서버로부터 EOF를 수신함
*/
shutdown(clnt_sd,SHUT_WR);
/* 클라이언트가 파일을 다 수신하고 보낸 메시지를 서버가 수신 */
read(clnt_sd,buf,BUF_SIZE);
printf("Message from client: %s\n",buf);
fclose(fp); // /* 파일 닫기 */
close(clnt_sd); /* 클라이언트와 연결을 위해 생성된 소켓 연결종료 */
close(serv_sd); /* 서버소켓 연결종료 */
return 0;
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(EXIT_FAILURE);
}
file_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char *message);
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) // 실행파일 경로/IP/PORT번호 를 입력받아야 함
{
printf("Usage: %s <IP> <port>\n",argv[0]);
exit(EXIT_FAILURE);
}
fp=fopen("receive.dat","wb"); // 바이너리 파일을 쓰기전용으로 엶
sd=socket(PF_INET,SOCK_STREAM,0); // TCP 소켓 생성
/* 서버 주소정보 초기화 */
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;
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(EXIT_FAILURE);
}
[출처] : 윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어
'Programming > 열혈 TCP, IP 소켓 프로그래밍(저자 윤성우)' 카테고리의 다른 글
Ch 08. 도메인 이름과 인터넷 주소 (0) | 2020.08.08 |
---|---|
Ch 07. 내용 확인문제 (0) | 2020.08.08 |
Ch 06. 내용 확인문제 (0) | 2020.08.08 |
Ch 06. UDP 기반 서버/클라이언트 (0) | 2020.08.08 |
Ch 05. 내용 확인문제 (1) | 2020.08.06 |