《TCP/IP網絡編程》學習筆記 | Chapter 22:重疊 I/O 模型
- 《TCP/IP網絡編程》學習筆記 | Chapter 22:重疊 I/O 模型
- 理解重疊 I/O 模型
- 重疊 I/O
- 本章討論的重疊 I/O 的重點不在于 I/O
- 創建重疊 I/O 套接字
- 執行重疊 I/O 的 WSASend 函數
- 進行重疊 I/O 的 WSARecv 函數
- 重疊 I/O 的 I/O 完成確認
- 使用事件對象
- 使用 Completion Routine 函數
《TCP/IP網絡編程》學習筆記 | Chapter 22:重疊 I/O 模型
理解重疊 I/O 模型
第 21 章異步處理的并非 I/O,而是“通知”。本章講解的才是以異步方式處理 I/O 的方法。
重疊 I/O
同一線程內部向多個目標傳輸數據引起的 I/O 重疊現象稱為“重疊I/O”。為了完成這項任務,調用的 I/O 函數應立即返回,只有這樣才能發送后續數據。從結果來看,利用上述模型收發數據時,最重要的前提條件就是異步 I/O(調用的 I/O 函數應以非阻塞模式工作)。
本章討論的重疊 I/O 的重點不在于 I/O
重疊 I/O 的重點并非 I/O 本身,而是如何確認 I/O 完成時的狀態。
非阻塞模式的輸入輸出需要另外確認執行結果。
Windows 平臺下重疊 I/O 模型由非阻塞異步 I/O 函數和確認 I/O 完成狀態的方法組成。
創建重疊 I/O 套接字
首先要創建適用于重疊I/O的套接字,可以通過如下函數完成:
#include <winsock2.h>SOCKET WSASocket(int af, int type, int protocol, LPWSAPROTOCOL_INFO loProtocolInfo,GROUP g,DWORD dwFlags
);
參數:
- af:協議族信息
- type:套接字數據傳輸方式
- protocol:2 個套接字之間使用的協議信息
- lpProtocolInfo:包含創建的套接字信息的WSAPROTOCOL_INFO結構體變量地址值,不需要時傳遞 NULL。
- g:為擴展函數而預約的參數,可以使用 0
- dwFlags:套接字屬性信息
成功時返回套接字句柄,失敗時返回 INVALID_SOCKET。
各位對前 3 個參數比較熟悉,第四個和第五個參數與目前的工作無關,可以簡單設置為 NULL 和 0。可以向最后一個參數傳遞 WSA_FLAG_OVERLAPPED,賦予創建出的套接字重疊 I/O 特性。
可以通過如下函數調用創建出可以進行重疊 I/O 的非阻塞模式的套接字。
WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
執行重疊 I/O 的 WSASend 函數
創建出具有重疊 I/O 屬性的套接字后,接下來 2 個套接字(服務器端/客戶端之間的)連接過程與一般的套接字連接過程相同,但 I/O 數據時使用的函數不同。
先介紹重疊 I/O 中使用的數據輸出函數:
#include <winsock2.h>int WSASend(SOCKET s,LPWSABUF lpBuffers,DWORD dwBufferCount,LPDWORD lpNumberOfBytesSent,DWORD dwFlags,LPWSAOVERLAPPED lpOverlapped,LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
參數:
- s:套接字句柄,傳遞具有重疊 I/O 屬性的套接字句柄時,以重疊 I/O 模型輸出。
- IpBuffers:WSABUF 結構體變量數組的地址值,WSABUF 中存有待傳輸數據。
- dwBufferCount:第二個參數中數組的長度。
- IpNumberOfBytesSent:用于保存實際發送字節數的變量地址值
- dwFlags:用于便改數據傳輸特性,如傳遞 MSG_OOB 時發送 OOB 模式的數據。
- IpOverlapped:WSAOVERLAPPED 結構體變量的地址值,使用事件對象,用于確認完成數據傳輸。
- IpCompletionRoutine:傳入 Completion Routine 函數的入口地址值,可以通過該函數確認是否完成數據傳輸。
成功時返回 0,失敗時返回 SOCKET_ERROR。
接下來介紹上述函數的第二個結構體參數類型,該結構體中存有待傳輸數據的地址和大小等信息。
typedef struct __WSABUF
{u_long len; // 待傳輸數據的大小char FAR * buf; // 緩沖地址值
} WSABUF, *LPWSABUF;
利用上述函數和結構體,傳輸數據時可以按如下方式編寫代碼:
WSAEVENT event;
WSAOVERLAPPED overlapped;
WSABUF dataBuf;
char buf[BUF_SIZE] = {"待傳輸的數據"};
int revcBytes = 0;
......
event = WSACreateEvent();
memset(&overlapped, 0, sizeof(overlapped));
overlapped.hEvent = event;
dataBuf.len = sizeof(buf);
dataBuf.buf = buf;
WSASend(hSocket, &dataBuf, 1, &recvBytes, 0, &overlapped, NULL);
......
調用 WSASend 函數時將第三個參數設置為 1,因為策二個參數中待傳輸數據的緩沖個數為 1。另外,多余參數均設置為 NULL 或 0,其中需要注意第六個和第七個參數。
第六個參數中的 WSAOVERLAPPED 結構體定義如下:
typedef struct _WSAOVERLAPPED
{ DWORD Internal;DWORD InternalHigh;DWORD Offset;DWORD OffsetHigh;WSAEVENT hEvent;
} WSAOVERLAPPED, *LPWSAOVERLAPPED;
Internal、InternalHigh 成員是進行重疊 I/O 時操作系統內部使用的成員,而 Offset、OffsetHigh 同樣屬于具有特殊用途的成員。所以各位實際只需要關注 hEvent 成員。
關于 WSAOVERLAPPED 結構體有 3 點需要注意:
- 為了進行重疊 I/O,WSASend 函數的 lpOverlapped 參數中應該傳遞有效的結構體變量地址值,而不是 NULL。
- 若向 lpOverlapped 傳遞 NULL,WSASend 函數的第一個參數中的句柄所指的套接字將以阻
塞模式工作。 - 利用 WSASend 函教同時向多個目標傳輸數據時,需要分別構建傳入第六個參數的 WSAOVERLAPPED 結構體變量。這是因為,進行重疊 I/O 的過程中,操作系統將使用 WSAOVERLAPPED 結構體變量。
WSASend 函數調用過程中,函數返回時間點和數據傳輸完成時間點并非總不一致。分為以下兩種情況:
- 如果輸出緩沖是空的,且傳輸的數據并不大,那么函數調用后可以立即完成數據傳輸。此時,WSASend 函數將返回 0,lpNumberOfBytesSent 中將保存實際傳輸的數據大小的信息。
- 反之,WSASend 函數返回后仍需要傳輸數據時,將返回 SOCKET_ERROR,并將 WSA_IO_PENDING 注冊為錯誤代碼,該代碼可以通過 WSAGetLastError 函數(稍后再介紹)得到。這時應該通過如下函效獲取實際傳輸的數據大小。
#include <winsock2.h>BOOL WSAGetOverlappedResult(SOCKET s,LPWSAOVERLAPPED lpOverlapped,LPDWORD lpcbTransfer,BOOL fWait,LPDWORD lpdwFlags
);
參數:
- s:進行重疊 I/O 的套接字句柄。
- IpOverlapped:進行重疊 I/O 時傳遞的 WSAOVERLAPPED 結構體變量的地址值。
- lpcbTransfer:用于保存實際傳輸的字節數的變量地址值。
- fWait:如果調用該函數時仍在進行 I/O,fWait 為 TRUE 時等待 I/O 完成,fWait 為 FALSE 時將返回 FALSE 并跳出函數。
- IpdwFlags:調用 WSARecv 函數時,用于獲取附加信息(例如 OOB 消息)。如果不需要,可以傳遞 NULL。
成功時返回 TRUE,失敗時返回 FALSE。
通過此函數不僅可以獲取數據傳輸結果,還可以驗證接收數據的狀態。
進行重疊 I/O 的 WSARecv 函數
#include <winsock2.h>int WSARecv(SOCKET s,LPWSABUF lpBuffers,DWORD dwBufferCount,LPDWORD lpNumberOfBytesRecvd,LPDWORD lpFlags,LPWSAOVERLAPPED lpOverlapped,LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
參數:
- s:具有重疊 I/O 屬性套接字句柄。
- IpBuffers:用于保存接收數據的 WSABUF 結構體變量數組的地址值。
- dwBufferCount:第二個參數中數組的長度。
- lpNumberOfBytesRecvd:用于保存接收字節數的變量地址值。
- lpFlags:用于設置或讀取傳輸特性信息。
- IpOverlapped:WSAOVERLAPPED 結構體變量地址值。
- IpCompletionRoutine:Completion Routine 函數地址值。
成功時返回 0,失敗時返回 SOCKET_ERROR。
Gather 輸出指將多個緩沖中的數據累積到一定程度后一次性輸出,Scatter 輸入指將接收的數據分批保存。
重疊 I/O 的 WSASend 和 WSARecv 函數可以獲得 writev & readv 函數的 Gather/Scatter I/O 功能。
重疊 I/O 的 I/O 完成確認
重疊 I/O 中有 2 種方法確認 I/O 的完成并獲取結果。
- 利用 WSASend、WSARecv 函數的第六個參數,基于事件對象。
- 利用 WSASend、WSARecv 函數的第七個參數,基于 Completion Routine。
只有理解了這 2 種方法,才能算是掌握了重疊 I/O。首先介紹利用第六個參數的方法。
使用事件對象
直接給出示例。希望各位通過該示例驗證如下 2 點:
- 完成 I/O 時,WSAOVERLAPPED 結構體變量引用的事件對象將變為 signaled 狀態。
- 為了驗證 I/O 的完成和完成結果,需要調用 WSAGetOvrlappedResult 函數。
發送端代碼:
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>void ErrorHandling(char *msg);int main(int argc, char *argv[])
{WSADATA wsaData;SOCKET hSocket;SOCKADDR_IN sendAdr;WSABUF dataBuf;char msg[] = "Network is Computer!";int sendBytes = 0;WSAEVENT evObj;WSAOVERLAPPED overlapped;if (argc != 3){printf("Usage : %s <IP> <port> \n", argv[0]);exit(1);}if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)ErrorHandling("WSAStartup() error");hSocket = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);memset(&sendAdr, 0, sizeof(sendAdr));sendAdr.sin_family = AF_INET;sendAdr.sin_addr.s_addr = inet_addr(argv[1]);sendAdr.sin_port = htons(atoi(argv[2]));if (connect(hSocket, (SOCKADDR *)&sendAdr, sizeof(sendAdr)) == SOCKET_ERROR)ErrorHandling("connect() error");evObj = WSACreateEvent();memset(&overlapped, 0, sizeof(overlapped));overlapped.hEvent = evObj;dataBuf.len = strlen(msg) + 1;dataBuf.buf = msg;if (WSASend(hSocket, &dataBuf, 1, &sendBytes, 0, &overlapped, NULL) == SOCKET_ERROR){if (WSAGetLastError() == WSA_IO_PENDING){puts("Background data send");WSAWaitForMultipleEvents(1, &evObj, TRUE, WSA_INFINITE, FALSE);WSAGetOverlappedResult(hSocket, &overlapped, &sendBytes, FALSE, NULL);}else{ErrorHandling("WSASend() error");}}printf("Send data size: %d \n", sendBytes);WSACloseEvent(evObj);closesocket(hSocket);WSACleanup();return 0;
}void ErrorHandling(char *msg)
{fputs(msg, stderr);fputc('\n', stderr);exit(1);
}
上述示例調用的 WSAGetLastError 函數定義如下。調用套接字相關函數后,可以通過該函數獲取錯誤信息。
#include<winsock2.h>int WSAGetLastError(void); // 返回錯誤代碼(表示錯誤原因)
上述示例中該函數的返回值為 WSA_IO_PENDING,由此可以判斷 WSASend 函數的調用結果并非發生了錯誤,而是尚未完成的狀態。
下面介紹與上述示例配套使用的接收端代碼:
在這里插入代碼片