当前位置: 首页 > news >正文

UDP协议详解

UDP协议详解

一、理解socket套接字

1.1理解IP

​ 我们都知道在网络中IP用来标识主机的唯一性。那么?这句话该如何理解呢?大家来思考一个问题:计算机之间传输传输数据是目的吗?就好比,你爸叫你给你妈带句话,你在听到你爸的指令,去找你妈传话的这个过程,你爸和你妈关心不关心?答案显然:不关心的。当你爸把这句话给你说之后,他就明白你妈肯定得到这句话,在你妈听到这句话后,是不是转而去执行这句话,对于中间的过程,双方都不在乎。所以说,传输数据只是手段,你用什么手段,作为用户关心不关心?只要你能给我传输到对应的主机上,我的目的就达到了。所以,我们得到以下结论:

数据传输到主机不是目的,而是手段。到达主机内部,交给主机的线程这才是目的。

网络通信的本质还是进程间通信。

​ 那么,我们该如何保证我们的数据能够传输正确呢?答案是:我们要有唯一的标识符。在网络中,我们把这个标识符称之为IP。在这里插入图片描述

1.2理解端口号

​ 通过上文,我们知道,数据进行传输的时候,通过IP来确定主机的唯一性,那么,数据传输到主机后,我们如何交给正确的进程呢?这个时候我们就不得不提到我们这个标题的概念了:通过端口号来确定进程的唯一性!!!

​ 这里我们来认识一下端口号:

  • 端口号是一个2字节16位的整数
  • 端口号用来表示一个进程,告诉操作系统这个数据交给哪个进程来处理
  • 一个端口号只能被一个进程占用
  • IP地址+端口号用来标识网络上某一台主机上的某个进程。

​ 端口号划分如下:

  • 0 –1023 都是知名端口号,HTTP,FTP,SSH等这些协议使用,端口号固定,不可被占用。
  • 1024 - 65535 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的。

​ 那么?有人要问了?怎么感觉端口号和操作系统中的进程ID “PID”这么相似呢?有什么关系吗?答案是:是老婆和老婆饼的关系,也就是没有关系。

  • PID标识的是正在运行中的进程,一旦程序退出,这个进程就找不到了

  • PID是变化的,可能每次运行程序得到的PID都不一样

  • 进程 ID 属于系统概念, 技术上也具有唯一性, 确实可以用来标识唯一的一个进程, 但是这样做, 会让系统进程管理和网络强耦合, 实际设计的时候, 并没有选择这样做。
    一个进程可以绑定多个端口号,而一个端口号不能被多个进程绑定

1.3 理解Socket编程

​ 我们可以通过源IP+源端口号,目标IP+目标端口号 来进行通信,我们把IP+端口号(port)称为套接字(Socket)。以下来介绍UDP协议中常见接口。

1.3.1网络字节序

​ 我们通过前面的学习明白,计算机主机有大小端之分,那么它们传输的数据岂不是大端机传大端的数据,小端机传小端的数据,那这样不就很坑了?大小端机器不能进行通信了?这咋办?这时候是不是需要一个组织站出来进行统一,所以我们就规定:

TCP/IP 协议规定,网络数据流应采用大端字节序,即低地址高字节,所以如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可。

​ 那么,我们在传输数据前是不是要进行检查,大端机就不管了,小端机就进行转化?既然有协议,那么肯定有具体的接口已经实现好了,我们发挥拿来主义就行了

NAMEhtonl,  htons,  ntohl,  ntohs - convert values between host andnetwork byte orderSYNOPSIS#include <arpa/inet.h>uint32_t htonl(uint32_t hostlong);uint16_t htons(uint16_t hostshort);uint32_t ntohl(uint32_t netlong);uint16_t ntohs(uint16_t netshort);
  • htonl函数的作用是 将32 位的长整数从主机字节序转换为网络字节序,例如将 IP 地址转换后准备发送
  • htons函数的作用是 将16 位的短整数从主机字节序转换为网络字节序,例如将 端口号 地址转换后准备发送
  • ntohl函数的作用是 将32 位的长整数从网络字节序转换为主机字节序,例如将 接收到的 IP 地址转换后使用
  • ntohs函数的作用是 将16 位的短整数从网络字节序转换为主机字节序,例如将接收到的端口号转换后使用
1.3.2 socket常见api
// 经典四个头文件
// 基础网络编程头文件
#include <sys/types.h>      // 数据类型定义(如 pid_t)
#include <sys/socket.h>     // 套接字核心函数(socket、bind等)
#include <arpa/inet.h>      // 地址转换函数(inet_pton、inet_ntop)
#include <netinet/in.h>     // IPv4/IPv6地址结构定义// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
// 发送数据(UDP,服务器 + 客户端)
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
// 接收数据(UDP,服务器 + 客户端)
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
// 关闭套接字
int close(int fd);

socket

  • socket函数来创建一个套接字
  • domain:地址簇,常见的有AF_INET(IPv4),和AF_INET6(IPv6)
  • type:套接字类型,常见的有SOCK_STREAM(TCP),SOCK_DGRAM(UDP)
  • protocol:协议,通常为0(自动选择),也可以指定协议,如IPPROTO_TCP 或 IPPROTO_UDP。
  • 成功时返回一个套接字描述符,失败时返回-1,并设置error。

这个的套接字描述符可以理解为文件描述符,网络的本质就是文件操作

bind

  • bind函数将套接字绑定到一个IP地址和端口号。
  • sockfd:套接字描述符
  • addr:指向ockaddr结构体的指针,该结构体对象包含了要绑定的地址信息,对于IPv4,使用sockaddr_in 结构体;对于IPv6,使用 sockaddr_in6 结构体。
  • addr_len:addr指向结构体对象的大小,通常使用sizeof获取。
  • 成功时返回0,失败时返回-1并设置errno。

sendto

  • 用于在 无连接套接字(如UDP)上发送数据 的。
  • sockfd: 套接字描述符,通过 socket 函数创建
  • buf: 指向要发送的数据缓冲区
  • len: 要发送的数据的长度
  • flags: 发送标志,通常为0
  • dest_add: 指向 sockaddr 结构体的指针,包含目标地址和端口号
  • addrlen: sockaddr 结构体的大小
  • 成功时返回发送的字节数,失败时返回-1,并设置 errno 以指示错误

recvfrom

  • 在无连接的套接字(如UDP)上接收数据
  • sockfd: 套接字描述符,通过 socket 函数创建。
  • buf: 指向存储接收数据的缓冲区。
  • len: 缓冲区的长度,即可以接收的最大字节数。
  • flags: 接收标志,通常为0。
  • src_addr: 指向 sockaddr 结构体的指针,用于存储发送方的地址信息。
  • addrlen: 指向 socklen_t 变量的指针,表示 sockaddr 结构体的大小。调用函数时需要设置为 sockaddr 结构体的大小,函数返回时设置为实际地址的长度。

close

  • 关闭套接字

  • 成功返回0,失败返回-1

1.3.3 sockaddr

​ sockaddr 结构体用于存储套接字地址信息。该结构体是网络地址结构体的通用形式。在具体使用时,通常会用到特定协议族的派生结构体,如 sockaddr_in、sockaddr_un。各种网络协议的地址格式并不相同

在这里插入图片描述

sockaddr

struct sockaddr {sa_family_t sa_family;  // 地址族(Address family)char sa_data[14];       // 套接字地址数据(Socket address data)
};

sockaddr_in

struct sockaddr_in {sa_family_t sin_family;   // 地址族(AF_INET)in_port_t sin_port;       // 端口号(Port number),网络字节序struct in_addr sin_addr;  // IPv4地址char sin_zero[8];         // 填充字节,使结构体大小与 `sockaddr` 一致
};

in_addr:

struct in_addr {uint32_t s_addr;  // 32位IPv4地址,网络字节序
};

二、UDP实现通信

2.1 echoserver

​ 目标:

