本篇文章的源碼同樣來自網絡上,自己稍加整理,並做一下源碼方面的分析。本例子的作用一方面是爲了理解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(<ime);
newtime = gmtime(<ime);
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操作,它的作用是 數據從 內核空間複製到用戶空間,這個過程仍然是阻塞的。但是它不會阻塞當前線程)。
參考: