본문 바로가기

진리는어디에

간단한 에코 서버 제작

/**
 간만에 주말이 왔다. 내일은 약속도 없고(뭐 평소에도 그렇게 약속이 있었던 편은 아니지만), 할 일도 없으니(뭐 평소에도 주말이 바빴던 것은 아니지만) 오랫동안 생각해 왔던 것의 처음을 시작 하려고 한다. 지금 부터 얼마나 걸릴지 모르겠지만 리눅스 소켓 프로그래밍에 관련된 글을 쓰고자 한다.

 이 문서는 오로지 리눅스/유닉스 계열의 소켓 프로그래밍에 관려해서만 다룰 예정이며, 내가 아직 소켓프로그래밍의 정점에 선 사람이 아니기 때문에 내 머리 속에 있는 내용을 정리 하기 보다는 다른 사람들이 적어 놓은 책들을 보고 정리 하는 차원이 될 지도 모르겠다.
*/
 
 아래의 코드는 단순히 하나의 컨넥션을 받아 들이고, 딱 한번만 클라이언트에서 오는 메시지를 에코하고 종료 되는 초 간단한 에코서버다. 다른 설명 보다도 일단 아래의 코드를 작성 해보도록 하자.

example1.cpp
      1 #include <iostream>
      2 #include <sys/socket.h>
      3 #include <arpa/inet.h>
      4
      5 #define BUFSIZE 256
      6
      7 int main(int argc, char** argv) {
      8     if(argc != 2) {
      9         std::cerr << "Usage : " << argv[0] << " PORT" << std::endl;
     10         return -1;
     11     }
     12
     13     int listenSock = ::socket(AF_INET, SOCK_STREAM, 0);
     14     if(listenSock < 0) {
     15         std::cerr << "socket() error" << std::endl;
     16         return -1;
     17     }
     18
     19     struct sockaddr_in addr;
     20     memset(&addr, 0, sizeof(sockaddr_in));
     21     addr.sin_family = AF_INET;
     22     addr.sin_addr.s_addr = htonl(INADDR_ANY);
     23     addr.sin_port = htons(atoi(argv[1]));
     24
     25     if(0 > ::bind(listenSock, (struct sockaddr*)&addr, sizeof(struct sockaddr_in))) {
     26         std::cerr << "bind() error" << std::endl;
     27         return -1;
     28     }
     29
     30     if(0 > ::listen(listenSock, 5)) {
     31         std::cerr << "listen() error" << std::endl;
     32         return -1;
     33     }
     34
     35     int clntSock = -1;
     36     socklen_t len = sizeof(struct sockaddr_in);
     37     struct sockaddr_in clntAddr;
     38
     39     if((clntSock = ::accept(listenSock, (struct sockaddr*)&clntAddr, &len)) < 0) {
     40         std::cerr << "accept() error" << std::endl;
     41         return -1;
     42     }
     43
     44     char recvBuffer[BUFSIZE];
     45
     46     int recvByte = ::read(clntSock, recvBuffer, BUFSIZE-1);
     47     if(recvByte == 0) {
     48         std::cerr << "read EOF" << std::endl;
     49         ::close(clntSock);
     50         ::close(listenSock);
     51         return -1;
     52     }
     53     recvBuffer[recvByte] = '\0';
     54     std::cout << "receive : " << recvBuffer;
     55
     56     int writeByte = ::write(clntSock, recvBuffer, recvByte);
     57     if(writeByte < 0) {
     58         std::cerr << "write() error" << std::endl;
     59         ::close(clntSock);
     60         ::close(listenSock);
     61         return -1;
     62     }
     63
     64     ::close(clntSock);
     65     ::close(listenSock);
     66
     67     return 0;
     68 }

 위의 코드를 정상적으로 작성 했다면 이제 컴파일을 하도록 하도록 하자.

$ g++ example1.cpp

 아무런 메시지 없이 프롬프트가 뜬다면 정상적으로 컴파일이 이루어 졌다는 뜻이다(보통 리눅스는 무슨 잘못이 있기 전 까지는 아무런 이야기를 해 주지 않는 성향이 있다). 이제 실행을 시켜 보자.

$ a.out 19999

 이제 19999번 포트로 서버가 실행 되고 있다. 이제 클라이언트를 실행 시켜 보도록 하자. 정석대로라면 클라이언트 프로그램을 작성함이 옳지만 지금은 당장 실행 시켜 보고 싶으므로(그리고 내가 클라이언트 프로그램을 작성하기 귀찮으므로) 리눅스에서 제공하는 telnet을 이용하기로 한다. 자, 그럼 다른 터미널을 하나 더 띄워 보도록 하자. 같은 서버로 접속했다고 가정하고 터미널에서 아래와 같이 커맨드를 넣어보자(만일 다른 서버에서 telnet을 실행 시켰다면 localhost가 아니라 서버가 떠있는 주소가 되어야 한다).

$ telnet localhost 19999
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.


 위와 같이 telnet을 실행 시키고 나면 무엇인가 커맨드를 입력 받기 위해서 대기 상태로 들어간다. 아무 것이나 타이핑해 보자. 필자는 오랜 전통과 역사를 가진 "Hello, world"를 쳐보도록 하겠다.

$ telnet localhost 19999
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Hello, World!!
Hello, World!!
Connection closed by foreign host.
$

 클라이언트 telnet 측에서 Hello, World!! 를 타이핑하자 같은 글자가 한번 더 출력 되고 종료 되었다. 이번에는 아까 띄워놨던 서버를 살펴 보자

$ ./a.out 19999
receive : Hello, World!!
$

 성공적으로 클라이언트 telnet이 보낸 메시지가 수신되었음을 확인 할 수 있다.
 이것으로 간단한 에코 서버의 제작이 완료 되었다(하지만 아직 어디가서 자랑하지는 마시길...)
 
/**
 이것으로 처음을 마치도록 하겠다. 다음번 문서에는 소스코드를 한줄씩 분석하면서 서버 프로세스가 시작하면서 종료 할때 까지의 오퍼레이션에 대해서 알아보고 각각의 오퍼레이션에서의 예외 상황들과 그에 대한 해결 방안 들을 살펴 보도록 하겠다.
*/
유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!