网络通信实战-UDP实现网络英汉字典

目录

  • [1. 格式转换接口扩展](#1. 格式转换接口扩展)
    • [1.1 字符串IP地址与网络字节序相互转换(支持IPv4和IPv6)](#1.1 字符串IP地址与网络字节序相互转换(支持IPv4和IPv6))
      • [1. inet_pton(字符串格式IP转网络字节序)](#1. inet_pton(字符串格式IP转网络字节序))
      • [2. inet_ntop(网络字节序转字符串格式IP)](#2. inet_ntop(网络字节序转字符串格式IP))
  • [2. 字典实现(UDP服务-客户端)](#2. 字典实现(UDP服务-客户端))
    • [2.1 网络字节序&主机字节序转换的类InetAddr封装](#2.1 网络字节序&主机字节序转换的类InetAddr封装)
    • [2.2 字典实现](#2.2 字典实现)
    • [2.3 服务端实现](#2.3 服务端实现)
    • [2.4 客户端实现](#2.4 客户端实现)
  • [3. 功能验证](#3. 功能验证)
  • [4. 字典代码](#4. 字典代码)

1. 格式转换接口扩展

1.1 字符串IP地址与网络字节序相互转换(支持IPv4和IPv6)

上一篇文章的2.5小节提到一对"点分十进制IP与网络字节序相互转换"的接口(inet_addr和inet_ntoa),但是这对接口只能对IPv4格式的IP地址进行转换。以下介绍的inet_pton和inet_ntop既可以同时支持IPv4和IPv6的IP地址转换。

了解

inet_pton是为支持 IPv6 而出现的函数,它在 IPv6(1995 年通过 RFC 1883 定义)提出之后问世(最早见于 1996 年左右的 NetBSD 1.3 系统);它属于 POSIX 标准的用户空间 C 标准库(如 Linux 下的 glibc)

1. inet_pton(字符串格式IP转网络字节序)

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

// 第一个函数:inet_pton(presentation to numeric,字符串格式 → 二进制数值格式)
int inet_pton(int af, const char *src, void *dst);
// 功能: 将人类易读的IP地址字符串(IPv4点分十进制/IPv6冒号十六进制),转换为计算机/网络协议可识别的二进制数值(网络字节序/大端序),
//       供对应套接字地址结构体(sockaddr_in/IPv4、sockaddr_in6/IPv6)使用,是现代跨协议(IPv4/IPv6)的安全转换函数。
// 参数: 
// 		int af:指定IP协议族,仅支持两个合法常量,不可混用:
// 			   AF_INET:表示处理IPv4地址,对应后续src为点分十进制字符串(如"192.168.1.100"),dst为struct in_addr结构体指针。
// 			   AF_INET6:表示处理IPv6地址,对应后续src为冒号十六进制字符串(如"fe80::a00:27ff:fe12:3456"),dst为struct in6_addr结构体指针。
// 		const char *src:指向合法IP地址字符串的常量指针,要求字符串格式与af指定的协议族匹配,否则转换失败。
// 		void *dst:指向用于存储转换结果的二进制数值缓冲区的通用指针,需提前分配内存(对应struct in_addr/struct in6_addr),
// 				  转换成功后,二进制数值(网络字节序)会写入该缓冲区,可直接赋值给对应套接字地址结构体的地址成员。

// 返回值:
// 		转换成功:返回1,src字符串格式合法且与af协议族匹配,二进制数值已成功写入dst缓冲区。
// 		无错但不兼容:返回0,src字符串格式合法,但与af指定的协议族不匹配(如af=AF_INET,src传入IPv6字符串),未写入dst。
// 		转换失败:返回-1,同时设置全局错误码errno,常见错误码:
// 				  EINVAL:af不是合法协议族(非AF_INET/AF_INET6),或src字符串格式非法(如IPv4传入"192.168.1.256")。
// 				  ENOSPC:dst缓冲区大小不足,无法存储转换后的二进制数值(极少出现,正常使用对应结构体可避免)。

2. inet_ntop(网络字节序转字符串格式IP)

cpp 复制代码
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// 第二个函数:inet_ntop(numeric to presentation,二进制数值格式 → 字符串格式)
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
// 功能: 将计算机/网络协议可识别的IP二进制数值(网络字节序/大端序,来自sockaddr_in/IPv4、sockaddr_in6/IPv6),
//       转换为人类易读的字符串格式(IPv4点分十进制/IPv6冒号十六进制),支持IPv4/IPv6双协议,使用用户提供的缓冲区存储结果,线程安全无数据覆盖风险。
// 参数: 
// 		int af:指定IP协议族,仅支持两个合法常量:
// 			   AF_INET:处理IPv4二进制数值,对应src为struct in_addr结构体指针,转换后为点分十进制字符串。
// 			   AF_INET6:处理IPv6二进制数值,对应src为struct in6_addr结构体指针,转换后为冒号十六进制字符串。
// 		const void *src:指向IP二进制数值缓冲区的通用常量指针,数据格式需与af指定的协议族匹配(如AF_INET对应struct in_addr),否则转换失败。
// 		char *dst:指向用户提前分配的输出字符串缓冲区的指针,转换后的IP字符串会写入该缓冲区,需保证缓冲区大小足够存储结果。
// 				  推荐使用系统预定义常量指定缓冲区大小,避免不足:
// 				  INET_ADDRSTRLEN:IPv4字符串缓冲区最小大小(16字节),足够存储最长IPv4点分十进制字符串。
// 				  INET6_ADDRSTRLEN:IPv6字符串缓冲区最小大小(46字节),足够存储最长IPv6冒号十六进制字符串。
// 		socklen_t size:指定dst输出缓冲区的字节大小,需与实际缓冲区大小一致(如sizeof(dst)、INET_ADDRSTRLEN),
// 					   若大小不足,转换失败并设置errno为ENOSPC。

// 返回值:
// 		转换成功:返回指向dst输出缓冲区的常量字符指针(与dst参数值一致),缓冲区中已存储合法的IP字符串格式数据。
// 		转换失败:返回NULL,同时设置全局错误码errno,常见错误码:
// 				  EINVAL:af不是合法协议族,或src二进制数值格式非法,无法转换为对应IP字符串。
// 				  ENOSPC:dst缓冲区大小size不足,无法存储转换后的IP字符串(需使用INET_ADDRSTRLEN/INET6_ADDRSTRLEN规避)。

2. 字典实现(UDP服务-客户端)

  • 字典的实现相对于上一节的简单UDP服务-客户端通信来讲,增加了几个功能:
  1. 对网络字节序&主机字节序之间的转换进行封装解耦,对网络中接收到的sockaddr_in直接调用InetAddr类即可得到客户端的ip和port。
  2. 服务端实例化使用了unique_ptr智能指针:
    unique_ptr是C++11引入的独占式智能指针,核心优势在于能自动释放所管理的动态资源以避免内存泄漏,通过独占所有权杜绝重复释放问题,同时支持动态数组管理和自定义删除器以实现通用RAII资源管理,还可借助移动语义安全地转移资源所有权,且几乎无额外性能开销,是替代裸指针进行独占资源管理的首选。

2.1 网络字节序&主机字节序转换的类InetAddr封装

  1. 网络转主机: sockaddr_in _addr 解析出 _ip_port

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>

// 网络转主机 格式转换 的类
// 1.网络转主机: sockaddr_in _addr 解析出 _ip和_port

class InetAddr
{
public:
    // 1.网络转主机: sockaddr_in _addr 解析出 _ip和_port
    InetAddr(struct sockaddr_in &addr) : _addr(addr)
    {
        _port = ntohs(_addr.sin_port);      //从网络中拿到的!网络序列
        _ip = inet_ntoa(_addr.sin_addr);    // 4字节网络风格的IP -> 点分十进制的字符串风格的IP
    }
    uint16_t Port() {return _port;}
    std::string Ip() {return _ip; }

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

2.2 字典实现

  1. 加载字典
  2. 翻译功能实现unordered_map

Dict.hpp

cpp 复制代码
#pragma

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

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

using namespace LogModule;

// 字典实现
// 1.加载字典
// 2.翻译功能实现unordered_map

class Dict
{
public:
    Dict(const std::string &path = defaultdict) : _dict_path(path)
    {
    }
    // 加载字典
    bool LoadDict()
    {
        std::ifstream in(_dict_path);
        if (!in.is_open())
        {
            LOG(LogLevel::DEBUG) << "打开字典: " << _dict_path << " 错误";
            return false;
        }
        std::string line;
        while (std::getline(in, line)) // 打开并从文件里面一次读入一行
        {
            auto pos = line.find(sep);
            if (pos == std::string::npos)
            {
                LOG(LogLevel::WARNING) << "解析: " << line << " 失败";
                continue;
            }
            std::string english = line.substr(0, pos);
            std::string chinese = line.substr(pos + sep.size()); // sep = ": "
            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;
};

dictionary.txt

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



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

2.3 服务端实现

  1. 创建套接字socket
  2. 创建套接字地址sockaddr_in,
  3. bind绑定套接字与套接字地址
  4. 发送/接收消息数据

UdpServer.hpp

cpp 复制代码
#pragma

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

using namespace LogModule;

using func_t = std::function<std::string(const std::string&, InetAddr&)>;

const int defaultfd = -1;

// 网络通信服务端
// 1.创建套接字socket
// 2.创建套接字地址sockaddr_in,
// 3.bind绑定套接字与套接字地址
// 4.发送/接收消息数据

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 error!";
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket success, sockfd : " << _sockfd;

        // 2.创建套接字地址sockaddr_in,填充协议sin_famil、ip地址(sin_addr)(网络序列)、sin_port端口号
        struct sockaddr_in local;
        bzero(&local, sizeof(local));   // 内存清0

        local.sin_family = AF_INET;		 	// IPv4协议是AF_INET
        local.sin_port = htons(_port);  	// 端口号 //本地格式 -> 网络序列 htons()函数转换
        local.sin_addr.s_addr = INADDR_ANY; // 但是实际使用Server服务端要接收发送到本机所有IP的信息。IP不止1个。所以IP地址要使用INADDR_ANY宏定义来赋值

        // 3.bind绑定套接字与套接字地址
        // 那么为什么服务器端要显式的bind呢?IP和端口必须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;
    }
    // 4.发送/接收消息数据
    void Start()
    {
        _isrunning = true;
        while(_isrunning)
        {
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            //  1. 收消息,client为什么要向服务器发送消息?目的就是要服务器端处理数据
            ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if(s > 0)
            {
                InetAddr client(peer);  // InetAddr类用于接收目标客户端主机的socket套接字信息,并修改peer

                buffer[s] = 0;
                // 2.收到的内容,当作英文单词,执行翻译功能
                std::string result = _func(buffer, client);  

                // 3.发消息
                // 将接收到的数据再返回client客户端,peer已经是目标客户端主机的套接字
                sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr*)&peer, len);
            }
        }
    }
    ~UdpServer()
    {
    }

private:
    int _sockfd;     // 套接字
    uint16_t _port;  // 端口号
    bool _isrunning; // 运行状态

    func_t _func;    // 服务器的回调 函数,对数据进行处理
};

UdpServer.cc

  1. 加载字典

  2. 使用服务-客户端通信实现网络字典翻译功能

cpp 复制代码
#include <iostream>
#include <memory>
#include "Dict.hpp" // 翻译功能
#include "UdpServer.hpp"    // 网络通信功能

// 仅仅是用来测试的
std::string defaulthandler(const std::string &message)
{
    std::string hello = "hello, ";
    hello += message;
    return hello;
}

// 功能:网络字典翻译
// 1.加载字典
// 2.使用服务-客户端通信实现网络字典翻译功能

// ./udpserver port
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        std::cerr << "usage: " << argv[0] << " port" << std::endl;
        return 1;
    }

    // std::string ip = argv[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&cli)->std::string{
        return dict.Translate(word, cli);
    });

    usvr->Init();
    usvr->Start();
    return 0;
}

2.4 客户端实现

  1. 创建套接字socket

  2. 创建套接字地址sockaddr_in,填写目标服务器信息

  3. 发送/接收信息

UdpClient.cc

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

// ./udpclient server_ip server_port
// 1.创建套接字socket
// 2.创建套接字地址sockaddr_in,填写目标服务器信息
// 3.发送/接收信息

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];            // 第2个参数为目标ip地址
    uint16_t server_port = std::stoi(argv[2]);  // 第3个参数为目标进程端口号

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

    // 关于bind:上一节已经提过,只需要将服务端的主机socket套接字进行bind固定绑定
    
    // 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());

    // 3.发送/接收消息
    while(true)
    {
        std::string input;
        std::cout << "Please Enter# ";
        std::getline(std::cin, input);

        // 3.1 发送消息
        int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&server, sizeof(server));
        (void)n;

        char buffer[1024];
        // 3.2 创建套接字地址,用于接收信息来源主机的套接字地址(IP和port端口号)
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);

        // 3.3 接受消息
        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;
}

3. 功能验证

  1. 运行./udpserver 8080之后,首先加载字典到内存中,字典中有问题的单词翻译会解析失败
  2. 另开一个终端运行./udpclient 127.0.0.1 8080,之后输入单词,通过网络发送到udpserver之后,网络对单词进行翻译并回传,则在udpclient终端会得到单词的翻译结果。

4. 字典代码

【免费】网络通信-UDP-UDP实现网络英汉字典资源-CSDN下载

相关推荐
xixixi777777 小时前
CDN(内容分发网络)——缓存和分发网站、应用程序、视频等内容,以提高用户访问速度和稳定性,减少网络延迟和拥塞,同时减轻源服务器的压力
网络·缓存·架构·系统架构·cdn·业务·内容分发网络
-To be number.wan7 小时前
【补漏版】计算机网络期末大题预测合集
网络·计算机网络
liulilittle8 小时前
OPENPPP2 Code Analysis Two
网络·c++·网络协议·信息与通信·通信
爱怪笑的小杰杰8 小时前
紧急补救:TCP心跳检测失效问题复盘与彻底解决
网络
学烹饪的小胡桃9 小时前
WGCAT工单系统 v1.2.7 更新说明
linux·运维·服务器·网络·工单系统
云飞云共享云桌面9 小时前
非标自动化工厂的设计云桌面为什么要选云飞云智能共享云桌面?
大数据·运维·服务器·网络·自动化·负载均衡
Lhan.zzZ9 小时前
基于Qt的UDP广播发现与TCP连接系统的设计与实现
qt·tcp/ip·udp
lowhot10 小时前
各种网络协议比较
网络·网络协议
运维有小邓@10 小时前
如何实现基于角色的访问控制?
运维·网络
EasyGBS10 小时前
EasyGBS打造变电站高效智能视频监控解决方案
网络·人工智能·音视频