目录
[1. UDP⽹络编程](#1. UDP⽹络编程)
[1.1 服务端初始化实现](#1.1 服务端初始化实现)
[1.2 服务端运行](#1.2 服务端运行)
[1.3 客户端初始化](#1.3 客户端初始化)
[1.4 客户端运行](#1.4 客户端运行)
[1.5 绑定IP地址](#1.5 绑定IP地址)
[2.1 字典](#2.1 字典)
[2.2 字典类,获取IP地址和端口类](#2.2 字典类,获取IP地址和端口类)
[2.3 完整实现](#2.3 完整实现)
[3. 基于UDP⽹络编程实现群聊](#3. 基于UDP⽹络编程实现群聊)
[3.1 UDP⽹络编程实现群聊有关的其他类](#3.1 UDP⽹络编程实现群聊有关的其他类)
[3.2 Server和Client](#3.2 Server和Client)
[3.3 处理任务类Route](#3.3 处理任务类Route)
[4.1 网络转主机字节序](#4.1 网络转主机字节序)
[4.2 主机转网络的字节序](#4.2 主机转网络的字节序)
UDP ( User Datagram Protocol ⽤⼾数据报协议)
传输层协议
⽆连接(对讲机,一方说话,另一方直接就能听见)
不可靠传输(允许丢包,简单,占有资源少)
⾯向数据报(发快递,发邮件,都是确定好的,例如发十个快递,就只能收到十个快递)
1. UDP⽹络编程
对于网络间的通信,需要服务端和客户端,服务端一般要接受客户端的信息进行处理,并发送个客户端,而客户端则需要发送信息个服务端并接收服务端处理完的信息
1.1 服务端初始化实现
对于服务端初始化,需要套接字,此时就需要以下函数创建套接字
cpp
头文件
#include <sys/types.h>
#include <sys/socket.h>
说明
创建 socket ⽂件描述符 (TCP/UDP, 客⼾端 + 服务器)
int socket(int domain, int type, int protocol);
参数:
domain:表示这个套接字想要做本地通信还是网络通信
本地通信:传AF_UNIX
网络通信:传AF_INET,使用IPv4进行网络通信
还有其他的参数,这里不进行叙述
type:代表套接字的类型,一般有两种
Udp-->面向数据报,传SOCK_DGRAM
Tcp-->面向字节流,传SOCK_STREAM
还有其他的参数,这里不进行叙述
protocol:代表设定的协议类型
对于网络通信,由于domain和type就可以已经证明是什么协议,因此一般设为0
返回值:
创建成功,返回文件描述符,失败返回-1
对于服务端还需要绑定对应的端口号和IP地址,此时就需要以下函数
cpp
头文件:
#include<netinet/in.h>
#include<arpa/inet.h>
说明:
绑定端⼝号和IP地址 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
参数
socket:套接字
address:
网络通信传struct sockaddr_in结构,并进行强转为struct sockaddr
本地通信传struct sockaddr_un结构,并进行强转为struct sockaddr
address_len:
指定 address 结构的长度
接下来说明一下struct sockaddr_in结构
struct sockaddr_in 是 C/C++ 网络编程中用于表示 IPv4 地址的结构体
结构体字段说明
sin_family: 地址族,通常为 AF_INET(IPv4)。
sin_port: 端口号,类型为uint16_t,需使用 htons() 转换为网络字节序(大端)。
sin_addr: 也是一个结构体,内部只有一个成员s_addr,类型为uint32_t,代表IP 地址(点分十进制),需要inet_addr() 或 inet_pton() 网络字节序,转换网络通信的时候,必须是4字节IP吗,例如198.167.0.123(点分十进制)IP地址,每一段代表一字节
还有其他的字段不做说明
cpp
// 服务端
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <string>
#include "Log.hpp" //日志模块
using namespace LogModuls;
const int defaultfd = -1;
class UdpServer // UDP服务端
{
public:
UdpServer(uint16_t port, const std::string ip)
: _sockfd(defaultfd),
_port(port),
_ip(ip)
{
}
void Init() // 初始化
{
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::ERROR) << "create socket failed";
exit(1);
}
LOG(LogLevel::INFO) << "create socket success";
// 2. 绑定socket信息,ip和端口, ip
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 清空local
local.sin_family = AF_INET; // 使用IPv4协议
local.sin_port = htons(_port); // 端口号转换为网络字节序
local.sin_addr.s_addr = inet_addr(_ip.c_str()); // IP地址转换为网络字节序
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(LogLevel::ERROR) << "bind socket failed";
exit(2);
}
LOG(LogLevel::INFO) << "bind socket success";
}
~UdpServer()
{
}
private:
int _sockfd; // 套接字
uint16_t _port; // 端口号
std::string _ip; // IP地址,使用的是点分十进制,例如192.168.1.100
}
日志类
cpp
#ifndef __LOG_HPP__
#define __LOG_HPP__
#include <iostream>
#include "Mutex.hpp"
#include <fstream>
#include <filesystem> //c++17新标准库,用于文件系统操作
#include <ctime>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <memory>
#include <string>
namespace LogModuls
{
using namespace MutexMudule;
const std::string gsep = "\r\n"; // 日志分隔符
// 策略模式,C++多态特性
// 刷新策略 a: 显示器打印 b:向指定的文件写入
// 刷新策略基类
class LogStrategy
{
public:
~LogStrategy() = default; // 强制生成默认的析构函数
virtual void SyncLog(const std::string &message) = 0; // 同步打印日志
};
// 显示器刷新策略
class DisplayStrategy : public LogStrategy
{
public:
void SyncLog(const std::string &message) override
{
// 防止多线程同时访问同一个文件,加锁
Condition condition(_mutex);
std::cout << message << gsep;
}
private:
Mutex _mutex;
};
// 文件刷新策略
const std::string DEFAULT_LOG_PATH = "./csdn.76/";
const std::string DEFAULT_LOG_NAME = "log.txt";
class FileStrategy : public LogStrategy
{
public:
FileStrategy(const std::string &fileName = DEFAULT_LOG_NAME, const std::string &path = DEFAULT_LOG_PATH)
: _fileName(fileName), _path(path)
{
// 同样的,加锁
Condition condition(_mutex);
if (std::filesystem::exists(_path)) // 判断路径是否存在
{
return;
}
try
{
std::filesystem::create_directories(_path); // 创建路径
}
catch (const std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << '\n'; // 输出错误信息
}
}
void SyncLog(const std::string &message) override
{
// 防止多线程同时访问同一个文件,加锁
// 创建日志文件,由于可能有多个进程,因此防止创建多个文件,加锁
Condition condition(_mutex);
std::string fileName = _path + _fileName;
std::ofstream ofs(fileName, std::ios::app); // 以追加的方式打开日志文件
if (!ofs.is_open())
{
return;
}
ofs << message << gsep;
ofs.close();
}
private:
std::string _fileName; // 日志文件名
std::string _path; // 日志文件路径
Mutex _mutex;
};
// 日志等级
enum LogLevel
{
DEBUG,
INFO,
WARN,
ERROR,
FATAL
};
// 输出字符形式
std::string LogLevelToString(LogLevel level)
{
switch (level)
{
case DEBUG:
return "DEBUG";
case INFO:
return "INFO";
case WARN:
return "WARN";
case ERROR:
return "ERROR";
case FATAL:
return "FATAL";
default:
return "UNKNOWN";
}
}
// 日志时间
std::string GetLogTime()
{
time_t curr = time(nullptr);
struct tm curr_tm; // tm结构体,包含了时间相关的信息,时,分,秒,年,月,日,星期等
localtime_r(&curr, &curr_tm); // 将时间戳转换为tm结构体
char timebuffer[128];
snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d",
curr_tm.tm_year + 1900,
curr_tm.tm_mon + 1,
curr_tm.tm_mday,
curr_tm.tm_hour,
curr_tm.tm_min,
curr_tm.tm_sec);
return timebuffer;
}
// 日志类
class Log
{
public:
Log()
{
EnableDisplayLogStrategy(); // 默认使用显示器刷新策略
}
// 往文件中写入日志
void EnableFileLogStrategy()
{
_strategy = std::make_unique<FileStrategy>(); // 使用make_unique创建策略对象
}
// 显示器打印日志
void EnableDisplayLogStrategy()
{
_strategy = std::make_unique<DisplayStrategy>();
}
// 代表一条日志信息
class LogMessage
{
public:
LogMessage(LogLevel &level, const std::string &name, int line, Log &log)
: _level(level),
_pid(getpid()),
_name(name),
_time(GetLogTime()),
_line(line),
_log(log)
{
// 日志的左边部分,合并起来
std::stringstream ss;
ss << "[" << _time << "] "
<< "[" << LogLevelToString(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _name << "] "
<< "[" << _line << "] "
<< "- ";
_message = ss.str();
}
// 往日志中添加内容
template <typename T>
LogMessage &operator<<(const T &t)
{
std::stringstream ss;
ss << t;
_message += ss.str();
return *this;
}
//对日志信息进行刷新(打印)
~LogMessage()
{
if(_log._strategy)
_log._strategy->SyncLog(_message);
}
private:
LogLevel _level; // 日志等级
pid_t _pid; // 日志id
std::string _name; // 日志文件名
std::string _time; // 日志时间
int _line; // 日志行号
std::string _message; // 日志信息,合并到一起打印
Log &_log;
};
//调用日志类,输出日志信息
//注意返回的是一个临时对象,可以往里面添加内容,当使用完毕后,会自动调用析构,刷新日志
LogMessage operator()(LogLevel level, const std::string &name, int line)
{
return LogMessage(level, name, line, *this);
}
private:
std::unique_ptr<LogStrategy> _strategy; // 日志策略
};
// 全局日志对象
Log logger;
// 使用宏,简化用户操作,获取文件名和行号
#define LOG(level) logger(level, __FILE__, __LINE__)
//在显示器上打印日志
#define Enable_Console_Log_Strategy() logger.EnableDisplayLogStrategy()
//在文件中写入日志
#define Enable_File_Log_Strategy() logger->EnableFileLogStrategy()
}
#endif
1.2 服务端运行
对于服务端运行,好比与周围使用的软件,除非用户手动清理后台,不然永远不关闭,因此服务端运行是一个死循环
对于服务端运行,就是要处理客户端所发送的信息,因此需要接收信息,前提是要找到对应的客户端的端口号和IP地址,因此需要以下函数
cpp
说明:
主要用于从套接字接收数据
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
参数:
sockfd:一个已打开的套接字描述符。
buf:指向用于存放接收数据的缓冲区的指针。
len:缓冲区的大小,以字节为单位。
flags:控制接收行为的标志,通常可以设置为0代表阻塞状态
src_addr:指向一个sockaddr结构的指针,该结构用于保存发送数据的源地址。
addrlen:一个值-结果参数,表示 src_addr 缓冲区的大小,调用后会被修改为实际地址的长度。
返回值:
成功时,recvfrom 返回接收到的字节数。
如果没有数据可读或套接字已关闭,返回0。
出错时,返回-1,并设置全局变量 errno 以指示错误类型。
同样服务端还需要对处理后的数据发送给服务端,需要一下函数
cpp
说明:
函数用于通过指定的 socket 将数据发送到目标主机
int sendto(int s, const void *msg, int len, unsigned int flags,
const struct sockaddr *to, int tolen);
参数:
s: 已建立连接的 socket 描述符。
msg: 指向要发送的数据内容的指针。
len: 要发送的数据长度。
flags: 一般设为 0。
to: 指定目标地址的 sockaddr 结构体。
tolen: sockaddr 结构体的长度。
cpp
void Run() // 运行
{
_isRun = true;
while (_isRun)
{
char recvbuf[1024] = {0};// 接收缓冲区
struct sockaddr_in clientaddr;// 客户端地址
socklen_t len = sizeof(clientaddr);// 地址长度
ssize_t n = recvfrom(_sockfd, recvbuf, sizeof(recvbuf)-1, 0, (struct sockaddr *)&clientaddr, &len);
if(n>0)
{
LOG(LogLevel::INFO) << "recv data from " << inet_ntoa(clientaddr.sin_addr) << ":" << n;
// 处理数据
int client_port = ntohs(clientaddr.sin_port); // 客户端端口号
std::string client_ip = inet_ntoa(clientaddr.sin_addr); // 客户端IP地址
recvbuf[n] = 0; // 字符串结尾
// 处理数据
LOG(LogLevel::INFO) << "recv data from " << client_ip << ":" << client_port << " data:" << recvbuf;// 打印接收到的数据
// 服务端向客户端发送处理后数据
std::string send_data = "server recv data";
send_data+=recvbuf;
sendto(_sockfd, send_data.c_str(), send_data.size(), 0, (struct sockaddr *)&clientaddr, len);
}
}
}
1.3 客户端初始化
客户端的初始化在创建socket一样,之后绑定IP地址和端口号和服务端不同,在服务端绑定IP地址和端口号是显示写,但在客户端中不需要显示写,首次发送消息时,OS会自动给客户端进行绑定,OS知道IP,端口号采用随机端口号的方式
为什么客户端的端口号采用随机端口号的方式?
一个端口号,只能被一个进程绑定,为了避免客户端端口冲突,同时在客户端中有的端口号不能被使用,例如(110手机号),当A进程中若把端口号绑定死了,之后退出,B进程也要绑定这个端口号运行,如果这时候运行A进程,就会导致A进程无法运行
cpp
// 1. 创建socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
std::cerr << "socket error" << std::endl;
return 2;
}
//填写服务器信息
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());
1.4 客户端运行
对于客户端来说,首先向相应的服务端发送信息,之后接收服务端处理后的信息
cpp
// 发送数据
while (true)
{
std::string input;
std::cout << "input message: ";
std::getline(std::cin, input);//输入要发送的消息
sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr*)&server, sizeof(server));//发送消息
// 接收数据
char recv_buf[1024];
memset(recv_buf, 0, sizeof(recv_buf));
//注意这里创建client变量,如果服务端有许多个,收到消息,需要知道是哪个服务端发来的
struct sockaddr_in client;
socklen_t len = sizeof(client);
int n = recvfrom(sockfd, recv_buf, sizeof(recv_buf)-1, 0, (struct sockaddr*)&client, &len);
if (n > 0)
{
recv_buf[n] = '\0';
std::cout << "recv message: " << recv_buf << std::endl;
}
}
1.5 绑定IP地址
对于IP地址的绑定,bind不能绑定公网IP,但是对于127.0.0.1这个IP和服务器的内网IP可以进行绑定,同样若服务端绑定127.0.0.1客户端绑定内网IP两端无法通信,同样反过来也是如此,因此当用户显示绑定IP地址,客户端访问时,就必须使用服务端绑定的IP地址
此时如果无法使用bind绑定公网IP,那怎么实现跨网络通信?
因此服务端不建议手动绑定IP
cpp
void Init() // 初始化
{
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::ERROR) << "create socket failed";
exit(1);
}
LOG(LogLevel::INFO) << "create socket success";
// 2. 绑定socket信息,ip和端口, ip
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 清空local
local.sin_family = AF_INET; // 使用IPv4协议
local.sin_port = htons(_port); // 端口号转换为网络字节序
//local.sin_addr.s_addr = inet_addr(_ip.c_str()); // IP地址转换为网络字节序
local.sin_addr.s_addr = INADDR_ANY;//绑定到所有地址
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(LogLevel::ERROR) << "bind socket failed";
exit(2);
}
LOG(LogLevel::INFO) << "bind socket success";
}
2.基于UDP⽹络编程实现字典翻译的功能
2.1 字典
实现字典翻译,首先就需要一个基本的字典,这里就放一个用来测试的

2.2 字典类,获取IP地址和端口类
对于字典翻译的功能,可以设置成一个类,用于用户创建对应的对象,使用对应的翻译方法,对与字典类,如果要了解到那个用户进行翻译,就可以利用获取IP地址和端口类进行查询
inerAddr.hpp(获取IP地址和端口类)
cpp
#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
//获取IP地址和端口号类
class inerAddr
{
public:
inerAddr(struct sockaddr_in addr) : _addr(addr)
{
_port = ntohs(_addr.sin_port);
_ip = inet_ntoa(_addr.sin_addr);
}
uint16_t getPort() const { return _port; }
std::string getIP() const { return _ip; }
~inerAddr() {}
private:
struct sockaddr_in _addr;
std::string _ip;
uint16_t _port;
};
Dict.hpp(字典类)
cpp
#include <iostream>
#include<unordered_map>
#include<fstream>
#include"Log.hpp"//引入日志模块
#include"inerAddr.hpp"
using namespace LogModuls;
const std::string defaultdict = "./dict.txt";//默认字典路径
const std::string sep = ": ";//字典文件中每行的分隔符
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::ERROR) << "Failed to open dict file: " << _dict_path;
return false;
}
std::string line;
while(std::getline(in,line))
{
auto pos = line.find(sep);
if(pos == std::string::npos)
{
LOG(LogLevel::WARN) << "解析 " << line<<" 失败";
continue;
}
std::string english = line.substr(0, pos);
std::string chinese = line.substr(pos + sep.size());
if(english.empty() || chinese.empty())
{
LOG(LogLevel::WARN) << "没有有效内容" << line;
continue;
}
_dict_map.insert(std::make_pair(english, chinese));
LOG(LogLevel::INFO) << "加载 " << english << " -> " << chinese;
}
in.close();
return true;
}
std::string Translate(const std::string &english,inerAddr &client) //翻译英文
{
auto it = _dict_map.find(english);
if(it == _dict_map.end())
{
LOG(LogLevel::DEBUG) <<"["<< client.getIP() << ":" << client.getPort() <<"]" << " 没有找到 " ;
return "None";
}
//顺便记录一下客户端的IP和端口
LOG(LogLevel::DEBUG) <<"["<< client.getIP() << ":" << client.getPort() <<"]" << " 请求翻译 " << english << " -> " << it->second;
return it->second;
}
~Dict()
{}
private:
std::string _dict_path;//字典的路径
std::unordered_map<std::string, std::string> _dict_map;//字典的map,映射对应的翻译
};
2.3 完整实现
UdpServer.hpp(服务端类)
cpp
// 服务端
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "inerAddr.hpp"//网络地址模块
#include "Log.hpp"//日志模块
using namespace LogModuls;
using func_t =std::function<std::string(const std::string&,inerAddr&)>;
const int defaultfd = -1;
class UdpServer // UDP服务端
{
public:
UdpServer(uint16_t port,func_t func)
: _sockfd(defaultfd),
_port(port),
_func(func)
{
}
void Init() // 初始化
{
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::ERROR) << "create socket failed";
exit(1);
}
LOG(LogLevel::INFO) << "create socket success";
// 2. 绑定socket信息,ip和端口, ip
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 清空local
local.sin_family = AF_INET; // 使用IPv4协议
local.sin_port = htons(_port); // 端口号转换为网络字节序
local.sin_addr.s_addr = INADDR_ANY;//绑定到所有地址
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(LogLevel::ERROR) << "bind socket failed";
exit(2);
}
LOG(LogLevel::INFO) << "bind socket success";
}
void Run() // 运行
{
_isRun = true;
while (_isRun)
{
char recvbuf[1024] = {0}; // 接收缓冲区
struct sockaddr_in clientaddr; // 客户端地址
socklen_t len = sizeof(clientaddr); // 地址长度
ssize_t n = recvfrom(_sockfd, recvbuf, sizeof(recvbuf) - 1, 0, (struct sockaddr *)&clientaddr, &len);
if (n > 0)
{
inerAddr client_addr(clientaddr);//把IP地址和端口号地址转换
recvbuf[n] = 0; // 字符串结尾
// 处理数据
std::string recv_dict=_func(recvbuf,client_addr);//把任务交给用户处理
// 服务端向客户端发送处理后数据
sendto(_sockfd, recv_dict.c_str(), recv_dict.size(), 0, (struct sockaddr *)&clientaddr, len);
}
}
}
~UdpServer()
{
}
private:
int _sockfd; // 套接字
uint16_t _port; // 端口号
bool _isRun; // 是否运行
func_t _func; // 回调函数,把任务交给用户处理
};
UdpServer.cc(服务端)
cpp
#include <iostream>
#include <memory>
#include"Dict.hpp"
#include "UdpServer.hpp"
int main(int argc, char *argv[])
{
if(argc != 2)
{
std::cerr << "Usage: " << argv[0] << " port" << std::endl;
return 1;
}
Enable_Console_Log_Strategy();
// 创建UDP服务端
uint16_t port = std::stoi(argv[1]);
//字典
Dict dict;
dict.LoadDict();//加载默认字典
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port,[&dict](const std::string &message,inerAddr &cli)->std::string{
return dict.Translate(message, cli);
});
// 初始化UDP服务端
usvr->Init();
// 运行UDP服务端
usvr->Run();
return 0;
}
UdpClient.cc(客户端)
cpp
//客户端
#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
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;
}
//填写服务器信息
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 << "input message: ";
std::getline(std::cin, input);//输入要发送的消息
sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr*)&server, sizeof(server));//发送消息
// 接收数据
char recv_buf[1024];
memset(recv_buf, 0, sizeof(recv_buf));
struct sockaddr_in client;
socklen_t len = sizeof(client);
int n = recvfrom(sockfd, recv_buf, sizeof(recv_buf)-1, 0, (struct sockaddr*)&client, &len);
if (n > 0)
{
recv_buf[n] = '\0';
std::cout << "recv message: " << recv_buf << std::endl;
}
}
return 0;
}
3. 基于UDP⽹络编程实现群聊
对于服务器而言,它可以收到来自客户端发送的消息和对应的IP地址及端口号,如果实现群聊,服务器会把客户端发送的消息转发给所有人,但是由服务端转发效果并不好,此时就可以让服务端给线程池推送任务,线程池中的线程来进行转发,这样效率会高一些,而UDP协议⽀持全双⼯,⼀个sockfd,既可以读取,⼜可以写⼊,对于客⼾端和服务端同样如此,因此需要支持多线程客⼾端,同时读取和写⼊

