티스토리 뷰

반응형

리눅스에서 네트워크 소켓 프로그래밍을 이용해 파일을 다운로드하고 업로드할 수 있는 클라이언트와 서버를 만들어보았다.

 

- 파일 전송 서버는 서버에서 실행중이며 클라이언트가 다운받고자 하는 파일을 전송하는 역할을 한다.

- 파일 전송 클라이언트는 서버에 전송할 파일을 선택하여 전송(업로드)한다.

- 클라이언트는 접속한 디렉토리에 대해서 파일을 업로드, 다운로드 하도록 한다.

- text파일과 binary 파일 모두 전송이 가능하도록 한다.

 

명령어들은 다음과 같이 구현했다.

cd : 현재 디렉토리 이동, get : 다운로드 명령, put : 업로드 명령, quit : 종료 명령

 

 

<서버>

#include <stdio.h >
#include <stdlib.h >
#include <string.h >
#include <sys /stat.h >
#include <sys /socket.h >
#include <netinet /in.h >
#include <fcntl.h >
#include <unistd.h >
#define MAXLINE 511
#define BUFSIZE 256

int tcp_listen(int host, int port, int backlog);  //소켓 생성 및 listen
void errquit(char *mesg) { perror(mesg); exit(0); }

int main(int argc, char *argv[])
{
	struct sockaddr_in cliaddr;
	int listen_sock, accp_sock;
	char command[5], filename[MAXLINE], dir[MAXLINE], buf[BUFSIZE];
	int addrlen, success =0, nbyte;
	FILE* file;
	size_t fsize;
	
	if(argc !=2){
		printf("사용법 : %s [port]\n", argv[0]);
		exit(0);
	}

	listen_sock = tcp_listen(INADDR_ANY, atoi(argv[1]), 5);
	if(listen_sock ==-1)
		errquit("tcp_listen fail");

	addrlen =sizeof(cliaddr);
	accp_sock = accept(listen_sock, (struct sockaddr *)&cliaddr, &addrlen);
	
	while (1) {
		recv(accp_sock, command, 5, 0);  //명령어 수신

		if(!strcmp(command, "cd\n")){  //cd 명령어 처리(경로 변경)
			recv(accp_sock, dir, MAXLINE, 0);  //경로 수신
			if (chdir(dir) ==0) success =1;  //경로 변경 성공
			else success =0;	
			send(accp_sock, &success, sizeof(int), 0);  //성공 여부 전송
		}
		else if (!strcmp(command, "get\n")) {  //get 명령어 처리(다운로드)
			recv(accp_sock, filename, MAXLINE, 0);  //파일 이름 수신
			
			file = fopen(filename, "rb");  //읽기 전용, 이진 모드로 파일 열기
			
			int isnull =0;
			if(file ==NULL){
				isnull =1;
				send(accp_sock, &isnull, sizeof(isnull), 0);
				continue;			
			}	
			send(accp_sock, &isnull, sizeof(isnull), 0);  //파일 존재 여부 전송			
			/*파일 크기 계산*/
			fseek(file, 0, SEEK_END);  //파일 포인터를 끝으로 이동
			fsize = ftell(file);  //파일 크기 계산
			fseek(file, 0, SEEK_SET);  //파일 포인터를 다시 처음으로 이동
			
			size_t size = htonl(fsize);
			send(accp_sock, &size, sizeof(fsize), 0);  //파일 크기 전송
			
			int nsize =0;
			/*파일 전송*/
			while(nsize != fsize){  //256씩 전송
				int fpsize = fread(buf, 1, BUFSIZE, file);
				nsize += fpsize;
				send(accp_sock, buf, fpsize, 0);
			}
			fclose(file);
		}
		
		else if (!strcmp(command, "put\n")) {  //put 명령어 처리(업로드)
			int isnull =0;
			recv(accp_sock, &isnull, sizeof(isnull), 0);
			if(isnull ==1){
				continue;
			}
			
			recv(accp_sock, filename, MAXLINE, 0);  //파일 이름 수신
			
			file = fopen(filename, "wb");  //쓰기 전용, 이진모드로 파일 열기
			recv(accp_sock, &fsize, sizeof(fsize), 0);  //파일 크기 수신
			
			nbyte = BUFSIZE;
			while(nbyte >= BUFSIZE){
				nbyte = recv(accp_sock, buf, BUFSIZE, 0);  //256씩 수신
				success = fwrite(buf, sizeof(char), nbyte, file);  //파일 쓰기
				if(nbyte < BUFSIZE) success =1;
			}
			send(accp_sock, &success, sizeof(int), 0);  //성공 여부 전송
			fclose(file);		
		}
		
		else if (!strcmp(command, "quit")) {  //quit 명령어 처리(종료)
			int isexit =0;
			recv(accp_sock, &isexit, sizeof(int), 0);

			if(isexit){
				printf("프로그램을 종료합니다.\n");
				close(listen_sock);
				close(accp_sock);
				exit(0);
			}
		}
	}
	return 0;
}

