【Linux】Socket编程接口 | 实现简单的UDP网络程序

news2024/11/22 6:51:14

文章目录

  • 一、预备知识
    • 理解源IP地址和目的IP地址
    • 理解源mac地址和目的mac地址
    • 认识端口号
      • 理解源端口号和目的端口号
      • 理解“端口号(PORT)”和“进程ID(PID)”
    • 认识TCP和UDP协议
      • TCP协议
      • UDP协议
    • 网络字节序
      • 为什么网络字节序采用的是大端?而不是小端?
      • 网络字节序与主机字节序之间的转换
        • `arpa/inet.h`
        • `netinet/in.h`
  • 二、socket编程接口
    • socket常见API
    • struct sockaddr结构体
      • struct sockaddr
      • struct sockaddr_in
      • struct sockaddr_in6
      • struct in_addr
      • 设计特点
        • 1. sockaddr的设计很像C++中的类的继承
        • 2. 为什么没有用`void*`代替`struct sockaddr*`类型?
  • 三、简单的UDP网络程序
    • 服务端
      • 服务端创建套接字并绑定网络信息
      • 封装服务端 - udpserver.hpp
      • 服务端主文件 - Main.cc
    • 客户端
      • 客户端创建套接字并绑定网络信息
    • 组件
      • 日志系统 - Log.hpp
      • 简化IP和端口获取 - InetAddr.hpp
      • 公用的 - Comm.hpp
      • 禁用类对象的赋值与拷贝 - nocopy.hpp
      • Makefile
    • 本地测试
      • 使用本地环回地址 - 127.0.0.1
    • 网络测试
      • INADDR_ANY
      • 执行Linux命令的服务器 - executor server

一、预备知识

理解源IP地址和目的IP地址

因特网上的每台计算机都有一个唯一的IP地址,如果一台主机上的数据要传输到另一台主机,那么对端主机的IP地址就应该作为该数据传输时的目的IP地址。但仅仅知道目的IP地址是不够的,当对端主机收到该数据后,对端主机还需要对该主机做出响应,因此对端主机也需要发送数据给该主机,此时对端主机就必须知道该主机的IP地址。因此一个传输的数据当中应该涵盖其源IP地址和目的IP地址,目的IP地址表明该数据传输的目的地,源IP地址作为对端主机响应时的目的IP地址。

在数据进行传输之前,会先自顶向下贯穿网络协议栈完成数据的封装,其中在网络层封装的IP报头当中就涵盖了源IP地址和目的IP地址。而除了源IP地址和目的IP地址之外,还有源MAC地址和目的MAC地址的概念。

理解源mac地址和目的mac地址

大部分数据的传输都是跨局域网的,数据在传输过程中会经过若干个路由器,最终才能到达对端主机:请添加图片描述

源MAC地址和目的MAC地址是包含在 链路层的报头 当中的,而MAC地址实际只在当前局域网内有效,因此当数据跨网络到达另一个局域网时,其源MAC地址和目的MAC地址就需要发生变化,因此当数据达到路由器时,路由器会将该数据当中链路层的报头去掉,然后再重新封装一个报头,此时该数据的源MAC地址和目的MAC地址就发生了变化。

例如,在图中主机1向主机2发送数据的过程中,数据的源MAC地址和目的MAC地址的变化过程如下:

时间轴源MAC地址目的MAC地址
刚开始主机1的MAC地址路由器A的MAC地址
经过路由器A之后路由器A的MAC地址路由器B的MAC地址
经过路由器B之后路由器B的MAC地址路由器C的MAC地址
经过路由器C之后路由器C的MAC地址路由器D的MAC地址
经过路由器D之后路由器D的MAC地址主机2的MAC地址

认识端口号

理解源端口号和目的端口号

socket通信的本质

现在通过IP地址和MAC地址已经能够将数据发送到对端主机了,但实际我们是想将数据发送给对端主机上的某个服务进程,此外,数据的发送者也不是主机,而是主机上的某个进程,比如当我们用浏览器访问数据时,实际就是浏览器进程向对端服务进程发起的请求。

也就是说,socket通信本质上就是两个进程之间在进行通信,只不过这里是跨网络的进程间通信。比如逛淘宝和刷抖音的动作,实际就是手机上的淘宝进程和抖音进程在和对端服务器主机上的淘宝服务进程和抖音服务进程之间在进行通信。

因此进程间通信的方式除了管道、消息队列、信号量、共享内存等方式外,还有套接字,只不过前者是不跨网络的,而后者是跨网络的。

[!Tip] 端口号(port)

  • 端口号是一个2字节16位的整数;
  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
  • 一个端口号只能被一个进程占用.

理解“端口号(PORT)”和“进程ID(PID)”

我们之前在学习系统编程的时候,学习了进程的PID可以唯一标识一个进程。
此处我们的端口号也是唯一表示一个进程,那么这两者之间是怎样的关系?

进程ID(PID)是用来标识系统内所有进程的唯一性的,它是属于系统级的概念;而端口号(port)是用来标识需要对外进行网络数据请求的进程的唯一性的,它是属于网络的概念。

一台机器上可能会有大量的进程,但并不是所有的进程都要进行网络通信,可能有很大一部分的进程是不需要进行网络通信的本地进程,此时PID虽然也可以标识这些网络进程的唯一性,但在该场景下就不太合适了。

我们所有的网络通信的行为:本质都是 进程间通信

  1. 先让数据到达机器 - IP
  2. 找到指定的进程 - port:端口号

一个端口号一般和一个进程相关联:

  1. 一个端口号可以和多个进程关联吗?不可以
  2. 一个进程可以和多个端口号关联吗?可以

认识TCP和UDP协议

