网络编程--select实现IO复用

何为复用

简单来说,复用就是在1个通信频道中传递多个数据的技术。

常见的复用方式有时分复用和频分复用。

时分复用:即在某一时间段内容,只允许传输一个数据。
频分复用:指的是在某一时间段可以传输多个“频率”不同的数据。

之前的回声服务器只能服务一个客户端,本章将使用IO复用技术实现一个服务端向多个客户端提供回声服务。

select函数

select函数是实现IO复用服务器的关键,使用select函数可将多个套接字集中到一起统一监视,监视项目如下:
①是否存在套接字接收数据?
②无需阻塞传输数据的套接字有哪些?
③哪些套接字发生了异常?

原型如下:

#include <winsock2.h>
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* excepfds, const struct timeval* timeout);

nfds: 该参数是为了保持与Linux系统的同名函数兼容而添加的,在windows系统的select函数中无实际意义,使用时传0即可。
readfds:将所有关注“是否存在待读取数据”的文件描述符(套接字)注册到fd_set型变量,并传递其地址值
writefds:将所有关注“是否可传输无阻塞数据”的文件描述符(套接字)注册到fd_set型变量,并传递其地址值
excepfds:将所有关注“是否发生异常”的文件描述符(套接字)注册到fd_set型变量,并传递其地址值
timeout:调用select函数后,为防止陷入无限阻塞状态,传递超时(time-out)信息
返回值:发生错误时返回-1,超时返回0.因发生关注的事件返回时,返回大于0的值,该值是发生事件的文件描述符(套接字)

这里重点关注readfds、writefds、exceptfds三个参数,对应着三个监视项,类型为fd_set,下面将详细介绍该类型如何使用。

fd_set的使用

首先看一下fd_set的定义:

typedef struct fd_set {
        u_int fd_count;               /* how many are SET? */
        SOCKET  fd_array[FD_SETSIZE];   /* an array of SOCKETs */
} fd_set;

可以看到该类型有两个成员,一个是统一监视的套接字数组,一个是监视的套接字的数量。

注意:针对fd_set变量的操作都是以位为单位进行的,因此不能直接将套接字的值直接写的fd_set的fd_array成员中。需要借助相关设置接口完成

fd_set相关设置接口

提供操作fd_set的相关接口如下:

  • FD_ZERO(fd_set* fdset) : 将fd_set变量的所有位初始化为0
  • FD_SET(int fd, fd_set* fdset) : 在fdset指向的变量中注册套接字fd的信息
  • FD_CLR(int fd, fd_set* fdset) : 在参数fdset指向的变量中清除套接字fd的信息
  • FD_ISSET(int fd, fd_set* fdset) : 若参数fdset指向的变量中包含文件描述符fd的信息,则返回TRUE,不包含则返回FALSE

示例如下:
fd_set相关接口示例

timeval结构体

select函数的最后一个参数为timeval结构体,定义如下:

struct timeval {
        long    tv_sec;         /* seconds */
        long    tv_usec;        /* and microseconds */
};

本来select函数只有监视的套接字发送相应事件时才返回,如果未发生变化,就会进入阻塞状态。而该参数就是给开发人员提供设置阻塞事件的机会。

通过声明上述结构体变量,将秒数填入tv_sec 成员, 将微秒填入tv_usec成员,然后将结构体地址传入select函数的最后一个参数。这样,当指定时间内监视的套接字没有变化,select函数也会返回,只不过这时的返回值为0,表示超时

若不行设置超时,则设置该参数为NULL即可。

select返回结果

前面已经提到,调用失败返回-1, 超时返回0,若返回大于0的整数,则说明相应数量的套接字有发生对应监视项目的变化。

PS:上述的套接字发生变化是指监视的套接字发生了相应的监视事件。例如:通过select函数的第二个参数传递的套接字集合中存在需要读数据的套接字时,就意味着这些套接字发送了变化,然后select函数返回发生变化的套接字数量。

变化规则如下:
变化规则
此次,可以知道select函数的使用步骤如下:
①设置套接字
②设置监视范围
③设置超时
④调用select函数
⑤查看调用结果

