IT_Programming/Network Programming

[펌] Linux 강좌 #3 : epoll, worker thread를 이용한 chatting server

JJun ™ 2012. 6. 8. 17:36

-----------------------------------------------------------------------

출처: http://chonga.pe.kr/blog/index.php?pl=972


아주 좋은 강좌가 있어서 출처 밝히고 옮겨봅니다. ^^

-----------------------------------------------------------------------


요즘 초보자를 위한 시리즈로 몇 가지 리눅스 강좌를 하면서 학생들에게 숙제를 내긴 했는데, 해답이 없으면 뭐해서 직접 간단히 해답을 만들어 보았다. 숙제는 linux의 최근에 안정화된 것으로 보이는 network i/o model인 epoll을 이용해서 edge triggered model의 oneshot flag를 설정하고 패킷의 읽기 처리를 다른 worker thread에서 하는 채팅서버를 구현하라는 것이었다. 또한 반드시 thread cancellation을 이용해서 쓰레드를 종료하도록 숙제를 내었는데, 이는 pthread의 condition 관련 함수에서는 cancellation 영역을 제대로 처리해주지 못할 수도 있어서 스스로 해결책을 찾아볼 수 있도록 준비했다나 뭐라나.. 2001년 6월의 Linux High Performance Server 기사와 2003년 4월에 event poll, rtsignal로 간단히 테스트해보고 작성했던 기사 Linux Network Engine for online Game 이후에 2007년 들어서 리눅스 강좌와 더불어 실로 epoll도 오랜만의 포스팅인 듯 하다.



- 작성자: 어린미르 (orinmir _at_ gmail.com)
- 문서 위치: http://chonga.pe.kr/blog/index.php?pl=972
- 작성일: 20070502
- 문서 변경 이력 : 
20070502 초판 릴리즈
20070507 수정판 릴리즈
20070510 worker thread의 종료에 대한 설명 추가
20070518 example code에 null 처리 안한 부분 추가
20071129 accept, waitpop관련 버그 수정, 소스 갱신 업데이트

선언: 본 문서는 저자의 개인적인 의견이 있을 수 있습니다. 이 곳에 게재된 내용은 상업적 판단을 위한 자료로 이용될 수 없으며 그로 인한 책임은 저자에게 없습니다.

----
1 개요

2 채팅 서버의 설명
2.1 기본 파일 설명
2.2 호출 관계 그래프
2.3 main(chatserver.cpp) 함수에서 하는 일
2.4 cchatmgr 클래스(chatmgr.cpp)에 대해
2.5 userlist와 user정보
2.6 worker thread와 workerjobqueue에 대해
2.7 network 관련 유틸리티에 대해

3 프로그램의 실행
3.1 서버 실행
3.2 첫번째 클라이언트 실행 (소켓 번호 5번으로 접속됨)
3.3 두번째 클라이언트 실행 (소켓 번호 6번으로 접속됨)
3.4 서버 종료
----

1 개요
Linux Programming에서도 새로운 network io model인 epoll의 edge-triggered의 one-shot 모델을 기반으로 소켓 읽기 부분을 worker thread에서 처리할 수 있도록 채팅서버의 예제를 구성해보고 간단한 설명을 하려고 한다. 성능이나 설계면에서 부족한 부분이나 stl의 자료 구조를 다양하게 이용해본 측면도 있으므로 그 부분은 감안하고 예제를 참고해주기를 바란다. 

이 예제는 centos 4.4의 기본 환경에서 테스트 되었다.

2 채팅 서버의 설명
2.1 기본 파일 설명
◆ 소스 다운 로드 : linux-chatting-server-example-by-leehongki-20071129.zip
chatdefine.h     : 각종 정의
chatmgr.cpp     : 채팅 서버의 메인 모듈
chatmgr.h       
chatserver.cpp   : 채팅 서버의 시작점
makefile         : 빌드 스크립트
network.cpp     : 소켓 관련 유틸리티
network.h
thread.cpp       : 쓰레드 풀, 뮤텍스, 워커쓰레드용 큐
thread.h
user.cpp        : 사용자 객체의 정의, 사용자목록관리자
user.h


2.2 호출 관계 그래프
main함수로부터 어떻게 호출되는 관계인지 전체 그래프를 아래와 같이 볼 수 있다. 아래 그림을 클릭하면 확대된 그림을 확인할 수 있다.

chatserver의 호출관계 그래프


2.3 main(chatserver.cpp) 함수에서 하는 일
한마디로 간단하게 설명하면 서버를 기동하고 정지하는 기능이 구현되어 있다. 즉 구체적으로는 다음과 같은 일을 한다.

   - 첫번째 인수로부터 서버 port 받아오기 
   - cchatmgr 객체의 인스턴스 생성
   - cchatmgr 인스턴스에 port 설정
   - cchatmgr->start() 로 서버 기동
   - 표준입력(stdin)으로부터 enter key의 입력를 기다림
   - enter key의 입력이 있을 때까지 서버 기동 상태


