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

文章目录

  • 一、预备知识
    • 理解源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;
}
  • 运行:请添加图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

华媒舍:7种方式,打造出旅游媒体套餐

现如今&#xff0c;伴随着旅游业发展与繁荣&#xff0c;更多旅游业发展从业人员越来越重视产品营销品牌基本建设&#xff0c;希望可以将自己的度假旅游产品和服务营销推广给更多的潜在用户。而建立一个优秀的旅游业发展媒体套餐内容品牌是吸引目标客户的重要步骤。下面我们就详…

6.3Python之字典的内置方法

1、创建字典 dict.fromkeys() &#xff1a;可将列表、元组、集合转为字典 knowledgeL [语文, 数学, 英语] scoresD1 dict.fromkeys(knowledgeL, 60) print(scoresD1) knowledgeT (Chinese, Math, English) scoresD2 dict.fromkeys(knowledgeT, 60) print(scoresD2) knowl…

用html写一个雨的特效

<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>雨特效</title><link rel"stylesheet" href"./style.css"> </head> <body> <div id"wrap-textu…

一文掌握 React 开发中的 JavaScript 基础知识

前端开发中JavaScript是基石。在 React 开发中掌握掌握基础的 JavaScript 方法将有助于编写出更加高效、可维护的 React 应用程序。 在 React 开发中使用 ES6 语法可以带来更简洁、可读性更强、功能更丰富,以及更好性能和社区支持等诸多好处。这有助于提高开发效率,并构建出更…

Stable Diffusion——SDXL Turbo让 AI 出图速度提高10倍

摘要 在本研究中&#xff0c;我们提出了一种名为对抗扩散蒸馏&#xff08;ADD&#xff09;的创新训练技术&#xff0c;它能够在1至4步的采样过程中&#xff0c;高效地对大规模基础图像扩散模型进行处理&#xff0c;同时保持图像的高质量。该方法巧妙地结合了分数蒸馏技术&…

【企业场景】设计模式重点解析

设计模式 在平时的开发中&#xff0c;涉及到设计模式的有两块内容&#xff1a; 我们平时使用的框架&#xff08;比如spring、mybatis等&#xff09;我们自己开发业务使用的设计模式。 在平时的业务开发中&#xff0c;其实真正使用设计模式的场景并不多&#xff0c;虽然设计号…

企业业务遇到CC攻击,为何让人如此头疼。

随着互联网的普及和应用&#xff0c;网络安全已经成为人们越来越关注的一个问题。 随着网络信息化不断发展&#xff0c;用户对网站体验有着更高的要求&#xff0c;在网络时代&#xff0c;网站的稳定性至关重要&#xff0c;活跃在网络中的恶意攻击者惯用各类攻击手段破坏网站的稳…

C# Solidworks二次开发:几何公差IGot相关操作API详解

大家好&#xff0c;今天要介绍的是关于几何公差IGot相关操作的API。 几何公差之前没有讲过&#xff0c;具体API如下面所示&#xff1a; &#xff08;1&#xff09;第一个为GetText&#xff0c;这个API的含义为获取此几何公差的指定文本部分&#xff0c;下面是官方的具体解释&…

每日OJ题_01背包①_牛客DP41 【模板】01背包(滚动数组优化)

目录 牛客DP41 【模板】01背包 问题一解析 问题二解析 解析代码 滚动数组优化代码 牛客DP41 【模板】01背包 【模板】01背包_牛客题霸_牛客网 #include <iostream> using namespace std;int main() {int a, b;while (cin >> a >> b) { // 注意 while 处…

智慧污水井物联网远程监控案例

智慧污水井物联网远程监控案例 在当今数字化转型的浪潮中&#xff0c;智慧水务已成为城市基础设施建设的重要组成部分。其中&#xff0c;基于物联网技术的智慧污水井远程监控系统以其高效、精准、实时的特性&#xff0c;在提升污水处理效能、保障城市水环境安全、实现精细化管…

