文章目录
- 前言
- [一、V1版本 - Echo_server](#一、V1版本 - Echo_server)
-
- [1. 写代码前的预备知识](#1. 写代码前的预备知识)
- [2. V1版本 - Echo server 的最初版本(服务端 绑定指定的 ip)](#2. V1版本 - Echo server 的最初版本(服务端 绑定指定的 ip))
- [3. 查看系统中的udp](#3. 查看系统中的udp)
- [4. 特殊的ip地址:127.0.0.1 和 0.0.0.0](#4. 特殊的ip地址:127.0.0.1 和 0.0.0.0)
- [5. V1版本 - Echo server 的最终版本(服务端 绑定ip:0.0.0.0)](#5. V1版本 - Echo server 的最终版本(服务端 绑定ip:0.0.0.0))
- [6. UDP通信的核心流程 总结](#6. UDP通信的核心流程 总结)
- [二、V2版本 - Dict_server(加入业务处理模块)](#二、V2版本 - Dict_server(加入业务处理模块))
-
- [1. 业务处理模块(业务:简单的英译汉的网络字典)](#1. 业务处理模块(业务:简单的英译汉的网络字典))
- [2. 在服务端接入 业务处理模块](#2. 在服务端接入 业务处理模块)
- [三、V3版本 - Chat_server(加入线程池模块)](#三、V3版本 - Chat_server(加入线程池模块))
-
- [1. 全流程图解(含gitee代码链接)](#1. 全流程图解(含gitee代码链接))
- [2. 业务处理模块(业务:简单的聊天室)](#2. 业务处理模块(业务:简单的聊天室))
- [3. 线程池模块](#3. 线程池模块)
- [4. 在服务端 加入线程池模块(将业务处理模块 作为任务放入线程池的任务队列中)](#4. 在服务端 加入线程池模块(将业务处理模块 作为任务放入线程池的任务队列中))
- [5. 对客户端进行修改(修改后进行代码测试)](#5. 对客户端进行修改(修改后进行代码测试))
前言
使用Socket编程UDP通信的相关函数,要包含以下头文件:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
一、V1版本 - Echo_server
1. 写代码前的预备知识
- socket函数
c
#include <sys/socket.h>
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
返回值:
调用成功,返回一个为socket创建的文件描述符;调用失败,返回 -1,并设置错误码。

- bind函数
c
#include <sys/socket.h>
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
// const struct sockaddr *address: 指向的内容 保存了主机的ip和端口号,将socket文件 与 ip+port进行绑定
// socklen_t address_len: 传入的结构体 的实际大小
返回值:
调用成功,返回0;调用失败,返回 -1,并设置错误码
IPv4 和 IPv6的地址格式定义在 netinet/in.h 中,IPv4地址用sockaddr_in结构体表示
IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6

注: 用户(应用层)使用的 ip地址是以"点分十进制"的字符串格式保存的(例如:192.168.1.1 或 127.0.0.1)
- C语言提供了专门的函数 inet_addr,该函数能将 "点分十进制"的字符串格式的ip地址 转换为 32位网络序列ip
函数原型:uint32_t inet_addr( const char * cp );- 当然 也提供了函数 inet_ntoa,该函数能将 网络序列ip 转换为 "点分十进制"的字符串格式的ip地址
函数原型:char * inet_ntoa( struct in_addr sin );
- recvfrom函数 和 sendto函数
// 注: udp通信是面向数据报的,数据报数据的发送 和 接收需要专门的收发函数,比如:接收数据报使用 recvfrom函数,发送数据报使用 sendto函数

返回值:
成功时,返回 实际接收的字节数。失败时,返回 -1,并设置错误码

返回值:
成功时,返回 实际发送的字节数。失败时,返回 -1,并设置错误码
2. V1版本 - Echo server 的最初版本(服务端 绑定指定的 ip)
- InetAddr.hpp
每个InetAddr类对象 管理 一对ip+port (将网络序列 转 主机序列)
cpp
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// 每个InetAddr类对象 管理 一对ip+port (将网络序列 转 主机序列)
class InetAddr
{
public:
InetAddr(struct sockaddr_in &addr) : _addr(addr)
{
_port = ntohs(_addr.sin_port);
_ip = inet_ntoa(_addr.sin_addr);
// char* inet_ntoa( struct in_addr sin );
// 该函数能将 网络序列ip 转换为 "点分十进制"的字符串格式的ip地址
}
std::string Ip() { return _ip; }
uint16_t Port() { return _port; }
std::string PrintDebug()
{
std::string info = _ip;
info += ":";
info += std::to_string(_port); // 例如:"127.0.0.1:4444"
return info;
}
~InetAddr() {}
private:
std::string _ip;
uint16_t _port;
struct sockaddr_in _addr;
};
- Server_udp.hpp
cpp
#pragma once
#include "Log.hpp"
#include "InetAddr.hpp"
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <iostream>
class Server_udp
{
public:
Server_udp(u_short port, std::string ip)
: _port(port),
_ip(ip)
{
}
void Init()
{
// 1. 创建Udp_Socket文件
_socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (_socket_fd < 0)
{
LOG(FATAL) << "socket err: " << strerror(errno);
exit(1);
}
LOG(INFO) << "socket success, _socket_fd: " << _socket_fd;
// 2. 将socket文件绑定 ip+port
// 服务器端必须绑定一个固定的IP和端口,因为它是被动等待连接的一方
struct sockaddr_in sock_in;
sock_in.sin_family = AF_INET;
sock_in.sin_port = htons(_port);
sock_in.sin_addr.s_addr = inet_addr(_ip.c_str());
int n = bind(_socket_fd, (sockaddr *)&sock_in, sizeof(sock_in));
if (n != 0)
{
LOG(FATAL) << "bind err: " << strerror(errno);
exit(1);
}
LOG(INFO) << "bind success";
}
void Start()
{
char buffer[1024];
// 服务器一旦开始运行,永不退出(除非特殊情况)
while (1)
{
buffer[0] = 0;
// 3. 接收数据
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int n = recvfrom(_socket_fd, buffer, sizeof(buffer) - 1,
0, (sockaddr *)&peer, &len);
if (n > 0)
{
// 成功接收到数据,打印该数据
buffer[n] = 0;
InetAddr addr(peer); // 用InetAddr类对象 管理 客户端的ip+port
std::cout << addr.PrintDebug() << "# " << buffer << std::endl;
// 收到消息后,向消息发送方 回复消息
// 4. 发送数据
std::string str("client# ");
str += buffer;
sendto(_socket_fd, str.c_str(), str.size(), 0, (sockaddr *)&peer, len);
}
}
}
~Server_udp()
{
}
private:
int _socket_fd;
u_short _port;
std::string _ip;
};
- Server_udp.cpp
bash
g++ Server_udp.cpp -o Server_udp
cpp
#include "Server_udp.hpp"
#include <memory>
int main(int argc, char *argv[])
{
ENABLE_CONSOLE_LOG_STRATEGY(); // 开启日志类
if (argc != 3)
{
std::cout << "请输入:" << argv[0] << " ip port" << std::endl;
exit(1);
}
std::string ip(argv[1]);
u_short port = atoi(argv[2]);
std::unique_ptr<Server_udp> baz1 = std::make_unique<Server_udp>(port, ip);
baz1->Init();
baz1->Start();
return 0;
}
- Client_udp.cpp
bash
g++ Client_udp.cpp -o Client_udp
cpp
#include <stdlib.h>
#include <iostream>
#include <string>
#include <memory>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cout << "请输入:" << argv[0] << " ip port" << std::endl;
exit(1);
}
std::string ip(argv[1]);
u_short port = atoi(argv[2]);
// 1. 创建Udp_Socket文件
int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
// 客户端不需要显示绑定自己的 ip+port
// 向服务器发送信息前,设置对应服务器 的ip+port
// 服务器就是用来给客户端访问的(服务器的ip+port是公开的)
struct sockaddr_in tar_server;
tar_server.sin_family = AF_INET;
tar_server.sin_port = htons(port);
tar_server.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(tar_server);
while(1)
{
// 2. 发送数据(第一次发数据时,系统会自动为客户端socket 绑定主机ip 和 合适的port)
std::string str;
std::cin >> str;
sendto(socket_fd, str.c_str(), str.size(), 0, (sockaddr *)&tar_server, len);
char buffer[1024] = {0};
struct sockaddr_in peer;
socklen_t lenth = sizeof(peer);
// 3. 接收数据
recvfrom(socket_fd, buffer, sizeof(buffer), 0, (sockaddr *)&peer, &lenth);
std::cout << buffer << std::endl;
}
return 0;
}
在UDP/TCP通信中:服务器端必须绑定一个固定的IP和端口,因为它是被动等待连接的一方。 客户端是主动发起连接的一方,不需要显式绑定IP和端口,因为操作系统会自动分配一个临时端口。如果客户端也绑定固定端口,可能会导致端口冲突(特别是当多个客户端同时运行时)。
- 在UDP通信中,当客户端第一次调用sendto() 发送数据包时,系统自动为socket绑定ip+空闲端口
- 在TCP通信中,当客户端第一次调用connect() 时,系统自动为socket绑定ip+空闲端口
3. 查看系统中的udp
netstat -uanp :查看系统中的udp信息
- -u:查看udp信息
- -a:展示所有
- -n:把能显示成数字的信息 转换成 数字
- -p:增加进程信息
- 服务端的socket绑定 ip+port

- 客户端在第一次调用sendto()向服务端发送消息时,系统才自动为客户端socket 绑定 主机ip + 空闲的port

4. 特殊的ip地址:127.0.0.1 和 0.0.0.0


- 主机A上的 进程服务端绑定 127.0.0.1的 ip,这个ip被叫做 本地环回ip(只能用于 本地调试,禁止外部访问)
假设客户端要 访问127.0.0.1的ip,客户端发出的数据包 送到网络层时,网络层检测到要访问的 ip是本地环回ip,该数据包就不会被向下发送到数据链路层,而是直接往回发了

- 主机A上的 进程服务端绑定 0.0.0.0的 ip,代表:该服务端的 所有网络接口(公网+内网),对外开放服务
主机A上的 客户端可以通过 127.0.0.1(本地环回ip)、172.31.13.207(私有ip)和 113.44.10.137(弹性公网ip)访问到该服务端。
主机B上的客户端可以通过 113.44.10.137(弹性公网ip)访问到该服务端。

- 如果服务端绑定 172.31.13.207(私有ip),那么 仅在云服务器所属的内部网络 的客户端才能通过 172.31.13.207(私有ip)访问到该服务端(非本地客户端无法直接通过私有IP访问云服务器上的服务端);
- 如果服务端绑定 113.44.10.137(弹性公网ip),那么 客户端只能通过 113.44.10.137(弹性公网ip)才能访问到该服务端。
5. V1版本 - Echo server 的最终版本(服务端 绑定ip:0.0.0.0)
只需要对服务端代码进行修改,服务端不需要用户输入 ip进行绑定,而是绑定固定的ip:0.0.0.0
- Server_udp.hpp
cpp
#pragma once
#include "Log.hpp"
#include "InetAddr.hpp"
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <iostream>
class Server_udp
{
public:
/* 原代码:
Server_udp(u_short port, std::string ip)
: _port(port),
_ip(ip)
{
}
*/
Server_udp(u_short port)
: _port(port)
{
}
void Init()
{
// 1. 创建Udp_Socket文件
_socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (_socket_fd < 0)
{
LOG(FATAL) << "socket err: " << strerror(errno);
exit(1);
}
LOG(INFO) << "socket success, _socket_fd: " << _socket_fd;
// 2. 将socket文件绑定 ip+port
// 服务器端必须绑定一个固定的IP和端口,因为它是被动等待连接的一方
struct sockaddr_in sock_in;
sock_in.sin_family = AF_INET;
sock_in.sin_port = htons(_port);
/* 原代码:
sock_in.sin_addr.s_addr = inet_addr(_ip.c_str());
*/
sock_in.sin_addr.s_addr = INADDR_ANY;
// INADDR_ANY: ((uint32_t) 0x00000000)
// 不需要转换字节序,32位全0的数据 不受大小端存储的影响
int n = bind(_socket_fd, (sockaddr *)&sock_in, sizeof(sock_in));
if (n != 0)
{
LOG(FATAL) << "bind err: " << strerror(errno);
exit(1);
}
LOG(INFO) << "bind success";
}
void Start()
{
char buffer[1024];
// 服务器一旦开始运行,永不退出(除非特殊情况)
while (1)
{
buffer[0] = 0;
// 3. 接收数据
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int n = recvfrom(_socket_fd, buffer, sizeof(buffer) - 1,
0, (sockaddr *)&peer, &len);
if (n > 0)
{
// 成功接收到数据,打印该数据
buffer[n] = 0;
InetAddr addr(peer); // 用InetAddr类对象 管理 客户端的ip+port
std::cout << addr.PrintDebug() << "# " << buffer << std::endl;
// 收到消息后,向消息发送方 回复消息
// 4. 发送数据
std::string str("client# ");
str += buffer;
sendto(_socket_fd, str.c_str(), str.size(), 0, (sockaddr *)&peer, len);
}
}
}
~Server_udp()
{
}
private:
int _socket_fd;
u_short _port;
/* 删除:
std::string _ip;
*/
};
- Server_udp.cpp
bash
g++ Server_udp.cpp -o Server_udp
cpp
#include "Server_udp.hpp"
#include <memory>
int main(int argc, char *argv[])
{
ENABLE_CONSOLE_LOG_STRATEGY(); // 开启日志类
/* 原代码:
if (argc != 3)
{
std::cout << "请输入:" << argv[0] << " ip port" << std::endl;
exit(1);
}
*/
if (argc != 2)
{
std::cout << "请输入:" << argv[0] << " port" << std::endl;
exit(1);
}
// std::string ip(argv[1]);
// u_short port = atoi(argv[2]);
u_short port = atoi(argv[1]);
// std::unique_ptr<Server_udp> baz1 = std::make_unique<Server_udp>(port, ip);
std::unique_ptr<Server_udp> baz1 = std::make_unique<Server_udp>(port);
baz1->Init();
baz1->Start();
return 0;
}
6. UDP通信的核心流程 总结
(1) UDP通信的核心流程
- 服务端准备
服务端创建UDP Socket(socket()) → 绑定(bind())到 IP+端口(例如弹性公网IP 113.44.10.137:8080)。
关键点:绑定IP必须是服务端所在主机的有效IP(公网IP或私有IP均可,但影响访问范围)。
- 客户端发送
客户端创建UDP Socket → 调用 sendto() 向目标地址(113.44.10.137:8080)发送数据。
无需显式绑定: 客户端通常由操作系统自动分配临时端口(IP:随机端口)。
网络传输路径
- 步骤1:主机定位
数据包通过公网路由,根据目标IP(113.44.10.137)找到云服务器所在的物理主机。 - 步骤2:端口分发
数据包到达主机后,操作系统根据目标端口(8080)将数据交给绑定该端口的服务端进程。
(2) 关于"绑定私有IP"的误区修正
若服务端绑定私有IP(如172.31.13.207:8080):
外部客户端向公网IP 113.44.10.137:8080 发送的数据包能到达云服务器主机(因为公网IP是主机的入口)。
但是! 操作系统发现该数据包的目标端口8080绑定的是私有IP而非公网IP,会丢弃此包(因IP不匹配)。
结果:外部客户端收不到响应,表现为"超时"或"连接被拒绝"。
(3) 解决方案:服务端需正确绑定
- 最佳实践:绑定0.0.0.0
c
// C语言示例(UDP服务端)
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0
server_addr.sin_port = htons(8080);
bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
作用: 服务端监听所有网络接口(包括公网IP、私有IP、本地环回IP)。
效果: 无论客户端通过公网IP还是私有IP访问,数据包均能被接收。
(4) 为什么客户端无需绑定固定IP+端口?
UDP是无连接的,客户端只需指定目标地址(服务端的IP+端口)。
操作系统自动为客户端Socket分配源IP+临时端口,并在收到服务端响应时匹配到该Socket。
二、V2版本 - Dict_server(加入业务处理模块)
1. 业务处理模块(业务:简单的英译汉的网络字典)
- dictionary.txt
c
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天
- Dictionary.hpp
cpp
#pragma once
#include <fstream>
#include <map>
#include <string>
#include <iostream>
#include "Log.hpp"
class Dictionary
{
public:
// 构造函数: 读取字典文件
Dictionary(std::string dict_path = "./dictionary.txt")
: _dict_path(dict_path)
{
// 1. 读取字典文件
std::ifstream ifs(_dict_path);
if (!ifs)
{
LOG(FATAL) << "ifstream err, 打开字典文件失败, _dict_path: " << _dict_path;
exit(1);
}
LOG(INFO) << "ifstream success, _dict_path: " << _dict_path;
// 按行读取
std::string line;
while (std::getline(ifs, line)) {
// 处理每一行
size_t pos = line.find(": ");
if (pos == std::string::npos)
{
continue;
}
std::string word = line.substr(0, pos);
std::string meaning = line.substr(pos + 2);
_dict[word] = meaning;
}
// 2. 关闭文件流
ifs.close();
}
// 翻译函数: 根据单词返回其翻译结果
std::string Translate(const std::string &key)
{
auto iter = _dict.find(key);
if(iter == _dict.end())
return std::string("Unknown");
else
return iter->second;
}
private:
std::string _dict_path;
std::map<std::string, std::string> _dict;
};
2. 在服务端接入 业务处理模块
- Server_udp.hpp (在Server_udp类的Start方法中 调用业务处理模块的函数)
cpp
#pragma once
#include "Log.hpp"
#include "InetAddr.hpp"
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <iostream>
#include <functional>
using func_t = std::function< std::string(const std::string &req)>;
class Server_udp
{
public:
Server_udp(u_short port, func_t& func)
: _port(port), _func(func)
{
}
void Init()
{
// 1. 创建Udp_Socket文件
_socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (_socket_fd < 0)
{
LOG(FATAL) << "socket err: " << strerror(errno);
exit(1);
}
LOG(INFO) << "socket success, _socket_fd: " << _socket_fd;
// 2. 将socket文件绑定 ip+port
struct sockaddr_in sock_in;
sock_in.sin_family = AF_INET;
sock_in.sin_port = htons(_port);
sock_in.sin_addr.s_addr = INADDR_ANY; // 任意IP地址: 0.0.0.0
// sock_in.sin_addr.s_addr = inet_addr(_ip.c_str());
int n = bind(_socket_fd, (sockaddr *)&sock_in, sizeof(sock_in));
if (n != 0)
{
LOG(FATAL) << "bind err: " << strerror(errno);
exit(1);
}
LOG(INFO) << "bind success";
}
void Start()
{
char buffer[1024];
// 服务器一旦开始运行,永不退出(除非特殊情况)
while (1)
{
buffer[0] = 0;
// 接收数据
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int n = recvfrom(_socket_fd, buffer, sizeof(buffer) - 1,
0, (sockaddr *)&peer, &len);
if (n > 0)
{
// 成功接收到数据,打印该数据
buffer[n] = 0;
InetAddr addr(peer);
std::cout << addr.PrintDebug() << "# " << buffer << std::endl;
// 调用翻译函数
// 翻译函数返回翻译结果
std::string resp = _func(buffer); // 调用业务处理模块的函数
std::string str("client# ");
str += resp;
// 向客户端回复翻译结果
sendto(_socket_fd, str.c_str(), str.size(), 0, (sockaddr *)&peer, len);
}
}
}
~Server_udp()
{
}
private:
int _socket_fd;
u_short _port;
func_t _func; // 用包装器封装 业务处理模块的函数
// std::string _ip;
};
- Server_udp.cpp (构造字典类对象,将字典类中的 业务处理方法 传入服务器类对象)
cpp
#include "Server_udp.hpp"
#include <memory>
#include "Dictionary.hpp"
int main(int argc, char *argv[])
{
ENABLE_CONSOLE_LOG_STRATEGY(); // 开启日志类
if (argc != 2)
{
std::cout << "请输入:" << argv[0] << " port" << std::endl;
exit(1);
}
u_short port = atoi(argv[1]);
// 构造字典类对象
Dictionary dict;
func_t func = std::bind(&Dictionary::Translate, &dict, std::placeholders::_1);
// 构造服务器类对象(绑定端口 和 翻译函数包装器)
std::unique_ptr<Server_udp> baz1 = std::make_unique<Server_udp>(port, func);
baz1->Init();
baz1->Start();
return 0;
}
- 效果展示

三、V3版本 - Chat_server(加入线程池模块)
1. 全流程图解(含gitee代码链接)
gitee代码链接: V3版本 - Chat_server(加入线程池模块)
- 全流程图解

2. 业务处理模块(业务:简单的聊天室)
- InetAddr.hpp (每个InetAddr类对象 管理 一对ip+port,用一对ip+port 指代一名用户)
cpp
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// 每个InetAddr类对象 管理 一对ip+port (将网络序列 转 主机序列)
class InetAddr
{
public:
InetAddr(struct sockaddr_in &addr) : _addr(addr)
{
_port = ntohs(_addr.sin_port);
_ip = inet_ntoa(_addr.sin_addr);
}
std::string Ip() { return _ip; }
uint16_t Port() { return _port; }
std::string PrintDebug()
{
std::string info = _ip;
info += ":";
info += std::to_string(_port); // "127.0.0.1:4444"
return info;
}
struct sockaddr_in &GetAddr()
{
return _addr;
}
bool operator==(const InetAddr &peer)
{
return (_ip == peer._ip) && (_port == peer._port);
}
~InetAddr() {}
private:
std::string _ip;
uint16_t _port;
struct sockaddr_in _addr;
};
- Talk_manager.hpp (维护一个在线用户列表。功能:当一名用户发消息时,将消息转发各其它在线用户)
cpp
#pragma once
#include <vector>
#include "Mutex.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
class Talk_manager
{
private:
bool is_exist(InetAddr &peer)
{
for (auto &user : _online_user)
{
if (user == peer)
return true;
}
return false;
}
void AddUser(InetAddr &peer)
{
_online_user.push_back(peer);
LOG(LogLevel::INFO) << "添加⼀个在线用户: " << peer.PrintDebug() << " 成功";
}
void DeleteUser(InetAddr &peer)
{
for (auto iter = _online_user.begin(); iter != _online_user.end(); iter++)
{
if (*iter == peer)
{
LOG(LogLevel::INFO) << "删除⼀个在线用户:" << peer.PrintDebug() << " 成功";
_online_user.erase(iter);
break;
}
}
}
public:
Talk_manager()
{
}
// 消息路由
void MessageRoute(int sockfd, const std::string &message, InetAddr &peer)
{
if (!is_exist(peer)) // 如果用户不存在,添加到在线用户列表
{
LockGuard lockguarg(_mutex);
AddUser(peer);
}
std::string send_message = peer.PrintDebug() + "# " + message; // 127.0.0.1:8080# 你好
// 发送消息给所有在线用户
for (auto &user : _online_user)
{
if (user == peer)
continue;
sendto(sockfd, send_message.c_str(), send_message.size(), 0, (sockaddr *)&(user.GetAddr()), sizeof(user.GetAddr()));
}
// 这个用户⼀定已经在线了
if (message == "QUIT")
{
LockGuard lockguarg(_mutex);
DeleteUser(peer);
}
}
private:
// 首次给我发消息,等同于登录
std::vector<InetAddr> _online_user; // 在线用户列表 ,每个元素是一个InetAddr对象
My_Mutex _mutex; // 互斥锁,保护在线用户列表的访问,防止多线程并发修改
};
3. 线程池模块
- Thread.hpp (线程模块)
cpp
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <functional>
#include "../Log.hpp"
using func = std::function<void(const std::string &)>;
class My_Thread
{
private:
static void *thread_routine(void *th)
{
My_Thread *self = (My_Thread *)th;
self->_start_routine(self->_thread_name);
LOG(DEBUG) << self->_thread_name << "退出成功";
return (void *)0;
}
public:
My_Thread(func routine, const std::string &thread_name)
: _start_routine(routine),
_thread_name(thread_name)
{
LOG(DEBUG) << "创建" << _thread_name << "成功";
}
void Start()
{
int n = pthread_create(&_id, nullptr, thread_routine, this);
_run = true;
LOG(DEBUG) << "启动" << _thread_name << "成功";
}
void Stop()
{
if (_run)
{
pthread_cancel(_id);
LOG(DEBUG) << "取消" << _thread_name << "成功";
}
}
bool Join()
{
if (_run)
{
int n = pthread_join(_id, nullptr); // 阻塞等待目标线程退出
if (n == 0)
LOG(DEBUG) << "回收" << _thread_name << "成功";
_run = false;
return true;
}
return false;
}
private:
func _start_routine;
std::string _thread_name;
pthread_t _id;
bool _run = false;
};
- Threadpool_Singleton.hpp (单例模式 的线程池模块)
cpp
#pragma once
#include <vector>
#include <queue>
#include "../Mutex.hpp"
#include "../Cond.hpp"
#include "../Log.hpp"
#include "Thread.hpp"
using namespace std;
using placeholders::_1;
using Task = std::function<void(void)>;
class Thread_Pool
{
private:
void Handler_Task(const string &thread_name)
{
while (true)
{
Task task;
{
// 多线程要串行从任务队列拿取任务
LockGuard lockguarg(_mutex);
while (_task_queue.empty() && _isrunning)
{
_waitnum++;
_cond.Wait(_mutex.Get());
_waitnum--;
}
if (_task_queue.empty() && (!_isrunning))
break;
task = _task_queue.front();
_task_queue.pop();
LOG(DEBUG) << thread_name << "获取任务成功";
}
// 多线程执行任务可以并发
task();
}
}
// 将构造函数设置为私有
Thread_Pool(int thread_num)
: _thread_num(thread_num)
{
for (int i = 0; i < _thread_num; i++)
{
string str = "thread_";
str += to_string(i + 1);
_threads.emplace_back(bind(&Thread_Pool::Handler_Task, this, _1), str);
}
LOG(DEBUG) << "创建 Thread_Pool 成功";
}
// 禁用拷贝构造 和 赋值重载
Thread_Pool(const Thread_Pool &) = delete;
Thread_Pool &operator=(const Thread_Pool &) = delete;
public:
void threads_start()
{
_isrunning = true;
for (int i = 0; i < _thread_num; i++)
{
_threads[i].Start();
}
LOG(DEBUG) << "启动 Thread_Pool 成功";
}
void threads_stop()
{
LockGuard lockguarg(_mutex);
_isrunning = false;
_cond.NotifyAll();
}
void threads_wait()
{
for (int i = 0; i < _thread_num; i++)
{
_threads[i].Join();
}
LOG(DEBUG) << "回收 Thread_Pool 成功";
}
void Enqueue(const Task &task)
{
if (!_isrunning)
return;
LockGuard lockguarg(_mutex);
_task_queue.push(task);
if (_waitnum > 0)
_cond.Notify();
}
// 添加单例模式
static Thread_Pool *GetInstance()
{
if (_instance == nullptr)
{
LockGuard lockguarg(_Singleton_mutex);
if (_instance == nullptr)
{
_instance = new Thread_Pool(5); // 线程池默认创建5个线程
}
}
return _instance;
}
~Thread_Pool()
{
}
private:
vector<My_Thread> _threads;
int _thread_num; // 线程个数
queue<Task> _task_queue;
My_Mutex _mutex;
My_Cond _cond;
int _waitnum; // 在条件变量_cond等待队列的线程个数
bool _isrunning = false; // 线程池是否开启
// 添加单例模式
static Thread_Pool *_instance;
static My_Mutex _Singleton_mutex;
};
Thread_Pool *Thread_Pool::_instance = nullptr;
My_Mutex Thread_Pool::_Singleton_mutex;
4. 在服务端 加入线程池模块(将业务处理模块 作为任务放入线程池的任务队列中)
- Server_udp.hpp
cpp
#pragma once
#include "Log.hpp"
#include "InetAddr.hpp"
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <iostream>
#include <functional>
using func_t = std::function<void(int sockfd, const std::string &message, InetAddr &peer)>;
class Server_udp
{
public:
Server_udp(u_short port, func_t func)
: _port(port), _func(func)
{
}
void Init()
{
// 1. 创建Udp_Socket文件
_socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (_socket_fd < 0)
{
LOG(FATAL) << "socket err: " << strerror(errno);
exit(1);
}
LOG(INFO) << "socket success, _socket_fd: " << _socket_fd;
// 2. 将socket文件绑定 ip+port
struct sockaddr_in sock_in;
sock_in.sin_family = AF_INET;
sock_in.sin_port = htons(_port);
sock_in.sin_addr.s_addr = INADDR_ANY; // 任意IP地址: 0.0.0.0
// sock_in.sin_addr.s_addr = inet_addr(_ip.c_str());
int n = bind(_socket_fd, (sockaddr *)&sock_in, sizeof(sock_in));
if (n != 0)
{
LOG(FATAL) << "bind err: " << strerror(errno);
exit(1);
}
LOG(INFO) << "bind success";
}
void Start()
{
char buffer[1024];
// 服务器一旦开始运行,永不退出(除非特殊情况)
while (1)
{
buffer[0] = 0;
// 接收数据
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int n = recvfrom(_socket_fd, buffer, sizeof(buffer) - 1,
0, (sockaddr *)&peer, &len);
if (n > 0)
{
// 成功接收到数据,打印该数据
buffer[n] = 0;
InetAddr addr(peer);
std::cout << addr.PrintDebug() << "# " << buffer << std::endl;
// 调用聊天业务函数
_func(_socket_fd, buffer, addr);
}
}
}
~Server_udp()
{
}
private:
int _socket_fd; // 服务器socket文件描述符
u_short _port; // 服务器端口
func_t _func;
};
- Server_udp.cpp (将业务处理模块的Talk_manager::MessageRoute方法 作为任务放入线程池的任务队列中)
cpp
#include "Server_udp.hpp"
#include "Talk_manager.hpp"
#include "./Thread_pool/Threadpool_Singleton.hpp"
#include <memory>
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;
int main(int argc, char *argv[])
{
ENABLE_CONSOLE_LOG_STRATEGY(); // 开启日志类
if (argc != 2)
{
std::cout << "请输入:" << argv[0] << " port" << std::endl;
exit(1);
}
u_short port = atoi(argv[1]);
// 构造聊天管理类对象
Talk_manager tm_manager;
// 构造线程池对象
std::shared_ptr<Thread_Pool> thread_pool(Thread_Pool::GetInstance());
thread_pool->threads_start();
// 构造服务器类对象(绑定端口 和 翻译函数包装器)
std::unique_ptr<Server_udp> baz1 = std::make_unique<Server_udp>(port, [&tm_manager, &thread_pool](int sockfd, const std::string &message, InetAddr &peer)
{
// 将业务处理模块的Talk_manager::MessageRoute方法 作为任务放入线程池的任务队列中
Task task = std::bind(&Talk_manager::MessageRoute, &tm_manager, sockfd, message, peer);
thread_pool->Enqueue(task);
});
baz1->Init();
baz1->Start();
return 0;
}
5. 对客户端进行修改(修改后进行代码测试)
原来的客户端的发送 和 接收消息串行执行(要先发送一次数据,才能接收一次数据),不符合聊天业务的需求(比如:如果一个用户一直不发送信息,客户端就会一直阻塞等待发送数据的准备,客户端就无法接收其它客户端发送过来的数据)
- Client_udp.cpp(原来的客户端代码)
cpp
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cout << "请输入:" << argv[0] << " ip port" << std::endl;
exit(1);
}
std::string ip(argv[1]);
u_short port = atoi(argv[2]);
int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in tar_server;
tar_server.sin_family = AF_INET;
tar_server.sin_port = htons(port);
tar_server.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(tar_server);
while(1)
{
std::cout << "请输入要发送的内容:";
fflush(stdout);
// 发送数据(第一次发数据时,系统会自动为socket 绑定主机ip 和 合适的port)
std::string str;
std::cin >> str;
sendto(socket_fd, str.c_str(), str.size(), 0, (sockaddr *)&tar_server, len);
char buffer[1024] = {0};
struct sockaddr_in peer;
socklen_t lenth = sizeof(peer);
// 接收数据
recvfrom(socket_fd, buffer, sizeof(buffer), 0, (sockaddr *)&peer, &lenth);
std::cout << buffer << std::endl;
}
return 0;
}
使用多线程,将接收数据的操作放在一个新线程中,使接收数据的操作不会被 发送数据的操作阻塞!
- Client_udp.cpp(修改后的客户端代码,将接收数据操作放在一个新线程中)
cpp
#include <stdlib.h>
#include <iostream>
#include <string>
#include <memory>
#include <thread>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cout << "请输入:" << argv[0] << " ip port" << std::endl;
exit(1);
}
std::string ip(argv[1]);
u_short port = atoi(argv[2]);
// 1. 创建Udp_Socket文件
int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
// 客户端不需要显示绑定自己的 ip+port
// 向服务器发送信息前,设置对应服务器 的ip+port
// 服务器就是用来给客户端访问的(服务器的ip+port是公开的)
struct sockaddr_in tar_server;
tar_server.sin_family = AF_INET;
tar_server.sin_port = htons(port);
tar_server.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(tar_server);
int flag = -1;
while(1)
{
std::cout << "请输入要发送的内容:";
fflush(stdout);
// 发送数据(第一次发数据时,系统会自动为socket 绑定主机ip 和 合适的port)
std::string str;
std::cin >> str;
sendto(socket_fd, str.c_str(), str.size(), 0, (sockaddr *)&tar_server, len);
if(flag == -1) // 第一次发送数据后,创建新线程接收数据(后续发送数据时,不会创建线程接收数据)
{
flag = 0;
// 将接收数据的操作放在一个新线程中,使接收数据的操作不会被 发送数据的操作阻塞
std::thread t([socket_fd](){
while(1)
{
char buffer[1024] = {0};
struct sockaddr_in peer;
socklen_t lenth = sizeof(peer);
// 接收数据
recvfrom(socket_fd, buffer, sizeof(buffer), 0, (sockaddr *)&peer, &lenth);
std::cout << buffer << std::endl;
}
});
t.detach();
}
}
return 0;
}
每个用户先输入一次消息,这样所有用户都被添加为在线用户:

用户3每次输入消息,全部被转发给用户1 和 用户2,效果符合预期:



