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 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> UDP/IP select模型 -> 正文阅读

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

select模型服务端代码:

UDP/IP:
?? ??? ??? ??? ?UDP/IP协议的C/S模型不存在傻等的问题,而select模型只是能让recvfrom更灵活一些
?? ??? ??? ??? ??? ?select 代码逻辑:
?? ??? ??? ??? ??? ??? ?1.所有的socket装进一个集合FD_SET UDP只有一个socket即将服务端的socket装进集合中就可以了
?? ??? ??? ??? ??? ??? ?2.通过select函数,检测中的socket集合中那个有响应了,就是检测有没有消息来了,
?? ??? ??? ??? ??? ??? ?3.有消息了,则进行处理 调用 recvfrom
?? ??? ??? ?TCP/IP:
?? ??? ??? ??? ?TCP/IP协议的C/S模型中accept、recv存在傻等问题,select模型是为了解决accept、recv傻等问题,
?? ??? ??? ??? ??? ?select 代码逻辑:
?? ??? ??? ??? ??? ??? ?1.每个客户端都有socket,服务器也有自己的socket,将所有的socket装进一个数据结构里,即数组 fd_set
?? ??? ??? ??? ??? ??? ?2.通过select函数,遍历1中socket数组,当某个socket有响应,select就会通过其参数/返回值反馈出来,之后做相应的处理
?? ??? ??? ??? ??? ??? ?3.处理有响应的socket,
?? ??? ??? ??? ??? ??? ??? ?判断检测到是的服务端socket,有客户端来链接了,调用accept(创建客户端socket)
?? ??? ??? ??? ??? ??? ??? ?判断检测到的是客户端socket,客户端请求通信,调用send或者recv进行发收消息

select() 函数:

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

select() 函数:
?? ?作用:监视socket集合,如果某个socket发送事件(链接或者发收数据),通过返回值以及参数反馈给我们
参数1:直接填 0 忽略
参数2:检测是否有可读的socket,即客户端发来消息了,该socket就会被设置,这里只有一个服务端socket
参数3:检测是否有可读的socket,没有客户端就不用了,直接填 NULL
参数4:检测套接字上的异常错误,这里只有一个服务端socket,直接填 NULL
参数5:最大等待时间,比如当客户端没有请求时,那么select函数可以等一会,一段时间过后,还没有,
?? ??? ?就继续执行select下面的语句,如果有了,就立即执行下面的语句
?? ??? ?//定义select函数中的参数5 最大等待事件
?? ??? ?timeval ts;
?? ??? ?ts.tv_sec = 3;?? ?//秒
?? ??? ?ts.tv_usec = 0;?? ?//微秒

服务端代码:

#define _WINSOCK_DEPRECATED_NO_WARNINGS

#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>

#pragma comment(lib, "ws2_32.lib")

/*
	UDP/IP (User Datagram Protocol/Internet Protocol) 用户数据报协议
	TCP/IP 协议的特点:
		面向连接的,可靠的,基于字节流的传输层协议。
	UDP/IP 协议的特点:
		面向非连接的,不可靠的,基于数据报的传输层协议。
	
	UDP/IP 与 TCP/IP 的区别:
		1.基本C/S模型中服务端代码:
			UDP/IP:
				是面向非连接的,不用进行listen(进行监听)、不进行accept(接收连接)
				与客户端进行收发消息调用的是 recvfrom、sendto函数
			TCP/IP:
				是面向连接的,进行listen(进行监听)、进行accept(接收连接 本质创建客户端socket)
				与客户端进行收发消息调用的是 recv、send函数
		
		2.select模型服务端代码:
			UDP/IP:
				UDP/IP协议的C/S模型不存在傻等的问题,而select模型只是能让recvfrom更灵活一些
					select 代码逻辑:
						1.所有的socket装进一个集合FD_SET UDP只有一个socket即将服务端的socket装进集合中就可以了
						2.通过select函数,检测中的socket集合中那个有响应了,就是检测有没有消息来了,
						3.有消息了,则进行处理 调用 recvfrom
			TCP/IP:
				TCP/IP协议的C/S模型中accept、recv存在傻等问题,select模型是为了解决accept、recv傻等问题,
					select 代码逻辑:
						1.每个客户端都有socket,服务器也有自己的socket,将所有的socket装进一个数据结构里,即数组 fd_set
						2.通过select函数,遍历1中socket数组,当某个socket有响应,select就会通过其参数/返回值反馈出来,之后做相应的处理
						3.处理有响应的socket,
							判断检测到是的服务端socket,有客户端来链接了,调用accept(创建客户端socket)
							判断检测到的是客户端socket,客户端请求通信,调用send或者recv进行发收消息
*/