这里给出一个简单的select使用示例模板:

	fd_set reads, temps;
	FD_ZERO(&reads);

	//0表示标准输入
	SOCKET hSock = socket(PF_INET, SOCK_STREAM, 0);
	FD_SET(hSock , &reads);

	timeval timeout;
	int nSelRet = -1;

	char buf[BUF_SIZE];
	int nRecvLen = 0;
	while (true)
	{
		temps = reads;			//因为每次调用select后,fd_set中所有位都会置0,因此为了下次调用能够正常监视,这里使用拷贝的fd_set来调用select
		timeout.tv_sec = 5;		//设置5s超时
		timeout.tv_usec = 0;
		
		nSelRet = select(0, &temps, nullptr, nullptr, &timeout);	//使用拷贝fd_set调用select,第一个参数无意义传0
		if (nSelRet == -1)
		{
			puts("select() error!");
			break;
		}
		else if (nSelRet == 0)
		{
			puts("time out!");
		}
		else
		{
			for (int i = 0; i < temps.fd_count; i++)
			{
				if (FD_SET(temps.fd_array[i], &temps)
				{
					if (temps.fd_array[i] == hSock)
					{
						//hSock套接字有数据可读取
						
					}
				}
			}
		}
	}

实现IO复位服务器

基于上面的介绍,可以使用select函数实现IO复用服务器。代码如下:

// echo_select_server.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "select.h"

#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")

#define BUF_SIZE 1024

int _tmain(int argc, _TCHAR* argv[])
{
	
	if (argc != 2)
	{
		printf("argc error!\n");
		return -1;
	}

	WSADATA wsaData;
	if (0 != WSAStartup(MAKEWORD(2, 2), &wsaData))
	{
		printf("WSAStartup error!");
		return -1;
	}

	SOCKET srvSock = socket(PF_INET, SOCK_STREAM, 0);
	if (INVALID_SOCKET == srvSock)
	{
		printf("socket error!\n");
		return -1;
	}

	SOCKADDR_IN srvAddr;
	memset(&srvAddr, 0, sizeof(srvAddr));
	srvAddr.sin_family = PF_INET;
	srvAddr.sin_addr.s_addr = htonl(ADDR_ANY);
	srvAddr.sin_port = htons(_ttoi(argv[1]));

	if (SOCKET_ERROR == bind(srvSock, (sockaddr*)&srvAddr, sizeof(srvAddr)))
	{
		printf("bind error!\n");
		return -1;
	}

	if (SOCKET_ERROR == listen(srvSock, 5))
	{
		printf("listen error!\n");
		return -1;
	}

	fd_set reads, temps;
	FD_ZERO(&reads);
	FD_SET(srvSock, &reads);

	timeval timeout;

	int nFDNum;
	
	SOCKADDR_IN cltAddr;
	memset(&cltAddr, 0, sizeof(cltAddr));
	int nCltAddrLen = 0;

	int nRecvLen = 0;
	char Msg[BUF_SIZE];

	while (true)
	{
		temps = reads;
		timeout.tv_sec = 5;
		timeout.tv_usec = 5000;

		if ((nFDNum = select(0, &temps, nullptr, nullptr, &timeout)) == SOCKET_ERROR)
		{
			printf("select error!\n");
			break;
		}

		printf("nFDNum: %d \n", nFDNum);

		if (nFDNum == 0)
		{
			printf("select time out!\n");
			continue;
		}

		//temps只是一个拷贝集合,只有添加或关闭新的套接字时,需对原始reads集合操作,其余都可使用temps完成
		for (int i = 0; i < temps.fd_count; i++)
		{
			//这里为什么要用reads.fd_array[i],而不能用temps.fd_array[i].
			//可以
			if (FD_ISSET(temps.fd_array[i], &temps))
			{
				if (temps.fd_array[i] == srvSock)
				{
					nCltAddrLen = sizeof(cltAddr);
					SOCKET cltSock = accept(srvSock, (sockaddr*)&cltAddr, &nCltAddrLen);

					if (INVALID_SOCKET == cltSock)
					{
						printf("accept error!\n");
						continue;
					}

					FD_SET(cltSock, &reads);
					printf("connected client: %d \n", cltSock);
				}
				else
				{
					//读取客户端发来信息
					//这里为什么要用reads.fd_array[i],而不能用temps.fd_array[i]
					//可用temps
					nRecvLen = recv(temps.fd_array[i], Msg, BUF_SIZE, 0);
					if (nRecvLen == 0)
					{
						//断开连接
						FD_CLR(temps.fd_array[i], &reads);
						closesocket(temps.fd_array[i]);
						printf("closed client: %d \n", temps.fd_array[i]);
					}
					else
					{
						//回发
						//这里为什么要用reads.fd_array[i],而不能用temps.fd_array[i]
						//可用temps
						Msg[nRecvLen] = 0;
						printf("echo to client: %s\n", Msg);
						send(temps.fd_array[i], Msg, nRecvLen, 0);
					}
				}
			}
		}
	}

	closesocket(srvSock);
	WSACleanup();

	fputs("任意键继续...", stdout);
	getchar();

	getchar();
	return 0;
}

这里也在给出客户端代码:

// echo_client.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <stdio.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")

#define BUF_SIZE 1024

int _tmain(int argc, _TCHAR* argv[])
{

	if (argc != 3)
	{
		printf("arg error!");
		return -1;
	}

	WSADATA wsaData;
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		printf("WSAStartup error!");
		return -1;
	}

	SOCKET srvSock = socket(PF_INET, SOCK_STREAM, 0);
	if (INVALID_SOCKET == srvSock)
	{
		printf("socket error!");
		WSACleanup();
		return -1;
	}

	SOCKADDR_IN srvAddr;
	memset(&srvAddr, 0, sizeof(srvAddr));
	srvAddr.sin_family = PF_INET;
	srvAddr.sin_addr.s_addr = inet_addr(argv[1]);
	srvAddr.sin_port = htons(_ttoi(argv[2]));

	if (SOCKET_ERROR == connect(srvSock, (sockaddr*)&srvAddr, sizeof(srvAddr)))
	{
		printf("connect error!");
		closesocket(srvSock);
		WSACleanup();
		return -1;
	}

	char Msg[BUF_SIZE];
	int strLen = 0;
	int sendLen = 0;
	while (true)
	{
		fputs("Input Msg(Q to quit): ", stdout);
		fgets(Msg, BUF_SIZE, stdin);

		if (!strcmp(Msg, "q\n") || !strcmp(Msg, "Q\n"))
		{
			break;
		}

		sendLen = 0;
		sendLen += send(srvSock, Msg, strlen(Msg), 0);

		strLen = 0;
		while (strLen < sendLen)
		{
			int recvLen = recv(srvSock, &Msg[strLen], BUF_SIZE - 1, 0);
			if (recvLen == -1)
			{
				closesocket(srvSock);
				WSACleanup();
				return -1;
			}
			strLen += recvLen;
		}

		Msg[strLen] = 0;

		printf("Msg From Server: %s \n", Msg);
	}

	closesocket(srvSock);
	WSACleanup();

	fputs("任意键继续...", stdout);
	getchar();

	return 0;
}


