Echo Server 구현
🙊

Echo Server 구현

Created
Jan 22, 2024 01:41 AM
Last edited time
Last updated January 26, 2024
Tags
CS
Language
URL

Intro::

에코 서버 구현전에 참고하면 좋습니다!
 

socket()

#include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol);
해당 소켓이 가리키는 소켓 디스크립터를 반환한다.
  • int domain: AF_UNIX(프로토콜 내부에서), AF_INET(IPv4), AF_INET6(IPv6)
  • int type: SOCK_STREAM(TCP), SOCK_DGRAM(UDP), SOCK_RAW(사용자 정의)
  • int protocol: 0, IPPROTO_TCP(TCP 일때), IPPROTO_UDP(UDP 일때)
 

bind()

int bind( SOCKET sockfd, const sockaddr *my_addr, int namelen );
소켓에 IP와 포트를 할당한다.
 
  • SOCKET sockfd: socket()으로 구한 소켓 파일 디스크립터
  • sockaddr *my_addr: IP 주소와 port번호를 저장하기 위한 변수가 있는 구조체
  • int namelen: 구조체의 데이터 크기

listen()

int listen(int sockfd, int backlog)
클라이언트로부터 연결 요청을 기다린다.
  • SOCKET sockfd: socket()으로 구한 소켓 파일 디스크립터
  • int backlog: 동시에 연결을 시도하는 최대 클라이언트 수

kqueue()

#include <sys/time.h> #include <sys/event.h> #include <sys/types.h> int kqueue(void);
 
kqueue()는 커널에 새로운 event queue를 만들고, fd를 return한다. return된 fd는 kevent()에서 이벤트를 등록, 모니터링하는데 사용된다. 이 queue는 fork로 자식 프로세스 분기 시 상속되지 않는다.
 
단순하게 이벤트를 감지해주는 시스템 콜이라고 생각하면 된다.

kevent()

 
int kevent(int kq, const struct kevent *changelist, int nchanges, struct kevent *eventlist, int nevents, const struct timespec *timeout);
 
kevent 는 ident와 filter를 식별자로 삼아 kevent의 중복 등록을 막고, 해당 이벤트 발생 조건이 부합하지 않는 경우에는 kqueue에서 삭제되어 최소한의 kevent만 남아있도록 관리된다.
  • int kq: 새로 모니터링 등록할 이벤트
  • const struct kevent *changelist: kqueue에 등록될 kevent 구조체 배열이다.
    • struct kevent { uintptr_t ident; /* event에 대한 식별자로 fd 번호가 입력된다 */ int16_t filter; /* 커널이 event를 핸들링할 때 이용되는 필터 */ uint16_t flags; /* event를 적용시키서나, event가 return 되었을 때 사용되는 flag */ uint32_t fflags; /* filter에 따라 다르게 적용되는 flag */ intptr_t data; /* filter에 따라 다르게 적용되는 data */ void *udata; /* event와 함께 등록하여 event return시 사용할 수 있는 user-data */ };
    • filter 종류
      • EVFILT_READ: ident의 fd를 모니터링하고, 읽을 수 있는 data 가 있다면 event return
      • EVFILT_WRITE: ident의 fd를 모니터링하고, write가 가능하다면 event return
      • EVFILT_VNODE, EVFILT_SIGNAL, ….
    • flags 종류
      • EV_ADD: kqueue에 event 등록
      • EV_ENABLE: kevent()가 return 할 수 있도록 활성화
      • EV_DISABLE: kevent()가 return 못하도록 비활성화
      • EV_DELETE: queue에서 event 삭제
      • etc…
  • int nchanges: changelist 배열의 크기
  • struct kevent *eventlist: 발생한 event가 return될 배열
  • int nevents: eventlist 배열의 크기
  • const struct timespec *timeout: timespec 구조체의 포인터를 전달, NULL을 전달할 셩우 이벤트 발생까지 block(무한정 대기)
 