网络协议栈是贯穿整个体系结构的,在应用层、操作系统层和驱动层各有一部分。当我们使用系统调用接口实现网络数据通信时,不得不面对的协议层就是传输层,而传输层最典型的两种协议就是TCP协议和UDP协议

TCP协议

TCP协议叫做传输控制协议(Transmission Control Protocol),TCP协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。

TCP协议是面向连接的,如果两台主机之间想要进行数据传输,那么必须要先建立连接,当连接建立成功后才能进行数据传输。其次,TCP协议是保证可靠的协议,数据在传输过程中如果出现了丢包、乱序等情况,TCP协议都有对应的解决方法。

UDP协议

UDP协议叫做用户数据报协议(User Datagram Protocol),UDP协议是一种无需建立连接的、不可靠的、面向数据报的传输层通信协议。

使用UDP协议进行通信时无需建立连接,如果两台主机之间想要进行数据传输,那么直接将数据发送给对端主机就行了,但这也就意味着UDP协议是不可靠的,数据在传输过程中如果出现了丢包、乱序等情况,UDP协议本身是不知道的。

[!Question] 既然UDP协议是不可靠的,那为什么还要有UDP协议的存在?

TCP协议是一种可靠的传输协议,使用TCP协议能够在一定程度上保证数据传输时的可靠性,而UDP协议是一种不可靠的传输协议,UDP协议的存在有什么意义?

首先,可靠是需要我们做更多的工作的,TCP协议虽然是一种可靠的传输协议,但这一定意味着TCP协议在底层需要做更多的工作,因此TCP协议底层的实现是比较复杂的,我们不能只看到TCP协议面向连接可靠这一个特点,我们也要能看到TCP协议对应的缺点。

同样的,UDP协议虽然是一种不可靠的传输协议,但这一定意味着UDP协议在底层不需要做过多的工作,因此UDP协议底层的实现一定比TCP协议要简单,UDP协议虽然不可靠,但是它能够快速的将数据发送给对方,虽然在数据在传输的过程中可能会出错。

编写网络通信代码时具体采用TCP协议还是UDP协议,完全取决于上层的应用场景。如果应用场景严格要求数据在传输过程中的可靠性,此时我们就必须采用TCP协议,如果应用场景允许数据在传输出现少量丢包,那么我们肯定优先选择UDP协议,因为UDP协议足够简单。

网络字节序

我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分。

大小端的概念:

  • 大端模式: 数据的高字节内容保存在内存的低地址处,数据的低字节内容保存在内存的高地址处。
  • 小端模式: 数据的高字节内容保存在内存的高地址处,数据的低字节内容保存在内存的低地址处。
    请添加图片描述

网络规定:

  1. 所有到达网络的数据,必须是大端;
  2. 所有从网络收到数据的机器,都会知道数据是大端的!

为什么网络字节序采用的是大端?而不是小端?

网络字节序采用的是大端,而主机字节序一般采用的是小端,那为什么网络字节序不采用小端呢?如果网络字节序采用小端的话,发送端和接收端在发生和接收数据时就不用进行大小端的转换了。

  • 说法一: TCP在Unix时代就有了,以前Unix机器都是大端机,因此网络字节序也就采用的是大端,但之后人们发现用小端能简化硬件设计,所以现在主流的都是小端机,但协议已经不好改了。
  • 说法二: 大端序更符合现代人的读写习惯。

网络字节序与主机字节序之间的转换

netinet/in.harpa/inet.h 是两个常用于网络编程的 C 语言头文件,它们包含了一些用于处理网络地址和字节序转换的函数。

以下是这些头文件中涉及网络和主机字节序转换的主要函数:

arpa/inet.h

inet的含义是“Internet”的缩写

这个头文件中的转化函数做的事情(或者1和2反过来):

  1. 字符串风格IP四字节整数IP
  2. 再转网络序列
  1. uint32_t inet_addr(const char *cp)

    • 功能:将点分十进制的 IP 地址字符串转换为网络字节序的 32 位整数。
    • 参数:cp 是一个指向 IP 地址字符串的指针。
    • 返回值:转换后的网络字节序的 32 位整数。如果转换失败,则返回 INADDR_NONE
  2. int inet_aton(const char *cp, struct in_addr *inp)

    • 功能:将点分十进制的 IP 地址字符串转换为网络字节序的 struct in_addr 结构。
    • 参数:cp 是一个指向 IP 地址字符串的指针,inp 是一个指向 struct in_addr 的指针,用于存储转换后的结果。
    • 返回值:如果转换成功,则返回非零值;否则返回零。
  3. char *inet_ntoa(struct in_addr in)

    • 功能:将网络字节序的 struct in_addr 结构转换为点分十进制的 IP 地址字符串。
    • 参数:in 是一个网络字节序的 struct in_addr
    • 返回值:指向转换后的点分十进制 IP 地址字符串的指针。
  4. int inet_pton(int af, const char *src, void *dst)

    • 功能:将一个地址族(af)指定的网络地址(src)转换为相应的表示形式,并存储在 dst 中。
    • 参数:af 是地址族(例如 AF_INETAF_INET6),src 是指向源地址的指针,dst 是指向目标缓冲区的指针。
    • 返回值:如果转换成功,则返回 1;如果输入的地址无效,则返回 0;如果发生错误,则返回 -1。
  5. const char *inet_ntop(int af, const void *src, char *dst, socklen_t cnt)

    • 功能:将一个地址族(af)指定的网络地址(src)转换为点分十进制的字符串形式,并存储在 dst 中。
    • 参数:af 是地址族,src 是指向源地址的指针,dst 是指向目标缓冲区的指针,cnt 是目标缓冲区的大小。
    • 返回值:如果转换成功,则返回指向目标缓冲区的指针;否则返回 NULL。

