모든 내용은 [윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어] 를 기반으로 제 나름대로 이해하여 정리한 것입니다. 다소 부정확한 내용이 있을수도 있으니 이를 유념하고 봐주세요!
01. select 함수를 기반으로 서버를 구현할 때 코드상에서 확인할 수 있는 단점 두 가지는 무엇인가?
# select 기반의 IO 멀티플렉싱이 느린 이유
1) select 함수호출 이후에 항상 등장하는, "모든 파일 디스크립터를 대상"으로 하는 반복문
2) select 함수를 호출할때마다 인자로 매번 전달해야 하는 관찰대상에 대한 정보들
=> 매번 운영체제에게 이 정보를 전달해야 함.
(1보다 2가 성능에 치명적인 약점이 될 수 있음, 코드의 개선을 통해서 덜 수 있는 유형의 부담이 아니기 때문)
02. select 방식이나 epoll 방식이나, 관찰의 대상이 되는 파일 디스크립터의 정보를 함수호출을 통해서 운영체제에게 전달해야 한다. 그렇다면 이들 정보를 운영체제에게 전달하는 이유는 어디에 있는가?
# select 함수와 epoll 함수는 파일 디스크립터(즉, 소켓)의 변화를 관찰하는 함수인데,
소켓은 운영체제에 의해 관리되는 대상이므로, 두 함수 모두 절대적으로 운영체제에 의해 기능이 완성되는 함수임.
03. select 방식과 epoll 방식의 가장 큰 차이점은 관찰의 대상이 되는 파일 디스크립터를 운영체제에게 전달하는 방식에 있다. 어떻게 차이가 나는지, 그리고 그러한 차이를 보이는 이유는 무엇인지 설명해보자.
# epoll의 장점
- 장점(select 함수의 단점과 반대)
1) 상태변화의 확인을 위한, 전체 파일 디스크립터를 대상으로 하는 반복문이 필요없음
2) select 함수에 대응하는 epoll_wait 함수 호출시, 관찰대상의 정보를 매번 전달할 필요가 없다.
=> epoll 방식은 select 방식과 다르게 리눅스 커널에서 관찰대상의 정보를 "기억"하고 있다.
04. select 방식을 개선시킨 것이 epoll 방식이긴 하지만 select 방식도 나름의 장점이 있다.
어떠한 상황에서 select 방식을 선택하는 것이 보다 현명한 선택이 될 수 있는가?
# select 함수의 장점도 있긴 있다!
- 개선된 IO 멀티플렉싱 모델은 운영체제별로 호환이 되지 않음.
- 반면, select 함수는 대부분의 운영체제에서 지원함.
- 다음 두가지 유형의 조건이 만족 또는 요구된다면,
리눅스라고 할지라도 epoll을 사용할 필요가 없음
1) 서버의 접속자 수가 많지 않다.
2) 다양한 운영체제에서 운영이 가능해야 한다.
05. epoll은 레벨 트리거 방식, 또는 엣지 트리거 방식으로 동작한다.
그렇다면 이 둘이 어떻게 차이가 나는지 이벤트의 발생시점을 입력버퍼 기준으로 설명해보자.
# 레벨 트리거와 엣지 트리거 : 차이점은 "이벤트가 발생하는 시점"
1) 레벨 트리거 : 입력버퍼에 데이터가 남아있는 동안에 계속해서 이벤트가 등록됨.
2) 엣지 트리거 : 입력버퍼에 데이터가 수신된 상황에 딱 한번만 이벤트가 등록됨.
- "이벤트가 등록되었다"는 의미는 운영체제가 변화가 발생한 디스크립터로 등록했다는 의미.
즉, 해당 이벤트를 struct epoll_event 배열에 추가함.
06. 엣지 트리거 방식을 사용하면 데이터의 수신과 데이터의 처리시점을 분리할 수 있다고 하였다.
그 이유는 무엇이고, 이는 어떠한 장점이 있는가?
- 엣지 트리거 방식은 입력버퍼에 데이터가 수신될 때 딱 한번만 이벤트가 발생하고,
입력버퍼에 데이터가 남아있는 동안에는 이벤트가 발생하지 않는다.
=> 따라서, 데이터가 수신된 다음에 원하는 시점에 가서 데이터를 처리할 수 있다.
서버가 데이터의 수신과 처리의 시점을 결정할 수 있으므로 서버 구현에 엄청난 유연성을 제공한다.
07. 서버에 접속한 모든 클라이언트들 사이에서 메시지를 주고받는 형태의 채팅 서버를 레벨 트리거 방식의 epoll 기반으로, 엣지 트리거 방식의 epoll 기반으로 각각 구현해보자. 물론 서버의 실행을 위해서는 채팅 클라이언트가 필요한데, 이는 Chapter 18에서 소개하는 예제 chat_clnt.c를 활용하자.
# chat_clnt.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>
#define BUF_SIZE 100
#define NAME_SIZE 20
void* send_msg(void* arg);
void* recv_msg(void* arg);
void error_handling(char* message);
char name[NAME_SIZE]="[DEFAULT]";
char msg[BUF_SIZE];
int main(int argc, char * argv[])
{
int sock;
struct sockaddr_in serv_addr;
pthread_t snd_thread, rcv_thread;
void* thred_return;
if(argc!=4) // 실행파일 경로/IP/PORT번호/채팅닉네임 을 입력으로 받아야 함
{
printf("Usage : %s <IP> <port> <name> \n",argv[0]);
exit(EXIT_FAILURE);
}
sprintf(name,"[%s]",argv[3]);
sock=socket(PF_INET,SOCK_STREAM,0); // TCP 소켓 생성
/* 서버 주소정보 초기화 */
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
serv_addr.sin_port=htons(atoi(argv[2]));
/* 서버 주소정보를 기반으로 연결요청
이때 비로소 클라이언트 소켓이 됨 */
connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
pthread_create(&snd_thread,NULL,send_msg,(void*)&sock); // 쓰레드 생성 및 실행
pthread_create(&rcv_thread,NULL,recv_msg,(void*)&sock); // 쓰레드 생성 및 실행
pthread_join(snd_thread,&thred_return); // 쓰레드 종료까지 대기
pthread_join(rcv_thread,&thred_return); // 쓰레드 종료까지 대기
close(sock); // 클라이언트 소켓 연결종료
return 0;
}
void* send_msg(void* arg) // send thread main
{
int sock=*((int*)arg); // 클라이언트의 파일 디스크립터
char name_msg[NAME_SIZE+BUF_SIZE];
while(true)
{
fgets(msg,BUF_SIZE,stdin);
if(!strcmp(msg,"q\n")||!strcmp(msg,"Q\n"))
{
close(sock); // 클라이언트 소켓 연결종료 후
exit(EXIT_SUCCESS); // 프로그램 종료
}
sprintf(name_msg,"%s %s",name,msg); // client 이름과 msg를 합침
write(sock, name_msg, strlen(name_msg)); // 널문자 제외하고 서버로 문자열을 보냄
}
return NULL;
}
void* recv_msg(void* arg) // read thread main
{
int sock=*((int*)arg); // 클라이언트의 파일 디스크립터
char name_msg[NAME_SIZE+BUF_SIZE];
int str_len;
while(true)
{
str_len=read(sock,name_msg,NAME_SIZE+BUF_SIZE-1);
if(str_len==-1) // read 실패시
return (void*)-1;
name_msg[str_len]='\0';
fputs(name_msg,stdout);
}
return NULL;
}
void error_handling(char* message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(EXIT_FAILURE); // -1
}
# chat_EPLTserv.c - 레벨 트리거 기반
#define _XOPEN_SOURCE 700
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#define BUF_SIZE 4
#define MAX_CLNT 256
#define EPOLL_SIZE 50
int clnt_cnt=0; // 서버에 접속한 클라이언트 수
int clnt_socks[MAX_CLNT]; // 클라이언트와의 송수신을 위해 생성한 소켓의 파일 디스크립터를 저장한 배열
void send_msg(char *buf, int len);
void error_handling(char *message);
int main(int argc, char * argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
socklen_t adr_sz;
int str_len, i,j;
char buf[BUF_SIZE];
struct epoll_event *ep_events;
struct epoll_event event;
int epfd, event_cnt;
if(argc!=2) // 실행파일 경로/PORT번호 를 입력으로 받아야 함
{
printf("Usage : %s <port> \n", argv[0]);
exit(EXIT_FAILURE);
}
serv_sock=socket(PF_INET,SOCK_STREAM,0); // TCP 소켓 생성
/* 서버 주소정보 초기화 */
memset(&serv_adr,0,sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_adr.sin_port=htons(atoi(argv[1]));
/* 서버 주소정보를 토대로 주소할당 */
if(bind(serv_sock,(struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
error_handling("bind() error");
/* 클라이언트로부터 연결요청을 수락할 준비 완료(진정한 서버소켓이 됨) */
if(listen(serv_sock,5)==-1)
error_handling("listen() error");
epfd=epoll_create(EPOLL_SIZE); // epoll 인스턴스 생성(관심 대상 파일 디스크립터 저장소)
ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
/*
epoll 인스턴스에 있는 파일 디스크립터 중
실제로 이벤트가 발생한 파일 디스크립터를 따로 모아놓는 동적배열
최대 EPOLL_SIZE 만큼 이벤트가 발생할 수 있음
*/
event.events=EPOLLIN; // 수신한 데이터가 있는 이벤트
event.data.fd=serv_sock; // 서버소켓이 대상
epoll_ctl(epfd,EPOLL_CTL_ADD,serv_sock,&event); // epoll 인스턴스에 이벤트 등록
while(true)
{
event_cnt=epoll_wait(epfd,ep_events,EPOLL_SIZE,-1);
/*
epoll 인스턴스에 있는 관심대상에서
이벤트가 발생할때까지 무한대기
*/
if(event_cnt==-1)
{
puts("epoll_wait() error");
break;
}
puts("return epoll_wait");
for(i=0;i<event_cnt;++i) // 이벤트 발생한 파일 디스크립터에 대해서만 반복문(select 기반과 다른 점)
{
if(ep_events[i].data.fd==serv_sock)
/*
클라이언트의 연결요청도 데이터 전송을 통해 이루어지므로
서버소켓에 수신된 데이터가 존재한다는 것은 클라이언트의 연결요청이 있었다는 의미
*/
{
adr_sz=sizeof(clnt_adr);
clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_adr,&adr_sz); // 클라이언트의 연결요청을 수락
/* 클라이언트와의 송수신을 위해 새로 생성된 소켓에 대해 이벤트 등록*/
event.events=EPOLLIN;
event.data.fd=clnt_sock;
epoll_ctl(epfd,EPOLL_CTL_ADD,clnt_sock,&event);
clnt_socks[clnt_cnt++]=clnt_sock; // 현재 접속중인 클라이언트의 파일 디스크립터 저장
printf("connected client: %d \n",clnt_sock);
}
else // 클라이언트의 메시지를 실제로 수신하는 소켓에 대해(accept 함수호출로 생성된 소켓)
{
str_len=read(ep_events[i].data.fd,buf,BUF_SIZE); // 클라이언트로부터 데이터 수신
if(str_len==0) // close request!(EOF 수신)
{
epoll_ctl(epfd,EPOLL_CTL_DEL,ep_events[i].data.fd,NULL);
// 클라이언트가 종료했으므로 이 소켓 또한 관심대상에서 제외하고
close(ep_events[i].data.fd); // 이 소켓의 연결도 종료
printf("close client: %d \n",ep_events[i].data.fd);
for(j=0;j<clnt_cnt;++j)
{
if(clnt_socks[j]==ep_events[i].data.fd)
{
while(j<clnt_cnt-1)
{
clnt_socks[j]=clnt_socks[j+1];
++j;
}
break;
}
}
--clnt_cnt;
}
else
send_msg(buf, str_len); // 현재 접속중인 모든 클라이언트에게 문자열 전달
}
}
}
close(serv_sock); // 서버소켓 소멸
close(epfd); // epoll 인스턴스 소멸
return 0;
}
void send_msg(char* msg, int len) // send to all
{
int i;
for(i=0;i<clnt_cnt;++i) // 현재 연결된 모든 클라이언트에게 메세지를 전송
write(clnt_socks[i],msg,len);
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(EXIT_FAILURE); // -1
}
# chat_EPETserv.c - 엣지 트리거 기반
#define _XOPEN_SOURCE 700
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#define BUF_SIZE 4
#define MAX_CLNT 256
#define EPOLL_SIZE 50
int clnt_cnt=0; // 서버에 접속한 클라이언트 수
int clnt_socks[MAX_CLNT]; // 클라이언트와의 송수신을 위해 생성한 소켓의 파일 디스크립터를 저장한 배열
void send_msg(char *buf, int len);
void setnonblockingmode(int fd);
void error_handling(char *message);
int main(int argc, char * argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
socklen_t adr_sz;
int str_len, i,j;
char buf[BUF_SIZE];
struct epoll_event *ep_events;
struct epoll_event event;
int epfd, event_cnt;
if(argc!=2) // 실행파일 경로/PORT번호 를 입력으로 받아야 함
{
printf("Usage : %s <port> \n", argv[0]);
exit(EXIT_FAILURE);
}
serv_sock=socket(PF_INET,SOCK_STREAM,0); // TCP 소켓 생성
/* 서버 주소정보 초기화 */
memset(&serv_adr,0,sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_adr.sin_port=htons(atoi(argv[1]));
/* 서버 주소정보를 토대로 주소할당 */
if(bind(serv_sock,(struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
error_handling("bind() error");
/* 클라이언트로부터 연결요청을 수락할 준비 완료(진정한 서버소켓이 됨) */
if(listen(serv_sock,5)==-1)
error_handling("listen() error");
epfd=epoll_create(EPOLL_SIZE); // epoll 인스턴스 생성(관심 대상 파일 디스크립터 저장소)
ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
/*
epoll 인스턴스에 있는 파일 디스크립터 중
실제로 이벤트가 발생한 파일 디스크립터를 따로 모아놓는 동적배열
최대 EPOLL_SIZE 만큼 이벤트가 발생할 수 있음
*/
setnonblockingmode(serv_sock);
event.events=EPOLLIN; // 수신한 데이터가 있는 이벤트
event.data.fd=serv_sock; // 서버소켓이 대상
epoll_ctl(epfd,EPOLL_CTL_ADD,serv_sock,&event); // epoll 인스턴스에 이벤트 등록
while(true)
{
event_cnt=epoll_wait(epfd,ep_events,EPOLL_SIZE,-1);
/*
epoll 인스턴스에 있는 관심대상에서
이벤트가 발생할때까지 무한대기
*/
if(event_cnt==-1)
{
puts("epoll_wait() error");
break;
}
puts("return epoll_wait");
for(i=0;i<event_cnt;++i) // 이벤트 발생한 파일 디스크립터에 대해서만 반복문(select 기반과 다른 점)
{
/*
클라이언트의 연결요청도 데이터 전송을 통해 이루어지므로
서버소켓에 수신된 데이터가 존재한다는 것은 클라이언트의 연결요청이 있었다는 의미
*/
if(ep_events[i].data.fd==serv_sock)
{
adr_sz=sizeof(clnt_adr);
clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_adr,&adr_sz); // 클라이언트의 연결요청을 수락
setnonblockingmode(clnt_sock);
/* 클라이언트와의 송수신을 위해 새로 생성된 소켓에 대해 이벤트 등록*/
event.events=EPOLLIN|EPOLLET; // 이벤트 등록방식을 엣지 트리거 방식으로
event.data.fd=clnt_sock;
epoll_ctl(epfd,EPOLL_CTL_ADD,clnt_sock,&event);
clnt_socks[clnt_cnt++]=clnt_sock; // 현재 접속중인 클라이언트의 파일 디스크립터 저장
printf("connected client: %d \n",clnt_sock);
}
else // 클라이언트의 메시지를 실제로 수신하는 소켓에 대해(accept 함수호출로 생성된 소켓)
{
while(true) // 이벤트 발생시 입력버퍼에 모든 데이터를 수신하기 위함임
{
str_len=read(ep_events[i].data.fd,buf,BUF_SIZE); // 클라이언트로부터 데이터 수신
if(str_len==0) // close request!(EOF 수신)
{
epoll_ctl(epfd,EPOLL_CTL_DEL,ep_events[i].data.fd,NULL);
// 클라이언트가 종료했으므로 이 소켓 또한 관심대상에서 제외하고
close(ep_events[i].data.fd); // 이 소켓의 연결도 종료
printf("close client: %d \n",ep_events[i].data.fd);
for(j=0;j<clnt_cnt;++j)
{
if(clnt_socks[j]==ep_events[i].data.fd)
{
while(j<clnt_cnt-1)
{
clnt_socks[j]=clnt_socks[j+1];
++j;
}
break;
}
}
--clnt_cnt;
}
else if(str_len<0)
{
if(errno==EAGAIN) // 다 읽어들여 입력버퍼가 빈 경우
break;
if(errno==EBADF) // 이미 닫힌 소켓으로 송수신이나 파일 조작을 시도한 경우
break;
}
else
send_msg(buf, str_len); // 현재 접속중인 모든 클라이언트에게 문자열 전달
}
}
}
}
close(serv_sock); // 서버소켓 소멸
close(epfd); // epoll 인스턴스 소멸
return 0;
}
void setnonblockingmode(int fd)
{
int flag=fcntl(fd,F_GETFL,0);
fcntl(fd,F_SETFL,flag|O_NONBLOCK);
}
void send_msg(char* msg, int len) // send to all
{
int i;
for(i=0;i<clnt_cnt;++i) // 현재 연결된 모든 클라이언트에게 메세지를 전송
write(clnt_socks[i],msg,len);
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(EXIT_FAILURE); // -1
}
[출처] : 윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어
'Programming > 열혈 TCP, IP 소켓 프로그래밍(저자 윤성우)' 카테고리의 다른 글
Ch 18. 내용 확인문제 (0) | 2020.08.10 |
---|---|
Ch 18. 멀티쓰레드 기반의 서버구현 (0) | 2020.08.10 |
Ch 17. select보다 나은 epoll (0) | 2020.08.10 |
Ch 16. 내용 확인문제 (0) | 2020.08.10 |
Ch 16. 입출력 스트림의 분리에 대한 나머지 이야기 (0) | 2020.08.10 |