伺服器模型

2021-09-07 19:41:18 字數 4461 閱讀 3145

**

在使用socket進行網路程式設計時,首先要選擇乙個合適的伺服器模型是很重要的。在網路程式裡,通常都是乙個伺服器服務多個客戶機,為了處理多個客戶機的請求,伺服器端的程式有不同的處理方式。

迭代模型算是最早期的伺服器模型,其核心實現是每來乙個使用者,然後為這個使用者服務到底,過程中不接受任何新的使用者請求,單台伺服器就服務乙個使用者,其流程圖如圖1。

//tcp核心**:

bind(listenfd);

listen(listenfd);

for( ; ; )

close(connfd)

}

這種模式最大的問題是幾個主要的操作都是阻塞的,譬如accept,如果一直沒有使用者過來,那麼程序一直堵在這兒。還有read操作,前面假設的使用者建立連線後傳送資源請求,但是使用者不傳送呢?如果開啟了tcp保活檢測,那也是幾分鐘後的事才能關掉這個惡意連線,如果沒有開啟tcp保活檢測,那也要設定乙個連線有效時間。即使這樣,伺服器在中間這幾分鐘也是完全空閒的,但是不能接受新的使用者連線。

為了解決上述操作阻塞,不能接受新使用者連線,使用多程序模型。此模型的核心思想是在主程序接受使用者連線,子程序中處理業務,這樣就不會阻塞新使用者連線。

//核心**:

bind(listenfd);

listen(listenfd);

for( ; ; )

}voidclient_handler(user_info)

shutsown(connfd)

}//此種模式的劣勢會在多執行緒中給出。

linux上面執行緒又稱為輕量級程序,它和主線程共享整個程序的資料,執行緒切換的開銷遠小於程序。多執行緒模型的核心思想是每來乙個使用者連線就為使用者建立乙個執行緒,其流程圖如圖2,只需將fork改為pthread_create即可。

// 核心**:

bind(listenfd);

listen(listenfd);

for( ; ; )

void worker(user_info)

shutsown(connfd)

}

多程序模型、多執行緒模型的劣勢:2多執行緒情況下,如果乙個執行緒出現問題,可能導致所在程序掛掉

撇開框架不說,select/poll就是用來解決上述乙個使用者就使用乙個程序、執行緒的問題,select/poll可以在乙個程序、執行緒監聽多個檔案控制代碼。

//**:

bind(listenfd);

listen(listenfd);

fd_zero(&set);

fd_set(listenfd, &aset);

for( ; ; )

else if ...

//迴圈檢測檔案描述符

for( ; ; )

}

select監聽多個檔案描述符,set的本質時候乙個整形陣列,陣列的每一位元位表示乙個檔案描述符,select可以監聽檔案描述符上的讀事件、寫事件、異常事件,當檔案描述符上發生其中某件事,系統呼叫就會把相應的位置1。

select最讓人詬病的有兩點

bind(listenfd);

listen(listenfd);

addtopoll(listenfd);

for( ; ; )

else if …

//迴圈檢測檔案描述符

for( ; ; )

}

poll 的實現機制是將檔案描述符以及對此檔案描述符感興趣的事件寫入乙個結構體,poll呼叫返回,作業系統會把檔案描述符發生過的事件寫入乙個變數中。poll較select的優化之處在於不用每次拷貝檔案描述符、將事件都寫入了乙個變數,不像select使用三個變數,poll呼叫僅能儲存檔案描述符。但是poll呼叫返回也必須再次遍歷陣列,這也是乙個o(n)操作。

linux io復用使用最多就是epoll,epoll的實現同poll類似,但是它做了兩點改進。

這對有些場景是很又意義的,譬如rtsp協議在使用udp傳送資料的使用,其信令資料使用tcp傳輸,信令資料較業務資料少得多,一般較長時間才會有乙個信令互動。以60g的場景,使用者資料2m位元速率,那麼需要監聽的檔案描述符為30000,某個時刻乙個檔案描述符產生了事件,如果是poll呼叫則需遍歷長度為30000的陣列,而epoll只需要1次

// **:

bind(listenfd);

listen(listenfd);

addtoepoll(listenfd);

for( ; ; )

else if …

//迴圈檢測檔案描述符

for( ; ; )

}