关于inet_ntoa

inet_ntoa这个函数返回了一个char*,很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果。那么是否需要调用者手动释放呢?

man手册上说,inet_ntoa函数,是把这个返回结果放到了静态存储区。这个char*不需要我们手动进行释放:
请添加图片描述

那么问题来了,如果我们调用多次这个函数,会有什么样的效果呢?参见如下代码:

#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
	struct sockaddr_in addr1;
	struct sockaddr_in addr2;
	addr1.sin_addr.s_addr = 0;
	addr2.sin_addr.s_addr = 0xffffffff;
	char* ptr1 = inet_ntoa(addr1.sin_addr);
	char* ptr2 = inet_ntoa(addr2.sin_addr);
	printf("ptr1: %s %p\nptr2: %s %p\n", ptr1, ptr1, ptr2, ptr2);
	return 0;
}

运行结果如下:请添加图片描述

因为inet_ntoa把结果放到自己内部的一个静态存储区,这样第二次调用时的结果会覆盖掉上一次的结果:

  • 思考: 如果有多个线程调用 inet_ntoa,是否会出现异常情况呢?
  • 在APUE中,明确提出inet_ntoa不是线程安全的函数
  • 但是在centos7上测试,并没有出现问题,可能内部的实现加了互斥锁
  • 同学们课后自己写程序验证一下在自己的机器上inet_ntoa是否会出现多线程的问题
  • 在多线程环境下,推荐使用inet_ntop,这个函数由调用者提供一个缓冲区保存结果,可以规避线程安全问题

多线程调用inet_ntoa代码示例如下:

#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>

void* Func1(void* p) 
{
	struct sockaddr_in* addr = (struct sockaddr_in*)p;
	while (1) 
	{
		char* ptr = inet_ntoa(addr->sin_addr);
		printf("addr1: %s\n", ptr);
	}
	return NULL;
}

void* Func2(void* p) 
{
	struct sockaddr_in* addr = (struct sockaddr_in*)p;
	while (1) 
	{
		char* ptr = inet_ntoa(addr->sin_addr);
		printf("addr2: %s\n", ptr);
	}
	return NULL;
}

int main() 
{
	pthread_t tid1 = 0;
	struct sockaddr_in addr1;
	struct sockaddr_in addr2;
	addr1.sin_addr.s_addr = 0;
	addr2.sin_addr.s_addr = 0xffffffff;
	pthread_create(&tid1, NULL, Func1, &addr1);
	pthread_t tid2 = 0;
	pthread_create(&tid2, NULL, Func2, &addr2);
	pthread_join(tid1, NULL);
	pthread_join(tid2, NULL);
	return 0;
}
netinet/in.h

这个头文件主要定义了与网络编程相关的数据类型和常量,并没有直接提供字节序转换的函数。但是,它定义了 htonlntohlhtonsntohs 这四个宏,用于处理主机和网络字节序之间的转换。

#include <arpa/inet.h>

uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

记法:

  • h表示host
  • n表示network
  • l表示32位的long
  • s表示16位的short
  1. uint32_t htonl(uint32_t hostlong)

    • 功能:将主机字节序的 32 位长整数转换为网络字节序。
  2. uint32_t ntohl(uint32_t netlong)

    • 功能:将网络字节序的 32 位长整数转换为主机字节序。
  3. uint16_t htons(uint16_t hostshort)

    • 功能:将主机字节序的 16 位短整数转换为网络字节序。
  4. uint16_t ntohs(uint16_t netshort)

    • 功能:将网络字节序的 16 位短整数转换为主机字节序。

这些函数和宏在处理网络编程中的字节序问题时非常有用,特别是在处理 IP 地址和端口号时。

二、socket编程接口

socket常见API

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);

// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);

// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);

// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);

// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

struct sockaddr结构体

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket。然而,各种网络协议的地址格式并不相同。

在进行跨网络通信时我们需要传递的端口号和IP地址,而本地通信则不需要,因此套接字提供了sockaddr_in结构体和sockaddr_un结构体,其中sockaddr_in结构体是用于跨网络通信的,而sockaddr_un结构体是用于本地通信的。

为了让套接字的网络通信和本地通信能够使用同一套函数接口,于是就出现了sockeaddr结构体,该结构体与sockaddr_insockaddr_un的结构都不相同,但这三个结构体头部的16个比特位都是一样的,这个字段叫做协议家族。

请添加图片描述

  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型,16位端口号和32位IP地址。
  • IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6。这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。
  • socket API可以都用struct sockaddr *类型表示,在使用的时候需要强制转化成sockaddr_in。这样的好处是程序的通用性,可以接收IPv4、IPv6,以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数。

struct sockaddr

struct sockaddr
{
  __SOCKADDR_COMMON (sa_); /* 这里定义了 sa_family 字段 */
  char sa_data[14]; /* 地址数据,具体的格式取决于地址族 */
};

在这个结构体中,__SOCKADDR_COMMON(sa_) 展开为 sa_family_t sa_family;,这是 struct sockaddr 结构体中唯一的公共字段。

公共字段的设计用到了C语言宏定义中的双井号


/* POSIX.1g specifies this type name for the `sa_family' member.  */
typedef unsigned short int sa_family_t;
#define  __SOCKADDR_COMMON(sa_prefix) sa_family_t sa_prefix##fami

__SOCKADDR_COMMON 是一个宏定义,用于在 struct sockaddr 及其派生结构体(如 struct sockaddr_instruct sockaddr_in6)中定义共同的字段。这样做的目的是确保这些结构体在内存中的布局具有一致性,以便能够正确地进行类型转换和访问。

