1. 併發服務器
服務器同時向多個發起請求的客戶端提供服務,以提高平均滿意度。而不是一個一個處理客戶端的請求。
實現方式:
(1)多進程服務器(適用Linux)
(2)多路複用服務器
(3)多線程服務器
2. 多進程服務器
(1)進程查看命令
$ ps au //命令可以查看當前運行的所有進程
(2)子進程的創建
pid_t pid = fork();
//子進程會複製一份正在運行的父進程。
//然後父進程和子進程會分別運行fork()之後的代碼。
//父進程返回子進程的PID,子進程返回0。
(3)殭屍進程
定義:進程執行完後沒有被銷燬,仍然佔用系統資源。
產生的原因:父進程創建的子進程結束返回後,操作系統不會銷燬子進程,也不會主動把返回值傳遞給父進程。直到父進程主動發起請求(函數調用)時,操作系統纔會傳遞該值。
解決:
- 方法一:wait函數
阻塞式的
pid_t wait(&status); //如果有子進程終止,則返回子進程的返回值給status;否則阻塞,直到有子進程終止。
- 方法二:waitpid函數
while(!waitpid(-1, &status, WNOHANG)){} //如果有子進程終止,返回PID;否則,返回0.
產生的問題:會導致父進程無法執行下去,一直在等待子進程結束。
- 方法三:信號處理
子進程結束時產生SIGCHILD信號。
void read_childproc(int sig)
{
pid_t pid = waitpid(-1, &status, WNOHANG); //調用waitpid函數,判斷子進程是否正常結束
if(WIFEXITED(status))
{
}
}
int main()
{
struct sigaction act;
act.se_handler = read_childproc;//sigaction結構體綁定響應函數read_childproc;
...
sigaction(SIGCILD, &act, 0); //如果有子進程結束,則觸發SIGCHILD信號,read_childproc函數響應
...
pid = fork();
...
}
3. IO多路複用服務器端
(1)多進程服務器端的問題:進程創建和銷燬的代價太大,進程間通信相對複雜。
(2)多路複用:服務器端只提供一個進程,向多個客戶提供服務。
(3)**select函數**實現IO多路複用
fd_set reads, temps; //文件描述符集合
FD_ZERO(&reads);
FD_SET(0, &reads);
struct timeval timeout;
while(1)
{
temps = reads;
result = select(1, &temps, 0, 0, &timeout); //select函數會返回文件描述符集合的結果到temps,timeout爲計時器。
if(result==-1){...}
else if(result==0){...}
else
{
if(FD_ISSET(0, &temps)) //驗證文件描述符是否發生變化
{
strlen = read(0, buf, BUF_SIZE);
}
}
}
(4)**epoll函數**實現IO多路複用
-
select方式不適合以web服務器端開發爲主流的環境,無法滿足同時接入上百個客戶端。
-
具體原因(select的缺點):
需要對所有的文件描述符進行循環,找出發生變化的文件描述符。
需要傳遞監視對象的信息,複製傳遞前的信息,然後調用select時需要向操作系統傳遞fd_sets,然而應用程序向操作系統傳遞數據很慢。 -
解決方法:
只向操作系統傳遞一次監視對象,只有當監視範圍或內容發生變化時,只通知發生變化的事項。(Linux epoll,Windows IOCP) -
epoll:
不需要對所有文件描述符進行循環
不需要傳遞監視對象的信息 -
具體實現:
FD_SET == epoll_create 創建監視對象的文件描述符保存空間
FD_CLR == epoll_ctl 添加、刪除監視對象的文件描述符
select == epoll_wait 等待文件描述符的變化
fd_set == epoll_event 將發生變化的文件描述符集中
struct epoll_event *ep_events;
int ep_fd = epoll_create(EPOLL_SIZE); //創建文件描述符保存空間
ep_events = malloc(sizeof(struct epoll_events)*EPOLL_SIZE); //保存發生變化的文件描述符
struct epoll_event event;
event.events=EPOLLIN;
event.data.fd=serv_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event); //註冊文件描述符event
while(1)
{
event_cnt=epoll_wait(epfd, ep_events, EPOLL_SIZE, -1); //發生變化的文件描述符保存在ep_events中
for(i=0; i<event_cnt; i++)
{
if(ep_events[i].data.fd==serve_sock)
{
//建立和client的連接,註冊一個文件描述符
event...
event_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event); //註冊一個文件描述符
}
else
{
str_len = read(ep_events[i].data.fd)...
if(str_len==0)//關閉連接,刪除文件描述符
{
epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd. NULL); //刪除文件描述符
close(..);
}
else
{
write();
}
}
}
}
4. select和epoll的區別
select:它僅僅知道了,有I/O事件發生了,卻並不知道是哪那幾個流(可能有一個,多個,甚至全部),我們只能無差別輪詢所有流,找出能讀出數據,或者寫入數據的流,對他們進行操作。所以select具有O(n)的無差別輪詢複雜度,同時處理的流越多,無差別輪詢時間就越長
epoll:epoll可以理解爲event poll,不同於忙輪詢和無差別輪詢,epoll會把哪個流發生了怎樣的I/O事件通知我們。所以我們說epoll實際上是事件驅動(每個事件關聯上fd)的,此時我們對這些流的操作都是有意義的。(複雜度降低到了O(1))。
此外,沒有最大併發連接的限制,能打開的FD的上限遠大於1024(1G的內存上能監聽約10萬個端口)。