🌈欢迎来到Linux专栏 ~~ 网络编程
- 🌍博客主页 :张小姐的猫~江湖背景
- 🔥所属专栏 :Linux ~ 不破不立
- 作者水平很有限,如果发现错误,可在评论区指正,感谢🙏

网络编程
- [🌈欢迎来到Linux专栏 ~~ 网络编程](#🌈欢迎来到Linux专栏 ~~ 网络编程)
- [一、UDP 网络程序](#一、UDP 网络程序)
- 🎉字符串回响
-
- 🔥核心功能
- [🔥EchoServer.hpp 服务端](#🔥EchoServer.hpp 服务端)
- [🔥Client.cc 客户端](#🔥Client.cc 客户端)
- 📚汉译英网络字典
- 🏠简单聊天室
-
- 🌈核心功能
- [🌈InetAddr.hpp 描述客户端](#🌈InetAddr.hpp 描述客户端)
- [🌈UserManager.hpp 管理客户端](#🌈UserManager.hpp 管理客户端)
- [🌈Route.hpp 路由模块 ------ 专注于业务](#🌈Route.hpp 路由模块 —— 专注于业务)
- [🌈UdpServer.hpp 纯粹的服务器](#🌈UdpServer.hpp 纯粹的服务器)
- [🌈ChatServerMain.cc 中间人](#🌈ChatServerMain.cc 中间人)
- [🌈客户端优化 --- 多线程](#🌈客户端优化 — 多线程)
- 💫理顺全流程
- 📢写在最后

一、UDP 网络程序
🎉字符串回响
🔥核心功能
分别实现客户端与服务器,客户端向服务器发送消息,服务器收到消息后,回响给客户端,有点类似于 echo 指令

该程序的核心在于 使用 socket 套接字接口,以 UDP 协议的方式实现简单网络通信
程序由
Server.hpp、Server.cc、Client.cc组成
🔥EchoServer.hpp 服务端
♻️常用函数介绍
初始化Init函数:
1️⃣第一步 --- 创建套接字 使用 socket 函数,本质就是打开网络文件,打开网卡
cpp
#include <sys/types.h>
#include <sys/socket.h>
// 创建套接字(TCP/UDP 服务器/客户端)
int socket(int domain, int type, int protocol);
参数解析:
-
domain:创建套接字用于哪种通信 (网络:AF_INET、本地:AF_UNIX本质上都是宏) -
type选择数据传输类型 (流式/数据报)
-
protocol选择协议类型 (支持根据参数2自动推导,一般设置为0)
返回值 :创建成功后,返回套接字(文件描述符fd),失败返回 -1,错误码被设置
2️⃣填充网络信息 :ip与端口号 port
其中ip地址分为两种:
- 字符串风格的点分十进制
IP地址 ,给人看的:192.168.1.1~ 15字符 - 4字节IP地址
此处的
IP地址与端口号都需要由用户传入
接着我们创建一个sockaddr_in结构体,
cpp
struct sockaddr_in local;
转到定义
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr)
- __SOCKADDR_COMMON_SIZE
- sizeof (in_port_t)
- sizeof (struct in_addr)];
};
其中sin_port只是用short int(无符号短整数)包装变成的,而sin_addr则是一个结构体,其只包含了32位的s_addr
cpp
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};

此处为什么也要有AF_INET呢?上面创建socket时候不是已经有了吗
- 这其实是两种不同的检查,第一个
socket是告诉系统我要创建一个网络套接字 ,此处的AF_INET,是为了在未来的继承体系下辨别出来我要进行网络通信(多态)
接着来看看 16 位地址类型 ,转到定义可以发现它是一个宏函数 ,并且使用了 C语言 中一个非常少用的语法 ##(将两个字符串拼接)
cpp
typedef unsigned short int sa_family_t; //无符号短整数,16位
#define __SOCKADDR_COMMON(sa_prefix) \
sa_family_t sa_prefix##family
最终会拼接成 sa_family_t sin_family 16位。此时所有的数据都对上了
了解完 sockaddr_in 结构体中的内容后,就可以创建该结构体了,再定义该结构体后,需要清空,确保其中的字段干净可用
将变量置为
0可用使用bzero函数
cpp
#include <strings.h> // bzero 函数的头文件
//2.补充网络信息
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = inet_addr(_ip.c_str()); //1.字符串风格转4字节 2.主机序列转网络序列
local.sin_port= htons(_port);
注意此时并没有 把IP地址和端口号信息设置到内核里,sockaddr_in 是一个数据类型,存在于local用户栈上的(仅仅停留在用户层)
注意:
- 需要把主机序列转换为网络序列 ,可以使用
htons函数 - 需要把点分十进制的字符串,转换为无符号短整数 ,可以使用
inet_addr函数,这个函数在进行转换的同时,会将主机序列转换为网络序列 - 绑定IP地址和端口号这个行为并非直接绑定到当前主机中,而是在当前程序中,将创建的 socket 套接字,与目标IP地址与端口号进行绑定,当程序终止后,这个绑定关系也会随之消失
cpp
//字符串类型转成4字节,并切换成网络序列
typedef uint32_t in_addr_t;
in_addr_t inet_addr(const char *cp); //指向字符串的指针
那如果是将4字节转换成字符串类型,并把网络序列切换成本地序列
cpp
#include <arpa/inet.h>
char *inet_ntoa(struct in_addr in);
参数:4 字节的 IP地址的结构体
3️⃣绑定socket信息
使用 bind 函数进行绑定操作
cpp
#include <sys/types.h>
#include <sys/socket.h>
// 绑定IP地址和端口号(TCP/UDP 服务器)
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
参数解读
sockfd创建成功的套接字addr包含通信信息的sockaddr结构体地址。此处要做强转(子类对象传给基类)addrlen结构体的大小
返回值 :成功返回 0,失败返回 -1

当前编写的 回响服务器 需要服务器拥有读取信息,然后回响给客户端的能力
首先服务器是一个死循环,所以Start函数直接进while
读取信息 使用 recvfrom 函数
cpp
#include <sys/types.h>
#include <sys/socket.h>
// 读取信息(TCP/UDP 服务器/客户端)
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
这个函数参数比较多,首先来看看前半部分
sockfd使用哪个套接字进行读取(如果系统存在多个) ------ 一般是socket()返回的那个描述符buf读取数据存放缓冲区,用户提供,保存收到的数据!len缓冲区的大小flags读取方式(阻塞读取 :0、非阻塞读取)
前半部分主要用于读取数据,并进行存放,接下来看看后半部分
src_addr输入输出型参数 ,对端主机的sockaddr结构体 ,包含了对端的IP地址 和 端口号addrlen输入输出型参数 ,传入对端主机的sockaddr结构体大小,返回实际结构体大小(往往两个值是相等的)
这个输入输出型参数就类似于送礼时留下自己的信息,待对方还礼时可以知道还给谁,接收信息也是如此,当服务器获取客户端的
sockaddr结构体信息 后,同样可以给客户端发送信息,双方就可以愉快的进行通信了
返回值 :成功返回实际读取的字节数 ,失败返回 -1
接收消息步骤:
- 创建缓冲区、对端
sockaddr_in结构体 - 接收信息,判断是否接收成功
- 处理信息
注意: 因为 recvfrom 函数的参数 src_addr 类型为 sockaddr,需要将 sockaddr_in 类型强转后,再进行传递
既然能接受信息,那么也要可以发送信息: sendto 函数
cpp
#include <sys/types.h>
#include <sys/socket.h>
// 发送信息(TCP/UDP 服务器/客户端)
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
这个函数的参数几乎与 recvfrom 的一模一样
sockfd:使用哪个套接字进行发送buf:发送数据存放缓冲区 ------ 发送什么数据?len:缓冲区的大小flags:发送方式(阻塞/非阻塞)------ 怎么发送给对方?src_addr:对端主机的sockaddr结构体,包含了对端的IP地址 和 端口号 ------ 发送给谁?addrlen:对端主机的sockaddr结构体大小
返回值 :成功返回实际发送的字节数 ,失败返回 -1
注意: 发送数据回去时,不需要把主机序列转成网络序列 ,因为从网络里收到的客户端的ip和端口本来就是网络序列的,不需要再转,直接用即可
cpp
#pragma once
#include <iostream>
#include <strings.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include "Logger.hpp"
using namespace NS_LOG_MODULE;
const static int defult_fd = -1;
const static int defult_port = 8888;
enum
{
SUCCESS = 0,
SOCKET_ERR,
USAGE_ERR,
BING_ERR,
};
class UdpServer
{
public:
UdpServer(uint16_t port)
: _sockfd(defult_fd),
_port(port)
{
}
~UdpServer()
{
close(_sockfd);
}
void Init()
{
// 1.创建socket, 本质:打开网络文件 网卡
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
// 引入日志
LOG(LogLevel::FATAL) << "creat socket err";
exit(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "creat socket success, socket:" << _sockfd;
// 2.补充网络信息
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1.字符串风格转4字节 2.主机序列转网络序列
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY; //接受任何ip的信息
// 3. bind socket信息
int n = bind(_sockfd, (const sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind socket err";
exit(BING_ERR);
}
LOG(LogLevel::INFO) << "bind socket success" << ", port: " << _port;
}
void Start()
{
// 传递的是字符串,不希望得到'\0'
char inbuffer[1024];
while (true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 0.接受信息
// 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_addr;
// peer.sin_port;
uint16_t client_port = ntohs(peer.sin_port);
std::string client_ip = inet_ntoa(peer.sin_addr);
std::string client_address = "[" + client_ip + ":" + std::to_string(client_port) + "]";
//2.处理信息
inbuffer[n] = 0;
LOG(LogLevel::DEBUG) << client_address << inbuffer;
std::string echo_string = "sever echo# ";
echo_string += inbuffer;
//3.发送数据回去
//此处不需要把主机序列转成网络序列,因为从网络里收到的客户端口本来就是网络序列的,不需要再转,直接用即可
sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, len);
}
else
{
LOG(LogLevel::ERROR) << "recvfrom error";
}
}
}
private:
int _sockfd;
// std::string _ip; //"192.168.1.1"(字符串风格的点分十进制IP地址,给人看的) 15个字节 && 4字节
uint16_t _port; // 用户设置好的,server port必须是固定的!防止客户端找不到
};
EchoServerMain.cc
cpp
#include "EchoServer.hpp"
#include <memory>
static void Usage(const std::string &process)
{
std::cerr << "Usage:\t";
std::cerr << process << "local_port" << std::endl;
}
// ./server_udp ip udp
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
ENABLE_CONSOLE_LOG_STRATEGY();
// std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(server_port);
usvr->Init();
usvr->Start();
return 0;
}
🔥Client.cc 客户端
客户端在运行时,必须知道服务器的 IP 地址 和 端口号 ,否则不知道自己该与谁进行通信,所以对于 UdpClient 类来说,ip 和 port 者两个字段是肯定少不了的。
- 不同于服务器的是 💥客户端不需要自己手动绑定 IP 地址与端口号(不让显示绑定)
1️⃣为什么不让client显示bind?
client port 只需要具有唯一性即可 。具体是多少,不重要。服务器并不关心客户端到底使用哪个端口。它只需要知道:请求来自192.168.1.100:50032,回复的时候发回这个地址即可。
所以客户端端口完全可以交给操作系统随机分配。
- 显示绑定有可能会出现冲突问题! 如果恰好有两个程序使用了同一个端口,会导致其中一方的客户端直接绑定失败,无法运行。
2️⃣如何设置自己的IP和端口呢?
将绑定 端口号 这个行为交给 OS 自动执行 ,也就是 udp client 首次发送数据 的时候,OS底层会隐式自动的帮你进行获取随机端口 ,然后绑定 ~ Port + IP
服务端是一定要求显示绑定的,不能随机绑定!(好比110、119这样的紧急号码能经常变动吗??相反我们老百姓的电话只要能标识唯一性即可,是多少不重要)
第一步:创建socket,打开网卡
cpp
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
std::cerr << "socket create fail" << std::endl;
exit(2);
}
构建sever端socket信息
cpp
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(server_ip.c_str()); //本地到网络,并且变成4字节
server.sin_port = htons(sever_port); //主机转网络
第二步:获取用户输入 ------ 发送给服务器 ------ 再收取网络的消息
1️⃣我们客户端怎么知道sever端的IP和端口呢?类似IP + Port是被内置到client的!!(抖音的客户端能不知道它自己的服务端吗? 都是一家的,早给你内置好了)
cpp
while (true)
{
std::string message;
// 1. 获取用户输入
std::cout << "Please Enter#: ";
std::getline(std::cin, message);
// 2. client 发送数据给sever端,首次发送自动绑定客户端的IP + Port
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 tmp;
socklen_t len = sizeof(tmp);
ssize_t m = recvfrom(sockfd, &inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&tmp, &len);
if(m > 0)
{
inbuffer[m] = 0;
std::cout << inbuffer << std::endl;
}
}
}
接下来我们可以在本地测试一下这个demo,ip地址为127.0.0.1(本地环回IP)


如果我们绑定公网IP呢? ------ 会出现绑定错误

最佳实践: 任意地址绑定
- 云服务器的公网IP,是禁止被显示的
bind的!! - 在服务器开发的时候,特别不建议用户显示
bindIP地址! - 云服务器的公网IP,是禁止被显示的bind 不等于 云服务器的公网
ip不会被使用 ------ 服务端上不给绑定,但客户端上可以公网访问服务端 。
使用了任意地址绑定后,客户端发三种ip地址(公网,局域网,本地环回),服务端都能接收到
源码:
cpp
#include <iostream>
#include <string>
#include <cstdlib>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
static void Usage(const std::string &process)
{
std::cerr << "Usage:\t";
std::cerr << process << "sever_ip sever_port" << std::endl;
}
// ./client sever_ip sever_port
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
std::string server_ip = argv[1];
uint16_t sever_port = std::stoi(argv[2]);
// 1.创建套接字:打开网卡
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cerr << "socket create fail" << std::endl;
exit(2);
}
// 2.构建sever端socket信息
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(server_ip.c_str()); // 本地到网络,并且变成4字节
server.sin_port = htons(sever_port); // 主机转网络
while (true)
{
std::string message;
// 1. 获取用户输入
std::cout << "Please Enter#: ";
std::getline(std::cin, message);
// 2. client 发送数据给sever端,首次发送自动绑定客户端的IP + Port
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 tmp;
socklen_t len = sizeof(tmp);
ssize_t m = recvfrom(sockfd, &inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&tmp, &len);
if(m > 0)
{
inbuffer[m] = 0;
std::cout << inbuffer << std::endl;
}
}
}
return 0;
}
接下来再学习一个命令netstat:查看系统中已经启动的服务
-a选项:查看所有的服务-n选项:以数字方式查看-u选项:只显示的是udp相关的内容,-t就是显示tcp内容-p选项:多一列来显示清楚进程信息
谈谈细节:
云服务器的端口号都是被禁止打开的! 我们能用到的端口号是因为在腾讯云的后台上提前设置了。
如果要用云服务器的端口,记得提前打开
📚汉译英网络字典
🌊业务处理函数解耦
基于模块化处理 的思想,将服务器中处理消息的函数与启动服务的函数解耦 ,由程序员传入指定的回调函数

此时业务处理函数已经变成一个模块了,可以自由变换
- 业务处理函数A:实现大写转小写
- 业务处理函数B:实现远程 汉译英
- 业务处理函数C:实现 xxx
服务器在启动时,只需要传入对应的业务处理函数(回调函数) 即可
于是乎修改server.hpp
cpp
#pragma once
#include <iostream>
#include <strings.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <functional>
#include "Logger.hpp"
using namespace NS_LOG_MODULE;
const static int defult_fd = -1;
const static int defult_port = 8888;
using callback_t = std::function<std::string (std::string)>;
enum
{
SUCCESS = 0,
SOCKET_ERR,
USAGE_ERR,
BING_ERR,
};
class UdpServer
{
public:
UdpServer(callback_t cb, uint16_t port)
: _sockfd(defult_fd),
_port(port),
_cb(cb)
{
}
~UdpServer()
{
close(_sockfd);
}
void Init()
{
// 1.创建socket, 本质:打开网络文件 网卡
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
// 引入日志
LOG(LogLevel::FATAL) << "creat socket err";
exit(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "creat socket success, socket:" << _sockfd;
// 2.补充网络信息
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1.字符串风格转4字节 2.主机序列转网络序列
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY; //接受任何ip的信息
// 3. bind socket信息
int n = bind(_sockfd, (const sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind socket err";
exit(BING_ERR);
}
LOG(LogLevel::INFO) << "bind socket success" << ", port: " << _port;
}
void Start()
{
// 传递的是字符串,不希望得到'\0'
char inbuffer[1024];
while (true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 0.接受信息
// 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_addr;
// peer.sin_port;
uint16_t client_port = ntohs(peer.sin_port);
std::string client_ip = inet_ntoa(peer.sin_addr);
std::string client_address = "[" + client_ip + ":" + std::to_string(client_port) + "]";
//2.处理信息
inbuffer[n] = 0;
std::string result = _cb(inbuffer);
//3.发送数据回去
//此处不需要把主机序列转成网络序列,因为从网络里收到的客户端口本来就是网络序列的,不需要再转,直接用即可
sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr *)&peer, len);
}
else
{
LOG(LogLevel::ERROR) << "recvfrom error";
}
}
}
private:
int _sockfd;
uint16_t _port; // 用户设置好的,server port必须是固定的!防止客户端找不到
callback_t _cb; //用回调方式来进行数据处理
};
🌊英译汉转化
此处选择使用一个哈希表 来存储对应的数据。在构造时进行文件加载操作
文件加载过程中将每行数据进行分割存入哈希
cpp
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include "Logger.hpp"
#include <unordered_map>
static const std::string default_dict = "./dixt.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 (getline(in, line)) // 从指定文件流获取一行的内容,行分隔符'\n'
{
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 _dict[word];
}
else
{
return "None";
}
}
private:
std::string _dict_path;
std::unordered_map<std::string, std::string> _dict;
};
主函数:Main.cc
cpp
#include "EchoServer.hpp"
#include "Dict.hpp"
#include <memory>
static void Usage(const std::string &process)
{
std::cerr << "Usage:\t";
std::cerr << process << " local_port" << std::endl;
}
// ./server_udp udp
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
ENABLE_CONSOLE_LOG_STRATEGY();
//1.构建字典
Dict dict;
//2.构建网络服务
uint16_t server_port = std::stoi(argv[1]);
//匿名函数绑定两个模块
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>([&dict](std::string word){
return dict.Translate(word);
} ,server_port);
usvr->Init();
usvr->Start();
return 0;
}
最后进行测试:跑出来确实是可以进行转化的

