IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> TCP/IP select模型 -> 正文阅读

[网络协议]TCP/IP select模型

select模型适用于服务端

服务端:

1.打开网络库

int WSAStartup(WORD wVersionRequired, LPWSADATA lpWSAData);

2.检验版本号

2 != HIBYTE(wsaData.wVersion) || 2 != LOBYTE(wsaData.wVersion)  // (检验版本号是 2.2)

3.创建socket 套接字

SOCKET socket(int af, int type, int protocol);

4.绑定地址与端口号

int bind(SOCKET s, const sockaddr* addr, int namelen);

5.进行监听

int WSAAPI listen(SOCKET s, int backlog);


6.select 核心:

处理基本C/S模型中的accept recv傻等问题,调用select() 函数循环遍历所有的socket

select() 函数原型:

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

通过参数2检查是否有可读的所有的socket;

通过参数3检查是否有可写的所有的socket;

通过参数4检查是否有异常的所有的socket

7.调用回调函数,处理点击服务端右上角叉号关闭程序是释放所有的socket

SetConsoleCtrlHandler() ,函数原型:

SetConsoleCtrlHandler(
    _In_opt_ PHANDLER_ROUTINE HandlerRoutine,
    _In_ BOOL Add
    );

?服务端代码:

#define _WINSOCK_DEPRECATED_NO_WARNINGS

#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")

/*
	select 模型的特点:
		1、解决基本 C/S 模型中,accpet recv 傻等的问题
			傻等阻塞函数:accept recv send
			执行阻塞:
				accept recv send 在执行的赋值黏贴过程中都是阻塞的,
				select模型只是解决傻等的问题,但解决不了这几个函数本身阻塞的问题
		2、实现多个客户端链接,与多个客户端分别通信
		3、用于服务端,客户端不用select模型,因为只有一个socket
				解决客户端recv的时候会傻等,不能send的问题:
					只需要单独创建一根线程,recv放到线程中就可以了

	// select主要是处理的是 accept 和 recv 傻等阻塞问题 
	// 声明集合(数组)fd_set 用来装所有的socket ,通过select模型遍历所有的socket,
	// 通过select函数的参数处理socket分类处理可读,可写,异常 
	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;

	
*/

// 定义全局的WSAStartup()函数所需的数据结构体
WSADATA WSAData;
// 定义全局的集合 fd_set 装所有的socket
// 声明select函数中所需的结构体 fd_set
fd_set allSocket;	// 装所有的socket