// 1.打开网络库 2.校验版本号
void OpenAndCheckWSAStartup();

// 3.创建服务端socket
SOCKET Sock_Server();

// 全局声明要创建 创建服务端socket
SOCKET socketServer;

// 4.绑定地址与端口号
void Bind(SOCKET sock);

// 5.与客户端进行发收数据
void RecvAndSendBySelect(SOCKET sock);

// 6.监视关闭窗口按钮函数所需的参数1 回调函数
BOOL WINAPI fun(DWORD CtrlType);

int main()
{
	// 1.打开网络库 2.校验版本号
	OpenAndCheckWSAStartup();

	// 3.创建服务端socket
	socketServer = Sock_Server();

	// 4.绑定地址与端口号
	Bind(socketServer);

	// 5.与客户端进行发收数据
	RecvAndSendBySelect(socketServer);

	// 6.监视关闭窗口
	SetConsoleCtrlHandler(fun, TRUE);

	// 关闭socket
	closesocket(socketServer);
	// 关闭网络库
	WSACleanup();
	system("pause");
	return 0;
}

// 1.打开网络库 2.校验版本号
void OpenAndCheckWSAStartup()
{
	// 定义WSAStartup函数中参数1 所需的版本号
	WORD wVersionRequestd = MAKEWORD(2, 2);	// 设置2.2版本号
	// 声明WSAStartup函数中参数1 所需的结构体
	WSADATA WSAData;
	// 打开网络库
	int nRet = WSAStartup(wVersionRequestd, &WSAData);
	if (0 != nRet)
	{
		//出错了
		int a = WSAGetLastError();
		printf("网络库打开失败 %d\n", a);
		return;
	}
	// 检验版本号
	if (2 != HIBYTE(WSAData.wVersion) || 2 != LOBYTE(WSAData.wVersion))
	{
		printf("校验版本号错误\n");
		// 关闭网络库
		WSACleanup();
		return;
	}
}
// 3.创建服务端socket
SOCKET Sock_Server()
{
	// 创建服务端 socket
	SOCKET sock_server = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
	// 判断服务端socket是否成功创建
	if (INVALID_SOCKET == sock_server)
	{
		//出错了
		int a = WSAGetLastError();
		printf("创建服务端socket失败,获取的错误码为 %d\n", a);
		//关闭网络库
		WSACleanup();
		return 0;
	}
	return sock_server;
}
// 4.绑定地址与端口号
void Bind(SOCKET sock)
{
	// 声明bind函数参数2所需的 结构体
	struct sockaddr_in sockMsg;
	// 给结构体成员赋值
	sockMsg.sin_family = AF_INET;
	sockMsg.sin_port = htons(12345);
	sockMsg.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	// 绑定地址与端口号
	int nRet = bind(sock, (struct sockaddr*)&sockMsg, sizeof(sockMsg));
	// 判断是否成功绑定地址与端口号
	if (SOCKET_ERROR == nRet)
	{
		//出错了
		int a = WSAGetLastError();
		printf("绑定地址与端口号失败,获取的错误码为 %d\n", a);
		//关闭socket
		closesocket(sock);
		//关闭网络库
		WSACleanup();
		return;
	}
}
// 5.与客户端进行发收数据
void RecvAndSendBySelect(SOCKET sock)
{
	/*int recvfrom(
	  SOCKET   s,
	  char     *buf,
	  int      len,
	  int      flags,
	  sockaddr *from,
	  int      *fromlen
	);
	recvfrom函数:
		作用:得到当前服务端接收到的消息
		本质:复制	将协议缓存区的数据复制黏贴进自定义的buf
		与recv函数一样是进行接收消息,不同的是recv是傻等,recvfrom是死等
	参数1:服务端的socket
		特点:
			TCP是获取指定的,recv函数与客户端是1对1的关系 -- 傻等
			UDP是无差别获取,recvfrom函数与客户端是1对多的关系 -- 死等
	参数2:客户端消息的存储空间,也就是一个字符数组
		广域网:各级路由器上的MTU最小值是576字节
			tcp = 576 - 20(TCP包头) - 20(IP包头) = 536;
			udp = 576 - 8(UDP包头) - 20(IP包头) = 548;
	参数3:想要读取的字节个数, 参数2的字节数
	参数4:数据的读取方式
			0			从协议缓存区取一个数据报,然后就删除掉
			MSG_PEEK	取出数据报,但是协议缓存内不删除,一直残留,导致影响后面的数据读取
			MSG_OOB		带外数据
	参数5:对方的IP地址端口号
			此处结构体为相应的IP地址和端口号 struct sockaddr* sockClient; 填 &sockClient;
	参数6:参数5结构体的大小 int len = sizeof(&sockClient);	填 &len;
	*/
	
	/*int sendto(
	  SOCKET         s,
	  const char     *buf,
	  int            len,
	  int            flags,
	  const sockaddr *to,
	  int            tolen
	);
	sendto函数:
		作用:向目标发送数据报
		本质:send函数将我们的数据复制黏贴进行系统的协议发送缓冲区,计算机伺机发出去
	参数1:当前服务端的socket(自己的socket)
	参数2:给接收方发送的字节串  UDP统一字节数 548
	参数3:参数2的大小(字节个数 548)
	参数4:填0
	参数5:对方IP地址与端口号结构体
	参数6:参数5的大小
	*/

/*int WSAAPI select(
  int           nfds,
  fd_set        *readfds,
  fd_set        *writefds,
  fd_set        *exceptfds,
  const timeval *timeout
);
select() 函数:
	作用:监视socket集合,如果某个socket发送事件(链接或者发收数据),通过返回值以及参数反馈给我们
参数1:直接填 0 忽略
参数2:检测是否有可读的socket,即客户端发来消息了,该socket就会被设置,这里只有一个服务端socket
参数3:检测是否有可读的socket,没有客户端就不用了,直接填 NULL
参数4:检测套接字上的异常错误,这里只有一个服务端socket,直接填 NULL
参数5:最大等待时间,比如当客户端没有请求时,那么select函数可以等一会,一段时间过后,还没有,
		就继续执行select下面的语句,如果有了,就立即执行下面的语句
		//定义select函数中的参数5 最大等待事件
		timeval ts;
		ts.tv_sec = 3;	//秒
		ts.tv_usec = 0;	//微秒
*/

	// 循环与客户端进行收发消息
	while (1)
	{
		//select模型
		// 声明select所需的结构体 集合fd 
		fd_set fd;
		// 清空集合
		FD_ZERO(&fd);
		// 将服务端socket装进集合中
		FD_SET(sock, &fd);

		// 声明select函数中参数5所需的最大等待时间结构体
		struct timeval ts;
		// 给结构体成员赋值
		ts.tv_sec = 0;
		ts.tv_usec = 0;
		// 调用select函数
		int nSel = select(0, &fd, NULL, NULL, &ts);
		// 判断select函数的返回值
		if (SOCKET_ERROR == nSel)
		{
			// 出错了,通过WSAGetLastError() 获取错误码
			int a = WSAGetLastError();
			printf("执行select函数错误,%d\n", a);
			break;	// 退出函数
		}
		if (0 == nSel)
		{
			// 在最大等待时间内没有等待到事件
			continue;	// 进行继续等待
		}
		if (0 < nSel)
		{
			// socket有消息了,进行收发消息处理
			// 1.收消息
			// 定义recvfrom 函数所需的接收字节数组
			char buf[548] = { 0 };
			// 声明recvfrom函数中参数5所需的 结构体相应的IP地址与端口号
			struct sockaddr sockClient;	// 对方的IP地址与端口号
			// 定义recvfrom函数中参数6所需的 参数5结构体的大小
			int len = sizeof(sockClient);
			int nRet = recvfrom(sock, buf, 548, 0, &sockClient, &len);
			// 判断是否成功接收消息
			if (SOCKET_ERROR == nRet)
			{
				// 出错了
				int a = WSAGetLastError();
				printf("接收消息失败,获取的错误码为:%d\n", a);
				continue;
			}
			// 进行接收消息
			printf("client say: %s\n", buf);

			// 2.发消息
			int nSet = sendto(sock, "send ok", sizeof("send ok"), 0, &sockClient, sizeof(sockClient));
			// 判断是否成功发送消息
			if (SOCKET_ERROR == nSet)
			{
				// 出错了
				int a = WSAGetLastError();
				printf("发送消息失败,获取的错误码为:%d\n", a);
				continue;
			}
		}
	}
}
// 6.监视关闭窗口按钮函数所需的参数1 回调函数
BOOL WINAPI fun(DWORD CtrlType)
{
	switch (CtrlType)
	{
	case CTRL_CLOSE_EVENT:
		// 关闭socket
		closesocket(socketServer);
		// 关闭网络库
		WSACleanup();
		break;
	}
	return TRUE;
}

