目录
[Windows 客户端环境(VS2022)](#Windows 客户端环境(VS2022))
[Linux 服务端环境(Ubuntu + Xshell)](#Linux 服务端环境(Ubuntu + Xshell))
[运行结果 :](#运行结果 :)
[二、新增 Dict 字典翻译模块的 UDP 通信](#二、新增 Dict 字典翻译模块的 UDP 通信)
[Dict 类](#Dict 类)
[Dict 类整体结构](#Dict 类整体结构)
[EchoServer.hpp 文件中 UdpServer 类的改造](#EchoServer.hpp 文件中 UdpServer 类的改造)
[EchoServerMain.cc 服务端入口文件的改造](#EchoServerMain.cc 服务端入口文件的改造)
[运行结果 :](#运行结果 :)
我们接着上一篇文章我们写的 UDP 服务端与客户端的通信,之前我们所有的网络操作、调试、通信逻辑,全部都在 Linux 环境下完成。不管是本地回环、公网访问,还是不同 Linux 机器之间的互通。
**那 Windows 能不能和 Linux 互相通信?**Windows 电脑上的程序,能不能通过网络,直接给 Linux 机器发数据?
答案是:完全可以!而且只要遵循标准的 Socket 接口(不管是 Windows 还是 Linux),两者可以实现完全互通。
下面我们就来验证一下 :
一、Windows与Linux通信
下面我们搭建一个跨操作系统的真实通信环境。其中,客户端运行在 Windows 上,而服务端继续运行在 Linux 上。
Windows 客户端环境(VS2022)
我们在 Windows 平台下开发并运行客户端程序,用到的开发工具是 Visual Studio 2022,VS2022 是 Windows 平台下最主流、最强大的 IDE 之一,用于开发 C/C++等应用。它内置 Windows 特有的 Winsock 网络库,提供标准的 BSD Socket 接口封装。
这里我们直接把 Windows 下的代码复制粘贴过来使用 :

客户端程序:使用 C/C++ 编写,调用 socket()、sendto()、recvfrom() 等标准 Socket 接口。向 Linux 服务端的 公网 IP "124.222.191.171"+ 8080 端口 发送 UDP 数据
虽然 Windows 和 Linux 的底层网络子系统不同,但 Socket 接口层面实现了跨平台标准化。因此,我们写的客户端代码几乎不需要修改,就能在 Windows 上编译、运行、直接发数据给 Linux。
Linux 服务端环境(Ubuntu + Xshell)
服务端继续保持我们熟悉的 Linux 环境 : 通过 Xshell 远程连接 Linux 云服务器
Linux 服务端使用标准的 POSIX Socket,通过 bind、recvfrom、sendto 等接口接受客户端请求,不区分客户端操作系统,只根据 IP + 端口 匹配数据。

运行结果 :
我们先运行 Windows 下的 VS2022 的客户端,运行完后向 Linux 中的服务端输入"hello Linux" :
按下回车后,Linux 下的服务端收到了 Windows 下的客户端发来的消息 :
此时我们就完成了 Windows 与 Linux 之间的通信。
二、新增 Dict 字典翻译模块的UDP 通信
上一篇文章中我们实现了 UDP 服务端和客户端的通信,但是这种通信也仅完成了基础的网络 IO 回声交互:服务端只负责接收客户端消息、原样拼接前缀后回发,核心逻辑集中在网络收发本身,没有额外业务处理能力。
而在真实开发场景中,网络服务的核心价值是承载上层业务逻辑,网络 IO 仅作为数据传输通道。基于此,我们对原有 UDP 服务端架构进行分层升级:在原有的服务端网络 IO 模块上层,新增 Dict 字典翻译业务模块,形成清晰的两层架构:
底层:服务端模块,专注处理 UDP 网络 IO,负责接收客户端网络数据、向上层交付原始消息、接收上层处理结果并通过网络回发给客户端;
上层:Dict 翻译模块,专注实现业务逻辑,接收底层传递的原始消息,完成字典翻译计算后,将翻译结果回传给网络层。
升级后的数据流转链路严格遵循分层调用逻辑:客户端发送的消息,先由服务端完成网络层面的接收,交付给上层 Dict 模块进行翻译处理;翻译完成后,处理结果原路返回网络 IO 层,再由服务端通过 UDP 协议回发给客户端。
Dict 类
之前的 UDP 回声服务中,服务端收到数据后,直接在 Start() 函数里拼接字符串回发,业务和网络收发逻辑是耦合在一起的。如果想把回声改成其他功能,比如计算器、翻译器,就必须修改服务端的代码,导致扩展性很差。
所以为了解耦,我们新增了一个 Dict.hpp 文件,里面封装了一个的 Dict 类,专门负责字典翻译的业务逻辑,让网络层和业务层彻底分开。

Dict 类整体结构

LoadDict():从文件加载字典数据

Translate():实现核心翻译逻辑
Translate() 函数的返回值和参数类型都是 std::string,刚好和我们后面要定义的 callback_t 回调类型完全匹配。这意味着,我们可以直接把 Dict::Translate 作为回调函数,注入到 UdpServer 中,让网络层调用它来处理数据。
EchoServer.hpp 文件中 UdpServer 类的改造
EchoServer.hpp 文件是改动最大的文件,从纯回声服务改成了支持回调注入的通用 UDP 服务框架。
首先我们新增了回调类型定义,定义了一个函数签名:接收 std::string,返回 std::string。
同时新增了成员变量 callback_t _cb,用来保存注入的业务回调(也就是 Dict::Translate),让 Start() 循环里能随时调用。
同时也要修改构造函数,支持注入回调,新增了 callback_t cb 参数,把上层业务逻辑 "注入" 到网络层,_cb 作为成员变量保存,后面 Start() 中调用。

Start() 方法替换回声逻辑为回调调用,原来收到数据后,直接拼接字符串回发,现在收到数据后,先调用 _cb 处理数据,拿到处理结果再回发,这样网络层就和具体业务无关了,只负责收发和调用回调,从而实现解耦。
EchoServerMain.cc 服务端入口文件的改造
EchoServerMain.cc 文件绑定了网络层和业务层,是把 Dict 和 UdpServer 连起来的胶水代码。

首先定义了一个 dict 对象,用 Lambda 表达式捕获 dict 对象,把 dict.Translate() 适配成 callback_t 类型,这样 UdpServer 启动后,每次收到数据都会调用通过 _cb 来回调这个 Lambda,从而间接调用 dict.Translate() 是西安翻译功能。
EchoClient.cc 客户端文件不做任何改动,因为客户端只负责发字符串、收字符串,它本不知道服务端现在是回声还是翻译,网络协议(UDP)、数据格式(字符串)没变,所以客户端不用改。
回调的体现
先定义回调函数类型,定义一个叫 callback_t 的 "函数类型",要求这个函数必须:接收 1 个 std::string 参数,返回 1 个 std::string 结果。
这里的 _cb(cb) 就是关键:我们创建 UdpServer 时,会把一个符合 callback_t 类型的函数传进来
构造函数把这个函数保存到成员变量 _cb 里,从此,UdpServer 里就存了一个 "未来要调用的业务函数",但现在还没执行。
服务端收到数据时:调用回调函数处理数据,网络层 UdpServer 收到数据后,不自己处理,而是调用 _cb,把收到的 inbuffer 传给 _cb,拿到返回的 result,再把 result 回发给客户端,这一步里,UdpServer 根本不知道 _cb 里面是什么逻辑,它只知道:"我收到数据,按标准格式调用 _cb,拿到结果发回去就行。"

这里是把 Dict 翻译逻辑,包装成符合 callback_t 标准的函数,传给 UdpServer:用一个 Lambda 表达式捕获 dict 对象,Lambda 的签名是 std::string(std::string),刚好匹配 callback_t,里面实际执行的是 dict.Translate(word),把翻译结果返回,当 UdpServer 调用 _cb(inbuffer) 时,实际执行的就是这个 Lambda,再由 Lambda 去调用 dict.Translate。
这就是回调函数的 "本体":接收 Lambda 传过来的 word,在 _dict 里查找单词,返回中文释义或 "None"
数据流转链路
客户端发送 apple → 网络 → 服务端→ 服务端 UdpServer::recvfrom 收到数据 → 调用注入的回调 dict.Translate("apple") → Translate 方法返回结果 → UdpServer::sendto 把结果回发给客户端 → 客户端收到并打印
运行结果 :
因为执行服务端是定义了 dict 对象,所以会先执行 dict 的构造函数,构造函数里又有加载函数,加载函数会先通过日志打印出字典文件中的内容,加载完毕后继续向后执行,执行 Init()和 Start() 函数。
成功翻译!
代码:
Dict.hpp:
cpp
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "Logger.hpp"
static const std::string default_dict = "./dict.txt";
static const std::string sep = ": ";
using namespace NS_LOG_MODULE;
class Dict
{
public:
Dict(const std::string &dict_path = default_dict) : _dict_path(dict_path)
{
LoadDict();
}
~Dict()
{
}
void LoadDict()
{
std::ifstream in(_dict_path);
if (!in.is_open())
{
LOG(LogLevel::FATAL) << " open " << _dict_path << " error";
exit(1);
}
std::string line;
while (std::getline(in, line))
{
LOG(LogLevel::FATAL) << "load: " << line << " success";
// apple: 苹果
auto pos = line.find(sep);
if (pos == std::string::npos)
{
LOG(LogLevel::WARNING) << "Format: " << line << " error";
continue;
}
std::string k = line.substr(0, pos); // [)
std::string v = line.substr(pos + sep.size());
_dict.insert(std::make_pair(k, v));
}
in.close();
LOG(LogLevel::INFO) << "load done....";
}
std::string Translate(std::string word)
{
auto iter = _dict.find(word);
if(iter != _dict.end())
{
return iter->second;
}
else
{
return "None";
}
}
private:
std::string _dict_path;
std::unordered_map<std::string, std::string> _dict;
};
class Dict
{
public:
Dict(const std::string &dict_path = default_dict)
: _dict_path(dict_path)
{
LoadDict();
}
void LoadDict()
{
}
std::string Translate(std::string word)
{
}
private:
std::string _dict_path;
std::unordered_map<std::string, std::string> _dict;
};
EchoClient.cc 客户端:
cpp
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
static void Usage(const std::string &proc)
{
std::cout << "Usage:\n\t";
std::cout << proc << " server_ip server_port" << std::endl;
}
// 我怎么知道server对方的IP和端口啊, 类似IP+Port 是被内置到client的!!!
// ./client_udp server_ip server_port
int main(int argc, char *argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(1);
}
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
// 1. 创建socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
std::cerr << "socket error" << std::endl;
exit(2);
}
// 2. 构建server端socket信息
// client 需要有自己的IP和Port信息吗? 需要的!
// 需要显示的bind自己的ip和端口吗?不要显示bind!!!
// 1. 为什么不让client显示bind?client bind port 出现冲突!client port只需要具有唯一性即可,具体是几,不重要。
// 2. 如何设置自己的IP和端口呢?client 一般会采用随机端口的方式!由OS自主选择!
// udp client 首次发送数据的时候,OS底层会隐式自动帮你进行获取随机端口,然后bind + Port + IP
struct sockaddr_in server;
memset(&server, 0, sizeof(0));
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
while(true)
{
std::string message;
// 1. 获取用户输入
std::cout << "Please Enter# ";
std::getline(std::cin, message);
// 2. clinet 发送数据给 server,首次发送即自动bind
ssize_t n = sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
if(n > 0)
{
// recvfrom
char inbuffer[1024] = {0};
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t m = recvfrom(sockfd, inbuffer, sizeof(inbuffer)-1, 0, (struct sockaddr*)&temp, &len);
if(m > 0)
{
inbuffer[m] = 0;
std::cout << inbuffer << std::endl;
}
}
}
return 0;
}
EchoServer.hpp 服务端:
cpp
#ifndef __ECHOSERVER_HPP
#define __ECHOSERVER_HPP
#include <iostream>
#include <string>
#include <cstdlib>
#include <strings.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <functional>
#include <arpa/inet.h>
#include "Logger.hpp"
using namespace NS_LOG_MODULE;
const static int default_fd = -1;
const static int default_port = 8888;
using callback_t = std::function<std::string(std::string)>;
enum
{
SUCCESS = 0,
USAGE_ERR,
SOCKET_ERR,
BIND_ERR,
};
class UdpServer
{
public:
// UdpServer(const std::string &ip, uint16_t port = default_port)
UdpServer(callback_t cb, uint16_t port = default_port)
: _port(port),
_sockfd(default_fd),
_cb(cb)
{
}
~UdpServer()
{
close(_sockfd);
}
void Init()
{
// 第一步: 创建socket, 本质: 打开网卡 --- 系统特性
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "create socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "create socket success, sockfd: " << _sockfd;
// 第二步: 填充网络信息, 有没有IP和端口信息设置到内核中??设置到你刚刚打开的网络socket对应的文件内部?
struct sockaddr_in local; // struct sockaddr_in 数据类型,local 用户栈上的!!!,并没有设置到内核
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port); // h->n
// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. 字符串ip->4字节IP 2. hton
local.sin_addr.s_addr = INADDR_ANY; // 最佳实践:任意IP地址bind
// 第三步:bind socket 信息
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind socket error";
exit(BIND_ERR);
}
LOG(LogLevel::INFO) << "bind socket success"<< ", port: " << _port;
}
void Start()
{
// 传递的是字符串,echo server
char inbuffer[1024];
while (true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 1. 用户发来的数据
// 2. 用户的socket信息
ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (n > 0)
{
// ?
// 用户的IP + Port信息,是recvfrom从网络中获取到的数据,网络序列(大端)-> 转换成为主机序列
// peer.sin_port; // peer client port
// peer.sin_addr; // peer client ip
// uint16_t client_port = ntohs(peer.sin_port);
// std::string client_ip = inet_ntoa(peer.sin_addr); // 4字节IP-> ntoh -> 字符串
// std::string client_address = "[" + client_ip + ":" + std::to_string(client_port) + "]# ";
// 用户发来的数据
inbuffer[n] = 0;
// LOG(LogLevel::DEBUG) << client_address << inbuffer;
// std::string echo_string = "server echo# ";
// echo_string += inbuffer;
std::string result = _cb(inbuffer); // 单词
// h to n
sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr *)&peer, len);
}
else
{
LOG(LogLevel::ERROR) << "recvfrom error";
}
}
}
private:
int _sockfd;
// std::string _ip; // "192.168.2.2"(字符串风格的点分十进制IP地址, 让人看的) && 4字节IP ???
uint16_t _port; // 用户设置好的,server port必须是固定的!
callback_t _cb; // 用回调的方式进行数据加工
};
#endif
EchoServerMain.cc 服务端:
cpp
#include "Dict.hpp"
#include "EchoServer.hpp"
#include <memory>
static void Usage(const std::string &process)
{
std::cerr << "Usage:\n\t";
std::cerr << process << " local_port" << std::endl;
}
// ./server_udp port
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
ENABLE_CONSOLE_LOG_STRATEGY();
// 1. 定义字典
Dict dict;
// 2. 构建网络服务,处理IO问题
uint16_t server_port = std::stoi(argv[1]);
// 3. 绑定上下两层
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>([&dict](std::string word)->std::string{
return dict.Translate(word);
},
server_port);
usvr->Init();
usvr->Start();
return 0;
}
Logger.hpp:
cpp
#ifndef __LOGGER_HPP
#define __LOGGER_HPP
#include <iostream>
#include <cstdio>
#include <string>
#include <memory>
#include <sstream>
#include <ctime>
#include <sys/time.h>
#include <unistd.h>
#include <filesystem> // C++17
#include <fstream>
#include "Mutex.hpp"
namespace NS_LOG_MODULE
{
enum class LogLevel
{
INFO,
WARNING,
ERROR,
FATAL,
DEBUG
};
std::string LogLevel2Message(LogLevel level)
{
switch (level)
{
case LogLevel::INFO:
return "INFO";
case LogLevel::WARNING:
return "WARNING";
case LogLevel::ERROR:
return "ERROR";
case LogLevel::FATAL:
return "FATAL";
case LogLevel::DEBUG:
return "DEBUG";
default:
return "UNKNOWN";
}
}
// 1. 时间戳 2. 日期+时间
std::string GetCurrentTime()
{
struct timeval current_time;
int n = gettimeofday(¤t_time, nullptr);
(void)n;
// current_time.tv_sec; current_time.tv_usec;
struct tm struct_time;
localtime_r(&(current_time.tv_sec), &struct_time); // r: 可重入函数
char timestr[128];
snprintf(timestr, sizeof(timestr), "%04d-%02d-%02d %02d:%02d:%02d.%ld",
struct_time.tm_year + 1900,
struct_time.tm_mon + 1,
struct_time.tm_mday,
struct_time.tm_hour,
struct_time.tm_min,
struct_time.tm_sec,
current_time.tv_usec);
return timestr;
}
// 输出角度 -- 刷新策略
// 1. 显示器打印
// 2. 文件写入
// 策略模式,策略接口
class LogStrategy
{
public:
virtual ~LogStrategy() = default;
virtual void SyncLog(const std::string &message) = 0;
};
// 控制台日志刷新策略, 日志将来要向显示器打印
class ConsoleStrategy : public LogStrategy
{
public:
void SyncLog(const std::string &message) override
{
LockGuard lockguard(_mutex);
std::cerr << message << std::endl; // ??
}
~ConsoleStrategy()
{
}
private:
Mutex _mutex;
};
const std::string defaultpath = "./log";
const std::string defaultfilename = "log.txt";
// 文件策略
class FileLogStrategy : public LogStrategy
{
public:
FileLogStrategy(const std::string &path = defaultpath, const std::string &name = defaultfilename)
: _logpath(path),
_logfilename(name)
{
LockGuard lockguard(_mutex);
if (std::filesystem::exists(_logpath))
return;
try
{
std::filesystem::create_directories(_logpath);
}
catch (const std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << '\n';
}
}
void SyncLog(const std::string &message) override
{
{
LockGuard lockguard(_mutex);
if (!_logpath.empty() && _logpath.back() != '/')
{
_logpath += "/";
}
std::string targetlog = _logpath + _logfilename; // "./log/log.txt"
std::ofstream out(targetlog, std::ios::app); // 追加方式写入
if (!out.is_open())
{
std::cerr << "open " << targetlog << "failed" << std::endl;
return;
}
out << message << "\n";
out.close();
}
}
~FileLogStrategy()
{
}
private:
std::string _logpath;
std::string _logfilename;
Mutex _mutex;
};
// 日志类:
// 1. 日志的生成
// 2. 根据不同的策略,进行刷新
class Logger
{
// 日志的生成:
// 构建日志字符串
public:
Logger()
{
UseConsoleStrategy();
}
void UseConsoleStrategy()
{
_strategy = std::make_unique<ConsoleStrategy>();
}
void UseFileStrategy()
{
_strategy = std::make_unique<FileLogStrategy>();
}
// 内部类, 标识一条完整的日志信息
// 一条完整的日志信息 = 做半部分固定部分 + 右半部分不固定部分
// LogMessage RAII风格的方式,进行刷新
class LogMessage
{
public:
LogMessage(LogLevel level, std::string &filename, int line, Logger &logger)
: _level(level),
_curr_time(GetCurrentTime()),
_pid(getpid()),
_filename(filename),
_line(line),
_logger(logger)
{
// 先构建出来左半部分
std::stringstream ss;
ss << "[" << _curr_time << "] "
<< "[" << LogLevel2Message(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _filename << "] "
<< "[" << _line << "] "
<< " - ";
_loginfo = ss.str();
}
template <typename T>
LogMessage &operator<<(const T &info)
{
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this; // 返回当前LogMessage对象,方便下次继续进行<<
}
~LogMessage()
{
if (_logger._strategy)
{
_logger._strategy->SyncLog(_loginfo);
}
}
private:
LogLevel _level;
std::string _curr_time;
pid_t _pid;
std::string _filename;
int _line;
std::string _loginfo; // 一条完整的日志信息
// 一个引用,引用外部的Logger类对象
Logger &_logger; // 方便我们后续进行策略式刷新
};
// 这里已经不是内部类了
// 故意采用拷贝LogMessage
LogMessage operator()(LogLevel level, std::string filename, int line)
{
return LogMessage(level, filename, line, *this);
}
~Logger()
{
}
private:
std::unique_ptr<LogStrategy> _strategy; // 刷新策略
};
// 日志对象,全局使用
Logger logger;
#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleStrategy();
#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileStrategy();
#define LOG(level) logger(level, __FILE__, __LINE__)
}
#endif
Makefile:
cpp
.PHONY:all
all:client_udp server_udp
server_udp:EchoServerMain.cc
g++ -o $@ $^ -std=c++17
client_udp:EchoClient.cc
g++ -o $@ $^ -std=c++17 -static
.PHONY:clean
clean:
rm -f client_udp server_udp
Mutex.hpp
cpp
#pragma once
#include <iostream>
#include <pthread.h>
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&_lock, nullptr);
}
void Lock()
{
pthread_mutex_lock(&_lock);
}
pthread_mutex_t *Ptr()
{
return &_lock;
}
void Unlock()
{
pthread_mutex_unlock(&_lock);
}
~Mutex()
{
pthread_mutex_destroy(&_lock);
}
private:
pthread_mutex_t _lock;
};
class LockGuard // RAII风格代码
{
public:
LockGuard(Mutex &lock):_lockref(lock)
{
_lockref.Lock();
}
~LockGuard()
{
_lockref.Unlock();
}
private:
Mutex &_lockref;
};
dict.txt:
cpp
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天
fall: 秋天
Liz: 栗子
三、总结
本文验证了Windows与Linux系统间基于UDP协议的网络通信可行性,并实现了字典翻译功能的服务端分层架构改造。主要内容包括:
跨平台通信验证:Windows客户端通过VS2022开发,调用标准Socket接口与Linux服务端成功实现UDP通信,证明不同操作系统间网络互通的可行性。
服务端架构升级:
- 将原有回声服务改造为两层架构:底层网络IO模块负责数据传输,上层Dict模块专注业务逻辑
- 通过回调机制实现解耦,网络层调用注入的翻译函数处理数据
- 新增Dict类实现字典加载和单词翻译功能
- 功能实现:
- 客户端发送英文单词,服务端返回中文翻译
- 采用日志模块记录运行状态
- 通过Lambda表达式实现回调适配
代码结构清晰展示了网络层与业务层的分离,为后续功能扩展提供了良好基础。
谢谢大家的观看!



因为执行服务端是定义了 dict 对象,所以会先执行 dict 的构造函数,构造函数里又有加载函数,加载函数会先通过日志打印出字典文件中的内容,加载完毕后继续向后执行,执行 Init()和 Start() 函数。
