打怪升级之UDP数据接收实验的服务端代码

本文详细介绍了如何使用C++在Windows下通过VisualStudio2022实现UDP服务端的编程,包括WSA初始化、SOCKET初始化、IP地址处理、bind函数和recvfrom函数的使用,以及可能出现的问题和解决办法。
摘要由CSDN通过智能技术生成

C++语言下的windows网络编程(SOCKET)

本文的主要目的是以C++语言在windows下实现UDP网络服务端的代码(VS2022)。

所需的头文件调用

#include<iostream>
#include<WinSock2.h>
#include<WS2tcpip.h>
#include<stdlib.h>
#pragma comment(lib,"ws2_32.lib")

WSA初始化

WSA的初始化可以归结到WSAStartup()函数中去:https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsastartup

WSAStartup()返回值不为0时,说明出现了错误,使用WSAGetLastError()获取报错码。

#include<WinSock2.h>
#pragma comment(lib,"ws2_32.lib")

	//初始化WSA
	WSADATA wsaDATA;
	WORD wVersion = MAKEWORD(2, 2);

	if (WSAStartup(wVersion , &wsaDATA) != 0)
	{
		printf("WSAS errocode is %d\n",WSAGetLastError());
		return -1;
	}

错误代码的查询可以前往windows文档查看:https://learn.microsoft.com/en-us/windows/win32/winsock/windows-sockets-error-codes-2

SOCKET初始化

初始化主要函数位socket()函数,文档:https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-socket

其返回值不为0时发生错误,错误代码使用VS文档中可以自查的代码文档:https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes

也可以使用WSAGetLastError里的error数据。

使用getlasterror返回的错误值是系统、WSA等的错误值,如果你觉得直接的int值有问题,可以直接打印出来。

    int sockfd;
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        perror("socket err is \n");
        return -1;
    }

这里阅读原文文档可以知道,错误的socket函数会返回INVALID_SOCKET值。(正常的话返回不是0)(INVALID_SOCKET就是一个22长的整形 -1)

perror函数是一个输出给std标准流的报错函数。

IP地址

IP地址主要就是定义一个结构体:sockaddr与sockaddr_in

struct sockaddr{
	unsigned short sa_family;	//地址类型,AF_XXX
	char sa_data[14];			//14字节的端口和地址
};

struct sockaddr_in{
	short int sin_family;		//地址类型
	unsigned short int sin_port;//端口号
	struct in_addr sin_addr;	//地址
	unsigned char sin_zero[8];	//为了保持与struct sockaddr一样的长度。
};

struct in_addr{
	unsigned long s_addr;		//地址
};

这两种地址都是16字节的形式。包含了IP族名称,端口号和IP地址。
对于我们常用的IPV4来说,常用sockaddr_in这个结构体表示。
最终输入到bind函数中去的,只要时一个16位结构体数据就可以了,注意其中的字节序和IP地址可能会有的点序问题即可。

//函数将整形变量的网络字节顺序big-endian。
int htons();
//将一个网络字节顺序转换为主机字节顺序
int ntohs();
//函数将无符号长整形变量的网络字节顺序big-endian。
int htonl();
//函数将网络字节顺序变为主机顺序。
int ntonl();

//将主机字节序的IP地址转换为网络字节序,网络字节序为整形数
int inet_pton();
//将大端的整形数, 转换为小端的点分十进制的IP地址 (字符串)
int inet_ntop();

上述是字节序调整函数。

	#include<WinSock2.h>
	#include<WS2tcpip.h>
	//inet_pton函数在<WS2tcpip.h>头文件中。
	sockaddr_in Addr;
	Addr.sin_family = AF_INET;
	inet_pton(AF_INET,"100.65.90.180",&Addr.sin_addr);
	Addr.sin_port = htons(1234);

上述是地址写入举例,其中inet_pton是VS2022版本后由于不支持原来的inet_addr函数而更新的新函数,目的是将变量1指定(IP族格式)的变量2(String IP)按正确的方式写入变量3(可用于bind的地址结构体)中。

这里图省事就没有判断inet_pton返回值所代表的合法性了。

无论是socketaddr结构体还是socketaddr_in结构体,亦或是你自己定义的某个16字节的结构体,在保证顺序正确的前提下,都可以输入bind函数中作为IP地址的绑定。还是那句话,不管宏展开的变量叫什么,变量的本质不过是某一串二进制数据罢了。

如果你不是想要绑定某个特定的IP,而是直接接收所有的IP地址来源,你可以使用:

Addr.sin_addr.s_addr = htonl(INADDR_ANY);

这里的INADDR_ANY是提前定义好的变量名。

bind()

没什么好说的,bind就完事了。bind的返回值也是一个判断值,其文档是:https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-bind

	bind(sockfd, (sockaddr*)&Addr, sizeof(Addr));