对于服务器端

  1. 建立套接字(IPv4协议和UDP协议
  2. 绑定套接字
  3. 读取接收到的消息
  4. 发送响应
  5. 结束连接,关闭套接字

对于客户端

  1. 获取服务器端的IP和端口号
  2. 建立套接字
  3. 发送消息
  4. 读取服务器端发来的响应
  5. 结束连接,关闭套接字

​ 看到以上目标,我们不禁有以下疑问:为什么服务端要把套接字绑定到内核,而服务端不用呢?

这是因为客户端在发送消息的时候,操作系统会自动地绑定本机IP和一个随机的端口号到sockfd,这是为了避免端口号冲突

​ 对于echoserver我们进行详细讲述,剩下的两个只对重点部分进行讲述,为什么呢?大家会发现,套接字(UDP/TCP)基本是套路化的东西。

​ 我们先完善服务器部分:两个主机进行通信,我们一定要知道ip和port对吧,所以,我们的构造函数就是用来初始化ip和port。

​ 接着,我们来对其进行进行初始化,也就是创建套接字sockfd,我们可用库函数sockfd来进行创建。

//第一个参数表示IPv4 协议族,用于 Internet 通信
//第二个参数表示套接字类型为UDP
//第三个参数为具体的传输协议,设为0,系统会根据type选择默认协议(如SOCK_STREAM默认选 TCP,SOCK_DGRAM默认选 UDP)
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{exit(1);
}
_net_addr.sin_family = AF_INET;
_net_addr.sin_port = htons(_port);
_net_addr.sin_addr.s_addr = inet_addr(_ip.c_str()); ;

​ 我们写到这里是不是就创建好了?当然没有,我们只是创建出来了,内核还不知道,所以,我们要用bild来进行绑定。

int n = bind(_sockfd, (struct sockaddr*)(&_net_addr), sizeof(_net_addr));
if (n < 0)
{exit(1);
}

​ 这样绑定到内核,我们的任务就算完成了。以上便是初始化,都是套路化的东西。

​ 初始化了,我们就要对其进行启动,咱们这个服务端的目的就是收发信息,而且服务端一旦启动就轻易不会退出,所以,我们可以这样完成。

void Start(){_isrunning = true;while (true){char buffer[1024];struct sockaddr_in peer;//这里必须初始化!!!socklen_t len = sizeof(peer);ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, CONV(&peer), &len);if (n > 0){buffer[n] = 0;std::string echo_string = "echo# ";echo_string += buffer;sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, CONV(&peer), sizeof(peer));}}_isrunning = false;}

​ 以上便是对服务端的完成,接下来,我们去完成客户端。

​ 客户端的完成和服务端类似,但咱们再明确一点,客户端需不要要和内核进行绑定?答案是:不需要,它的端口交给操作系统来进行绑定!

​ 明确了这点,我们便开始着手完善我们的代码:

// 创建socketint sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cerr << "socket error" << std::endl;Die(SOCKET_ERR);}//初始化为服务端的IP和端口号struct sockaddr_in server;server.sin_family = AF_INET;server.sin_addr.s_addr = inet_addr(serverip.c_str());server.sin_port = htons(serverport);while (true){std::cout << "Please Enter# ";std::string message;std::getline(std::cin, message);int n = sendto(sockfd, message.c_str(), message.size(), 0, CONV(&server), sizeof(server));struct sockaddr_in temp;socklen_t len = sizeof(temp);char buffer[1024];n = ::recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, CONV(&temp), &len);if (n > 0){buffer[n] = 0;std::cout << buffer << std::endl;}}

​ 以上便是对echoserver的简易实现,我们可以看到我们对IP和端口号port经常使用,所以,我们可以对stuct sockaddr_in这个结构体进行封装,使其使用更加简便。

class InetAddr
{
private:void PortNet2Host(){_port = ntohs(_net_addr.sin_port);}void IpNet2Host(){char ipbuffer[64];const char *ip = inet_ntop(AF_INET, &_net_addr.sin_addr, ipbuffer, sizeof(ipbuffer));_ip = ipbuffer;(void)ip;}public:InetAddr(){}InetAddr(const struct sockaddr_in addr) : _net_addr(addr){PortNet2Host();IpNet2Host();}InetAddr(uint16_t port) : _port(port), _ip(""){_net_addr.sin_family = AF_INET;_net_addr.sin_port = htons(_port);_net_addr.sin_addr.s_addr = INADDR_ANY;}struct sockaddr *NetAddr() { return CONV(&_net_addr); }socklen_t NetAddrLen() { return sizeof(_net_addr); }std::string Ip() { return _ip; }uint16_t Port() { return _port; }~InetAddr(){}private:struct sockaddr_in _net_addr;std::string _ip;uint16_t _port;
};

​ 我们可用端口号和struct sockaddr_in来进行构造,并提供接口,使得我们更加便捷使用,同时,我们可以引入我们之前写过的日志,来进行判断是否符合我们的预期,以下便是改善后echoserver的核心部分:

UdpServer.hpp

#pragma once
#include "Common.hpp"
#include "InterAddr.hpp"
#include "Log.hpp"
#include <string.h>
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>using namespace LogModule;const static int gsockfd = -1;
const static uint16_t gport = 8080;class UdpServer
{
public:UdpServer(uint16_t port = gport) : _addr(port), _isrunning(false), _sockfd(gsockfd){}void InitServer(){_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){LOG(LogLevel::FATAL) << "socket: " << strerror(errno);Die(SOCKET_ERR);}int n = bind(_sockfd, _addr.NetAddr(), _addr.NetAddrLen());if (n < 0){LOG(LogLevel::FATAL) << "bind: " << strerror(errno);Die(BIND_ERR);}LOG(LogLevel::INFO) << "bind success";}void Start(){_isrunning = true;while (true){char buffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer);ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, CONV(&peer), &len);if (n > 0){buffer[n] = 0;InetAddr cli(peer);std::string clientinfo = cli.Ip() + ":" + std::to_string(cli.Port()) + " # " + buffer;LOG(LogLevel::DEBUG) << clientinfo;std::string echo_string = "echo# ";echo_string += buffer;sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, CONV(&peer), sizeof(peer));}}_isrunning = false;}~UdpServer(){if (_sockfd > gsockfd)close(_sockfd);}private:int _sockfd;InetAddr _addr;bool _isrunning;
};

