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

文章目录

  • 一、预备知识
    • 理解源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地址

大部分数据的传输都是跨局域网的,数据在传输过程中会经过若干个路由器,最终才能到达对端主机:【Linux】Socket编程接口 | 实现简单的UDP网络程序 (1)

源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协议足够简单。

网络字节序

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

大小端的概念:

  • 大端模式: 数据的高字节内容保存在内存的低地址处,数据的低字节内容保存在内存的高地址处。
  • 小端模式: 数据的高字节内容保存在内存的高地址处,数据的低字节内容保存在内存的低地址处。
    【Linux】Socket编程接口 | 实现简单的UDP网络程序 (2)

网络规定:

  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*不需要我们手动进行释放:
【Linux】Socket编程接口 | 实现简单的UDP网络程序 (3)

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

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

运行结果如下:【Linux】Socket编程接口 | 实现简单的UDP网络程序 (4)

因为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个比特位都是一样的,这个字段叫做协议家族。

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

  • 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. 创建socketint 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:allall : udp_server udp_clientudp_server : Main.ccg++ - o $@ $ ^ -std = c++17udp_client:UdpClient.ccg++ - o $@ $ ^ -std = c++17.PHONY:cleanclean :rm - f udp_serverrm - f udp_client

本地测试

使用本地环回地址 - 127.0.0.1

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

在执行 netstat -naup 命令后,显示以下内容:
【Linux】Socket编程接口 | 实现简单的UDP网络程序 (7)

这里的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,此时当我们重新编译程序再次运行服务端的时候会发现服务端绑定失败:【Linux】Socket编程接口 | 实现简单的UDP网络程序 (8)

由于云服务器的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 8888int 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;}
  • 运行:【Linux】Socket编程接口 | 实现简单的UDP网络程序 (9)

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

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

【Linux】Socket编程接口 | 实现简单的UDP网络程序 (2024)
Top Articles
Latest Posts
Article information

Author: Arielle Torp

Last Updated:

Views: 6332

Rating: 4 / 5 (41 voted)

Reviews: 80% of readers found this page helpful

Author information

Name: Arielle Torp

Birthday: 1997-09-20

Address: 87313 Erdman Vista, North Dustinborough, WA 37563

Phone: +97216742823598

Job: Central Technology Officer

Hobby: Taekwondo, Macrame, Foreign language learning, Kite flying, Cooking, Skiing, Computer programming

Introduction: My name is Arielle Torp, I am a comfortable, kind, zealous, lovely, jolly, colorful, adventurous person who loves writing and wants to share my knowledge and understanding with you.