__SOCKADDR_COMMON(sa_prefix) 宏定义了一个名为 sa_prefix##family 的字段,其中 sa_prefix 是传入的前缀,## 是宏连接符,用于连接 sa_prefixfamily。这个字段的类型是 sa_family_t,它通常是一个用于标识地址族(例如,IPv4、IPv6等)的枚举类型。

struct sockaddr_in

typedef uint16_t in_port_t;
struct sockaddr_in
{
  __SOCKADDR_COMMON (sin_); /* 这里定义了 sin_family 字段 */
  in_port_t sin_port; /* 端口号 */
  struct in_addr sin_addr; /* IPv4 地址 */
  /* ... 其他字段 ... */
};

在这个结构体中,__SOCKADDR_COMMON(sin_) 展开为 sa_family_t sin_family;。此外,该结构体还包含了端口号(sin_port)、IPv4 地址(sin_addr)以及其他一些字段。

struct sockaddr_in6

struct sockaddr_in6
{
  __SOCKADDR_COMMON (sin6_); /* 这里定义了 sin6_family 字段 */
  in_port_t sin6_port; /* 端口号 */
  uint32_t sin6_flowinfo; /* IPv6 流信息 */
  struct in6_addr sin6_addr; /* IPv6 地址 */
  /* ... 其他字段 ... */
};

在这个结构体中,__SOCKADDR_COMMON(sin6_) 展开为 sa_family_t sin6_family;。此外,该结构体还包含了端口号(sin6_port)、IPv6 地址(sin6_addr)以及其他一些字段。

struct in_addr

typedef uint32_t in_addr_t;
struct in_addr
{
  in_addr_t s_addr; /* IPv4 地址,以网络字节序存储 */
};

这个结构体用于表示一个 IPv4 地址。s_addr 字段是一个 32 位的无符号整数,以网络字节序存储 IPv4 地址。

设计特点

1. sockaddr的设计很像C++中的类的继承

这种设计使得函数可以接受一个通用的 struct sockaddr* 类型的参数,然后在函数内部根据地址族字段来确定如何处理具体的地址结构。这与C++中的类继承类似,基类(struct sockaddr)提供了通用的接口,派生类(struct sockaddr_instruct sockaddr_in6)则提供了具体的实现。

2. 为什么没有用void*代替struct sockaddr*类型?

我们可以将这些函数的struct sockaddr*参数类型改为void*,此时在函数内部也可以直接指定提取头部的16个比特位进行识别,最终也能够判断是需要进行网络通信还是本地通信,那为什么还要设计出sockaddr这样的结构呢?

实际在设计这一套网络接口的时候C语言还不支持void*,于是就设计出了sockaddr这样的解决方案。并且在C语言支持了void*之后也没有将它改回来,因为这些接口是系统接口,系统接口是所有上层软件接口的基石,系统接口是不能轻易更改的,否则引发的后果是不可想的,这也就是为什么现在依旧保留sockaddr结构的原因。

三、简单的UDP网络程序

服务端

服务端创建套接字并绑定网络信息

void Init()
{
    // 1. 创建socket,就是创建了文件细节
    _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (_sockfd < 0)
    {
        lg.LogMessage(Fatal, "socket error, %d : %s", errno, strerror(errno));
        exit(Socket_Err);
    }

    lg.LogMessage(Info, "socket success, socketfd: %d\n", _sockfd);

    // 2. 绑定,指定网络信息
    struct sockaddr_in local;
    bzero(&local, sizeof(local)); // 相当于memset

    local.sin_family = AF_INET;
    local.sin_port = htons(_port);
    local.sin_addr.s_addr = INADDR_ANY; // IP动态绑定
    // local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. 字符串转四字节ip 2. 转网络序列

    // 结构体填完了,但是还需要将它设置进内核
    int n = ::bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
    if (n != 0)
    {
        lg.LogMessage(Fatal, "bind error, %d : %s", errno, strerror(errno));
        exit(Bind_Err);
    }
}

封装服务端 - udpserver.hpp

#pragma once

#include <string>
#include <cstring>
#include <cerrno>
#include <iostream>

#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "nocopy.hpp"
#include "Log.hpp"
#include "Comm.hpp"
#include "InetAddr.hpp"

static const uint16_t defaultport = 8888;
static const int defaultfd = -1;
static const int defaultsize = 1024;

class UdpServer : public nocopy // 防止拷贝和赋值
{
public:
    UdpServer(const std::string& ip, uint16_t port = defaultport)
        : _ip(ip), _port(port), _sockfd(defaultfd)
    {}

    ~UdpServer()
    {}

    void Init()
    {
        // 1. 创建socket,就是创建了文件细节
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            lg.LogMessage(Fatal, "socket error, %d : %s", errno, strerror(errno));
            exit(Socket_Err);
        }

        lg.LogMessage(Info, "socket success, socketfd: %d\n", _sockfd);

        // 2. 绑定,指定网络信息
        struct sockaddr_in local;
        bzero(&local, sizeof(local)); // 相当于memset

        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = INADDR_ANY; // IP动态绑定
        // local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. 字符串转四字节ip 2. 转网络序列

        // 结构体填完了,但是还需要将它设置进内核
        int n = ::bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
        if (n != 0)
        {
            lg.LogMessage(Fatal, "bind error, %d : %s", errno, strerror(errno));
            exit(Bind_Err);
        }
    }

    void Start()
    {
        // 服务器永远不退出
        char buffer[defaultsize];
        for (;;)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
            if (n > 0)
            {
                InetAddr addr(peer);

                buffer[n] = '\0';
                std::cout << "[" << addr.PrintIp_Port() << "]" << "say# " << buffer << std::endl;
                sendto(_sockfd, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);
            }
        }
    }