UdpServer.cc

#include "Udpserver.hpp"int main(int argc, char *argv[])
{if (argc != 2){std::cerr << "Usage: " << argv[0] << " localport" << std::endl;Die(USAGE_ERR);}uint16_t port = std::stoi(argv[1]);ENABLE_CONSOLE_LOG();std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>(port);svr_uptr->InitServer();svr_uptr->Start();return 0;
}

UdpClient.cc

#include "Common.hpp"
#include "Udpclient.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>int main(int argc, char *argv[])
{if (argc != 3){std::cerr << "Usage: " << argv[0] << " serverip serverport" << std::endl;Die(USAGE_ERR);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cerr << "socket error" << std::endl;Die(SOCKET_ERR);}struct sockaddr_in server;server.sin_family = AF_INET;server.sin_addr.s_addr = inet_addr(serverip.c_str());server.sin_port = htons(serverport);while (true){std::cout << "Please Enter# ";std::string message;std::getline(std::cin, message);int n = sendto(sockfd, message.c_str(), message.size(), 0, CONV(&server), sizeof(server));struct sockaddr_in temp;socklen_t len = sizeof(temp);char buffer[1024];n = ::recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, CONV(&temp), &len);if (n > 0){buffer[n] = 0;std::cout << buffer << std::endl;}}return 0;
}

​ 对于客户端我们不做封装,可自行封装,Common.h文件实现的是强转和exit各位可自行实现。

2.2 英汉字典

​ 目标:

  • 创建套接字
  • 绑定套接字
  • 接收客户端要查的单词
  • 发送客户端翻译结果
  • 结束连接,关闭套接字

​ 这个相比于上面,我们增加了业务逻辑,不过大框架我们肯定是大差不差的,就是说对于客户端发来的单词,我们要进行翻译。我们有这样的实现思路:首先,我们先建立一个英汉字典,客户端输入,我们进行查询,如果没找到,我们则返回nullptr。对于这个思路,我们自然想到了,我们可借助哈希表unordered_map这个结构来帮助我们查询。所以,我们的大框架就出来了:

  1. 首先建立字典文件
  2. 进行分割,然后插入哈希表
  3. 根据发过来的单词,进行查询,返回结果

​ 这个我们就好奇,我们如何进行翻译呢?难道我们要在写一个函数吗?但是如果我们要自行决定如何翻译咋办?我们可用lamabda表达式,function包装器来继续自定义设置回调函数,来解决。另外,我们可用智能指针来帮助我们管理对象。以下是核心代码:

分割逻辑

bool SplitString(std::string line, std::string *key, std::string *value, std::string sep)
{auto pos = line.find(sep);if(pos == std::string::npos){return false;}*key = line.substr(0,pos);*value = line.substr(pos+sep.size());if(key->empty() || value->empty()) return false;return true;
}

Dictionary.hpp

