计算机网络socket编程(2)_UDP网络编程实现网络字典

个人主页:C++忠实粉丝
欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 C++忠实粉丝 原创

计算机网络socket编程(2)_UDP网络编程实现网络字典

收录于专栏【计算机网络
本专栏旨在分享学习计算机网络的一点学习笔记,欢迎大家在评论区交流讨论💌

目录

[功能介绍 :](#功能介绍 :)

dict.txt

[1. nocopy.hpp](#1. nocopy.hpp)

[2. InetAddr.hpp](#2. InetAddr.hpp)

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

[4. LockGuard.hpp](#4. LockGuard.hpp)

[5. Dict.hpp](#5. Dict.hpp)

[6. UdpServer.hpp](#6. UdpServer.hpp)

[7. UdpServerMain.cc](#7. UdpServerMain.cc)

[8. UdpClientMain.cc](#8. UdpClientMain.cc)

[9. 效果展示](#9. 效果展示)


功能介绍 :

实现一个简单的英译汉的网络字典

dict.txt

这里就放一些简单的单词, 方便测试~

1. nocopy.hpp

定义一个 nocopy的类, 通过 C++11 delete 关键字的特性, 阻止该类的拷贝构造和拷贝赋值.

cpp 复制代码
#pragma once

class nocopy
{
public:
    nocopy(){}
    ~nocopy(){}
    nocopy(const nocopy&) = delete;
    const nocopy& operator=(const nocopy&) = delete;
};

2. InetAddr.hpp

InetAddr类它封装了网络地址 (IP 端口), 以及提供了访问这些信息的方法, 该类通过了 struct sockaddr_in 来存储 IP 地址和端口, 并提供了转换和获取信息的功能

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

class InetAddr                          
{
private:
    void ToHost(const struct sockaddr_in &addr)
    {
        _port = ntohs(addr.sin_port);
        _ip = inet_ntoa(addr.sin_addr);
    }

public:
    InetAddr(const struct sockaddr_in &addr):_addr(addr)
    {
        ToHost(addr);
    }
    std::string Ip()
    {
        return _ip;
    }
    uint16_t Port()
    {
        return _port;
    }
    ~InetAddr()
    {
    }

private:
    std::string _ip;
    uint16_t _port;
    struct sockaddr_in _addr;
};

成员变量 :

_ip : 存储 IP 地址, 使用 std::string 类型, 因为 IP 地址通常表示一个点分十进制的字符串

**_port :**存储端口号, 使用 uint16_t 类型, 端口号是一个16位的无符号整数

**_addr :**存储原始的 struct sockaddr_in 结构体, 它包含了 IP 地址和端口信息

ToHost():

**ToHost() :**将 struct sockaddr_in 中的网络字节数据转换为主机字节序, 并提取 IP 和端口.

**ntohs(addr.sin_port) :**将网络字节序的端口号转换为主机字节序

**inet_ntoa(addr.sin_addr) :**将网络字节序的 IP 地址转换为点分十进制的字符串的形式

构造函数 :

构造函数 : 接受一个 struct sockaddr_in 类型的参数, 表示网络地址

构造函数初始化 _addr 成员, 存储传入的地址

然后调用 ToHost() 方法来从该地址中提取 IP 和端口, 并进行转换

Ip() && Port()

**Ip() :**返回存储的 IP 地址, 类型为 std::string

Port() : 返回存储的端口号, 类型为 uint16_t

析构函数 :

由于没有动态分配资源, 所以没有必要进行额外的清理工作~

3. Log.hpp

这段代码实现了一个简单的日志系统, 可以将日志信息输出到控制台或文件中.

cpp 复制代码
#pragma once

#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <ctime>
#include <cstdarg>
#include <fstream>
#include <cstring>
#include <pthread.h>
#include "LockGuard.hpp"

namespace log_ns
{

    enum
    {
        DEBUG = 1,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };

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

    std::string GetCurrTime()
    {
        time_t now = time(nullptr);
        struct tm *curr_time = localtime(&now);
        char buffer[128];
        snprintf(buffer, sizeof(buffer), "%d-%02d-%02d %02d:%02d:%02d",
                 curr_time->tm_year + 1900,
                 curr_time->tm_mon + 1,
                 curr_time->tm_mday,
                 curr_time->tm_hour,
                 curr_time->tm_min,
                 curr_time->tm_sec);
        return buffer;
    }

    class logmessage
    {
    public:
        std::string _level;
        pid_t _id;
        std::string _filename;
        int _filenumber;
        std::string _curr_time;
        std::string _message_info;
    };

#define SCREEN_TYPE 1
#define FILE_TYPE 2

    const std::string glogfile = "./log.txt";
    pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER;

    // log.logMessage("", 12, INFO, "this is a %d message ,%f, %s hellwrodl", x, , , );
    class Log
    {
    public:
        Log(const std::string &logfile = glogfile) : _logfile(logfile), _type(SCREEN_TYPE)
        {
        }
        void Enable(int type)
        {
            _type = type;
        }
        void FlushLogToScreen(const logmessage &lg)
        {
            printf("[%s][%d][%s][%d][%s] %s",
                   lg._level.c_str(),
                   lg._id,
                   lg._filename.c_str(),
                   lg._filenumber,
                   lg._curr_time.c_str(),
                   lg._message_info.c_str());
        }
        void FlushLogToFile(const logmessage &lg)
        {
            std::ofstream out(_logfile, std::ios::app);
            if (!out.is_open())
                return;
            char logtxt[2048];
            snprintf(logtxt, sizeof(logtxt), "[%s][%d][%s][%d][%s] %s",
                     lg._level.c_str(),
                     lg._id,
                     lg._filename.c_str(),
                     lg._filenumber,
                     lg._curr_time.c_str(),
                     lg._message_info.c_str());
            out.write(logtxt, strlen(logtxt));
            out.close();
        }
        void FlushLog(const logmessage &lg)
        {
            // 加过滤逻辑 --- TODO

            LockGuard lockguard(&glock);
            switch (_type)
            {
            case SCREEN_TYPE:
                FlushLogToScreen(lg);
                break;
            case FILE_TYPE:
                FlushLogToFile(lg);
                break;
            }
        }
        void logMessage(std::string filename, int filenumber, int level, const char *format, ...)
        {
            logmessage lg;

            lg._level = LevelToString(level);
            lg._id = getpid();
            lg._filename = filename;
            lg._filenumber = filenumber;
            lg._curr_time = GetCurrTime();

            va_list ap;
            va_start(ap, format);
            char log_info[1024];
            vsnprintf(log_info, sizeof(log_info), format, ap);
            va_end(ap);
            lg._message_info = log_info;

            // 打印出来日志
            FlushLog(lg);
        }
        ~Log()
        {
        }

    private:
        int _type;
        std::string _logfile;
    };

    Log lg;

#define LOG(Level, Format, ...)                                        \
    do                                                                 \
    {                                                                  \
        lg.logMessage(__FILE__, __LINE__, Level, Format, ##__VA_ARGS__); \
    } while (0)
#define EnableScreen()          \
    do                          \
    {                           \
        lg.Enable(SCREEN_TYPE); \
    } while (0)
#define EnableFILE()          \
    do                        \
    {                         \
        lg.Enable(FILE_TYPE); \
    } while (0)
};

日志级别定义 :

定义了五个日志级别:DEBUG、INFO、WARNING、ERRORFATAL,数字1到5代表不同的优先级。

DEBUG :

含义: DEBUG级别的日志用于调试目的,主要记录程序运行中的详细信息,帮助开发人员理解程序的内部状态。这些信息通常对开发人员在开发、调试、分析问题时非常有用。

应用场景:

记录函数调用的输入输出参数。

输出变量的值、内存地址或其他详细的系统状态信息。

打印细节,像是某个功能执行的具体步骤、算法的中间结果等。

优先级: 最低的日志优先级,通常只在开发环境中启用。

INFO :

含义: INFO级别的日志用于记录常规信息,表示系统按预期运行的正常操作。这些日志通常用于展示系统的状态或者某些操作的成功执行,提供程序执行过程中有用的背景信息。

应用场景:

系统启动或停止。

配置的加载。

用户执行操作的成功消息。

**优先级:**相对较低,但仍然是一个有用的日志级别。适用于生产环境中记录系统的常规行为和关键事件。

WARNING :

含义: WARNING级别的日志用于记录潜在的问题或不正常的情况,这些情况可能不会导致程序崩溃或立即失败,但可能会影响程序的执行或系统的稳定性。警告通常表明某些操作可能需要关注或修改,但并不一定立即需要处理。

应用场景:

配置文件中的可疑设置或过时的配置项。

资源的使用接近极限,比如内存占用接近最大值。

网络延迟、连接问题等。

**优先级:**高于 INFO 级别,意味着它是需要关注的,但不至于影响程序的正常运行。

ERROR :

含义: ERROR级别的日志用于记录系统中发生的错误,这些错误通常会导致某些功能或操作失败,但不会完全中断系统的运行。错误通常需要被开发人员注意和修复,或者需要采取措施来避免进一步的问题。

应用场景:

数据库连接失败。

文件读写错误。

网络请求失败。

输入参数错误,导致某个功能不能正常执行。

**优先级:**高于 WARNING 级别,意味着这类问题更严重,但仍然不是致命的。通常会影响用户体验,需要开发人员快速处理。

FATAL :

含义: FATAL级别的日志用于记录严重错误,这些错误通常会导致程序崩溃或系统完全无法继续运行。FATAL 级别的日志代表最严重的错误,需要立即处理。这些错误通常是程序中的致命缺陷,可能需要紧急修复或采取特殊措施来恢复系统的正常运行。

应用场景:

程序崩溃或内存泄漏导致系统无法继续运行。

系统无法启动或重要组件丢失。

某些关键操作失败,无法继续执行后续步骤。

**优先级:**最高的日志优先级。需要立即采取措施,通常需要系统管理员或开发人员介入。

LevelToString函数, 该函数用于将日志级别数字转换为对应的字符串:

GetCurrTime函数, 获取当前系统时间并格式化为字符串。使用 strftime 格式化时间,返回一个格式化后的时间字符串:

logmessage类用于封装日志消息的结构体,包含:

**_level:**日志级别

**_id:**进程ID

**_filename:**源代码文件名

**_filenumber:**行号

**_curr_time:**当前时间

**_message_info:**实际的日志消息内容

Log类是日志管理的核心类,包含了日志的输出控制和处理方法。它的主要功能是:

构造函数:接受一个日志文件路径并初始化日志类型为屏幕输出(SCREEN_TYPE)。

Enable方法:设置日志输出类型,支持屏幕输出或文件输出。

FlushLogToScreen:将日志信息输出到屏幕。

FlushLogToFile:将日志信息写入文件。默认文件路径为 ./log.txt。

FlushLog :根据日志类型决定将日志输出到屏幕还是文件。使用 LockGuard实现线程安全。

logMessage方法:日志记录的核心方法,接受文件名、行号、日志级别和日志内容格式化字符串。通过 va_list 支持可变参数。

析构函数:析构时没有特别的清理操作。

宏函数定义 :

#define LOG(Level, Format, ...)

LOG 是一个宏,用于简化日志记录的调用。这个宏接受以下参数:

**Level:**日志的级别(如 INFO, DEBUG, ERROR 等)。

Format: 格式化字符串,类似于 printf 中使用的格式说明符(例如 %s, %d)。

...: 可变参数,可以传递给 logMessage方法,具体取决于格式字符串和实际参数。

**do { ... } while (0):**这种结构常用于宏定义中,目的是将宏的多条语句包裹在一个代码块中,并确保宏使用时不受周围代码的影响。它保证了宏的调用语句总是作为一个完整的语句来执行。

FILELINE:这两个预定义宏分别提供当前源文件的文件名和当前行号,用来记录日志发生的位置。

**lg.logMessage(FILE, LINE, Level, Format, ##VA_ARGS):**这是宏的核心部分,调用 Log 类的 logMessage 方法。它将当前文件名、行号、日志级别、格式字符串和变长参数传递给 logMessage。

FILELINE 提供日志的上下文信息。

Level 指定日志的级别(如 INFO、ERROR 等)。

Format是日志的格式字符串。

**##VA_ARGS**用来传递实际的参数到 logMessage 中,## 语法在某些编译器中可以去掉多余的逗号(比如如果没有传递变长参数时)。

#define EnableScreen()

该宏用于启用屏幕日志输出。宏的定义如下:

**lg.Enable(SCREEN_TYPE):**调用 Log 类的 Enable 方法,传递一个常量 SCREEN_TYPE,该常量可能表示日志的输出目标是屏幕。具体来说,Enable 方法是配置日志输出的目标或启用某种日志记录模式。

**do { ... } while (0):**同样,使用这种方式包裹宏,确保宏调用的语法不会受到其他代码块的影响

#define EnableFILE()

**lg.Enable(FILE_TYPE):**调用 Log 类的 Enable 方法,传递一个常量 FILE_TYPE,表示日志的输出目标是文件。FILE_TYPE 是一个标志常量,表示日志应该写入到文件而不是其他地方(例如屏幕)。

**do { ... } while (0):**同样使用这个结构,以确保宏调用时不受外部代码的影响。

4. LockGuard.hpp

cpp 复制代码
#pragma once

#include <pthread.h>

class LockGuard
{
public:
    LockGuard(pthread_mutex_t *mutex):_mutex(mutex)
    {
        pthread_mutex_lock(_mutex);
    }
    ~LockGuard()
    {
        pthread_mutex_unlock(_mutex);
    }
private:
    pthread_mutex_t *_mutex;
};

这段代码定义了一个名为 LockGuard 的 C++ 类,其目的是为多线程环境中的互斥锁(pthread_mutex_t )提供一种自动管理锁的机制。LockGuard 是一种 RAII(Resource Acquisition Is Initialization) 风格的锁管理类,用于简化锁的获取和释放。

构造函数:LockGuard 对象被创建时,它会接受一个指向互斥锁的指针 mutex ,并将其存储在 _mutex 成员变量中。pthread_mutex_lock(_mutex) :此语句通过 pthread_mutex_lock 函数请求对传入的互斥锁的独占访问权限。若锁已经被其他线程持有,当前线程将会被阻塞,直到锁变为可用。这样,在 LockGuard 对象被创建时,它会立即获取互斥锁,确保对共享资源的访问在 LockGuard 对象的生命周期内是安全的。

析构函数:LockGuard 对象超出作用域(即生命周期结束时),析构函数会被自动调用。pthread_mutex_unlock(_mutex): 在析构函数中,调用 pthread_mutex_unlock 来释放锁。这样,互斥锁在 LockGuard 对象销毁时被自动解锁,避免了手动解锁的遗漏,保证了线程安全。

RAII 特性

LockGuard 类的核心是 RAII(资源获取即初始化)模式。通过这种模式,在对象创建时自动获取锁,在对象销毁时自动释放锁,避免了程序员忘记解锁的情况。RAII 确保了即使发生异常,锁也能被正确释放,因为栈上的局部对象(如 LockGuard)会在离开作用域时自动销毁,从而触发析构函数并释放资源。

5. Dict.hpp

cpp 复制代码
// 实现一个简单的英译汉的网络字典
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include <unistd.h>
#include "Log.hpp"

using namespace log_ns;

const static std::string sep = ": ";

// sad: 悲伤的

class Dict
{
private:
    void LoadDict(const std::string &path)
    {
        std::ifstream in(path);
        if (!in.is_open())
        {
            LOG(FATAL, "open %s failed!\n", path.c_str());
            exit(1);
        }

        std::string line;
        while (std::getline(in, line))
        {
            LOG(DEBUG, "load info: %s , success\n", line.c_str());
            if (line.empty())
                continue;
            auto pos = line.find(sep);
            if (pos == std::string::npos)
                continue;

            std::string key = line.substr(0, pos);
            if (key.empty())
                continue;
            std::string value = line.substr(pos + sep.size());
            if (value.empty())
                continue;

            _dict.insert(std::make_pair(key, value));
        }
        LOG(INFO, "load %s done\n", path.c_str());
        in.close();
    }

public:
    Dict(const std::string &dict_path) : _dict_path(dict_path)
    {
        LoadDict(_dict_path);
    }
    std::string Translate(std::string word)
    {
        if(word.empty()) return "None";
        auto iter = _dict.find(word);
        if(iter == _dict.end()) return "None";
        else return iter->second;
    }
    ~Dict()
    {
    }

private:
    std::unordered_map<std::string, std::string> _dict;
    std::string _dict_path;
};

const static std::string sep = ": ";

这里定义了一个常量 sep,它是字典文件中键值对之间的分隔符。在字典文件中,格式是 "word: translation",即单词和翻译之间由 ": " 分

Dict 类定义

私有成员 :

**_dict:**一个 unordered_map 类型的成员变量,用来存储词典数据。键是单词(std::string),值是对应的翻译(std::string)。

**_dict_path:**存储字典文件路径的成员变量。

LoadDict 方法 :

LoadDict 方法负责从指定路径加载字典文件,并将内容解析到 _dict 成员变量中

**打开文件:**使用 std::ifstream 打开字典文件。若打开失败,记录日志并退出程序。

**逐行读取文件:**使用 std::getline 逐行读取文件内容

解析字典内容 :

跳过空行。

查找 sep(即 ": ")在行中的位置,分割单词和翻译。

使用 substr 提取单词(key)和翻译(value)。

将有效的单词-翻译对插入到 _dict 中。

日志记录:

在加载每一行时记录调试级别日志。

在整个字典加载完成后记录信息级别日志。

最后关闭文件

公共方法

**构造函数:**在构造函数中调用 LoadDict 方法加载字典文件。

Translate 方法:

该方法用于翻译给定的单词(word)。

如果单词为空,返回 "None"。

使用 unordered_map 的 find 方法查找单词。如果找不到,返回 "None",否则返回对应的翻译

析构函数 : 析构函数目前没有特别的操作,因为 std::unordered_map 会自动管理内存。

6. UdpServer.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <unistd.h>
#include <string>
#include <cstring>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "nocopy.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"

using namespace log_ns;

static const int gsockfd = -1;
static const uint16_t glocalport = 8888;

enum
{
    SOCKET_ERROR = 1,
    BIND_ERROR
};

using func_t = std::function<std::string(std::string)>;
// UdpServer user("192.1.1.1", 8899)
// 一般服务器主要是用来进行网络数据读取和写入的, IO的
// 服务器IO逻辑 和 业务逻辑 解耦

class UdpServer : public nocopy
{
public:
    UdpServer(func_t func, uint16_t localport = glocalport)
        :_func(func),
        _sockfd(gsockfd),
        _localport(localport),
        _isrunning(false)
    {
    }
    void InitServer()
    {
        // 1. 创建socket文件
        _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
        if(_sockfd < 0)
        {
            LOG(FATAL, "socket error\n");
            exit(SOCKET_ERROR);
        }
        LOG(DEBUG, "socket create success, _sockfd : %d", _sockfd);// 3

        // 2. bind
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_localport);
        local.sin_addr.s_addr = INADDR_ANY; // 服务器端, 进行任意 IP 地址绑定
        
        int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if(n < 0) 
        {
            LOG(FATAL, "bind error");
            exit(BIND_ERROR);
        }
        LOG(DEBUG, "socket bind success\n");
    }
    void Start()
    {
        _isrunning = true;
        char inbuffer[1024];
        while(_isrunning)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if(n > 0)
            {
                InetAddr addr(peer);
                inbuffer[n] = 0;
                // 一个一个的单词
                std::cout << "[" << addr.Ip() << ":" << addr.Port() << "]#" << inbuffer << std::endl;
                std::string result = _func(inbuffer);
                sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr *)&peer, len);
            }
            else
            {
                std::cout << "recvfrom, error" << std::endl;
            }
        }
    }
    ~UdpServer()
    {
        if(_sockfd > gsockfd)
            ::close(_sockfd);
    }
private:
    int _sockfd;
    uint16_t _localport;
    bool _isrunning;
    func_t _func;
};

这段代码实现了一个简单的 UDP 服务器类 UdpServer,能够接收客户端发送的消息,并通过用户提供的回调函数处理消息后返回响应。该类还实现了日志记录、套接字管理、地址绑定等基础的网络通信功能。

全局常量与类型定义

**gsockfd:**默认的套接字文件描述符,设置为 -1 作为无效值。

**glocalport:**默认的本地端口号,设置为 8888,如果未传递特定端口号,默认使用此端口。

错误码枚举: 定义了SOCKET_ERRORBIND_ERROR,用于表示套接字创建和绑定失败的错误。

func_t 类型别名

**func_t:**定义了一个类型别名 func_t,表示一个回调函数类型。该函数接收一个 std::string 类型的参数,并返回一个 std::string 类型的结果。通过这个回调函数,服务器可以自定义处理接收到的数据。

1. using 关键字

using 是 C++11 引入的关键字,用于定义类型别名。它与传统的 typedef 类似,但更现代、更简洁。using 可以为复杂的类型提供更直观、更易读的别名。

例如,using 可以让我们更方便地为模板类型定义别名,这在某些情况下比 typedef 更易于理解。

这种写法在 C++11 和之后的标准中更常见,特别是在模板类型、函数类型等复杂类型定义时。

2. std::function<std::string(std::string)>

std::function 是 C++11 标准库提供的一个模板类,它用于封装任何可调用对象(例如函数指针、函数对象、Lambda 表达式、成员函数指针等),使它们能够被统一处理。

std::function 是一个通用的函数包装器,它提供了统一的接口来调用不同类型的可调用对象。具体来说,std::function 可以封装函数、函数指针、Lambda 表达式、绑定函数、成员函数等。

std::function<std::string(std::string)> 表示一个接受 std::string 类型参数并返回 std::string 类型结果的可调用对象。

解释一下 std::function<std::string(std::string)>:

std::string:表示返回类型,即被封装的函数调用时会返回一个 std::string 类型的结果。

std::string(std::string):表示封装的函数类型,即该函数接受一个 std::string 类型的参数,返回一个 std::string 类型的值。

简单来说,std::function<std::string(std::string)> 是一个可以封装任何接受 std::string 参数并返回 std::string 的可调用对象。

3. func_t 类型别名

func_t 是通过 using 定义的类型别名,它代表了一个类型为 std::function<std::string(std::string)> 的函数对象。

也就是说,func_t 类型可以表示:

普通函数

Lambda 表达式

函数对象(即重载了 operator() 的对象)

函数指针等

这些可调用对象的共同特点是,它们都能够接收一个 std::string 类型的参数,并返回一个 std::string 类型的值。

UdpServer 类

UdpServer 类是实现 UDP 服务器功能的核心部分。它继承自 nocopy,意味着不能拷贝该类的实例。

成员变量 :

**_sockfd:**存储套接字文件描述符,用于进行网络通信。

**_localport:**服务器监听的本地端口号。

**_isrunning:**标志服务器是否正在运行。

**_func:**存储一个回调函数,用于处理接收到的数据。

构造函数

**func:**构造函数接受一个回调函数 func,用于处理接收到的消息。

**localport:**设置监听端口,如果没有提供,默认使用 glocalport(8888)。

InitServer 方法

创建套接字:使用**::socket()** 创建一个 UDP 套接字。AF_INET 表示 IPv4 地址族,SOCK_DGRAM表示 UDP 类型。

如果创建失败,记录错误日志并退出。

绑定套接字:::bind() 将套接字与指定的本地端口绑定。

**local.sin_family = AF_INET:**表示使用 IPv4 地址。

**local.sin_port = htons(_localport):**设置本地端口号(使用 htons() 转换为网络字节序)。

**local.sin_addr.s_addr = INADDR_ANY:**绑定到所有本地网络接口的 IP 地址。

Start 方法

循环接收消息:recvfrom() 从套接字接收数据,并将数据存储在 inbuffer中。该方法会阻塞,直到接收到数据。

**peer:**存储客户端的地址信息(IP 和端口)。

**len:**保存 peer 地址的大小。

**n:**接收到的字节数。

如果接收到数据,则调用传入的回调函数 _func 对数据进行处理,并通过 sendto() 发送回响应数据。

处理异常:如果 **recvfrom()**返回错误(n <= 0),则输出错误信息。

析构函数

关闭套接字:当服务器停止运行时,关闭套接字,释放资源。

7. UdpServerMain.cc

cpp 复制代码
#include "UdpServer.hpp"
#include "Dict.hpp"

#include <memory>

// ./udp_server local-port
// ./udp_server 8888
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " local-port" << std::endl;
        exit(0);
    }
    uint16_t port = std::stoi(argv[1]);
    EnableScreen();

    Dict dict("./dict.txt");
    func_t translate = std::bind(&Dict::Translate, &dict, std::placeholders::_1);
    
    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(translate, port); //C++14的标准
    usvr->InitServer();
    usvr->Start();
    return 0;
}

这段代码实现了一个基于 UdpServer 的 UDP 服务器,目的是通过翻译字典(由 Dict 类提供的功能)处理客户端发送的请求。通过网络通信,服务器接收客户端发来的数据,根据字典内容进行翻译并返回结果。

int main(int argc, char *argv[])

**命令行参数检查:**main 函数首先检查传入的参数数量。如果参数不等于 2(即程序名和端口号),则打印使用说明并退出。参数的第一个是程序名,第二个是本地监听端口。

**argv[0]:**程序本身的路径或名称。

**argv[1]:**传入的本地端口号,用于 UDP 服务器的绑定。

端口号解析

端口号转换:通过 std::stoi() 函数将命令行输入的字符串 argv[1] 转换为 uint16_t 类型的端口号。这个端口号将用来绑定 UDP 套接字。

启用屏幕输出

这里调用了**EnableScreen()**函数,它用于初始化或启用屏幕输出/日志记录功能。

创建 Dict 对象

字典文件加载:创建一个 Dict 类的实例 dict,并从文件 dict.txt 加载字典内容。该文件路径传递给 Dict 的构造函数

创建翻译回调函数

创建回调函数:这里使用了std::bind来绑定 Dict 类的成员函数 Translate,将 dict 对象和 std::placeholders::_1 作为占位符传递给回调函数。std::placeholders::_1 代表传递给回调函数的第一个参数。

**std::bind(&Dict::Translate, &dict, std::placeholders::_1)**会返回一个新的可调用对象 translate,它能够接收一个 std::string 类型的参数并调用 dict.Translate() 方法进行翻译。

创建 UdpServer 对象

创建 UDP 服务器:使用 std::make_unique<UdpServer> 创建一个 UdpServer 对象,并传递 translate 回调函数和端口号 port。

这意味着 UDP 服务器将在启动时使用传入的端口号进行绑定,并将 translate 作为数据处理的回调函数。当服务器接收到数据时,它将调用 translate 函数来处理收到的数据并返回结果。

初始化和启动服务器

初始化服务器:usvr->InitServer() 初始化服务器,创建套接字并绑定端口。这一步会创建 UDP 套接字并将其绑定到本地端口 port。

启动服务器:**usvr->Start()**启动服务器,开始接收客户端的请求并处理数据。这个方法会进入一个循环,不断接收客户端数据,并调用 translate 函数进行处理后发送回响应。

8. UdpClientMain.cc

cpp 复制代码
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

// 客户端在未来一定要知道服务器的IP地址和端口号
// ./udp_client server-ip server-port
// ./udp_client 127.0.0.1 8888
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;
        exit(0);
    }

    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0)
    {
        std::cerr << "create socket error" << std::endl;
        exit(1);
    }

    // client的端口号,一般不让用户自己设定,而是让client OS随机选择?怎么选择,什么时候选择呢?
    // client 需要 bind它自己的IP和端口, 但是client 不需要 "显示" bind它自己的IP和端口, 
    // client 在首次向服务器发送数据的时候,OS会自动给client bind它自己的IP和端口

    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(1)
    {
        std::string line;
        std::cout << "Please Enter# ";
        std::getline(std::cin, line);

        // std::cout << "line message is@ " << line << std::endl;

        int n = sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr*)&server, sizeof(server)); // 你要发送消息,你得知道你要发给谁啊!
        if(n > 0)
        {
            struct sockaddr_in temp;
            socklen_t len = sizeof(temp);
            char buffer[1024];
            int m = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&temp, &len);
            if(m > 0)
            {
                buffer[m] = 0;
                std::cout << buffer << std::endl;
            }
            else
            {
                std::cout << "recvfrom error" << std::endl;
                break;
            }
        }
        else
        {
            std::cout << "sendto error" << std::endl;
            break;
        }
    }

    ::close(sockfd);
    return 0;
}