// 1.打开网络库
void OpenWSAStartup()
{
	WORD version = MAKEWORD(2, 2);	//版本号 2.2
	int nRet = WSAStartup(version, &WSAData);
	// 判断是否成功打开网络库
	if (0 != nRet)
	{
		// 分类处理错误码 给提示
		switch (nRet)
		{
		case WSASYSNOTREADY:
			printf("重启电脑试试,或者检查ws_2_32库是否存在\n");
			break;
		case WSAVERNOTSUPPORTED:
			printf("当前库版本号不支持,尝试更换版本试试\n");
			break;
		case WSAEPROCLIM:
			printf("已达到对Windows套接字实现支持的任务数量的限制\n");
			break;
		case WSAEINPROGRESS:
			printf("正在阻止Windows Sockets 1.1操作,当前函数运行期间,因为某些原因造成阻塞\n");
			break;
		case WSAEFAULT:
			printf("lpWASData参数不是有效的指针,参数写错了\n");
			break;
		default:
			break;
		}
	}
}
// 2.检验版本
void CheckVersion()
{
	if (2 != HIBYTE(WSAData.wVersion) || 2 != LOBYTE(WSAData.wVersion))
	{
		// 打开版本号 2.2 失败
		printf("打开版本号 2.2 失败, 正在退出程序...\n");
		// 关闭网络库
		WSACleanup();
		// 退出程序
		return;
	}
}
// 3.创建socket 套接字(服务端)
SOCKET CreateSocket()
{
	SOCKET createSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	// 检验是否成功创建套接字
	if (INVALID_SOCKET == createSocket)
	{
		// 创建套接字失败
		// 获取错误码
		int error = WSAGetLastError();
		printf("创建套接字失败,返回错误号为:%d\n", error);
		// 关闭网络库
		WSACleanup();
		// 退出程序
		return 0;
	}
	return createSocket;
}
// 4.绑定地址与端口号
void Bind(SOCKET tempSocket)
{
	struct sockaddr_in server_Msg;
	// 给结构体赋值
	server_Msg.sin_family = AF_INET;	//IP地址类型
	server_Msg.sin_port = htons(12345);	//端口号
	server_Msg.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");	//ip地址(本地回环地址)
	int Ret_bin = bind(tempSocket, (struct sockaddr*)&server_Msg, sizeof(server_Msg));
	// 检验是否成功绑定地址和端口号
	if (SOCKET_ERROR == Ret_bin)
	{
		// 绑定失败
		// 获取错误码
		int error = WSAGetLastError();
		printf("绑定地址和端口号失败,该错误码为:%d\n", error);
		// 关闭套接字
		closesocket(tempSocket);
		// 关闭网络库
		WSACleanup();
		// 退出程序
		return;
	}
}
// 5.进行监听
void Listen(SOCKET tempSocket)
{
	// 检验监听情况
	if (SOCKET_ERROR == listen(tempSocket, SOMAXCONN))
	{
		// 监听失败
		// 获取错误码
		int error = WSAGetLastError();
		printf("监听失败,该错误码为:%d\n", error);
		// 关闭套接字
		closesocket(tempSocket);
		// 关闭网络库
		WSACleanup();
		// 退出程序
		return;
	}
}
// 6. select模型核心代码
void Select(SOCKET tempSocket)
{
	// 处理fd_set 的4个参数宏
	FD_ZERO(&allSocket);				// 清空集合
	FD_SET(tempSocket, &allSocket);	// 向集合中添加socket
	// 这里暂时只需清空集合 将服务端的socket装进集合中就可以了
	//FD_CLR(socketServer, &allSocket);	// 集合中删除指定的socket
	//FD_ISSET(socketServer, &allSocket);	// 判断指定的socket 是否在集合中
	// 遍历处理所有的socket 通过select函数
	while (1)
	{
		// 记录集合allSokcket 防止被修改
		// 定义select函数中所需的参数2,参数3,参数4 用于检查所有的socket
		fd_set readSockets = allSocket;		// 检查是否有可读的socket
		fd_set writeSockets = allSocket;	// 检查是否有可写的socket
		fd_set errorSockets = allSocket;	// 检查是否以异常的socket
		// 声明select函数中的参数5的时间结构体
		// 最大等待时间
		struct timeval ts;
		// 给结构体成员赋值
		ts.tv_sec = 3;	// 3秒
		ts.tv_usec = 0;	// 0微秒
		// 调用select() 函数
		int retSel = select(0, &readSockets, &writeSockets, NULL, &ts);
		// 处理select函数的返回值
		if (0 == retSel)
		{
			// 客户端在等待的时间内没有反应
			// 直接continue 继续等待就可以了
			continue;
		}
		else if (0 < retSel)
		{
			// 处理客户端的交流请求
			// 处理select函数中的参数2 参数2 的功能是检测所有的socket 判断是否有可读的socket,
			// 将可读的socket装到allSocket的记录的集合中
			for (u_int i = 0; i < readSockets.fd_count; i++)
			{
				// 1.处理服务端的可读socket
				// 判断是否有可读的socket
				if (readSockets.fd_array[i] == tempSocket)
				{
					// 服务端接收连接 调用accept 本质是创建客户端的socket
					SOCKET socketClient = accept(tempSocket, NULL, NULL);
					// 判断是否成功创建客户端
					if (INVALID_SOCKET == socketClient)
					{
						// 客户端创建失败 服务端接收连接出错
						// 通过WSAGetLastError() 获取错误码
						int error = WSAGetLastError();
						printf("处理select函数中的参数2,服务端接收连接,创建客户端出错,该错误码为:%d\n", error);
					}
					// 将成功创建的客户端socket装进集合中
					FD_SET(socketClient, &allSocket);
					// 这里可以send信息告诉客户端服务端创建客户端socket成功
					if (SOCKET_ERROR == send(socketClient, "服务端创建客户端socket成功!", sizeof("服务端创建客户端socket成功!"), 0))
					{
						printf("select函数中参数2 处理服务端接收链接成功,发送信息给客户端失败!!!\n");
					}
				}
				else  // 处理客户端的可读的socket
				{
					// 客户端进行recv消息
					// 定义接收服务端数据的缓存区数组
					char recvFrom[1024] = { 0 };
					// 注意这里的recv函数的第一个参数填的是可读的客户端socket,即readSockets.fd_array[i],如果填的是服务端socketServer,会出出现10057错误码
					int retRecv = recv(readSockets.fd_array[i], recvFrom, 1024, 0);
					// 处理recv 函数的返回值
					if (0 == retRecv)
					{
						// 客户端正常下线
						printf("客户端正常下线了...\n");
						// 从集合中释放客户的socket
						// 定义中间变量记录集合中的要释放的客户端socket
						SOCKET tempSocket_1 = readSockets.fd_array[i];
						// 利用fd_set 宏参数 FD_CLR 删除下线的客户端socket
						FD_CLR(readSockets.fd_array[i], &allSocket);
						// 关闭套接字
						closesocket(tempSocket_1);
					}
					else if (0 < retRecv)
					{
						// 打印服务端发来的信息
						printf("服务端say: %s\n", recvFrom);
					}
					else
					{
						// SOCKET_ERROR 出错了
						// 注意这里客户端强制关闭也叫出错 错误码为 10054
						int error = WSAGetLastError();
						printf("select函数的参数2处理可读的客户端socket recv的返回值出错,该错误码为:%d\n", error);
						switch (error)
						{
						case 10054:
						{
							// 客户端正常下线
							printf("注意客户端非正常下线!!!\n");
							// 从集合中释放客户的socket
							// 定义中间变量记录集合中的要释放的客户端socket
							SOCKET tempSocket_2 = readSockets.fd_array[i];
							// 利用fd_set 宏参数 FD_CLR 删除下线的客户端socket
							FD_CLR(readSockets.fd_array[i], &allSocket);
							// 关闭套接字
							closesocket(tempSocket_2);
						}
						}
					}
				}
			}
			// 2.处理select函数中的参数3 检查可写的所有的socket
			for (u_int i = 0; i < writeSockets.fd_count; i++)
			{
				// 这里可以随时 send 发信息 注意send() 函数第一个参数要填 writeSockets.fd_array[i]
				if (SOCKET_ERROR == send(writeSockets.fd_array[i], "ok", 2, 0))
				{
					// 通过WSAGetLastError() 获取错误码
					int error = WSAGetLastError();
				}
			}
			// 3.处理select函数中的参数4 检查异常的所有的socket
			for (u_int i = 0; i < errorSockets.fd_count; i++)
			{
				// 通过getsockopt 获取错误信息
				// 定义getsockopt 函数中所需的参数4 接收错误信息的缓存区
				char errorBuf[1024] = { 0 };
				int len = sizeof(errorBuf);	// 获取接收信息的缓存区大小
				if (SOCKET_ERROR == getsockopt(errorSockets.fd_array[i], SOL_SOCKET, SO_ERROR, errorBuf, &len))
				{
					// 通过WSAGetLastError() 获取错误码
					int error = WSAGetLastError();
				}
				printf("%s", errorBuf);
			}
		}
		else
		{
			// SOCKET_ERROR 出错了 
			// 通过WSAGetLastError() 获取错误码
			int error = WSAGetLastError();
			printf("select 函数的返回值是SOCKET_ERROR,该错误码为:%d\n", error);
			// 可以退出循环,也可以根据实际情况进行处理
			break;
		}
	}
}