private:
    std::string _ip;
    uint16_t _port;
    int _sockfd;
};

服务端主文件 - Main.cc

#include "UdpServer.hpp"
#include "Comm.hpp"
#include <memory>

void Usage(std::string proc)
{
    std::cout << "Usage : \n\t" << proc << " local_port\n" << std::endl;
}

int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        return Usage_Err;
    }
    // std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>("0.0.0.0");
    UdpServer* usvr = new UdpServer("0.0.0.0");
    usvr->Init();
    usvr->Start();

    delete usvr;
    return 0;
}

客户端

客户端创建套接字并绑定网络信息

// 1. 创建socket
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
	std::cerr << "socket error: " << strerror(errno) << std::endl;
	return 2;
}

// 2.1 填充一下server信息
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());

client要不要进行bind?要bind!
但是不需要显式绑定,client会在首次发送数据的时候会自动进行bind
为什么?

  1. 因为server端的端口号,一定是众所周知的,不可改变的。所以client应该绑定随机>端口
  2. client端会非常多
    所以,让本地OS自动随机bind,随机选择端口号
#include <iostream>
#include <cerrno>
#include <string>
#include <cstring>
#include <strings.h>

#include <unistd.h>

// 四个网络常用头文件
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

void Usage(std::string process)
{
    std::cout << "Usage : \n\t" << process << "server_ip local_port\n"
        << std::endl;
}

int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        return 1;
    }

    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    // 1. 创建socket
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        std::cerr << "socket error: " << strerror(errno) << std::endl;
        return 2;
    }

    // 2. client要不要进行bind?要bind!
    // 但是不需要显式绑定,client会在首次发送数据的时候会自动进行bind
    // 让本地OS自动随机bind,随机选择端口号

    // 2.1 填充一下server信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());

    while (true)
    {
        // 我们要发的数据
        std::string inbuffer;
        std::cout << "Please Enter# ";
        std::getline(std::cin, inbuffer);

        // 发给谁?server
        ssize_t n = sendto(sock, inbuffer.c_str(), inbuffer.size(), 0, (struct sockaddr*)&server, sizeof(server));
        if (n > 0)
        {
            char buffer[1024];
            // 收消息
            struct sockaddr_in temp;
            socklen_t len = sizeof(temp);
            ssize_t m = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&temp, &len);
            if (m > 0)
            {
                buffer[m] = '\0';
                std::cout << "server echo# " << buffer << std::endl;
            }
            else
            {
                break;
            }
        }
        else
        {
            break;
        }
    }
    close(sock);
    return 0;
}

组件

日志系统 - Log.hpp

#pragma once

#include <ctime>
#include <iostream>
#include <fstream>
#include <string>
#include <cstdarg>

#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>

enum LogLevel
{
    Debug = 0,
    Info,
    Warning,
    Error,
    Fatal
};

enum
{
    Screen = 10,
    OneFile,
    ClassFile
};

const int defaultstyle = Screen;
const std::string default_filename = "log.";
const std::string logdir = "log";

std::string LevelToString(int level)
{
    switch (level)
    {
    case Debug:
        return "Debug";
    case Info:
        return "Info";
    case Warning:
        return "Warning";
    case Error:
        return "Error";
    case Fatal:
        return "Fatal";

    default:
        return "Unknown";
    }
}

class Log
{
public:
    Log()
        :style(defaultstyle)
        , filename(default_filename)
    {
        mkdir(logdir.c_str(), 0775);
    }

    ~Log() = default;

    void Enable(int sty)
    {
        style = sty;
    }

    std::string TimeStampExLocalTime()
    {
        time_t currtime = time(nullptr);
        struct tm* curr = localtime(&currtime);
        char time_buffer[128];
        snprintf(time_buffer, sizeof(time_buffer), "%d-%d-%d %d:%d:%d"
            , curr->tm_year + 1900, curr->tm_mon + 1, curr->tm_mday
            , curr->tm_hour, curr->tm_min, curr->tm_sec);
        return time_buffer;
    }

    void WriteLogToOneFile(const std::string& logname, const std::string& message)
    {
        std::ofstream out(logname, std::ios::app);
        if (!out.is_open())
        {
            return;
        }
        out.write(message.c_str(), message.size());
        out.close();
    }

    void WriteLogToClassFile(const std::string& levelstr, const std::string& message)
    {
        std::string logname = logdir;
        logname += "/";
        logname += filename;
        logname += levelstr;
        WriteLogToOneFile(logname, message);
    }

    void WriteLog(const std::string& levelstr, const std::string& message)
    {
        switch (style)
        {
        case Screen:
            std::cout << message;
            break;
        case OneFile:
            WriteLogToClassFile("all", message);
            break;
        case ClassFile:
            WriteLogToClassFile(levelstr, message);
            break;
        default:
            break;
        }
    }

    //LogMessage(LogLevel, "%s, %d, %f,...", ...); // C风格日志接口
    void LogMessage(LogLevel level, const char* format, ...)
    {
        char right_buffer[1024];

        va_list args;          // char*
        va_start(args, format);// 让args指向可变参数部分
        vsnprintf(right_buffer, sizeof(right_buffer), format, args);
        va_end(args);          // args = nullptr

        char left_buffer[1024];
        std::string levelstr = LevelToString(level);
        std::string currtime = TimeStampExLocalTime();
        std::string idstr = std::to_string(getpid());

        snprintf(left_buffer, sizeof(left_buffer), "[%-7s][%s][%s] ",
            levelstr.c_str(), currtime.c_str(), idstr.c_str());

        // printf("%s%s\n", left_buffer, right_buffer);

        std::string loginfo = left_buffer;
        loginfo += right_buffer;
        loginfo += "\n";

        WriteLog(levelstr, loginfo);
    }

private:
    int style;
    std::string filename;
};

