C++實現基於單線程單客戶模型的echo程序

        一個客戶一個子線程,也是阻塞式網絡編程,它的初始化要比一個客戶一個進程模型開銷要小;但是仍適合於長連接,不適合短連接、併發數不大的情況,尤其不適合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客戶端一樣,不再贅述;





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