int tcp_listen(int host, int port, int backlog) {
	int sd;
	struct sockaddr_in servaddr;

	if ((sd = socket(AF_INET, SOCK_STREAM, 0)) <0){
		errquit("socket fail");
	}

	// servaddr 구조체의 내용 세팅
	bzero((char *)&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(host);
	servaddr.sin_port = htons(port);
	if (bind(sd, (struct sockaddr *)&servaddr, sizeof(servaddr)) <0){
		errquit("bind fail");
	}
	
	// 클라이언트로부터 연결요청을 기다림
	listen(sd, backlog);
	return sd;
}

 

 

<클라이언트>

#include <stdio.h >
#include <stdlib.h >
#include <string.h >
#include <sys /socket.h >
#include <netinet /in.h >
#include <arpa /inet.h >
#include <fcntl.h >
#include <unistd.h >
#define MAXLINE 511
#define BUFSIZE 256

int tcp_connect(char * ip, int host);  //소켓 생성 및 listen
void errquit(char *mesg) { perror(mesg); exit(0); }

int main(int argc, char *argv[])
{
	int listen_sock;
	char command[5], filename[MAXLINE], dir[MAXLINE], temp[5], buf[BUFSIZE];
	int success =0, nbyte;
	FILE* file;
	size_t fsize;

	if (argc !=3) {
		printf("사용법 : %s [server_ip] [port]\n", argv[0]);
		exit(0);
	}

	listen_sock = tcp_connect(argv[1], atoi(argv[2]));
	if (listen_sock ==-1)
		errquit("tcp_connect fail");

	while (1) {
		printf("\n< 명령어 : cd, get, put, quit >\n");
		printf("[command] ");
		fgets(command, 5, stdin); //명령어 입력
		send(listen_sock, command, 5, 0);  //명령어 전송
		
		if (!strcmp(command, "cd\n")) {  //cd 명령어 처리(경로 변경)
			printf("이동을 원하는 경로 : ");
			scanf("%s", dir);  //경로 입력
			fgets(temp, 5, stdin);  //버퍼에 남은 \n 제거
			
			send(listen_sock, dir, MAXLINE, 0);  //경로 전송
			recv(listen_sock, &success, sizeof(int), 0);  //성공 여부 수신
			
			if (success) printf("경로가 변경되었습니다.\n");
			else printf("경로 변경에 실패하였습니다.\n");
		}
		
		else if (!strcmp(command, "get\n")) {  //get 명령어 처리(다운로드)
			printf("다운로드할 파일명 : ");
			scanf("%s", filename);  //파일 이름 입력
			fgets(temp, 5, stdin); //버퍼에 남은 \n 제거
			send(listen_sock, filename, MAXLINE, 0);  //파일 이름 전송
			
			int isnull =0;
			recv(listen_sock, &isnull, sizeof(isnull), 0); 
			if(isnull ==1){
				printf("해당 파일이 존재하지 않습니다.\n");
				continue;
			}
			
			recv(listen_sock, &fsize, sizeof(fsize), 0);  //파일 크기 수신
			file  = fopen(filename, "wb");  //쓰기전용, 이진모드로 파일 열기
			
			nbyte = BUFSIZE;
			while(nbyte >= BUFSIZE){
				nbyte = recv(listen_sock, buf, BUFSIZE, 0);  //256씩 수신
				fwrite(buf, sizeof(char), nbyte, file);  //파일 쓰기
			}
			fclose(file);		
			printf("다운로드가 완료되었습니다.\n");  //다운로드 성공
		}
		
		else if (!strcmp(command, "put\n")) {  //put 명령어 처리(업로드)
			printf("업로드할 파일명 : ");
			scanf("%s", filename);  //파일 이름 입력
			fgets(temp, 5, stdin);  //버퍼에 남은 \n 제거
			
			file = fopen(filename, "rb");  //읽기 전용, 이진 모드로 파일 열기
			
			int isnull =0;
			if(file ==NULL){
				isnull =1;
				send(listen_sock, &isnull, sizeof(isnull), 0);
				printf("해당 파일이 존재하지 않습니다.\n");
				continue;			
			}	
			send(listen_sock, &isnull, sizeof(isnull), 0);  //파일 존재 여부 전송
			
			send(listen_sock, filename, MAXLINE, 0);  //파일 이름 전송
			
			/*파일 크기 계산*/
			fseek(file, 0, SEEK_END);  //파일 포인터를 끝으로 이동
			fsize = ftell(file);  //파일 크기 계산
			fseek(file, 0, SEEK_SET);  //파일 포인터를 다시 처음으로 이동
			
			size_t size = htonl(fsize);
			send(listen_sock, &size, sizeof(fsize), 0);  //파일 크기 전송
			
			int nsize =0;
			/*파일 전송*/
			while(nsize != fsize){  //256씩 전송
				int fpsize = fread(buf, 1, BUFSIZE, file);
				nsize += fpsize;
				send(listen_sock, buf, fpsize, 0);
			}
			fclose(file);
			recv(listen_sock, &success, sizeof(int), 0);  //업로드 성공 여부 수신
			
			if (success) printf("업로드가 완료되었습니다.\n");
			else printf("업로드를 실패했습니다.\n");	
		}
		else if (!strcmp(command, "quit")) {  //quit 명령어 처리(종료)
			int isexit =1;
			send(listen_sock, &isexit, sizeof(int), 0);
			printf("프로그램을 종료합니다.\n");
			close(listen_sock);
			exit(0);
		}
	}
}

int tcp_connect(char *ip, int port) {
	int  sd;
	struct sockaddr_in servaddr;
	
	//소켓 생성
	if ((sd = socket(AF_INET, SOCK_STREAM, 0)) <0){
		errquit("socket fail");
	}
		
	//서버의 소켓주소 구조체 servaddr 내용 세팅
	bzero((char *)&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	inet_pton(AF_INET, ip, &servaddr.sin_addr);
	servaddr.sin_port = htons(port);

	// 연결 요청
	if (connect(sd, (struct sockaddr *)&servaddr, sizeof(servaddr))
		<0){
		errquit("connect fail");
	}
	return sd;
}

 

 

 

 

<동작 과정>

--> 일단, 서버를 위한 터미널 창 1개(왼쪽), 클라이언트를 위한 터미널 창 1개(오른쪽)를 띄운다. 서버 관련 파일들의 폴더는 np/server에 있고, 클라이언트 관련 파일들의 폴더는 np/client에 있다. 폴더 두 개도 띄워서 동시에 확인하도록 했다. 서버 폴더에는 c파일, 실행파일, img_serv.png와 server라는 텍스트 파일이 있다. 클라이언트 폴더에는 c파일, 실행파일, img_cli.png와 client라는 텍스트 파일이 있다.

 

 

 

--> 왼쪽 터미널에서 서버 프로그램 실행을 위해 2017136133_serv 실행파일을 실행한다. 서버 프로그램 실행 시에 인자로 포트번호(5001)를 작성한다. 서버는 socket()으로 소켓을 개설하고 이 소켓을 자신의 소켓 주소와 bind()해둔다. listen()을 호출하여 클라이언트가 연결 요청을 보내오는 것을 기다린다.

오른쪽 터미널 창에서 클라이언트 프로그램 2017136133_cli를 실행한다. 이 때, 첫 번째 명령문 인자로 서버의 ip주소(127.0.0.1), 두 번째 명령문 인자로 포트번호(5001)를 준다. 앞서 서버 프로그램을 실행할 때 작성했던 포트번호와 같아야한다. 연결할 서버의 소켓주소 구조체를 만들어 명령문 인자로 받은 서버의 ip주소와 포트번호를 구조체에 기록한다. 클라이언트의 connect() 연결 요청에 따라 서버는 accept()를 호출한다. 클라이언트 터미널 화면에는 입력 가능한 명령어들이 출력(cd, get, put, quit)되고 사용자로부터 명령어 입력을 기다린다.

* 캡쳐화면에 입력 가능한 명령어들 중 quit가 아니라 exit라고 되어있는데, quit으로 수정하기 전 캡쳐화면이라는 점 양해부탁드립니다.

 

 

 

 

--> 클라이언트 프로그램에서 get(다운로드) 명령을 입력했다. 클라이언트에서 입력하는 명령어는 서버에게 send()함수로 전송되어 서버는 recv()하고 그에 맞는 동작들을 수행한다. get명령을 입력하면 다운로드 할 파일명을 입력하라는 문구가 출력되고 입력을 기다린다. np/server폴더의 파일들을 다운로드하는 것이므로 server 텍스트 파일을 다운로드해보았다. server라고 파일명을 터미널창에 입력하면 클라이언트가 send()함수로 입력한 파일명을 서버에게 전송한다. 그러면 서버는 recv()함수로 수신한 파일명을 fopen()함수의 rb모드로 파일을 연다. (읽기 + 바이너리 파일 전송을 위해 이진모드 사용). 그 후에 클라이언트에게 파일의 크기를 계산하여 send()하고 클라이언트는 fopen()함수의 wb모드를 실행한다. (쓰기 + 바이너리 파일 전송을 위해 이진모드 사용). 서버는 버퍼 크기 256씩 fread()하여 send()하고 클라이언트는 recv()하여 fwrite()로 파일을 쓰게 된다. 앞의 과정들로 np/client 폴더에 원하는 파일이 다운로드(저장) 되었다. 클라이언트 터미널 창에 다운로드가 완료되었습니다 라고 출력되고 다음 명령어를 기다린다.

 

 

 

--> np/server폴더의 server 텍스트 파일과 다운로드한 np/client 폴더의 server 텍스트 파일의 내용이 같은 것을 보아 잘 복사되었음을 알 수 있다.

 

 

 

--> 이번엔 get명령어를 통해 binary 파일을 다운받았다. 다운로드할 파일명으로 np/server폴더의 img_serv.png를 입력했다. get명령어의 동작은 위에서 설명했던 것과 같다. fopen()을 rb모드와 wb모드로 실행했기 때문에 binary파일도 잘 전송된다. np/client폴더에 img_serv.png가 잘 다운로드 되었음을 볼 수 있다.

 

 

 

--> 만약, get 명령어를 사용하는데 다운로드 할 파일명을 존재하지 않는 파일명을 입력한다면 fopen()시에 리턴값이 NULL이 되기 때문에 해당 파일이 존재하지 않습니다 라는 문구가 출력되고 마치는 것으로 구현했다.

 

 

 

 

--> 이번에는 put 명령어(업로드)를 실행해보았다. put 명령어를 입력하면 업로드할 파일명을 입력하라고 한다. np/client폴더의 client 텍스트 파일을 서버 폴더에 업로드 해보겠다. 클라이언트는 입력받은 파일명을 fopen()의 rb모드(읽기 + 이진모드)로 실행하여 파일을 연다. 그 후에 서버에게 send()로 파일명을 전송한다. 서버는 recv()로 파일명을 수신하고 fopen()의 wb(쓰기 + 이진모드)를 실행한다. 그 다음, 클라이언트는 파일의 크기를 계산하여 send()하고 서버는 이를 recv()한다. 이 크기를 이용하여 클라이언트는 버퍼의 크기 256씩 fread()하여 send()한다. 서버는 클라이언트가 보내온 것을 recv()하고 fwrite()로 파일을 np/server 폴더에 저장한다. np/server 폴더에 client 텍스트 파일 업로드가 성공적으로 되었음을 볼 수 있다. 서버는 업로드 성공여부를 send()하고 클라이언트는 recv()하여 업로드가 완료되었습니다 문구를 출력한다.

 

 

 

 

 

--> 업로드된 np/server 폴더의 client 텍스트 파일과 원래 파일 np/client 폴더의 client 텍스트 파일이 내용이 같은 것을 보아 잘 업로드 되었음을 알 수 있다.

 

 

 

 

 

--> 이번에는 binary 파일을 업로드하기 위해 put명령을 입력한다. 업로드할 파일명으로 np/client 폴더의 img_cli.png를 입력한다. 앞서 설명한 put 명령어의 동작 방법과 똑같이 동작한다. binary파일인 이미지 파일도 잘 업로드되는 것을 볼 수 있다. np/server 폴더에 잘 업로드 되었다.

 

 

 

--> put 명령어에서도 업로드 할 파일명을 존재하지 않는 파일명을 입력한다면 fopen()시에 리턴값이 NULL이 되기 때문에 해당 파일이 존재하지 않습니다 라는 문구가 출력되고 마치는 것으로 구현했다.

 

 

 

 

-->  다운로드는 전체 디렉토리를 대상으로 가능하다고 했기 때문에 cd 명령어를 넣었다. cd 명령어로 현재 다운로드 경로를 변경하고 다운로드 할 수 있도록 했다. cd 명령어를 입력하면 이동을 원하는 경로를 입력하라고 터미널 창에 출력된다. ..를 입력하여 한 단계 상위 디렉토리로 경로를 변경해보았다. 클라이언트는 입력받은 경로를 서버에게 send()하고, 이를 recv()로 수신한 서버는 chdir()함수를 이용하여 디렉토리 경로를 변경했다. np/server 폴더에서 한 단계 상위 디렉토리인 np로 이동했다. 위의 캡쳐화면에 보면, np폴더를 띄워놓았다. cd 명령이 성공적으로 이루어지면 클라이언트 터미널 화면에 경로가 변경되었습니다 라는 문구가 출력되고 다음 명령을 기다린다.

 

 

 

 

--> 다음에 이동된 np 폴더에서 파일을 다운받기 위해 get 명령어를 실행해보았다. np 폴더의 tcp_talkserv 실행파일을 다운로드 해보겠다. 파일명을 입력하고 앞서 말한 get 동작 과정을 통해 np/client 폴더에 성공적으로 다운로드 된 것을 볼 수 있다. 성공적으로 다운로드 되어 클라이언트 터미널 화면에 다운로드가 왼료되었습니다 문구가 출력되었다. 어떤 종류의 파일이든 잘 동작하는 것을 볼 수 있다. 이렇게 cd 명령어로 현재 디렉토리를 변경해가면서 다운로드 할 수 있기 때문에 전체 디렉토리를 대상으로 가능하다.

 

 

 

--> 이번엔 명령어로 quit를 입력해보았다. 이는 프로그램 종료 명령어이다. quit 명령어를 입력하면, 클라이언트와 서버가 모두 소켓들을 close()하고 통신을 멈추고 프로그램을 종료한다. 클라이언트와 서버 모두 잘 종료되었음을 볼 수 있다.

 

 

 

 

 

엄청 어렵게 구현하진 않았다. 그런데 중간에 막혔던 점 중 하나는 다 구현했는데 텍스트 파일은 잘 다운로드/업로드 되는데 바이너리 파일이 잘 되지 않는 문제가 있었다. 내가 파일을 읽어 보내는 과정에서 잘못했다는 것을 깨달았다. 너무 큰 크기의 파일은 while문을 이용해 256 크기씩 읽고 보내야하는데, 이것을 구현하지 않아서 제대로 동작이 되지 않는 현상이 있었다.

또, send()와 recv()가 너무 많다보니 동작의 순서를 헷갈리지 않고 구현하는 것이 좀 힘들었다. 차근 차근 어떻게 동작해야하는지 생각하면서 코드를 작성하다보니 성공적으로 구현했다. 

하고 나니 뿌듯하다. 소켓 프로그래밍을 들어보기만 했는데, 무엇인지 구현하면서 몸소 느낄 수 있어서 좋았다. 더 깊이 공부할 수 있는 기회가 있었으면 좋겠다. 😎

반응형

'CS > 운영체제' 카테고리의 다른 글

[운영체제] 메모리 구조 (Heap vs Stack)  (0) 2023.12.14
[운영체제] 메모리 관리  (2) 2023.05.11
댓글
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday