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

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

   代碼結構如下:

 首先,定義一個結構體,用於存儲 接收的socket鏈表:

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

}Node,*pNode;

     服務端的開發,還是跟上一篇差不多,但是 採用了 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);
    //啓動工作者線程
    CreateThread(NULL,0,WorkerThread,NULL,0,NULL);

    WaitForSingleObject(hAcceptThread,INFINITE);
}

     它的作用是,開啓了一個線程專門用於接收。同時開啓一個工作者線程,它會定時的遍歷socket鏈表。  接收線程的中的關鍵代碼段:

//創建一個監聽套接字
    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阻塞並不會對接入程序帶來很大的影響,只是說服務端在接入後可以立即做其它操作而已。 

   工作者線程的核心代碼如下:

WSAEVENT Event = WSACreateEvent(); //該事件是與通信套接字關聯以判斷事件的種類
    WSANETWORKEVENTS NetWorkEvent;
    while (1){
        pNode _ptr_tmp = pHead;
        while (_ptr_tmp){
            //將事件 與當前 的套接字 進行關聯
            WSAEventSelect(_ptr_tmp->s, Event, FD_READ | FD_WRITE | FD_CLOSE); //關聯事件和套接字
            DWORD dwIndex = 0;
            dwIndex = WSAWaitForMultipleEvents(1,&Event,FALSE,100,FALSE);
            dwIndex = dwIndex - WAIT_OBJECT_0;
            if (dwIndex==WSA_WAIT_TIMEOUT||dwIndex==WSA_WAIT_FAILED)
            {
                //向後遍歷
                _ptr_tmp =_ptr_tmp->pNext;
            }
            // 分析什麼網絡事件產生
            WSAEnumNetworkEvents(_ptr_tmp->s,Event,&NetWorkEvent);
            //其他情況
            if(!NetWorkEvent.lNetworkEvents)
            {
                //向後遍歷
                _ptr_tmp =_ptr_tmp->pNext;
            }

            if (NetWorkEvent.lNetworkEvents & FD_READ)
            {
                //開闢客戶端線程用於通信
                CreateThread(NULL,0,ClientThread,(LPVOID)_ptr_tmp,0, nullptr);
            }
            if(NetWorkEvent.lNetworkEvents & FD_CLOSE)
            {
                //在這裏我沒有處理,我們要將內存進行釋放否則內存泄露
                //todo: 需要釋放的內存包括:socket句柄,thread句柄,以及thread句柄中所動態申請的資源
                //向後遍歷
                _ptr_tmp =_ptr_tmp->pNext;
            }
        }
    }

  工作者線程遍歷當前的socket鏈表,對鏈表中的socket進行事件選擇,如有讀事件,則開啓一個客戶線程。(注意,當前例子中,對鏈表的操作尚不是線程安全的。)

   客戶線程關鍵代碼如下:

......
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 區別和應用場景

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