SELECT I/O模型服务端的程序实现(C++)

本文详细介绍了Windows Sockets中的Select模型,通过示例代码展示了如何在服务器端利用select函数检测并处理多个客户端的连接请求和数据收发。在服务器端,先初始化套接字集合,然后在监听套接字上使用select函数,根据返回的可读可写套接字进行相应操作,包括接收客户端消息、发送回复以及处理异常情况。客户端则负责发送消息并接收服务器的响应。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 预备知识

1.1 SELECT模型介绍

Select模型是Windows Sockets中最常见的I/O模型。之所以称其为Select模型,是因为的核心是利用select()函数实现I/O管理。利用select()函数,Windows Sockets应用程序可以判断套接字上是否存在数据,或者能否向该套接字写入数据。
如图所示,在调用recv()函数接收数据之前,先调用select()函数。如果此时系统没有可读的数据,那么select()函数会阻塞在这里。当系统存在可读的数据时,该函数返回。此时应用程序就可以调用recv()函数接收数据了。
在这里插入图片描述

1.2 select函数

结构:

int WSAAPI select(
  int           nfds,
  fd_set        *readfds,
  fd_set        *writefds,
  fd_set        *exceptfds,
  const timeval *timeout
);

参数说明:

  • nfds 一般设置为0,可以忽略,主要是为了兼容其他系统参数兼容。
  • readfds 准备接收数据的套接字集合,即可读性集合。
  • writefds 准备发送数据的套接字集合,即可写性集合。
  • exceptfds 检查错误套接字集合指针。
  • timeout 等待时间,设置为NULL时,表示永久等待,直到有事件发生返回。

函数说明
当程序执行select函数时,程序被阻塞,直至内核检测到有可读可写等套接字时才返回,并修改fd_set集合中数据,这些的数据都是可读可写socket集合,不存在的或没有完成IO操作的套接字会被“删除”,返回值是这些可读可写集合的数量。
若设置超时,则超时时间达到后,函数返回值为0。
需要说明的是,select函数三个套接字指针集合,至少需要传入一个集合才可以 。

1.3 fd_set操作函数

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集合初始化为空集合。

1.4 select函数与宏的搭配使用

可通过以下步骤,来完成对套接字的可读可写判断。

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

2. 关键代码

2.1 服务器端

  1. 在套接字处于监听状态后,使用select模型
//在套接字处于监听状态后,使用select模型
FD_SET socketSet;//服务器套接字集合
FD_SET writeSet;//可写套接字集合
FD_SET readSet;//可读套接字集合
FD_ZERO(&socketSet);//初始化套接字集合,即清空集合
FD_SET(listenSocket, &socketSet);//加入监听套接字
  1. 检测可读套接字,调用检查套接字状态
FD_ZERO(&writeSet);//清空可读套接字集合
FD_ZERO(&readSet);//清空可写套接字集合
readSet = socketSet;//赋值
writeSet = socketSet;

//只检测可读套接字,该函数返回处于就绪状态且已包含在FD_SET结构中的套接字总数
ret = select(0, &readSet, &writeSet, NULL, NULL);
if (SOCKET_ERROR == ret) {
   
   
//调用select()失败处理
cout << "select() returned with error:" << ::WSAGetLastError() << endl;
break;
}
  1. 判断是否存在客户端的连接请求
 //判断是否存在客户端的连接请求
if (FD_ISSET(listenSocket, &readSet)) {
   
   
		SOCKADDR_IN ClientAddr;//保存客户端IP地址端口
		int nLen = sizeof(ClientAddr);
		//accept函数返回一个新的套接字,同时返回客户端的IP地址,初始化ClientAddr
		acceptSocket = accept(listenSocket, (sockaddr*)&ClientAddr, &nLen);

		if (INVALID_SOCKET == acceptSocket) {
   
   
			cout<<"accept() returned with error:" << ::WSAGetLastError() << endl;
		    continue;
		}
		else {
   
   
		 //将该套接字加入服务器套接字结合
		    FD_SET(acceptSocket, &socketSet);
		}

		char* pszClientIP = inet_ntoa(ClientAddr.sin_addr); //返回点分十进制的字符串在静态内存中的指针
		if (NULL != pszClientIP)
	    {
   
   
		//ntohs主要是将网络字节转为主机字节
		cout << "客户端[" << pszClientIP << ":" << ntohs(ClientAddr.sin_port) <<"]请求连接成功"<< endl;
	    cout << "目前客户端的数量为:" << (socketSet.fd_count - 1) << endl;
		//等待其他客户端连接
		Sleep(1000);
		}
		cout << endl;
		continue;
	}
  1. 遍历所有套接字,判断可读或可写
//遍历所有套接字
for (int i = 1; i < socketSet.fd_count; i++) {
   
   
	 SOCKET sAccept = socketSet.fd_array[i];//获取套接字

	 SOCKADDR_IN addrClient;
	 int nLen = sizeof(addrClient);
	 //获取当前连接的客户端的IP地址和端口号,即初始化addClient
	 getpeername(sAccept, (sockaddr*)&addrClient, &nLen);
	 //获取客户端地址以及它的主机字节
	 char* pszClientIp = inet_ntoa(addrClient.sin_addr);
	 unsigned short usClientPort = ntohs(addrClient.sin_port);

	 //该套接字可读
	 if (FD_ISSET(sAccept, &readSet)) {
   
   
		char buf[6400] = {
   
    '\0' }
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xw_lover

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值