併發服務器端的實現

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萬個端口)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章