1. 程序输入检查

argc 是命令行参数的数量,argv是一个字符串数组,其中存储了命令行输入的参数。

代码检查命令行参数数量是否正确。如果用户没有提供服务器的 IP 地址和端口号(即参数数量不等于 3),则输出使用帮助信息并退出程序。

2. 服务器地址和端口号的解析

serverip:获取用户输入的服务器 IP 地址(例如 "127.0.0.1")。

serverport:获取用户输入的服务器端口号,并将其转换为 uint16_t 类型。

3. 创建 UDP 套接字

使用 ::socket函数创建一个套接字。

AF_INET:指定使用 IPv4 协议。

SOCK_DGRAM:指定使用 数据报 套接字,适用于 UDP。

0:通常表示选择默认的协议(对于 UDP,通常是 IPPROTO_UDP)。

如果创建套接字失败,socket 返回值小于零,程序会输出错误信息并退出。

4. 构造服务器地址结构

sockaddr_in结构用于存储 IPv4 地址和端口信息。

**memset(&server, 0, sizeof(server)):**将 server 结构体初始化为 0,避免不必要的垃圾数据。

**server.sin_family = AF_INET:**指定协议族为 IPv4。

**server.sin_port = htons(serverport):**将端口号转换为网络字节顺序(htons 是 host to network short,将主机字节顺序转换为网络字节顺序)。