에코 서버 구현

 
#include <sys/types.h> #include <sys/event.h> #include <sys/time.h> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> #include <fcntl.h> #include <iostream> #include <map> #include <vector> using namespace std; void exit_with_perror(const string& msg) { cerr << msg << endl; exit(EXIT_FAILURE); } void change_events(vector<struct kevent>& change_list, uintptr_t ident, int16_t filter, uint16_t flags, uint32_t fflags, intptr_t data, void* udata) { struct kevent tmp_event; EV_SET(&tmp_event, ident, filter, flags, fflags, data, udata);// kevent 구조체 초기화 매크로 change_list.push_back(tmp_event); } void disconnect_client(int client_fd, map<int, string>& clients) { cout << "client disconnected: " << client_fd << endl; close(client_fd); clients.erase(client_fd); } int main(int argc, char** argv) { if (argc != 2) { exit_with_perror(string(argv[0]) + " [PORT]"); } /* 서버 소켓 초기화 & listen */ int server_socket = socket(PF_INET, SOCK_STREAM, 0);// 서버 소켓 IPv4, TCP, 0 struct sockaddr_in server_addr; if (server_socket == -1) { exit_with_perror("socket() eroror: " + string(strerror(errno))); } memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET;// TCP, UDP 설정 server_addr.sin_addr.s_addr = htonl(INADDR_ANY);// 모든 ip접근 허가 server_addr.sin_port = htons(atoi(argv[1]));// 포트 설정 if (bind(server_socket, reinterpret_cast<struct sockaddr*>(&server_addr), sizeof(server_addr)) == -1 ) { exit_with_perror("bind() eroror: " + string(strerror(errno))); } if (listen(server_socket, 5) == -1) { exit_with_perror("listen() eroror: " + string(strerror(errno))); } fcntl(server_socket, F_SETFL, O_NONBLOCK);// 소켓 플레그를 논블로킹으로 설정 /* kqueue 초기화*/ int kq = kqueue(); if (kq == -1) { exit_with_perror("kq() eroror: " + string(strerror(errno))); } map<int, string> clients;// 클라이언트 소켓 데이터 vector<struct kevent> change_list;// kevent 배열 struct kevent event_list[8];// event list 배열 /* 서버 소켓에 이벤트 추가 */ change_events(change_list, server_socket, EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, NULL); cout << "echo server started" << endl; /* 메인 루프 */ int new_events; struct kevent* cur_event; while (1) { new_events = kevent(kq, &change_list[0], change_list.size(), event_list, 8, NULL);// change_list안의 값들은 kqueue에 등록된다. if (new_events == -1) { exit_with_perror("kevent() eroror: " + string(strerror(errno))); } change_list.clear(); cout << "감지된 이벤트 개수: " << new_events << "\n"; for (int i=0; i<new_events; i++) { cur_event = &event_list[i]; /* 에러 이벤트 체크 */ if (cur_event->flags & EV_ERROR) {// 에러 if (cur_event->ident == server_socket) { exit_with_perror("server socket eroror: " + string(strerror(errno))); } cerr << "client socket error" << endl; disconnect_client(cur_event->ident, clients); } else if (cur_event->filter == EVFILT_READ) {// 읽기 if (cur_event->ident == server_socket) { /* 새로운 클라이언트 승인 */ int client_socket = accept(server_socket, NULL, NULL); if (client_socket == -1) { exit_with_perror("accept() eroror: " + string(strerror(errno))); } cout << "accept new client: " << client_socket << endl; fcntl(client_socket, F_SETFL, O_NONBLOCK); /* 클라 소켓 이벤트 추가 - 쓰기, 읽기 이벤트 추가 */ change_events(change_list, client_socket, EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, NULL); change_events(change_list, client_socket, EVFILT_WRITE, EV_ADD | EV_ENABLE, 0, 0, NULL); clients[client_socket] = ""; } else if (clients.find(cur_event->ident) != clients.end()) { /* 클라이언트 데이터 읽어오기 */ char buf[1024]; int n = read(cur_event->ident, buf, sizeof(buf)); if (n <= 0) { if (n < 0) { cerr << "client read error!" << endl; } disconnect_client(cur_event->ident, clients); } else { buf[n] = '\0'; clients[cur_event->ident] += buf; cout << "received data from " << cur_event->ident << ": " << clients[cur_event->ident] << endl; } } } else if(cur_event->filter == EVFILT_WRITE) { // 쓰기 /* 클라이언트에게 데이터 송신 */ map<int, string>::iterator cur_client = clients.find(cur_event->ident); if (cur_client != clients.end()) { if (clients[cur_event->ident] != "") {// map에 클라이언트 데이터가 있다면 int n = write(cur_event->ident, cur_client->second.c_str(), cur_client->second.size()); if (n == -1) { cerr << "client write error!" << "\n"; disconnect_client(cur_event->ident, clients); } else { cout << "server echoed to:" << cur_event->ident << ", message: " << cur_client->second << endl; cur_client->second.clear(); } } else { cout << cur_event->ident << "에게 보낼 데이터가 없습니다." << endl; } } } } sleep(1); } return 0; }

에코 클라이언트 구현

 
#include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> #include <iostream> #define BUF_SIZE 1024 using namespace std; void exit_with_perror(const string& msg) { cerr << msg << endl; exit(EXIT_FAILURE); } int main(int argc, char** argv) { if (argc != 3) { exit_with_perror(string(argv[0]) + " [IP] [PORT]"); } /* 클라이언트 소켓 초기화 & connect */ int client_socket = socket(PF_INET, SOCK_STREAM, 0);// 클라이언트 소켓 IPv4, TCP, 0 struct sockaddr_in server_addr; if (client_socket == -1) { exit_with_perror("socket() eroror: " + string(strerror(errno))); } memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET;// TCP, UDP 설정 server_addr.sin_addr.s_addr = inet_addr(argv[1]);// IP server_addr.sin_port = htons(atoi(argv[2]));// PORT if (connect(client_socket, (const sockaddr*)&server_addr, sizeof(server_addr)) == -1) { exit_with_perror("connect() eroror: " + string(strerror(errno))); } char message[BUF_SIZE]; int len; while (1) { /* 메세지 송신 */ cout << "전송할 메세지를 입력: " << endl; cin.getline(message, BUF_SIZE); write(client_socket, message, strlen(message)); /* 메세지 수신 */ len = read(client_socket, message, BUF_SIZE - 1); message[len] = '\0'; cout << "수신한 메세지: " << message << endl; } close(client_socket); return 0; }
 

번외 - 웹 브라우저와 통신

위의 에코 서버 코드와 거의 동일하고 클라이언트에게 데이터 송신하는 부분을 다음 코드로 바꿔주면 된다. 아래 코드에서는 서버가 소켓에 메시지를 적어주고 바로 통신을 끊는다.
 
... } else if(cur_event->filter == EVFILT_WRITE) { // 쓰기 /* 클라이언트에게 데이터 송신 */ map<int, string>::iterator cur_client = clients.find(cur_event->ident); if (cur_client != clients.end()) { if (clients[cur_event->ident] != "") { string msg = "Hello, world!!!\nhi Chrome!!!";// 브라우저에 출력하고 싶은 문구 string header = "HTTP/1.1 200 OK\r\nContent-Length: " + to_string(msg.size()) + "\r\n\r\n" + msg; write(cur_event->ident, header.c_str(), header.size()); cout << "server responsed to:" << cur_event->ident << endl; cur_client->second.clear(); disconnect_client(cur_event->ident, clients); } } } ...
 
 

References::

Loading Comments...