Jmeter安装与测试

一&#xff1a;JMeter简介&#xff1a; JMeter&#xff0c;一个100&#xff05;的纯Java桌面应用&#xff0c;由Apache组织的开放源代码项目&#xff0c;它是功能 和性能测试的工具。具有高可扩展性、支持Web(HTTP/HTTPS)、SOAP、FTP、JAVA 等多种协议的特点。 官方网站&#x…

利用虚拟机建ITtools

网上给的虚拟机多数都是VMX格式的封包&#xff0c;而我这次用的是ovf 我先把虚拟机在导出为ovf 生成了三个文件 去服务器上创建虚拟机&#xff0c;选择从OVF或OVA文件部署虚拟机&#xff0c;点下一页 给虚拟机起个名字 把相应的文件扡到里面去&#xff08;这里生成的四个文件中…

【软件使用-MEGA】基于NJ和ML方法构建进化树结果比较

文章目录 概要对比细节小结 概要 构建进化树有很多可选的算法&#xff0c;其中比较常用的NJ&#xff08;邻接法&#xff09;&#xff0c;也有基于似然法NL&#xff0c;如下图所示&#xff0c;构建进化树具体方法可以参考我之前写的【软件使用-MEGA】如何基于ML方法构建进化树 …

STM32笔记---CAN采样点设置和报错

STM32笔记---CAN采样点设置和报错 采样点设置再同步补偿宽度&#xff08;SJW&#xff09;设置 报错分析CAN中断使能寄存器CAN错误状态寄存器 采样点设置 以前配置CAN参数的BS1和BS2参数时认为总线波特率符合要求就可以了&#xff0c;其实同一个波特率可能对应多组参数设置的情…

LC 515.在每个树行中找最大值

515. 在每个树行中找最大值 给定一棵二叉树的根节点 root &#xff0c;请找出该二叉树中每一层的最大值。 示例1&#xff1a; 输入: root [1,3,2,5,3,null,9] 输出: [1,3,9] 示例2&#xff1a; 输入: root [1,2,3] 输出: [1,3] 提示&#xff1a; 二叉树的节点个数的范围是…

内存函数memcpy、mommove、memset、memcmp

1、memcpy函数 描述&#xff1a; C 库函数 void *memcpy(void *str1, const void *str2, size_t n) 从存储区 str2 复制 n 个字节到存储区 str1。 声明&#xff1a; void *memcpy(void *str1, const void *str2, size_t n)参数&#xff1a; str1 -- 指向用于存储复制内容的目标…

windows环境下实现ffmpeg本地视频进行rtsp推流

摘要&#xff1a;有时候服务端&#xff08;如linux&#xff09;或者边缘端&#xff08;jetson盒子&#xff09;需要接受摄像头的视频流输入&#xff0c;而摄像头的输入视频流一般为rtsp&#xff0c;测试时需要搭建摄像头环境&#xff0c;很不方便&#xff0c;因此需要对本地视频…

MySQL SQL基础入门-你想要的我尽可能覆盖全

什么是SQL&#xff1f; SQL&#xff08;Structured Query Language) ,结构化查询语言。SQL是一种专用语言&#xff0c;用户关系型数据库管理系统或者在关系流数据管理系统中进行流处理。 SQL怎么读&#xff1f;两种读法&#xff1a;一个字母一个字母读或者连起来读。 一个字母…

PostgreSQL入门到实战-第二十二弹

PostgreSQL入门到实战 PostgreSQL中表连接操作(六)官网地址PostgreSQL概述PostgreSQL中self-join命令理论PostgreSQL中self-join命令实战更新计划 PostgreSQL中表连接操作(六) 使用PostgreSQL自联接技术来比较同一表中的行 官网地址 声明: 由于操作系统, 版本更新等原因, 文…

springCloud项目打包 ,maven package或install打包报错

解决思路一&#xff1a; <build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>2.3.7.RELEASE</version></plugin><plugin>&…