Winsock 可以在阻塞和非阻塞模式下执行 I/O 操作,套接字创建时默认工作在阻塞模式下。也就是说当某个操作不能执行时,程序会先阻塞,等待操作可以被执行时才继续程序。例如对 recv 函数的调用会使程序进入等待状态,直到接收到数据才返回。
阻塞套接字的好处是使用简单,但是当需要处理多个套接字连接时,就必须创建多个线程,给编程带来了许多不便。所以实际开发中使用最多的还是非阻塞模式,它使用起来比较复杂,但是处理发送和接收数据或者管理连接的 Winsock 调用将会立即返回,效率很高。
不过如果系统输入缓冲区中没有待处理的数据,那么对 recv 的调用将返回 WSAEWOULDBLOCK 错误。关键的问题在于如何确定套接字什么时候可读/可写,如果需要不断调用函数去测试的话,程序的性能势必会受到影响,解决的办法就是使用 Windows 提供的不同的 I/O 模型。
select 模型的设计源于 UNIX 系统,主要实现的原理是 IO 多路复用。select 模型的优势是程序能够在单个线程内同时处理多个套接字连接,这避免了阻塞模式下的线程膨胀问题。但是添加到 fd_set 结构的套接字数量是有限制的,如果能能添加的 socket 太多的话,服务器性能就会受到影响。
模型通过使用 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 将会无限阻塞。
注意无论是客户端还是服务器,都需要包含头文件 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