c++ 通信演進level2 ----多線程同步 非阻塞通信(NIO)

  本篇文章的源碼同樣來自網絡上,自己稍加整理,並做一下源碼方面的分析。本例子的作用一方面是爲了理解http服務器,另一方面,是作爲學習流操作的NIO模型層次。 地址在這裏:地址。 

   代碼結構如下:

 首先,定義兩個結構體,用於存儲 接收的socket鏈表,以及存儲 與當前socket綁定的 thread鏈表:

//標識客戶端的節點 鏈表
typedef struct _NODE_
{
    SOCKET s;
    sockaddr_in Addr;
    _NODE_* pNext;

}Node,*pNode;

//標識線程的節點,多線程處理多個客戶端的連接
typedef struct _THREAD_
{
    DWORD ThreadID;
    HANDLE hThread;
    _THREAD_* pNext;
}Thread,*pThread;

     服務端的開發,還是跟上一篇差不多,但是 採用了 winsock2的api,它是非阻塞的。 

int main()
{
    if (!InitSocket())
    {
        printf("InitSocket Error\n");
        return -1;
    }
    GetCurrentDirectory(512,HtmlDir);
    strcat(HtmlDir,"\\..\\html\\"); // 尋找 html 所在目錄
    strcat(HtmlDir,FileName);
    std::cout<<"the path is:"<<HtmlDir<<std::endl;
    //啓動一個接收線程
    HANDLE hAcceptThread = CreateThread(NULL,0,AcceptThread,NULL,0,NULL);
    WaitForSingleObject(hAcceptThread,INFINITE);
}

     它的作用是,開啓了一個線程專門用於接收。  線程的中的關鍵代碼段:

//創建一個監聽套接字
    SOCKET sListen = WSASocket(AF_INET,SOCK_STREAM,0,NULL,0,WSA_FLAG_OVERLAPPED); //使用事件重疊的套接字
.....
int Ret = bind(sListen,(sockaddr*)&LocalAddr,sizeof(LocalAddr));
.....
listen(sListen,5);
.....
//我們要爲新的連接進行接受並申請內存存入鏈表中
SOCKET sClient = WSAAccept(sListen, (sockaddr*)&ClientAddr, &nLen, NULL, NULL);
.....
AddClientList(sClient,ClientAddr);//當接收到的soccket放入內存中
.....

    需要注意的是,這裏的WSAAccept不是阻塞的,它會立即返回。 當然,這樣的話會導致大部分時間做無意義的操作,而它的觸發,通過事件機制是一個比較好的方式,這樣既兼顧了 效率,也兼顧了 易用性(因爲它帶來了非阻塞的好處)。我剛剛思考了下,貌似這裏accept阻塞並不會對接入程序帶來很大的影響,只是說服務端在接入後可以立即做其它操作而已。 

    在addClientList的過程中,會將當前接收到的socket加入鏈表,同時新開一個線程,用於處理用戶的消息收發。關鍵代碼如下:

......
//我們要爲用戶開闢新的線程
hThread = CreateThread(NULL,0,ClientThread,(LPVOID)pTemp,0,&ThreadID);
......
AddThreadList(hThread,ThreadID);//將當前線程句柄加入鏈表
......

   客戶線程關鍵代碼如下:

......
Ret = WSARecv(sClient,&Buffers,dwBufferCount,&NumberOfBytesRecvd,&Flags,NULL,NULL);
......
Ret = WSASend(sClient,&Buffers,1,&NumberOfBytesSent,0,0,NULL);
......

  也就是說,它的實際操作仍然是先從客戶端讀取 消息,然後發送消息給客戶端。 注意,此時的客戶端是  瀏覽器,而此時的應用層協議爲  http。 整體通信流程跟上一篇是差不多,但是這裏採用非阻塞的方式處理信息的接收。  

  另外,與http協議有關的部分處理代碼:

char szRequest[1024]={0}; //請求報文
char szResponse[1024]={0}; //響應報文
......
Ret = WSARecv(sClient,&Buffers,dwBufferCount,&NumberOfBytesRecvd,&Flags,NULL,NULL);
memcpy(szRequest,szBuffer,NumberOfBytesRecvd);
......
parseRequest部分
{
    char pResponseHeader[512]={0};
    char szStatusCode[20]={0};
    char szContentType[20]={0};
    strcpy(szStatusCode,"200 OK");
    strcpy(szContentType,"text/html");
    char szDT[128];
    struct tm *newtime;
    time_t ltime;
    time(&ltime);
    newtime = gmtime(&ltime);
    strftime(szDT, 128,"%a, %d %b %Y %H:%M:%S GMT", newtime);
    //讀取文件
    //定義一個文件流指針
    FILE* fp = fopen(HtmlDir,"rb");
    ......
            // 返回響應
    sprintf(pResponseHeader, "HTTP/1.0 %s\r\nDate: %s\r\nServer: %s\r\nAccept-Ranges: bytes\r\nContent-Length: %d\r\nConnection: %s\r\nContent-Type: %s\r\n\r\n",
                szStatusCode, szDT, SERVERNAME, length, bKeepAlive ? "Keep-Alive" : "close", szContentType);   //響應報文
}
......
Ret = WSASend(sClient,&Buffers,1,&NumberOfBytesSent,0,0,NULL);
......

   運行結果:

總結:

     1.http協議作爲應用層協議的一種,實際上跟端對端通信沒什麼兩樣,不要過於神祕化,以上代碼足以證明;

     2.當前的這個例子中,一個socket對應着一個線程,事實上,這也是最簡單的方式支持多個併發連接。 但是簡單的不一定是最好的,當連接數量很多時,線程調度將變爲災難(要知道操作系統中可不僅僅只有當前的應用線程)。   同時對於頻繁的連接操作,會導致線程資源的頻繁回收,降低效率。(很早以前,我也覺得服務器沒啥大不了嘛,要處理併發連接這還不簡單。。。)

    3.就當前的通信流程而言,我目前暫時看不出 非阻塞 較之於 阻塞的優點在哪(因爲當前的線程實際上只做了一件事,那就是socket的讀寫,並未用於其它操作,即當前線程的任務本來就已經很單一了。)。  我猜想可能時  阻塞佔用 cpu時間,而非阻塞不佔用cpu時間吧(它等待的是內核對象)。

    4.對BIO看了一些文章不是很懂。 目前的大致理解是: 通過內核事件,知道當前有要讀取的信息,然後讀取(讀取是io操作,它的作用是 數據從 內核空間複製到用戶空間,這個過程仍然是阻塞的但是它不會阻塞當前線程)。

參考:

BIO、NIO、AIO 區別和應用場景

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