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

news2024/11/23 11:34:51

何为复用

简单来说,复用就是在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函数
⑤查看调用结果

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/539964.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

朱金宝:数据治理产品发展趋势及Datablau产品最新动态

在刚刚结束的2023数据治理新实践峰会上&#xff0c;Datablau数语科技联合创始人&CTO朱金宝先生分享了对数据治理产品发展趋势的深度思考及Datablau新产品预览&#xff0c;并在现场发布了两款最新工具。 以下是朱金宝先生的演讲实录&#xff0c;为了方便阅读&#xff0c;小…

商家中心之java商城 java电子商务Spring Cloud+Spring Boot+mybatis+MQ+VR全景+b2b2c

1. 涉及平台 平台管理、商家端&#xff08;PC端、手机端&#xff09;、买家平台&#xff08;H5/公众号、小程序、APP端&#xff08;IOS/Android&#xff09;、微服务平台&#xff08;业务服务&#xff09; 2. 核心架构 Spring Cloud、Spring Boot、Mybatis、Redis 3. 前端框架…

字符串String

目录 String 字符串比较 charAt 取数组中的值 案例 substring 案例&#xff1a;手机号加密​ StringBuilder StringBuilder提高效率原理图 StringJoiner 字符串拼接底层方法 String 创造方法 直接赋值通过new关键字 / 构造方法 字符串比较 equals &#xff1a;要求比…

chatgpt赋能Python-pycharm滚轮调大小

PyCharm使用技巧&#xff1a;滚轮调整代码大小提高工作效率 介绍 PyCharm是目前最受欢迎的Python开发IDE之一。然而&#xff0c;即使在使用PyCharm多年的开发者中&#xff0c;很多人都不知道如何使用滚轮来调整代码显示的大小。这在工作中可能会导致眼睛疲劳&#xff0c;降低…

Mysql案例