顯然有了io復用這一特性,原有的多程序、多執行緒模式設計流程已經不適合。前面的所有流程中,接受新使用者連線(accept)這一操作都是在主程序或者主線程中完成中,但是在有些時候單程序、單執行緒處理就會遇到瓶頸,在前面的短連線例子中,單個程序、執行緒的caps是不到1s 2w的。關於主程序、執行緒和工作程序、執行緒的分工必須明確,到底誰負責連線、誰負責業務處理、誰負責讀寫。

為了解決accept瓶頸問題,有些模式是把處理accept放到每個程序、執行緒中,還有些公司在linux上開發核心模組,使用埠nat技術,每乙個核監聽乙個單獨的埠。好訊息是linux 3.7以上的版本支援 portreuse這一特性,多個程序可以同時監聽乙個埠又不會產生驚群效應

模型如圖3,工作執行緒能接受新使用者連線,主程序在listen之後建立多個程序。

// 核心**:

----------------------------------先fork 再 epoll

bind(listenfd);

listen(listenfd);

//一般建立同cpu個數個子程序

master = 1;

for( ;= 0);

if(pid > 0)

else

}if(1 == master)

else

void run_worker()

else if …

//迴圈檢測檔案描述符

for( ; ; )

}

可以看到主程序建立了多個字程序,然後在子程序建立自己的epoll檔案描述符,有些實現是在主程序epoll建立後才fork,個人不是很喜歡此種做法。越來越火的nginx也使用了類似的模式,為了避免驚群效應,其用共享記憶體實現了一把互斥鎖,在呼叫accept之前必須先獲取到此互斥鎖。

《linux 高效能伺服器程式設計》寫了一種免鎖的工作程序accept方式,具體的實現是子程序epoll中不加入監聽控制代碼。在程序建立初期,建立管道,父程序epoll監聽listenfd,但是不做accept操作,而是通過管道通知某個子程序去accept

核心**

void run_worker()

else if …

//迴圈檢測檔案描述符

for( ; ; )

}

從諸多的實際應用來看,使用執行緒的時候很少有在子執行緒中做accept操作,一般的做法是主線程只做accept操作,然後子執行緒負責資料的讀寫。這樣程式設計也是最簡單的,但是極易出現主線程accept的瓶頸。

在主線程accept之前,會建立一些執行緒和對應數量的epoll,為每乙個執行緒分配乙個epoll。主線程接受到新使用者後,因為是同一程序,直接將使用者新增到某個執行緒的epoll中。

核心**:

bind(listenfd);

listen(listenfd);

//建立同cpu個數個程序

for( ;epoll 執行緒池也有另外一種做法,主線程負責accept,負責分發任務,它會把使用者的scoket寫入鍊錶,然後多個執行緒鍊錶去競爭這個鍊錶,得到鍊錶的執行緒去除頭節點然後釋放所有權,工作執行緒只有業務處理,沒有epoll操作。這種做法有兩個缺點,一是主線程既要處理使用者連線請求,又要分發任務,造成主線程忙死、子執行緒閒死的現象,完全沒有發揮epoll和多執行緒的特點。

伺服器設計模型比較多,現在學習都是比較基礎的內容,類似事件觸發、流化、埠對映、埠**、半非同步方式、反應堆方式、追隨者模式等都沒有提及,總之要學習的還有很多。 

伺服器模型

伺服器模型 1 迴圈伺服器模型 tcp 迴圈伺服器 udp 迴圈伺服器 2 併發伺服器 tcp 併發伺服器 父子程序實現併發伺服器 父親程序 接收請求。accept 兒子程序 處理具體客戶端需求。send recv 注意點 殭屍程序,父親活著,兒子死亡,父親沒有為兒子程序收屍,會產生殭屍程序。避免殭...

TCP伺服器模型

迴圈伺服器 迴圈伺服器在同乙個時刻只可以響應乙個客戶端的請求 併發伺服器 併發伺服器在同乙個時刻可以響應多個客戶端的請求 9.1 迴圈伺服器 udp伺服器 udp迴圈伺服器的實現非常簡單 udp伺服器每次從套接字上讀取乙個客戶端的請求,處理,然後將結果返回給客戶機.可以用下面的演算法來實現.sock...

伺服器程式設計模型

從執行緒的角度,可以將伺服器程式設計分為兩類 單執行緒和多執行緒。單執行緒模型 乙個程序中只有乙個執行緒,由於只有乙個執行緒,所以要實現高效能,必須與 non blocking i o i o multiplexing 相結合,另外 libevent 本身也是單執行緒的。相對於多執行緒,單執行緒 s...