11. 입출력 스트림의 분리에 대한 나머지 이야기

2024. 9. 4. 17:40·스터디/TCP 와 IP
728x90
반응형

 

입력 스트림과 출력 스트림의 분리

 

입력 스트림과 출력 스트림의 분리는 매우 넓게 사용되는 표현이다. 입력과 출력을 위한 도구가 별도로 마련되어서 

이 둘을 별개의 것으로 구분 지을 수 있다면, 방법에 상관없이 입출력 스트림의 분리가 이뤄졌다고 표현할 수 있다.

 

 

두 번의 입출력 스트림 분리

 

지금까지 두 가지 방법으로 입력 스트름과 출력 스트림을 분리해보았다.


8. 멀티프로세스 기반의 서버구현

위에서 우린 fork 함수호출을 통해서 파일 디스크립터를 하나 복사해서 입력과 출력에 사용되는 파일 디스크립터를 구분, 멀티 프로세스 기반의 분리했고


10. 소켓과 표준 입출력


위에서 우린 입력을 위한 도구와 출력을 위한 도구가 구분되었음, FILE 구조체 포인터 기반의 분리했다.

 

 

 

스트림 분리의 이점


8. 멀티프로세스 기반의 서버구현
에서의 스트림 분리 목적과 10. 소켓과 표준 입출력 에서의 스트림 분리목적에는 차이가 있다.

8. 멀티프로세스 기반의 서버구현 에서 설명한 스트림 분리의 목적
1 : 입력루틴(코드)와 출력루틴의 독립을 통한 구현의 편의성 증대
2 : 입력에 상관없이 출력이 가능하게 함으로 인해서 속도의 향상 기대

10. 소켓과 표준 입출력  에서 설명한 스트림 분리의 이점
1 : FILE 포인터는 읽기 모드와 쓰기 모드를 구분해야 함
2 : 읽기 모드와 쓰기 모드의 구분을 통한 구현의 편의성 증대
3 : 입력 버퍼와 출력 버퍼를 구분함으로 버퍼링 기능의 향상

 

 

스트림 분리 이후의 EOF에 대한 문제점

 

6. 소켓의 우아한 연결 종료 에서 EOF의 전달 방법과 Half-close의 필요성에 대해서 공부하였다.

(기억나지 않으면 반드시 해당부분 복습)


shutdown(sock, SHUT_WR);

 

출력 스트림에 대해서 half-close 진행 시 EOF 전달

 

10. 소켓과 표준 입출력 에서 소개한 fdpoen 함수 호출 기반의 스트림 분리의 경우에는 이야기가 다르다.
writefp를 대상으로 fclose 함수를 호출하면 half-close가 진행될까 ?
먼저 코드를 보자

 

Server

#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;
}
	readfp=fdopen(clnt_sock, "r");
	writefp=fdopen(clnt_sock, "w");

clint_sock에 저장된 파일 디스크립터를 기반으로 읽기모드 FILE 포인터와 쓰기모드 FILE포인터를 생성하였다.

 

	fputs("FROM SERVER: Hi~ client? \n", writefp);
	fputs("I love all of the world \n", writefp);
	fputs("You are awesome! \n", writefp);
	fflush(writefp);

클라이언트로 문자열 데이터를 전송하고 fflush 함수호출을 통해서 전송을 마무리하고있다.

 

	fclose(writefp);	
	fgets(buf, sizeof(buf), readfp); fputs(buf, stdout);

fclose(writefp); 에서 쓰기모드 FILE 포인터를 대상으로 fclose 함수를 호출하고 있다. 이렇게 fclose 함수를 호출하면서

소켓을 종료시키면, 상대방 호스트에게는 EOF가 전달된다. 그런데 여전히 30행에서 생성한 읽기모드 FILE 포인터가 남아있으니

fgets(buf, sizeof(buf), readfp); fputs(buf, stdout); 의 함수호출의 통해서 클라이언트가 마지막으로 전송한 문자열을 수신할 수 있

다고 생각할 수있다. 물론 이 마지막 문자열은 클라이언트가 EOF를 수신한 다음에 전송하는 문자열이다.

 

EOF가 뭔지 헷갈린다면 아래글을 다시 읽어보자