注意输入函数的变量格式,地址结构体的输入方法是一个地址的地址指针。

加上错误判断的话就是:

    if (bind(sockfd, (struct sockaddr *)&Addr, sizeof(Addr)) != 0)
    {
        perror("bind err is");
        return -1;
    }

bind返回值是基于0的判断条件的。

read()

可以直接使用WSA下的recvfrom函数接收来自指定地点的信息。

recvfrom(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&clientaddr, &len)

具体文档参见:https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-recvfrom

其返回值代表接收到的字节数,如果连接正常关闭,则返回值为零。

while(1){
	if (recvfrom(sockfd, (char *) &msg, sizeof(msg), 0, (struct ClientAddr*)&ClientAddr, &len) < 0)
	{
		 perror("recvfrom err.");
		return -1;
	}
}

这里要注意的有两点,第一个是msg作为一种内存结构体,在recvfrom函数中由内存首位指针(&msg)和内存长度(sizeof(msg))决定。

recvfrom的IP地址(函数的第五个变量)是对接收端socket的ip地址的描述,需要提前声明这个变量后,函数将对这个变量完成赋值。

具体的函数功能需要查看文档中的in、out描述来确定,而接收到的数据呢,则是一个char *类型的(你也可以使用转换函数编程任何你想要的东西)。

另外,如果你想要正确的显示你的IP地址,显示你的端口号,显示你的实际接收内容,那么你需要调换字节序并可能使用到下列函数:

//int af指的是AF_INET和AF_INET6,后两者是写入变量
int inet_pton(int family, const char * src, void * dst);
//返回值其实是一个PCSTR类型(标准类型的IP格式和字符串表现格式,
//详情见另一篇专门讲这个的博客或者去查找C++ANSI编码与Unicode编码之间的关系。)
PCSTR WSAAPI inet_ntop(int family, const char * pAddr,  PSTR pStringBuf, size_t StringBufSize);
//这里的PCSTR是win系统下的一种单独def的指针,可以通过(char * )来转换,其返回的就直接是地址的string指针。
(char *) inet_ntop;

为了方便的使用messge传递信息,我们最好定义一个新的结构体:

typedef struct msg_t
{
	int type;
	char text[100];
} MSG_t;

注意将结构体的指针转化成对应的char *指针输入到对应的read函数中去就好了。

好了,写到这里,差不多已经可以检验一下我们的socket成色如何了。

使用网络调试助手或者wireshark可以轻松的生成指定IP地址和端口的数据(但受限于windows API 往往不能同时调用同样的IP作为主机,但你可以以发送端IP去发送是没有问题的),让我们来实际检验一下吧。

检验代码:

	//初始化云端的ip地址变量
	sockaddr_in ClientAddr;
	int Clientlen = sizeof(ClientAddr);

	//初始化消息结构体s
	MSG_t msg;

	char buf[32] = { 0 };

	//循环接收来自云端的数据
	while (1)
	{
		printf("UDP server is ready to get information!\n");
		printf("ServerAddr is %s\n", (char*)inet_ntop(AF_INET, &ServerAddr.sin_addr, buf, sizeof(buf)));
		printf("ServerAddr is %d\n", ntohs(ServerAddr.sin_port));
		if (recvfrom(sockfd, (char *) & msg, sizeof(msg), 0, (struct sockaddr*)&ClientAddr, &Clientlen) < 0)
		{
			perror("recvfrom err \n");
			printf("WSAS errocode is %d\n", WSAGetLastError());
			return -1;
		}
		printf("ClientAddr is %s\n", (char*)inet_ntop(AF_INET, &ClientAddr.sin_addr, buf, sizeof(buf)));
		printf("ClientAddr is %d\n", ntohs(ClientAddr.sin_port));
		printf("msg typr is %d\n",msg.type);
		printf("msg text is %s\n", msg.text);
	}

这里提前定义好服务器主机的IP地址,注意,服务器主机的IP地址固定只有那几个,可以上CMD用-arp -a指令调出来。

在这里插入图片描述调出本地可用的IP地址后,打开网络调试助手(没有就自己百度下一个去)。调整发送IP地址和端口为你的服务器IP地址和端口(注意由于Windows API的限制,一个IP主机地址一次只能由一个进程调用,所以网络调试的本地主机地址不能跟服务器主机地址在一个局域网中完全一样)。
在这里插入图片描述

这里发送了一个UDP数据包:http://www.bilibili.com这一串字符串,通过我们的msg.type定义可以知道,UDP数据包的前一个int(也就是4个字节)数据给到了type实体里,后面的数据给到了text里。

实际编码过程中使用的是ASCII编码,http这四个最前面的字符就变成了68 74 74 70这四个数字(16进制)。其二进制码表现为:

字母ASCII (16进制)二进制
h6801101000
t7001110000
t7001110000
p7401110100

再看实际转换为int后的type数据为:
1886680168(十进制)
1110000011101000111010001101000(二进制)
1110000 01110100 01110100 01101000 (二进制)

整理成下面这个样子就是:

二进制:
01110000 p
01110100 t
01110100 t
01101000 h

刚好就是http,低位在前的意思。

所以,想要给int type赋值为1,前四个字符串应该为
00000000
00000000
00000000
00000001

转换成UDP字节序就是,你需要发送01 00 00 00 到UDP就可以让你的int值刚好为1了。
或者你也可以用一个简单的办法就是转换成char,单字节的顺序和UDP就是完全相同的了。

最后还有一个问题是,为什么text里面会出现那么多的“烫”。

这是因为,编译器会自动给msg里面填充检错码0xCC对应到ASCII里面就是汉字烫。

如何这些修复BUG?

第一个看上去是BUG的东西是msg.text的最后还加上了一串IP地址。不要慌,我们查阅recvfrom文档看看怎么肥是:https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-recvfrom。结果发现没有啊,不应该啊,应该就是那么长啊,怎么回事呢?

后来发现主要的原因是,如果你的UDP包里面没有ASCII里面的NULL符号的话,C++的char[]是不知道应该在哪里停的,所以就会一直读下去,解决的办法就是在ASCII码的最后加上00。如下图:

在这里插入图片描述

第一次没有加最后红圈的00,所以出现了char数组的读取错误,第二次加上之后就正常了。00的ASCII码为\意味右划线,可以加到char数组的最后一个。

在这里插入图片描述

第二个可能的bug是,如果你使用了wireshark这一类软件进行监控的话,那么你的C语言进程很可能就收不到信号了,因为wireshark已经调用了这个IP地址。

服务端代码总览

#include<iostream>
#include<WinSock2.h>
#include<WS2tcpip.h>
#include<stdlib.h>
#pragma comment(lib,"ws2_32.lib")

typedef struct msg_t
{
	int type;
	char text[100];
} MSG_t;

int main(int argc, char* argv[])
{
	//初始化WSA
	WSADATA wsaDATA;
	WORD wVersion = MAKEWORD(2, 2);

	if (WSAStartup(wVersion, &wsaDATA) != 0)
	{
		printf("WSAS errocode is %d\n", WSAGetLastError());
		return -1;
	}

	//初始化SOCKET
	int sockfd;
	sockfd = socket(AF_INET, SOCK_DGRAM, 0);
	if (sockfd < 0)
	{
		perror("socket err is \n");
		printf("WSAS errocode is %d\n", WSAGetLastError());
		return -1;
	}

	//建立socket_addr
	sockaddr_in ServerAddr;
	ServerAddr.sin_family = AF_INET;
	inet_pton(AF_INET, "192.168.1.102", &ServerAddr.sin_addr);
	ServerAddr.sin_port = htons(1234);

	//bind绑定socket和ip地址、端口
	if (bind(sockfd, (struct sockaddr*)&ServerAddr, sizeof(ServerAddr)) != 0)
	{
		perror("bind err is \n");
		printf("WSAS errocode is %d\n", WSAGetLastError());
		return -1;
	}

	//初始化云端的ip地址变量
	sockaddr_in ClientAddr;
	int Clientlen = sizeof(ClientAddr);

	//初始化消息结构体s
	MSG_t msg;

	char buf[32] = { 0 };

	//循环接收来自云端的数据
	while (1)
	{
		printf("UDP server is ready to get information!\n");
		printf("ServerAddr is %s\n", (char*)inet_ntop(AF_INET, &ServerAddr.sin_addr, buf, sizeof(buf)));
		printf("ServerAddr is %d\n", ntohs(ServerAddr.sin_port));
		printf("msg text len is %d\n", sizeof(msg));
		if (recvfrom(sockfd, (char *) & msg, sizeof(msg), 0, (struct sockaddr*)&ClientAddr, &Clientlen) < 0)
		{
			perror("recvfrom err \n");
			printf("WSAS errocode is %d\n", WSAGetLastError());
			return -1;
		}
		printf("ClientAddr is %s\n", (char*)inet_ntop(AF_INET, &ClientAddr.sin_addr, buf, sizeof(buf)));
		printf("ClientAddr is %d\n", ntohs(ClientAddr.sin_port));
		printf("msg typr is %d\n",msg.type);
		printf("msg text is %s\n", msg.text);
		printf("msg text len is %d\n", sizeof(msg.text));
		printf("msg text len is %d\n", sizeof(msg));
	}

	closesocket(sockfd);
	return 0;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

考琪

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

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

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

打赏作者

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

抵扣说明:

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

余额充值