간단한 웹서버 만들기(simple webserver)[C,Linux,Fork]

간단한 웹서버 만들기(simple webserver)[C, Linux, fork]

안녕하세요.

오늘은 유닉스라는 과목을 들으면서 진행했던 프로젝트입니다.

  • 간단한 웹서버를 만들기
  • multiple client의 요청을 동시에 처리가능하게 만들어야 함.
  • 웹서버를 시작할때 인자로 서비스할 폴더의 위치와 포트를 받는다.
  • total.cgi(nn~mm까지의 총 합을 구해서 화면에 출력)
  • 전송 기록을 log파일로 저장한다.(로그: 클라이언트 아이피, 전송한 파일명, 전송한 크기)

코드

코드는 아래와 같습니다.

코드 파일은 깃허브에서 다운받으실 수 있습니다.(깃허브에 쓰레드로 만들다 만 웹서버도 있습니다.)

https://github.com/ykarr/webserver/blob/master/webserver_fork_01.c

 

ykarr/webserver

Contribute to ykarr/webserver development by creating an account on GitHub.

github.com

/*fork 서버*/
#include <stdio.h> //표준입출력라이브러리
#include <stdlib.h> //표준입출력라이브러리
#include <unistd.h> //fork사용라이브러리
#include <errno.h> //오류코드 매크로 정의
#include <string.h> //문자열처리 라이브러리
#include <fcntl.h> //파일관련 라이브러리
#include <signal.h> //시그널처리 라이브러리
#include <sys/types.h> //시스템관련 라이브러리
#include <sys/socket.h> //네트워크통신 라이브러리
#include <netinet/in.h> //인터넷주소체계 사용 라이브러리
#include <arpa/inet.h> //버클리소켓사용 라이브러리
#include <sys/stat.h> // 파일정보 라이브러리
#define BUFSIZE 1012 // 버프사이즈 정의
#define LOG   44 //로그 정의
#define HOME /index.html //home 정의
struct stat s; //아래에서 파일 크기를 구하기 위해서 사용함 
struct {//구조체 
	char *ext; //char 형식 변수 선언
	char *filetype; // char 형식 변수 선언
} extensions [] = {
	{"gif", "image/gif" },  //gif 
	{"jpg", "image/jpg"},    //jpg
	{"jpeg","image/jpeg"},   //jpeg
	{"png", "image/png" },  //png
	{"htm", "text/html" },  //htm
	{"html","text/html" },  //html
	{0,0} };//NULL

int cgi(char *buf){ //cgi 
	char *result; //char형식의 변수 선언 
	char token[]="=&";// char 형식의  변수 선언 
	int i; //int형 변수 선언 
	int n1,n2;//int형변수 n1,n2선언 
	int sum=0;//int형 변수 sum선언 
	result=strtok(buf,token); // =이전의 문자열 자르기
	result=strtok(NULL,token); // =,&사이의 NNN 잘라서 저장
	n1=atoi(result); //result를 정수형으로 n1에 저장 
	strtok(NULL,token); // &,=사이의 to 자르기
	result=strtok(NULL,token);// &,=사이의 MMM 잘라서 저장
	n2=atoi(result);//result를 정수형으로 n2에 저장 

	i=-(n1-n2)+1;// 더하는 원소 갯수 판별
	int half=i/2;//i를 반절로 
	if(i&1){//변경 후 %2대신 &1사용해서 홀수 인지 짝수인지 
		//if(i%2==1){//변경전 
		sum=(n1+n2)*half;//cgi계산 
		sum+=n1+half;//cgi 계산 
	}
	else
		sum=(n1+n2)*half;//cgi 계산 
	return sum;//계산 결과를 리턴 
}

void log(char *s1, char *s2, int size)//로그 매개변수 
{
	int fpp;//로그에 쓰기 위해서 선언 
	char logbuffer[200];//로그 버 퍼 
	sprintf(logbuffer,"%s %s %d\n",s1, s2,size); //s0=send/req, s1= ip ,s2= path/filename , size=크기,num=숫자    
	if((fpp= open("./server.log",O_WRONLY | O_APPEND,0644)) >= 0) {// 파일을 연다. 
		write(fpp,logbuffer,strlen(logbuffer)); //버퍼의 내용을 로그에 작성한다. 
		close(fpp);//type을 close해준다. 
	}
}

int main(int argc, char *argv[])//메인함수 
{
	int ff;//로그 파일을 재설정하기 위해서  
	ff=open("./server.log", O_CREAT|O_TRUNC,0644);//로그파일을 열어준다. 
	//printf("start\n");
	close(ff);//로그파일을 닫아준다. 
	int i, port,listenfd, socketfd, hit;//int형으로 선언 
	pid_t pid;//포크를 사용하기 위해 선언 
	size_t length;//
	static struct sockaddr_in cli_addr,serv_addr; //소켓 사용을 위한 구조체 
	char *path=argv[1]; // path에 경로 지정
	signal(SIGCLD, SIG_IGN);  // 자식프로세스중 하나라도 종료되면 부모에게 신호전달 
	signal(SIGHUP, SIG_IGN);  // 사용자 터미널의 단절 보고
	if((listenfd = socket(AF_INET, SOCK_STREAM,0)) <0){ //소켓 파일기술자 생 
		perror("error");//에러 
		exit(1);//나간다. 
	}
	port = atoi(argv[2]); //입력받은 포트값을 port에 저장 
	memset((char*)&serv_addr,'\0',sizeof(serv_addr));//초기화 
	serv_addr.sin_family = AF_INET;//소켓 주소 구조체1 
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);//all ip?소켓 주소 구조체2 
	serv_addr.sin_port = htons(port);//넣어준 포트로 설정 소켓 주소 구조체3
	if(bind(listenfd, (struct sockaddr *)&serv_addr,sizeof(serv_addr)) <0){//소켓에 이름 지정하기 
		perror("error");//bind실패시 출력 
		exit(1); //나간다. 
	}
	if( listen(listenfd,100) <0){//클라이언트 연결 기다리기 
		perror("error");//listen실패시 출력 
		exit(1);//나간다. 
	}
	char *buff;//문자형 변수선언 
	for(hit=1;;hit++){//while문과 동일 
		length = sizeof(cli_addr);//cli_addr사이즈를 length에 저장한다. 
		if((socketfd = accept(listenfd, (struct sockaddr *)&cli_addr, &length)) < 0){//연결요청 수락 
			perror("error");//accept가 잘안되면 실행 
			exit(1);//나간다. 
		}
		buff=inet_ntoa(cli_addr.sin_addr);//아이피를 buff에 저장한다. 
		if((pid = fork()) < 0) {//안열리면 
			exit(1);//나간다. 
		}
		else{//아니면 실행 
			if(pid == 0) {////////////////////fork시작 
				close(listenfd);//listenfd를 닫아준다. 
				char file_name[50];//파일 이름
				// int size;//파일 크기를 구하기 위한 변수 선언 
				int j, file_fd, buflen, len;//int형변수 선언 
				int i, ret;//int형 변수 선언 
				char * fstr;//content type을 저장할 문자열 변수 
				static char buffer[BUFSIZE+1];//버퍼 선언   
				ret =read(socketfd,buffer,BUFSIZE); //fd에서 계속 읽어옴  
				if(ret == 0 || ret == -1) {//읽기 실패하면 
					exit(1);//나간다. 
				}
				if(ret > 0 && ret < BUFSIZE)  //ret이 0보다 크고 BUFSIZE보다 작으면 
					buffer[ret]=0;   //buffer[ret]은 0이 된다. 
				else buffer[0]=0;//위를 만족하지 않는다면 buffer[0]=0이된다. 
					for(i=4;i<BUFSIZE;i++) { //GET /images/05_08-over.gif 이런식으로 만들어줌 
					if(buffer[i] == ' ') { //공백을 확인 
						buffer[i] = 0;//공백일때 0 
						break;//for문 탈출 
					}
				}
				if( !strncmp(&buffer[0],"GET /\0",6))//GET /\0일때  
					strcpy(buffer,"GET /index.html");   //index.html출력하도록 request변경 
				buflen=strlen(buffer); // buflen에 buffer길이 저장
				fstr = NULL;//null로 초기화 
				for(i=0;extensions[i].ext != 0;i++) { // 구조체 내 탐색
					len = strlen(extensions[i].ext); // 길이지정
					if( !strncmp(&buffer[buflen-len], extensions[i].ext, len)) { // 지정한 문자 갯수까지만 비교
						fstr =extensions[i].filetype; //gif형식이면 image/gif로 
						break;//for문을 나간다. 
					}
				}
				strcpy(file_name,&buffer[5]);//buffer[5] 즉 파일 이름을 filename에 복사해준다. 
			//	printf("%s\n",file_name); 
				sprintf(path,"%s/%s",path,file_name);//path에 path/filename을 해준다. 
				file_fd = open(path,O_RDONLY); //get을 떼어네고 파일을 열어봄 없는 파일일때 
				fstat(file_fd,&s);//폴더인지 아닌지 확인하기 위해서 사용 
				if(file_fd==-1){//파일이 아니라면? 
					if(strstr(file_name,"&to")){ //받은 파일 이름에 &to가 있다면? 
						int n=cgi(&buffer[5]);//파일내용을 cgi에 넣고 cgi의 리턴값을 n으로 받는다. 
						sprintf(buffer,"HTTP/1.1 200 OK\nContent-Type: text/html\r\n\r\n<HTML><BODY><H1>%d</H1></BODY></HTML>\r\n",n); /* Header + a blank line */
						// write(socketfd,buffer,strlen(buffer));//socketfd에 버퍼내용을 써줌. 
						// sprintf(buffer, "",n);//cgi내용 출력 
						write(socketfd,buffer,strlen(buffer));//socketfd에 버퍼를 써준다.
					//	printf("%s\n",file_name); 
						log(buff,file_name,strlen(buffer)-80); //ip,파일, 몇번 째                     
					}
					else{
						sprintf(buffer,"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<HTML><BODY><H1>NOT FOUND</H1></BODY></HTML>\r\n");//200으로 헤더를 설정 
						//write(socketfd,buffer,strlen(buffer));       //fd에 버퍼의 내용을 써준다.  
						//sprintf(buffer, "");   //버퍼에 화면에 입력할 내용을 저장 
						write(socketfd,buffer,strlen(buffer)); //버퍼의 내용을 fd에 써준다. 
						log(buff,file_name,9);//로그작성    
					}
					exit(1);//나간다. 
				}
				else if(S_ISDIR(s.st_mode)){//파일일때 
					sprintf(buffer,"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<HTML><BODY><H1>NOT FOUND</H1></BODY></HTML>\r\n");//200으로 헤더를 설정 
					//write(socketfd,buffer,strlen(buffer));       //fd에 버퍼의 내용을 써준다.  
					//sprintf(buffer, "");   //버퍼에 화면에 입력할 내용을 저장 
					write(socketfd,buffer,strlen(buffer)); //버퍼의 내용을 fd에 써준다. 
					log(buff,file_name,9);//로그작성 
					exit(1);    //나감 
				}
				sprintf(buffer,"HTTP/1.1 200 OK\r\nContent-Type: %s\r\n\r\n", fstr);//200으로 헤더를 설정 
				log(buff,file_name,s.st_size); //로그작성 
				write(socketfd,buffer,strlen(buffer));//socekfd에 버퍼를 써준다. 
				while ((ret = read(file_fd, buffer, BUFSIZE)) > 0 ) {//파일을 읽는다. 
					write(socketfd,buffer,ret);//읽은 내용을 써준다. 
				}
				exit(1);//나간다. 
			} 
			else {
				close(socketfd);//socketfd를 닫는다. 
			}
		}
	}
	return 0; // 0반환
}

실행(간단한 기능 확인)

리눅스환경에서 실행했습니다.

실행할때 html이 있는 폴더를 지정해주고 포트를 설정해줍니다.

이런식으로 웹 서버는 잘 동작합니다.

cgi역시 제대로 동작하네요.

로그는 아래와같이 클라이언트 아이피, 요청한파일(전송한 파일)파일, 크기가 정상적으로 출력이 되는 것을 확인할 수 있었습니다.

주요 코드 설명

이번 글은 코드에 주석이 다 달려있으므로 생략하겠습니다.

 

+추가 파일&코맨트

유닉스의 프로젝트로 만든 웹서버입니다.(이리저리 뒤져가며 만들었습니다.)

진짜 뭣도 모르는 상태에서 시작했는데 짧은 시간에 한 것 치고는 결과가 괜찮은 것 같습니다.

아래 파일은 어짜피 나밖에 못보니까 다운받지 마세요.

server.zip
5.97MB

 

Designed by JB FACTORY