非阻塞模式
Winsock 可以在阻塞和非阻塞模式下执行 I/O 操作,套接字创建时默认工作在阻塞模式下。也就是说当某个操作不能执行时,程序会先阻塞,等待操作可以被执行时才继续程序。例如对 recv 函数的调用会使程序进入等待状态,直到接收到数据才返回。
阻塞套接字的好处是使用简单,但是当需要处理多个套接字连接时,就必须创建多个线程,给编程带来了许多不便。所以实际开发中使用最多的还是非阻塞模式,它使用起来比较复杂,但是处理发送和接收数据或者管理连接的 Winsock 调用将会立即返回,效率很高。
不过如果系统输入缓冲区中没有待处理的数据,那么对 recv 的调用将返回 WSAEWOULDBLOCK 错误。关键的问题在于如何确定套接字什么时候可读/可写,如果需要不断调用函数去测试的话,程序的性能势必会受到影响,解决的办法就是使用 Windows 提供的不同的 I/O 模型。
Select 模型
select 模型的设计源于 UNIX 系统,主要实现的原理是 IO 多路复用。select 模型的优势是程序能够在单个线程内同时处理多个套接字连接,这避免了阻塞模式下的线程膨胀问题。但是添加到 fd_set 结构的套接字数量是有限制的,如果能能添加的 socket 太多的话,服务器性能就会受到影响。
select 函数
模型通过使用 select 函数来管理 I/O,函数可以确定一个或者多个套接字的状态。如果套接字上没有网络事件发生,便进入等待状态,以便执行同步 I/O。
int
WSAAPI
select(
_In_ int nfds,
_Inout_opt_ fd_set FAR * readfds,
_Inout_opt_ fd_set FAR * writefds,
_Inout_opt_ fd_set FAR * exceptfds,
_In_opt_ const struct timeval FAR * timeout
);
函数调用成功返回发生网络事件的所有 socket 数量的综合,超过时间限制就返回 0.
参数 | 说明 |
---|---|
nfds | 忽略,为了与 Berkeley 套接字兼容 |
readfds | 指向一个套接字集合,用来检查其可读性 |
writefds | 指向一个套接字集合,用来检查其可写性 |
exceptfds | 指向一个套接字集合,用来检查错误 |
timeout | 指定此函数等待的最长时间,为 NULL 时最长时间为无限大 |
套接字集合
fd_set 结构是 socket 集合,它可以把多个套接字连在一起,select 函数可以测试这个集合中哪些套接字有事件发生。
typedef struct fd_set {
u_int fd_count; /* how many are SET? */
SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */
} fd_set;
WINSOCK 定义了 4 个操作 fd_set 的宏。
宏 | 功能 |
---|---|
FD_ZERO(*set) | 初始化 set 为空集合,集合在使用前应该总是清空 |
FD_CLR(s, *set) | 从 set 移除套接字 s |
FD_ISSET(s, *set) | 检查 s 是不是 set 的成员,如果是返回 TRUE |
FD_SET(s, *set) | 添加套接字到集合 |
网络事件
传递给 select 函数的 3 个 fd_set 结构分别用于为了检查可读性(readfds)、检查可写性(writefds)和检查错误(exceptfds)。当我们想要测试某个 socket 的某种状态是,就把它放入对应的 fd_set 中,等待 select 函数返回。select 函数调用完成后,若 socket 还在 fd_set 中,就说明该 socket 满足可读、可写或者出错了。
设置超时
timeout 是 timeval 结构的指针,它指定了 select 函数等待的最长时间。
/*
* Structure used in select() call, taken from the BSD file sys/time.h.
*/
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* and microseconds */
};
参数 | 说明 |
---|---|
tv_sec | 等待多少秒 |
tv_usec | 等待多少毫秒 |
如果 timeout 设为 NULL,select 将会无限阻塞。
Select 模型样例
注意无论是客户端还是服务器,都需要包含头文件 initsock.h 来载入 Winsock。
功能设计
模拟实现 TCP 协议通信过程,要求编程实现服务器端与客户端之间双向数据传递。也就是在一条 TCP 连接中,客户端和服务器相互发送一条数据即可。
服务器
使用 Select 模型实现的服务器需要按照如图所示的步骤进行编程,具体编码如下所示。
#include "initsock.h"
#include <iostream>
using namespace std;
CInitSock theSock; // 初始化Winsock库
int main()
{
// 创建监听套接字
SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(4567);
sin.sin_addr.S_un.S_addr = INADDR_ANY;
// 绑定套接字到本地机器
if (::bind(sListen, (sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR)
{
cout << " Failed bind()" << endl;
return -1;
}
// 进入监听模式
if (::listen(sListen, 5) == SOCKET_ERROR)
{
cout << " Failed listen()" << endl;
return 0;
}
cout << "服务器已启动监听,可以接收连接!" << endl;
// select模型处理过程
// 1)初始化一个套接字集合fdSocket,添加监听套接字句柄到这个集合
fd_set fdSocket; // 所有可用套接字集合
FD_ZERO(&fdSocket);
FD_SET(sListen, &fdSocket);
while (TRUE)
{
// 2)将fdSocket集合的一个拷贝fdRead传递给select函数,
// 当有事件发生时,select函数移除fdRead集合中没有未决I/O操作的套接字句柄,然后返回。
fd_set fdRead = fdSocket;
int nRet = ::select(0, &fdRead, NULL, NULL, NULL);
if (nRet > 0)
{
// 3)通过将原来fdSocket集合与select处理过的fdRead集合比较,
// 确定都有哪些套接字有未决I/O,并进一步处理这些I/O。
for (int i = 0; i < (int)fdSocket.fd_count; i++)
{
if (FD_ISSET(fdSocket.fd_array[i], &fdRead))
{
if (fdSocket.fd_array[i] == sListen) // (1)监听套接字接收到新连接
{
if (fdSocket.fd_count < FD_SETSIZE)
{
sockaddr_in addrRemote;
int nAddrLen = sizeof(addrRemote);
//接收客户端的连接请求
SOCKET sNew = ::accept(sListen, (SOCKADDR*)&addrRemote, &nAddrLen);
FD_SET(sNew, &fdSocket);
cout << "\n与主机" << ::inet_ntoa(addrRemote.sin_addr) << "建立连接" << endl;
}
else
{
cout << " Too much connections!" << endl;
continue;
}
}
else
{
char szText[256];
int nRecv = ::recv(fdSocket.fd_array[i], szText, strlen(szText), 0);
if (nRecv > 0) // (2)可读
{
//接收数据
szText[nRecv] = '\0';
cout << " 接收到数据:" << szText << endl;
//发送数据
char result[20];
char sendText[] = "你好,客户端!";
if(::send(fdSocket.fd_array[i], sendText, strlen(sendText), 0) > 0)
{
cout << " 向客户端发送数据:" << sendText << endl;
}
}
else // (3)连接关闭、重启或者中断
{
::closesocket(fdSocket.fd_array[i]);
FD_CLR(fdSocket.fd_array[i], &fdSocket);
}
}
}
}
}
else
{
cout << " Failed select()" << endl;
break;
}
}
return 0;
}
客户端
#include "InitSock.h"
#include <iostream>
using namespace std;
CInitSock initSock; // 初始化Winsock库
int main()
{
// 创建套节字
SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (s == INVALID_SOCKET)
{
cout << " Failed socket()" << endl;
return 0;
}
// 也可以在这里调用bind函数绑定一个本地地址
// 否则系统将会自动安排
char address[20] = "127.0.0.1";
// 填写远程地址信息
sockaddr_in servAddr;
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(4567);
// 注意,这里要填写服务器程序(TCPServer程序)所在机器的IP地址
// 如果你的计算机没有联网,直接使用127.0.0.1即可
servAddr.sin_addr.S_un.S_addr = inet_addr(address);
if (::connect(s, (sockaddr*)&servAddr, sizeof(servAddr)) == -1)
{
cout << " Failed connect() " << endl;
return 0;
}
else
{
cout << "与服务器 " << address << "建立连接" << endl;
}
char szText[] = "你好,服务器!";
if (::send(s, szText, strlen(szText), 0) > 0)
{
cout << " 发送数据:" << szText << endl;
}
// 接收数据
char buff[256];
int nRecv = ::recv(s, buff, 256, 0);
if (nRecv > 0)
{
buff[nRecv] = '\0';
cout << " 接收到数据:" << buff << endl;
}
// 关闭套节字
::closesocket(s);
return 0;
}
运行效果
参考资料
《Windows 网络与通信编程》,陈香凝 王烨阳 陈婷婷 张铮 编著,人民邮电出版社
UNIX再学习 -- 函数 select、poll、epoll