接下来理顺整个demo的执行逻辑:


上述的这个英译汉还只停留在串行地发和收,可否把收到所有的客户端都统一管理起来,然后进行并发的执行呢?
🏠简单聊天室
之前我们写线程池的时候,只说过让线程帮我们处理各种任务,但是之前没说过任务是从哪里来的? ------ 任务从网络中来
在此版本我们还需把所收到的客户端给管理起来

🌈核心功能
这是基于 UDP 协议实现的最后一个网络程序,主要功能是 构建一个多人聊天室,当某个用户发送消息时,其他用户可以立即收到,形成一个群聊
在这个程序中,服务器扮演了一个接收消息和分发消息的角色,将消息发送给已知的用户主机

接下来就是按各类头文件去逐个实现
🌈InetAddr.hpp 描述客户端
InetAddr.hpp 封装了网络地址,主要功能:
- 两种构造方式 --- 从
struct sockaddr_in构造(解析出IP和端口 ),或从port + ip字符串构造(填充sockaddr_in结构体) - 获取底层网络地址 ---
GetNetAddress()返回sockaddr_in* - 重载
operator==按IP和端口判断地址是否相等 - 便捷的
Len()--- 返回地址结构体长度
cpp
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// 对客户端进行先描述
class InetAddr
{
public:
//从网络序列解析iP和端口
InetAddr(const struct sockaddr_in &address) : _address(address), _len(sizeof(address))
{
_ip = inet_ntoa(_address.sin_addr);
_port = ntohs(_address.sin_port);
}
// ip和端口号来填充sockaddr_in
InetAddr(uint16_t port, const std::string &ip = "0.0.0.0") : _ip(ip), _port(port) // 给ip和端口转化成_address
{
bzero(&_address, sizeof(_address));
_address.sin_family = AF_INET;
_address.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1.字符串风格转4字节 2.主机序列转网络序列
_address.sin_port = htons(_port);
_len = sizeof(_address);
}
bool operator == (const InetAddr &addr)
{
return (this->_ip == addr._ip) && (this->_port == addr._port);
}
struct sockaddr_in *GetNetAddress()
{
return &_address;
}
socklen_t Len()
{
return _len;
}
InetAddr()
{}
~InetAddr()
{}
private:
// net address
struct sockaddr_in _address;
socklen_t _len;
// host address
std::string _ip;
uint16_t _port;
};
上述已经完成了对客户端的描述工作了,接下来就是对其进行管理!
🌈UserManager.hpp 管理客户端
主要实现五种:增删查改 + 遍历 ,为了方便此处选择使用vector容器进行管理~
| 方法 | 功能 |
|---|---|
| AddUser | 添加用户(已存在则跳过) |
| DelUser | 删除用户 |
| SearchUser | 按 InetAddr 查找用户是否存在 |
| ModUser | "修改"用户(实际是先删再增) |
| Users | 返回当前所有在线用户列表 |
cpp
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include "InetAddr.hpp"
#include "Logger.hpp"
using namespace NS_LOG_MODULE;
// 增删查改
class UserManager
{
public:
UserManager()
{
}
void AddUser(const InetAddr &addr)
{
if(SearchUser(addr))
return;
_users.push_back(addr);
}
void DelUser(const InetAddr &addr)
{
for(auto iter = _users.begin(); iter != _users.end(); iter++)
{
if(*iter == addr)
{
_users.erase(iter); //删除后可能出现迭代器失效问题,直接break
break;
}
}
}
bool SearchUser(const InetAddr &addr)
{
for(auto user : _users)
{
if(user == addr)
return true;
}
return false;
}
void ModUser(const InetAddr &addr)
{
//偷懒
DelUser(addr);
AddUser(addr);
}
//返回其用户
std::vector<InetAddr> &Users()
{
return _users;
}
~UserManager()
{}
private:
std::vector<InetAddr> _users;
};
🌈Route.hpp 路由模块 ------ 专注于业务
此模块负责消息的路由分发,此模块要进行加锁处理

此模块重点是Broadcast函数:加锁 → 拿到在线用户列表 → 把同一条消息依次发给每个在线用户
- 创建
RAII锁对象 - 通过调用
UserManager::Users(),获得内部 vector 容器users的引用 - 遍历所有在线用户 ------ 逐个
sendto,层层下探,最后可以拿到GetNetAddress()、和Len()。因为最后遍历的个体是InetAddr
cpp
#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include "UserManager.hpp"
// 路由模块
class Route
{
public:
Route() : _uma(std::make_unique<UserManager>())
{}
void CheckUser(const InetAddr &addr)
{
LockGuard Lockguard(_lock);
_uma->AddUser(addr);
}
void OfflineUser(const InetAddr &addr)
{
LockGuard Lockguard(_lock);
_uma->DelUser(addr);
}
void Broadcast(int sockfd, std::string message)
{
LockGuard Lockguard(_lock);
auto &users = _uma->Users(); //获取_user中的每一份
for (auto &user : users)
{
//发送给
sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)user.GetNetAddress(), user.Len());
}
}
~Route()
{}
private:
std::unique_ptr<UserManager> _uma; // _uma管理的是UserManager类型的对象
Mutex _lock;
};
🌈UdpServer.hpp 纯粹的服务器
UdpServer 只负责网络 IO (收数据 ),自己不处理业务逻辑。它通过 std::function 回调把控制权交还给上层(Route),上层决定怎么样去处理
UdpServer 的设计核心是将网络 I/O 与业务逻辑完全解耦,通过两个回调作为桥梁:
RegisterService()--- 接收上层注册的两个回调并存储Start()--- 阻塞循环recvfrom(),收到数据后依次触发两个回调

