Windows通訊端可以在兩種模式下執行I/O操作:阻塞模式和非阻塞模式。在阻塞模式下,I/O操作完成前,執行操作的Winsock調用(例如send和recv)會一直等候下雲,不會立即返回到程式中。
我們現在就來研究一下阻塞模式是怎樣工作的。
阻塞模式的socket都遵照一種“生產者-消費者“模型來編製。下面先以一個最簡單的server-client程式碼範例:
#include <winsock2.h>#include <stdio.h>#pragma comment(lib,"WS2_32.lib")void main(void){WSADATA wsaData;SOCKET ListeningSocket;SOCKET NewConnection;SOCKADDR_IN ServerAddr;SOCKADDR_IN ClientAddr;int ClientAddrLen;int Port = 5150;int Ret;char DataBuffer[1024];// 初始化 Winsock version 2.2if ((Ret = WSAStartup(MAKEWORD(2,2), &wsaData)) != 0){//注意,如果這裡調用失敗了,我們不可以像處理其他錯誤一樣使用WSAGetLastError來得到錯誤碼。//因為Winsock沒有啟動成功,就不可以使用Winsock相關函數來處理問題。我們只能報告這個錯誤狀態,//接著退出。printf("WSAStartup failed with error %d\n", Ret);return;}//建立一個新的socket來監聽客戶的串連請求,AF_INET指明使用IPV4協議//SOCK_STREAM指明基於流來傳送而不是資料報//IPPROTO_TCP指明使用使用TCP協議if ((ListeningSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == INVALID_SOCKET){printf("socket failed with error %d\n", WSAGetLastError());WSACleanup();return;}//建立一個SOCKADDR_IN結構以供bind綁定使用ServerAddr.sin_family = AF_INET;//使用IPV4協議ServerAddr.sin_port = htons(Port);//轉換成網路位元組序 ServerAddr.sin_addr.s_addr = htonl(INADDR_ANY);//接收任何IP的串連請求// 將地址資訊與socket綁定起來if (bind(ListeningSocket, (SOCKADDR *)&ServerAddr, sizeof(ServerAddr)) == SOCKET_ERROR){printf("bind failed with error %d\n", WSAGetLastError());closesocket(ListeningSocket);WSACleanup();return;}//設定ListeningSocket為監聽狀態,設定最多儲存5個未處理的串連請求if (listen(ListeningSocket, 5) == SOCKET_ERROR){printf("listen failed with error %d\n", WSAGetLastError());closesocket(ListeningSocket);WSACleanup();return;} printf("We are awaiting a connection on port %d.\n", Port);// accept建立會一個新的socket,如果有一個串連請求到達,//則接收串連請求把這個新的串連控制代碼返回給NewConnectionClientAddrLen = sizeof(SOCKADDR);if ((NewConnection = accept(ListeningSocket, (SOCKADDR *) &ClientAddr,&ClientAddrLen)) == INVALID_SOCKET){printf("accept failed with error %d\n", WSAGetLastError());closesocket(ListeningSocket);WSACleanup();return;}printf("We successfully got a connection from %s:%d.\n",inet_ntoa(ClientAddr.sin_addr), ntohs(ClientAddr.sin_port));//這個時候可以回頭再監聽其他串連請求,也可以處理NewConnection這個串連上的//收發訊息事務,為示範的簡單起見,我們不回頭監聽其他串連,在這裡關閉這個監聽,//然後直接處理收發訊息的事件。closesocket(ListeningSocket);//為了簡單起見,這裡只是簡單的接收NewConnection這個已經串連的客戶的一個訊息//然後輸出接收到的內容和長度。printf("We are waiting to receive data...\n");if ((Ret = recv(NewConnection, DataBuffer, sizeof(DataBuffer), 0)) == SOCKET_ERROR){printf("recv failed with error %d\n", WSAGetLastError());closesocket(NewConnection);WSACleanup();return;} printf("We successfully received %s %d byte(s).\n", DataBuffer,uRet); //這是一個簡單樣本,我們不打算做太多的處理,現在可以關閉NewConnection串連了。printf("We are now going to close the client connection.\n");closesocket(NewConnection);//最後要記得調用WSACleanup來終止WinsockWSACleanup();}
上面的關鍵點在accept函數調用的時候,如果當時沒有串連請求到達,會形成阻塞,使程式進入等待狀態。另外recv函數如果沒有收到客戶的資料,也會進入阻塞狀態。其他部分在代碼注釋已經解析的很清楚。
下面再看一下配套的用戶端,代碼相對較簡單:
//使用方式:本程式名 服務端IP 如:tcpclient.exe 192.168.0.213#include <winsock2.h>#include <stdio.h>#pragma comment(lib,"WS2_32.lib")void main(int argc, char **argv){WSADATA wsaData;SOCKET s;SOCKADDR_IN ServerAddr;int Port = 5150;int Ret;if (argc <= 1){printf("USAGE: tcpclient <Server IP address>.\n");return;}if ((Ret = WSAStartup(MAKEWORD(2,2), &wsaData)) != 0){printf("WSAStartup failed with error %d\n", Ret);return;}if ((s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP))== INVALID_SOCKET){printf("socket failed with error %d\n", WSAGetLastError());WSACleanup();return;}ServerAddr.sin_family = AF_INET;ServerAddr.sin_port = htons(Port); ServerAddr.sin_addr.s_addr = inet_addr(argv[1]);//以第一個參數作為服務端IP// 使用s請求串連printf("We are trying to connect to %s:%d...\n",inet_ntoa(ServerAddr.sin_addr), htons(ServerAddr.sin_port));if (connect(s, (SOCKADDR *) &ServerAddr, sizeof(ServerAddr)) == SOCKET_ERROR){printf("connect failed with error %d\n", WSAGetLastError());closesocket(s);WSACleanup();return;} printf("Our connection succeeded.\n");//在串連已經完成的情況下,我們可以接收和發送資料,這裡為了簡單示範//只做發送資料printf("We will now try to send a hello message.\n");if ((Ret = send(s, "Hello", 5, 0)) == SOCKET_ERROR){printf("send failed with error %d\n", WSAGetLastError());closesocket(s);WSACleanup();return;}printf("We successfully sent %d byte(s).\n", Ret);// 當串連的socket上不需要收發資料了,關閉這個socketprintf("We are closing the connection.\n");closesocket(s);//退出之前調用WSACleanupWSACleanup();}
從用戶端看來,請求串連和發送資料是沒有等待的,也沒是沒有阻塞。實際上,阻塞只存在於服務端的accept,以及任何一端的recv。
實際上,從上面的服務端和用戶端看來,都是非常簡單而不實用的。那麼,如果需要做一個真實可用的阻塞模式的服務和客戶程式,怎麼做?答案是利用多線程。