【网络编程】select模型

十、基于I/O模型的网络开发

接着上次的博客继续分享:基于I/O模型的网络开发

10.7 选择模型

10.7.1 基本概念

选择 (select) 模型是一种比较常用的I/O 模型。利用该模型可以使Windows socket应 用 程序同时管理多个套接字。使用select 模型,可以使当执行操作的套接字满足可读可写条件时 给应用程序发送通知。收到这个通知后,应用程序再去调用相应的Windows socket API去执行 函数调用。
select 模型的核心是select 函数。调用select 函数检查当前各个套接字的状态。根据函数 的返回值判断套接字的可读可写性,然后调用相应的Windows Sockets API完成数据的发送、 接收等。

select 模型的原理图如图10-1所示。

在这里插入图片描述

select 模 型 是Windows sockets中常见的I/O 模型,利用select 函数实现I/O 管理。通过对 select函数的调用,应用程序可以判断套接字是否存在数据、能否向该套接字写入数据。比如, 在调用recv 函数之前,先调用select函数,如果系统没有可读数据,select函数就会阻塞在这 里。当系统存在可读或可写数据时,select 函数返回,就可以调用recv 函数接收数据了。

可以看出使用select 模型需要两次调用函数。第一次调用select 函数,第二次调用收发函 数。使用该模式的好处是可以等待多个套接字。

select也有几个缺点:

  • (1)I/O 线程需要不断地轮询套接字集合状态,浪费了大量CPU 资源。
  • (2)不适合管理大量客户端连接。
  • (3)性能比较低下,要进行大量查找和复制。

10.7.2 10.7.2 select函数

select 模型利用select 函数实现I/O 管理。通过对select 函数的调用,应用程序可以判断套 接字是否存在数据、能否向该套接字写入数据。例如,在调用 recv 函数之前,先调用 select 函数,如果系统没有可读数据,那么 select 函数会阻塞在这里。当系统存在可读数据时,select 函数返回,就可以调用recv 函数接收数据了。发送数据的形式也是如此。

select 函数声明如下:

int select(
    Int nfds,                       // 被忽略,传入0即可
    fd set *readfds,                // 可读套接字集合
    fd set *writefds,               // 可写套接字集合
    fd_set *exceptfds,              // 错误套接字集合
    const struct timeval *timeout); // select函数等待时间
  • 其中,参数nfds 被忽略;
  • 参数readfds 为可读性套接字集合指针;
  • 参数writefds 为可写性 套接字集合指针;
  • 参数 exceptfds为检查错误套接字集合指针;
  • 参数timeout表示 select的等待 时间,定义如下:
struct timeval
{
    long tv_sec;  // 秒
    long tv_usec; // 毫秒
};
  • 当 timeval为空指针时,select会一直等待,直到有符合条件的套接字时才返回。

  • 当tv_sec 和 tv_usec 之和为0时,无论是否有符合条件的套接字,select 都会立即返回。

  • 当tv sec 和 tv usec 之和为非0时,如果在等待的时间内有套接字满足条件,那么该函数 将返回符合条件的套接字。

  • 如果在等待的时间内没有套接字满足设置的条件,那么 select会 在 时间用完时返回,并且返回值为0。select函数返回处于就绪态并且已经被包含在fd_set 结构 中的套接字总数,如果超时就返回0。

fd_set结构是一个结构体,声明如下:

typedef struct fd_set
{
    u_int fd_count;
    socket fd_array[FD SETSIZE];
} fd_set;
  • 其 中 ,fd_count 表示该集合套接字数量,最大为64;
  • fd_array 为套接字数组。 我们可以看到,select 函数中需要3个fd_set 结构:
  • readfds: 准备接收数据的套接字集合,即可读性集合。
  • writefds: 准备发送数据的套接字集合,即可写性集合。
  • exceptfds: 出错的套接字集合。

在select函数返回时,会在fd_set结构中填入相应的套接字。

其中,readfds数组将包括满 足以下条件的套接字:

  • (1)有数据可读。此时在此套接字上调用recv, 立即收到对方的数据。
  • (2)连接已经关闭、重设或终止。
  • (3)正在请求建立连接的套接字。此时调用accept 函数会成功。

writefds数组包含满足下列条件的套接字:

  • (1)有数据可以发出。此时在此套接字上调用send, 可以向对方发送数据。
  • (2)调用connect 函数,并连接成功的套接字。

exceptfds 数组将包括满足下列条件的套接字:

  • (1)调用connection 函数,但连接失败的套接字。
  • (2)有带外 (out of band) 数据可读。

当select 函数返回时,它通过移除没有未决I/O 操作的套接字句柄来修改每个fd_set 集 合。(这里解释下未决I/O, 它意思是你没有做出决定的I/O。比如套接字上可以读数据了, 即调用recv 会成功,而你没有在那个socket 上做出recv 调用,那这个socket 就叫作未决I/O 套接字。)