cpp
#pragma once
#include <iostream>
#include <strings.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <functional>
#include "InetAddr.hpp"
#include "Logger.hpp"
using namespace NS_LOG_MODULE;
const static int defult_fd = -1;
const static int defult_port = 8888;
// 回调给上层 Route
using handle_addr_t = std::function<void(const InetAddr &)>;
using handle_msg_t = std::function<void(int sockfd, std::string msg)>;
enum
{
SUCCESS = 0,
SOCKET_ERR,
USAGE_ERR,
BING_ERR,
};
class UdpServer
{
public:
UdpServer(uint16_t port)
: _sockfd(defult_fd),
_port(port)
{
}
~UdpServer()
{
close(_sockfd);
}
void Init()
{
// 1.创建socket, 本质:打开网络文件 网卡
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
// 引入日志
LOG(LogLevel::FATAL) << "creat socket err";
exit(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "creat socket success, socket:" << _sockfd;
// 2.补充网络信息
InetAddr local(_port);
// 3. bind socket信息
int n = bind(_sockfd, (const sockaddr *)(local.GetNetAddress()), local.Len());
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind socket err";
exit(BING_ERR);
}
LOG(LogLevel::INFO) << "bind socket success" << ", port: " << _port;
}
// 注册服务
void RegisterService(handle_addr_t handle_addr, handle_msg_t handle_msg)
{
_handle_addr = handle_addr;
_handle_msg = handle_msg;
}
void Start()
{
// 传递的是字符串,不希望得到'\0'
char inbuffer[1024];
while (true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 0.接受信息
// 1.用户发来的信息
// 2.用户的socket信息
ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (n > 0)
{
inbuffer[n] = 0;
// 1.检测新用户
InetAddr clientaddress(peer);
_handle_addr(clientaddress);
// 2.转发消息
_handle_msg(_sockfd, inbuffer);
}
else
{
LOG(LogLevel::ERROR) << "recvfrom error";
}
}
}
private:
int _sockfd;
uint16_t _port; // 用户设置好的,server port必须是固定的!防止客户端找不到
handle_addr_t _handle_addr;
handle_msg_t _handle_msg;
};
🌈ChatServerMain.cc 中间人
此模块就是组装层,负责把三个模块拼起来(匿名函数)