#pragma once
#include "Log.hpp"
#include "Common.hpp"
#include <sstream>
#include <unordered_map>using namespace LogModule;const std::string gpath = "./";
const std::string gdictname = "dict.txt";
const std::string gsep = ": ";class Dictionary
{
private:bool LoadDictionary(){std::string file = _path + _filename;std::ifstream in(file.c_str());if (!in.is_open()){LOG(LogLevel::ERROR) << "open file " << file << " error";return false;}std::string line;while (std::getline(in, line)){std::string key;std::string value;if (SplitString(line, &key, &value, gsep)){ _dictionary.insert(std::make_pair(key, value));}}in.close();return true;}public:Dictionary(const std::string path = gpath, const std::string filename = gdictname): _path(path), _filename(filename){LoadDictionary();Print();}std::string TeansLate(const std::string &word){auto iter = _dictionary.find(word);if (iter == _dictionary.end())return "None";return iter->second;}void Print(){for (auto &item : _dictionary){std::cout << item.first << ":" << item.second << std::endl;}}~Dictionary(){}private:std::unordered_map<std::string, std::string> _dictionary;std::string _path;std::string _filename;
};

UdpServer.hpp:

using func_t = std::function<std::string(const std::string &)>;class UdpServer
{
public:UdpServer(func_t func, uint16_t port = gport): _addr(port), _isrunning(false), _sockfd(gsockfd), _func(func){}void Start(){_isrunning = true;while (true){char buffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer);ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, CONV(&peer), &len);if (n > 0){buffer[n] = 0;std::string result = _func(buffer);sendto(_sockfd, result.c_str(), result.size(), 0, CONV(&peer), sizeof(peer));}}_isrunning = false;}~UdpServer(){if (_sockfd > gsockfd)close(_sockfd);}private:int _sockfd;InetAddr _addr;bool _isrunning;func_t _func;
};

​ 以上展示的是增减部分。

UdpServer.cc

#include "Udpserver.hpp"
#include "Dictionary.hpp"int main(int argc, char *argv[])
{if (argc != 2){std::cerr << "Usage: " << argv[0] << " localport" << std::endl;Die(USAGE_ERR);}uint16_t port = std::stoi(argv[1]);ENABLE_CONSOLE_LOG();std::shared_ptr<Dictionary> dict_ptr = std::make_shared<Dictionary>();std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>([&dict_ptr](const std::string& message){std::cout << "|" << message << "|" << std::endl;return dict_ptr->TeansLate(message);},port);svr_uptr->InitServer();svr_uptr->Start();return 0;
}

dict.txt

apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天

​ 以上是测试的翻译文件

2.3 多人聊天室

​ 目标:

  • 创建套接字
  • 绑定套接字
  • 服务端实现路由转发功能 ,即每个注册用户其它用户的消息
  • 客户端实现发消息和接收其它用户消息

​ 接下来,我们来实现多人聊天室,以上那个echoserver可用理解为一个简易的单人聊天室。我们多人聊天室,用IP加端口号来绑定用户,这样的策略。这里我们着重讲解用户实现模块。

​ 对于用户我们肯定要对多个用户进行管理,所以,我们管理策略为:**先描述,在组织。**我们知道一个用户要实现的核心功能为:**转发信息。**所以,用户的描述为:转发信息,支持比较,提供ID。

class user
{
public:void SendTo(int sockfd,const std::string& message){}bool operator==(const InetAddr &u){}std::string Id(){};
private:InetAddr _id;
}

​ 那我们该如何组织呢?无外乎三点:增加用户,删除用户,转发路由。我们可以用链表来进行管理,直接对user这个对象管理太过麻烦,所以我们可用继承的思想,直接对它父类进行管理即可。

class UserInterface
{
public:virtual ~UserInterface() = default;virtual void SendTo(int sockfd, const std::string &message) = 0;virtual bool operator==(const InetAddr &u) = 0;virtual std::string Id() = 0;
};class user : public UserInterface
{
public:void SendTo(int sockfd,const std::string& message){}bool operator==(const InetAddr &u){}std::string Id(){};
private:InetAddr _id;
}class UserManager
{
public:UserManager(){}void AddUser(InetAddr &id) {}void DelUser(InetAddr &id) {}void Router(int sockfd, const std::string &message) {}~UserManager() {}
private:std::list<std::shared_ptr<UserInterface>> _online_user;Mutex _mutex;
}

​ 以上便是要完成的大框架,我们可以发现,用户在这里充当的是一个观察者,他可以自由选择是否加入这个对话,这个模式。这个模式称之为:观察者模式

观察者模式(Observer Pattern)是一种行为设计模式,用于在对象之间建立一对多**的依赖关系,使得当一个对象(被观察者)状态发生改变时,所有依赖它的对象(观察者)都能自动收到通知并更新。

