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;
}
相关推荐
扶尔魔ocy6 分钟前
【Linux C/C++开发】轻量级关系型数据库SQLite开发(包含性能测试代码)
linux·数据库·c++·sqlite
今儿敲了吗1 小时前
计网| 网际控制报文协议(ICMP)
网络·智能路由器
追赶sun1 小时前
Ubuntu 添加系统调用
linux·ubuntu·操作系统·系统调用
北漂老男孩1 小时前
在 Linux 上安装 MATLAB:完整指南与疑难解决方案
linux·运维·matlab
像风一样的男人@2 小时前
Linux --systemctl损坏
linux·运维·服务器
南棱笑笑生2 小时前
20250515测试飞凌的OK3588-C的核心板在Linux R4下适配以太网RTL8211F-CG时跑iperf3的极速
linux·服务器·网络
南方以南_2 小时前
【云实验】搭建个人网盘实验
linux·运维·服务器
酷爱码2 小时前
Linux实现临时RAM登录的方法汇总
linux·前端·javascript
muxue1783 小时前
chmod 777含义:
linux
帷幄庸者3 小时前
CentOS 上配置 Docker 使用 NVIDIA GPU
linux·docker·centos