3.1 UDP⽹络编程实现群聊有关的其他类
在实现这个功能时首先要映入其他的类,例如:线程池类,互斥锁的类,线程类,条件变量类,还有上述字典功能中实现的获取IP地址和端口类,如下
线程类
cpp
#ifndef _THREAD_H_
#define _THREAD_H_
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include <cstdio>
#include <cstring>
#include <functional>
namespace chuxin
{
static uint32_t num = 1;
class Thread
{
using func_t = std::function<void()>;
private:
void Running()
{
_running = true;
}
void Detached()
{
_detached = true;
}
static void *Routine(void *arg)
{
Thread *p = static_cast<Thread *>(arg);
p->Running();
if (p->_detached)
p->Detach();
pthread_setname_np(p->_tid, p->_name.c_str());
p->_func();
return nullptr;
}
public:
Thread(func_t func)
: _tid(0), _running(false), _detached(false), _func(func), _res(nullptr)
{
_name = "thread-" + std::to_string(num++);
}
~Thread()
{
}
void Detach()
{
if (_detached)
return;
if (_running)
pthread_detach(_tid);
Detached();
}
bool Start()
{
if (_running)
return false;
int n = pthread_create(&_tid, NULL, Routine, this); // 这里this
if (n != 0)
{
return false;
}
else
{
return true;
}
}
bool Stop()
{
if (_running)
{
int n = pthread_cancel(_tid);
if (n != 0)
{
return false;
}
else
{
_running = false;
return true;
}
}
return false;
}
void Join()
{
if (_detached)
{
return;
}
int n = pthread_join(_tid, &_res);
if (n != 0)
{
}
else
{
}
}
pthread_t Id()
{
return _tid;
}
private:
pthread_t _tid;
std::string _name;
bool _running;
bool _detached;
void *_res;
func_t _func;
};
}
#endif
互斥锁类
cpp
#pragma once
#include<pthread.h>
#include<iostream>
namespace MutexMudule
{
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&m_mutex, NULL);
}
void lock()
{
pthread_mutex_lock(&m_mutex);
}
void unlock()
{
pthread_mutex_unlock(&m_mutex);
}
pthread_mutex_t* getMutex()
{
return &m_mutex;
}
~Mutex()
{
pthread_mutex_destroy(&m_mutex);
}
private:
pthread_mutex_t m_mutex;
};
class Condition
{
public:
Condition(Mutex& mutex)
:m_mutex(mutex)
{
m_mutex.lock();
}
~Condition()
{
m_mutex.unlock();
}
private:
Mutex &m_mutex;
};
}
条件变量类
cpp
#include <iostream>
#include "Mutex.hpp" //互斥量的封装
namespace chuxin
{
using namespace MutexMudule; //使用互斥量模块
class Cond
{
public:
Cond()
{
pthread_cond_init(&_cond, NULL);
}
void Cond_signal_wait()
{
pthread_cond_signal(&_cond);
}
void Broadcast()
{
pthread_cond_broadcast(&_cond);
}
void Wait(Mutex &mutex)
{
pthread_cond_wait(&_cond, mutex.getMutex());
}
~Cond()
{
pthread_cond_destroy(&_cond);
}
private:
pthread_cond_t _cond;
};
}
线程池类
cpp
#pragma once
#include <iostream>
#include "Mutex.hpp" //互斥锁
#include "code.hpp" //线程
#include "Cond.hpp" //条件变量
#include "Log.hpp" //日志
#include <vector>
#include <queue>
#include<string>
namespace ThreanPoolModuls
{
using namespace chuxin;
using namespace MutexMudule;
using namespace LogModuls;
static const int gnum = 5; // 线程池中线程数量
template <typename T>
class ThreadPool
{
private:
void WakeUpAllThread()
{
// 加锁
Condition lock(_mutex);
// 通知所有线程
if (_waitnum)
_cond.Broadcast();
}
void WakeUpOne()
{
_cond.Cond_signal_wait();
}
void HandlerTask()
{
char name[128];
pthread_getname_np(pthread_self(), name, sizeof(name)); // 获取线程名
while (true)
{
T t; // 任务
{
Condition lock(_mutex); // 互斥锁
while (_task_.empty() && _isStarted) // 任务队列为空
{
_waitnum++; // 等待线程数量加1
_cond.Wait(_mutex); // 条件变量等待
_waitnum--; // 等待线程数量减1
}
if (!_isStarted && _task_.empty())
{
break;
}
t = _task_.front(); // 取出任务
_task_.pop();
}
t(); // 执行任务
}
}
ThreadPool(int num = gnum, bool isStart = false, int waitnum = 0)
: _num(num),
_isStarted(isStart),
_waitnum(waitnum)
{
for (int i = 0; i < num; i++)
{
_thread.emplace_back([this]()
{
HandlerTask(); // 处理任务
});
}
}
public:
void Start()
{
if (_isStarted)
{
return;
}
_isStarted = true;
for (auto &t : _thread)
{
t.Start();
}
}
void Join() // 等待所有线程退出
{
for (auto &t : _thread)
{
t.Join();
}
}
void Stop()
{
if (!_isStarted)
{
return;
}
_isStarted = false;
// 首先唤醒所有线程,让它们退出,如果有任务在队列中,则处理完后再退出
WakeUpAllThread();
}
bool Enqueue(const T &in) // 提供给外部线程调用,将任务放入队列
{
if (_isStarted)
{
Condition lock(_mutex);
_task_.push(in);
if (_waitnum == _thread.size())
WakeUpOne(); // 唤醒一个线程
return true;
}
return false;
}
private:
//对拷贝构造函数和赋值运算符进行删除
ThreadPool(const ThreadPool<T> &) = delete;
ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;
public:
static ThreadPool<T> *GetInstance()
{
if (inc == nullptr)
{
Condition lockguard(_lock);
if (inc == nullptr)
{
inc = new ThreadPool<T>();
inc->Start();
}
}
return inc;
}
private:
std::vector<Thread> _thread; // 线程池
int _num; // 线程数量
std::queue<T> _task_; // 任务队列
Mutex _mutex; // 互斥锁
Cond _cond; // 条件变量
bool _isStarted; // 线程池是否启动
int _waitnum; // 等待任务的线程数量
static ThreadPool<T> *inc; // 单例指针
static Mutex _lock;
};
//静态变量类外初始化
template <typename T>
ThreadPool<T> *ThreadPool<T>::inc = nullptr;// 线程池实例
template <typename T>
Mutex ThreadPool<T>::_lock;// 互斥锁
}
3.2 Server和Client
Server.hpp
cpp
// 服务端
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "inerAddr.hpp"//网络地址模块
#include "Log.hpp"//日志模块
using namespace LogModuls;
using func_t =std::function<void(int sockfd,const std::string&,inerAddr&)>;
const int defaultfd = -1;
class UdpServer // UDP服务端
{
public:
UdpServer(uint16_t port,func_t func)
: _sockfd(defaultfd),
_port(port),
_func(func)
{
}
void Init() // 初始化
{
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::ERROR) << "create socket failed";
exit(1);
}
LOG(LogLevel::INFO) << "create socket success";
// 2. 绑定socket信息,ip和端口, ip
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 清空local
local.sin_family = AF_INET; // 使用IPv4协议
local.sin_port = htons(_port); // 端口号转换为网络字节序
local.sin_addr.s_addr = INADDR_ANY;//绑定到所有地址
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(LogLevel::ERROR) << "bind socket failed";
exit(2);
}
LOG(LogLevel::INFO) << "bind socket success";
}
void Run() // 运行
{
_isRun = true;
while (_isRun)
{
char recvbuf[1024] = {0}; // 接收缓冲区
struct sockaddr_in clientaddr; // 客户端地址
socklen_t len = sizeof(clientaddr); // 地址长度
ssize_t n = recvfrom(_sockfd, recvbuf, sizeof(recvbuf) - 1, 0, (struct sockaddr *)&clientaddr, &len);
if (n > 0)
{
inerAddr client_addr(clientaddr);//把IP地址和端口号地址转换
recvbuf[n] = 0; // 字符串结尾
_func(_sockfd,recvbuf,client_addr);//把任务交给用户处理
}
}
}
~UdpServer()
{
}
private:
int _sockfd; // 套接字
uint16_t _port; // 端口号
bool _isRun; // 是否运行
func_t _func; // 回调函数,把任务交给用户处理
};
cpp
#include <iostream>
#include <memory>
#include "UdpServer.hpp"
#include "ThreadPoll.hpp"//线程池
#include "Route.hpp"//任务类
using namespace ThreanPoolModuls;
using task_t = std::function<void()>;
int main(int argc, char *argv[])
{
if(argc != 2)
{
std::cerr << "Usage: " << argv[0] << " port" << std::endl;
return 1;
}
// 创建UDP服务端
uint16_t port = std::stoi(argv[1]);
// 1. 路由服务
Route r;
// 2. 线程池
auto tp = ThreadPool<task_t>::GetInstance();
// 3. 网络服务器对象,提供通信功能
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, [&r, &tp](int sockfd, const std::string &message, inerAddr&peer){
task_t t = std::bind(&Route::MessageRoute,&r, sockfd, message, peer);
tp->Enqueue(t);
});
// 初始化UDP服务端
usvr->Init();
// 运行UDP服务端
usvr->Run();
return 0;
}
cpp
//客户端
#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include"code.hpp"//线程类
#include"ThreadPoll.hpp"
using namespace chuxin;
using namespace ThreanPoolModuls;
int sockfd = 0;
std::string server_ip;
uint16_t server_port = 0;
pthread_t id;
void Recv()
{
while(true)
{
// 接收数据
char recv_buf[1024];
struct sockaddr_in client;
socklen_t len = sizeof(client);
int n = recvfrom(sockfd, recv_buf, sizeof(recv_buf)-1, 0, (struct sockaddr*)&client, &len);
if (n > 0)
{
recv_buf[n] = 0;
std::cerr << recv_buf<< std::endl; // 2
}
}
}
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());
// 发送数据
while (true)
{
std::string input;
std::cout << "input message: ";
std::getline(std::cin, input);//输入要发送的消息
sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr*)&server, sizeof(server));//发送消息
if(input == "QUIT")
{
pthread_cancel(id);
break;
}
}
}
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;
}
//创建线程
Thread recver(Recv);
Thread sender(Send);
recver.Start();
sender.Start();
id = recver.Id();
recver.Join();
sender.Join();
return 0;
}
3.3 处理任务类Route
对于任务类Route主要就是用来给所有的客户端进行转发消息,同时还要管理所有的在线用户
cpp
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include "inerAddr.hpp"
#include "Log.hpp"
#include"Mutex.hpp"//引入互斥锁
using namespace LogModuls;
using namespace MutexMudule;
class Route {
private:
bool IsExist(inerAddr &assr)
{
for(auto& user : m_route)
{
if(user==assr)
{
return true;
}
}
return false;
}
void AddUser(inerAddr& user) {
LOG(LogLevel::INFO) << "添加一个在线用户: " << user.StringAddr();
m_route.push_back(user);
}
void DeleteUser(inerAddr& assr)
{
for(auto it=m_route.begin();it!=m_route.end();++it)
{
if(*it==assr)
{
LOG(LogLevel::INFO) << "删除一个在线用户: " << assr.StringAddr();
m_route.erase(it);
break;
}
}
}
public:
Route() {
}
void MessageRoute(int sockfd,const std::string& msg,inerAddr& addr) {
Condition lock(m_mutex);
if(!IsExist(addr))
{
AddUser(addr);
}
std::string message =addr.StringAddr() + "# " + msg;
//转发给所有在线用户消息
for (auto& user : m_route) {
sendto(sockfd,message.c_str(),message.size(),0,(const struct sockaddr*)&(user.NetAddr()),sizeof(user.NetAddr()));
}
if(msg=="QUIT")
{
LOG(LogLevel::INFO) << "删除一个在线用户: " << addr.StringAddr();
DeleteUser(addr);
}
}
~Route() {}
private:
std::vector<inerAddr> m_route;//在线用户
Mutex m_mutex;
};
4.地址转换函数
IP地址和端口号主机和网络使用的是不一样的,因此需要利用地址转换函数,在上文实现了一个获取IP地址和端口号的类,如下
4.1 网络转主机字节序
cpp
//获取IP地址和端口号类
class inerAddr
{
public:
inerAddr(struct sockaddr_in &addr) : _addr(addr)
{
//网络转主机字节序
_port = ntohs(_addr.sin_port);
_ip = inet_ntoa(_addr.sin_addr);
}
uint16_t getPort() const { return _port; }
std::string getIP() const { return _ip; }
~inerAddr() {}
private:
struct sockaddr_in _addr;
std::string _ip;
uint16_t _port;
};
ntohs函数
cpp
说明;
主要用于将网络字节顺序(Network Byte Order)转换为主机字节顺序(Host Byte Order)。
#include <arpa/inet.h>
uint16_t ntohs(uint16_t netshort);
参数
netshort:端口号
inet_ntoa函数
inet_ntoa这个函数返回了⼀个char*,,很显然是这个函数⾃⼰在内部申请了⼀块内存来保存ip的 结果. man⼿册上说, inet_ntoa函数,,是把这个返回结果放到了静态存储区. 这个时候不需要⼿动进⾏释放,如果调⽤多次这个函数,此时因为inet_ntoa把结果放到⾃⼰内部的⼀个静态存储区, 这样第⼆次调⽤时的结果会覆盖掉上⼀次的结果.
在APUE中, 明确提出inet_ntoa不是线程安全的函数;
在多线程环境下, 推荐使⽤inet_ntop, 这个函数由调⽤者提供⼀个缓冲区保存结果, 可以规避线程安全问题;
cpp
说明:
将网络字节序的二进制地址转换为点分十进制的IP地址格式,
或者对于IPv6地址,转换为冒号十六进制的格式。
#include <arpa/inet.h>
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
参数:
af参数指定地址族,可以是AF_INET(IPv4)或AF_INET6(IPv6)。
src参数指向要转换的二进制IP地址。
dst参数指向一个足够大的缓冲区,用于存放转换后的文本地址。
size参数指定dst缓冲区的大小。
函数成功时返回一个指向dst的非空指针,失败时返回NULL。
对此可以修改为
cpp
class inerAddr
{
public:
inerAddr(struct sockaddr_in &addr) : _addr(addr)
{
//网络转主机字节序
_port = ntohs(_addr.sin_port);
//_ip = inet_ntoa(_addr.sin_addr);
char ipbuffer[64];
inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(ipbuffer));
_ip = ipbuffer;
}
uint16_t getPort() const { return _port; }
std::string getIP() const { return _ip; }
~inerAddr() {}
private:
struct sockaddr_in _addr;
std::string _ip;
uint16_t _port;
};
4.2 主机转网络的字节序
对于IP地址用inet_pton函数,端口号htons函数
htons函数
cpp
说明:
其作用是将主机字节序转换为网络字节序
uint16_t htons(uint16_t hostshort);
参数:
hostshort表示主机端口号字节序的值。
返回值: 转换后的网络字节序值。
inet_pton函数
cpp
说明:
它可以将 IP 地址从点分十进制的字符串形式转换为网络字节顺序的二进制形式。
int inet_pton(int family, const char *strptr, void *addrptr);
参数:
family: 地址族,可以是 AF_INET(IPV4) 或 AF_INET6(IPV6)。
strptr: 指向要转换的 IP 地址字符串的指针。
addrptr: 指向存储转换结果的缓冲区的指针。
返回值:
1: 转换成功。
0: 输入的 IP 地址字符串不是有效的表达式。
-1: 发生错误,可以通过 errno 获取错误代码。
cpp
//获取IP地址和端口号类
class inerAddr
{
public:
inerAddr(struct sockaddr_in &addr) : _addr(addr)
{
//网络转主机字节序
_port = ntohs(_addr.sin_port);
//_ip = inet_ntoa(_addr.sin_addr);
char ipbuffer[64];
inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(ipbuffer));
_ip = ipbuffer;
}
inerAddr(const std::string &ip, uint16_t port): _ip(ip), _port(port)
{
//主机字节序转网络字节序
_addr.sin_port = htons(_port);
//_addr.sin_addr.s_addr = inet_addr(_ip.c_str());
inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);
}
uint16_t getPort() const { return _port; }
std::string getIP() const { return _ip; }
~inerAddr() {}
private:
struct sockaddr_in _addr;
std::string _ip;
uint16_t _port;
};