天天看點

WINSOCK.053.重疊IO模型重疊IO模型介紹重疊IO模型代碼邏輯重疊IO模型代碼實作整體代碼未完成工作及問題

文章目錄

  • 重疊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信号,但是在第一次接收消息的時候就會收完所有資料。

繼續閱讀