main() 做的事:创建三者 → 通过 RegisterService 把 Route 的方法绑到 UdpServer 的回调上 → 启动事件循环

cpp
Start() 收消息 → _handle_msg 入队 → 立即返回继续收
↓
worker 线程取出任务 → r.Broadcast
cpp
#include "UdpServer.hpp" //获取事件
#include "Route.hpp" //任务(包装事件后 交给执行者处理)
#include "SignalTon.hpp" //执行者, 处理动作
#include <memory>
static void Usage(const std::string &process)
{
std::cerr << "Usage:\t";
std::cerr << process << " local_port" << std::endl;
}
using namespace NS_THREAD_POOL;
using task_t = std::function<void()>;
// ./server_udp udp
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
ENABLE_CONSOLE_LOG_STRATEGY();
//2.构建网络服务
uint16_t server_port = std::stoi(argv[1]);
//线程池模块
auto Thread_Pool = ThreadPool<task_t>::Instance();
//路由模块
Route r;
//网络模块
UdpServer usvr(server_port);
usvr.Init();
//绑定路由 ------ 把server层检测到的任务无情转发给 路由模块
usvr.RegisterService(
[&r](const InetAddr &addr){
r.CheckUser(addr);
},
[&r, Thread_Pool](int sockfd, std::string msg){
Thread_Pool->Enqueue([&r, &sockfd, &msg](){
r.Broadcast(sockfd, msg);
});
});
usvr.Start();
return 0;
}
🌈客户端优化 --- 多线程
make测试后发现,客户端之间不能及时的收到消息(甚至我发的lll,收到的是其他客户端发来的ccc)