**server.sin_addr.s_addr = inet_addr(serverip.c_str()):**将字符串格式的 IP 地址转换为网络字节顺序的地址。

5. 发送和接收数据

发送数据

**std::getline(std::cin, line):**从标准输入读取用户输入的字符串。

**sendto:**通过 UDP 套接字向服务器发送数据。

**sockfd:**套接字描述符。

**line.c_str():**要发送的消息,c_str() 返回一个指向 std::string 数据的指针,sendto 需要 C 风格的字符串。

**line.size():**消息的长度。

0:标志位(通常为 0,没有特定的设置)。

**(struct sockaddr*)&server:**服务器的地址信息。

**sizeof(server):**地址结构的大小。

sendto返回发送的字节数,若发送成功,返回大于零的值。

接收数据

**recvfrom:**接收来自服务器的响应。

**buffer:**接收数据的缓冲区。

sizeof(buffer)-1:指定缓冲区大小,recvfrom 会在收到数据后填充该缓冲区。

**(struct sockaddr*)&temp:**接收者的地址信息,虽然客户端在此不需要使用接收者的地址,但仍需传入一个 sockaddr_in 类型的变量来接收。

**&len:**地址结构的大小,用于接收 recvfrom 填充的信息。

如果 recvfrom 成功,返回实际接收的字节数;如果失败,返回负值。