Log lg;

简化IP和端口获取 - InetAddr.hpp

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

class InetAddr
{
public:
    InetAddr(struct sockaddr_in& addr)
        :_addr(addr)
    {
        _port = ntohs(_addr.sin_port);
        _ip = inet_ntoa(_addr.sin_addr);
    }

    ~InetAddr() = default;

    std::string Ip()
    {
        return _ip;
    }

    uint16_t Port()
    {
        return _port;
    }

    std::string PrintIp_Port()
    {
        std::string info = _ip;
        info += ":";
        info += std::to_string(_port);
        return info;
    }

private:
    std::string _ip;
    uint16_t _port;
    struct sockaddr_in _addr;
};

公用的 - Comm.hpp

#pragma once

enum
{
    Usage_Err = 1,
    Socket_Err,
    Bind_Err
};

禁用类对象的赋值与拷贝 - nocopy.hpp

#pragma once

#include <iostream>

class nocopy
{
public:
    nocopy() {}

    nocopy(const nocopy&) = delete;

    const nocopy& operator=(const nocopy&) = delete;

    ~nocopy() {}
};

Makefile

.PHONY:all
all : udp_server udp_client

udp_server : Main.cc
g++ - o $@ $ ^ -std = c++17
udp_client:UdpClient.cc
g++ - o $@ $ ^ -std = c++17

.PHONY:clean
clean :
rm - f udp_server
rm - f udp_client

本地测试

使用本地环回地址 - 127.0.0.1

请添加图片描述

在执行 netstat -naup 命令后,显示以下内容:
请添加图片描述

这里的IP为0.0.0.0,表示监听所有接口,意思是当应用程序希望监听来自所有网络接口的连接时,可能会使用0.0.0.0作为监听地址。这样做意味着应用程序将接受来自任何IP地址的连接。

  1. Local Address:指的是本地端口绑定的地址。对于 UDP 客户端来说,就是客户端发送数据时绑定的本地 IP 地址和端口号。对于 UDP 服务端来说,就是服务端监听的本地 IP 地址和端口号。
  2. Foreign Address:指的是远程主机的地址。对于 UDP 客户端来说,就是客户端发送数据到的远程服务器的 IP 地址和端口号。对于 UDP 服务端来说,就是接收到数据包的远程客户端的 IP 地址和端口号。

网络测试

INADDR_ANY

现在将服务端设置的本地环回127.0.0.1改为服务器的公网IP,此时当我们重新编译程序再次运行服务端的时候会发现服务端绑定失败:请添加图片描述

由于云服务器的IP地址是由对应的云厂商提供的,这个IP地址并不一定是真正的公网IP,这个IP地址是不能直接被绑定的,如果需要让外网访问,此时我们需要bind 0。系统当当中提供的一个INADDR_ANY,这是一个宏值,它对应的值就是0:

local.sin_addr.s_addr = INADDR_ANY; // IP动态绑定
// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 固定ip

因此如果我们需要让外网访问,那么在云服务器上进行绑定时就应该绑定INADDR_ANY,此时我们的服务器才能够被外网访问。

如果绑定固定IP

  • IP更为具体和限制
  • 服务端只能监听和接收特定IP地址上的连接。
  • 如果服务端的网络配置发生变化(例如,IP地址更改或网络接口添加/删除),那么可能需要手动更新绑定设置。

因此,在大多数情况下,如果服务端不需要特定于某个IP地址的行为,那么绑定到任意IP(INADDR_ANY0.0.0.0)通常是一个更可取的选择,因为它提供了更大的灵活性和易用性。

执行Linux命令的服务器 - executor server

  • UdpServer.hpp:
#pragma once

#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "nocopy.hpp"
#include "Log.hpp"
#include "Comm.hpp"
#include "InetAddr.hpp"

const static uint16_t defaultport = 8888;
const static int defaultfd = -1;
const static int defaultsize = 1024;

using func_t = std::function<std::string(std::string)>; // 定义了一个函数类型

//聚焦在IO上
class UdpServer : public nocopy
{
public:
    UdpServer(func_t OnMessage, uint16_t port = defaultport)
        : _port(port), _sockfd(defaultfd), _OnMessage(OnMessage)
    {}

    void Init()
    {
        // 1. 创建socket,就是创建了文件细节
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            lg.LogMessage(Fatal, "socket errr, %d : %s\n", errno, strerror(errno));
            exit(Socket_Err);
        }

        lg.LogMessage(Info, "socket success, sockfd: %d\n", _sockfd);

        // 2. 绑定,指定网络信息
        struct sockaddr_in local;
        bzero(&local, sizeof(local)); // memset
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = INADDR_ANY; // 0

        // local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. 4字节IP 2. 变成网络序列

        // 结构体填完,设置到内核中了吗??没有
        int n = ::bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
        if (n != 0)
        {
            lg.LogMessage(Fatal, "bind errr, %d : %s\n", errno, strerror(errno));
            exit(Bind_Err);
        }
    }
    void Start()
    {
        // 服务器永远不退出
        char buffer[defaultsize];
        for (;;)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer); // 不能乱写
            ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
            if (n > 0)
            {
                InetAddr addr(peer);
                buffer[n] = 0;

                //处理消息
                std::string response = _OnMessage(buffer);

                // std::cout << "[" << addr.PrintDebug() << "]# " << buffer << std::endl;
                sendto(_sockfd, response.c_str(), response.size(), 0, (struct sockaddr*)&peer, len);
            }
        }
    }
    ~UdpServer() = default;

