Winsock网络编程套接字I/O模型之选择模型

Winsock网络编程套接字I/O模型之选择模型

第一章 I/O模型简介与选择模型

Winsock 提供了一些 I/O 模型帮助应用程序以异步方式在一个或者多个套接字上管理 I/O。大体上,这样的 I/O 模型共有 6 种:

  1. 阻塞(blocking)模型
  2. 选择(select)模型
  3. WSAAsyncSelect模型
  4. WSAEventSelect 模型
  5. 重叠(overlapped)模型
  6. 完成端口(completion port)模型

一、引言

Winsock 以两种模式执行 I/O 操作:阻塞和非阻塞。在阻塞模式下,执行 I/O 的 Winsock 调用(如 send 和 recv)一直到操作完成才返回。在非阻塞模式下,Winsock 函数会立即返回。

1. 阻塞模式
套接字创建时,默认工作在阻塞模式下。例如,对 recv 函数的调用会使程序进入等待状态,直到接收到数据才返回。大多数 Winsock 程序设计者都是从阻塞套接字模式开始学习的,因为这是最容易和最直接的方式。处理阻塞模式套接字的应用程序使用的程序框架便是阻塞模型。它的好处是使用简单,但是当需要处理多个套接字连接时,就必须创建多个线程,即典型的一个连接使用一个线程的问题,这给编程带来了许多不便。

2. 非阻塞模式
非阻塞套接字使用起来比较复杂,但是却有许多优点。应用程序可以调用 ioctlsocket 函数显式地让套接字工作在非阻塞模式下,如下代码所示。

u_long ul = 1; 
SOCKET s = socket(AF_INET, SOCK_STREAM, 0); 
ioctlsocket(s, FIONBIO, (u_long *)&ul);

3. 为何要使用I/O模型
一旦套接字被置于非阻塞模式,处理发送和接收数据或者管理连接的 Winsock 调用将会立即返回。大多少情况下,调用失败的出错代码是 WSAEWOULDBLOCK,这意味着请求的操作在调用期间没有完成。例如,如果系统输入缓冲区中没有待处理的数据,那么对 recv 的调用将返回 WSAEWOULDBLOCK。通常,要对相同函数调用多次直到它返回成功为止。非阻塞调用经常以 WSAEWOULDBLOCK 出错代码失败,所以将套接字设置为非阻塞之后,关键的问题在于如何确定套接字什么时候可读/可写,也就是说确定网络事件何时发生。如果需要自己不断调用函数去测试的话,程序的性能势必会受到影响(线程膨胀),解决的办法就是使用Windows 提供的不同的 I/O 模型。

二、选择模型

1. select函数

int select(
  __in     int nfds, // 忽略,仅是为了与 Berkeley 套接字兼容
  __inout  fd_set* readfds, // 指向一个套接字集合,用来检查其可读性
  __inout  fd_set* writefds,  // 指向一个套接字集合,用来检查其可写性
  __inout  fd_set* exceptfds, // 指向一个套接字集合,用来检查错误
  __in     const struct timeval* timeout // 指定此函数等待的最长时间,如果为 NULL,则最长时间为无限大
);

select 函数可以确定一个或者多个套接字的状态。如果套接字上没有网络事件发生,便进入等待状态,以便执行同步 I/O。函数调用成功,返回发生网络事件的所有套接字数量的总和。如果超过了时间限制,返回 0,失败则返回 SOCKET_ERROR(更多信息可自行查看MSDN Library)。

2. 套接字集合fd_set

typedef struct fd_set 
{
	u_int fd_count; // 下面这个数组fd_array的大小
	SOCKET fd_array[FD_SETSIZE]; // 套接字句柄数组
} fd_set;
  • fd_set 结构可以把多个套接字连在一起,形成一个套接字集合。select 函数可以测试这个集合中哪些套接字有事件发生
  • FD_SETSIZE确定了一个集合中套接字的最大数目,默认值是64,可以自行修改,但不要超过1024。FD_SETSIZE 值太大,服务器性能就会受到影响。例如有 1000 个套接字,那么在调用 select 之前就不得不设置这 1000 个套接字,select 返回之后,又必须检查这 1000 个套接字。
  • 在头文件Winsock2.h中定义了4个宏,用于操作和检查套接字集合。fd_set 结构中的套接字句柄并不像Berkeley Unix那样表示为位标志。它们的数据表示是不透明的,使用这些宏可以保持不同套接字环境之间的软件可移植性。用于操作和检查fd_set内容的4个宏如下:
FD_CLR(s, *set)  // 将指定套接字s从集合中移除
FD_ISSET(s, *set) // 如果s是集合的成员,则为非零,否则为零
FD_SET(s, *set) // 添加s到集合中
FD_ZERO(*set) // 将集合初始化为空集

3. 网络事件

传递给 select 函数的 3 个 fd_set 结构中,一个是为了检查可读性(readfds),一个是为了检查可写性(writefds),另一个是为了检查错误(exceptfds)。select 函数返回之后,如果有下列事件发生,其对应的套接字就会被标识。

(1)readfds 集合

  • 数据可读
  • 连接已经关闭、重启或者中断
  • 如果 listen 已经被调用,并且有一个连接未决,accept 函数将成功

(2)writefds 集合

  • 数据能够发送
  • 如果一个非阻塞连接调用正在被处理,连接已经成功

(3)exceptfds集合

  • 如果一个非阻塞连接调用正在被处理,连接试图失败
  • OOB 数据可读

当 select 返回时,它通过移除没有未决 I/O 操作的套接字句柄修改每个 fd_set 集合。例如,想要测试套接字 s 是否可读时,必须将它添加到 readfds 集合,然后等待 select 函数返回。当 select 调用完成后再确定 s 是否仍然还在 readfds 集合中,如果还在,就说明 s 可读了。3个参数中的任意两个都可以是 NULL(至少要有一个不是 NULL),任何不是 NULL 的集合必须至少包含一个套接字句柄。下图示例了使用 select 确定套接字状态的过程。

4. timeval 设置超时

select函数最后的参数 timeout 是 timeval 结构的指针,它指定了 select 函数等待的最长时间。如果设为 NULL,select 将会无限阻塞,直到有网络事件发生。timeval 结构定义如下。

typedef struct timeval {  
	long tv_sec; // 指示等待多少秒
	long tv_usec; // 指示等待多少毫秒
} timeval;

5. 完整代码实现

采用 select 模型之后,即便是在单个线程中,也可以管理多个套接字。流程如下:
(1)初始化套接字集合 fdSocket,向这个集合添加监听套接字句柄。
(2)将 fdSocket 集合的拷贝fdRead传递给 select 函数,当有事件发生时,select 函数移除fdRead集合中没有未决 I/O 操作的套接字句柄,然后返回。
(3)比较原来 fdSocket 集合与 select 处理过的 fdRead 集合,确定哪些套接字有未决 I/O, 并进一步处理这些 I/O。
(4)回到第(2)步继续进行选择处理。

#include "../common/initsock.h"
#include <stdio.h>

CInitSock theSock;		// 初始化Winsock库
int main()
{
	USHORT nPort = 2356;	// 此服务器监听的端口号

	// 创建监听套节字
	SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);	
	sockaddr_in sin;
	sin.sin_family = AF_INET;
	sin.sin_port = htons(nPort);
	sin.sin_addr.S_un.S_addr = INADDR_ANY;
	// 绑定套节字到本地机器
	if(::bind(sListen, (sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR)
	{
		printf(" Failed bind() \n");
		return -1;
	}
	// 进入监听模式
	::listen(sListen, 5);

		// 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);
							printf("接收到连接(%s)\n", ::inet_ntoa(addrRemote.sin_addr));
						}
						else
						{
							printf(" Too much connections! \n");
							continue;
						}
					}
					else
					{
						char szText[256];
						int nRecv = ::recv(fdSocket.fd_array[i], szText, strlen(szText), 0);
						if(nRecv > 0)						// (2)可读
						{
							szText[nRecv] = '\0';
							printf("接收到数据:%s \n", szText);
						}
						else								// (3)连接关闭、重启或者中断
						{
							::closesocket(fdSocket.fd_array[i]);
							FD_CLR(fdSocket.fd_array[i], &fdSocket);
						}
					}
				}
			}
		}
		else
		{
			printf(" Failed select() \n");
			break;
		}
	}
	return 0;
}

参考文献

《Windows 网络与通信程序设计》
MSDN Library

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值