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.2
if ((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,如果有一个连接请求到达,
//则接收连接请求把这个新的连接句柄返回给NewConnection
ClientAddrLen = 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来终止Winsock
WSACleanup();
}
上面的关键点在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上不需要收发数据了,关闭这个socket
printf("We are closing the connection.\n");
closesocket(s);
//退出之前调用WSACleanup
WSACleanup();
}
从客户端看来,请求连接和发送数据是没有等待的,也没是没有阻塞。实际上,阻塞只存在于服务端的accept,以及任何一端的recv。
实际上,从上面的服务端和客户端看来,都是非常简单而不实用的。那么,如果需要做一个真实可用的阻塞模式的服务和客户程序,怎么做?答案是利用多线程。