private:
    // std::string _ip; // 后面要调整
    uint16_t _port;
    int _sockfd;

    func_t _OnMessage;   // 回调
};
  • UdpClient.cc:
#include <iostream>
#include <cerrno>
#include <string>
#include <cstring>
#include <strings.h>

#include <unistd.h>

// 四个网络常用头文件
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

void Usage(std::string process)
{
    std::cout << "Usage : \n\t" << process << " server_ip local_port\n"
        << std::endl;
}

int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        return 1;
    }

    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    // 1. 创建socket
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        std::cerr << "socket error: " << strerror(errno) << std::endl;
        return 2;
    }

    // 2. client要不要进行bind?要bind!
    // 但是不需要显式绑定,client会在首次发送数据的时候会自动进行bind

    // 为什么?
    // 1. 因为server端的端口号,一定是众所周知的,不可改变的。所以client应该绑定随机端口
    // 2. client端会非常多
    // 所以,让本地OS自动随机bind,随机选择端口号

    // 2.1 填充一下server信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());

    while (true)
    {
        // 我们要发的数据
        std::string inbuffer;
        std::cout << "Please Enter# ";
        std::getline(std::cin, inbuffer);

        // 发给谁?server
        ssize_t n = sendto(sock, inbuffer.c_str(), inbuffer.size(), 0, (struct sockaddr*)&server, sizeof(server));
        if (n > 0)
        {
            char buffer[1024];
            // 收消息
            struct sockaddr_in temp; // 用于获得server的信息
            socklen_t len = sizeof(temp);
            ssize_t m = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&temp, &len);
            if (m > 0)
            {
                buffer[m] = '\0';
                std::cout << "server echo# " << buffer << std::endl;
            }
            else
            {
                break;
            }
        }
        else
        {
            break;
        }
    }
    close(sock);
    return 0;
}
  • Main.cc:
#include "UdpServer.hpp"
#include "Comm.hpp"
#include <memory>
#include <vector>
#include <cstdio>

void Usage(std::string proc)
{
    std::cout << "Usage : \n\t" << proc << " local_port\n" << std::endl;
}

std::vector<std::string> black_words = {
    "rm",
    "unlink",
    "cp",
    "mv",
    "chmod",
    "exit",
    "reboot",
    "halt",
    "shutdown",
    "top",
    "kill",
    "dd",
    "vim",
    "vi",
    "nano",
    "man"
};

std::string OnMessageDefault(std::string request)
{
    return request + "[haha, got you!!]";
}

bool SafeCheck(std::string command)
{
    for (auto& k : black_words)
    {
        std::size_t pos = command.find(k);
        if (pos != std::string::npos) return false;
    }

    return true;
}

// ls -a -l/ rm / tocuh 
std::string ExecuteCommand(std::string command)
{
    if (!SafeCheck(command)) return "bad man!!";

    std::cout << "get a message: " << command << std::endl;
    FILE* fp = popen(command.c_str(), "r");
    if (fp == nullptr)
    {
        return "execute error, reason is unknown";
    }

    std::string response;
    char buffer[1024];
    while (true)
    {
        char* s = fgets(buffer, sizeof(buffer), fp);
        if (!s) break;
        else response += buffer;
    }
    pclose(fp);
    return response.empty() ? "success" : response;
}

// ./udp_server 8888
int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        return Usage_Err;
    }

    // std::string ip = argv[1];
    uint16_t port = std::stoi(argv[1]);
    // std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(OnMessageDefault, port);
    UdpServer* usvr = new UdpServer(ExecuteCommand, port);
    usvr->Init();
    usvr->Start();
    delete usvr;
    return 0;
}
  • 运行:请添加图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1589406.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用 npm 工具高效更新项目依赖包

团队内部会用工具定时检查包的最新版本并通知&#xff0c;以便我们及时跟进社区进展&#xff0c;避免和技术栈出现版本脱节导致无法使用最新特性和优化内容 这里只说明手动查看和更新包的主要几个命令。 npm outdated&#xff1a;检查项目中过时的依赖包及其最新版本。 npm i…

漫画|数据工程师面试常见问题之数据倾斜

话说&#xff0c;闹钟一响&#xff0c;现实照进梦想&#xff0c;又是李大虎面试找工作的一天。 李大虎心里一直有个想法&#xff0c;如果一天睡20个小时&#xff0c;然后这20个小时全做美梦&#xff0c;醒来的4个小时用来吃喝拉撒&#xff0c;这样岂不就和那些富二代一样了&am…

Golang学习笔记(上)

Golang学习笔记&#xff08;上&#xff09; 安装Golang 来源&#xff1a;linux 安装 golang - 知乎 (zhihu.com) 由于我用的是linux系统&#xff0c;所以本文采用linux的安装方式介绍&#xff0c;如果你使用的是Windows/Mac 也可以看下该文章&#xff0c;或者自己去下列地址进…

Android RecyclerView性能优化及Glide流畅加载图片丢帧率低的一种8宫格实现,Kotlin

Android RecyclerView性能优化及Glide流畅加载图片丢帧率低的一种8宫格实现&#xff0c;Kotlin <uses-permission android:name"android.permission.READ_EXTERNAL_STORAGE" /><uses-permission android:name"android.permission.READ_MEDIA_IMAGES&qu…

关于UCG游戏平台的一些思考

UCG游戏平台&#xff0c;全称User Generated Content&#xff0c;即用户生成内容。它涵盖了所有玩家可以自主编辑的部分&#xff0c;包含并不限于换装、捏脸、关卡摆放等内容。 UCG概念在最近又火了起来&#xff0c;但这个模式出现的并不早。早在10多年前&#xff0c;war3编辑器…

