Winsock 套接字模式
阻塞模式
在阻塞模式下,I/O操作完成之前,Winsock调用会一直等待,不会立即返回。
Winsock应用程序通常是遵照“生产者-消费者”模型,应用程序需要读取(或写入)指定数量的字节,然后以该数据为基础执行计算。
SOCKET sock; char buff [256]; int done = 0; … while (!done) { nBytes = recv(sock, buff, 65); if (nBytes == SOCKET_ERROR) { printf(“recv failed with error %d \n”, WSAGetLastError()); return; } DoComputationOnData(buff); } …
如果输入缓冲区没有数据可读,recv将一直阻塞。可以调用ioctlsocket(设置FIONREAD选项),事先查看缓冲区中是否存在必要的字节数量。不过这将造成系统开销,并不是一种好的编程习惯,应尽力避免。
如果上述代码放在主线程中,则recv可能会阻塞主线程。比较好的办法是创建一个Read线程和一个Process线程,两个线程共享同一数据缓冲区(使用同步对象进行同步)。Read线程负责读入数据并置入缓冲区,当读入Process线程所需的最小数据量之后,触发一个事件,通知Process线程进行处理,处理完之后删除这些数据。
#define MAX_BUFFER_SIZE 4096 // 执行初始化,并在创建两个线程之前,创建一个自动重置的事件(hEvent) CRITICAL_SECTION data; HANDLE hEvent; SOCKET sock; TCHAR buff[MAX_BUFFER_SIZE]; int done = 0; // 创建并连接套接字 … void ReadThread() { int nTotal = 0, nRead = 0, nLeft = 0, nBytes = 0; while (!done) { nTotal = 0; nLeft = NUM_BYTES_REQUIRED; while (nTotal != NUM_BYTES_REQUIRED) { EnterCriticalSection(&data); nRead = recv(sock, &(buff[MAX_BUFFER_SIZE – nBytes]), nLeft, 0); if (nRead == -1) { printf(“error \n”); ExitThread(); } nTotal += nRead; nLeft -= nRead; nBytes += nRead; LeaveCriticalSection(&data); } SetEvent(hEvent); } } void ProcessThread() { WaitForSingleObject(hEvent); EnterCriticalSection(&data); DoSOmeComputationOnData(buff); // 从缓冲区中删除已处理过的数据,并将余下数据移到数组起始处 nBytes -= NUM_BYTES_REUERED; LeaveCriticalSection(&data); }
非阻塞模式
在非阻塞模式下,Winsock函数无论如何都会立即返回。它在功能上比较强大,但用起来会存在一些难度。下面代码创建一个套接字,并置为非阻塞模式。
SOCKET sock; unsigned long ul = 1; int nRet; sock = socket(AF_INET, SOCK_STREAM, 0); nRet = ioctlsocket(sock, FIONBIO, (unsigned long *)&ul); if (nRet == SOCKET_ERROR) { // 未能将套接字置入非阻塞模式 }
在非阻塞模式下,Winsock API会立即返回,但大多数情况下会返回一个WSAEWOULDBLOCK错误,意味着操作在调用期间没有足够时间来完成。通常需要重复调用同一个函数,直到获得成功,但这种做法并没有什么优势可言,所以Winsock的套接字I/O模型可帮助应用程序判断一个套接字何时可供读写。
阻塞套接字容易使用,但在建立多个套接字时,或在数据的收发量不均、时间不定时,极难管理。非阻塞套接字需要编写更多的代码,在任何时候都需仔细检查所有Winsock API的返回代码,对可能的WSAEWOULDBLOCK错误加以处理,所以有些难以操作。在这种情况下,可考虑使用套接字I/O模型,它将帮助应用程序通过一种异步方式,同时对一个或多个套接字上进行的通信加以管理。
【Windows网络编程 – 读书笔记】