使用select 的好处是程序能够在单个线程内同时处理多个套接字连接,避免了阻塞模式下的线程膨胀问题。

  • 但是,添加到 fd set 结构的套接字数量是有限制的,默认情况下, 最大值是FD SETSIZE, 在 winsock2.h 文件中定义为64。

  • 为了增加套接字数量,应用程序可以将FD_SETSIZE定义为更大的值(这个定义必须在包含winsock2.h 之前出现)。

  • 不过,自定义的值也不能超过Winsock 下层提供者的限制(通常是1024)。

  • 另外,FD_SETSIZE值太 大的话,服务器性能就会受到影响。例如,有1000个套接字,那么在调用select 之前就不得 不设置这1000个套接字,select 返回之后又必须检查这1000个套接字。

10.7.3 实战select 模型

  • 在调用select 函数对套接字进行监视之前,必须将要监视的套接字分配给上述3个数组(即 readfds 、writefds 和 exceptfds) 中的一个。

  • 然后调用select 函数,当select 函数返回时,判断 需要监视的套接字是否还在原来的集合中,就可以知道该集合是否正在发生I/O 操作。比如, 应用程序想要判断某个套接字是否存在可读的数据,需要进行如下步骤:

    • (1)将该套接字加入 readfds 集合。
    • (2) 以readfds作为第二个参数调用select函数。
    • (3) 当select 函数返回时,应用程序判断该套接字是否仍然存在于readfds 集合。
    • (4)如果该套接字存在于readfds 集合,就表明该套接字可读。此时可以调用recv 函数 接收数据;否则,该套接字不可读。
  • 在调用select 函 数 时 ,readfds 、writefds 和 exceptfds 这3个参数至少有一个为非空,并且 在该非空的参数中,必须至少包含一个套接字,否则select 函数将没有任何套接字可以等待。

  • 为了方便使用,Windows sockets 提供了下列宏,用来对fd set 进行一系列操作。使用以 下宏可以使编程工作简化。

    • FD_CLR(s,*set): 从 set 集合中删除s 套接字。
    • FD_ISSET(s,*set): 检 查s 是否为set 集合的成员。
    • FD_SET(s,*set): 将套接字加入到set 集合中。
    • FD_ZERO(*set): 将 set 集合初始化为空集合。

在开发Windows sockets 应用程序时,通过下面的步骤可以完成对套接字的可读写判断:

  • (1)使用FD_ZERO 初始化套接字集合,如“FD_ZERO(&readfds);”。
  • (2)使用FD_SET 将某套接字放到readfds内,如“FD_SET(s,&readfds);”。
  • (3) 以readfds 为第二个参数调用select 函 数 。select 在返回时会返回所有fd_set 集合中

在开发Windows sockets 应用程序时,通过下面的步骤可以完成对套接字的可读写判断:

  • (1)使用FD_ZERO 初始化套接字集合,如“FD_ZERO(&readfds);”。
  • (2)使用FD_SET 将某套接字放到readfds内,如“FD_SET(s,&readfds);”。
  • ( 3 ) 以readfds 为第二个参数调用select 函 数 。select 在返回时会返回所有fd set 集合中套接字的总个数,并对每个集合进行相应的更新。将满足条件的套接字放在相应的集合中。
  • (4)使用FD_ISSET判断s 是否还在某个集合中,如“FD_ISSET(s,&readfds);”。
  • (5)调用相应的Windows socket api函数对某套接字进行操作。

select 返回后会修改每个fd_set 结构。删除不存在的或没有完成I/O 操作的套接字。这也 正是在第四步中可以使用FD _ISSET 来判断一个套接字是否仍在集合中的原因。

下面看一个例子,演示一个服务器程序如何使用select模型管理套接字。

服务端

#define _WINSOCK_DEPRECATED_NO_WARNINGS

#include <iostream>
#include <WinSock2.h>

using namespace std;
#pragma comment(lib, "ws2_32")