// 7.监控控制台点击叉号
// 控制当前服务端点右上角叉号退出程序是释放所有的socket
BOOL WINAPI fun(DWORD dwCtrlType)
{
	switch (dwCtrlType)
	{
	case CTRL_CLOSE_EVENT:
		// 释放所有的socket
		for (u_int i = 0; i < allSocket.fd_count; i++)
		{
			closesocket(allSocket.fd_array[i]);
		}
		// 清理网络库
		WSACleanup();
	}
	return TRUE;
}

int main()
{
	// 控制台点击退出 主函数投递一个监视
	SetConsoleCtrlHandler(fun, TRUE);
	// 1.打开网络库
	OpenWSAStartup();
	// 2.检验版本
	CheckVersion();
	// 3.创建socket 套接字(服务端)
	SOCKET socketServer = CreateSocket();
	// 4.绑定地址与端口号
	Bind(socketServer);
	// 5.进行监听
	Listen(socketServer);
	// 6. select模型核心代码
	Select(socketServer);	
	// 释放所有的socket
	for (u_int i = 0; i < allSocket.fd_count; i++)
	{
		// 关闭套接字
		closesocket(allSocket.fd_array[i]);
	}
	 关闭套接字
	//closesocket(socketServer);
	// 关闭网络库
	WSACleanup();
	system("pause");
	return 0;
}

客户端代码:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <Winsock.h>
#pragma comment(lib,"ws2_32.lib")
 
