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

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' };
		ret = recv(sAccept, buf, sizeof(buf), 0);
		if (ret > 0)
		{
			cout << "-------------接收消息-------------" << endl;
			cout << "是否显示客户端[" << pszClientIp << ":" << usClientPort << "]的消息及长度?(用1表示是,其他表示否)" << endl;
			int n = 0;
			cin >> n;
			if (n == 1) {
				cout << "消息长度为:" << ret << endl;
				cout << "消息内容为:" << buf << endl << endl;
			}
			else {
				cout << "不显示消息" << endl;
			}
		}else if (ret == 0) {
			//对方关闭连接
			cout << "客户端[" << pszClientIp << ":" << usClientPort << "]主动关闭连接" << endl;
			closesocket(sAccept);
			FD_CLR(sAccept, &socketSet);
			cout << "目前客户端数量为:" << socketSet.fd_count - 1 << endl;
			continue;
		}
		else {
			// 客户端的socket没有被正常关闭,即没有调用closesocket
			if (::WSAGetLastError() == WSAECONNRESET)
			{
				cout << "客户端[" << pszClientIp << ":" << usClientPort << "]被强行关闭" << endl;
			}
			else
			{
				cout << "recv data error:" << ::WSAGetLastError() << endl;
			}

			closesocket(sAccept);
			//FD_CLR(s,set);从set集合中删除s套接字
			FD_CLR(sAccept, &socketSet);

			//监听socket不算客户端
			printf("目前客户端的数量为: %d\n", socketSet.fd_count - 1);
			continue;
		}
	 } 

	 //该套接字可写
	 if (FD_ISSET(sAccept, &writeSet)) {
		 t++;
		 if (t < socketSet.fd_count) {
			 char buf1[] = { "Hello,I am Server" };
			 cout << "-------------发送消息-------------" << endl;
			 cout << "是否向客户端[" << pszClientIp << ":" << usClientPort << "]发送消息(用1表示是,其他表示否)" << endl;
			 int n = 0;
			 cin >> n;
			 if (n == 1) {
				 ret = send(sAccept, buf1, sizeof(buf1), 0);
				 cout << "发送信息大小为:" << ret << endl;
				 cout << "信息内容为:" << buf1 << endl;
				 cout << endl;
			 }
		 }
	 }
 }
}

2.2 客户端

设置三个客户端,即Client1,Client2,Client3

只显示一个,其余两个同理

//4、接收消息
char buf2[6400]={ '\0' };
ret = recv(s, buf2, sizeof(buf2), 0);
cout << "接收信息大小为:" << ret << endl;
cout << "信息内容为:" << buf2 << endl;


//5.关闭套接字(判断)
cout << "是否关闭套接字?(1表示是,其他表示否)" << endl;
int n;
cin >> n;
if (n == 1) {
	closesocket(s);
}

2.3 完整代码

select_Server.cpp

#include <winsock2.h>
#include <iostream>
using namespace std;

//指定动态库的lib文件
#pragma comment(lib,"ws2_32.lib")

