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

2.1 网络字节序&主机字节序转换的类InetAddr封装
- 网络转主机:
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 字典实现
- 加载字典
- 翻译功能实现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 服务端实现
- 创建套接字socket
- 创建套接字地址sockaddr_in,
- bind绑定套接字与套接字地址
- 发送/接收消息数据
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
-
加载字典
-
使用服务-客户端通信实现网络字典翻译功能
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 客户端实现
-
创建套接字socket
-
创建套接字地址sockaddr_in,填写目标服务器信息
-
发送/接收信息
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. 功能验证
- 运行
./udpserver 8080之后,首先加载字典到内存中,字典中有问题的单词翻译会解析失败 - 另开一个终端运行
./udpclient 127.0.0.1 8080,之后输入单词,通过网络发送到udpserver之后,网络对单词进行翻译并回传,则在udpclient终端会得到单词的翻译结果。