int main(int argc, char** argv) {
	WSADATA wsaData;
	WSAStartup(WINSOCK_VERSION, &wsaData);


	USHORT uPort = 6000;
	SOCKET sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (INVALID_SOCKET == sListen)
	{
		cout << "socket error : " << GetLastError() << endl;
		return 0;
	}


	SOCKADDR_IN sin;
	sin.sin_family = AF_INET;
	sin.sin_port = htons(uPort);
	sin.sin_addr.S_un.S_addr = INADDR_ANY;


	if (SOCKET_ERROR == bind(sListen, (PSOCKADDR)&sin, sizeof(sin)))
	{
		cout << "Bind error : " << WSAGetLastError() << endl;
		closesocket(sListen);
		WSACleanup();
		return 0;
	}


	if (SOCKET_ERROR == listen(sListen, 5))
	{
		cout << "listen error : " << WSAGetLastError() << endl;
		closesocket(sListen);
		WSACleanup();
		return 0;
	}


	fd_set fdSocket;
	FD_ZERO(&fdSocket);
	FD_SET(sListen, &fdSocket);


	while (TRUE)
	{
		fd_set fdRead = fdSocket;
		int iRet = select(0, &fdRead, NULL, NULL, NULL);
		if (iRet > 0)
		{
			for (size_t i = 0; i < fdSocket.fd_count; i++)
			{
				if (FD_ISSET(fdSocket.fd_array[i], &fdRead))
				{
					if (fdSocket.fd_array[i] == sListen)
					{
						if (fdSocket.fd_count < FD_SETSIZE)
						{
							SOCKADDR_IN addrRemote;
							int iAddrLen = sizeof(addrRemote);
							SOCKET sNew = accept(sListen, (PSOCKADDR)&addrRemote, &iAddrLen);
							FD_SET(sNew, &fdSocket);
							cout << "接收到连接(" << inet_ntoa(addrRemote.sin_addr) << ")" << endl;
						}
						else
						{
							cout << "连接太多!" << endl;
							continue;
						}
					}
					else
					{
						char szText[256];
						int iRecv = recv(fdSocket.fd_array[i], szText, strlen(szText), 0);
						if (iRecv > 0)
						{
							szText[iRecv] = '\0';
							cout << "接收到数据:" << szText << endl;
						}
						else
						{
							closesocket(fdSocket.fd_array[i]);
							FD_CLR(fdSocket.fd_array[i], &fdSocket);
						}
					}
				}
			}
		}
		else
		{
			cout << "select error : " << WSAGetLastError() << endl;
			closesocket(sListen);
			WSACleanup();
			break;
		}
	}


	shutdown(sListen, SD_RECEIVE);
	WSACleanup();


	return 0;
}

客户端

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<stdlib.h>
#include<WINSOCK2.H>
#include <windows.h> 
#include <process.h>  

#include<iostream>
#include<string>
using namespace std;

#define BUF_SIZE 64
#pragma comment(lib,"WS2_32.lib")

int main(){
	WSADATA wsd;
	SOCKET sHost;
	SOCKADDR_IN servAddr;//服务器地址
	int retVal;//调用Socket函数的返回值
	char buf[BUF_SIZE];
	//初始化Socket环境
	if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0)
	{
		printf("WSAStartup failed!\n");
		return -1;
	}
	sHost = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	//设置服务器Socket地址
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	//在实际应用中,建议将服务器的IP地址和端口号保存在配置文件中
	servAddr.sin_port = htons(6000);
	//计算地址的长度
	int sServerAddlen = sizeof(servAddr);



	//调用ioctlsocket()将其设置为非阻塞模式
	int iMode = 1;
	retVal = ioctlsocket(sHost, FIONBIO, (u_long FAR*) & iMode);


	if (retVal == SOCKET_ERROR)
	{
		printf("ioctlsocket failed!");
		WSACleanup();
		return -1;
	}

	printf("client is running....\n");
	//循环等待
	while (true)
	{
		//连接到服务器
		retVal = connect(sHost, (LPSOCKADDR)&servAddr, sizeof(servAddr));
		if (SOCKET_ERROR == retVal)
		{
			int err = WSAGetLastError();
			//无法立即完成非阻塞Socket上的操作
			if (err == WSAEWOULDBLOCK || err == WSAEINVAL)
			{
				Sleep(1);
				printf("check  connect!\n");
				continue;
			}
			else if (err == WSAEISCONN)//已建立连接
			{
				break;
			}
			else
			{
				printf("connection failed!\n");
				closesocket(sHost);
				WSACleanup();
				return -1;
			}
		}
	}

	while (true)
	{
		//向服务器发送字符串,并显示反馈信息
		printf("\ninput a string to send:\n");
		std::string str;
		//接收输入的数据
		std::cin >> str;
		//将用户输入的数据复制到buf中
		ZeroMemory(buf, BUF_SIZE);
		strcpy_s(buf, str.c_str());
		if (strcmp(buf, "quit") == 0)
		{
			printf("quit!\n");
			break;
		}

		while (true)
		{
			retVal = send(sHost, buf, strlen(buf), 0);
			if (SOCKET_ERROR == retVal)
			{
				int err = WSAGetLastError();
				if (err == WSAEWOULDBLOCK)
				{
					//无法立即完成非阻塞Socket上的操作
					Sleep(5);
					continue;
				}

				else
				{
					printf("send failed!\n");
					closesocket(sHost);
					WSACleanup();
					return -1;
				}
			}
			break;
		}
	}

	return 0;
}

前面提到,在select函数返回时会在fd set结构中填入相应的套接字。其中,readfds数组将包括满足以下条 件的套接字:

  • 有数据可读。此时在此套接字上调用recv, 立即收到对方的数据。
  • 连接已经关闭、重设或终止。
  • 正在请求建立连接的套接字,此时调用accept函数会成功。

我们把监听套接字sListen 放到fdSocket 集合中,但然后阻塞在select 函数,当有请求连 接的时候,select 函数返回,然后调用accept 接受连接,并把客户套接字放到fdSocket 集合中。 以后select 再次返回的时候,可能是有数据要接收了,我们通过下列判断来确定是有连接请求 还是有数据可读。如果数据可读,就调用recv 接收数据,并打印出来。

客户端与服务端通信如下

在这里插入图片描述

参考书籍:《Visual C++2017 网络编程实战》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值