int main() {
	WSADATA wsd;//WSADATA变量
	SOCKET listenSocket;//服务器监听套接字
	SOCKET acceptSocket;//接受客户端连接请求的套接字

	int ret;//返回值
	int t = 0;//计数

	//初始化套接字动态库
	if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0) {
		cout << "WSAStartup error:"<<::WSAGetLastError()<<endl;
		return 1;
	}

	//创建套接字
     listenSocket= socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	 if (INVALID_SOCKET == listenSocket) {
		 cout << "create socket error:" << ::WSAGetLastError() << endl;
		 WSACleanup();//释放套接字资源
		 return -1;
	}

	 //绑定套接字
	 sockaddr_in addr;//服务器地址
	 addr.sin_port = htons(8000);//网络字节序,设置端口号为8000
	 addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//网络字节序,服务器ip地址
	 addr.sin_family = AF_INET;//地址族

	 ret= bind(listenSocket, (SOCKADDR*)&addr, sizeof(SOCKADDR_IN));
	 if (SOCKET_ERROR == ret) {
		 cout<<"Bind error:"<< ::WSAGetLastError() << endl;
		 closesocket(listenSocket);
		 WSACleanup();
		 return -1;
	 }

	 //监听套接字
	 ret= listen(listenSocket, SOMAXCONN);//系统中每一个端口最大的监听队列的长度,默认值为1024
	 if (SOCKET_ERROR == ret)
	 {
		 cout<< "Listen error:" << ::WSAGetLastError() << endl;
		 closesocket(listenSocket);
		 WSACleanup();
		 return -1;
	 }
	 //在套接字处于监听状态后,使用select模型
	 FD_SET socketSet;//服务器套接字集合
	 FD_SET writeSet;//可写套接字集合
	 FD_SET readSet;//可读套接字集合

	 
	 FD_ZERO(&socketSet);//初始化套接字集合,即清空集合
	 FD_SET(listenSocket, &socketSet);//加入监听套接字
	 cout << "服务器启动监听..." << endl;
	 //等待套接字满足可读可写条件,在此之前需不断循环
	 while (true) {
		 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;
		 }

		 //判断是否存在客户端的连接请求
		 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;
		 }


		 //遍历所有套接字
		 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' };
				ret = recv(sAccept, buf, sizeof(buf), 0);
				if (ret > 0)
				{
					cout << "-------------接收消息-------------" << endl;
					cout << "是否显示客户端[" << pszClientIp << ":" << usClientPort << "]的消息及长度?(用1表示是,其他表示否)" << endl;
					int n = 0;
					cin >> n;
					if (n == 1) {
						cout << "消息长度为:" << ret << endl;
						cout << "消息内容为:" << buf << endl << endl;
					}
					else {
						cout << "不显示消息" << endl;
					}
				}else if (ret == 0) {
					//对方关闭连接
					cout << "客户端[" << pszClientIp << ":" << usClientPort << "]主动关闭连接" << endl;
					closesocket(sAccept);
					FD_CLR(sAccept, &socketSet);
					cout << "目前客户端数量为:" << socketSet.fd_count - 1 << endl;
					continue;
				}
				else {
					// 客户端的socket没有被正常关闭,即没有调用closesocket
					if (::WSAGetLastError() == WSAECONNRESET)
					{
						cout << "客户端[" << pszClientIp << ":" << usClientPort << "]被强行关闭" << endl;
					}
					else
					{
						cout << "recv data error:" << ::WSAGetLastError() << endl;
					}

					closesocket(sAccept);
					//FD_CLR(s,set);从set集合中删除s套接字
					FD_CLR(sAccept, &socketSet);

					//监听socket不算客户端
					printf("目前客户端的数量为: %d\n", socketSet.fd_count - 1);
					continue;
				}
			 } 

			 //该套接字可写
			 if (FD_ISSET(sAccept, &writeSet)) {
				 t++;
				 if (t < socketSet.fd_count) {
					 char buf1[] = { "Hello,I am Server" };
					 cout << "-------------发送消息-------------" << endl;
					 cout << "是否向客户端[" << pszClientIp << ":" << usClientPort << "]发送消息(用1表示是,其他表示否)" << endl;
					 int n = 0;
					 cin >> n;
					 if (n == 1) {
						 ret = send(sAccept, buf1, sizeof(buf1), 0);
						 cout << "发送信息大小为:" << ret << endl;
						 cout << "信息内容为:" << buf1 << endl;
						 cout << endl;
					 }
				 }
			 }
		 }
	 }
	 //关闭监听套接字
	 closesocket(listenSocket);

	 //清理套接字库的使用
	 WSACleanup();
	 return 0;
}

Client1.cpp(其余同理)

#include<winsock2.h>//winsock的头文件
#include<iostream>
using namespace std;

//指定动态库的lib文件
#pragma comment(lib,"ws2_32.lib")

//TCP客户端
int main() {
	WSADATA wsd;//WSADATA变量
	int ret;//返回值

	//初始化套接字动态库
	if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0) {
		cout << "WSAStartup error:" << ::WSAGetLastError() << endl;
		return 1;
	}

	//1.创建TCP Socket,流式套接字
	SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (s == INVALID_SOCKET) {
		cout << "create socket error:" << WSAGetLastError() << endl;
		return -1;
	}

	//2.链接服务器
	sockaddr_in addr;//不建议使用sockaddr,建议使用sockaddr_in
	//设置服务器地址
	addr.sin_port = htons(8000);//网络字节序
	addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//网络字节序
	addr.sin_family = AF_INET;//地址族
	int len = sizeof(sockaddr_in);

	if (connect(s, (sockaddr*)&addr, len) == SOCKET_ERROR) {
		cout << "connect error:" << WSAGetLastError() << endl;
		return -1;
	}

	//3.发送消息
	char buf1[] = {"Hello! I am Client1!"};
	ret = send(s, buf1, sizeof(buf1), 0);
	cout << "发送信息大小为:" << ret << endl;
	cout << "信息内容为:" << buf1<<endl;
	cout << endl;

	//4、接收消息
	char buf2[6400]={ '\0' };
	ret = recv(s, buf2, sizeof(buf2), 0);
	cout << "接收信息大小为:" << ret << endl;
	cout << "信息内容为:" << buf2 << endl;


	//5.关闭套接字(判断)
	cout << "是否关闭套接字?(1表示是,其他表示否)" << endl;
	int n;
	cin >> n;
	if (n == 1) {
		closesocket(s);
	}

	//清理winsock环境
	WSACleanup();
}

3. 运行结果

  1. 当四个程序(一个服务端,三个客户端)同时启动时
    在这里插入图片描述
  2. 而三个客户端分别向服务端发送不同的消息
    在这里插入图片描述
  3. 服务端显示各客户端的消息,并一一回复它们消息
    在这里插入图片描述
  4. 客户端一一退出,服务端的显示情况(分正常退出和异常退出)
    在这里插入图片描述
  • 5
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

xw_lover

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

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

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

打赏作者

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

抵扣说明:

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

余额充值