嘉兴住房和城乡建设厅网站,asp.net 网站 结构,产品分类 网站模板,网站搭建的流程个人主页#xff1a;C忠实粉丝 欢迎 点赞#x1f44d; 收藏✨ 留言✉ 加关注#x1f493;本文由 C忠实粉丝 原创 计算机网络socket编程(2)_UDP网络编程实现网络字典 收录于专栏【计算机网络】 本专栏旨在分享学习计算机网络的一点学习笔记#xff0c;欢迎大家在评论区交流讨… 个人主页C忠实粉丝 欢迎 点赞 收藏✨ 留言✉ 加关注本文由 C忠实粉丝 原创 计算机网络socket编程(2)_UDP网络编程实现网络字典 收录于专栏【计算机网络】 本专栏旨在分享学习计算机网络的一点学习笔记欢迎大家在评论区交流讨论 目录 功能介绍 :
dict.txt
1. nocopy.hpp
2. InetAddr.hpp
3. Log.hpp
4. LockGuard.hpp
5. Dict.hpp
6. UdpServer.hpp
7. UdpServerMain.cc
8. UdpClientMain.cc
9. 效果展示 功能介绍 :
实现一个简单的英译汉的网络字典
dict.txt
这里就放一些简单的单词, 方便测试~
1. nocopy.hpp
定义一个 nocopy 的类, 通过 C11 delete 关键字的特性, 阻止该类的拷贝构造和拷贝赋值.
#pragma onceclass nocopy
{
public:nocopy(){}~nocopy(){}nocopy(const nocopy) delete;const nocopy operator(const nocopy) delete;
};
2. InetAddr.hpp
InetAddr 类它封装了网络地址 (IP 端口), 以及提供了访问这些信息的方法, 该类通过了 struct sockaddr_in 来存储 IP 地址和端口, 并提供了转换和获取信息的功能
#pragma once#include iostream
#include string
#include sys/types.h
#include sys/socket.h
#include arpa/inet.h
#include netinet/in.hclass InetAddr
{
private:void ToHost(const struct sockaddr_in addr){_port ntohs(addr.sin_port);_ip inet_ntoa(addr.sin_addr);}public:InetAddr(const struct sockaddr_in addr):_addr(addr){ToHost(addr);}std::string Ip(){return _ip;}uint16_t Port(){return _port;}~InetAddr(){}private:std::string _ip;uint16_t _port;struct sockaddr_in _addr;
};成员变量 :
_ip : 存储 IP 地址, 使用 std::string 类型, 因为 IP 地址通常表示一个点分十进制的字符串
_port : 存储端口号, 使用 uint16_t 类型, 端口号是一个16位的无符号整数
_addr : 存储原始的 struct sockaddr_in 结构体, 它包含了 IP 地址和端口信息 ToHost():
ToHost() : 将 struct sockaddr_in 中的网络字节数据转换为主机字节序, 并提取 IP 和端口.
ntohs(addr.sin_port) : 将网络字节序的端口号转换为主机字节序
inet_ntoa(addr.sin_addr) : 将网络字节序的 IP 地址转换为点分十进制的字符串的形式 构造函数 :
构造函数 : 接受一个 struct sockaddr_in 类型的参数, 表示网络地址
构造函数初始化 _addr 成员, 存储传入的地址
然后调用 ToHost() 方法来从该地址中提取 IP 和端口, 并进行转换
Ip() Port()
Ip() : 返回存储的 IP 地址, 类型为 std::string
Port() : 返回存储的端口号, 类型为 uint16_t 析构函数 :
由于没有动态分配资源, 所以没有必要进行额外的清理工作~
3. Log.hpp
这段代码实现了一个简单的日志系统, 可以将日志信息输出到控制台或文件中.
#pragma once#include iostream
#include sys/types.h
#include unistd.h
#include ctime
#include cstdarg
#include fstream
#include cstring
#include pthread.h
#include LockGuard.hppnamespace log_ns
{enum{DEBUG 1,INFO,WARNING,ERROR,FATAL};std::string LevelToString(int level){switch (level){case DEBUG:return DEBUG;case INFO:return INFO;case WARNING:return WARNING;case ERROR:return ERROR;case FATAL:return FATAL;default:return UNKNOWN;}}std::string GetCurrTime(){time_t now time(nullptr);struct tm *curr_time localtime(now);char buffer[128];snprintf(buffer, sizeof(buffer), %d-%02d-%02d %02d:%02d:%02d,curr_time-tm_year 1900,curr_time-tm_mon 1,curr_time-tm_mday,curr_time-tm_hour,curr_time-tm_min,curr_time-tm_sec);return buffer;}class logmessage{public:std::string _level;pid_t _id;std::string _filename;int _filenumber;std::string _curr_time;std::string _message_info;};#define SCREEN_TYPE 1
#define FILE_TYPE 2const std::string glogfile ./log.txt;pthread_mutex_t glock PTHREAD_MUTEX_INITIALIZER;// log.logMessage(, 12, INFO, this is a %d message ,%f, %s hellwrodl, x, , , );class Log{public:Log(const std::string logfile glogfile) : _logfile(logfile), _type(SCREEN_TYPE){}void Enable(int type){_type type;}void FlushLogToScreen(const logmessage lg){printf([%s][%d][%s][%d][%s] %s,lg._level.c_str(),lg._id,lg._filename.c_str(),lg._filenumber,lg._curr_time.c_str(),lg._message_info.c_str());}void FlushLogToFile(const logmessage lg){std::ofstream out(_logfile, std::ios::app);if (!out.is_open())return;char logtxt[2048];snprintf(logtxt, sizeof(logtxt), [%s][%d][%s][%d][%s] %s,lg._level.c_str(),lg._id,lg._filename.c_str(),lg._filenumber,lg._curr_time.c_str(),lg._message_info.c_str());out.write(logtxt, strlen(logtxt));out.close();}void FlushLog(const logmessage lg){// 加过滤逻辑 --- TODOLockGuard lockguard(glock);switch (_type){case SCREEN_TYPE:FlushLogToScreen(lg);break;case FILE_TYPE:FlushLogToFile(lg);break;}}void logMessage(std::string filename, int filenumber, int level, const char *format, ...){logmessage lg;lg._level LevelToString(level);lg._id getpid();lg._filename filename;lg._filenumber filenumber;lg._curr_time GetCurrTime();va_list ap;va_start(ap, format);char log_info[1024];vsnprintf(log_info, sizeof(log_info), format, ap);va_end(ap);lg._message_info log_info;// 打印出来日志FlushLog(lg);}~Log(){}private:int _type;std::string _logfile;};Log lg;#define LOG(Level, Format, ...) \do \{ \lg.logMessage(__FILE__, __LINE__, Level, Format, ##__VA_ARGS__); \} while (0)
#define EnableScreen() \do \{ \lg.Enable(SCREEN_TYPE); \} while (0)
#define EnableFILE() \do \{ \lg.Enable(FILE_TYPE); \} while (0)
};
日志级别定义 :
定义了五个日志级别DEBUG、INFO、WARNING、ERROR 和 FATAL数字1到5代表不同的优先级。
DEBUG : 含义: DEBUG 级别的日志用于调试目的主要记录程序运行中的详细信息帮助开发人员理解程序的内部状态。这些信息通常对开发人员在开发、调试、分析问题时非常有用。 应用场景: 记录函数调用的输入输出参数。 输出变量的值、内存地址或其他详细的系统状态信息。 打印细节像是某个功能执行的具体步骤、算法的中间结果等。 优先级: 最低的日志优先级通常只在开发环境中启用。 INFO : 含义: INFO 级别的日志用于记录常规信息表示系统按预期运行的正常操作。这些日志通常用于展示系统的状态或者某些操作的成功执行提供程序执行过程中有用的背景信息。 应用场景: 系统启动或停止。 配置的加载。 用户执行操作的成功消息。 优先级: 相对较低但仍然是一个有用的日志级别。适用于生产环境中记录系统的常规行为和关键事件。 WARNING : 含义: WARNING 级别的日志用于记录潜在的问题或不正常的情况这些情况可能不会导致程序崩溃或立即失败但可能会影响程序的执行或系统的稳定性。警告通常表明某些操作可能需要关注或修改但并不一定立即需要处理。 应用场景: 配置文件中的可疑设置或过时的配置项。 资源的使用接近极限比如内存占用接近最大值。 网络延迟、连接问题等。 优先级: 高于 INFO 级别意味着它是需要关注的但不至于影响程序的正常运行。 ERROR : 含义: ERROR 级别的日志用于记录系统中发生的错误这些错误通常会导致某些功能或操作失败但不会完全中断系统的运行。错误通常需要被开发人员注意和修复或者需要采取措施来避免进一步的问题。 应用场景: 数据库连接失败。 文件读写错误。 网络请求失败。 输入参数错误导致某个功能不能正常执行。 优先级: 高于 WARNING 级别意味着这类问题更严重但仍然不是致命的。通常会影响用户体验需要开发人员快速处理。 FATAL : 含义: FATAL 级别的日志用于记录严重错误这些错误通常会导致程序崩溃或系统完全无法继续运行。FATAL 级别的日志代表最严重的错误需要立即处理。这些错误通常是程序中的致命缺陷可能需要紧急修复或采取特殊措施来恢复系统的正常运行。 应用场景: 程序崩溃或内存泄漏导致系统无法继续运行。 系统无法启动或重要组件丢失。 某些关键操作失败无法继续执行后续步骤。 优先级: 最高的日志优先级。需要立即采取措施通常需要系统管理员或开发人员介入。 LevelToString 函数, 该函数用于将日志级别数字转换为对应的字符串
GetCurrTime 函数, 获取当前系统时间并格式化为字符串。使用 strftime 格式化时间返回一个格式化后的时间字符串
logmessage 类用于封装日志消息的结构体包含 _level日志级别 _id进程ID _filename源代码文件名 _filenumber行号 _curr_time当前时间 _message_info实际的日志消息内容 Log 类是日志管理的核心类包含了日志的输出控制和处理方法。它的主要功能是 构造函数接受一个日志文件路径并初始化日志类型为屏幕输出SCREEN_TYPE。 Enable 方法设置日志输出类型支持屏幕输出或文件输出。 FlushLogToScreen将日志信息输出到屏幕。 FlushLogToFile将日志信息写入文件。默认文件路径为 ./log.txt。 FlushLog根据日志类型决定将日志输出到屏幕还是文件。使用 LockGuard 实现线程安全。 logMessage 方法日志记录的核心方法接受文件名、行号、日志级别和日志内容格式化字符串。通过 va_list 支持可变参数。 析构函数析构时没有特别的清理操作。 宏函数定义 :
#define LOG(Level, Format, ...) LOG 是一个宏用于简化日志记录的调用。这个宏接受以下参数 Level: 日志的级别如 INFO, DEBUG, ERROR 等。 Format: 格式化字符串类似于 printf 中使用的格式说明符例如 %s, %d。 ...: 可变参数可以传递给 logMessage 方法具体取决于格式字符串和实际参数。 do { ... } while (0)这种结构常用于宏定义中目的是将宏的多条语句包裹在一个代码块中并确保宏使用时不受周围代码的影响。它保证了宏的调用语句总是作为一个完整的语句来执行。
__FILE__ 和 __LINE__这两个预定义宏分别提供当前源文件的文件名和当前行号用来记录日志发生的位置。
lg.logMessage(__FILE__, __LINE__, Level, Format, ##__VA_ARGS__)这是宏的核心部分调用 Log 类的 logMessage 方法。它将当前文件名、行号、日志级别、格式字符串和变长参数传递给 logMessage。 __FILE__ 和 __LINE__ 提供日志的上下文信息。 Level 指定日志的级别如 INFO、ERROR 等。 Format 是日志的格式字符串。 ##__VA_ARGS__ 用来传递实际的参数到 logMessage 中## 语法在某些编译器中可以去掉多余的逗号比如如果没有传递变长参数时。 #define EnableScreen()
该宏用于启用屏幕日志输出。宏的定义如下
lg.Enable(SCREEN_TYPE)调用 Log 类的 Enable 方法传递一个常量 SCREEN_TYPE该常量可能表示日志的输出目标是屏幕。具体来说Enable 方法是配置日志输出的目标或启用某种日志记录模式。
do { ... } while (0)同样使用这种方式包裹宏确保宏调用的语法不会受到其他代码块的影响 #define EnableFILE()
lg.Enable(FILE_TYPE)调用 Log 类的 Enable 方法传递一个常量 FILE_TYPE表示日志的输出目标是文件。FILE_TYPE 是一个标志常量表示日志应该写入到文件而不是其他地方例如屏幕。
do { ... } while (0)同样使用这个结构以确保宏调用时不受外部代码的影响。
4. LockGuard.hpp
#pragma once#include pthread.hclass LockGuard
{
public:LockGuard(pthread_mutex_t *mutex):_mutex(mutex){pthread_mutex_lock(_mutex);}~LockGuard(){pthread_mutex_unlock(_mutex);}
private:pthread_mutex_t *_mutex;
};
这段代码定义了一个名为 LockGuard 的 C 类其目的是为多线程环境中的互斥锁pthread_mutex_t提供一种自动管理锁的机制。LockGuard 是一种 RAIIResource Acquisition Is Initialization 风格的锁管理类用于简化锁的获取和释放。
构造函数当 LockGuard 对象被创建时它会接受一个指向互斥锁的指针 mutex并将其存储在 _mutex 成员变量中。pthread_mutex_lock(_mutex)此语句通过 pthread_mutex_lock 函数请求对传入的互斥锁的独占访问权限。若锁已经被其他线程持有当前线程将会被阻塞直到锁变为可用。这样在 LockGuard 对象被创建时它会立即获取互斥锁确保对共享资源的访问在 LockGuard 对象的生命周期内是安全的。
析构函数当 LockGuard 对象超出作用域即生命周期结束时析构函数会被自动调用。pthread_mutex_unlock(_mutex)在析构函数中调用 pthread_mutex_unlock 来释放锁。这样互斥锁在 LockGuard 对象销毁时被自动解锁避免了手动解锁的遗漏保证了线程安全。
RAII 特性
LockGuard 类的核心是 RAII资源获取即初始化模式。通过这种模式在对象创建时自动获取锁在对象销毁时自动释放锁避免了程序员忘记解锁的情况。RAII 确保了即使发生异常锁也能被正确释放因为栈上的局部对象如 LockGuard会在离开作用域时自动销毁从而触发析构函数并释放资源。
5. Dict.hpp
// 实现一个简单的英译汉的网络字典
#pragma once
#include iostream
#include string
#include fstream
#include unordered_map
#include unistd.h
#include Log.hppusing namespace log_ns;const static std::string sep : ;// sad: 悲伤的class Dict
{
private:void LoadDict(const std::string path){std::ifstream in(path);if (!in.is_open()){LOG(FATAL, open %s failed!\n, path.c_str());exit(1);}std::string line;while (std::getline(in, line)){LOG(DEBUG, load info: %s , success\n, line.c_str());if (line.empty())continue;auto pos line.find(sep);if (pos std::string::npos)continue;std::string key line.substr(0, pos);if (key.empty())continue;std::string value line.substr(pos sep.size());if (value.empty())continue;_dict.insert(std::make_pair(key, value));}LOG(INFO, load %s done\n, path.c_str());in.close();}public:Dict(const std::string dict_path) : _dict_path(dict_path){LoadDict(_dict_path);}std::string Translate(std::string word){if(word.empty()) return None;auto iter _dict.find(word);if(iter _dict.end()) return None;else return iter-second;}~Dict(){}private:std::unordered_mapstd::string, std::string _dict;std::string _dict_path;
};const static std::string sep : ;
这里定义了一个常量 sep它是字典文件中键值对之间的分隔符。在字典文件中格式是 word: translation即单词和翻译之间由 : 分
Dict 类定义
私有成员 :
_dict一个 unordered_map 类型的成员变量用来存储词典数据。键是单词std::string值是对应的翻译std::string。
_dict_path存储字典文件路径的成员变量。 LoadDict 方法 :
LoadDict 方法负责从指定路径加载字典文件并将内容解析到 _dict 成员变量中
打开文件使用 std::ifstream 打开字典文件。若打开失败记录日志并退出程序。
逐行读取文件使用 std::getline 逐行读取文件内容
解析字典内容 : 跳过空行。 查找 sep即 : 在行中的位置分割单词和翻译。 使用 substr 提取单词key和翻译value。 将有效的单词-翻译对插入到 _dict 中。 日志记录
在加载每一行时记录调试级别日志。
在整个字典加载完成后记录信息级别日志。
最后关闭文件
公共方法
构造函数在构造函数中调用 LoadDict 方法加载字典文件。
Translate 方法
该方法用于翻译给定的单词word。
如果单词为空返回 None。
使用 unordered_map 的 find 方法查找单词。如果找不到返回 None否则返回对应的翻译
析构函数 : 析构函数目前没有特别的操作因为 std::unordered_map 会自动管理内存。
6. UdpServer.hpp
#pragma once
#include iostream
#include unistd.h
#include string
#include cstring
#include functional
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h#include nocopy.hpp
#include Log.hpp
#include InetAddr.hppusing namespace log_ns;static const int gsockfd -1;
static const uint16_t glocalport 8888;enum
{SOCKET_ERROR 1,BIND_ERROR
};using func_t std::functionstd::string(std::string);
// UdpServer user(192.1.1.1, 8899)
// 一般服务器主要是用来进行网络数据读取和写入的, IO的
// 服务器IO逻辑 和 业务逻辑 解耦class UdpServer : public nocopy
{
public:UdpServer(func_t func, uint16_t localport glocalport):_func(func),_sockfd(gsockfd),_localport(localport),_isrunning(false){}void InitServer(){// 1. 创建socket文件_sockfd ::socket(AF_INET, SOCK_DGRAM, 0);if(_sockfd 0){LOG(FATAL, socket error\n);exit(SOCKET_ERROR);}LOG(DEBUG, socket create success, _sockfd : %d, _sockfd);// 3// 2. bindstruct sockaddr_in local;memset(local, 0, sizeof(local));local.sin_family AF_INET;local.sin_port htons(_localport);local.sin_addr.s_addr INADDR_ANY; // 服务器端, 进行任意 IP 地址绑定int n ::bind(_sockfd, (struct sockaddr *)local, sizeof(local));if(n 0) {LOG(FATAL, bind error);exit(BIND_ERROR);}LOG(DEBUG, socket bind success\n);}void Start(){_isrunning true;char inbuffer[1024];while(_isrunning){struct sockaddr_in peer;socklen_t len sizeof(peer);ssize_t n recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)peer, len);if(n 0){InetAddr addr(peer);inbuffer[n] 0;// 一个一个的单词std::cout [ addr.Ip() : addr.Port() ]# inbuffer std::endl;std::string result _func(inbuffer);sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr *)peer, len);}else{std::cout recvfrom, error std::endl;}}}~UdpServer(){if(_sockfd gsockfd)::close(_sockfd);}
private:int _sockfd;uint16_t _localport;bool _isrunning;func_t _func;
};
这段代码实现了一个简单的 UDP 服务器类 UdpServer能够接收客户端发送的消息并通过用户提供的回调函数处理消息后返回响应。该类还实现了日志记录、套接字管理、地址绑定等基础的网络通信功能。
全局常量与类型定义 gsockfd默认的套接字文件描述符设置为 -1 作为无效值。 glocalport默认的本地端口号设置为 8888如果未传递特定端口号默认使用此端口。 错误码枚举定义了 SOCKET_ERROR 和 BIND_ERROR用于表示套接字创建和绑定失败的错误。 func_t 类型别名
func_t定义了一个类型别名 func_t表示一个回调函数类型。该函数接收一个 std::string 类型的参数并返回一个 std::string 类型的结果。通过这个回调函数服务器可以自定义处理接收到的数据。 1. using 关键字 using 是 C11 引入的关键字用于定义类型别名。它与传统的 typedef 类似但更现代、更简洁。using 可以为复杂的类型提供更直观、更易读的别名。 例如using 可以让我们更方便地为模板类型定义别名这在某些情况下比 typedef 更易于理解。 这种写法在 C11 和之后的标准中更常见特别是在模板类型、函数类型等复杂类型定义时。 2. std::functionstd::string(std::string) std::function 是 C11 标准库提供的一个模板类它用于封装任何可调用对象例如函数指针、函数对象、Lambda 表达式、成员函数指针等使它们能够被统一处理。 std::function 是一个通用的函数包装器它提供了统一的接口来调用不同类型的可调用对象。具体来说std::function 可以封装函数、函数指针、Lambda 表达式、绑定函数、成员函数等。 std::functionstd::string(std::string) 表示一个接受 std::string 类型参数并返回 std::string 类型结果的可调用对象。 解释一下 std::functionstd::string(std::string) std::string表示返回类型即被封装的函数调用时会返回一个 std::string 类型的结果。 std::string(std::string)表示封装的函数类型即该函数接受一个 std::string 类型的参数返回一个 std::string 类型的值。 简单来说std::functionstd::string(std::string) 是一个可以封装任何接受 std::string 参数并返回 std::string 的可调用对象。 3. func_t 类型别名 func_t 是通过 using 定义的类型别名它代表了一个类型为 std::functionstd::string(std::string) 的函数对象。 也就是说func_t 类型可以表示 普通函数 Lambda 表达式 函数对象即重载了 operator() 的对象 函数指针等 这些可调用对象的共同特点是它们都能够接收一个 std::string 类型的参数并返回一个 std::string 类型的值。 UdpServer 类
UdpServer 类是实现 UDP 服务器功能的核心部分。它继承自 nocopy意味着不能拷贝该类的实例。
成员变量 : _sockfd存储套接字文件描述符用于进行网络通信。 _localport服务器监听的本地端口号。 _isrunning标志服务器是否正在运行。 _func存储一个回调函数用于处理接收到的数据。 构造函数
func构造函数接受一个回调函数 func用于处理接收到的消息。
localport设置监听端口如果没有提供默认使用 glocalport8888。
InitServer 方法
创建套接字使用 ::socket() 创建一个 UDP 套接字。AF_INET 表示 IPv4 地址族SOCK_DGRAM 表示 UDP 类型。
如果创建失败记录错误日志并退出。
绑定套接字::bind() 将套接字与指定的本地端口绑定。
local.sin_family AF_INET表示使用 IPv4 地址。
local.sin_port htons(_localport)设置本地端口号使用 htons() 转换为网络字节序。
local.sin_addr.s_addr INADDR_ANY绑定到所有本地网络接口的 IP 地址。
Start 方法
循环接收消息recvfrom() 从套接字接收数据并将数据存储在 inbuffer 中。该方法会阻塞直到接收到数据。
peer存储客户端的地址信息IP 和端口。
len保存 peer 地址的大小。
n接收到的字节数。
如果接收到数据则调用传入的回调函数 _func 对数据进行处理并通过 sendto() 发送回响应数据。
处理异常如果 recvfrom() 返回错误n 0则输出错误信息。
析构函数
关闭套接字当服务器停止运行时关闭套接字释放资源。
7. UdpServerMain.cc
#include UdpServer.hpp
#include Dict.hpp#include memory// ./udp_server local-port
// ./udp_server 8888
int main(int argc, char *argv[])
{if(argc ! 2){std::cerr Usage: argv[0] local-port std::endl;exit(0);}uint16_t port std::stoi(argv[1]);EnableScreen();Dict dict(./dict.txt);func_t translate std::bind(Dict::Translate, dict, std::placeholders::_1);std::unique_ptrUdpServer usvr std::make_uniqueUdpServer(translate, port); //C14的标准usvr-InitServer();usvr-Start();return 0;
}
这段代码实现了一个基于 UdpServer 的 UDP 服务器目的是通过翻译字典由 Dict 类提供的功能处理客户端发送的请求。通过网络通信服务器接收客户端发来的数据根据字典内容进行翻译并返回结果。
int main(int argc, char *argv[])
命令行参数检查main 函数首先检查传入的参数数量。如果参数不等于 2即程序名和端口号则打印使用说明并退出。参数的第一个是程序名第二个是本地监听端口。 argv[0]程序本身的路径或名称。 argv[1]传入的本地端口号用于 UDP 服务器的绑定。 端口号解析
端口号转换通过 std::stoi() 函数将命令行输入的字符串 argv[1] 转换为 uint16_t 类型的端口号。这个端口号将用来绑定 UDP 套接字。
启用屏幕输出
这里调用了 EnableScreen() 函数它用于初始化或启用屏幕输出/日志记录功能。
创建 Dict 对象
字典文件加载创建一个 Dict 类的实例 dict并从文件 dict.txt 加载字典内容。该文件路径传递给 Dict 的构造函数
创建翻译回调函数
创建回调函数这里使用了 std::bind 来绑定 Dict 类的成员函数 Translate将 dict 对象和 std::placeholders::_1 作为占位符传递给回调函数。std::placeholders::_1 代表传递给回调函数的第一个参数。
std::bind(Dict::Translate, dict, std::placeholders::_1) 会返回一个新的可调用对象 translate它能够接收一个 std::string 类型的参数并调用 dict.Translate() 方法进行翻译。
创建 UdpServer 对象
创建 UDP 服务器使用 std::make_uniqueUdpServer 创建一个 UdpServer 对象并传递 translate 回调函数和端口号 port。
这意味着 UDP 服务器将在启动时使用传入的端口号进行绑定并将 translate 作为数据处理的回调函数。当服务器接收到数据时它将调用 translate 函数来处理收到的数据并返回结果。
初始化和启动服务器
初始化服务器usvr-InitServer() 初始化服务器创建套接字并绑定端口。这一步会创建 UDP 套接字并将其绑定到本地端口 port。
启动服务器usvr-Start() 启动服务器开始接收客户端的请求并处理数据。这个方法会进入一个循环不断接收客户端数据并调用 translate 函数进行处理后发送回响应。
8. UdpClientMain.cc
#include iostream
#include string
#include cstring
#include unistd.h
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h// 客户端在未来一定要知道服务器的IP地址和端口号
// ./udp_client server-ip server-port
// ./udp_client 127.0.0.1 8888
int main(int argc, char *argv[])
{if(argc ! 3){std::cerr Usage: argv[0] server-ip server-port std::endl;exit(0);}std::string serverip argv[1];uint16_t serverport std::stoi(argv[2]);int sockfd ::socket(AF_INET, SOCK_DGRAM, 0);if(sockfd 0){std::cerr create socket error std::endl;exit(1);}// client的端口号一般不让用户自己设定而是让client OS随机选择怎么选择什么时候选择呢// client 需要 bind它自己的IP和端口 但是client 不需要 “显示” bind它自己的IP和端口 // client 在首次向服务器发送数据的时候OS会自动给client bind它自己的IP和端口struct sockaddr_in server;memset(server, 0, sizeof(server));server.sin_family AF_INET;server.sin_port htons(serverport);server.sin_addr.s_addr inet_addr(serverip.c_str());while(1){std::string line;std::cout Please Enter# ;std::getline(std::cin, line);// std::cout line message is line std::endl;int n sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr*)server, sizeof(server)); // 你要发送消息你得知道你要发给谁啊if(n 0){struct sockaddr_in temp;socklen_t len sizeof(temp);char buffer[1024];int m recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)temp, len);if(m 0){buffer[m] 0;std::cout buffer std::endl;}else{std::cout recvfrom error std::endl;break;}}else{std::cout sendto error std::endl;break;}}::close(sockfd);return 0;
}
1. 程序输入检查
argc 是命令行参数的数量argv 是一个字符串数组其中存储了命令行输入的参数。
代码检查命令行参数数量是否正确。如果用户没有提供服务器的 IP 地址和端口号即参数数量不等于 3则输出使用帮助信息并退出程序。
2. 服务器地址和端口号的解析
serverip获取用户输入的服务器 IP 地址例如 127.0.0.1。
serverport获取用户输入的服务器端口号并将其转换为 uint16_t 类型。
3. 创建 UDP 套接字
使用 ::socket 函数创建一个套接字。
AF_INET指定使用 IPv4 协议。
SOCK_DGRAM指定使用 数据报 套接字适用于 UDP。
0通常表示选择默认的协议对于 UDP通常是 IPPROTO_UDP。
如果创建套接字失败socket 返回值小于零程序会输出错误信息并退出。
4. 构造服务器地址结构
sockaddr_in 结构用于存储 IPv4 地址和端口信息。
memset(server, 0, sizeof(server))将 server 结构体初始化为 0避免不必要的垃圾数据。
server.sin_family AF_INET指定协议族为 IPv4。
server.sin_port htons(serverport)将端口号转换为网络字节顺序htons 是 host to network short将主机字节顺序转换为网络字节顺序。
server.sin_addr.s_addr inet_addr(serverip.c_str())将字符串格式的 IP 地址转换为网络字节顺序的地址。
5. 发送和接收数据
发送数据
std::getline(std::cin, line)从标准输入读取用户输入的字符串。
sendto通过 UDP 套接字向服务器发送数据。
sockfd套接字描述符。
line.c_str()要发送的消息c_str() 返回一个指向 std::string 数据的指针sendto 需要 C 风格的字符串。
line.size()消息的长度。
0标志位通常为 0没有特定的设置。
(struct sockaddr*)server服务器的地址信息。
sizeof(server)地址结构的大小。
sendto 返回发送的字节数若发送成功返回大于零的值。
接收数据
recvfrom接收来自服务器的响应。
buffer接收数据的缓冲区。
sizeof(buffer)-1指定缓冲区大小recvfrom 会在收到数据后填充该缓冲区。
(struct sockaddr*)temp接收者的地址信息虽然客户端在此不需要使用接收者的地址但仍需传入一个 sockaddr_in 类型的变量来接收。
len地址结构的大小用于接收 recvfrom 填充的信息。
如果 recvfrom 成功返回实际接收的字节数如果失败返回负值。
数据处理
如果成功接收到数据则将数据写入输出流std::cout并打印接收到的消息。
如果接收失败输出错误信息并退出循环。
6. 关闭套接字
9. 效果展示 这里我们做的比较简陋, 只能英翻中~~ 不过我们实现还是没有问题的, 好了, 篇幅已经很长了, 网络编程是这样的, 我们要考虑的东西很多, 我们下期再见~~