我们的客户端不是阻塞发送吗:先发送数据再读取信息,不发送就不会进行读取
于是要改成多线程:
cpp
#include <iostream>
#include <string>
#include <cstdlib>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "InetAddr.hpp"
#include "Thread.hpp"
using namespace NS_Thread_Module;
int sockfd = 0; // 全局
std::string server_ip;
uint16_t server_port = 0;
static void Usage(const std::string &process)
{
std::cerr << "Usage:\t";
std::cerr << process << "sever_ip sever_port" << std::endl;
}
void RecvMessage()
{
while (true)
{
// 读取消息 recvfrom
char inbuffer[1024] = {0};
struct sockaddr_in tmp;
socklen_t len = sizeof(tmp);
ssize_t m = recvfrom(sockfd, &inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&tmp, &len);
if (m > 0)
{
inbuffer[m] = 0;
std::cerr << inbuffer << std::endl; //往二号文件描述符打印
}
}
}
// 不断发送信息
void SendMessage()
{
InetAddr serveraddr(server_port, server_ip); // 填充sockaddr_in
while (true)
{
std::string message;
// 1. 获取用户输入
std::cout << "Please Enter#: "; //打印往标准输出
std::getline(std::cin, message);
// 2. client 发送数据给sever端,首次发送自动绑定客户端的IP + Port
ssize_t n = sendto(sockfd, message.c_str(), message.size(), 0,
(struct sockaddr *)serveraddr.GetNetAddress(), serveraddr.Len());
(void)n;
}
}
// ./client sever_ip sever_port
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
server_ip = argv[1];
server_port = std::stoi(argv[2]);
// 1.创建套接字:打开网卡
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cerr << "socket create fail" << std::endl;
exit(2);
}
// 创建线程
Thread recver(RecvMessage);
Thread sender(SendMessage);
recver.Start();
sender.Start();
recver.Join();
sender.Join();
return 0;
}
继续测试:发现是可以实现两个客户端进行简单通信的 ~

此处与什么管道并没有关系,客户端里接收消息走的是 recvfrom(),收到后打印到 std::cerr。2>pipe、2>pipe2 只改变了 cerr 的目的地(从终端改成管道),并不改变消息的接收方式 ------ 两个客户端依然通过同一个 UDP 服务器转发广播,所以不管你把 stderr 重定向到哪里,客户端都能正常收到消息
如果刚开始客户端不说话,是发送不了给你的。只有你启动客户端,给服务器发个消息之后,就会把你这个用户添加到在线列表里 ------ 才能收到后续消息
接着把自己的登录名字和ip与端口号加上 ------ 完善

哈哈哈全体起立 ~ 卢本伟广场
💫理顺全流程


📢写在最后
接下来是 tcp 编程