数据处理

如果成功接收到数据,则将数据写入输出流(std::cout)并打印接收到的消息。

如果接收失败,输出错误信息并退出循环。

6. 关闭套接字

9. 效果展示

这里我们做的比较简陋, 只能英翻中~~ 不过我们实现还是没有问题的, 好了, 篇幅已经很长了, 网络编程是这样的, 我们要考虑的东西很多, 我们下期再见~~

相关推荐
我的K840912 分钟前
Flink整合Hudi及使用
linux·服务器·flink
19004322 分钟前
linux6:常见命令介绍
linux·运维·服务器
Camellia-Echo28 分钟前
【Linux从青铜到王者】Linux进程间通信(一)——待完善
linux·运维·服务器
Linux运维日记39 分钟前
k8s1.31版本最新版本集群使用容器镜像仓库Harbor
linux·docker·云原生·容器·kubernetes
我是唐青枫1 小时前
Linux dnf 包管理工具使用教程
linux·运维·服务器
黑客Ash1 小时前
计算机中的网络安全
网络·安全·web安全
PersistJiao1 小时前
Spark 分布式计算中网络传输和序列化的关系(二)
大数据·网络·spark·序列化·分布式计算
寒笙LED1 小时前
C++详细笔记(六)string库
开发语言·c++·笔记
岳不谢2 小时前
VPN技术-VPN简介学习笔记
网络·笔记·学习·华为
Lws2 小时前
CS144 lab0(个人理解)
网络协议