※ EOF란?

 

EOF란?

EOF (End-Of-File) 완벽 가이드EOF는 **"End-Of-File"**의 약자로, 파일의 끝을 나타내는 특수한 표시입니다. 컴퓨터는 파일을 읽을 때 EOF를 만나면 더 이상 읽을 데이터가 없다는 것을 인식하고 파일 읽기

ddd-a.tistory.com

 

위 예제에서 fclose 함수가 호출되면 EOF가 전달되는 것은 사실이다. 그리고 잠시 후에 보이는 클라이언트에서는 실제로

EOF를 수신하면서 마지막 문자열을 서버로 전달한다. 다만 39행의 함수호출을 통해서 클라이언트가 마지막으로

전송한 문자열을 수신할 수 있는지는 확인해 볼 일이다.

 

그럼이어서 클라이언트 코드를 소개하겠다.

 

Client

#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(sock, "r");
	writefp=fdopen(sock, "w");

표준 입출력 함수의 호출을 위해서 읽기모드, 그리고 쓰기모드 FILE 포인터를 생성하고 있다.

 

		if(fgets(buf, sizeof(buf), readfp)==NULL) 
			break;

EOF가 전달되면 fgets 함수는 NULL 포인터를 반환한다. 따라서 NULL이 반환되는 경우에 반복문을 빠져나가도록

if문이 구성되어있다.

 

	fputs("FROM CLIENT: Thank you! \n", writefp);

이 문장에 의해서 서버로 마지막 문자열이 전송된다. 물론 이 문자열은 서버로부터 전달된 EOF 수신후에 전송하는 문자열

 

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가 아닌 쓰기도 읽기도 불가능한 완전종료로 이어졌다.

 

 

serv와 clnt 실행 과정

 

 

 

파일 디스크립터의 복사와 Half-close

이번 Chapter의 주제가 FILE 포인터를 대상으로하는 Half-close에 맞춰져 있지만, 잠시후에 소개하는

dup 그리고 dup2 함수에 대한 경험은 시스템 프로그래밍에 대한 경험적 측면에서도 많은 도움이 될 것이다.

 

스트림 종료 시 Half-close가 진행되지 않은 이유

 

 

 

파일 디스크립터의 복사

 

위에서 말한 복사는 fork 함수 호출 시 진행되는 복사와 차이가 있다.
fork 함수 호출 시에 진행되는 복사는 프로세스를 통째로 복사하기 때문에 하나의 프로세스에 원본과 복사본이 존재하지 않는다.
그러나 여기서 말하는 복사는 프로세스의 생성없이 원본과 복사본이 하나의 프로세스 내에 존재하는 형태의 복사를 의미한다.

 

하나의 프로세스 내에 동일한 파일에 접근할 수 있는 파일 디스크립터가 두 개 존재하는 상황
파일 디스크립터는 값이 고유하기 때문에 정수 값은 서로 다르다.
이러한 형태로 구성하려면 파일 디스크립터를 복사해야 한다.
즉, 여기서 말하는 복사는 다음과 같다.
"동일한 파일 또는 소켓의 접근을 위한 또 다른 파일 디스크립터의 생성"
일반적으로 우리가 아는 복사의 의미와 다르다.

 

 

dup & dup2

 

이번에는 파일 디스크립터의 복사 방법에 대해 이야기하겠다.
다음 두 함수 중 하나를 이용해서 진행한다.

 

#include <unistd.h>

int dup(int fildes);
int dup2(int fildes, int fildes2);
// 성공 시 복사된 파일 디스크럽터, 실패 시 -1 반환

/*
fildes : 복사할 파일 디스크럽터 전달
fildes2 : 명시적으로 지정할 파일 디스크립터의 정수 값 전달
*/

dup2 함수는 복사된 파일 디스크립터의 정수 값을 명시적으로 지정할 때 사용한다. 이 함수의 인자로 0보다 크고 프로세스당

생성할 수 있는 파일 디스크립터의 수보다 작은 값을 전달하면, 해당 값을 복사 되는 파일 디스크립터의 정수 값으로

지정해준다.

 

