[Linux#49][UDP] 2w字详解 | socketaddr | 常用API | 实操:实现简易Udp传输

目录

套接字地址结构(sockaddr)

[1.Socket API](#1.Socket API)

2.sockaddr结构

[3. sockaddr、sockaddr_in 和 sockaddr_un 的关系](#3. sockaddr、sockaddr_in 和 sockaddr_un 的关系)

[sockaddr 结构体](#sockaddr 结构体)

[sockaddr_in 结构体(IPv4 套接字地址)](#sockaddr_in 结构体(IPv4 套接字地址))

[sockaddr_un 结构体(Unix域套接字地址)](#sockaddr_un 结构体(Unix域套接字地址))

[4. sockaddr 通用结构的意义](#4. sockaddr 通用结构的意义)

[5. 通用性带来的优势](#5. 通用性带来的优势)

[6. IPv4 与 IPv6 的地址表示](#6. IPv4 与 IPv6 的地址表示)

[7. 代码示例](#7. 代码示例)

[in_addr 结构体(用于表示IPv4地址)](#in_addr 结构体(用于表示IPv4地址))

[Socket 接口](#Socket 接口)

[1. 创建 Socket 文件描述符](#1. 创建 Socket 文件描述符)

[2. 绑定 bind 端口号 (服务器)](#2. 绑定 bind 端口号 (服务器))

[3. 开始监听 Socket (TCP 服务器)](#3. 开始监听 Socket (TCP 服务器))

[4. 接收连接请求 (TCP 服务器)](#4. 接收连接请求 (TCP 服务器))

[5. 建立连接 (TCP 客户端)](#5. 建立连接 (TCP 客户端))

[6. 设置套接字选项 (进阶)](#6. 设置套接字选项 (进阶))

[7. 地址转换函数](#7. 地址转换函数)

[8. 数据传输函数](#8. 数据传输函数)

sendto() (UDP)

recvfrom() (UDP)

[send() 和 recv() (TCP)](#send() 和 recv() (TCP))

[9. 实际使用中的注意事项](#9. 实际使用中的注意事项)

INADDR_ANY

客户端是否需要绑定?

[Listening Socket vs Connected Socket](#Listening Socket vs Connected Socket)

[10. TCP 通信流程](#10. TCP 通信流程)

[11. TCP vs UDP](#11. TCP vs UDP)

[12. popen() 和 pclose()](#12. popen() 和 pclose())

预备知识

简易的实验

Log.hpp

[1. 头文件的引用](#1. 头文件的引用)

[2. 宏定义](#2. 宏定义)

[3. Log 类](#3. Log 类)

成员变量

构造函数

[Enable 方法](#Enable 方法)

[levelToString 方法](#levelToString 方法)

[printLog 方法](#printLog 方法)

[printOneFile 方法](#printOneFile 方法)

[printClassFile 方法](#printClassFile 方法)

[重载的 operator()](#重载的 operator())

[4. 总结](#4. 总结)

Makefile

UdpServer.hpp

[1. 头文件的引用](#1. 头文件的引用)

[2. typedef 定义](#2. typedef 定义)

[3. 日志对象和错误码枚举](#3. 日志对象和错误码枚举)

[4. 全局变量](#4. 全局变量)

[5. UdpServer 类](#5. UdpServer 类)

构造函数

[Init 方法](#Init 方法)

[Run 方法](#Run 方法)

析构函数

[6. 总结](#6. 总结)

Main.cc

[1. 头文件引用](#1. 头文件引用)

[2. Usage 函数](#2. Usage 函数)

[3. Handler 函数](#3. Handler 函数)

[4. ExcuteCommand 函数](#4. ExcuteCommand 函数)

[5. main 函数](#5. main 函数)

[6. 程序的工作流程](#6. 程序的工作流程)

[7. 总结](#7. 总结)

UdpClient.cc

[1. 头文件引用](#1. 头文件引用)

[2. Usage 函数](#2. Usage 函数)

[3. 主程序 main](#3. 主程序 main)

参数检查与处理

服务器地址设置

创建UDP套接字

[客户端是否需要 bind?](#客户端是否需要 bind?)

消息发送和接收

关闭套接字

[4. 程序工作流程](#4. 程序工作流程)

[5. 总结](#5. 总结)

注意


上篇文章我们所说 ip+port ----->该主机上对应的服务进程,是全网中是唯一的一个进程!

ip+port就是套接字,socket

套接字地址结构(sockaddr)

1.Socket API

Socket API 是一层网络编程接口,抽象了底层的网络协议,定义在 netinet/in.h 中。它适用于多种网络通信方式,如 IPv4、IPv6,以及 UNIX 域套接字(用于本地进程间通信)。通过 Socket API,程序可以实现跨网络的进程间通信(如通过IP地址和端口号进行的网络通信),也可以实现本地的进程间通信。

常见 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);

2.sockaddr结构

我们可以看到上面struct sockaddr *addr出现次数挺多的。实际上在网络上通信的时候套接字种类是比较多的,下面是常见的三种:

  1. unix 域间套接字编程--同一个机器内
  2. 原始套接字编程--网络工具
  3. 网络套接字编程--用户间的网络通

设计者想将网络接口统一抽象化--参数的类型必须是统一的,底层是一种多态的设计

运用场景:

  • 网络套接字:运用于网络跨主机之间通信+本地通信
  • unix域间套接字: 本地通信
  • 我们现在在使用网络编程通信时是应用层调传输层的接口,而原始套接字:跳过传输层访问其他层协议中的有效数据。主要用于抓包,侦测网络情况。。

我们现在知道++套接字种类很多,它们应用的场景也是不一样的++。所以未来要完成这三种通信就需要有三套不同接口,但是思想上用的都是套接字的思想。因此接口设计者不想设计三套接口,只想设计一套接口,可以通过不通的参数,解决所有网络或者其他场景下的通信网络。

由于不同的通信方式(跨网络或本地通信)有不同的地址格式,套接字使用不同的结构体来封装地址信息:

  • sockaddr_in**:用于跨网络通信(例如通过 IP 和端口号进行通信)。**
  • sockaddr_un**:用于本地通信(通过文件路径进行通信)。**

为了解决这些不同地址格式的兼容性问题,套接字提供了一个通用的地址结构体 sockaddr,用于统一处理不同的地址结构。

3. sockaddrsockaddr_insockaddr_un 的关系

sockaddr 是一个通用的套接字地址结构,它为所有不同类型的通信方式提供了统一的接口。具体通信时,sockaddr 实际上指向特定的地址结构(如 sockaddr_insockaddr_un),然后通过强制类型转换来区分是哪种通信方式。

这种设计类似于面向对象编程中的"多态":sockaddr****可以看作一个"父类",而 sockaddr_in****和 sockaddr_un****是它的"子类 "。在程序中,套接字函数接受 sockaddr* 类型的参数,然后根据具体的通信类型进行处理。

sockaddr 结构体
复制代码
struct sockaddr {
    __SOCKADDR_COMMON(sa_); /* 公共数据:地址家族和长度 */
    char sa_data[14];       /* 地址数据 */
};
sockaddr_in 结构体(IPv4 套接字地址)
复制代码
struct sockaddr_in {
    __SOCKADDR_COMMON(sin_);
    in_port_t sin_port;      /* 端口号 */
    struct in_addr sin_addr; /* IP地址 */
    unsigned char sin_zero[sizeof(struct sockaddr) - 
                           __SOCKADDR_COMMON_SIZE - 
                           sizeof(in_port_t) - 
                           sizeof(struct in_addr)];
};
sockaddr_un 结构体(Unix域套接字地址)
复制代码
struct sockaddr_un {
    __SOCKADDR_COMMON(sun_);
    char sun_path[108]; /* 文件路径 */
};

4. sockaddr 通用结构的意义

sockaddr 作为通用结构,它的前16个比特用于存储协议家族(sa_family 字段)。这个字段用来表明使用的是哪种通信方式:

  • AF_INET:IPv4网络通信。
  • AF_INET6:IPv6网络通信。
  • AF_UNIX:本地通信(UNIX 域套接字)。

通过这种设计,++Socket API 可以通过统一的函数接口,处理不同类型的地址格式。开发者只需要将具体的地址结构转换为 sockaddr++,并设置协议家族字段,套接字函数就能识别出应该进行哪种通信。

5. 通用性带来的优势

Socket API 的这种设计带来了极大的通用性,使得开发者在同一套代码中可以处理不同的协议类型。例如,函数 sendto()** recvfrom()**可以接受 sockaddr作为参数,无论是处理 IPv4、IPv6 还是 UNIX Domain Socket,代码都不需要做出太大改动。*

复制代码
int sendto(int sockfd, const void *msg, size_t len, int flags,
           const struct sockaddr *dest_addr, socklen_t addrlen);

在这个函数中,dest_addr 是一个通用的 sockaddr*,程序只需根据实际使用的通信方式(如 IPv4 或 IPv6)对其进行强制类型转换即可。

6. IPv4 与 IPv6 的地址表示

  • IPv4 地址格式 使用 sockaddr_in 结构体,地址类型是 AF_INET,端口号和IP地址需要转换为网络字节序(大端序)。
  • IPv6 地址格式 使用 sockaddr_in6 结构体,地址类型是 AF_INET6

7. 代码示例

in_addr 结构体(用于表示IPv4地址)
复制代码
typedef uint32_t in_addr_t;
struct in_addr {
    in_addr_t s_addr; // 32位IP地址
};

in_addr 是一个32位的整数,用来表示IPv4的IP地址。通信过程中,IP地址通常是通过字符串格式(如 "192.168.1.1")转换为 in_addr_t 类型的数值来表示。

总结

  • 通过 sockaddr 结构体,Socket API 实现了网络通信和本地通信的统一接口
  • 它的设计理念类似于"多态",即通过一个通用的接口来处理多种类型的地址格式

Socket 接口

1. 创建 Socket 文件描述符

在 TCP 和 UDP 通信中,首先要创建一个 Socket 文件描述符,它本质上是一个网络文件。其函数原型为:

  • 功能:打开一个网络通讯端口,返回一个文件描述符,如果失败,返回 -1。
  • 参数
    • domain:协议域,如 AF_INET(IPv4)、AF_INET6(IPv6)、AF_LOCAL(Unix域套接字)。
    • type:套接字类型,如 SOCK_STREAM(字节流,TCP)、SOCK_DGRAM(数据报,UDP)。
    • protocol:协议类别,通常设置为 0,自动推导出对应的协议,如 TCP/UDP。

2. 绑定 bind 端口号 (服务器)

在服务器端,必须绑定一个 IP 地址和端口号,以便客户端可以与服务器建立通信。bind() 函数用于将套接字与 IP 和端口号绑定:

  • 功能:将指定的 IP 和端口号绑定到套接字,使之监听指定地址。
  • 参数
    • socket:套接字文件描述符。
    • address:存储地址和端口号的结构体指针。
    • address_len:地址结构体的长度。

3. 开始监听 Socket (TCP 服务器)

在服务器中,调用 listen() 函数使套接字进入监听状态,准备接受连接请求:

  • 功能:让服务器套接字进入监听状态,准备接收客户端连接。
  • 参数
    • socket:监听套接字描述符。
    • backlog:全连接队列的最大长度,用于处理多个客户端连接请求。

4. 接收连接请求 (TCP 服务器)

服务器使用 accept() 从连接队列中提取下一个连接请求,并返回新的套接字用于与客户端通信:

  • 功能:获取一个已完成的连接请求,并返回新的套接字用于客户端通信。
  • 参数
    • socket:监听套接字。
    • address:存储客户端的地址信息。
    • address_len:地址结构的长度。

5. 建立连接 (TCP 客户端)

客户端通过 connect() 向服务器发起连接请求:

  • 功能:TCP 客户端使用该函数建立与服务器的连接。
  • 参数
    • sockfd:用于通信的套接字文件描述符。
    • addr:服务器的地址。
    • addrlen:地址长度。

6. 设置套接字选项 (进阶)

通过 setsockopt() 可以设置套接字的各种属性,例如端口重用等高级功能:

  • 功能:设置套接字的选项,如端口重用等。
  • 参数
    • sockfd:套接字文件描述符。
    • level:选项的层次(如 SOL_SOCKETIPPROTO_TCP 等)。
    • optname:选项名。
    • optval:指向设置值的指针。
    • optlen:设置值的长度。

7. 地址转换函数

IP 地址可以以字符串或整数形式存在。常见的地址转换函数包括:

  • 字符串 IP 转整数 IP

    in_addr_t inet_addr(const char *strptr);
    int inet_pton(int family, const char *strptr, void *addrptr);

  • 整数 IP 转字符串 IP

    char *inet_ntoa(struct in_addr addr);
    const char *inet_ntop(int family, const void *src, char *dest, size_t len);

8. 数据传输函数

sendto() (UDP)

用于在 UDP 协议下发送数据:

  • 功能:发送数据到指定地址。
  • 参数
    • sockfd:套接字文件描述符。
    • buf:要发送的数据。
    • len:数据长度。
recvfrom() (UDP)

接收来自远程主机的数据:

  • 功能:接收数据。
  • 参数
    • sockfd:套接字文件描述符。
    • buf:存放接收数据的缓冲区。
send()recv() (TCP)
  • send() 用于在 TCP 协议下发送数据。

    ssize_t send(int sockfd, const void *buf, size_t len, int flags);

  • recv() 用于接收 TCP 协议下的数据。

    ssize_t recv(int sockfd, void *buf, size_t len, int flags);

9. 实际使用中的注意事项

INADDR_ANY

在服务器端,INADDR_ANY(0.0.0.0)可以让服务器监听所有可用的网络接口,而不必指定具体的 IP 地址。这种方式提高了代码的可移植性。

复制代码
local.sin_addr.s_addr = INADDR_ANY;
客户端是否需要绑定?

客户端不需要手动绑定端口,操作系统会自动选择一个可用的端口。除非明确需要使用固定的端口,否则不建议手动绑定。

Listening Socket vs Connected Socket
  • Listening Socket:服务器使用它来监听连接请求。它在整个服务器生命周期内存在。
  • Connected Socket:服务器接收连接请求后,用于与客户端通信的套接字。每个客户端有一个独立的连接套接字。

10. TCP 通信流程

  1. 服务器初始化 :调用 socket() 创建套接字,调用 bind() 绑定地址和端口,调用 listen() 进入监听状态。
  2. 客户端连接 :客户端通过 socket() 创建套接字,使用 connect() 发起连接请求。
  3. 三次握手:TCP 客户端与服务器通过三次握手建立连接。
  4. 数据传输 :连接建立后,双方可以通过 send()recv() 进行数据传输。
  5. 断开连接:通过四次挥手,客户端和服务器断开连接。

这里只是简单提一下,要有个印象,下篇文章将详细讲解~

11. TCP vs UDP

  • TCP :可靠的连接,字节流传输,保证数据顺序。
  • UDP :不可靠传输,数据报传输,适用于实时通信。

12. popen()pclose()

popen() 创建一个管道用于与子进程通信:来实现命令行通信

复制代码
FILE *popen(const char *command, const char *type);

pclose() 关闭通过 popen() 打开的文件指针:

复制代码
int pclose(FILE *stream);

使用场景:Udpcommand

结语

Socket 编程是网络编程的基础,通过 Socket API,开发者可以实现 TCP 和 UDP 通信。了解每个函数的作用、参数和使用场景,可以帮助开发者在构建高效、稳定的网络应用时得心应手。


预备知识

这个部分的设计非常重要

复制代码
struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port_); //主机转网络,16位
        local.sin_addr.s_addr = inet_addr(ip_.c_str()); 
//1. string -> uint32_t 2. uint32_t必须是网络序列的 // ??
        // local.sin_addr.s_addr = htonl(INADDR_ANY);

端口号,是要在网络部分,来回传递的

一个关于绑定 IP 的问题

转化代码 local.sin_addr.s_addr = inet_addr(ip_.c_str());

一个关于 port 的问题

转化代码 local.sin_addr.s_addr = htonl(INADDR_ANY);

关于 port 的测试报错,要 sudo 用超级用户去绑

0,1023\]: 系统内定的端口号,一般都要有固定的应用层协议使用,http:80,https:443,mysql:3306... *** ** * ** *** ## 简易的实验 ![](https://img-blog.csdnimg.cn/img_convert/5275f16521d819c0568fc710ead01633.png) ## Log.hpp #pragma once #include #include #include//这个头文件是干啥的 #include #include #include//? #include #include #define SIZE 1024 #define Info 0 #define Debug 1 #define Warning 2 #define Error 3 #define Fatal 4 #define Screen 1 #define Onefile 2 #define Classfile 3 #define LogFile "log.txt" class Log { public: Log() { printfMethod=Screen; path="./log/"; } void Enable(int method) { printfMethod=method; } //将日志级别转化为对应的字符串表示形式 std::string levelToString(int level) { switch (level) { case Info: return "Info"; case Debug: return "Debug"; case Warning: return "Warning"; case Error: return "Error"; case Fatal: return "Fatal"; default: return "None"; } } //根据日志的输出方式,将日志输出到相应的目标 void printLog(int level, const std::string &logtxt) { switch (printMethod) { case Screen: std::cout << logtxt << std::endl; break; case Onefile: printOneFile(LogFile, logtxt); break; case Classfile: printClassFile(level, logtxt); break; default: break; } } //将日志输出到指定文件,文件名通过将路径和文件名拼接得到,并以追加的方式写入日志 void printOneFile(const std::string &logname,const std::string &logtxt) { std::string _logname=path+logname; int fd=open(_logname.c_str(),O_WRONLY | O_CREAT | O_APPEND,0666); if(fd<0) return; write(fd, logtxt.c_str(), logtxt.size()); close(fd); } //根据日志级别输出到不同的文件 void printClassFile(int level, const std::string &logtxt) { std::string filename = LogFile; filename += "."; filename += levelToString(level); printOneFile(filename, logtxt); } //重载的operator()使得Log类的实例可以像函数一样调用,简化日志记录的使用 //使用可变参数处理日志消息的格式化 void operator()(int level, const char *format, ...) { time_t t = time(nullptr); struct tm *ctime = localtime(&t); char leftbuffer[SIZE]; snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(), ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday, ctime->tm_hour, ctime->tm_min, ctime->tm_sec); va_list s; va_start(s, format); char rightbuffer[SIZE]; vsnprintf(rightbuffer, sizeof(rightbuffer), format, s); va_end(s); char logtxt[SIZE * 2]; snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer); printLog(level, logtxt); } private: int printfMethod; std::string path; }; 这段代码实现了一个简单的日志系统,允许将日志输出到控制台、单一日志文件或根据日志级别分类的不同文件中。下面是对代码的详细解释和整理。分析: #### 1. 头文件的引用 #include #include #include #include #include #include #include #include 这些头文件提供了实现该日志系统所需的基本库功能: * `` 用于控制台输入输出。 * `` 用于处理时间。 * `` 用于处理可变参数列表。 * ``, ``, ``, `` 提供文件操作相关的系统调用。 * `` 提供了一些标准库函数,如 `exit()` 等。 #### 2. 宏定义 #define SIZE 1024 #define Info 0 #define Debug 1 #define Warning 2 #define Error 3 #define Fatal 4 #define Screen 1 #define Onefile 2 #define Classfile 3 #define LogFile "log.txt" 这些宏定义了一些常量: * `SIZE` 定义了缓冲区的大小。 * `Info`, `Debug`, `Warning`, `Error`, `Fatal` 定义了日志级别。 * `Screen`, `Onefile`, `Classfile` 定义了日志的输出方式。 * `LogFile` 定义了默认的日志文件名。 #### 3. `Log` 类 `Log` 类是日志系统的核心,包含了日志输出的主要功能。 ##### 成员变量 private: int printMethod; std::string path; * `printMethod` 用于保存日志的输出方式(屏幕、单一文件、分类文件)。 * `path` 保存日志文件的路径。 ##### 构造函数 public: Log() { printMethod = Screen; path = "./log/"; } * 构造函数初始化日志输出方式为屏幕输出 (`Screen`),并设置日志文件的默认路径为 `"./log/"`。 ##### `Enable` 方法 void Enable(int method) { printMethod = method; } * 这个方法用于设置日志的输出方式。 ##### `levelToString` 方法 std::string levelToString(int level) { switch (level) { case Info: return "Info"; case Debug: return "Debug"; case Warning: return "Warning"; case Error: return "Error"; case Fatal: return "Fatal"; default: return "None"; } } * 将日志级别转换为对应的字符串表示形式。 ##### `printLog` 方法 void printLog(int level, const std::string &logtxt) { switch (printMethod) { case Screen: std::cout << logtxt << std::endl; break; case Onefile: printOneFile(LogFile, logtxt); break; case Classfile: printClassFile(level, logtxt); break; default: break; } } * 根据日志输出方式,将日志输出到相应的目标(屏幕、单一文件或分类文件)。 ##### `printOneFile` 方法 void printOneFile(const std::string &logname, const std::string &logtxt) { std::string _logname = path + logname; int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); if (fd < 0) return; write(fd, logtxt.c_str(), logtxt.size()); close(fd); } * 将日志输出到指定文件。文件名通过将路径和文件名拼接得到,并以追加方式写入日志。 ##### `printClassFile` 方法 void printClassFile(int level, const std::string &logtxt) { std::string filename = LogFile; filename += "."; filename += levelToString(level); printOneFile(filename, logtxt); } * 根据日志级别输出到不同的文件,例如 `log.txt.Debug` 或 `log.txt.Error`。 ##### 重载的 `operator()` void operator()(int level, const char *format, ...) { time_t t = time(nullptr); struct tm *ctime = localtime(&t); char leftbuffer[SIZE]; snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(), ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday, ctime->tm_hour, ctime->tm_min, ctime->tm_sec); va_list s; va_start(s, format); char rightbuffer[SIZE]; vsnprintf(rightbuffer, sizeof(rightbuffer), format, s); va_end(s); char logtxt[SIZE * 2]; snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer); printLog(level, logtxt); } * 重载的 `operator()` 使得 `Log` 类的实例可以像函数一样调用,简化了日志记录的使用。 * 使用可变参数处理日志消息的格式化,并根据当前时间戳和日志级别生成日志消息。 #### 4. 总结 实现了一个简单而灵活的日志系统,可以根据用户需求将日志输出到不同的目标。日志级别和输出方式都可以通过方法进行配置,使用方便。预留了部分扩展功能,例如注释掉的 `logmessage` 方法,可以进行进一步扩展和定制。 ## Makefile .PHONY:all all:udpserver udpclient udpserver:Main.cc g++ -o $@ $^ -std=c++11 udpclient:UdpClient.cc g++ -o $@ $^ -std=c++11 .PHONY:clean clean: rm -f udpserver udpclient ## UdpServer.hpp #pragma once #include #include #include #include #include #include #include #include #include #include "Log.hpp" // using func_t = std::function; typedef std::function func_t; Log lg; enum{ SOCKET_ERR=1, BIND_ERR }; uint16_t defaultport = 8080; std::string defaultip = "0.0.0.0"; const int size = 1024; class UdpServer{ public: UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip):sockfd_(0), port_(port), ip_(ip),isrunning_(false) {} void Init() { // 1. 创建udp socket sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // PF_INET if(sockfd_ < 0) { lg(Fatal, "socket create error, sockfd: %d", sockfd_); exit(SOCKET_ERR); } lg(Info, "socket create success, sockfd: %d", sockfd_); // 2. bind socket struct sockaddr_in local; bzero(&local, sizeof(local)); local.sin_family = AF_INET; local.sin_port = htons(port_); //需要保证我的端口号是网络字节序列,因为该端口号是要给对方发送的 local.sin_addr.s_addr = inet_addr(ip_.c_str()); //1. string -> uint32_t 2. uint32_t必须是网络序列的 // ?? // local.sin_addr.s_addr = htonl(INADDR_ANY); if(bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) < 0) { lg(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno)); exit(BIND_ERR); } lg(Info, "bind success, errno: %d, err string: %s", errno, strerror(errno)); } void Run(func_t func) // 对代码进行分层 { isrunning_ = true; char inbuffer[size]; while(isrunning_) { struct sockaddr_in client; socklen_t len = sizeof(client); ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len); if(n < 0) { lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno)); continue; } inbuffer[n] = 0; std::string info = inbuffer; std::string echo_string = func(info); sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (const sockaddr*)&client, len); } } ~UdpServer() { if(sockfd_>0) close(sockfd_); } private: int sockfd_; // 网路文件描述符 std::string ip_; // 任意地址bind 0 uint16_t port_; // 表明服务器进程的端口号 bool isrunning_; }; 这段代码实现了一个简单的UDP服务器,提供了初始化和运行的功能,并通过一个回调函数处理接收到的数据。以下是对代码的详细解释和整理。 #### 1. 头文件的引用 #include #include #include #include #include #include #include #include #include #include "Log.hpp" * ``:用于标准输入输出流。 * ``:提供了 `std::string` 类。 * ``:提供了 `bzero` 函数用于清空内存。 * ``:用于C字符串操作,比如 `strerror` 函数。 * ``:定义了系统数据类型,如 `socklen_t`。 * ``:提供了套接字接口。 * ``:定义了 Internet 地址族所需的数据结构和宏,如 `struct sockaddr_in` 和 `htons` 等。 * ``:提供了用于处理IP地址的函数,如 `inet_addr`。 * `"Log.hpp"`:自定义的日志系统,方便记录日志信息。 #### 2. `typedef` 定义 typedef std::function func_t; * `func_t` 定义了一个类型,该类型表示一个接受 `std::string` 参数并返回 `std::string` 的函数对象。这种类型用于处理接收到的数据。 #### 3. 日志对象和错误码枚举 Log lg; enum{ SOCKET_ERR = 1, BIND_ERR }; * `Log lg;`:创建一个全局的日志对象 `lg`,用于记录日志信息。 * `enum` 定义了一些错误码,便于在出现错误时通过 `exit` 函数终止程序。 #### 4. 全局变量 uint16_t defaultport = 8080; std::string defaultip = "0.0.0.0"; const int size = 1024; * `defaultport` 和 `defaultip` 定义了默认的端口号和IP地址。 * `size` 定义了接收缓冲区的大小。 #### 5. `UdpServer` 类 `UdpServer` 类实现了UDP服务器的核心功能,包括初始化、运行和析构。 ##### 构造函数 UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip) : sockfd_(0), port_(port), ip_(ip), isrunning_(false) {} * 构造函数初始化服务器的端口、IP、文件描述符等变量,默认使用全局定义的端口和IP。 ##### `Init` 方法 void Init() { sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); if(sockfd_ < 0) { lg(Fatal, "socket create error, sockfd: %d", sockfd_); exit(SOCKET_ERR); } lg(Info, "socket create success, sockfd: %d", sockfd_); struct sockaddr_in local; bzero(&local, sizeof(local)); local.sin_family = AF_INET; local.sin_port = htons(port_); local.sin_addr.s_addr = inet_addr(ip_.c_str()); if(bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) < 0) { lg(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno)); exit(BIND_ERR); } lg(Info, "bind success, errno: %d, err string: %s", errno, strerror(errno)); } * `socket` 函数创建一个UDP套接字,如果失败,则记录日志并退出。 * `bzero` 函数将 `local` 结构体清零。 * `htons` 和 `inet_addr` 函数用于处理端口号和IP地址的转换。 * `bind` 函数将套接字绑定到指定的IP地址和端口号。如果绑定失败,则记录日志并退出。 ##### `Run` 方法 对**代码进行分层** void Run(func_t func) { isrunning_ = true; char inbuffer[size]; while(isrunning_) { struct sockaddr_in client; socklen_t len = sizeof(client); ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len); if(n < 0) { lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno)); continue; } inbuffer[n] = 0; std::string info = inbuffer; std::string echo_string = func(info); sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (const sockaddr*)&client, len); } } * `Run` 方法是服务器的核心运行逻辑。它接受一个 `func_t` 类型的函数,用于处理接收到的数据。 * 在循环中,服务器使用 `recvfrom` 接收来自客户端的数据,处理后再使用 `sendto` 发送响应。 * 如果 `recvfrom` 失败,记录警告日志并继续下一次接收。 ##### 析构函数 ~UdpServer() { if(sockfd_ > 0) close(sockfd_); } * 析构函数负责关闭套接字,释放资源。 #### 6. 总结 实现一个UDP服务器的基本框架,通过日志系统记录服务器的运行状态,并允许用户通过回调函数自定义数据处理逻辑。服务器可以接收客户端的数据,并根据用户定义的处理逻辑返回相应的数据。 *** ** * ** *** ## Main.cc 使用 `UdpServer` 类构建一个简单的UDP服务器,并且通过一个回调函数处理接收到的命令。服务器的核心功能是接收客户端发送的消息,然后执行特定的命令,并将结果返回给客户端。 #### 1. 头文件引用 #include "UdpServer.hpp" #include #include * `"UdpServer.hpp"`:包含之前定义的 `UdpServer` 类,用于创建和管理UDP服务器。 * ``:提供智能指针 `std::unique_ptr`,用于管理动态分配的对象。 * ``:提供C语言的标准输入输出函数,如 `popen`、`fgets` 和 `pclose`。 #### 2. `Usage` 函数 void Usage(std::string proc) { std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl; } * `Usage` 函数用于在命令行参数不正确时,向用户显示正确的使用方法。它会输出如何正确地执行程序及端口号的要求(1024以上)。 #### 3. `Handler` 函数 std::string Handler(const std::string &str) { std::string res = "Server get a message: "; res += str; std::cout << res << std::endl; // pid_t id = fork(); // if(id == 0) // { // // ls -a -l -> "ls" "-a" "-l" // // exec*(); // } return res; } * 该函数用于处理从客户端接收到的消息,简单地将接收到的消息加上前缀 `"Server get a message: "` 后返回,并在服务器端输出消息内容。 * 函数中注释掉的 `fork()` 和 `exec()` 代码提示可能有计划在子进程中执行命令,但目前这部分未实现。 #### 4. `ExcuteCommand` 函数 std::string ExcuteCommand(const std::string &cmd) { FILE *fp = popen(cmd.c_str(), "r"); if(nullptr == fp) { perror("popen"); return "error"; } std::string result; char buffer[4096]; while(true) { char *ok = fgets(buffer, sizeof(buffer), fp); if(ok == nullptr) break; result += buffer; } pclose(fp); return result; } * 该函数使用 **popen()****执行来自客户端的命令**,并将命令的输出结果返回给客户端。其工作流程是: * * 通过 `popen` 打开子进程并执行传入的命令。 * 使用 `fgets` 逐行读取子进程的输出,并将其存储到 `result` 字符串中。 * 读取完成后通过 `pclose` 关闭文件指针,返回命令的执行结果。 #### 5. `main` 函数 int main(int argc, char *argv[]) { if(argc != 2) { Usage(argv[0]); exit(0); } uint16_t port = std::stoi(argv[1]); std::unique_ptr svr(new UdpServer(port)); svr->Init(/**/); svr->Run(ExcuteCommand); return 0; } * `main` 函数是程序的入口,负责启动UDP服务器并处理命令行参数。 * * 首先检查命令行参数是否正确(即端口号是否提供),如果不正确则调用 `Usage` 函数输出使用方法并退出。 * 使用 `std::stoi` 将命令行参数(端口号)转换为 `uint16_t` 类型。 * 通过 `std::unique_ptr` 创建一个 `UdpServer` 实例,使用智能指针管理 `UdpServer` 对象的生命周期,确保资源在退出时被正确释放。 * 调用 `UdpServer` 的 `Init()` 方法初始化服务器。 * 调用 `Run()` 方法开始服务器运行,并将 `ExcuteCommand` 函数作为回调函数传递给 `UdpServer`,服务器接收到客户端消息后将会调用该函数来执行命令。 #### 6. 程序的工作流程 1. 服务器启动时,用户通过命令行提供一个端口号。 2. 服务器初始化并绑定到指定端口号。 3. 服务器进入循环,等待客户端发送消息。 4. 一旦服务器接收到消息,它会调用 `ExcuteCommand` 函数执行接收到的命令,并将结果返回给客户端。 #### 7. 总结 通过 `UdpServer` 类实现网络通信,接收到客户端的消息后,通过 `ExcuteCommand` 函数执行来自客户端的命令。服务器使用智能指针管理 `UdpServer` 的生命周期,确保资源管理的安全性。 ## UdpClient.cc > 客户端需要绑定吗?一定需要 这段代码实现了一个简单的UDP客户端,它可以向指定的服务器发送消息并接收服务器的响应。客户端通过命令行参数指定服务器的IP地址和端口号,使用UDP协议进行通信。 #### 1. 头文件引用 #include #include #include #include #include #include #include #include #### 2. `Usage` 函数 void Usage(std::string proc) { std::cout << "\n\rUsage: " << proc << " serverip serverport\n" << std::endl; } * 该函数用于提示用户如何正确使用命令行参数。需要输入程序名、服务器IP地址和端口号。 #### 3. 主程序 `main` ##### 参数检查与处理 int main(int argc, char *argv[]) { if (argc != 3) { Usage(argv[0]); exit(0); } std::string serverip = argv[1]; uint16_t serverport = std::stoi(argv[2]); * 主程序首先检查命令行参数的数量,确保用户输入了正确数量的参数(服务器IP和端口号)。如果参数不对,调用 `Usage()` 输出提示并退出。 * `std::stoi(argv[2])` 将输入的端口号从字符串转换为 `uint16_t` 类型。 ##### 服务器地址设置 struct sockaddr_in server; bzero(&server, sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(serverport); // 转换端口号为网络字节序 server.sin_addr.s_addr = inet_addr(serverip.c_str()); socklen_t len = sizeof(server); * 通过 `sockaddr_in` 结构体设置服务器的地址信息。 * * `bzero` 将 `server` 结构体清空。 * `sin_family` 设置为 `AF_INET` 表示使用IPv4地址族。 * `sin_port` 使用 `htons` 将端口号转换为网络字节序(大端序),确保与服务器端匹配。 * `sin_addr.s_addr` 使用 `inet_addr()` 将服务器IP地址字符串转换为合适的格式。 ##### 创建UDP套接字 int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd < 0) { cout << "socket error" << endl; return 1; } * `socket()` 创建一个UDP套接字。如果创建失败(返回值小于0),程序输出错误信息并退出。 * * `AF_INET` 指定使用IPv4。 * `SOCK_DGRAM` 指定UDP协议。 ##### 客户端是否需要 `bind`? // client 要bind吗?要!只不过不需要用户显示的bind!一般有OS自由随机选择! // 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此! // 其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以! // 系统什么时候给我bind呢?首次发送数据的时候 * 注释解释了客户端的 `bind` 行为。客户端不需要显式调用 `bind`,操作系统会在首次发送数据时自动选择一个可用的端口号。 ##### 消息发送和接收 string message; char buffer[1024]; while (true) { cout << "Please Enter@ "; getline(cin, message); // 发送数据到服务器 sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, len); struct sockaddr_in temp; socklen_t len = sizeof(temp); // 接收服务器响应 ssize_t s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len); if(s > 0) { buffer[s] = 0; cout << buffer << endl; } } * 通过循环持续等待用户输入消息,使用 `getline()` 获取用户输入的字符串。 * `sendto()` 函数将输入的消息发送到服务器,参数包括: * * `sockfd`:套接字描述符。 * `message.c_str()`:要发送的数据。 * `message.size()`:数据长度。 * `(struct sockaddr *)&server`:服务器的地址信息。 * `recvfrom()` 函数从服务器接收响应: * * `buffer` 用于存储接收到的数据,最大长度为1023。 * 数据接收到后,将结尾补上空字符(`buffer[s] = 0`)并输出。 ##### 关闭套接字 close(sockfd); return 0; } * 当程序退出时,使用 `close()` 关闭套接字,释放资源。 #### 4. 程序工作流程 1. 启动客户端时,用户需要提供服务器的IP地址和端口号。 2. 创建UDP套接字,并将服务器的地址信息(IP和端口)封装在 `sockaddr_in` 结构体中。 3. 客户端进入一个循环,等待用户输入消息。 4. 用户输入消息后,客户端使用 `sendto()` 将消息发送到服务器。 5. 客户端通过 `recvfrom()` 等待服务器的响应,并将接收到的数据打印到控制台。 6. 程序关闭套接字并退出。 #### 5. 总结 该程序实现了一个UDP客户端,可以向指定的服务器发送消息并接收响应。通过简单的 `sendto()` 和 `recvfrom()` 函数,客户端能够与UDP服务器进行通信。 ### 注意 我们要打开我们的云服务器的特定的端口--开放端口的行为 下篇文章将继续进行优化,例如添加 safecheck command,实现一个聊天室,window 做客户端,linux 做服务器\~下篇文章,敬请期待\~

相关推荐
Asthenia04121 分钟前
JUC:CompletableFuture 详细用法讲解
后端
良许Linux6 分钟前
为什么我学了几天 STM32 感觉一脸茫然?
linux
ん贤7 分钟前
2024第十五届蓝桥杯大赛软件赛省赛C/C++ 大学 B 组
c语言·数据结构·c++·经验分享·笔记·算法·蓝桥杯
吴梓穆7 分钟前
UE5学习笔记 FPS游戏制作42 按钮添加回调函数
笔记·学习·ue5
吴梓穆11 分钟前
UE5学习笔记 FPS游戏制作39 制作一个带有背景的预制面板 使用overlay和nameSlot
笔记·学习·ue5
良许Linux17 分钟前
为什么程序员必须坚持写技术博客?
linux
azaz_plus18 分钟前
Linux makefile的一些语法
linux·makefile
freejackman22 分钟前
MySQL 基础入门
数据库·后端·sql·mysql
圈圈编码22 分钟前
WebSocket
java·网络·spring boot·websocket·网络协议·spring
奔跑吧 android23 分钟前
《Linux内存管理:实验驱动的深度探索》【附录】【实验环境搭建 4】【Qemu 如何模拟numa架构】
linux·qemu·内存管理·kernel