enter key를 누르면 정상적으로 셧다운을 시키기 위해서 쓰레드를 종료시키고 자원을 해제하고 난 후에 exit가 호출되고 종료된다.

2.4 cchatmgr 클래스(chatmgr.cpp)에 대해
main함수로부터 클래스가 생성되면, stl의 list로 작성된 userlist와 threadpool 등이 초기화된다. start() 함수가 호출되면서부터 크게 2가지 종류의 쓰레드가 초기화된다. 첫번째 종류의 쓰레드는 accept socket을 생성하여 사용자로부터 소켓 접속을 받을 수 있도록 준비하고 epoll 관련 함수들이 초기화된다. 마지막으로는 epoll의 epoll_wait의 루프문으로 무한루프를 수행하게 된다. 이 루프에서 하는 일은 accept socket을 감시하여 새로운 사용자를 추가하거나 사용자로부터의 입력이 있을 경우에 입력 이벤트를 workerjobqueue에 넣어주는 일을 하게된다. 두번째 종류의 쓰레드는 worker thread로 기본값으로 3개의 쓰레드가 기동되어 workerjobqueue에 들어온 이벤트가 있을 때마다 깨어나 패킷을 읽고 채팅 메시지를 파싱하여 주변 클라이언트에게 전송해주는 일을 해준다.

2.5 userlist와 user정보
cmutex class를 상속받은 cuserlist 클래스에는 cuser를 원소로 갖는 list가 존재한다. 이 리스트는 cchatmgr의 인스턴스의 생성과 함께 초기화된다. 이후에 사용자가 새로 접속하면 cuser가 하나 생성되고 소켓이 끊기면 메모리에서 삭제된다. 

cuser class 의 역할은 말그대로 사용자 정보를 담고 있는데 client의 socket id 등의 정보와 함께 socket buffer를 stl vector로 구현한 원형큐로서 enqueue와 dequeue 관련 함수들이 마련되어 있다. 여기서는 dequeue를 하는 경우에 간단한 채팅 패킷을 파싱해주기도 한다. 파싱을 하기위해서 구분자를 '\n'을 ENDMARK로 인식하게 하는데, cuser의 settextprotocol(true)로 초기화해주면 파싱 기능을 이용할 수 있도록 하였다. 이 클래스를 좀더 확장하여 사용자의 이름 등의 정보를 담을 수도 있다. 설계의 문제겠지만 때에 따라서는 버퍼 및 프로토콜의 처리 부분은 이 클래스에서 분리하는 방법도 생각할 수 있다. cuserlist와 마찬가지로 cmutex로부터 상속받아 자원의 보호를 수행한다.

2.6 worker thread와 workerjobqueue에 대해
worker thread도 다른 객체들과 마찬가지로 ccharmgr가 생성될 때 함께 초기화된다. 다만, cchatmgr의 인스턴스의 start()가 호출되면서 실제 worker thread의 생성이 이루어진다. setthreadfuncptr을 통해서 workerthread의 쓰레드 함수를 지정할 수 있는데, 여기서는 epoll_wait에서 감지된 클라이언트의 소켓 읽기 이벤트를 workerjobqueue로부터 가져와서 ccharmgr의 handlefd() 함수에서 실제로 채팅 메시지 읽어오기와 주변 클라이언트로 패킷을 전송하는 기능을 수행하게 된다. workerjobqueue는 이벤트 감지시에 여러 쓰레드 중에 단 1개의 쓰레드만 깨어나서 수행하게 된다. 현재 worker thread의 개수는 3개로 잡아두었다.   

workerjobqueue는 stl의 queue로 FIFO(First In First Out) 구조로 수행된다. 내부적으로 mutex와 condition을 갖고 있어, epoll_wait가 수행되는 쓰레드에서는 queue에 push를 수행하면서 동시에 signal을 발생시킨다. worker thread의 쓰레드 루프에서는 condition wait를 하고 있는데, signal을 받으면 대기중인 쓰레드 중에 단 하나만이 깨어나서 처리한다. 대기하고 있는 모든 worker thread가 깨어날 수 있도록 broadcast() 함수도 지원한다. 이는 프로그램 종료시에 사용하고 있다.

worker thread에서 종료시에 pthread의 cancellation을 사용하는 곳에 문제가 있는데, pthread_cond_* 함수의 경우에 여러 쓰레드가 대기하고 있는 경우에 cancellation 시그널을 단 하나만 받게되는 것 같다. 이에 대한 해결책은 여러가지가 있겠지만, 여기서는 broadcast를 해서 모든 쓰레드를 깨우고, 그 때에 리턴한 값에 서버 종료에 대한 값을 넣어서 처리했다.

condition관련 기능을 사용할 때에, 주의할 점은 pthread_cond_signal로 발송한 시그널을 보장할 수 없는 경우가 생길 수 있다는 점인데, pthread_cond_wait의 쓰레드에서 완벽하게 wait 상태로 진입하지 않은 상태에서는 skip될 수가 있다. 이 부분에 대한 처리도 고려해야한다.

