fopen 함수를 호출해서 파일을 열고 나면, 파일로부터 데이터를 송/수신할 수 있게 됨
fopen 함수를 호출해서 파일을 여는 경우 스트림이 생성되었다고 표현함
스트림이란 “데이터가 이동하는 흐름”을 의미 (일종의 다리)
파일 포인터를 스트림이라고 표현하는 것은 아니고 파일 포인터 생성 시 데이터 입/출력을 위한 내부적인 상황(다리가 놓여진 상황)을 의미하는 것임
(1) 스트림을 분리하는 이유는 무엇인가?
입력과 출력이 모두 가능한 스트림을 만들어서 사용하게 되면, 입력에서 출력으로, 출력에서 입력으로 사용 용도가 변경될 때마다 버퍼를 비워주는 작업을 거쳐야 함
번거롭기 때문에 표준 입/출력 함수를 사용하기 위해서 스트림을 생성하는 경우 각각 분리시켜서 생성함
두 개의 호수트가 통신을 하기 위해서 소켓을 생성하고 그 소켓을 기반으로 각각 다른 두 개의 스트림을 생성하고 있음
→ 한 호스트의 출력 스트림을 통해 전송된 데이터는 다른 호스트의 입력 스트림을 통해서 전달됨
(2) 스트림을 분리했을 경우 어떤 문제점이 생길까?
EOF 메시지를 전송해 줘야 하는 경우 입력과 출력용 파일 포인터가 각각 존재하므로, 출력용 파일 포인터를 이용해서 fclose 함수를 호출해 주면 잘 동작할 것 같지만 실제로는 문제가 생김
// sep_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#define BUFSIZE 1024
void error_handling(char* message);
int main(int argc, char** argv)
{
int serv_sock;
int clnt_sock;
FILE* rstrm;
FILE* wstrm;
struct sockaddr_in serv_addr;
struct sockaddr_in clnt_addr;
int clnt_addr_size;
char buf[BUFSIZE] = {0,};
if(argc != 2)
{
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock == -1)
error_handling("socket() error");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("bind() error");
if(listen(serv_sock, 5) == -1)
error_handling("listen() error");
clnt_addr_size = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
if(clnt_sock == -1)
error_handling("accept() error");
// 입력 스트림과 출력 스트림 생성
rstrm = fdopen(clnt_sock, "r");
wstrm = fdopen(clnt_sock, "w");
fputs("FROM SERVER : Hello?\n", wstrm);
fputs("I like network programming\n", wstrm);
fputs("I like socket programming\n\n", wstrm);
fflush(wstrm);
// EOF 메시지 전송을 위해 출력용 파일포인터를 이용하여
// 출력 스트림을 종료하고 있음
// 그러나 실제로는 해당 소켓도 종료가 되기 때문에
// 입/출력 스트림 모두 종료되어 버림
fclose(wstrm);
// 입력 스트림으로부터 전달되는 데이터를 수신하려 하고 있지만
// 소켓이 닫혀진 상태이므로 데이터를 더 이상 수신할 수 없게 됨
fgets(buf, sizeof(buf), rstrm);
fputs(buf, stdout);
fclose(rstrm);
return 0;
}
void error_handling(char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
// sep_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#define BUFSIZE 1024
void error_handling(char* message);
int main(int argc, char** argv)
{
int sock;
FILE* rstrm;
FILE* wstrm;
char buf[BUFSIZE];
struct sockaddr_in serv_addr;
if(argc != 3)
{
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
if(sock == -1)
error_handling("socket() error");
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]));
if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("connect error!");
// 입력 스트림과 출력 스트림 생성
rstrm = fdopen(sock, "r");
wstrm = fdopen(sock, "w");
// EOF 메시지가 들어올 때까지 데이터를 읽어서 콘솔에 출력해 주고 있음
while(1)
{
// fgets 함수는 EOF 수신 시 NULL 포인터가 리턴됨
if(fgets(buf, sizeof(buf), rstrm) == NULL) break;
fputs(buf, stdout);
fflush(stdout);
}
// 인사 메시지 전송
fputs("FROM CLIENT : Thanks you!\n", wstrm);
fflush(wstrm);
fclose(wstrm);
fclose(rstrm);
return 0;
}
void error_handling(char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
[실행 결과]
클라이언트가 보내는 최종 메시지를 서버가 수신하지 못하고 있음
(1) 스트림 종료 시의 문제점과 해결 방안
지금까지 작성한 예제에서 생성했던 입력용 파일 포인터나 출력용 파일 포인터는 하나의 퍄일 디스크립터를 기반으로 생성된 것이기 때문에 fclose() 호출 시 어떠한 파일 포인터를 인자로 전달하더라도 파일 디스크립터의 종료와 동시에 소켓은 완전히 종료되어 버림
→ 더이상의 송/수신 불가
half-close를 위해서는 어떻게 해야할까?
하나의 파일 디스크립터를 복사해서 두 개로 만들고 각각의 디스크립터를 기반으로 파일 포인터를 얻어내면 됨
하나의 파일(소켓)에 접근할 수 있는 파일 디스크립터가 두 개 이상인 경우에는 모든 파일 디스크립터가 종료되어야만 해당 파일 소켓이 종료됨
따라서 fclose 함수의 인자로 출력용 파일 포인터를 넣어주면 해당 파일 디스크립터만 종료되지 소켓은 종료되지 않음
파일 디스크립터는 시스템 함수를 통해서 파일을 여는 경우 리턴되는 정수임
우리는 이 정수를 이용해서 여러 함수를 호출해 가며 파일을 조작함
그럼 파일 디스크립터를 복사한다는 것은 어떤 의미일까?
파일 하나를 열었고, 그 결과로 리턴된 파일 디스크립터 값이 5라고 가정해보자
복사를 한다는 것은 변수 하나 선언하고 나서 5를 대입하라는 것을 의미하는 게 아님!
여기서의 복사는 가리키는 파일에 접근할 수 있는 파일 디스크립터 정수 하나를 더 만들어내는 것을 의미함
파일 디스크립터가 복사되어 Data.dat라는 파일에 접근할 수 있는 파일 디스크립터는 5와 7 두 개가 됨
이 경우 close 함수를 호출해서 하나의 파일 디스크립터를 종료해 줘도 파일은 닫히지 않음
[파일 디스크립터를 복사하는 함수]
#include <unistd.h>
int dup(int fildes);
int dup2(int fildes, int dildes2);
// 성공 시 복사된 파일 디스크립터, 실패 시 -1 리턴
// fildes : 복사하고자 원하는 파일 디스크립터를 인자로 넘겨줌
// fildes2 : dup2 함수의 경우 인자가 하나 더 있는데, 두 번째 인자의 용도는 복사되는 파일 디스크립터의 값을
// 명시적으로 지정해주고 싶을 때 사용함
기본적으로 열리는 파일 디스크립터(0, 1, 2)도 일반 파일 디스크립터와 차이가 없으므로 복사 및 직접 종료 가능
(다만 프로그램이 시작할 때 기본적으로 열리고, 프로그램이 종료될 때 자동적으로 종료될 뿐임)
// dup1.c
#include <stdio.h>
#include <unistd.h>
int main(void)
{
int fd;
fd = dup(1); // 파일 디스크립터 복사
printf("복사된 파일 디스크립터 : %d\n", fd);
write(fd, "복사된 파일 디스크립터에 의한 출력 \n", 72);
return 0;
}
[실행 결과]
복사된 파일의 디스크립터 값은 3이며, 복사된 파일 디스크립터를 사용해서 출력을 해도 콘솔상에 제대로 출력됨
→ 복사된 파일 디스크립터도 원본 파일 디스크립터와 동일한 권한을 가짐
// dup2.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(void)
{
int fd;
int state;
fd = dup(1); // standard out 복사
printf("복사된 파일 디스크립터 : %d\n", fd);
write(fd, "복사된 파일 디스크립터에 의한 출력 \n", 52);
if(close(1) == -1)
{
puts("에러 발생 \n");
exit(1);
}
write(fd, "복사된 파일 디스크립터에 의한 출력 \n", 52);
return 0;
}
[실행 결과]
원본 파일 디스크립터를 종료한 다음에도 복사본 파일 디스크립터를 사용해서 출력 가능
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(void)
{
int fd;
int state;
fd = dup(1); // standard out 복사
printf("복사된 파일 디스크립터 : %d\n", fd);
write(fd, "복사된 파일 디스크립터에 의한 출력 \n", 52);
if(close(1) == -1)
{
puts("에러 발생 \n");
exit(1);
}
write(fd, "복사된 파일 디스크립터에 의한 출력 \n", 52);
if(close(fd) == -1)
{
puts("에러 발생 \n");
exit(1);
}
write(fd, "복사된 파일 디스크립터에 의한 출력 \n", 52);
}
[실행 결과]
원본과 복사된 파일 디스크립터를 모두 종료한 경우 출력 불가
// sep_server2.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#define BUFSIZE 1024
void error_handling(char* message);
int main(int argc, char** argv)
{
int serv_sock;
int clnt_sock;
FILE* rstrm;
FILE* wstrm;
struct sockaddr_in serv_addr;
struct sockaddr_in clnt_addr;
int clnt_addr_size;
char buf[BUFSIZE];
if(argc != 2)
{
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock == -1)
error_handling("socket() error");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("bind() error");
if(listen(serv_sock, 5) == -1)
error_handling("listen error");
clnt_addr_size = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
if(clnt_sock == -1)
error_handling("accept() error");
rstrm = fdopen(clnt_sock, "r");
wstrm = fdopen(dup(clnt_sock), "w");
fputs("FROM SERVER : Hello?\n", wstrm);
fputs("I like network programming\n", wstrm);
fputs("I like socket programming\n\n", wstrm);
fflush(wstrm);
shutdown(fileno(wstrm), SHUT_WR);
fclose(wstrm);
fgets(buf, sizeof(buf), rstrm);
fputs(buf, stdout);
fclose(rstrm);
return 0;
}
void error_handling(char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
[실행 결과]
서버는 클라이언트가 마지막으로 전송하는 메시지를 받을 수 있게 됨