探索Java中的栈:Stack与Deque(ArrayDeque和LinkedList)

文章目录 1. 栈&#xff08;Stack&#xff09;1.1 定义方式1.2 特点1.3 栈的层次结构 2. 双端队列&#xff08;Deque&#xff09;2.1 定义方式及继承关系2.2 特点&#xff1a;2.3 ArrayDeque2.4 LinkedList2.5 Deque 的各种方法2.6 如何选择ArrayDeque和LinkedList 3. 如何选择…

NAPI 类对象导出及其生命周期管理(下)

4. 样例工程源码剖析 工程的模板是Native C,模型是Stage。源码剖析主要围绕以下几个文件 4.1. NAPI导出对象和生命周期管理具体实现 4.1.1. 定义NapiTest类及方法 Napi.h文件内容如下&#xff1a; #ifndef __NAPI_TEST_H__ #define __NAPI_TEST_H__#include "napi/nat…

一款自研Python解释器

项目简介: PikaScript是一个完全重写的超轻量级python引擎,具有完整的解释器,字节码和虚拟机架构,可以在少于4KB的RAM下运行,用于小资源嵌入式系统。相比同类产品,如MicroPython,LuaOS等,资源占用减少85%以上。 入选2021年度 Gitee最有价值开源项目,加入RT-Thread嵌入…

从0开始创建单链表

前言 这次我来为大家讲解链表&#xff0c;首先我们来理解一下什么是单链表&#xff0c;我们可以将单链表想象成火车 每一节车厢装着货物和连接下一个车厢的链子&#xff0c;单链表也是如此&#xff0c;它是将一个又一个的数据封装到节点上&#xff0c;节点里不仅包含着数据&…

【c语言】结构体的访问

&#x1f388;个人主页&#xff1a;豌豆射手^ &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏 &#x1f917;收录专栏&#xff1a;C语言 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共同学习、交流进步&…

力扣HOT100 - 41. 缺失的第一个正数

解题思路&#xff1a; 原地哈希 就相当于&#xff0c;让每个数字n都回到下标为n-1的家里。 而那些没有回到家里的就成了孤魂野鬼流浪在外&#xff0c;他们要么是根本就没有自己的家&#xff08;数字小于等于0或者大于nums.size()&#xff09;&#xff0c;要么是自己的家被别…

51-37 由浅入深理解 Stable Diffusion 3

2024年3月5日&#xff0c;Stability AI公开Stable Diffusion 3论文&#xff0c;Scaling Rectified Flow Transformers for High-Resolution Image Synthesis。公司像往常一样承诺后续将开源代码&#xff0c;开源之光&#xff01;&#xff01;&#xff01; 在LDW潜在扩散模型论文…

学习51单片机必备:从电子基础到编程技巧全解析

学习51单片机需要掌握一系列的基础知识和技能&#xff0c;以下是一些主要的学习内容&#xff1a; 电子基础知识 了解基本的电子元件和电路原理是学习单片机的基础。这有助于理解单片机如何与外围设备交互以及如何设计电路。 数字逻辑 理解数字逻辑和布尔代数&#xff0c;对于编…

第十三届蓝桥杯真题:x进制减法,数组切分,gcd,青蛙过河

目录 x进制减法 数组切分 gcd 青蛙过河 x进制减法 其实就是一道观察规律的题。你发现如果a这个位置上的数x&#xff0c;b这个位置上的数是y&#xff0c;那么此位置至少是max(x,y)1进制。一定要把位置找对啊 #include <bits/stdc.h> using namespace std; typedef l…

如何恢复 iPhone 删除的照片?

照片是iPhone空间不足的主要原因&#xff0c;因此许多用户选择删除一些重复或不喜欢的图片来释放设备。然而&#xff0c;人们在清洁过程中不小心遗漏了自己喜欢的照片的情况很常见。如果你找不到这些珍贵的照片&#xff0c;你一定很难过。其实&#xff0c;您不必担心这个问题&a…

echarts 多环形图

环形图效果&#xff1a; option {"angleAxis": {"max": 1,"show": false,"splitLine": {"show": false},"axisLabel": {"show": false},"axisTick": {"show": false}},"ra…

Idea显示无法自动装配。找不到‘ xxx’类型的Bean

虽然只标红&#xff0c;不报错&#xff0c;但是看着非常别扭&#xff01; 原因&#xff1a; 当我们在使用Autowired注解的时候&#xff0c;默认requiredtrue,表示注入的时候bean必须存在&#xff0c;否则注入失败。 解决方案一&#xff1a; 在自动转配的注解后面添加(require…

低频电磁仿真 | 新能源汽车性能提升的利器

永磁同步电机 新能源汽车的心脏 近年来&#xff0c;全球变暖的趋势日益加剧&#xff0c;极端天气事件层出不穷&#xff0c;这些现象都反映出当前气候形势的严峻性。为了应对这一全球性挑战&#xff0c;各国纷纷采取行动&#xff0c;制定了一系列降碳、减碳的措施。中国在2020年…

2024年淘宝天猫京东618超级红包领取入口

2024年淘宝天猫京东618超级红包领取入口 1、打开「词令」&#xff0c;输入词令关键词直达口令「超红88」&#xff1b; 2、搜索直达2024年淘宝天猫、京东618超级红包领取入口&#xff1b;

新天龙八部3永恒经典之江山策仿官方_源码架设教程

本教程仅限学习使用&#xff0c;禁止商用&#xff0c;一切后果与本人无关&#xff0c;此声明具有法律效应&#xff01;&#xff01;&#xff01;&#xff01; 教程是本人亲自搭建成功的&#xff0c;绝对是完整可运行的&#xff0c;踩过的坑都给你们填上了 一. 效果演示 新天龙…