本文做爲本身學習網絡編程的總結筆記。打算分析一下主流服務器模式的優缺點,及適用場景,每種模型實現一個回射服務器。客戶端用同一個版本,服務端針對每種模型編寫對應的回射服務器。react
本文全部代碼放在:github.com/oscarwin/mu…linux
單進程迭代服務器是我接觸網絡編程編寫的第一個服務器模型,雖然代碼只有幾行,可是每個套接字編程的函數都涉及到大量的知識,這裏我並不打算介紹每一個套接字函數的功能,只給出一個套接字編程的基礎流程圖。git
有幾點須要解釋的是:github
服務器調用listen函數之後,客戶端與服務端的3次握手是由內核本身完成的,不須要應用程序的干預。內核爲全部的鏈接維護兩個個隊列,隊列的大小之和由listen函數的backlog參數決定。服務端收到客戶算的SYN請求後,會回覆一個SYN+ACK給客戶端,並往未完成隊列中插入一項。因此未完成隊列中的鏈接都是SYN_RCVD狀態的。當服務器收到客戶端的ACK應答後,就將該鏈接從未完成隊列轉移到已完成隊列。編程
當未完成隊列和已完成隊列滿了後,服務器就會直接拒絕鏈接。常見的SYN洪水攻擊,就是經過大量的SYN請求,佔滿了該隊列,致使服務器拒絕其餘正常請求達到攻擊的目的。segmentfault
accept函數會一直阻塞,直到已完成隊列不爲空,而後從已完成隊列中取出一個完成鏈接的套接字。bash
單進程服務器只能同時處理一個鏈接。新創建的鏈接會一直呆在已完成隊列裏,得不處處理。所以,天然想到經過多進程來實現同時處理多個鏈接。爲每個鏈接產生一個進程去處理,稱爲PPC模式,即process per connection。其流程圖以下(圖片來自網絡,侵刪):服務器
這種模式下有幾點須要注意:網絡
這種模式存在的問題:多線程
核心代碼段以下,完整代碼在ppc_server目錄。
while(1)
{
clilen = sizeof(stCliAddr);
if ((iConnectFd = accept(iListenFd, (struct sockaddr*)&stCliAddr, &clilen)) < 0)
{
perror("accept error");
exit(EXIT_FAILURE);
}
// 子進程
if ((childPid = fork()) == 0)
{
close(iListenFd);
// 客戶端主動關閉,發送FIN後,read返回0,結束循環
while((n = read(iConnectFd, buf, BUFSIZE)) > 0)
{
printf("pid: %d recv: %s\n", getpid(), buf);
fflush(stdout);
if (write(iConnectFd, buf, n) < 0)
{
perror("write error");
exit(EXIT_FAILURE);
}
}
printf("child exit, pid: %d\n", getpid());
fflush(stdout);
exit(EXIT_SUCCESS);
}
// 父進程
else
{
close(iConnectFd);
}
}
複製代碼
既然fork進程時的開銷比較大,所以很天然的一種優化方式是,在服務器啓動的時候就預先派生子進程,即prefork。每一個子進程本身進行accept,大概的流程圖以下(圖片來自網絡,侵刪):
相比於pcc模式,prefork在創建鏈接時的開銷小了不少,可是另外兩個問題——鏈接數有限和進程間通訊複雜的問題仍是存在。除此以外,prefork模式還引入了新的問題,當有一個新的鏈接到來時,雖然只有一個進程可以accept成功,可是全部的進程都被喚醒了,這個現象被稱爲驚羣。驚羣致使沒必要要的上下文切換和資源的調度,應該儘可能避免。好在linux2.6版本之後,已經解決了驚羣的問題。對於驚羣的問題,也能夠在應用程序中解決,在accept以前加鎖,accept之後釋放鎖,這樣就能夠保證同一時間只有一個進程阻塞accept,從而避免驚羣問題。進程間加鎖的方式有不少,好比文件鎖,信號量,互斥量等。
無鎖版本的代碼在prefork_server目錄。加鎖版本的代碼在prefork_lock_server目錄,使用的是進程間共享的線程鎖。
線程是一種輕量級的進程(linux實現上派生進程和線程都是調用do_fork函數來實現),線程共享同一個進程的地址空間,所以建立線程時不須要像fork那樣,拷貝父進程的資源,維護獨立的地址空間,所以相比進程而言,多線程模型開銷要小不少。多線程併發服務器模型與多進程併發服務器模型相似。
多線程併發服務器模型,與多進程併發服務器模型相比,開銷小了不少。可是一樣存在鏈接數頗有限這個限制。除此以外,多線程程序還引入了新的問題
和預先派生子進程類似,能夠經過預先派生線程來消除建立線程的開銷。
預先派生線程的代碼在pthread_server目錄。
前面說起的幾種模式都沒能解決的一個問題是——鏈接數有限。而IO多路複用就是用來解決海量鏈接數問題的,也就是所謂的C10K問題。
IO多路複用有三種實現方案,分別是select,poll和epoll,關於三者之間的區別就不在贅述,網絡上已經有不少文章講這個的了,好比這篇文章 Linux IO模式及 select、poll、epoll詳解。
epoll由於其能夠打開的文件描述符不像select那樣受系統的限制,也不像poll那樣須要在內核態和用戶態之間拷貝event,所以性能最高,被普遍使用。
epoll有兩種工做模式,一種是LT(level triggered)模式,一種是ET(edge triggered)模式。LT模式下,假如來了4k的數據,可是程序只讀了前面2k的數據,那麼再次阻塞在epoll_wait上時,x系統還會再次通知該文件可讀。而ET模式下,若是隻讀了2k的數據,而後就退出並從新阻塞在epoll上時,系統不會通知該文件可讀,除非又有新的數據發送過來。所以,ET模式下每次通知可讀時就要把發送過來的數據所有讀完。這個特性使得ET模式下只能採用非阻塞IO,在while循環中讀取這個文件描述符中的數據,直到read或write返回EAGAIN。若是採用阻塞IO,read或write在屢次循環讀完了數據,最後一次的讀寫操做會一直阻塞,致使進程或者線程沒有阻塞在epoll_wait上,IO多路複用就失效了。非阻塞IO配合IO多路複用就是reactor模式。reactor是核反應堆的意思,光是聽這名字我就以爲牛不不要不要的了。
epoll編碼的核心代碼,我直接從man命令裏的說明裏拷貝過來了,咱們的實如今目錄reactor_server裏。
#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd;
/* Set up listening socket, 'listen_sock' (socket(),bind(), listen()) */
// 建立epoll句柄
epollfd = epoll_create(10);
if (epollfd == -1) {
perror("epoll_create");
exit(EXIT_FAILURE);
}
// 將監聽套接字註冊到epoll上
ev.events = EPOLLIN;
ev.data.fd = listen_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
perror("epoll_ctl: listen_sock");
exit(EXIT_FAILURE);
}
for (;;) {
// 阻塞在epoll_wait
nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_pwait");
exit(EXIT_FAILURE);
}
for (n = 0; n < nfds; ++n) {
if (events[n].data.fd == listen_sock) {
conn_sock = accept(listen_sock, (struct sockaddr *) &local, &addrlen);
if (conn_sock == -1) {
perror("accept");
exit(EXIT_FAILURE);
}
// 將鏈接套接字設定爲非阻塞、邊緣觸發,而後註冊到epoll上
setnonblocking(conn_sock);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = conn_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,
&ev) == -1) {
perror("epoll_ctl: conn_sock");
exit(EXIT_FAILURE);
}
} else {
do_use_fd(events[n].data.fd);
}
}
}
複製代碼
而後咱們再分析一下epoll的原理。
epoll_create建立了一個文件描述符,這個文件描述符實際是指向的一個紅黑樹。當用epoll_ctl函數去註冊文件描述符時,就是往紅黑樹中插入一個節點,該節點中存儲了該文件描述符的信息。當某個文件描述符準備好了,回去調用一個回調函數ep_poll_callback將這個文件描述符準備好的信息放到rdlist裏,epoll_wait則阻塞於rdlist直到其中有數據。
proactor模式就是採用異步IO加上IO多路複用的方式。使用異步IO,將讀寫的任務也交給了內核來作,當數據已經準備好了,用戶線程直接就能夠用,而後處理業務邏輯就OK了。
常量鏈接常量請求,如:管理後臺,政府網站,可使用ppc和tpc模式
常量鏈接海量請求,如:中間件,可使用ppc和tpc模式
海量鏈接常量請求,如:門戶網站,ppc和tpc不能知足需求,可使用reactor模式
海量鏈接海量請求,如:電商網站,秒殺業務等,ppc和tpc不能知足需求,可使用reactor模式