前言
上一篇简单实现了 UDP Echo 服务器和客户端,主要熟悉了 socket、bind、sendto 和 recvfrom 等接口。
不过,单纯实现数据的发送和接收还不够。实际开发中,服务器通常还需要根据客户端发送的内容执行不同的操作。
这篇继续在 UDP 的基础上做一些扩展,主要记录三个内容:
- 实现一个简单的英译汉字典服务器。
- 对 UDP 通信接口进行封装。
- 利用 UDP 实现一个简单聊天室。
一、实现一个简单的网络字典
先来做一个比较简单的功能:客户端输入一个英文单词,服务器查询对应的中文意思,然后返回给客户端。
例如:
text
hello: 你好
apple: 苹果
book: 书
teacher: 老师
student: 学生
这里可以把单词和对应的中文意思保存到一个文件中,服务器启动时读取文件,再将数据存入哈希表。
1. 使用unordered_map存储单词
这里使用 C++ 中的 unordered_map:
cpp
unordered_map<string, string> _dict;
其中:
- key 保存英文单词。
- value 保存对应的中文意思。
读取文件时,可以使用 getline 按行读取。
每行通过 ": " 分隔,分别取出单词和中文意思。
核心代码:
cpp
const std::string sep = ": ";
void LoadDict()
{
std::ifstream in(_confpath);
if(!in.is_open())
{
std::cerr << "open file error" << std::endl;
return;
}
std::string line;
while(std::getline(in, line))
{
if(line.empty())
continue;
auto pos = line.find(sep);
if(pos == std::string::npos)
continue;
std::string key = line.substr(0, pos);
std::string value = line.substr(pos + sep.size());
_dict.insert(std::make_pair(key, value));
}
in.close();
}
这里主要用到了几个字符串操作:
find():查找分隔符的位置。substr():截取字符串。insert():将键值对插入哈希表。
例如:
text
apple: 苹果
通过 find 找到分隔符的位置,就可以把前面的 apple 和后面的 苹果 分别取出来。
2. 实现查询功能
字典加载完成之后,查询就比较简单了。
cpp
std::string Translate(const std::string& key)
{
auto iter = _dict.find(key);
if(iter == _dict.end())
return "Unknown";
return iter->second;
}
如果找到了对应单词,就返回它的中文意思;如果没有找到,就返回 Unknown。
这样字典的基本功能就实现了。
二、通过回调函数分离通信和业务逻辑
现在虽然已经有了字典查询功能,但是还有一个问题:
如果以后服务器不只是查字典,还需要实现其他功能,是不是每增加一种功能,都要修改 UDP 服务器内部的代码?
显然这样不太方便。
所以这里引入了回调函数。
1. 使用std::function定义回调类型
先定义一个函数类型:
cpp
using func_t = std::function<void(const std::string& req,
std::string* resp)>;
这里的回调函数接收两个参数:
req:客户端发送过来的请求。resp:服务器处理后返回的响应。
然后在 UDP 服务器中保存这个回调函数:
cpp
class UdpServer
{
public:
UdpServer(func_t func, uint16_t port)
: _func(func), _port(port)
{}
private:
func_t _func;
};
服务器接收到客户端的数据之后,不再直接处理具体业务,而是调用:
cpp
std::string value;
_func(buffer, &value);
最后再通过 sendto 将处理结果返回给客户端:
cpp
sendto(_sockfd,
value.c_str(),
value.size(),
0,
(struct sockaddr*)&peer,
len);
这样,UDP 服务器只负责接收数据和发送数据,具体要做什么交给外部传入的回调函数处理。
2. 为什么使用std::function?
这里使用 std::function 而不是直接使用普通函数指针,是因为它可以兼容普通函数、仿函数和 Lambda 表达式。
例如,我们可以直接传入一个普通函数:
cpp
void Execute(const std::string& req, std::string* resp)
{
*resp = gdict.Translate(req);
}
创建服务器时:
cpp
UdpServer server(Execute, 8888);
也可以传入 Lambda 表达式。
这样就不需要把具体的业务逻辑写死在 UDP 服务器里面,后续想增加其他业务,只需要提供不同的回调函数即可。
三、DictServer代码
cpp
// Dict.hpp
#pragma once
#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;
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))
{
// "apple: 苹果"
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());
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;
};
cpp
// InetAddr.hpp
#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
// 网络地址和主机地址之间进行转换的类
class InetAddr
{
public:
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;
};
cpp
// UdpClient.cc
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// ./udpclient server_ip server_port
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];
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;
return 2;
}
// 2. client端不用显式地bind, 首次发送消息, OS会自动给client进行bind, OS知道IP, 端口号采用随机端口号的方式
// client端的端口号是几不重要, 只要唯一就行
// 填写服务器信息
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());
while (true)
{
std::string input;
std::cout << "Please Enter# ";
std::getline(std::cin, input);
int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&server, sizeof(server));
(void)n;
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
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;
}
cpp
// UdpServer.cc
#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;
}
// ./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;
}
cpp
// UdpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <strings.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;
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 sucess, sockfd: " << _sockfd;
// 2. 绑定socket信息, ip和端口
// 2.1 绑定sockaddr_in结构体
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
// IP信息和端口号信息要发送到网络
// 本地格式->网络序列
local.sin_port = htons(_port);
// IP, 1. IP转成4字节 2. 4字节转成网络序列 -> in_addr_t inet_addr(const char *cp);
// local.sin_addr.s_addr = inet_addr(_ip.c_str());
local.sin_addr.s_addr = INADDR_ANY;
// 服务器端为什么要显式地bind呢? 服务器端的IP和端口必须是众所周知且不能轻易改变的
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;
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 1. 收消息
ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (s > 0)
{
InetAddr client(peer);
buffer[s] = 0;
std::string result = _func(buffer, client);
// LOG(LogLevel::DEBUG) << "[" << peer_ip << ":" << peer_port << "]# " << buffer;
// 2. 发消息
// std::string echo_string = "server echo@ ";
// echo_string += buffer;
sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr *)&peer, len);
}
}
}
~UdpServer()
{
}
private:
int _sockfd;
uint16_t _port;
// std::string _ip; // 用的是字符串风格, 点分十进制
bool _isrunning;
func_t _func; // 服务器的回调函数, 用来对数据进行处理
};
四、利用UDP实现一个简单聊天室
前面实现的字典服务器,基本上都是一个客户端发送请求,服务器处理之后再返回结果。
现在换一个需求:
多个客户端连接到同一个服务器,每个人发送的消息都可以转发给其他在线用户。
这就是一个简单的聊天室。
1. 如何记录在线用户?
UDP 通信中,我们可以通过客户端的 IP 地址和端口号来区分不同的用户。
所以可以定义一个 InetAddr 类,用来封装客户端的网络地址信息。
其中主要保存:
cpp
std::string _ip;
uint16_t _port;
struct sockaddr_in _addr;
还可以提供一些接口:
cpp
Ip();
Port();
PrintDebug();
GetAddr();
另外,可以重载 == 运算符:
cpp
bool operator == (const InetAddr& addr)
{
return this->_ip == addr._ip &&
this->_port == addr._port;
}
这样就可以通过 IP 和端口判断两个地址是否属于同一个客户端。
接下来使用 vector 保存在线用户:
cpp
std::vector<InetAddr> _online_user;
2. 实现用户管理
在聊天室中,需要处理三个基本操作:
- 判断用户是否已经在线。
- 添加新用户。
- 删除退出的用户。
判断用户是否存在:
cpp
bool IsExist(InetAddr& peer)
{
for(auto& user : _online_user)
{
if(user == peer)
return true;
}
return false;
}
添加用户:
cpp
void AddUser(InetAddr& peer)
{
_online_user.push_back(peer);
}
删除用户:
cpp
void DeleteUser(InetAddr& peer)
{
for(auto iter = _online_user.begin();
iter != _online_user.end();
iter++)
{
if(*iter == peer)
{
_online_user.erase(iter);
break;
}
}
}
这里采用的方式是:客户端第一次发送消息,就认为这个用户已经上线。
也就是说,不需要额外设计一个复杂的登录流程。
3. 消息路由和广播
接下来是聊天室最重要的部分:消息转发。
当服务器收到某个客户端的消息时,首先判断这个客户端是否已经存在于在线用户列表中。
如果不存在,就把它添加进去。
然后给消息加上发送者的地址信息:
cpp
std::string send_message =
peer.StringAddr() + "# " + message;
例如:
text
127.0.0.1:8080# 你好
这样其他用户就可以知道是谁发送的消息。
接下来遍历在线用户列表,把消息发送给每一个用户:
cpp
for(auto& user : _online_user)
{
sendto(sockfd,
send_message.c_str(),
send_message.size(),
0,
(const struct sockaddr*)&user.NetAddr(),
sizeof(user.NetAddr()));
}
这样就实现了简单的消息广播。
4. 用户退出
如果客户端发送的消息是:
text
QUIT
服务器就将这个用户从在线列表中删除:
cpp
if(message == "QUIT")
{
DeleteUser(peer);
}
这样聊天室就具备了最基本的用户上线、消息广播和退出功能。
五、客户端使用两个线程实现收发分离
聊天室还有一个问题。
如果客户端仍然采用之前的方式,先发送消息,再等待服务器返回,那么客户端在等待接收消息的时候,就无法方便地继续输入其他内容。
所以这里可以使用两个线程:
- 一个线程专门负责发送消息。
- 一个线程专门负责接收消息。
发送线程:
cpp
void SenderRoutine(ThreadData& td)
{
while(true)
{
std::string inbuffer;
std::cout << "Please Enter# ";
std::getline(std::cin, inbuffer);
auto server = td._serveraddr.GetAddr();
sendto(td._sockfd,
inbuffer.c_str(),
inbuffer.size(),
0,
(struct sockaddr*)&server,
sizeof(server));
}
}
接收线程:
cpp
void RecverRoutine(ThreadData& td)
{
char buffer[4096];
while(true)
{
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t n = recvfrom(td._sockfd,
buffer,
sizeof(buffer) - 1,
0,
(struct sockaddr*)&temp,
&len);
if(n > 0)
{
buffer[n] = '\0';
std::cerr << buffer << std::endl;
}
}
}
在 main 函数中创建两个线程:
cpp
Thread<ThreadData> recver("recver", RecverRoutine, td);
Thread<ThreadData> sender("sender", SenderRoutine, td);
recver.Start();
sender.Start();
recver.Join();
sender.Join();
这里两个线程使用的是同一个 socket 文件描述符。
UDP 套接字既可以发送数据,也可以接收数据,因此可以让一个线程负责发送,另一个线程负责接收。
这样客户端就可以一边接收其他用户的消息,一边继续发送自己的消息。
六、服务器引入线程池
前面聊天室的消息处理主要由服务器直接完成。
如果后续客户端数量增加,可以进一步引入线程池处理业务任务。
整体思路是:
- UDP 服务器负责接收客户端消息。
- 收到消息之后,构造一个任务。
- 将任务放入线程池。
- 线程池中的工作线程取出任务并执行。
- 通过路由对象处理消息。
例如:
cpp
auto tp = ThreadPool<task_t>::GetInstance();
std::unique_ptr<Route> r =
std::make_unique<Route>();
std::unique_ptr<UdpServer> usvr =
std::make_unique<UdpServer>(
port,
[&r, &tp](int sockfd,
const std::string& message,
InetAddr& peer)
{
task_t t = std::bind(
&Route::MessageRoute,
r.get(),
sockfd,
message,
peer
);
tp->Enqueue(t);
}
);
这里使用 Lambda 表达式捕获路由对象和线程池对象,再通过 std::bind 将消息路由函数绑定成一个任务,最后放入线程池。
这样就把网络通信和具体的消息处理进一步分开了。
七、简单聊天室代码
cpp
// UdpClient.cc
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Thread.hpp"
int sockfd = 0;
std::string server_ip;
uint16_t server_port = 0;
pthread_t id;
using namespace ThreadModule;
void Recv()
{
while (true)
{
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (m > 0)
{
buffer[m] = 0;
std::cerr << buffer << std::endl;
}
}
}
void Send()
{
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());
const std::string online = "online";
sendto(sockfd, online.c_str(), online.size(), 0, (struct sockaddr *)&server, sizeof(server));
while (true)
{
std::string input;
std::cout << "Please Enter# ";
std::getline(std::cin, input);
int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&server, sizeof(server));
(void)n;
if (input == "QUIT")
{
pthread_cancel(id);
break;
}
}
}
// ./udpclient server_ip server_port
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
return 1;
}
server_ip = argv[1];
server_port = std::stoi(argv[2]);
// 1. 创建socket
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cerr << "socket error" << std::endl;
return 2;
}
// 2. 创建线程
Thread recver(Recv);
Thread sender(Send);
recver.Start();
sender.Start();
id = recver.Id();
recver.Join();
sender.Join();
return 0;
}
cpp
// UdpServer.cc
#include <iostream>
#include <memory>
#include "Route.hpp"
#include "UdpServer.hpp"
#include "ThreadPool.hpp"
using namespace ThreadPoolModule;
std::string defaulthandler(const std::string &message)
{
std::string hello = "hello, ";
hello += message;
return hello;
}
using task_t = std::function<void()>;
// ./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. 路由服务
Route r;
// 2. 线程池
auto tp = ThreadPool<task_t>::GetInstance();
// 3. 网络服务器对象提供通信功能
// std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, [&r](int sockfd, const std::string &message, InetAddr &peer)
// { r.MessageRoute(sockfd, message, peer); });
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, [&r, &tp](int sockfd, const std::string& message, InetAddr& peer){
task_t t = std::bind(&Route::MessageRoute, &r, sockfd, message, peer);
tp->Enqueue(t);
});
usvr->Init();
usvr->Start();
return 0;
}
cpp
// UdpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <strings.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<void(int sockfd, const std::string &, InetAddr &)>;
const int defaultfd = -1;
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 sucess, sockfd: " << _sockfd;
// 2. 绑定socket信息, ip和端口
// 2.1 绑定sockaddr_in结构体
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
// IP信息和端口号信息要发送到网络
// 本地格式->网络序列
local.sin_port = htons(_port);
// IP, 1. IP转成4字节 2. 4字节转成网络序列 -> in_addr_t inet_addr(const char *cp);
// local.sin_addr.s_addr = inet_addr(_ip.c_str());
local.sin_addr.s_addr = INADDR_ANY;
// 服务器端为什么要显式地bind呢? 服务器端的IP和端口必须是众所周知且不能轻易改变的
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;
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 1. 收消息
ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (s > 0)
{
InetAddr client(peer);
buffer[s] = 0;
_func(_sockfd, buffer, client);
// LOG(LogLevel::DEBUG) << "[" << peer_ip << ":" << peer_port << "]# " << buffer;
}
}
}
~UdpServer()
{
}
private:
int _sockfd;
uint16_t _port;
// std::string _ip; // 用的是字符串风格, 点分十进制
bool _isrunning;
func_t _func; // 服务器的回调函数, 用来对数据进行处理
};
cpp
// Route.hpp
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include "InetAddr.hpp"
#include "Log.hpp"
#include "Mutex.hpp"
using namespace LogModule;
using namespace MutexModule;
class Route
{
private:
bool IsExist(InetAddr &peer)
{
for (auto &user : _online_user)
{
if (user == peer)
return true;
}
return false;
}
void AddUser(InetAddr &peer)
{
LOG(LogLevel::INFO) << "新增一个在线用户: " << peer.StringAddr();
_online_user.push_back(peer);
}
void DeleteUser(InetAddr &peer)
{
for (auto iter = _online_user.begin(); iter != _online_user.end(); iter++)
{
if (*iter == peer)
{
LOG(LogLevel::INFO) << "删除一个在线用户: " << peer.StringAddr() << "成功";
_online_user.erase(iter);
break;
}
}
}
public:
Route()
{
}
void MessageRoute(int sockfd, const std::string &message, InetAddr &peer)
{
LockGuard lockguard(_mutex);
if (!IsExist(peer))
{
AddUser(peer);
}
std::string send_message = peer.StringAddr() + "# " + message;
for (auto &user : _online_user)
{
sendto(sockfd, send_message.c_str(), send_message.size(), 0, (const struct sockaddr *)&user.NetAddr(), sizeof(user.NetAddr()));
}
if (message == "QUIT")
{
LOG(LogLevel::INFO) << "删除一个在线用户: " << peer.StringAddr();
DeleteUser(peer);
}
}
~Route()
{
}
private:
// 首次给我发消息, 等同于登录
std::vector<InetAddr> _online_user; // 在线用户
Mutex _mutex;
};
八、总结
这一部分是在之前 UDP 基础通信的基础上继续扩展。
从最开始的 Echo 服务器,到后面的网络字典,再到聊天室,可以看到 UDP 网络编程逐渐从简单的数据收发,发展到了具体的业务处理。
这里有几个比较重要的知识点:
- 使用
unordered_map保存字典数据,通过文件读取完成初始化。 - 使用
std::function封装回调函数,让通信逻辑和业务逻辑分离。 - 使用
vector保存在线用户,通过 IP 和端口识别客户端。 - 使用消息路由实现聊天室的广播功能。
- 使用两个线程分别处理客户端的发送和接收。
- 使用线程池处理服务器端的业务任务。
我觉得这一部分最值得学习的地方是代码结构上的变化。
一开始所有操作都写在服务器里面,后面逐渐将通信、业务处理和消息路由拆分开来。这样不仅代码更容易维护,后续想增加新的功能也会方便很多。