Mysql案例 1.分组查询排名优先的数据1.1 分组获取最新一条记录1.2 分组获取最新的两条数据 1.分组查询排名优先的数据 -- ---------------------------- -- Table structure for user -- ---------------------------- DROP TABLE IF EXISTS user; CREATE TABLE user (id int(…

力扣82删除排序链表中的重复元素 II:思路分析+代码实现+方法总结(三指针法快慢指针法【双指针】递归法)

文章目录 第一部分&#xff1a;题目描述第二部分&#xff1a;代码实现2.1 三指针法2.2 快慢指针法&#xff08;双指针&#xff09;2.3 递归 第一部分&#xff1a;题目描述 &#x1f3e0; 链接&#xff1a;82. 删除排序链表中的重复元素 II - 力扣&#xff08;LeetCode&#xf…

简单快速的在openEuler22.03上部署openGauss2.10

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、安装openEuler1.创建虚拟机2.安装openEuler系统 二、安装openGauss1.设置openGauss2.创建数据库用户并设置权限3.设置防火墙4.远程连接openGauss 总结 前言…

PyQt5桌面应用开发(15):界面动画

本文目录 PyQt5桌面应用系列界面动画PyQt5的动画框架QPropertyAnimationQAnimationGrouppyqtProperty与插值 一个例子代码代码解析 结论 PyQt5桌面应用系列 PyQt5桌面应用开发&#xff08;1&#xff09;&#xff1a;需求分析 PyQt5桌面应用开发&#xff08;2&#xff09;&#…

【手机建站】安卓Termux+cpolar内网穿透,搭建外网可以访问的网站

文章目录 概述1.搭建apache2.安装cpolar内网穿透3.公网访问配置4.固定公网地址5.添加站点 概述 Termux是一个Android终端仿真应用程序&#xff0c;用于在 Android 手机上搭建一个完整的Linux 环境&#xff0c;能够实现Linux下的许多基本操作&#xff0c;不需要root权限Termux就…

电脑格式化后数据恢复软件EasyRecovery16

EasyRecovery是一款由Kroll Ontrack公司开发的专业数据恢复软件&#xff0c;旨在帮助用户从各种数据丢失情况下恢复文件。无论是因为误删除、格式化、分区丢失、系统崩溃还是其他原因导致的数据丢失&#xff0c;EasyRecovery都具有强大的恢复功能。 EasyRecovery提供了多种恢复…

什么是 Git 的 cherry-pick?

官方解析 Git 的 cherry-pick 是一种将指定的提交&#xff08;commit&#xff09;应用到当前分支的操作。它可以帮助我们将某个分支上的某次提交复制到另一个分支上&#xff0c;而无需将整个分支合并过来。 通常&#xff0c;我们在使用 Git 进行版本控制时&#xff0c;会在不…

JAVA-Activiti 7与达梦、人大金仓兼容-修改源码包(1)

目录 第一步,下载源码包 第二步,修改源码内容 1.1进行部分源码包修改 1.1.1 在org.activiti.engine.impl.cfg.ProcessEngineConfigurationImpl&#xff0c;增加成员变量。 1.1.2 修改org.activiti.engine.impl.cfg.ProcessEngineConfigurationImpl 类的getDefaultDatabase…

UML类图画法及其关系

UML类图画法及其关系 本文主要是介绍 UML类图画法及其关系&#xff0c;方便今后温习&#xff01;&#xff01;&#xff01; 一、类之间的关系汇总 泛化&#xff08;Generalization&#xff09;实现&#xff08;Realization&#xff09;关联&#xff08;Association&#xff…

基于springboot+jsp的校园音乐网站系统

该系统能完成的主要功能分为管理员和用户两个用户角色。主要功能包括主页、个人中心、用户管理、校园歌手管理、明星歌手管理、歌曲类别管理、校园歌曲管理、歌曲MV管理、歌手歌曲管理、系统管理等。而用户登入系统也可以对自己的信息以及修改个人资料进行查看等功能。该系统在…

YOLO-NAS C++部署 2023.5.17

这不最近新出了网络&#xff0c;YOLO-NAS&#xff0c;听过性能和速度都不错&#xff0c;而且int8量化后效果也不错。 一、吐槽 但是我打开该项目阅读readme.txt时候&#xff0c;发现这些示例网站一个都打不开&#xff01; 而且readme.txt很不详细&#xff0c;你想训练自己的模…

设计模式之【访问者模式】,动态双分派的魅力

文章目录 一、什么是访问者模式1、访问者模式的应用场景2、访问者模式的五大角色3、访问者模式的优缺点 二、实例1、访问者模式的通用写法2、宠物喂食实例3、KPI考核案例小总结 三、分派1、什么是分派2、静态分派3、动态分派4、双分派5、访问者模式中的伪动态双分派 四、访问者…

mkv转mp4格式怎么转,5种便捷工具盘点

mkv转mp4格式怎么转&#xff1f;因为当我们下载视频时&#xff0c;通常无法选择格式&#xff0c;这可能会导致下载的视频无法打开。如果下载的是MKV格式&#xff0c;它可以容纳多个音频、视频和字幕流。然而&#xff0c;并非所有播放器都支持MKV格式的视频文件。尽管MKV是常见的…

2D车道线检测算法总结

关于2D车道线检测算法的总结主要分为两类&#xff1a;一类基于语义分割来做&#xff0c;一类基于anchor和关键点来做。还有基于曲线方程来做的&#xff0c;但是落地的话还是上面两种为主。 一、基于语义分割的车道线检测算法 1.LaneNet 论文创新点&#xff1a; 1.将车道线检…

【软考数据库】第十五章 知识产权和标准化

目录 15.1 知识产权概述 15.2 保护期限 15.3 知识产权人的确定 15.4 侵权判断 15.5 标准划分 前言&#xff1a; 笔记来自《文老师软考数据库》教材精讲&#xff0c;精讲视频在b站&#xff0c;某宝都可以找到&#xff0c;个人感觉通俗易懂。 15.1 知识产权概述 知识产权是…

RN_iOS项目部署流程实例

文章目录 1、环境配置1.1 安装node1.2 安装Watchman1.3 安装npm1.4 安装cocoapods 2、百家云demo下载3、运行百家云demo3.1 顺利的话3.2 踩过的坑&#xff08;按这个目录流程走&#xff09;3.2.1 npm install -g react-native-cli3.2.2 安装&#xff1a;npm install3.2.3 npm降…