网络通信实战-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下载

相关推荐
贞明电子2 小时前
工业三色报警灯怎么接入项目系统?开关量与RS485控制应用指南
运维·网络·微信公众平台·贞明电子
明天也要努力学习3 小时前
HTTP 协议的基本格式和 fiddler 的用法
网络协议·http·fiddler
HideF3 小时前
Linux
linux·服务器·网络
万联WANFLOW3 小时前
海外网络故障怎么排查?Cloudflare 三季度断网报告解读与 mtr 实战
运维·网络·故障分析
听风说ml3 小时前
socket编程 【C语言】
c语言·udp·tcp·socket编程
sbjdhjd3 小时前
网工进阶笔记:H3CSE 园区网全栈技术与协议汇总
网络·笔记·云计算·ssh·网络工程·vlan·aaa
taktok_ai5 小时前
用 cc-switch 连接 Taktok
网络
小嘉丶学长7 小时前
Day14 · 字典与集合
java·开发语言·网络·python·学习·1024程序员节
pt10439 小时前
Cisco设备网络安全系列-18:SIEM与SOAR安全解决方案
网络·安全
Tel199253080049 小时前
温度湿度信号转WiFi/MQTT/Modbus TCP采集模块 无线传输
网络·网络协议·tcp/ip·工业自动化·工控设备