总结

select函数是实现IO复用服务器的关键,因此需要熟练掌握。这里也总结了select函数的使用步骤及示例模板,后续也可参考在实际开发时使用。

步骤:
①设置套接字
②设置监视范围
③设置超时
④调用select函数
⑤查看调用结果

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
多路 I/O 复用是一种在系统编程中常用的技术,它允许一个进程同时监视多个 I/O 事件,以提高程序的效率和响应能力。在 C 语言中,一个常用的多路 I/O 复用函数是 `select`。 `select` 函数的原型如下: ```c #include <sys/select.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); ``` 参数说明: - `nfds`:监视的文件描述符集合中最大的文件描述符值加1。 - `readfds`:读事件的文件描述符集合。 - `writefds`:写事件的文件描述符集合。 - `exceptfds`:异常事件的文件描述符集合。 - `timeout`:超时时间,如果设置为 NULL,则阻塞直到有事件发生;如果设置为零,立即返回;如果设置为一个指定时间,超过该时间还没有事件发生,则返回。 `select` 函数的工作原理是将进程阻塞,直到监视的文件描述符集合中的任意一个文件描述符就绪(可读、可写或出现异常),或者超过指定的超时时间。 使用 `select` 函数进行多路 I/O 复用的一般步骤如下: 1. 创建并初始化文件描述符集合。 2. 将需要监视的文件描述符添加到相应的集合中。 3. 调用 `select` 函数进行阻塞等待。 4. 检查哪些文件描述符已经就绪。 5. 处理就绪的文件描述符。 需要注意的是,`select` 函数在每次调用时都会修改传入的文件描述符集合,因此在每次调用前需要重新初始化。 除了 `select`,还有其他的多路 I/O 复用函数,如 `poll` 和 `epoll`,它们在不同的操作系统中有不同的实现方式和特性,可以根据具体需求选择合适的函数。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值