客户端代码:

#define _WINSOCK_DEPRECATED_NO_WARNINGS

#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>

#pragma comment(lib, "ws2_32.lib")

/*
	UDP/IP 基本C/S模型 客户端:
		与 UDP/IP 基本C/S模型 服务端代码的区别:
		代码基本一致,只有bind 函数有区别
		服务端需要进行绑定地址与端口号,因为服务端是一对多
		客户端只需要创建服务端地址与端口结构体,与服务端发送数据报时,
		  直接传递的是创建的结构体就可以了,不用进行绑定,因为服务端的ip与端口号固定不变的	

*/
// 1.打开网络库 2.校验版本号
void OpenAndCheckWSAStartup();

// 3.创建服务端socket
SOCKET Sock_Server();

// 全局声明要创建 创建客户端socket
SOCKET socketClient;

// 4.与客户端进行发收数据
void SendToAndRecvFrom(SOCKET sock);

int main()
{
	// 1.打开网络库 2.校验版本号
	OpenAndCheckWSAStartup();

	// 3.创建服务端socket
	SOCKET socketClient = Sock_Server();

	// 4.与客户端进行发收数据
	SendToAndRecvFrom(socketClient);

	//释放socket 
	closesocket(socketClient);
	//清理网络库
	WSACleanup();
	system("pause");
	return 0;
}