그럼 위 함수의 기능을 확인하기 위한 예제를 하나 보여주겠다. 다음 예제에서는 시스템에 의해서 자동으로 열리는, 표준출력을

의미하는 파일 디스크립터 1을 복사하여, 복사된 파일 디스크립터를 이용해서 출력을 진행한다.

참고로 자동으로 열리는 파일 디스크립터 0, 1, 2 역시 소켓 기반의 파일 디스크립터와 차이가 없으므로 dup 함수의 기능확인 목적으로 사용하기엔 충분하다.

 

dup

#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;
}
	cfd1=dup(1);
	cfd2=dup2(cfd1, 7);

cfd1=dup(1); 에서는 dup 함수호출을 통해서 파일 디스크립터 1을 복사하고 11행에선 dup2 함수호출을 통해서 복사한

파일 디스크립터를 재 복사하고있다. 그리고 정수값도 7로 지정하였다.

 

	write(cfd1, str1, sizeof(str1));
	write(cfd2, str2, sizeof(str2));

복사된 파일 디스크립터를 이용해서 출력을 진행하고 있다. 이출력결과를 통해 실제 복사가 이뤄진 것인지 확인할 수 있다.

 

	close(cfd1);
	close(cfd2);
	write(1, str1, sizeof(str1));

복사된 파일 디스크립터를 모두 종료하고있다. 그러나 아직 하나가 남아있는 상태이기 때문에 출력이 여전히 이뤄짐을
write(1, str1, sizeof(str1)); 여기서 보이고 있다.

 

	close(1);
	write(1, str2, sizeof(str2));

close(1); 여기에선 마지막 파일 디스크립터를 종료하였다. 때문에 write(1, str2, sizeof(str2));의 출력은 이뤄지지 않을 것이다.

 

 

 

파일 디스크립터의 복사 후 스트림의 분리

 

앞에서 구현한 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;
}

/*
swyoon@my_linux:~/tcpip$ gcc sep_serv2.c -o serv2
swyoon@my_linux:~/tcpip$ ./serv2 9190
FROM CLIENT: Thank you! 
*/

 

fdopen 함수 호출을 통해서 FILE 포인터를 생성하였다. 특히 26행에서는 dup 함수 호출의 반환 값을 대상으로 FILE 포인터를 생성하는 것을 볼 수 있다.


33행에서는 fileno 함수 호출 시 반환되는 파일 디스크립터를 대상으로 shutdown 함수를 호출하는 것을 볼 수 있다. 이로 인해서 Half-close가 진행되어 클라이언트로 EOF가 전달된다.
이로 인해서 복사된 파일 디스크립터의 수에 상관없이 Half-close가 진행되며, 이 과정에서 EOF도 전달된다.

728x90
반응형

'스터디 > TCP 와 IP' 카테고리의 다른 글

12. 멀티쓰레드 기반의 서버구현  (0) 2024.09.05
10. 소켓과 표준 입출력  (0) 2024.09.04
9. 다양한 입출력 함수들  (0) 2024.09.03
8. 멀티프로세스 기반의 서버구현  (0) 2024.09.03
7. 도메인 이름과 인터넷 주소  (0) 2024.09.03
'스터디/TCP 와 IP' 카테고리의 다른 글
  • 12. 멀티쓰레드 기반의 서버구현
  • 10. 소켓과 표준 입출력
  • 9. 다양한 입출력 함수들
  • 8. 멀티프로세스 기반의 서버구현
DDD Developer
DDD Developer
  • DDD Developer
    DDD
    DDD Developer
  • 전체
    오늘
    어제
    • 분류 전체보기
      • 개발 일지
        • C언어
        • python 파이썬
        • 기타
        • 데이터베이스
        • TCP 와 IP
        • C++
        • QT
        • C#
      • 스터디
        • C언어
        • python 파이썬
        • TCP 와 IP
        • C++ 스터디
        • QT 스터디
      • 프로젝트
      • 문제풀이
        • C언어
        • python 파이썬
  • 인기 글

  • 최근 글

  • 반응형
  • hELLO· Designed By정상우.v4.10.2
DDD Developer
11. 입출력 스트림의 분리에 대한 나머지 이야기
상단으로

티스토리툴바