一個客戶一個子線程,也是阻塞式網絡編程,它的初始化要比一個客戶一個進程模型開銷要小;但是仍適合於長連接,不適合短連接、併發數不大的情況,尤其不適合pthread_create()的開銷大於本身服務的情況;
編程模型
(1)併發服務器1,類似於一個客戶一個進程的併發服務器1,它通常阻塞在accept,阻塞返回後派生一個子線程來處理每個客戶端,每一個客戶一個線程,創建線程的開銷比fork()要低,進程的地址空間在線程內共享;(注:fork子進程是拷貝父進程的地址空間,但是寫內容時纔會申請對應的內存,也就是寫時複製的思想,簡稱COW,而主進程創建的子線程仍與主進程位於同一個地址空間)
(3)併發服務器2,類似於一個客戶一個進程的併發服務器2,只不過預先派生一定數量爲N的子線程,子線程也同時也監聽,當各個客戶連接到達時,這些子線程就能夠立即爲它們服務,無需創建的開銷;但是如果連接數等於N時(注,父進程不參與服務),此時子進程將會被使用完,新到的連接需等到一個子線程可用,如果連接數還未到達listen調用的backlog數,三次握手已經完成,但是客戶端無法被服務,需要等到子線程執行到accept返回纔可被服務,客戶端將會明顯察覺到服務器在響應時間上的惡化,雖然客戶端的connect會立即返回,但是第一個請求在一段時間之後纔會被服務器處理;
(4)併發服務器3,它與併發服務器2類似,只不過在accpet加上互斥量,使得accept這段代碼成爲臨界區;由原先的accept爭用變成了鎖爭用,最終只有一個進程阻塞在accept上,也就是臨界區只有一個線程阻塞在accept上;
(5)併發服務器4,它使用分發機制;子線程不進行accept調用,統一由父進程accept然後將連接描述符傳遞(共享)給對應的子線程,然後由子線程爲客戶端服務,父線程可使用普通的輪轉法來選擇子線程服務;
特點
(1)TCP是一個全雙工的協議,同時支持read()和write();而阻塞式網絡編程中,服務器主進程通常阻塞在accept上,而由子線程具體負責與具體的客戶端通信,客戶端通常阻塞在read系統調用上,等待客戶端發來的命令;這樣就需要服務端和客戶端的編程需要相互配合起來;假設客戶端進程由於錯誤的程序邏輯阻塞在read上,服務器端也阻塞在read上,那麼雙方出現了通信死鎖的情況;
(2)某些客戶端繼續阻塞的讀連接數據,又需要讀鍵盤輸入,如果阻塞的讀連接數據,那麼是無法從鍵盤讀輸入的;服務器爲每一個連接準備一個線程,一個連接將會獨佔一個線程,服務器的開銷較大;如果客戶端不主動退出,將會耗費服務器端的資源;
(3)適合計算響應的工作量大於本身創建開銷的服務;
實現內容
(1)下面是針對併發服務器1的具體實現;
(2)實現的內容是一個echo服務器,由客戶端從鍵盤輸入相關內容,發送給服務器,然後由服務器收到後轉發至客戶端,客戶端打印至終端;
(3)服務器不主動斷開連接,而由客戶端從鍵盤獲得EOF或客戶端退出後,服務器也將會退出;
TcpServer服務端實現
TcpServer接口
class TcpServer final
{
public:
TcpServer(const TcpServer&) = delete;
TcpServer& operator=(const TcpServer&) = delete;
explicit TcpServer(const struct sockaddr_in& serverAddr);
void start();
private:
static void* _service(void* conn);
const int _listenfd;
const struct sockaddr_in _serverAddress;
bool _started;
};
說明幾點:
(1)與一個客戶一個進程接口類似;不同點:只不過少了子進程終止的信號處理程序,以及缺少析構函數,因爲只有一個主進程,沒有孩子線程產生;這裏也並沒有處理終止線程的pthread_join方法;可以使用一個列表將所有的pthread蒐集起來,最後TcpServer析構的時候來使用pthread_join終止每一個子線程;
(2)_serviceCount將表示TcpServer服務的次數,而不是fork後的子進程服務次數;(注:fork子進程拷貝父進程的地址空間,但是寫時纔會申請對應的內存,也就是寫時複製的思想,簡稱COW,而主進程創建的子線程仍與主進程位於同一個地址空間)
服務器啓動
void TcpServer::start()
{
assert(!_started);
sockets::bind(_listenfd, _serverAddress);
sockets::listen(_listenfd);
printf("TcpServer start...\n");
_started = true;
while (_started)
{
int connfd = sockets::accept(_listenfd, NULL); //so far, we will not concern client address
if (connfd >= 0)
{
pthread_t tid;
::pthread_create(&tid, NULL, &TcpServer::_service, reinterpret_cast<void *>(connfd));
}
else
{
printf("In TcpServer::_service, open error : %s\n", strerror_r(errno, g_errorbuf, sizeof g_errorbuf));
}
}
}
說明幾點:
(1) ::pthread_create創建線程,並將連接描述符,通過參數傳入;
(2)與一個客戶一個進程不同的是,並不需要在創建線程後關閉connd,因爲線程與主進程共享,並不像fork()將增加connd的引用計數;
服務實現
void* TcpServer::_service(void* arg)
{
::pthread_detach(::pthread_self());
int connfd = reinterpret_cast<int>(arg);
char buf[20];
int n;
while ((n = sockets::read(connfd, buf, sizeof buf)) > 0) {
sockets::writen(connfd, buf, n);
}
sockets::close(connfd);
return NULL;
}
說明幾點:
(1)服務內容,主要就是將從客戶端讀取的內容直接轉發至對應的連接,由於read和write屬於阻塞操作,可以保證接收到的字節全部轉發至客戶端;
(3)最後當read到0,說明客戶端已經斷開連接;服務器執行::exit(0)將會使內核發送Fin報文,服務器端將會從CLOSE_WAIT變爲LAST_ACK狀態;
TcpClient客戶端實現
與一個客戶一個進程的TcpClient客戶端一樣,不再贅述;