文章目錄
- 重疊IO模型介紹
- 重疊IO模型代碼邏輯
- 重疊IO模型代碼實作
-
- 1.-5.
- 6.投遞AcceptEx
- 3.1
- 3.3循環等待/查詢事件
- 3.3.2有信号
- 3.3.2.X
-
- 3.3.2.1
- 3.3.2.2
- 3.3.2.3、3.3.2.4
- PostSend
- 整體代碼
- 未完成工作及問題
https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-socket
基于TCP/IP的網絡程式設計有5種模型:
SELECT模型
事件選擇模型
異步選擇模型
重疊IO模型
完成端口模型
這次先講第四種。
重疊IO模型介紹
重疊IO是Windows提供的一種異步讀寫檔案的機制。
如果我們把網絡發送消息,讀取消息中的消息看成檔案,那麼SOCKET的本質就是檔案操作。正常的讀檔案,例如recv,是阻塞的,等待協定緩沖區中的全部複制到我們自己定義的buff中,函數才能結束并傳回複制的内容,如果多次調用recv函數,那麼這些recv函數是依次一個個執行的。寫(send)的過程也一樣,同一時間隻能執行一個send函數,其他的操作隻能等。
重疊IO機制則是把上面描述的過程做成異步操作,将的指令以及我們自定義的buff投遞給作業系統,然後函數直接傳回,由作業系統獨立打開一個線程,将資料複制到buff,這個過程,我們的應用可以做别的事情,也就意味我們可以同時投遞多個讀或寫指令,同時進行多個讀寫操作。
從代碼上看,就是将原來的accept、recv、send函數,轉化為可異步執行的AcceptEx、WSARecv、WSASend函數。
關于異步讀寫這個我記得當年教SOCKET的時候有一個釣魚的例子,不過書沒找見了。
重疊IO的由來是由其結構體WSAOVERLAPPED得來
typedef struct _WSAOVERLAPPED {
DWORD Internal;
DWORD InternalHigh;
DWORD Offset;
DWORD OffsetHigh;
WSAEVENT hEvent;
} WSAOVERLAPPED, *LPWSAOVERLAPPED;
該結構體的前四個成員是保留給系統使用的,第五個成員WSAEVENT hEvent是事件對象的句柄。操作完事件句柄後,系統将WSAOVERLAPPED結構體中的第五個成員設定為有信号。
在使用的時候,我們就是将SOCKET和WSAOVERLAPPED綁定後,投遞給作業系統,系統會以重疊IO機制處理回報,回報方式有兩種:事件通知、完成例程。
對于事件通知而言:
1.調用AcceptEx WSARecv WSASend投遞
2.作業系統将被完成的操作,事件信号置成有信号
3.調用WSAGetOverlappedResult擷取事件信号
對于完成例程而言:
1.調用AcceptEx WSARecv WSASend投遞
2.完成後自動調用回調函數
重疊IO模型代碼邏輯
先看事件通知的回報方式
1.建立事件(optional)、SOCKET數組,重疊結構體數組(根據下标來進行對應,相同下标是一組)
2.建立重疊IO模型使用的SOCKET:WSASocket
3.投遞AcceptEx
3.1立即完成,此時有用戶端連接配接
3.1.1對用戶端套接字投遞WSARecv
3.1.1.1有用戶端消息,系統空閑,立即完成,跳3.1.1
3.1.1.2無用戶端消息,跳3.3
3.1.2根據需求對用戶端套接字投遞WSASend
3.1.2.1有消息要發送,系統空閑,立即完成,跳3.1.2
3.1.2.2無消息要發送,跳3.3
3.1.3如果需要連接配接用戶端跳3
3.2延遲完成,此時沒有用戶端連接配接,跳3.3
3.3循環等待信号(WSAWaitForMultipleEvents)
3.3.1 沒信号,繼續等
3.3.2 有信号,先擷取重疊結構上的資訊(WSAGetOverlappedResult)
3.3.2.1 如果信号是伺服器端上檢測到有用戶端連接配接,跳轉3(這個步驟和3.3.2.2不可以調換順序)
3.3.2.2 如果信号是用戶端退出,則關閉用戶端SOCKET,并從數組删除用戶端的資訊
3.3.2.3 如果信号是需要接收資訊,跳轉3.1.1
3.3.2.4 如果信号是需要發送資訊,跳轉3.1.2
說明:由于AcceptEx WSARecv WSASend每調用一次,隻會處理一次,要處理多次就要多次調用,是以上面有跳轉。
重疊IO模型代碼實作
1.-5.
和之前一樣,重疊IO模型伺服器端的SOCKET代碼套路的前面部分是一樣的:
1.打開網絡庫
2.校驗版本
3.建立SOCKET 這裡要用WSASocket來建立
再次強調:WSA是windows socket async的縮寫
SOCKET WSAAPI WSASocketA(
int af,
int type,
int protocol,
LPWSAPROTOCOL_INFOA lpProtocolInfo,
GROUP g,
DWORD dwFlags
);
前面幾個參數可以參考第一篇簡單通信模型,這裡不展開介紹。
參數1:位址類型
參數2:套接字類型
參數3:協定類型
參數4:協定屬性設定,不用則設定為NULL,可以設定的内容包括:
發送資料是否需要連接配接
是否保證資料完整到達(選擇TCP面向連接配接的協定或UDP面向傳輸的協定的處理方式)
參數3填0,那麼比對哪個協定在這裡設定,貌似是一個清單,按清單的先後順序進行比對
設定傳輸接收位元組數限制
設定套接字權限,設定後進行相應套接字操作的時候會有相應的提示,例如發送資料會觸發某個操作,具體操作在參數6裡面設定WSA_FLAG_ACCESS_SYSTEM_SECURITY
其他很多保留字段,供以後拓展使用
以上參數4的設定,實際是修改WSAPROTOCOL _INFO結構的指針,具體可以看:
https://docs.microsoft.com/en-us/windows/win32/api/winsock2/ns-winsock2-wsaprotocol_infoa
參數5:保留參數,預設寫0(看名字應該是組ID,可以一次操作多個socket,多/多點傳播?)
參數6:指定套接字屬性/設定。
這裡用:WSA_FLAG_OVERLAPPED,表示建立重疊IO模型的SOCKET
還有别的一些宏:
WSA_FLAG_ACCESS_SYSTEM_SECURITY:套接字權限設定,配合參數4中的設定一起使用
WSA_FLAG_NO_HANDLE_INHERIT:套接字不可繼承,這個在多線程SOCKET裡面用。在多線程開發中,子線程會繼承父線程的SOCKET,即主線程創了一個SOCKET,那麼子線程有兩種使用方式:
方式 | 方法 |
---|---|
共享 | 直接用父類的,父子都使用這一個SOCKET(隻用關閉一次) |
繼承(預設) | 把父類的這個SOCKET複制一份,自己用,這兩父子用兩個SOCKET,但是本質一樣(要關閉兩次) |
多點傳播(1vs.n)用的:
WSA_FLAG_MULTIPOINT_C_ROOT
WSA_FLAG_MULTIPOINT_C_LEAF
WSA_FLAG_MULTIPOINT_D_ROOT
WSA_FLAG_MULTIPOINT_D_LEAF
傳回值:
成功傳回可用的SOCKET句柄
失敗傳回INVALID_SOCKET
具體看:
https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsasocketa
4.綁定位址與端口
5.開始監聽
再往下的步驟就和基本架構有很大不同,詳細寫
6.投遞AcceptEx
這個玩意的流程在上面有寫,友善看就直接copy下來:
3.投遞AcceptEx
3.1立即完成,此時有用戶端連接配接
3.1.1對用戶端套接字投遞WSARecv
3.1.1.1有用戶端消息,系統空閑,立即完成,跳3.1.1
3.1.1.2無用戶端消息,跳3.3
3.1.2根據需求對用戶端套接字投遞WSASend
3.1.2.1有消息要發送,系統空閑,立即完成,跳3.1.2
3.1.2.2無消息要發送,跳3.3
3.1.3如果需要連接配接用戶端跳3
3.2延遲完成,此時沒有用戶端連接配接,跳3.3
3.3循環等待信号(WSAWaitForMultipleEvents)
3.3.1 沒信号,繼續等
3.3.2 有信号,先擷取重疊結構上的資訊(WSAGetOverlappedResult)
3.3.2.1 如果信号是伺服器端上檢測到有用戶端連接配接,跳轉3(這個步驟和3.3.2.2不可以調換順序)
3.3.2.2 如果信号是用戶端退出,則關閉用戶端SOCKET,并從數組删除用戶端的資訊
3.3.2.3 如果信号是需要接收資訊,跳轉3.1.1
3.3.2.4 如果信号是需要發送資訊,跳轉3.1.2
由于要對多個SOCKET進行操作,是以先要建立SOCKET數組和重疊結構體數組,套路和前面兩節的選擇模型一樣的,數組+個數
#define MAX_COUNT 1024
SOCKET garr_sockAll[MAX_COUNT];//SOCKET數組
OVERLAPPED garr_olpAll[MAX_COUNT];//重疊結構體數組
int gi_count;//數組中SOCEKT的個數,全局變量預設值是0
然後把上面建立好的SOCKET放到數組裡面去,事件也要初始化:
garr_sockAll[gi_count] = socketServer;//初始化後的SOCKET放到SOCKET數組裡面
garr_olpAll[gi_count].hEvent = WSACreateEvent();//事件初始化
gi_count++;
接下來的投遞代碼為了簡潔,單獨将其寫成一個函數。先來看看AcceptEx函數
//投遞伺服器SOCKET句柄,異步接收用戶端連接配接
BOOL AcceptEx(
SOCKET sListenSocket,
SOCKET sAcceptSocket,
PVOID lpOutputBuffer,
DWORD dwReceiveDataLength,
DWORD dwLocalAddressLength,
DWORD dwRemoteAddressLength,
LPDWORD lpdwBytesReceived,
LPOVERLAPPED lpOverlapped
);
參數1:伺服器SOCKET句柄
參數2:接收的用戶端SOCKET句柄,這個句柄要通過Socket或者WSASocket來建立,然後放在AcceptEx這裡相當于原始模型中的accept+bind操作,将用戶端的IP和端口号綁定到參數2上。
參數3:緩沖區的指針,接收在新連接配接上發送的第一個資料。意思是這裡可以直接拿到用戶端第一次send過來的資料,第二次和之後send的資料就要用WSARecv來接收了。這個參數是一個字元數組,不能設定為NULL。
參數4:上面參數3功能估計是程式猿打瞌睡弄出來bug,太惡搞了,都有recv幹嘛還要設定這個功能,難道每個用戶端都和伺服器YYQ,隻發送一次資料,然後為了偷懶弄出來的?是以參數4進行補救,如果參數4設定為0,那麼參數3無效,但是不管參數4是否設定為0,參數3都不可設定為NULL。如果要使得參數3生效,參數4則要設定為參數3的長度,此時,用戶端連接配接伺服器并發送了一條消息後伺服器才産生信号,才能recv到資料。
參數5:The number of bytes reserved for the local address information. This value must be at least 16 bytes more than the maximum address length for the transport protocol in use.是以固定寫成:sizeof(struct sockaddr _in)+16
參數6:和參數5一樣,隻不過存在Remote端,固定寫成:sizeof(struct sockaddr _in)+16
簡要分析:由于SOCKET操作可以看做是檔案操作,是以參數5可以看做是本地硬碟用的位址,參數6可以看做是記憶體用的位址。
參數7:配合參數3和參數4使用,如果第一次是在這裡接收資訊,而且立即執行(步驟3.1)接收到了資訊,那麼參數7将得到資訊的長度。
如果不想擷取資訊的長度,填寫NULL即可;否則填寫DWORD變量的位址即可。
參數8:伺服器SOCKET句柄對應的重疊結構體。注意不要寫錯為用戶端的重疊結構體
傳回值:
TRUE:表示執行完成就有用戶端到了,accept成功
FALSE:出錯,用int acceptexerr = WSAGetLastError()擷取錯誤碼并處理
情況1:acceptexerr ==ERROR_IO_PENDING,表示執行成功,但處于異步等待狀态,或者此時還沒有用戶端請求連接配接;
情況2:其他錯誤碼要根據情況解決。
注意:AcceptEx要加載自己的頭檔案和庫檔案:
#include <mswsock.h>//這個頭檔案要在#include <WinSock2.h>下面
#pragma comment(lib, "mswsock.lib")
//投遞AcceptEx
int PostAccept()
{
garr_sockAll[gi_count]=WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,0,WSA_FLAG_OVERLAPPED);
garr_olpAll[gi_count].hEvent = WSACreateEvent();//事件初始化
char str[1024] = {0};
DWORD dwRecvCount = 0;
BOOL bRes = AcceptEx(garr_sockAll[0],garr_sockAll[gi_count],str,0,sizeof(struct sockaddr_in)+16,sizeof(struct sockaddr_in)+16,&dwRecvCount,garr_olpAll[0]);
if (bRes == TRUE)
{
gi_count++;
//這裡執行3.1
//執行成功,并連接配接成功
return 0;
}
else
{
int acceptexerr = WSAGetLastError();
if (acceptexerr == ERROR_IO_PENDING)
{
//延遲處理
return 0;
}
else
{
//出錯處理
}
}
}
3.1
接下來寫上面自定義函數中3.1部分,這裡acceptEx執行成功,并有用戶端進行連接配接上來。
這裡要用到WSARecv函數,該函數用來投遞異步Recv消息
int WSAAPI WSARecv(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesRecvd,
LPDWORD lpFlags,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
參數1:用戶端SOCKET句柄
參數2:接收用戶端資訊的BUFF,這裡不能用自定義的字元串數組,要用:
typedef struct _WSABUF {
ULONG len;
CHAR *buf;
} WSABUF, *LPWSABUF;
參數3:有幾個參數2,這裡一般是1個
參數4:接收成功,則這裡可以設定接收到的資訊長度,如果參數6設定的重疊結構體不為空,這裡也可以設定為NULL,表示不擷取接收到的資訊長度
參數5:和recv函數中的參數4意思一樣,用于設定WSARecv的标注,把前面的内容拷貝過來:
資料的讀取方式。預設是0即可。正常情況下recv根據參數3讀取資料緩沖區指定長度的資料後(指定長度大于資料長度則全部讀取),資料緩沖區中被讀取的資料會清除,把空間留出來給别的消息進來(不清理的話時間長了記憶體會溢出,資料緩沖區資料結構相當于隊列)。
例如資料緩沖區中有如下資料:
a | b | c | d | e | f |
---|
調用recv(socketClient,buff,2,0);從資料緩沖區讀取兩個位元組的資料得到a,b。則變成
c | d | e | f |
---|
這個時候再調用recv(socketClient,buff,2,0);從資料緩沖區讀取兩個位元組的資料得到c,d。
懂得正常邏輯後我們可以看下其他幾種模式。
數值 | 含義 |
---|---|
0(預設值) | 從資料緩沖區讀取資料後清空被讀取的資料 |
MSG_PEEK(不建議使用,記憶體會爆) | 從資料緩沖區讀取資料後不清空被讀取的資料 |
MSG_OOB | 接收帶外資料,每次可以額外傳輸1個位元組的資料,具體資料内容可以自己定義,這個方法可以用分别調用兩次send函數,而且在不同TCP協定規範這個模式還不怎麼相容,是以也不推薦使用 |
MSG_WAITALL | 等待知道系統緩沖區位元組數大于等于參數3所指定的位元組數,才開始讀取 |
如果使用MSG_PEEK模式,那麼調用recv(socketClient,buff,2,MSG_PEEK);從資料緩沖區讀取兩個位元組的資料得到a,b。由于不清空被讀取的資料,緩沖區還是不變:
a | b | c | d | e | f |
---|
如果再次執行recv(socketClient,buff,2,MSG_PEEK);從資料緩沖區讀取兩個位元組的資料還是得到a,b。
看了一下MSDN,WSARecv比Recv函數要多了2個模式
數值 | 含義 |
---|---|
MSG_PUSH_IMMEDIATE | 該辨別隻能用在tream-oriented sockets,盡快處理請求,不做延遲,該辨別在接收資料較小的時候比較有用,但是當資料較大進行分包傳輸後,使用該辨別會造成後面傳輸的包會被丢棄,導緻傳輸失敗! |
MSG_PARTIAL | 從資料緩沖區讀取的資料是用戶端發送的部分資料,程式員應該讀取完整資料後再進行處理,該辨別是由用戶端設定的 |
參數6:重疊IO結構體指針
參數7:回調函數,先設定為NULL
傳回值:
0:表示執行成功。這裡要注意的是和AcceptEx不一樣,AcceptEx傳回的TRUE
SOCKET_ERROR :出錯,用int wasrecverr = WSAGetLastError()擷取錯誤碼并處理
情況1:wasrecverr ==ERROR_IO_PENDING,表示執行成功,但處于異步等待狀态,或者此時還沒有用戶端請求連接配接;
情況2:其他錯誤碼要根據情況解決。
//投遞RecvEx
//參數socketIndex是目前SOCKET數組下标
int PostRecv(int socketIndex)
{
WSABUF wsabuff;
wsabuff.buf = gc_recvbuff;
wsabuff.len= MAX_RECV_LENGTH;
DWORD dwRecvedLength;
DWORD dwRecvFlag=0;
int iret = WSARecv(garr_sockAll[socketIndex],&wsabuff,1,&dwRecvedLength,&dwRecvFlag,&garr_olpAll[socketIndex],NULL);
if (iret == 0)
{
//立即完成,執行成功
//收取資訊後傳回
printf("%s\n",wsabuf.buf);
memset(gc_recvbuff,0,MAX_RECV_LENGTH);//清空buff
//根據情況投遞send
//跳3.1.1繼續投遞Recv
PostRecv(socketIndex);
return 0;
}
else
{
int wasrecverr = WSAGetLastError();
if (wasrecverr == ERROR_IO_PENDING)
{
//延遲處理
return 0;
}
else
{
//出錯處理
}
}
return 0;
}
3.3循環等待/查詢事件
DWORD WSAAPI WSAWaitForMultipleEvents(
DWORD cEvents,
const WSAEVENT *lphEvents,
BOOL fWaitAll,
DWORD dwTimeout,
BOOL fAlertable
);
參數1:要等待/查詢的事件數量
參數2:要等待/查詢的事件句柄
參數3:如果查詢一組/多個事件,當參數3為TRUE的時候,要等所有事件都有信号才傳回,填FALSE則隻要有一個事件有信号就傳回
參數4:查詢沒有信号等待的時間,不等待就寫0
參數5:設定線程是否進入alertable wait state,跟多線程有關,目前單線程且是事件通知先FALSE
傳回值
整型,具體看
https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsawaitformultipleevents
//accept成功就循環等待事件發生
while(1)
{
for(int i = 0; i < gi_count; i++)
{
//笨方法,一次查詢一個事件是否有信号
int nRes=WSAWaitForMultipleEvents(1,&(garr_olpAll[i].hEvent), FALSE,0, FALSE);
if(nRes==WSA_WAIT_FAILED || nRes==WSA_WAIT_TIMEOUT)//查詢失敗或者逾時
{
continue;
}
}
}
3.3.2有信号
這時要擷取socket上的具體信号
BOOL WSAAPI WSAGetOverlappedResult(
SOCKET s,
LPWSAOVERLAPPED lpOverlapped,
LPDWORD lpcbTransfer,
BOOL fWait,
LPDWORD lpdwFlags
);
參數1:有信号的SOCKET句柄
參數2:有信号的SOCKET句柄對應的重疊結構的位址
參數3:發送或者接收到的實際位元組數,如果得到0,表示用戶端下線
參數4:當重疊操作選擇了基于事件的完成通知時,設定為TRUE,這裡就預設TRUE,設定false的解釋如下:
If FALSE and the operation is still pending, the function returns FALSE and the WSAGetLastError function returns WSA_IO_INCOMPLETE.
參數5:不可為NULL,裝WSARecv的參數5:lpflags,具體看上面的表
傳回值:
TRUE:成功
FALSE:失敗,具體錯誤碼看
https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsagetoverlappedresult
這裡要注意,如果錯誤碼是10054表示點×關閉視窗,要單獨判斷釋放用戶端SOCKET。
//經過查詢有信号
//3.3.2有信号
DWORD dwTransfer;//接收或發送的資料長度
DWORD dwFlagrecvpara5;//和recv中的參數5一緻
BOOL bret = WSAGetOverlappedResult(garr_sockAll[i],&garr_olpAll[i],&dwTransfer,TRUE,&dwFlagrecvpara5);
if (bret == FALSE)//擷取信号失敗則跳過
{
continue;
}
//擷取信号成功,則分類處理
//0号位代表伺服器SOCKET,說明接受連接配接完成
if(i == 0)
{
}
//長度為0表示用戶端正常下線
if(dwTransfer == 0)
{
}
//發送或者接收資料成功
if(dwTransfer != 0)
{
}
注意:WSAGetOverlappedResult沒有自帶重置信号的功能,要在執行它後面接下面代碼重置信号:
3.3.2.X
下面處理上面代碼中if判斷裡面的内容,這裡對應僞代碼的3.3.2.X的内容。
3.3.2.1
如果信号是用戶端連接配接,跳轉3
實際上這裡就是要先收資料,因為連接配接成功後必定要先收,然後再投遞accept
//0号位代表伺服器SOCKET,說明接受連接配接完成,且剛連接配接上的用戶端SOCKET在數組的第gi_count位上
if(i == 0)
{
//執行成功,并連接配接成功
//走流程3.1的兩種情況
//投遞recv
PostRecv(gi_count);
gi_count++;//注意這裡gi_count++的位置
//再次投遞AcceptEx
PostAccept();
continue;//一次處理一個響應,處理完跳出循環後面不用看了
}
3.3.2.2
如果信号是用戶端退出,則關閉用戶端SOCKET,并從數組删除用戶端的資訊
//長度為0表示用戶端下線
if(dwTransfer == 0)
{
//關閉用戶端SOCKET和事件句柄
closesocket(garr_sockAll[i]);
WSACloseEvent(garr_olpAll[i].hEvent);
//從數組中删除用戶端SOCKET和事件,這裡思路用數組最後一位替換目前元素
garr_sockAll[i] = garr_sockAll[gi_count-1];
garr_olpAll[i] = garr_olpAll[gi_count-1];
gi_count--;
i--;//循環控制變量i回退一位,重新循環目前替換的新元素
}
3.3.2.3、3.3.2.4
3.3.2.3如果信号是需要接收資訊,跳轉3.1.1
3.3.2.4 如果信号是需要發送資訊,跳轉3.1.2
這裡用gc_recvbuff全局變量來判斷是接受還是發送資訊
//發送或者接收資料成功
if(dwTransfer != 0)
{
//根據全局變量gc_recvbuff是否為空來判斷是否發送資料
if(gc_recvbuff[0] != 0)//不空說明收到資料,應該是recv
{
//立即完成,執行成功
//收取資訊後傳回
printf("%s\n",gc_recvbuff);
memset(gc_recvbuff,0,MAX_RECV_LENGTH);//清空buff
//根據情況投遞send
//跳3.1.1繼續投遞Recv
PostRecv(i);
}
else//發送資料
{
//send
}
}
PostSend
上面的代碼中還缺少對發送資料的操作進行處理,這裡也将其寫成一個函數的形式。
主要函數看這裡:
https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsasend
int WSAAPI WSASend(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent,
DWORD dwFlags,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
參數1:目标用戶端SOCKET句柄
參數2:WSABUF結構體,參考WSARecv
參數3:WSABUF結構體的個數,一般為1
參數4:發送成功後,擷取到已發送消息長度的位元組數
參數5:同WSARecv參數5,不過不是位址,而是訓示辨別
參數6:重疊IO結構體位址
參數7:回調函數,先設定為NULL
整體代碼
#include <WinSock2.h>
#include <mswsock.h>
#include <stdio.h>
#pragma comment(lib, "Ws2_32.lib")
#pragma comment(lib, "mswsock.lib")
#define MAX_COUNT 1024//這裡上萬個SOCKET應該沒壓力
#define MAX_RECV_LENGTH 1000//接收緩沖區一次最多接收字元串長度
#define MAX_SEND_LENGTH 1000//接收緩沖區一次最多發送字元串長度
SOCKET garr_sockAll[MAX_COUNT];//SOCKET數組
OVERLAPPED garr_olpAll[MAX_COUNT];//重疊結構體數組
int gi_count;//數組中SOCEKT的個數,全局變量預設值是0
char gc_recvbuff[MAX_RECV_LENGTH];//接收緩沖區全局變量,也可以放到main函數中定義,然後PostRecv傳址調用
//函數聲明
int PostAccept();
int PostRecv(int socketIndex);
int PostSend(int socketIndex);
//清理兩個數組,逐個關閉SOCKET和事件
void ClearArr()
{
for(int i = 0;i < gi_count;i++)
{
closesocket(garr_sockAll[i]);//關閉SOCKET
WSACloseEvent(garr_olpAll[i].hEvent);//關閉事件
}
}
BOOL WINAPI cls(DWORD dwCtrlType)
{
switch (dwCtrlType)
{
case CTRL_CLOSE_EVENT :
//釋放數組中的事件和SOCKET句柄
ClearArr();
WSACleanup();
break;
}
return TRUE;
}
int main(void)
{
SetConsoleCtrlHandler(cls,TRUE);
/* Use the MAKEWORD(lowbyte, highbyte) macro declared in Windef.h */
WORD wdVersion=MAKEWORD(2,2);
WSADATA wdScokMsg;
int nRes = WSAStartup(wdVersion,&wdScokMsg);
if (0 != nRes)
{
switch(nRes)
{
case WSASYSNOTREADY:
printf("解決方案:重新開機電腦。。。\n");
break;
case WSAVERNOTSUPPORTED:
printf("請更新網絡庫\n");
break;
case WSAEINPROGRESS:
printf("請重新開機程式\n");
break;
case WSAEPROCLIM:
printf("請關閉空閑軟體,釋放資源來運作程式\n");
break;
case WSAEFAULT:
break;
}
return 0;
}
//校驗版本
if (2!=HIBYTE(wdScokMsg.wVersion)|| 2!=LOBYTE(wdScokMsg.wVersion))
{
printf("版本有問題!\n");
WSACleanup();
return 0;
}
//這裡和前面不一樣,要把Socket初始化換成WSASocket
SOCKET socketServer=WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,0,WSA_FLAG_OVERLAPPED);
if(INVALID_SOCKET == socketServer)
{
int err=WSAGetLastError();
printf("WSASocket初始化失敗,錯誤碼是:%d\n",err);
//清理網絡庫,不關閉句柄
WSACleanup();
return 0;
}
struct sockaddr_in si;
si.sin_family = AF_INET;
si.sin_port = htons(9527);//用htons宏将整型轉為端口号的無符号整型
si.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
if(SOCKET_ERROR==bind(socketServer,(const struct sockaddr *)&si,sizeof(si)))
{
int err = WSAGetLastError();//取錯誤碼
printf("伺服器bind失敗錯誤碼為:%d\n",err);
closesocket(socketServer);//釋放
WSACleanup();//清理網絡庫
return 0;
}
printf("伺服器端bind成功!\n");
if(SOCKET_ERROR==listen(socketServer,SOMAXCONN))
{
int err = WSAGetLastError();//取錯誤碼
printf("伺服器監聽失敗錯誤碼為:%d\n",err);
closesocket(socketServer);//釋放
WSACleanup();//清理網絡庫
return 0;
}
printf("伺服器端監聽成功!\n");
//6.投遞AcceptEx
garr_sockAll[gi_count] = socketServer;//初始化後的SOCKET放到SOCKET數組裡面
garr_olpAll[gi_count].hEvent = WSACreateEvent();//事件初始化
gi_count++;
if (PostAccept() != 0)//出錯
{
ClearArr();
WSACleanup();
return 0;
}
//accept成功就循環等待事件發生
//3.3循環等待/查詢事件
while(1)
{
for(int i = 0; i < gi_count; i++)
{
//笨方法,一次查詢一個事件是否有信号
int nRes=WSAWaitForMultipleEvents(1,&(garr_olpAll[i].hEvent), FALSE,0, FALSE);
if(nRes==WSA_WAIT_FAILED || nRes==WSA_WAIT_TIMEOUT)//查詢失敗或者逾時
{
continue;
}
//經過查詢有信号
//3.3.2有信号
DWORD dwTransfer;//接收或發送的資料長度
DWORD dwFlagrecvpara5;//和recv中的參數5一緻
//擷取socket上的具體信号
BOOL bret = WSAGetOverlappedResult(garr_sockAll[i],&garr_olpAll[i],&dwTransfer,TRUE,&dwFlagrecvpara5);
//擷取到信号後要重置信号
WSAResetEvent(garr_olpAll[i].hEvent);
if (bret == FALSE)//擷取信号失敗則跳過
{
int a = WSAGetLastError();
if (a ==10054)//代表直接點×關閉視窗
{
printf("用戶端點×關閉下線,數組共有元素");
//關閉用戶端SOCKET和事件句柄
closesocket(garr_sockAll[i]);
WSACloseEvent(garr_olpAll[i].hEvent);
//從數組中删除用戶端SOCKET和事件,這裡思路用數組最後一位替換目前元素
garr_sockAll[i] = garr_sockAll[gi_count-1];
garr_olpAll[i] = garr_olpAll[gi_count-1];
gi_count--;//數組元素個數減一
i--;//循環控制變量i回退一位,重新循環目前替換的新元素
printf("數組共有元素:%d\n",gi_count);
}
continue;
}
//擷取信号成功,則按情況分類處理
//情況1:
//0号位代表伺服器SOCKET,說明接受連接配接完成
//剛連接配接上的用戶端SOCKET在數組的第gi_count位上
if(i == 0)
{
printf("情況1:接受連接配接完成\n");
//執行成功,并連接配接成功
//走流程3.1的兩種情況
//對連接配接上的用戶端send消息
PostSend(gi_count);
//投遞recv
PostRecv(gi_count);
gi_count++;//注意這裡gi_count++的位置
//再次投遞AcceptEx
PostAccept();
continue;//一次處理一個響應,處理完跳出循環後面不用看了
}
//情況2:長度為0表示用戶端下線
if(dwTransfer == 0)
{
printf("情況2:用戶端下線\n");
//關閉用戶端SOCKET和事件句柄
closesocket(garr_sockAll[i]);
WSACloseEvent(garr_olpAll[i].hEvent);
//從數組中删除用戶端SOCKET和事件,這裡思路用數組最後一位替換目前元素
garr_sockAll[i] = garr_sockAll[gi_count-1];
garr_olpAll[i] = garr_olpAll[gi_count-1];
gi_count--;//數組元素個數減一
i--;//循環控制變量i回退一位,重新循環目前替換的新元素
continue;//一次處理一個響應,處理完跳出循環後面不用看了
}
//情況3:
//發送或者接收資料成功
if(dwTransfer != 0)
{
//根據全局變量gc_recvbuff是否為空來判斷是否發送資料
if(gc_recvbuff[0] != 0)//不空說明收到資料,應該是recv
{
printf("情況3:捕獲接收信号from:%d\n",i);
//立即完成,執行成功
//收取資訊後傳回
printf("%s\n",gc_recvbuff);
memset(gc_recvbuff,0,MAX_RECV_LENGTH);//清空buff
//根據情況投遞send
//跳3.1.1繼續投遞Recv
PostRecv(i);
}
else//發送資料
{
printf("捕獲發送信号\n");
//send
PostSend(i);
}
}
}
}
ClearArr();
WSACleanup();
system("pause");
return 0;
}
//投遞AcceptEx
int PostAccept()
{
//用戶端句柄加到數組裡面,注意gi_count++的位置
garr_sockAll[gi_count]=WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,0,WSA_FLAG_OVERLAPPED);
garr_olpAll[gi_count].hEvent = WSACreateEvent();//事件初始化
char str[1024] = {0};
DWORD dwRecvCount = 0;
//AcceptEx涉及的SOCKET句柄和重疊事件結構體都是針對伺服器的
BOOL bRes = AcceptEx(garr_sockAll[0],garr_sockAll[gi_count],str,0,sizeof(struct sockaddr_in)+16,
sizeof(struct sockaddr_in)+16,&dwRecvCount,&garr_olpAll[0]);
printf("PostAccept\n");
if (bRes == TRUE)
{
printf("PostAccept Success\n");
//PostSend(gi_count);
//執行成功,并連接配接成功
//走流程3.1的兩種情況
//投遞recv
PostRecv(gi_count);
gi_count++;//注意這裡gi_count++的位置
//再次投遞AcceptEx
PostAccept();
return 0;
}
else
{
int acceptexerr = WSAGetLastError();
if (acceptexerr == ERROR_IO_PENDING)
{
//延遲處理
return 0;
}
else
{
//出錯處理
printf("PostAccept出錯,錯誤碼是:%d\n",acceptexerr);
return acceptexerr;
}
}
}
//投遞WSASend
//參數socketIndex是目前SOCKET數組下标
int PostSend(int socketIndex)
{
WSABUF wsabuff; //接收資料專用
wsabuff.buf = "這是重疊IO模型伺服器消息~!";
wsabuff.len= MAX_SEND_LENGTH;
DWORD dwSendedLength;
DWORD dwSendFlag=0;//這裡要初始化,否則有錯
int iret = WSASend(garr_sockAll[socketIndex],&wsabuff,1,&dwSendedLength,dwSendFlag,&garr_olpAll[socketIndex],NULL);
printf("PostSendto:%d\n",socketIndex);
if (iret == 0)
{
//立即完成,執行成功
printf("WSASend發送給:%d成功\n",socketIndex);
//memset(gc_recvbuff,0,MAX_RECV_LENGTH);//清空buff
//根據情況投遞send,不需要循環調用
return 0;
}
else
{
int wassenderr = WSAGetLastError();
if (wassenderr == ERROR_IO_PENDING)
{
//延遲處理
return 0;
}
else
{
printf("WSASend發送失敗,錯誤碼是:%d\n",wassenderr);
//出錯處理
return wassenderr;
}
}
}
//投遞WSARecv
//參數socketIndex是目前SOCKET數組下标
int PostRecv(int socketIndex)
{
WSABUF wsabuff; //接收資料專用
wsabuff.buf = gc_recvbuff;
wsabuff.len= MAX_RECV_LENGTH;
DWORD dwRecvedLength;
DWORD dwRecvFlag=0;//這裡要初始化,否則有錯
int iret = WSARecv(garr_sockAll[socketIndex],&wsabuff,1,&dwRecvedLength,&dwRecvFlag,&garr_olpAll[socketIndex],NULL);
printf("PostRecv from id:%d\n",socketIndex);
if (iret == 0)
{
//立即完成,執行成功
//收取資訊後傳回
printf("%s\n",wsabuff.buf);
memset(gc_recvbuff,0,MAX_RECV_LENGTH);//清空buff
//根據情況投遞send
//跳3.1.1繼續投遞Recv
PostRecv(socketIndex);
return 0;
}
else
{
int wasrecverr = WSAGetLastError();
if (wasrecverr == ERROR_IO_PENDING)
{
//延遲處理
return 0;
}
else
{
printf("WSARecv接收失敗,錯誤碼是:%d\n",wasrecverr);
//出錯處理
return wasrecverr;
}
}
}
未完成工作及問題
在上面的代碼中,循環對事件數組進行輪詢時(就是while下面的那個for循環),是一個個對事件數組中的元素進行周遊判斷是否有信号,如果數組元素較多,例如:10000個,那麼會輪詢會造成較大的延遲,這個時候,可以考慮開多線程來進行優化,将10000個元素分十組,每組由一個線程來進行處理。
無論是異步選擇模型還是重疊IO模型,都存在一個相同的問題,就是當用戶端多次向伺服器端發送資料(調用多次send),伺服器會産生多個recv信号,但是在第一次接收消息的時候就會收完所有資料。