Echo server 는 클라이언트로부터 받은 메시지를 그대로 돌려보내는 간단한 서버다. 네트워크 연결, 소켓 프로그래밍, 서버-클라이언트 아키텍처 등을 이해하기 위한 교육용 또는 테스트용으로 주로 사용된다.
echoserveri.c
#include " csapp.h "
void echo ( int connfd );
int main ( int argc , char ** argv )
{
int listenfd , connfd ;
socklen_t clientlen ;
struct sockaddr_storage clientaddr ;
char client_hostname [ MAXLINE ], client_port [ MAXLINE ];
if ( argc != 2 ) {
fprintf ( stderr , " usage: %s <port> \n " , argv [ 0 ]);
exit ( 0 );
}
listenfd = Open_listenfd ( argv [ 1 ]);
while ( 1 ) {
clientlen = sizeof( struct sockaddr_storage );
connfd = Accept ( listenfd , ( SA *)& clientaddr , & clientlen );
Getnameinfo (( SA *) & clientaddr , clientlen , client_hostname , MAXLINE ,
client_port , MAXLINE , 0 );
printf ( " Connected to ( %s , %s ) \n " , client_hostname , client_port );
echo ( connfd );
Close ( connfd );
}
exit ( 0 );
}
1. 프로그램은 실행할 때 하나의 인자(포트 번호)를 받아야 한다. 인자의 개수( argc)가 2가 아니면(프로그램 이름 포함), 사용법을 출력하고 프로그램을 종료한다.
2. Open_listenfd 함수는 제공된 포트 번호에 대해 듣기(listen) 상태의 소켓을 열고, 이 소켓의 파일 기술자를 반환한다. 이 함수는 socket(), bind(), listen() 시스템 호출을 캡슐화하여 사용하기 쉽게 만든 것이다.
3. 무한 루프 안에서, 서버는 Accept 함수를 통해 클라이언트의 연결 요청을 기다린다. 연결이 수립되면, 클라이언트의 주소 정보를 가져오기 위해 Getnameinfo 함수를 사용한다. 이 정보는 클라이언트의 호스트 이름과 포트 번호를 문자열로 변환한다.
4. 클라이언트로부터 받은 데이터를 처리하고, 그 데이터를 클라이언트에게 다시 전송하는 echo 함수를 호출한다. echo 함수의 구현은 제공되지 않았으나, 일반적으로 클라이언트로부터 데이터를 읽고( read), 동일한 데이터를 클라이언트로 다시 쓰는( write) 작업을 수행한다.
echoclient.c
#include " csapp.h "
int main ( int argc , char ** argv )
{
int clientfd ;
char * host , * port , buf [ MAXLINE ];
rio_t rio ;
if ( argc != 3 ) {
fprintf ( stderr , " usage: %s <host> <port> \n " , argv [ 0 ]);
exit ( 0 );
}
host = argv [ 1 ];
port = argv [ 2 ];
clientfd = Open_clientfd ( host , port );
Rio_readinitb (& rio , clientfd );
while ( Fgets ( buf , MAXLINE , stdin ) != NULL ) {
Rio_writen ( clientfd , buf , strlen ( buf ));
Rio_readlineb (& rio , buf , MAXLINE );
Fputs ( buf , stdout );
}
Close ( clientfd ); //line:netp:echoclient:close
exit ( 0 );
}
1. 프로그램은 실행할 때 두 개의 인자(호스트와 포트)를 받아야 한다. 인자의 개수( argc)가 3이 아니면 사용법을 출력하고 프로그램을 종료한다.
2. Open_clientfd 함수를 사용해 주어진 호스트와 포트에 대한 연결을 시도하고, 연결된 소켓의 파일 기술자를 반환한다. 이 함수는 socket(), connect() 등의 시스템 호출을 캡슐화한다.
3. Rio_readinitb 함수를 사용해 robust I/O (RIO) 패키지를 초기화한다. 이 패키지는 네트워크 I/O를 위한 더 안정적이고 편리한 인터페이스를 제공한다.
4. Fgets, Fputs를 이용한 루프는 EOF(End Of File)를 입력할 때까지 (예: 터미널에서 Ctrl+D, C를 누를 때) 계속된다.
echoserver 테스트
Tiny server 는 극도로 가벼운 웹 서버를 의미한다. 리소스가 제한된 환경에서 작동하거나, 간단한 웹 기반 애플리케이션을 호스팅하는 데 사용될 수 있다. 가볍다는 특성 때문에 빠른 성능을 요구하는 작은 규모의 프로젝트나 임베디드 시스템에서 유용하다.
tiny.c(main)
int main ( int argc , char ** argv )
{
int listenfd , connfd ;
char hostname [ MAXLINE ], port [ MAXLINE ];
socklen_t clientlen ;
struct sockaddr_storage clientaddr ;
/* Check command line args */
if ( argc != 2 )
{
fprintf ( stderr , " usage: %s <port> \n " , argv [ 0 ]);
exit ( 1 );
}
listenfd = Open_listenfd ( argv [ 1 ]);
while ( 1 )
{
clientlen = sizeof( clientaddr );
connfd = Accept ( listenfd , ( SA *)& clientaddr , & clientlen ); // line:netp:tiny:accept
Getnameinfo (( SA *)& clientaddr , clientlen , hostname , MAXLINE , port , MAXLINE , 0 );
printf ( " Accepted connection from ( %s , %s ) \n " , hostname , port );
doit ( connfd ); // line:netp:tiny:doit
Close ( connfd ); // line:netp:tiny:close
}
}
프로그램은 정확히 하나의 명령줄 인자(서버가 리슨할 포트 번호)를 기대한다. 만약 제대로 된 인자가 제공되지 않으면, 사용법을 출력하고 프로그램을 종료한다.
Open_listenfd 함수를 호출하여 리슨 소켓을 생성하고, 제공된 포트 번호에 바인드한다. 이 함수는 내부적으로 socket(), bind(), listen() 시스템 호출을 수행한다.
tiny.c(parse_uri)
int parse_uri ( char * uri , char * filename , char * cgiargs )
{
char * ptr ;
if (! strstr ( uri , " cgi-bin " ))
{ /* Static content */ // line:netp:parseuri:isstatic
strcpy ( cgiargs , "" ); // line:netp:parseuri:clearcgi
strcpy ( filename , " . " ); // line:netp:parseuri:beginconvert1
strcat ( filename , uri ); // line:netp:parseuri:endconvert1
if ( uri [ strlen ( uri ) - 1 ] == ' / ' ) // line:netp:parseuri:slashcheck
strcat ( filename , " adder.html " ); // line:netp:parseuri:appenddefault
return 1 ;
}
else
{ /* Dynamic content */ // line:netp:parseuri:isdynamic
ptr = index ( uri , ' ? ' ); // line:netp:parseuri:beginextract
if ( ptr )
{
strcpy ( cgiargs , ptr + 1 );
* ptr = ' \0 ' ;
}
else
strcpy ( cgiargs , "" ); // line:netp:parseuri:endextract
strcpy ( filename , " . " ); // line:netp:parseuri:beginconvert2
strcat ( filename , uri ); // line:netp:parseuri:endconvert2
return 0 ;
}
}
정적 콘텐츠 처리 :
1. strstr 함수를 사용하여 URI에 "cgi-bin" 문자열이 포함되어 있는지 확인한다. 포함되어 있지 않다면, 요청은 정적 콘텐츠로 간주된다.
2. cgiargs를 빈 문자열로 설정한다. 이는 정적 콘텐츠 요청에서 CGI 인자가 필요 없음을 의미한다.
3. filename에 현재 디렉토리를 나타내는 "."을 먼저 복사하고, 그 뒤에 URI를 이어 붙인다. 이렇게 함으로써 서버의 로컬 파일 시스템 상에서 해당 파일의 경로를 생성한다.
4. URI가 슬래시( /)로 끝난다면, 이는 디렉토리에 대한 요청으로 간주되며, 기본 파일 이름(예: "adder.html")을 filename에 추가한다.
동적 콘텐츠 처리 :
1. URI에 "cgi-bin"이 포함되어 있다면, 요청은 동적 콘텐츠로 간주된다.
2. index 함수를 사용하여 URI 내의 '?' 문자를 찾는다. 이 문자는 CGI 인자의 시작을 나타낸다.
3. '?' 문자가 존재한다면, 이를 기준으로 문자열을 나눈다. '?' 뒤의 문자열은 CGI 인자로, cgiargs에 복사된다. '?' 문자 자체는 '\0'(null 문자)로 대체되어, URI 문자열을 종료시킨다.
4. filename에 현재 디렉토리를 나타내는 "."을 복사하고, 변경된 URI(인자가 제거된 상태)를 이어 붙인다.
tiny.c(clienterror)
void clienterror ( int fd , char * cause , char * errnum , char * shortmsg , char * longmsg ){
char buf [ MAXLINE ], body [ MAXBUF ];
/* Build the HTTP response body*/
sprintf ( body , " <html><title>Tiny Error</title> " );
sprintf ( body , " %s <body bgcolor= "" ffffff "" > \r\n " , body );
sprintf ( body , " %s%s : %s \r\n " , body , errnum , shortmsg );
sprintf ( body , " %s <p> %s : %s \r\n " , body , longmsg , cause );
sprintf ( body , " %s <hr><em>The Tiny Web Server</em> \r\n " , body );
/* Print the HTTP response */
sprintf ( buf , " HTTP/1.1 %s %s \r\n " , errnum , shortmsg );
Rio_writen ( fd , buf , strlen ( buf ));
sprintf ( buf , " Content-type: text/html \r\n " );
Rio_writen ( fd , buf , strlen ( buf ));
sprintf ( buf , " Content-length: %d \r\n\r\n " , ( int ) strlen ( body ));
Rio_writen ( fd , buf , strlen ( buf ));
Rio_writen ( fd , body , strlen ( body ));
}
/* $end clienterror */
void echo ( int connfd )
{
size_t n ;
char buf [ MAXLINE ];
rio_t rio ;
Rio_readinitb (& rio , connfd );
while (( n = Rio_readlineb (& rio , buf , MAXLINE )) != 0 )
{
if ( strcmp ( buf , " \r\n " ) == 0 )
break ;
Rio_writen ( connfd , buf , n );
}
}
sprintf를 사용해 HTML 형식의 오류 메시지를 body 버퍼에 작성한다. 오류 제목, 배경색, HTTP 상태 코드, 짧은 오류 메시지, 긴 오류 메시지, 오류 원인이 포함된다.
오류 상태 코드와 짧은 메시지를 포함하는 HTTP 응답의 시작 라인을 buf 버퍼에 작성한다. 컨텐츠 타입( text/html)을 지정하고, 계산된 응답 본문의 길이를 사용해 Content-length 헤더를 작성한다.
adder.c
#include " csapp.h "
int main ( void ) {
char * buf , * p ;
char arg1 [ MAXLINE ], arg2 [ MAXLINE ], content [ MAXLINE ];
int n1 = 0 , n2 = 0 ;
/* Extract the two arguments */
if (( buf = getenv ( " QUERY_STRING " )) != NULL ) {
p = strchr ( buf , ' & ' );
* p = ' \0 ' ;
sscanf ( buf , " first= %d " , & n1 );
sscanf ( p + 1 , " second= %d " , & n2 );
}
/* Make the response body */
sprintf ( content , " QUERY_STRING= %s " , buf );
sprintf ( content , " Welcome to add.com: " );
sprintf ( content , " %s THE Internet addition portal. \r\n <p> " , content );
sprintf ( content , " %s The answer is: %d + %d = %d \r\n <p> " , content , n1 , n2 , n1 + n2 );
sprintf ( content , " %s Thanks for visiting! \r\n " , content );
/* Generate the HTTP response */
printf ( " Connection: close \r\n " );
printf ( " Content-length: %d \r\n " , ( int ) strlen ( content ));
printf ( " Content-type: text/html \r\n\r\n " );
printf ( " %s " , content );
fflush ( stdout );
exit ( 0 );
}
1. getenv("QUERY_STRING")를 통해 환경변수에서 쿼리 문자열( QUERY_STRING)을 가져온다. 이 문자열은 웹 브라우저(또는 다른 클라이언트)에서 입력한 데이터를 포함하며, first=숫자1&second=숫자2 형식으로 되어 있다.
2. 쿼리 문자열에서 & 문자를 찾아 두 부분으로 나눈다( first=숫자1과 second=숫자2). 이후, sscanf를 사용해 각 부분에서 숫자를 추출하고, 정수 변수 n1, n2에 저장한다.
3. 응답 본문을 구성하는 여러 sprintf 호출을 통해 HTML 형식의 응답을 만든다. 이 응답은 방문자에게 환영 메시지, 계산된 결과, 그리고 방문에 대한 감사 메시지를 포함한다.
4. printf를 사용해 HTTP 헤더를 출력한다. 여기에는 연결 종료( Connection: close), 컨텐츠 길이( Content-length), 컨텐츠 타입( Content-type: text/html)이 포함된다. 마지막으로, 만들어진 HTML 컨텐츠 본문을 출력한다.
5. fflush(stdout)를 호출해 출력 버퍼를 비우고, exit(0)을 호출해 프로그램을 정상적으로 종료한다.
이 코드는 예전에 구현한 코드이고 지금 보니, adder.c 코드에는 두 가지 문제점이 있다.
1. 현재 코드에는 sprintf(content, "QUERY_STRING=%s", buf);라는 라인이 있으나, 이후에 content에 다른 문자열을 쓸 때마다 이 내용을 덮어쓰게 되므로, 실제로는 이 내용이 응답에 포함되지 않는다. 적절한 응답 메시지 생성을 위해 sprintf 대신 strcat을 사용하거나, sprintf를 사용할 때 content에 계속해서 추가하는 방식으로 바꿔야 한다.
2. *p = '\0'; 이 부분에서 p가 NULL인 경우(즉, 쿼리 문자열에 &가 없는 경우)를 처리하지 않고 있다. 이 경우 p를 참조하려고 하면 프로그램이 비정상적으로 종료될 수 있다. 따라서 p가 NULL이 아닌지 확인하는 조건문이 필요하다.
위의 모든 코드는 yunsejin/tiny에 있다.
GitHub - yunsejin/Webproxy-lab
Contribute to yunsejin/Webproxy-lab development by creating an account on GitHub.
github.com