当前位置:首页 » 《关注互联网》 » 正文

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

23 人参与  2024年09月21日 12:40  分类 : 《关注互联网》  评论

点击全文阅读


目录

套接字地址结构(sockaddr)

1.Socket API

2.sockaddr结构

3. sockaddr、sockaddr_in 和 sockaddr_un 的关系

sockaddr 结构体

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

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

4. sockaddr 通用结构的意义

5. 通用性带来的优势

6. IPv4 与 IPv6 的地址表示

7. 代码示例

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

Socket 接口

1. 创建 Socket 文件描述符

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

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

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

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

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

7. 地址转换函数

8. 数据传输函数

sendto() (UDP)

recvfrom() (UDP)

send() 和 recv() (TCP)

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

INADDR_ANY

客户端是否需要绑定?

Listening Socket vs Connected Socket

10. TCP 通信流程

11. TCP vs UDP

12. popen() 和 pclose()

预备知识

简易的实验

Log.hpp

1. 头文件的引用

2. 宏定义

3. Log 类

成员变量

构造函数

Enable 方法

levelToString 方法

printLog 方法

printOneFile 方法

printClassFile 方法

重载的 operator()

4. 总结

Makefile

UdpServer.hpp

1. 头文件的引用

2. typedef 定义

3. 日志对象和错误码枚举

4. 全局变量

5. UdpServer 类

构造函数

Init 方法

Run 方法

析构函数

6. 总结

Main.cc

1. 头文件引用

2. Usage 函数

3. Handler 函数

4. ExcuteCommand 函数

5. main 函数

6. 程序的工作流程

7. 总结

UdpClient.cc

1. 头文件引用

2. Usage 函数

3. 主程序 main

参数检查与处理

服务器地址设置

创建UDP套接字

客户端是否需要 bind?

消息发送和接收

关闭套接字

4. 程序工作流程

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出现次数挺多的。实际上在网络上通信的时候套接字种类是比较多的,下面是常见的三种:

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

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

运用场景:

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

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

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

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

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

3. sockaddrsockaddr_insockaddr_un 的关系

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

这种设计类似于面向对象编程中的“多态”:sockaddr 可以看作一个“父类”,而 sockaddr_insockaddr_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 通信流程

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

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

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:allall:udpserver udpclientudpserver:Main.ccg++ -o $@ $^ -std=c++11udpclient:UdpClient.ccg++ -o $@ $^ -std=c++11.PHONY:cleanclean: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. 程序的工作流程

服务器启动时,用户通过命令行提供一个端口号。服务器初始化并绑定到指定端口号。服务器进入循环,等待客户端发送消息。一旦服务器接收到消息,它会调用 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. 程序工作流程

启动客户端时,用户需要提供服务器的IP地址和端口号。创建UDP套接字,并将服务器的地址信息(IP和端口)封装在 sockaddr_in 结构体中。客户端进入一个循环,等待用户输入消息。用户输入消息后,客户端使用 sendto() 将消息发送到服务器。客户端通过 recvfrom() 等待服务器的响应,并将接收到的数据打印到控制台。程序关闭套接字并退出。

5. 总结

该程序实现了一个UDP客户端,可以向指定的服务器发送消息并接收响应。通过简单的 sendto()recvfrom() 函数,客户端能够与UDP服务器进行通信。

注意

我们要打开我们的云服务器的特定的端口--开放端口的行为

下篇文章将继续进行优化,例如添加 safecheck command,实现一个聊天室,window 做客户端,linux 做服务器~下篇文章,敬请期待~


点击全文阅读


本文链接:http://zhangshiyu.com/post/162657.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1