// 1.打开网络库 2.校验版本号
void OpenAndCheckWSAStartup()
{
	// 定义WSAStartup函数中参数1 所需的版本号
	WORD wVersionRequestd = MAKEWORD(2, 2);	// 设置2.2版本号
	// 声明WSAStartup函数中参数1 所需的结构体
	WSADATA WSAData;
	// 打开网络库
	int nRet = WSAStartup(wVersionRequestd, &WSAData);
	if (0 != nRet)
	{
		//出错了
		int a = WSAGetLastError();
		printf("网络库打开失败 %d\n", a);
		return;
	}
	// 检验版本号
	if (2 != HIBYTE(WSAData.wVersion) || 2 != LOBYTE(WSAData.wVersion))
	{
		printf("校验版本号错误\n");
		// 关闭网络库
		WSACleanup();
		return;
	}
}
// 3.创建服务端socket
SOCKET Sock_Server()
{
	// 创建服务端 socket
	SOCKET sock_server = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
	// 判断服务端socket是否成功创建
	if (INVALID_SOCKET == sock_server)
	{
		//出错了
		int a = WSAGetLastError();
		printf("创建服务端socket失败,获取的错误码为 %d\n", a);
		//关闭网络库
		WSACleanup();
		return 0;
	}
	return sock_server;
}
// 4.与客户端进行发收数据
void SendToAndRecvFrom(SOCKET sock)
{
	/*int recvfrom(
	  SOCKET   s,
	  char     *buf,
	  int      len,
	  int      flags,
	  sockaddr *from,
	  int      *fromlen
	);
	recvfrom函数:
		作用:得到当前服务端接收到的消息
		本质:复制	将协议缓存区的数据复制黏贴进自定义的buf
		与recv函数一样是进行接收消息,不同的是recv是傻等,recvfrom是死等
	参数1:客户端的socket
		特点:
			TCP是获取指定的,recv函数与客户端是1对1的关系 -- 傻等
			UDP是无差别获取,recvfrom函数与客户端是1对多的关系 -- 死等
	参数2:客户端消息的存储空间,也就是一个字符数组
		广域网:各级路由器上的MTU最小值是576字节
			tcp = 576 - 20(TCP包头) - 20(IP包头) = 536;
			udp = 576 - 8(UDP包头) - 20(IP包头) = 548;
	参数3:想要读取的字节个数, 参数2的字节数
	参数4:数据的读取方式
			0			从协议缓存区取一个数据报,然后就删除掉
			MSG_PEEK	取出数据报,但是协议缓存内不删除,一直残留,导致影响后面的数据读取
			MSG_OOB		带外数据
	参数5:对方的IP地址端口号
			此处结构体为相应的IP地址和端口号 struct sockaddr* sockClient; 填 &sockClient;
	参数6:参数5结构体的大小 int len = sizeof(&sockClient);	填 &len;
	*/
	/*int sendto(
	  SOCKET         s,
	  const char     *buf,
	  int            len,
	  int            flags,
	  const sockaddr *to,
	  int            tolen
	);
	sendto函数:
		作用:向目标发送数据报
		本质:send函数将我们的数据复制黏贴进行系统的协议发送缓冲区,计算机伺机发出去
	参数1:当前客户端的socket(自己的socket)
	参数2:给接收方发送的字节串  UDP统一字节数 548
	参数3:参数2的大小(字节个数 548)
	参数4:填0
	参数5:对方IP地址与端口号结构体
	参数6:参数5的大小

	*/
	// 声明sendto函数中参数5所需的 服务端地址与端口号结构体
	struct sockaddr_in sockServer;
	// 给服务端地址与端口号结构体成员赋值
	sockServer.sin_family = AF_INET;
	sockServer.sin_port = htons(12345);
	sockServer.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

	// 循环与客户端进行收发消息
	while (1)
	{
		// 定义要发送的字节数组
		char strBuf[548] = { 0 };
		// 手动发送
		scanf_s("%s", strBuf, 548);
		// 设置输入0时,将关闭客户端
		if ('0' == strBuf[0])
		{
			break;
		}

		// 1.发消息
		int nSet = sendto(sock, strBuf, sizeof(strBuf), 0, (struct sockaddr*)&sockServer, sizeof(sockServer));
		// 判断是否成功发送消息
		if (SOCKET_ERROR == nSet)
		{
			// 出错了
			int a = WSAGetLastError();
			printf("发送消息失败,获取的错误码为:%d\n", a);
			continue;
		}

		// 2.收消息
		// 定义recvfrom 函数所需的接收字节数组
		char buf[548] = { 0 };
		// 声明recvfrom函数中参数5所需的 结构体相应的IP地址与端口号
		struct sockaddr sockClient;	// 对方的IP地址与端口号
		// 定义recvfrom函数中参数6所需的 参数5结构体的大小
		int len = sizeof(sockClient);
		int nRet = recvfrom(sock, buf, 548, 0, &sockClient, &len);
		// 判断是否成功接收消息
		if (SOCKET_ERROR == nRet)
		{
			// 出错了
			int a = WSAGetLastError();
			printf("接收消息失败,获取的错误码为:%d\n", a);
			continue;
		}
		// 进行接收消息
		printf("Server say: %s\n", buf);	
	}
}

程序运行结果:

?

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2021-08-22 13:50:08  更:2021-08-22 13:50:42 
 
开发: 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/17 16:17:27-

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