Linux 的 UDP 网络编程 -- 回显服务器,翻译服务器

目录

[1. 回显服务器 -- echo server](#1. 回显服务器 -- echo server)

[1.1 相关函数介绍](#1.1 相关函数介绍)

[1.1.1 socket()](#1.1.1 socket())

[1.1.2 bind()](#1.1.2 bind())

[1.1.3 recvfrom()](#1.1.3 recvfrom())

[1.1.4 sendto()](#1.1.4 sendto())

[1.1.5 inet_ntoa()](#1.1.5 inet_ntoa())

[1.1.6 inet_addr()](#1.1.6 inet_addr())

[1.2 Udp 服务端的封装 -- UdpServer.hpp](#1.2 Udp 服务端的封装 -- UdpServer.hpp)

[1.3 服务端代码 -- UdpServer.cc](#1.3 服务端代码 -- UdpServer.cc)

[1.4 客户端代码 -- UdpClient.cc](#1.4 客户端代码 -- UdpClient.cc)

[1.4.1 Linux版本的客户端](#1.4.1 Linux版本的客户端)

[1.4.2 Windows 版本的客户端](#1.4.2 Windows 版本的客户端)

[1.5 demo 演示](#1.5 demo 演示)

[1.6 网络相关命令](#1.6 网络相关命令)

[2. 翻译服务器 -- Translation server](#2. 翻译服务器 -- Translation server)

[2.1 Udp 服务端封装 -- UdpServer.hpp](#2.1 Udp 服务端封装 -- UdpServer.hpp)

[2.2 字典结构体的封装 -- Dict.hpp](#2.2 字典结构体的封装 -- Dict.hpp)

[2.3 网络地址转主机地址的封装 -- InetAddr.hpp](#2.3 网络地址转主机地址的封装 -- InetAddr.hpp)

[2.4 Udp 服务端 -- UdpServer.cc](#2.4 Udp 服务端 -- UdpServer.cc)

[2.5 Udp 客户端 -- UdpClient.cc](#2.5 Udp 客户端 -- UdpClient.cc)


1. 回显服务器 -- echo server

使用C++实现一个回显服务器,该代码的作用是客户端向服务端发送消息,然后回显到客户端的显示器上。

先给出需要使用的互斥锁的封装模块线程安全的日志模块

cpp 复制代码
// Mutex.hpp

#pragma once 
#include <pthread.h>

// 将互斥量接口封装成面向对象的形式
namespace MutexModule
{
    class Mutex
    {
    public:
        Mutex()
        {
            int n = pthread_mutex_init(&_mutex, nullptr);
            (void)n;
        }
        ~Mutex()
        {
            int n = pthread_mutex_destroy(&_mutex);
            (void)n;
        }

        void Lock()
        {
            int n = pthread_mutex_lock(&_mutex);
            (void)n;
        }

        void Unlock()
        {
            int n = pthread_mutex_unlock(&_mutex);
            (void)n;
        }

        pthread_mutex_t* Get()  //  获取原生互斥量的指针
        {
            return &_mutex;
        }
    private:
        pthread_mutex_t _mutex;
    };
    
    // 采用RAII风格进行锁管理,当局部临界区代码运行完的时候,局部LockGuard类型的对象自动进行释放,调用析构函数释放锁
    class LockGuard
    {
    public:
        LockGuard(Mutex &mutex)
        : _mutex(mutex)
        {
            _mutex.Lock();
        }

        ~LockGuard()
        {
            _mutex.Unlock();
        }
    private:
        Mutex& _mutex;
    };
}
cpp 复制代码
// Log.hpp

#ifndef __LOG_HPP__
#define __LOG_HPP__

#include <iostream>
#include <cstdio>
#include <string>
#include <filesystem> //C++17
#include <sstream>
#include <fstream>
#include <ctime>
#include <memory>
#include <unistd.h>
#include "Mutex.hpp"

namespace LogModule
{
    using namespace MutexModule;

    const std::string gsep = "\r\n";
    // 策略模式 -- 利用C++的多态特性
    // 1. 刷新策略 a: 向显示器打印 b: 向文件中写入
    // 刷新策略基类
    class LogStrategy
    {
    public:
        virtual ~LogStrategy() = default;
        virtual void SyncLog(const std::string &message) = 0;
    };

    // 显示器打印日志的策略
    class ConsoleLogStrategy : public LogStrategy
    {
    public:
        ConsoleLogStrategy()
        {
        }

        void SyncLog(const std::string &message) override
        {
            // 加锁使多线程原子性的访问显示器
            LockGuard lockGuard(_mutex);
            std::cout << message << gsep;
        }

        ~ConsoleLogStrategy()
        {
        }

    private:
        Mutex _mutex;
    };

    // 文件打印日志策略
    // 默认的日志文件路径和日志文件名
    const std::string defaultPath = "./log";
    const std::string defaultFile = "my.log";
    class FileLogStrategy : public LogStrategy
    {
    public:
        FileLogStrategy(const std::string &path = defaultPath, const std::string &file = defaultFile)
            : _path(path),
              _file(file)
        {
            // 加锁使多线程原子性的访问文件
            LockGuard lockGuard(_mutex);
            // 判断目录是否存在
            if (std::filesystem::exists(_path)) // 检测文件系统对象(文件,目录,符号链接等)是否存在
            {
                return;
            }
            try
            {
                // 如果目录不存在,递归创建目录
                std::filesystem::create_directories(_path);
            }
            catch (const std::filesystem::filesystem_error &e) // 如果创建失败则打印异常信息
            {
                std::cerr << e.what() << '\n';
            }
        }

        void SyncLog(const std::string &message) override
        {
            LockGuard lockGuard(_mutex);
            // 追加方式向文件中写入
            std::string fileName = _path + (_path.back() == '/' ? "" : "/") + _file;
            // std::ofstream是C++标准库中用于输出到文件的流类,主要用于将数据写入文件
            std::ofstream out(fileName, std::ios::app);
            if (!out.is_open())
            {
                return;
            }
            out << message << gsep;
            out.close();
        }

        ~FileLogStrategy()
        {
        }

    private:
        std::string _path; // 日志文件所在路径
        std::string _file; // 日志文件本身
        Mutex _mutex;
    };

    // 2. 形成完整日志并刷新到指定位置
    // 2.1 日志等级
    enum class LogLevel
    {
        DEBUG,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };

    // 2.2 枚举类型的日志等级转换为字符串类型
    std::string Level2Str(LogLevel level)
    {
        switch (level)
        {
        case LogLevel::DEBUG:
            return "DEBUG";
        case LogLevel::INFO:
            return "INFO";
        case LogLevel::WARNING:
            return "WARNING";
        case LogLevel::ERROR:
            return "ERROR";
        case LogLevel::FATAL:
            return "FATAL";
        default:
            return "UNKNOWN";
        }
    }

    // 2.3 获取当前时间的函数
    std::string GetCurTime()
    {
        // time 函数参数为一个time_t类型的指针,若该指针不为NULL,会把获取到的当前时间值存储在指针指向的对象中
        // 若传入为NULL,则仅返回当前时间,返回从1970年1月1日0点到目前的秒数
        time_t cur = time(nullptr);
        struct tm curTm;
        // localtime_r是localtime的可重入版本,主要用于将time_t类型表示的时间转换为本地时间,存储在struct tm 结构体中
        localtime_r(&cur, &curTm);
        char timeBuffer[128];
        snprintf(timeBuffer, sizeof(timeBuffer), "%4d-%02d-%02d %02d:%02d:%02d",
                 curTm.tm_year + 1900,
                 curTm.tm_mon + 1,
                 curTm.tm_mday,
                 curTm.tm_hour,
                 curTm.tm_min,
                 curTm.tm_sec);
        return timeBuffer;
    }

    // 2.4 日志形成并刷新
    class Logger
    {
    public:
        // 默认刷新到显示器上
        Logger()
        {
            EnableConsoleLogStrategy();
        }

        void EnableConsoleLogStrategy()
        {
            // std::make_unique用于创建并返回一个std::unique_ptr对象
            _fflushStrategy = std::make_unique<ConsoleLogStrategy>();
        }

        void EnableFileLogStrategy()
        {
            _fflushStrategy = std::make_unique<FileLogStrategy>();
        }
        //  内部类默认是外部类的友元类,可以访问外部类的私有成员变量
        //  内部类LogMessage,表示一条日志信息的类
        class LogMessage
        {
        public:
            LogMessage(LogLevel &level, std::string &srcName, int lineNum, Logger &logger)
                : _curTime(GetCurTime()),
                  _level(level),
                  _pid(getpid()),
                  _srcName(srcName),
                  _lineNum(lineNum),
                  _logger(logger)
            {
                // 日志的基本信息合并起来
                // std::stringstream用于在内存中进行字符串的输入输出操作, 提供一种方便的方式处理字符串
                // 将不同类型的数据转换为字符串,也可以将字符串解析为不同类型的数据
                std::stringstream ss;
                ss << "[" << _curTime << "] "
                   << "[" << Level2Str(_level) << "] "
                   << "[" << _pid << "] "
                   << "[" << _srcName << "] "
                   << "[" << _lineNum << "] "
                   << "- ";

                _logInfo = ss.str();
            }

            //  使用模板重载运算符<< -- 支持不同数据类型的输出运算符重载
            template <typename T>
            LogMessage &operator<<(const T &info)
            {
                std::stringstream ss;
                ss << info;
                _logInfo += ss.str();
                return *this;
            }

            ~LogMessage()
            {
                if (_logger._fflushStrategy)
                {
                    _logger._fflushStrategy->SyncLog(_logInfo);
                }
            }

        private:
            std::string _curTime;   // 日志时间
            LogLevel _level;    // 日志等级
            pid_t _pid; // 进程pid
            std::string _srcName;   // 输出日志的文件名
            int _lineNum;   //输出日志的行号
            std::string _logInfo;   //完整日志内容
            Logger &_logger;    // 方便使用策略进行刷新
        };

        // 使用宏进行替换之后调用的形式如下
        // logger(level, __FILE__, __LINE__) << "hello world" << 3.14;
        // 这里使用仿函数的形式,调用LogMessage的构造函数,构造一个匿名的LogMessage对象
        // 返回的LogMessage对象是一个临时对象,它的生命周期从创建开始到包含它的完整表达式结束(可以简单理解为包含
        // 这个对象的该行代码)
        // 代码调用结束的时候,如果没有LogMessage对象进行临时对象的接收,则会调用析构函数,
        // 如果有LogMessage对象进行临时对象的接收,会调用拷贝构造或者移动构造构造一个对象,并析构临时对象
        // 所以通过临时变量调用析构函数进行日志的打印
        LogMessage operator()(LogLevel level, std::string name, int line)
        {
            return LogMessage(level, name, line, *this);
        }

        ~Logger()
        {
        }

    private:
        std::unique_ptr<LogStrategy> _fflushStrategy;
    };

    //  定义一个全局的Logger对象
    Logger logger;

    // 使用宏定义,简化用户操作并且获取文件名和行号
    #define LOG(level) logger(level, __FILE__, __LINE__) // 使用仿函数的方式进行调用
    #define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()
    #define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()
}

#endif

1.1 相关函数介绍

1.1.1 socket()

在网络编程领域,socket 是一个基础且关键的函数,主要用于创建网络通信的端点,也就是 "套接字"

cpp 复制代码
原型:
    int socket(int domain, int type, int protocol);

头文件:
    #include <sys/types.h>
    #include <sys/socket.h>

参数:
    domain(协议族):此参数用于确定网络通信所使用的协议栈,常见的取值有:AF_INET:代表 IPv4 协
议,AF_INET6:表示 IPv6 协议,AF_UNIX:用于本地通信的 Unix 域套接字。

    type(套接字类型):该参数决定了通信的特性,常用的类型有:SOCK_STREAM:提供面向连接的、可靠
的数据流服务,TCP 协议就属于这种类型。SOCK_DGRAM:实现无连接的、不可靠的数据报服务,UDP 协议是其
典型代表。SOCK_RAW:允许直接访问底层协议,可用于自定义协议的开发。
    
    protocol(协议):当套接字类型不能唯一确定使用的协议时,就需要通过这个参数来明确指定。一般情
况下,将其设置为 0 即可,系统会自动选择合适的协议。对于 SOCK_STREAM 类型,系统通常会选择 TCP 协
议。对于 SOCK_DGRAM 类型,系统一般会选择 UDP 协议。

返回值:
    成功,返回一个非负整数,即调节子描述符,类似文件描述符。
    失败,返回-1,并设置errno来指示具体的错误原因。

功能:
    创建网络通信的套接字

1.1.2 bind()

在网络编程中,bind() 函数是一个关键的系统调用,主要用于将一个套接字(通过 socket() 函数创建)与特定的网络地址和端口号进行绑定

cpp 复制代码
原型:
    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

头文件:
    #include <sys/types.h>
    #include <sys/socket.h>

参数:
    sockfd:这是通过 socket() 函数返回的套接字描述符,它标识了要进行绑定操作的套接字。

    addr:这是一个指向 struct sockaddr 类型的指针,其中包含了要绑定的地址和端口信息。不过,
在实际编程中,通常会使用特定协议的地址结构,比如 struct sockaddr_in(用于 IPv4)或 struct
sockaddr_in6(用于 IPv6),然后再将其强制转换为 struct sockaddr 类型。

    addrlen:该参数表示 addr 结构的长度,其类型为 socklen_t

返回值:
    成功,返回0.
    失败,返回-1,并设置 errno 来指示具体的错误原因。

功能:
    用于将一个套接字(通过 socket() 函数创建)与特定的网络地址和端口号进行绑定。

1.1.3 recvfrom()

在网络编程里,recvfrom 函数主要用于从 UDP 套接字接收数据并 获取发送方的套接字信息

cpp 复制代码
原型:
    ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

头文件:
    #include <sys/types.h>
    #include <sys/socket.h>

参数:
    sockfd:这是通过 socket() 函数返回的套接字描述符,它标识了要接收数据的套接字。

    buf:这是一个指向缓冲区的指针,用于存储接收到的数据。

    len:表示缓冲区 buf 的最大长度,即最多可以接收的字节数。

    flags:这是一个可选的标志参数,通常设置为 0。常见的标志选项有:MSG_DONTWAIT:将操作设置为非
阻塞模式。MSG_PEEK:查看数据但不将其从接收队列中移除。

    src_addr:这是一个指向 struct sockaddr 类型的指针,用于存储发送方的地址信息。

    addrlen:这是一个指向 socklen_t 类型的指针,用于指定 src_addr 结构的长度。函数返回时,该参
数会被更新为实际存储的地址结构长度。

返回值:
    成功,返回实际接收到的字节数。
    返回0,表示连接已关闭(对于TCP套接字而言)。
    返回-1,表示调用失败,此时会设置 errno 来指示具体的错误原因。

功能:
    用于从 UDP 套接字接收数据和获取发送方的套接字信息。
    

1.1.4 sendto()

sendto() 是 C 语言网络编程中的一个关键函数,主要用于在无连接的套接字(如 UDP)上发送数据sendto() 在发送数据时需要指定目标地址,这使得它非常适合 UDP 这种无连接的通信模式。

cpp 复制代码
原型:
    ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

头文件:
    #include <sys/types.h>
    #include <sys/socket.h>

参数:
    sockfd:这是通过 socket() 函数创建的套接字描述符,用于标识发送数据的套接字。

    buf:指向要发送数据的缓冲区的指针。

    len:要发送数据的长度(以字节为单位)。

    flags:可选的标志参数,通常设置为 0。常见的标志选项有:MSG_DONTWAIT:将操作设置为非阻塞模
式。MSG_NOSIGNAL:避免在连接断开时发送 SIGPIPE 信号。

    dest_addr:指向目标地址的指针,类型为 struct sockaddr。对于 IPv4,通常使用 struct 
sockaddr_in;对于 IPv6,则使用 struct sockaddr_in6。

    addrlen:目标地址结构的长度,类型为 socklen_t。

返回值:
    成功,返回实际发送的字节数(可能小于请求发送的字节数)。
    失败,返回-1,并设置 errno 来指示具体的错误原因。

功能:
    主要用于在无连接的套接字(如 UDP)上发送数据。

1.1.5 inet_ntoa()

inet_ntoa() 是 C 语言网络编程中的一个关键函数,其主要作用是将 32 位二进制 IPv4 地址 转换为 点分十进制字符串(如 192.168.1.1

cpp 复制代码
原型:
    char *inet_ntoa(struct in_addr in);

头文件:
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>

参数:
    in:struct in_addr 类型的结构体,该结构体内部有一个 s_addr 成员,用于存储 32 位的 IPv4 地址
(以网络字节序表示)。

返回值:
    返回一个指向点分十进制字符串风格的ip地址。

功能:
    将 32 位二进制 IPv4 网络字节序的 ip 地址转换为点分十进制字符串(如 192.168.1.1)

1.1.6 inet_addr()

inet_addr() 是 C 语言网络编程中的一个基础函数,其主要功能是将点分十进制格式(如 192.168.1.1 的 IPv4 地址转换为 32 位二进制网络字节序整数

cpp 复制代码
原型:
    in_addr_t inet_addr(const char *cp);

头文件:
    #include <sys/types.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>

参数:
    cp:指向点分十进制字符串的指针,例如 "127.0.0.1"。

返回值:
    成功,返回 in_addr_t 类型的 32 位整数(网络字节序)。
    失败,返回 INADDR_NONE(通常为 0xFFFFFFFF),这意味着无法解析输入的字符串。

功能:
    将点分十进制字符串风格的 ip 地址,转换为4字节的网络字节序整数。

1.2 Udp 服务端的封装 -- UdpServer.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "Log.hpp"

using namespace LogModule;
using func_t = std::function<std::string(const std::string&)>;  // 参数为string& 返回值为 string 的函数类型

const int defaultfd = -1;

class UdpServer
{
public:
    UdpServer(uint16_t port, func_t func)
        : _sockfd(defaultfd),
         _port(port),
         _isrunning(false),
         _func(func)
        {}

    void Init()
    {
        // 1. 创建套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            // 创建套接字失败
            LOG(LogLevel::FATAL) << "socket create error!";
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket create seccess, sockfd: " << _sockfd;    // 创建成功只是打开文件

        // 2. 绑定 socket 信息,ip 和 端口号
        // 2.1 填充 sockaddr_in 结构体
        struct sockaddr_in local;   // 用于网络通信的结构体
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);  // 主机字节序转成网络字节序
        // 服务端不建议手动bind特定ip
        // 当一个机器有多张网卡的时候,服务端 ip 绑定INADDR_ANY,就可以接收任意ip中端口号为port
        local.sin_addr.s_addr = INADDR_ANY;

        // 2.2 绑定服务器的套接字信息
        // 为什么服务器端要显式的bind?
        // 服务器的ip和端口号必须是众所周知且不能轻易改变的.
        int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind success, sockfd: " << _sockfd;
    }

    void Start()
    {
        _isrunning = true;
        while(_isrunning)   // 启动服务器之后是死循环
        {
            // 1. 创建用于接收消息的缓冲器变量 buffer 以及接收远端主机的套接字变量 peer
            char buffer[1024];
            struct sockaddr_in peer;    // 客户端套接字结构体
            socklen_t len = sizeof(peer);

            // 2. 收消息,服务端收取客户端的数据,对数据进行处理
            // 从 _sockfd 指向的网络文件中收取客户端 peer 发送的 sizeof(buffer) - 1 个字节以及客户端的套接字信息
            // 第四个参数为0,表示阻塞读
            ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
            if (s > 0)  // 收到消息,s表示收到数据的字节数
            {
                int peer_port = ntohs(peer.sin_port);   // 将客户端端口号转成主机字节序
                std::string peer_ip = inet_ntoa(peer.sin_addr); // 将客户端ip转为字符串风格的ip
                buffer[s] = 0;

                // 服务端显式发送消息的客户端信息
                LOG(LogLevel::DEBUG) << "[" << peer_ip << ":" << peer_port << "]# " << buffer;

                // 2. 发消息,将消息进行处理后回发给客户端
                std::string result = buffer;
                result = _func(buffer);
                sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr*)&peer, len);
            }
        }
    }

    ~UdpServer()
    {}

private:
    int _sockfd;    // 套接字描述符
    uint16_t _port; // 端口号
    bool _isrunning;// 运行标志位
    func_t _func;   // 服务端处理数据的回调函数
};

1.3 服务端代码 -- UdpServer.cc

cpp 复制代码
#include <memory>
#include "UdpServer.hpp"

std::string defaultHandler(const std::string &message)
{
    std::string s = "server say@ ";
    s += message;
    return s;
}

// 通过命令行 ./udpserver port 启动服务器
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " port" << std::endl;
        return 1;
    }

    uint16_t port = std::stoi(argv[1]);
    Enable_Console_Log_Strategy();

    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, defaultHandler);
    usvr->Init();
    usvr->Start();
    return 0;
}

1.4 客户端代码 -- UdpClient.cc

1.4.1 Linux版本的客户端

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

// 通过命令行 ./udpclient server_ip server_port 启动客户端
int main(int argc, char *argv[])
{
    // 客户端访问目标服务器需要知道什么
    // 需要服务器的ip和端口
    // 怎么知道服务器的ip和端口呢 -- 内置的ip

    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
        return 1;
    }

    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);

    // 1. 创建套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket create error" << std::endl;
        return 2;
    }

    // 2. 填充服务端的套接字信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;    // AF_INET 或者 PF_INET
    server.sin_port = htons(server_port);
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());

    // client不需要显式的bind,首次发送消息,操作系统自动给client进行bind,
    // 端口号采用随机端口号,一个端口号只能被一个进程bind,为了避免client端口冲突
    // client端口号是多少不重要,只要是唯一的就行

    while(true)
    {
        // 1. 给客户端发消息
        std::string input;
        std::cout << "Please Enter# ";
        if (input.empty()) continue;
        std::getline(std::cin, input);
        int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr*)&server, sizeof(server));
        (void)n;

        // 2. 回显消息
        char buffer[1024];
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
        if (m > 0)
        {
            buffer[m] = 0;
            std::cout << buffer << std::endl;
        }
    }
    return 0;
}

1.4.2 Windows 版本的客户端

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS

#include <iostream>
#include <cstdio>
#include <thread>
#include <string>
#include <cstdlib>
// Windows中需要包含的头文件
#include <WinSock2.h>
#include <Windows.h>

#pragma warning(disable : 4996)	// 屏蔽一些 warning 报错

#pragma comment(lib, "ws2_32.lib")	// 引入 ws2_32.lib 库

std::string server_ip = "服务器ip地址";	// 服务器ip
uint16_t server_port = 8888;	// 服务器端口号

int main()
{
	WSADATA wsd;
	WSAStartup(MAKEWORD(2, 2), &wsd);	// 构建 2.2 版本

	// 1. 创建 udp 套接字
	SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);	// SOCKET == int
	if (sockfd == SOCKET_ERROR)
	{
		std::cerr << "socket create error" << std::endl;
		return 1;
	}

	// 2. 填充 sockaddr_in 结构体
	struct sockaddr_in server;
	memset(&server, 0, sizeof(server));
	server.sin_family = AF_INET;
	server.sin_port = htons(server_port);
	server.sin_addr.s_addr = inet_addr(server_ip.c_str());

	std::string message;
	char buffer[1024];
	while (true)
	{
		// 3. 发信息给服务端
		std::cout << "Please Enter# ";
		std::getline(std::cin, message);
		if (message.empty()) continue;
		sendto(sockfd, message.c_str(), sizeof(buffer), 0, (struct sockaddr*)&server, sizeof(server));

		// 4. 收消息,并显示到显示器上
		struct sockaddr_in temp;
		int len = sizeof(temp);
		int s = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&temp, &len);
		if (s > 0)
		{
			buffer[s] = 0;
			std::cout << buffer << std::endl;
		}
	}

	closesocket(sockfd);
	WSACleanup();
	return 0;
}

WinSock2.h 是 Windows Sockets API(应用程序接口)的头文件,用于在Windows 平台上进行网络编程。它包含了 Windows Sockets 2(Winsock2)所需的数据类型、函数声明和结构定义,使得开发者能够创建和使用套接字(sockets)进行网络通信。

在编写使用 Winsock2 的程序时,需要在源文件中包含 WinSock2.h 头文件。这样,编译器就能够识别并理解 Winsock2 中定义的数据类型和函数,从而能够正确地编译和链接网络相关的代码。

此外,与 WinSock2.h 头文件相对应的是 ws2_32.lib 库文件。在链接阶段,需要将这个库文件链接到程序中,以确保运行时能够找到并调用 Winsock2 API 中实现的函数。

在 WinSock2.h 中定义了一些重要的数据类型和函数,如:

WSADATA :保存初始化 Winsock 库时返回的信息。

SOCKET :表示一个套接字描述符类型,用于在网络中唯一标识一个套接字。

sockaddr_in :IPv4 地址结构体,用于存储 IP 地址和端口号等信息。

socket() :创建一个新的套接字。

bind() :将套接字与本地地址绑定。

listen() :将套接字设置为监听模式,等待客户端的连接请求。

accept():接受客户端的连接请求,并返回一个新的套接字描述符,用于与客户端进行通信。

WSAStartup 函数是 Windows Sockets API 的初始化函数,它用于初始化Winsock 库。该函数在应用程序或 DLL 调用任何 Windows 套接字函数之前必须首先执行,它扮演着初始化的角色。

以下是 WSAStartup 函数的一些关键点:

它接受两个参数:wVersionRequested 和 lpWSAData。wVersionRequested 用于指定所请求的 Winsock 版本,通常使用 MAKEWORD(major, minor)宏,其中major 和 minor 分别表示请求的主版本号和次版本号。lpWSAData 是一个指向 WSADATA 结构的指针,用于接收初始化信息。函数调用成功,它会返回 0;否则,返回错误代码。

在调用 WSAStartup 函数后,如果应用程序完成了对请求的 Socket 库的使用,应调用 WSACleanup 函数来解除与 Socket 库的绑定并释放所占用的系统资源。

1.5 demo 演示

(1)本地使用客户端和服务端进行通信。

服务端因为服务端 ip 进行绑定的时候绑定的是 INADDR_ANY,所以服务端启动的时候仅需要传入端口号

客户端启动的时候,可以传入 内网 ip 或者 本地环回 ip:127.0.0.1 和端口号

客户端和服务端启动之后即可进行通信,服务端显式客户端的套接字信息以及客户端发送的信息,客户端回显发送的信息:

(2)跨网络使用客户端和服务端进行通信。

服务端启动的时候也仅传入端口号。

客户端启动的时候传入服务端进程的公网 ip 和端口号。Windows 系统下也一样,但是Windows下需要启动 Windows 版本的客户端。

1.6 网络相关命令

ping -选项 网址或ip

功能:用于检测主机是否与网络进行了连接。

常用选项:

c次数,默认情况下 ping 是会一直持续下去的,这个选项表示 ping 的次数。

上述表示对百度的网站 ping 3 次。

netstat -选项

功能:查看网络状态信息。

常用选项:

n:拒绝显示别名,能显示数字的全部转化成数字。

l:仅列出有在 Listen(监听)的服务状态。

p:显示建立相关链接的程序名和pid。

t:仅显示 tcp 相关服务。

u:仅显示 udp 相关服务。

a:显示所有选项,默认是不显示 LISTEN 相关。

上述命令显示所有与 udp 相关的网络服务。

增加 p 选项会显示进程名和进程 pid,这里没有显示是因为 netstat 命令是用普通用户启动的,而这几个服务都是使用超级用户启动的,有权限问题。

n 选项可以将能用数字显示的信息用数字显示出来。

watch 命令可以周期性的指向命令。

watch -n 1 netstat -nuap -- 每个 1 秒执行一次 netstat -nuap 命令。

pidof 进程名

功能:查看进程的 pid。

xargs 命令

功能:将上一个命令传入管道的内容转换成后一个命令的参数。

通过上述命令快速杀掉启动的 udpserver 进程。

2. 翻译服务器 -- Translation server

2.1 Udp 服务端封装 -- UdpServer.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "Log.hpp"
#include "InetAddr.hpp"

using namespace LogModule;
using func_t = std::function<std::string(const std::string&, InetAddr&)>;  // 参数为string& 返回值为 string 的函数类型

const int defaultfd = -1;

class UdpServer
{
public:
    UdpServer(uint16_t port, func_t func)
        : _sockfd(defaultfd),
         _port(port),
         _isrunning(false),
         _func(func)
        {}

    void Init()
    {
        // 1. 创建套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket create error!";
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket create seccess, sockfd: " << _sockfd;

        // 2. 绑定 socket 信息,ip 和 端口号
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = INADDR_ANY;

        // 2.2 绑定服务器的套接字信息
        int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind success, sockfd: " << _sockfd;
    }

    void Start()
    {
        _isrunning = true;
        while(_isrunning)
        {
            // 1. 创建用于接收消息的缓冲器变量 buffer 以及接收远端主机的套接字变量 peer
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);

            // 2. 收消息,服务端收取客户端的数据,对数据进行处理
            ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
            if (s > 0)
            {
                InetAddr client(peer);
                int peer_port = ntohs(peer.sin_port);
                std::string peer_ip = inet_ntoa(peer.sin_addr);
                buffer[s] = 0;

                // 2. 发消息,将消息进行处理后回发给客户端
                std::string result = _func(buffer, client); // 处理数据
                sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr*)&peer, len);
            }
        }
    }

    ~UdpServer()
    {}

private:
    int _sockfd;    // 套接字描述符
    uint16_t _port; // 端口号
    bool _isrunning;// 运行标志位
    func_t _func;   // 服务端处理数据的回调函数
};

2.2 字典结构体的封装 -- Dict.hpp

字典文件 -- dictionary.txt

bash 复制代码
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
hello:
: 你好

run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天
cpp 复制代码
#pragma once

#include <iostream>
#include <fstream>
#include <string>
#include <unordered_map>
#include "Log.hpp"
#include "InetAddr.hpp"

const std::string defaultDictPath = "./dictionary.txt";
const std::string sep = ": ";

using namespace LogModule;

class Dict
{
public:
    Dict(const std::string &path = defaultDictPath)
        : _dict_path(path)
    {}

    bool LoadDict()
    {
        std::ifstream in(_dict_path);
        if (!in.is_open())
        {
            LOG(LogLevel::DEBUG) << "打开字典:" << _dict_path << " 失败";
            return false;
        }

        // 1. 循环加载字典的每行数据
        std::string line;
        while(std::getline(in, line))
        {
            auto pos = line.find(sep);
            // 1.1 排除字典中无效内容
            if (pos == std::string::npos)
            {
                LOG(LogLevel::WARNING) << "解析: " << line << " 失败";
                continue; 
            }

            // 1.2 将有效内容进行加载
            std::string english = line.substr(0, pos);
            std::string chinese = line.substr(pos + sep.size());
            _dict.insert(std::make_pair(english, chinese));
            if (english.empty() || chinese.empty())
            {
                LOG(LogLevel::WARNING) << line << "没有有效内容";
                continue;
            }

            _dict.insert(std::make_pair(english, chinese));
            LOG(LogLevel::DEBUG) << "加载: " << line << " 成功";
        }
        
        in.close();
        return true;
    }

    std::string Translate(const std::string &word, InetAddr &client)
    {
        auto iter = _dict.find(word);
        if (iter == _dict.end())
        {
            LOG(LogLevel::DEBUG) << "进入到了翻译模块,[" << client.Ip() << " : " << client.Port() << "]# " << word << "->None";
            return "None";
        }
        LOG(LogLevel::DEBUG) << "进入到了翻译模块,[" << client.Ip() << " : " << client.Port() << "]# " << word << "->" << iter->second;
        return iter->second;
    }

    ~Dict()
    {}

private:
    std::string _dict_path; // 路径 + 文件名
    std::unordered_map<std::string, std::string> _dict;
};

2.3 网络地址转主机地址的封装 -- InetAddr.hpp

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

class InetAddr
{
public:
    InetAddr(struct sockaddr_in &addr) : _addr(addr)
    {
        _port = ntohs(_addr.sin_port);
        _ip = inet_ntoa(_addr.sin_addr);
    }

    uint16_t Port() {return _port;}
    std::string Ip() {return _ip;}

    ~InetAddr()
    {}
private:
    struct sockaddr_in _addr;
    std::string _ip;
    uint16_t _port;
};

2.4 Udp 服务端 -- UdpServer.cc

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

// 回显服务经常用于检测
std::string defaultHandler(const std::string &message)
{
    std::string s = "server say@ ";
    s += message;
    return s;
}

// 通过命令行 ./udpserver port 启动服务器
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " port" << std::endl;
        return 1;
    }

    uint16_t port = std::stoi(argv[1]);
    Enable_Console_Log_Strategy();

    // 1. 字典对象,提供翻译功能
    Dict dict;
    dict.LoadDict();

    // 2. 网络服务器对象,提供通信功能
    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, [&dict](const std::string &word, InetAddr &client)->std::string{
        return dict.Translate(word, client);
    });
    usvr->Init();
    usvr->Start();
    return 0;
}

2.5 Udp 客户端 -- UdpClient.cc

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

// 通过命令行 ./udpclient server_ip server_port 启动客户端
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
        return 1;
    }

    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);

    // 1. 创建套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket create error" << std::endl;
        return 2;
    }

    // 2. 填充服务端的套接字信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;    // AF_INET 或者 PF_INET
    server.sin_port = htons(server_port);
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());

    // 3. 循环读取客户端消息
    while(true)
    {
        // 3.1. 给客户端发单词
        std::string input;
        std::cout << "Please Enter# ";
        std::getline(std::cin, input);
        if (input.empty()) continue;
        int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr*)&server, sizeof(server));
        (void)n;

        // 3.2. 显示翻译后的中文
        char buffer[1024];
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
        if (m > 0)
        {
            buffer[m] = 0;
            std::cout << buffer << std::endl;
        }
    }
    return 0;
}
相关推荐
A小辣椒4 小时前
TShark:Wireshark CLI 功能
linux
A小辣椒8 小时前
TShark:基础知识
linux
AlfredZhao10 小时前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao1 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334661 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪1 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠2 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush42 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5202 天前
Linux 11 动态监控指令top
linux
网络研究院2 天前
2026年网络安全
网络·安全·法律·法规·趋势·发展