WSADATA WSAData;
// 1,打开网络库
void OpenWSAStarup()
{
	// 1,打开网络库
	WORD version = MAKEWORD(2, 2);	//设置要使用的库的版本 MAKEWORD(主版本,副版本); 
	int n = WSAStartup(version, &WSAData);
	// 检验是否成功打开网络库
	if (0 != n)
	{
		// 打开网络库失败,通过n 返回的错误码,给出提示
		switch (n)
		{
		case WSASYSNOTREADY:
			printf("重启电脑试试,或者检查ws_2_32库是否存在\n");
			break;
		case WSAVERNOTSUPPORTED:
			printf("当前库版本号不支持,尝试更换版本试试\n");
			break;
		case WSAEPROCLIM:
			printf("已达到对Windows套接字实现支持的任务数量的限制\n");
			break;
		case WSAEINPROGRESS:
			printf("正在阻止Windows Sockets 1.1操作,当前函数运行期间,因为某些原因造成阻塞\n");
			break;
		case WSAEFAULT:
			printf("lpWASData参数不是有效的指针,参数写错了\n");
			break;
		}
	}
}
// 2.检验版本
void CheckVersion()
{
	if (2 != HIBYTE(WSAData.wVersion) || 2 != LOBYTE(WSAData.wVersion))
	{
		// 打开版本号 2.2 失败
		printf("打开版本号 2.2 失败, 正在退出程序...\n");
		// 关闭网络库
		WSACleanup();
		// 退出程序
		return;
	}
}
// 3.创建socket
SOCKET CreateSocket()
{
	// 3.创建套接字 socket 创建服务端的socket
	SOCKET socketCreate = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	// 检验是否成功创建套接字
	if (INVALID_SOCKET == socketCreate)
	{
		// 创建套接字失败
		// 获取错误码
		int error = WSAGetLastError();
		printf("创建套接字失败,返回错误号为:%d\n", error);
		// 关闭网络库
		WSACleanup();
		// 退出程序
		return 0;
	}
	return socketCreate;
}
// 4.连接服务器
void Connection(SOCKET socket_)
{
	// 4.连接到服务器
	// 创建connect() 函数中参数2所需的结构体 用来装IP地址和端口号
	sockaddr_in client_s;
	//给结构体成员赋值
	client_s.sin_family = AF_INET;		//IP地址类型
	client_s.sin_port = htons(12345);	//端口号
	client_s.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	int nCon = connect(socket_, (struct sockaddr*)&client_s, sizeof(client_s));
	// 检验是否成功连接到服务端
	if (SOCKET_ERROR == nCon)
	{
		// 连接服务端失败
		// 获取错误码
		int error = WSAGetLastError();
		printf("连接服务端失败,返回错误号为:%d\n", error);
		// 关闭套接字
		closesocket(socket_);
		// 关闭网络库
		WSACleanup();
		// 退出程序
		return;
	}
}
 
int main()
{
	// 1,打开网络库
	OpenWSAStarup();
	// 2.校验版本
	CheckVersion();
	// 3.创建socket
	SOCKET socketServer = CreateSocket();
	// 4.连接服务器
	Connection(socketServer);
	
	// 只收一次 信息
	// 定义接收服务端信息的缓存区
	char recvBuf[1024] = { 0 };
	int res = recv(socketServer, recvBuf, 1024, 0);
	// 判断是否成功接收到信息
	if (0 == res)
	{
		printf("服务端下线了\n");
		return 0;
	}
	else if (SOCKET_ERROR == res)
	{
		// 获取错误码
		int error = WSAGetLastError();
		printf("连接服务端失败,返回错误号为:%d", error);
		printf("\n");
		return 0;
	}
	else
	{
		// 打印信息
		printf("服务端say: %s\n", recvBuf);
	}
	printf("我是客户端,正在发送信息,请稍等...\n");
	
	// 5.与服务端进行收发信息
	while (1)
	{
		// 发信息
		char sendBuf[1024] = { 0 };
		scanf_s("%s", sendBuf, 1024);
		// 设置输入 0 时自动退出
		if ('0' == sendBuf[0])
		{
			break;	//退出循环
		}
		send(socketServer, sendBuf, 1024, 0);
	}
 
	// 关闭套接字
	closesocket(socketServer);
	// 关闭网络库
	WSACleanup();
	system("pause");
	return 0;
}

程序运行结果如下:

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2021-07-26 12:23:00  更:2021-07-26 12:23:44 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/2 11:30:37-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码