​ 了解了这些,这个框架的完成还是很容易的,我们再讨论其它细节:

  1. 要实现多用户模式,我们可引入之前线程池
  2. 要实现的方法,我们可以和之前一样采用回调的形式来进行实现

​ 以下是实现代码:

User.hpp

#pragma
#include <iostream>
#include <string>
#include <list>
#include <memory>
#include <algorithm>
#include <sys/types.h>
#include <sys/socket.h>#include "InterAddr.hpp"
#include "Log.hpp"
#include "Mutex.hpp"using namespace LogModule;
using namespace LockModule;class UserInterface
{
public:virtual ~UserInterface() = default;virtual void SendTo(int sockfd, const std::string &message) = 0;virtual bool operator==(const InetAddr &u) = 0;virtual std::string Id() = 0;
};class User : public UserInterface
{
public:User(const InetAddr &id): _id(id){}void SendTo(int sockfd, const std::string &message) override{LOG(LogLevel::DEBUG) << "send message to " << _id.Addr() << " info: " << message;int n = sendto(sockfd, message.c_str(), message.size(), 0, _id.NetAddr(), _id.NetAddrLen());(void)n;}bool operator==(const InetAddr &u) override{return _id == u;}std::string Id() override{return _id.Addr();}~User(){}private:InetAddr _id;
};class UserManager
{
public:UserManager(){}void AddUser(InetAddr &id){LockGuard lock(_mutex);for (auto &e : _online_user){if (*e == id){LOG(LogLevel::INFO) << id.Addr() << "用户已经存在";return;}}LOG(LogLevel::INFO) << " 新增该用户: " << id.Addr();_online_user.push_back(std::make_shared<User>(id));PrintUser();}void DelUser(InetAddr &id){// v1auto pos = std::remove_if(_online_user.begin(), _online_user.end(), [&id](std::shared_ptr<UserInterface> &user){ return *user == id; });_online_user.erase(pos, _online_user.end());PrintUser();}void Router(int sockfd, const std::string &message){LockGuard lockguard(_mutex);for (auto &user : _online_user){user->SendTo(sockfd, message);}}void PrintUser(){for (auto user : _online_user){LOG(LogLevel::DEBUG) << "在线用户-> " << user->Id();}}~UserManager(){}private:std::list<std::shared_ptr<UserInterface>> _online_user;Mutex _mutex;
};

UdpServer.hpp

#pragma once
#include "Common.hpp"
#include "InterAddr.hpp"
#include "Log.hpp"
#include <string.h>
#include <iostream>
#include <string>
#include <memory>
#include <cstring>
#include <cerrno>
#include <functional>
#include "ThreadPool.hpp"using namespace LogModule;
using namespace ThreadPoolModule;const static int gsockfd = -1;
const static uint16_t gport = 8080;using adduser_t = std::function<void(InetAddr& id)>;
using remove_t = std::function<void(InetAddr& id)>;using task_t = std::function<void()>;
using route_t = std::function<void(int sockfd, const std::string &message)>;class nocopy
{
public:nocopy(){}nocopy(const nocopy &) = delete;const nocopy& operator = (const nocopy &) = delete;~nocopy(){}
};class UdpServer : public nocopy
{
public:UdpServer(uint16_t port = gport): _addr(port), _isrunning(false), _sockfd(gsockfd){}void InitServer(){_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){LOG(LogLevel::FATAL) << "socket: " << strerror(errno);Die(SOCKET_ERR);}int n = bind(_sockfd, _addr.NetAddr(), _addr.NetAddrLen());if (n < 0){LOG(LogLevel::FATAL) << "bind: " << strerror(errno);Die(BIND_ERR);}LOG(LogLevel::INFO) << "bind success";}void RegisterService(adduser_t adduser, route_t route, remove_t removeuser){_adduser = adduser;_route = route;_removeuser = removeuser;}void Start(){_isrunning = true;while (true){char buffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer);ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, CONV(&peer), &len);if (n > 0){InetAddr cli(peer);buffer[n] = 0;std::string message;if (strcmp(buffer, "QUIT") == 0){// 移除观察者_removeuser(cli);message = cli.Addr() + "# " + "我走了,你们聊!";}else{// 2. 新增用户_adduser(cli);message = cli.Addr() + "# " + buffer;}// 3. 构建转发任务,推送给线程池,让线程池进行转发task_t task = std::bind(UdpServer::_route, _sockfd, message);ThreadPool<task_t>::getInstance()->Equeue(task);}}_isrunning = false;}~UdpServer(){if (_sockfd > gsockfd)close(_sockfd);}private:int _sockfd;InetAddr _addr;bool _isrunning;// 新增用户adduser_t _adduser;// 移除用户remove_t _removeuser;// 数据转发route_t _route;
};

UdpServer.cc

#include "Udpserver.hpp"
#include "User.hpp"int main(int argc, char *argv[])
{if (argc != 2){std::cerr << "Usage: " << argv[0] << " localport" << std::endl;Die(USAGE_ERR);}uint16_t port = std::stoi(argv[1]);ENABLE_CONSOLE_LOG();std::shared_ptr<UserManager> um = std::make_shared<UserManager>();std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>(port);svr_uptr->RegisterService([&um](InetAddr &id){ um->AddUser(id); },[&um](int sockfd, const std::string message){ um->Router(sockfd, message); },[&um](InetAddr &id){ um->DelUser(id); });svr_uptr->InitServer();svr_uptr->Start();return 0;
}

UdpClient.cc

#include "Common.hpp"
#include "Udpclient.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <signal.h>
#include <string.h>int sockfd = -1;
struct sockaddr_in server;void ClientQuit(int signo)
{(void)signo;const std::string quit = "QUIT";int n = ::sendto(sockfd, quit.c_str(), quit.size(), 0, CONV(&server), sizeof(server));exit(0);
}void *Recver(void *args)
{while (true){(void)args;struct sockaddr_in temp;socklen_t len = sizeof(temp);char buffer[1024];int n = ::recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, CONV(&temp), &len);if (n > 0){buffer[n] = 0;std::cerr << buffer << std::endl;}}
}int main(int argc, char *argv[])
{if (argc != 3){std::cerr << "Usage: " << argv[0] << " serverip serverport" << std::endl;Die(USAGE_ERR);}signal(2, ClientQuit);std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 1. 创建socketsockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cerr << "socket error" << std::endl;Die(SOCKET_ERR);}// 1.1 填充server信息memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = ::htons(serverport);server.sin_addr.s_addr = ::inet_addr(serverip.c_str());pthread_t tid;pthread_create(&tid, nullptr, Recver, nullptr);// 1.2 启动的时候,给服务器推送消息即可const std::string online = " ... 来了哈!";int n = ::sendto(sockfd, online.c_str(), online.size(), 0, CONV(&server), sizeof(server));// 2. clientdonewhile (true){std::cout << "Please Enter# ";std::string message;std::getline(std::cin, message);int n = ::sendto(sockfd, message.c_str(), message.size(), 0, CONV(&server), sizeof(server));(void)n;}return 0;
}

