UDP/IP select模型

本文详细介绍了UDP/IP和TCP/IP协议在使用select模型时的区别和处理逻辑。对于UDP/IP,select主要用于让recvfrom操作更加灵活;而对于TCP/IP,select模型则是为了解决accept和recv的阻塞问题。文章通过服务端和客户端的代码示例,展示了select如何监控socket集合,检测并处理来自客户端的连接和通信请求。

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);	
	}
}

程序运行结果:

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值