입력과 출력을 위한 도구가 별도로 마련되어서 이 둘을 별개의 것으로 구분 지을 수 있다면, 방법에 상관없이 입출력 스트림의 분리가 이뤄졌다고 표현할 수 있다.
지금까지 두 가지 방법으로 입력 스트름과 출력 스트림을 분리해보았다.
- Chapter 10
fork 함수호출을 통해서 파일 디스크립터를 하나 복사해서 입력과 출력에 사용되는 파일 디스크립터를 구분, 멀티 프로세스 기반의 분리- Chapter 15
입력을 위한 도구와 출력을 위한 도구가 구분되었음, FILE 구조체 포인터 기반의 분리
Chapter 10에서의 스트림 분리 목적과 Chapter 15에서의 스트림 분리목적에는 차이가 있다.
- Chapter 10에서 설명한 스트림 분리의 목적
1 : 입력루틴(코드)와 출력루틴의 독립을 통한 구현의 편의성 증대
2 : 입력에 상관없이 출력이 가능하게 함으로 인해서 속도의 향상 기대
- Chapter 15에서 설명한 스트림 분리의 이점
1 : FILE 포인터는 읽기 모드와 쓰기 모드를 구분해야 함
2 : 읽기 모드와 쓰기 모드의 구분을 통한 구현의 편의성 증대
3 : 입력 버퍼와 출력 버퍼를 구분함으로 버퍼링 기능의 향상
[소켓 #07]에서 EOF의 전달 방법과 Half-close의 필요성에 대해서 공부하였다.
shutdown(sock, SHUT_WR);
출력 스트림에 대해서 half-close 진행 시 EOF 전달
[소켓 #15]에서 소개한 fdpoen 함수 호출 기반의 스트림 분리의 경우에는 이야기가 다르다.
writefp를 대상으로 fclose 함수를 호출하면 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 1024
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
FILE * readfp;
FILE * writefp;
struct sockaddr_in serv_adr, clnt_adr;
socklen_t clnt_adr_sz;
char buf[BUF_SIZE]={0,};
serv_sock=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_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr));
listen(serv_sock, 5);
clnt_adr_sz=sizeof(clnt_adr);
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr,&clnt_adr_sz);
readfp=fdopen(clnt_sock, "r");
writefp=fdopen(clnt_sock, "w");
fputs("FROM SERVER: Hi~ client? \n", writefp);
fputs("I love all of the world \n", writefp);
fputs("You are awesome! \n", writefp);
fflush(writefp);
fclose(writefp);
fgets(buf, sizeof(buf), readfp); fputs(buf, stdout);
fclose(readfp);
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 1024
int main(int argc, char *argv[])
{
int sock;
char buf[BUF_SIZE];
struct sockaddr_in serv_addr;
FILE * readfp;
FILE * writefp;
sock=socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
serv_addr.sin_port=htons(atoi(argv[2]));
connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
readfp=fdopen(sock, "r");
writefp=fdopen(sock, "w");
while(1)
{
if(fgets(buf, sizeof(buf), readfp)==NULL)
break;
fputs(buf, stdout);
fflush(stdout);
}
fputs("FROM CLIENT: Thank you! \n", writefp);
fflush(writefp);
fclose(writefp); fclose(readfp);
return 0;
}
- 실행 결과
서버가 마지막 문자열을 수신하지 못했다.
readfp = fdopen(clnt_sock, "r");
writefp = fdopen(clnt_sock, "w");
fputs("FROM SERVER: Hi ~ client? \n", writefp);
fputs("I love all of the world \n", writefp);
fputs("You are awesome! \n", writefp);
fflush(writefp);
fclose(writefp); // 소켓의 완전 종료
//Unreachable Code
fgets(buf, siezof(buf), readfp);
마지막 행의 fgets 함수 호출은 성공하지 못한다.
하나의 소켓을 대상으로 입력용 그리고 출력용 FILE 구조체 포인터를 얻었다 해도, 이 중 하나를 대상으로 fclose 함수를 호출하면 half-close가 아닌, 완전 종료가 진행되기 때문이다.
즉, serv에서 호출한 fclose 함수호출의 결과가 Half-Close가 아닌 쓰기도 읽기도 불가능한 완전종료로 이어졌다.
제공되는 PPT에 이해가 쉬운 그림이 있어서 그대로 가져왔다.
이 이상의 설명은 딱히 필요없을 것 같아 생략하겠다.
위에서 말한 복사는 fork 함수 호출 시 진행되는 복사와 차이가 있다.
fork 함수 호출 시에 진행되는 복사는 프로세스를 통째로 복사하기 때문에 하나의 프로세스에 원본과 복사본이 존재하지 않는다.
그러나 여기서 말하는 복사는 프로세스의 생성없이 원본과 복사본이 하나의 프로세스 내에 존재하는 형태의 복사를 의미한다.
- 하나의 프로세스 내에 동일한 파일에 접근할 수 있는 파일 디스크립터가 두 개 존재하는 상황
파일 디스크립터는 값이 고유하기 때문에 정수 값은 서로 다르다.
이러한 형태로 구성하려면 파일 디스크립터를 복사해야 한다.
즉, 여기서 말하는 복사는 다음과 같다.
"동일한 파일 또는 소켓의 접근을 위한 또 다른 파일 디스크립터의 생성"
일반적으로 우리가 아는 복사의 의미와 다르다.
이번에는 파일 디스크립터의 복사 방법에 대해 이야기하겠다.
다음 두 함수 중 하나를 이용해서 진행한다.
#include <unistd.h>
int dup(int fildes);
int dup2(int fildes, int fildes2);
// 성공 시 복사된 파일 디스크럽터, 실패 시 -1 반환
/*
fildes : 복사할 파일 디스크럽터 전달
fildes2 : 명시적으로 지정할 파일 디스크립터의 정수 값 전달
*/
예제
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int cfd1, cfd2;
char str1[]="Hi~ \n";
char str2[]="It's nice day~ \n";
cfd1=dup(1);
cfd2=dup2(cfd1, 7);
printf("fd1=%d, fd2=%d \n", cfd1, cfd2);
write(cfd1, str1, sizeof(str1));
write(cfd2, str2, sizeof(str2));
close(cfd1);
close(cfd2);
write(1, str1, sizeof(str1));
close(1);
write(1, str2, sizeof(str2));
return 0;
}
- 실행 결과
이번 예제에서는 시스템에 의해서 자동으로 열리는 표춘 출력을 의미하는 파일 디스크립터 1을 복사하여 복사된 파일 디스크립터를 이용해서 출력을 진행한다.
참고로 자동으로 열리는 파일 디스크립터 0,1,2 또한 소켓의 파일 디스크립터와 차이가 없다.
앞에서 구현한 serv를 변경해보자.
여기서 바꾸고 싶은 것은 서버 측의 Half-close 진행으로 클라이언트가 전송하는 마지막 문자열이 수신되도록 하는 것이다. 이를 위해서 서버 측에서 EOF 전송을 동반하겠다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
FILE * readfp;
FILE * writefp;
struct sockaddr_in serv_adr, clnt_adr;
socklen_t clnt_adr_sz;
char buf[BUF_SIZE]={0,};
serv_sock=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_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr));
listen(serv_sock, 5);
clnt_adr_sz=sizeof(clnt_adr);
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr,&clnt_adr_sz);
readfp=fdopen(clnt_sock, "r");
writefp=fdopen(dup(clnt_sock), "w");
fputs("FROM SERVER: Hi~ client? \n", writefp);
fputs("I love all of the world \n", writefp);
fputs("You are awesome! \n", writefp);
fflush(writefp);
shutdown(fileno(writefp), SHUT_WR);
fclose(writefp);
fgets(buf, sizeof(buf), readfp); fputs(buf, stdout);
fclose(readfp);
return 0;
}
- 실행 결과
- 복사된 파일 디스크립터의 수에 상관없이 EOF의 전송을 동반하는 Half-close를 진행하기 위해서는 shutdown 함수를 호출해야 한다.
참고 : 윤성우의 열혈 TCP/IP 소켓 프로그래밍
Git : https://github.com/im2sh/Socket_Programming/tree/main/lab12