完!

相关文章:

  • 学习笔记(C++篇)--- Day 3
  • 今日行情明日机会——20250421
  • 数据结构第六章(五)-拓扑排序、关键路径
  • JavaScript数据结构与算法实战: 探秘Leetcode经典题目
  • Android 中实现 GIF 图片动画
  • DeepSeek R1模型微调怎么做?从入门到实战
  • CFIS-YOLO:面向边缘设备的木材缺陷检测轻量级网络解析
  • 经典文献阅读之--Kinematic-ICP(动态优化激光雷达与轮式里程计融合)
  • 从C语言变量看内存
  • LX3-初识是单片机
  • java集合框架day1————集合体系介绍
  • mongodb 存储数据的具体实现方式
  • 基于SpringBoot的篮球联盟管理系统(源码+数据库+万字文档)
  • 如何开发一套TRS交易系统:架构设计、核心功能与风险控制
  • 第十三讲、isaaclab中修改工作流的RL环境
  • CCF CSP 第37次(2025.03)(1_数值积分_C++)
  • Java 程序员的 Python 之旅
  • 【线段树】P1253 扶苏的问题|普及+
  • 操作系统期中复习
  • 初识Redis · C++客户端list和hash
  • 三部门:对不裁员少裁员的参保企业实施稳岗返还政策至今年底
  • KZ队史首冠,透过春决看CF电竞张扬的生命力
  • 在因关税战爆火的敦煌网上,美国人爱买什么中国商品
  • “雷公”起诉人贩子王浩文案将开庭:索赔6元,“讨个公道”
  • 学者建议:引入退休教师、青少年宫参与课后服务,为教师“减负”
  • 中国政府援缅第七批抗震救灾物资运抵交付