[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...


简易的实验

Log.hpp

#pragma once
#include <iostream>
#include<time.h>
#include<stdarg.h>//这个头文件是干啥的
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>//?
#include<unistd.h>
#include<stdlib.h>

#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 <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

这些头文件提供了实现该日志系统所需的基本库功能:

  • <iostream> 用于控制台输入输出。
  • <time.h> 用于处理时间。
  • <stdarg.h> 用于处理可变参数列表。
  • <sys/types.h>, <sys/stat.h>, <fcntl.h>, <unistd.h> 提供文件操作相关的系统调用。
  • <stdlib.h> 提供了一些标准库函数,如 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.Debuglog.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 <iostream>
#include <string>
#include <strings.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "Log.hpp"

// using func_t = std::function<std::string(const std::string&)>;
typedef std::function<std::string(const std::string&)> 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 <iostream>
#include <string>
#include <strings.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "Log.hpp"
  • <iostream>:用于标准输入输出流。
  • <string>:提供了 std::string 类。
  • <strings.h>:提供了 bzero 函数用于清空内存。
  • <cstring>:用于C字符串操作,比如 strerror 函数。
  • <sys/types.h>:定义了系统数据类型,如 socklen_t
  • <sys/socket.h>:提供了套接字接口。
  • <netinet/in.h>:定义了 Internet 地址族所需的数据结构和宏,如 struct sockaddr_inhtons 等。
  • <arpa/inet.h>:提供了用于处理IP地址的函数,如 inet_addr
  • "Log.hpp":自定义的日志系统,方便记录日志信息。

2. typedef 定义

typedef std::function<std::string(const std::string&)> 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;
  • defaultportdefaultip 定义了默认的端口号和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 结构体清零。
  • htonsinet_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 <memory>
#include <cstdio>
  • "UdpServer.hpp":包含之前定义的 UdpServer 类,用于创建和管理UDP服务器。
  • <memory>:提供智能指针 std::unique_ptr,用于管理动态分配的对象。
  • <cstdio>:提供C语言的标准输入输出函数,如 popenfgetspclose

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<UdpServer> svr(new UdpServer(port));

    svr->Init(/**/);
    svr->Run(ExcuteCommand);

    return 0;
}
  • main 函数是程序的入口,负责启动UDP服务器并处理命令行参数。
    • 首先检查命令行参数是否正确(即端口号是否提供),如果不正确则调用 Usage 函数输出使用方法并退出。
    • 使用 std::stoi 将命令行参数(端口号)转换为 uint16_t 类型。
    • 通过 std::unique_ptr 创建一个 UdpServer 实例,使用智能指针管理 UdpServer 对象的生命周期,确保资源在退出时被正确释放。
    • 调用 UdpServerInit() 方法初始化服务器。
    • 调用 Run() 方法开始服务器运行,并将 ExcuteCommand 函数作为回调函数传递给 UdpServer,服务器接收到客户端消息后将会调用该函数来执行命令。

6. 程序的工作流程

  1. 服务器启动时,用户通过命令行提供一个端口号。
  2. 服务器初始化并绑定到指定端口号。
  3. 服务器进入循环,等待客户端发送消息。
  4. 一旦服务器接收到消息,它会调用 ExcuteCommand 函数执行接收到的命令,并将结果返回给客户端。

7. 总结

通过 UdpServer 类实现网络通信,接收到客户端的消息后,通过 ExcuteCommand 函数执行来自客户端的命令。服务器使用智能指针管理 UdpServer 的生命周期,确保资源管理的安全性。

UdpClient.cc

客户端需要绑定吗?一定需要

这段代码实现了一个简单的UDP客户端,它可以向指定的服务器发送消息并接收服务器的响应。客户端通过命令行参数指定服务器的IP地址和端口号,使用UDP协议进行通信。

1. 头文件引用

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

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 结构体设置服务器的地址信息。
    • bzeroserver 结构体清空。
    • 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 做服务器~下篇文章,敬请期待~

相关推荐
捕鲸叉14 分钟前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer19 分钟前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
码农小旋风20 分钟前
详解K8S--声明式API
后端
Peter_chq21 分钟前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
Yaml444 分钟前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~1 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616881 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端
一坨阿亮1 小时前
Linux 使用中的问题
linux·运维
aloha_7891 小时前
从零记录搭建一个干净的mybatis环境
java·笔记·spring·spring cloud·maven·mybatis·springboot
青花瓷2 小时前
C++__XCode工程中Debug版本库向Release版本库的切换
c++·xcode