【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)); // 相当于memsetlocal.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)); // 相当于memsetlocal.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. 创建socketint 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);// 发给谁?serverssize_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 = nullptrchar 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 onceenum
{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_clientudp_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)); // memsetlocal.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. 创建socketint 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);// 发给谁?serverssize_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.mzph.cn/news/811064.shtml

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

相关文章

循环单链表算法库

学习贺老师数据结构 数据结构之自建算法库——循环单链表_循环单链表 csdn-CSDN博客​​​​​​ 整理总结出的循环单链表算法库 v1.0 : 基本实现功能 v2.0(2024.4.6): 修复Delete_SpecificLocate_CyclicList()删除节点函数bug,添加验证删除节点是否超范围判断 目录 1.主要功能…

react17中配置webpack:使用@代表src目录

在vue的项目中可以使用表示src目录&#xff0c;使用该符号表示绝对路径&#xff0c;那么在react中想要使用怎么办呢&#xff1f; 在react中使用表示src目录是需要在webpack中配置的&#xff0c;在核心模块node_modules-》react-scripts-》config-》webpack.config.js中搜索找到…

【数据结构与算法】之8道顺序表与链表典型编程题心决!

个人主页&#xff1a;秋风起&#xff0c;再归来~ 数据结构与算法 个人格言&#xff1a;悟已往之不谏&#xff0c;知来者犹可追 克心守己&#xff0c;律己则安&#xff01; 目录 1、顺序表 1.1 合并两个有序数组 1.2 原地移除数组中所有的元素va…

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潜在扩散模型论文…

​​​​网络编程学习探索系列之——广播原理剖析

hello &#xff01;大家好呀&#xff01; 欢迎大家来到我的网络编程系列之广播原理剖析&#xff0c;在这篇文章中&#xff0c; 你将会学习到如何在网络编程中利用广播来与局域网内加入某个特定广播组的主机&#xff01; 希望这篇文章能对你有所帮助&#xff0c;大家要是觉得我写…

Leetcode算法训练日记 | day24

一、组合问题 1.题目 Leetcode&#xff1a;第 77 题 给定两个整数 n 和 k&#xff0c;返回范围 [1, n] 中所有可能的 k 个数的组合。 你可以按 任何顺序 返回答案。 示例 1&#xff1a; 输入&#xff1a;n 4, k 2 输出&#xff1a; [[2,4],[3,4],[2,3],[1,2],[1,3],[1,4…

福建单航次最大批量汽车“出海”

3月12日这一天&#xff0c;在福州海关的严密监管下&#xff0c;共有4000辆上汽名爵品牌的汽车被高效有序地装载到“安吉智慧”号滚装船上&#xff0c;这批车辆即将启程前往荷兰、埃及、英国等多个海外市场。在这批出口汽车中&#xff0c;新能源车型占据了显著的比例&#xff0c…

OceanMind海睿思助力企业“数据入表”经济利益流入与生命周期管理

通过多年信息系统的建设与应用&#xff0c;企业积累了大量的数据。同时随着时间的推进&#xff0c;数据规模正以加速度快速增长。从国家到企业&#xff0c;都越来越关注所拥有的数据资源及其蕴含的深厚价值。很多企业已经逐渐认知到数据是重要的战略资源&#xff0c;数据资产化…

游标的定义和类型

Oracle从入门到总裁:​​​​​​https://blog.csdn.net/weixin_67859959/article/details/135209645 游标的基本概念 游标从字面上理解为游动的光标&#xff0c;可以使用 Excel 表格来想象游标的作用&#xff0c;游标指向每一行&#xff0c;通过游标访问每行数据。 在 Orac…

2024/4/5—力扣—字符串相乘

代码实现&#xff1a; 方法一&#xff1a;常规解法——超出整数表示范围 long long char_to_num(char *str) {long long num 0;for (int i 0; i < strlen(str); i) {num num * 10 (str[i] - 0);}return num; }char* multiply(char *num1, char *num2) {long long a cha…

Git基础操作及其分支管理

目录 一、git的用处&#xff1f; 1.1 git也不是银弹 二、安装git 三、git基础操作 3.1 创建git本地仓库 3.2 配置Git 3.3 认识工作区、暂存区、版本库 3.4 添加文件 3.5 Git文件目录 3.6 版本回退 3.7 撤销修改 3.7.1 对于工作区的代码&#xff0c;还没有进行add操作…

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

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

Redis性能管理和集群的三种模式(二)

一、Redis集群模式 1.1 redis的定义 redis 集群 是一个提供高性能、高可用、数据分片、故障转移特性的分布式数据解决方案 1.2 redis的功能 数据分片&#xff1a;redis cluster 实现了数据自动分片&#xff0c;每个节点都会保存一份数据故障转移&#xff1a;若个某个节点发生故…

探索柔性负荷在综合能源系统中的优化调度策略

柔性负荷&#xff0c;指的是那些可以根据系统需求和市场信号调整其使用模式的负荷。它们包括可平移负荷、可转移负荷和可削减负荷。这些负荷的灵活性为IES&#xff08; Integrated Energy System, 综合能源系统&#xff09;提供了额外的调节能力&#xff0c;有助于平衡供需、提…

MyBatis实例更新

MyBatis具体 准备工作 预编译SQL 新增 更新 查询 //查询员工Select("select * from emp where id #{id}")public Emp getById(Integer id);//方案一:给字段起别名&#xff0c;让别名与实体类属性一致Select("select id,username,password,name,gender,image,j…

DataX案例,MongoDB数据导入HDFS与MySQL

【尚硅谷】Alibaba开源数据同步工具DataX技术教程_哔哩哔哩_bilibili 目录 1、MongoDB 1.1、MongoDB介绍 1.2、MongoDB基本概念解析 1.3、MongoDB中的数据存储结构 1.4、MongoDB启动服务 1.5、MongoDB小案例 2、DataX导入导出案例 2.1、读取MongoDB的数据导入到HDFS 2…

Failed to load dll

Unity运行时提示 dll 加载失败 Plugins: Failed to load ‘Assets/Plugins/xxx.dll’ because one or more of its dependencies could not be loaded. 使用 Dependency Walker 查看这个 dll 引用&#xff0c;一推引用丢失 最后确认是 C 组件缺失 打开 Visual Studio Install…

Java基础入门--第十一章--JDBC(Java Database Connection)Java数据库连接

JDBC 11.1 什么是JDBC11.1.1 JDBC概述11.1.2 JDBC驱动程序 11.2 JDBC的常用API11.3 JDBC编程11.3.1 JDBC 编程步骤11.3.2 实现第一个JDBC程序 我的MySQL的root密码: root 11.1 什么是JDBC 11.1.1 JDBC概述 JDBC的全称是Java数据库连接&#xff08;Java Database Connectivit…

光威神策PRO PCIe 5.0 SSD发布,国产固态硬盘进入10G俱乐部

全球半导体供应链的紧张局势和闪存资源的短缺让许多行业都面临着不小的压力 &#xff0c; 连带的也让消费者难以获取物美价廉的闪存产品 。但是&#xff0c;总有一些企业能够逆流而上&#xff0c; 像是 光威科技这家国产存储品牌&#xff0c; 最近就给国内消费者 带来了一个惊喜…

【JAVA基础篇教学】第六篇:Java异常处理

博主打算从0-1讲解下java基础教学&#xff0c;今天教学第五篇&#xff1a; Java异常处理。 异常处理是Java编程中重要的一部分&#xff0c;它允许开发人员在程序运行时检测和处理各种错误情况&#xff0c;以保证程序的稳定性和可靠性。在Java中&#xff0c;异常被表示为对象&am…