2.7 network 관련 유틸리티에 대해
network.cpp를 보게되면 epoll 및 각종 소켓 서버를 위한 유틸리티들이 존재하고 있다. 구체적으로는 다음과 같다. NON BLOCKING SOCKET 설정, TCPNODELAY설정, REUSEADDR설정을 위한 함수와 epoll관련한 Wrapper 함수들이다. cchatmgr 클래스에서 적절히 사용하고 있다.

int setnonblock (int sock);
int settcpnodelay (int sock);
int setreuseaddr (int sock);

int init_epoll (int eventsize);
int init_acceptsock (unsigned short port);
int do_accept (int efd, int sfd);
int add_epollfd (int efd, int cfd, bool useoneshot = true);
int modify_epollfd (int efd, int cfd, bool useoneshot = true);
int del_epollfd (int efd, int cfd);


socket 프로그래밍을 할 때에 또 한가지 빠뜨려서는 안될 것이 있는데 SIG_PIPE의 signal을 무시해야한다. 보통 소켓 프로그램에서는 SIG_PIPE가 종종 비주기적으로 발생하는데, SIG_PIPE를 받으면 일반적으로 프로그램이 종료되어 버린다. 소켓 서버를 처음 만드는 사람을 당황하게 만든다. gdb와 같은 디버거로 실행시켜보면, SIG_PIPE로 인해 프로그램이 종료되는 것을 확인해볼 수 있다. 때문에 이를 방지하기 위해서 cchatmgr의 epoll 관련 초기화 함수에  signal (SIGPIPE, SIG_IGN)가 설정되어 있다. 

마지막으로, 이 예제에서 비동기 소켓을 사용하기 때문에 recv의 경우에는 WOULD_BLOCK을 처리해주도록 했는데, send 할 때에도 가급적 송신용 버퍼를 마련해두고 마찬가지로 nonblocking에서의 처리를 해줘야한다. 우선은 채팅 예제이기 때문에 빠져있는데 상용 어플리케이션에서는 주의해야한다. epoll의 경우 EPOLL_CTL_ADD시에 EPOLLIN|EPOLLOUT을 양쪽다 지정해주고 EPOLLOUT 이벤트가 발생하면 송신 버퍼에서 꺼내어 전송해주는 부분은 숙제로 남겨두기로 한다. 

3 프로그램의 실행
3.1 서버 실행
makefile을 이용해 make 명령으로 빌드를 하면 chatserver 라는 이름의 바이너리가 생성된다. 첫번째 인자를 server port로 인식한다. 다음과 같이 press 'enter' key to shutdown가 나오면 서버가 정상적으로 수행된 것이다. enter key를 누르면 서버는 자동으로 종료된다.

[orinmir@chonga chatserver]$ ./chatserver 4444
CHATSERVER 0.1 - test example (c)orinmir / build date: May 2 2007
! listening port : 4444
! server started
! press 'enter' key to shutdown.
! client closed - end of file detected - fd 6


3.2 첫번째 클라이언트 실행 (소켓 번호 5번으로 접속됨)
클라이언트는 일반 telnet 프로그램을 이용하도록 했다. 아래는 첫번째 클라이언트를 띄웠다. localhost로 접속해서 socket id 5번이 되었다. hello i am hongki 로부터 시작했다. 2번째로 접속한 클라이언트가 접속을 해제했는데, "6 is connection closed"라는 공지 메시지가 발송된다.

[orinmir@chonga homework]$ telnet localhost 4444
Trying 127.0.0.1...
Connected to localhost.localdomain (127.0.0.1).
Escape character is '^]'.
hello i am hongki
[   5]:hello i am hongki
[   6]:hi! this is orinmir
[   6]:i am gonna go out now. see ya
[notice]: 6 is connection closed


3.3 두번째 클라이언트 실행 (소켓 번호 6번으로 접속됨)
[orinmir@chonga homework]$ telnet localhost 4444
Trying 127.0.0.1...
Connected to localhost.localdomain (127.0.0.1).
Escape character is '^]'.
hi! this is orinmir
[   6]:hi! this is orinmir
i am gonna go out now. see ya
[   6]:i am gonna go out now. see ya
^]
telnet> close
Connection closed.


3.4 서버 종료

CHATSERVER 0.1 - test example (c)orinmir / build date: May 2 2007
! listening port : 4444
! server started
! press 'enter' key to shutdown.

! stop application.
! cthreadpool::stop() - picked up thread
! cthreadpool::stop() - picked up thread
! cthreadpool::stop() - picked up thread
! cchatmgr::stopnetwork - picked up thread


4. 참고 자료
- linux epoll server example : http://chonga.pe.kr/blog/index.php?pl=919
epoll, poll, select 성능 분석

- 구글검색, 맨페이지, beginning linux programming


linux-chatting-server-example-by-leehongki-20071129.zip




linux-chatting-server-example-by-leehongki-20071129.zip
0.01MB