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
目标:
对于服务器端
- 建立套接字(IPv4协议和UDP协议)
- 绑定套接字
- 读取接收到的消息
- 发送响应
- 结束连接,关闭套接字
对于客户端
- 获取服务器端的IP和端口号
- 建立套接字
- 发送消息
- 读取服务器端发来的响应
- 结束连接,关闭套接字
看到以上目标,我们不禁有以下疑问:为什么服务端要把套接字绑定到内核,而服务端不用呢?
这是因为客户端在发送消息的时候,操作系统会自动地绑定本机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这个结构来帮助我们查询。所以,我们的大框架就出来了:
- 首先建立字典文件
- 进行分割,然后插入哈希表
- 根据发过来的单词,进行查询,返回结果
这个我们就好奇,我们如何进行翻译呢?难道我们要在写一个函数吗?但是如果我们要自行决定如何翻译咋办?我们可用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)是一种行为设计模式,用于在对象之间建立一对多**的依赖关系,使得当一个对象(被观察者)状态发生改变时,所有依赖它的对象(观察者)都能自动收到通知并更新。
了解了这些,这个框架的完成还是很容易的,我们再讨论其它细节:
- 要实现多用户模式,我们可引入之前线程池
- 要实现的方法,我们可以和之前一样采用回调的形式来进行实现
以下是实现代码:
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;
}
完!
相关文章:
UDP协议详解
UDP协议详解 一、理解socket套接字 1.1理解IP 我们都知道在网络中IP用来标识主机的唯一性。那么?这句话该如何理解呢?大家来思考一个问题:计算机之间传输传输数据是目的吗?就好比,你爸叫你给你妈带句话ÿ…...
Unreal Engine中FRotator与FQuat在赛车游戏方向盘控制中的协同应用解析
摘要 深入剖析 Unreal Engine 中这两个关键组件在赛车游戏方向盘控制中的协同作用,涵盖全流程与实践技巧。 一、引言 在赛车游戏开发中,实现逼真的方向盘控制是提升玩家体验的关键要素之一。而在 Unreal Engine 里,FRotator 与 FQuat 这两…...
第十四届蓝桥杯 2023 C/C++组 飞机降落
目录 题目: 题目描述: 编辑题目链接: 思路: 核心思路: 思路详解: 代码: 代码详解: 题目: 题目描述: 题目链接: 洛谷 P9241 [蓝桥杯 20…...
完美解决Microsoft Edge浏览器无法同步/一直在同步中/更新失败等问题
在使用Microsoft Edge浏览器的过程中,我们可能会遇到一些常见的问题,例如无法同步数据或无法更新浏览器。这些问题通常可以归结为以下两个主要原因: 一、网络连接问题 当Edge浏览器无法同步或更新时,首要考虑的是网络连接问题。…...
CSS文本属性
CSS文本属性 在CSS中,可以使用以下属性来设置文本的样式和布局: 1.color : 设置文本颜色。可以使用颜色名称、十六进制值或RGB值来指定颜色: p{color:red; }font-family : 设置文本的字体系列。可以指定一个或多个字…...
思科路由器做DNS服务器
1.实验环境中,常常需要一台DNS服务器来做名称解析,一般会安装一台windows server,启用dns服务,或者安装一台Linux服务器,安装Bind来实现;虽然可以实现你想要的功能,但是费时费力且配置复杂&…...
Windows部署FunASR实时语音听写便捷部署教程
FunASR提供可便捷本地或者云端服务器部署的实时语音听写服务,内核为FunASR已开源的runtime-SDK。 FunASR集成了达摩院语音实验室在Modelscope社区开源的语音端点检测(VAD)、Paraformer-large非流式语音识别(ASR)、Paraformer-large流式语音识别(ASR)、标点预测(PUNC) 等相关能…...
C++之unordered封装
目录 一、哈希表的修改 1.1、哈希表节点结构 1.2、迭代器 1.3、哈希表结构 1.4、完整代码 二、unordered_map的实现 二、unordered_set的实现 一、哈希表的修改 注意:这里我们使用哈希桶来封装unordered_map和unordered_set。 1.1、哈希表节点结构 templa…...
Pycharm(九)函数的闭包、装饰器
目录 一、函数参数 二、闭包 三、装饰器 一、函数参数 def func01():print("func01 shows as follows") func01() # 函数名存放的是函数所在空间的地址 print(func01)#<function func01 at 0x0000023BA9FC04A0> func02func01 print(func02)#<function f…...
7. 栈与队列(随想录)
1.栈实现队列 2.用队列实现栈 3.有效的括号 4.删除字符串中的所有相邻重复项 5.逆波兰表达式 6.滑动窗口最大值 7.前k个高频元素...
GPU软硬件架构协同设计解析
GPU软硬件架构协同设计解析 GPU(图形处理器)的软硬件协同设计是其在通用计算和高性能计算(HPC)领域取得突破的核心原因。以下从硬件架构、软件架构、协同设计的关键技术及典型案例展开深度解析。 一、硬件架构的核心设计原则 流式多处理器(SM)的模块化设计 计算单元…...
【软考】论NoSQL数据库技术及其应用示例
论NoSQL数据库技术及其应用 随着互联网web2.0网站的兴起,传统关系数据库在应对web2.0 网站,特别是超大规模和高并发的web2.0纯动态SNS网站上已经显得力不从心,暴露了很多难以克服的问题,而非关系型的数据库则由于其本身的特点得到…...
特伦斯智慧钢琴评测:如何用科技重塑钢琴学习新体验
对于渴望学习钢琴的爱好者而言,传统钢琴的笨重体积、高昂成本与扰民问题往往成为绊脚石。而智能电钢琴的出现,正以轻量化设计、沉浸式体验与智能化功能打破这些壁垒。特伦斯智慧钢琴凭借其专业级硬件配置与创新教学系统,成为市场中兼具性能与…...
UML 状态图:解锁电子图书馆管理系统的高效设计
目录 一、UML 状态图的核心要素 状态:系统行为的 “栖息地” 转换:连接状态的 “桥梁” 动作:赋予功能的 “实践者” 二、电子图书馆管理系统状态图解析 系统空闲状态:一切的起点 读者登录与身份验证:安全的 “…...
UML 状态图:陪伴机器人系统示例
目录 一、状态图的基本概念 1.1 状态 1.2 转换 1.3 动作 二、陪伴机器人系统状态图解析 2.1 初始与待机状态 2.2 情绪检测中状态 2.3 陪伴模式下的细分 2.4 疏导模式的严谨流程 2.5 安抚模式的关键作用 三、状态图绘画 四、UML 状态图的强大 4.1 直观呈现系统行为…...
超详细实现单链表的基础增删改查——基于C语言实现
文章目录 1、链表的概念与分类1.1 链表的概念1.2 链表的分类 2、单链表的结构和定义2.1 单链表的结构2.2 单链表的定义 3、单链表的实现3.1 创建新节点3.2 头插和尾插的实现3.3 头删和尾删的实现3.4 链表的查找3.5 指定位置之前和之后插入数据3.6 删除指定位置的数据和删除指定…...
分布式光纤测温技术让森林火灾预警快人一步
2025年春季,多地接连发生森林火灾,累计过火面积超 3万公顷。春季历来是森林草原火灾易发、多发期,加之清明节已到来,生产生活用火活跃,民俗祭祀用火集中,森林火灾风险进一步加大。森林防火,人人…...
判断链表是否为环(Java版本自己用)
141. 环形链表 核心代码版本: public class Solution {public boolean hasCycle(ListNode head) {if (head null) {return false;}ListNode slow head;ListNode fast head.next;while (fast ! null && fast.next ! null) {if (slow fast) {return true…...
leetcode 516. Longest Palindromic Subsequence
题目描述: 代码: class Solution { public:int longestPalindromeSubseq(string s) {int n s.size();//i<j,dp[i][j]表示s[i,j]的最长回文子串的长度,按照这个定义dp[0][n-1]就是答案,i>j的dp[i][j]不定义vector<vector<int>> dp(n,…...
关于敏感文件或备份 安全配置错误 禁止通过 URL 访问 Vue 项目打包后的 .gz 压缩文件
要禁止通过 URL 访问 Vue 项目打包后的 .gz 压缩文件(如 sc.6abb69d9.css.gz)或其他敏感文件,可以通过 Nginx 配置和 Tomcat 配置双重防护来实现。以下是具体解决方案: 方法 1:通过 Nginx 配置禁止访问 .gz 文件 在 N…...
Linux系统启动全流程解析:从BIOS到用户登录
摘要 深度解析Linux系统启动五阶段:内核加载→init进程初始化→系统服务启动→终端创建→用户登录,涵盖SysV/Systemd差异及运行级别管理,提供故障排查指南。 一、启动流程全景概览 Linux系统启动过程严格遵循5个阶段顺序执行,每…...
unity动态骨骼架设+常用参数分享(包含部分穿模解决方案)
Unity骨骼物理模拟插件Dynamic Bone Dynamic Bone 可用于对角色的骨骼(bones)或者铰链系统(joints)施加物理效果。 物理效果可以使得游戏角色的头发、衣服、胸部或者是其他的任何部位,都可以以近似真实的状态运动。 …...
【云原生】k8s集群部署最新版ELFK日志采集平台
✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Golang开…...
基于瑞芯微RK3576国产ARM八核2.2GHz A72 工业评估板——ROS2系统使用说明
前 言 本文主要介绍创龙科技TL3576-MiniEVM评估板演示基于Ubuntu的ROS系统(版本:ROS2 Foxy)使用说明,包括镜像编译、镜像替换,以及ROS系统测试的方法。适用开发环境如下。 Windows开发环境:Windows 10 64bit Linux虚拟机环境:VMware16.2.5、Ubuntu22.04.5 64bit U-B…...
android studio sdk unavailable和Android 安装时报错:SDK emulator directory is missing
md 网上说的都是更换proxy代理什么的,还有一些二其他乱七八糟的,根本没用,感觉很多就是解决不了问题,还贼多贼一致,同质化,感觉很坑人,让人觉得他们和我的一样的,大家都是按他们说的…...
qemu构建arm环境(AI生成)
要编译 qemu-system-arm,你需要安装一些依赖库,配置编译环境,并执行编译过程。以下是一般步骤,适用于大多数基于Linux的系统。 1. 安装依赖 首先,你需要安装一些必要的开发工具和库。你可以使用包管理器来安装这些依…...
10天学会嵌入式技术之51单片机-day-4
第十二章 中断系统 中断系统是单片机用于处理外部紧急事件的一种机制。中断系统工作的大致流程如下 图所示:当 CPU 正在处理某项任务时,外部发生了某个紧急事件,此时 CPU 会暂停当前 的工作,转而去处理这个紧急事件,处…...
spark—SQL3
连接方式 内嵌Hive: 使用时无需额外操作,但实际生产中很少使用。 外部Hive: 在虚拟机下载相关配置文件,在spark-shell中连接需将hive-site.xml拷贝到conf/目录并修改url、将MySQL驱动copy到jars/目录、把core-site.xml和hdfs-sit…...
CENTOS 7 安装VNC
一、VNC简介 VNC(Virtual Network Computing),为一种使用RFB协议的屏幕画面分享及远程操作软件。此软件借由网络,可发送键盘与鼠标的动作及即时的屏幕画面。 VNC与操作系统无关,因此可跨平台使用,例如可用…...
第42讲:走进智慧农业的“感知神经系统”——农田遥感 + 边缘计算的融合实践
目录 ✨一、为什么要融合遥感与边缘计算? 🧪二、典型应用场景案例 ✅ 案例 1:棉花田的智能水分监测系统 ✅ 案例 2:水稻纹枯病自动识别与预警系统 💻三、关键技术框架与实现思路 🚦 1. 系统架构流程图: 📦 2. 模型部署建议: 💡四、未来发展趋势展望 �…...
Dify忘记管理员密码,重置的问题
今天本地win10电脑,使用源码启动dify,忘记了管理员账号和密码,于是网上查找解决办法。 1.有的网上资料说是去数据库删除用户表,于是进入数据库: docker exec -it docker-db-1 psql -U postgres -d dify 找到postgre…...
C#—Lazy<T> 类型(延迟初始化/懒加载模式)
C# 的 Lazy<T> 类型 Lazy<T> 是 C# 中的一个类,用于实现延迟初始化(懒加载)模式。它提供了一种线程安全的方式来延迟创建大型或资源密集型对象,直到第一次实际需要时才进行初始化。 主要特点 延迟初始化:…...
unity打包安卓时的签名文件jks转换keystore
前言 unity打包安卓时需要的签名文件格式默认是keystore,而有时我们拿到的是jks格式的签名文件,就需要把jks格式文件转换成keystore格式文件。 其实在windows下也可以不转换,在选择签名文件的文件选择框时,把文件扩展名筛选项&a…...
Android audio_policy_configuration.xml加载流程
目录 一、audio_policy_configuration.xml文件被加载流程 1、AudioPolicyService 创建阶段 2、createAudioPolicyManager 实现 3、AudioPolicyManager 构造 4、配置文件解析 loadConfig 5、核心解析逻辑 PolicySerializer::deserialize 二、AudioPolicyConfig类解析 1、…...
AOSP Android14 Launcher3——远程窗口动画关键类SurfaceControl详解
在 Launcher3 执行涉及其他应用窗口(即“远程窗口”)的动画时,例如“点击桌面图标启动应用”或“从应用上滑回到桌面”的过渡动画,SurfaceControl 扮演着至关重要的角色。它是实现这些跨进程、高性能、精确定制动画的核心技术。 …...
iframe下系统访问跨域问题解决办法
问题描述:iframe下嵌入web页面,访问后端接口跨域,导致接口调不通。 产生原因:iframe下,web端访问后端接口时,会优先向后端发送请求方法为OPTIONS的预检测请求,该请求调用不通,导致真…...
Kafka 如何理解Kafka的高可用
一、Kafka高可用核心思想:备胎的自我修养 核心口诀:“别把鸡蛋放在一个篮子里,除非你他妈有100个篮子!” Kafka的高可用设计,本质上就是一场**“分布式备胎大战”**。它的核心逻辑是: “老子不信任任何单…...
11-DevOps-Jenkins Pipeline流水线作业
前面已经完成了,通过在Jenkins中创建自由风格的工程,在界面上的配置,完成了发布、构建的过程。 这种方式的缺点就是如果要在另一台机器上进行同样的配置,需要一项一项去填写,不方便迁移,操作比较麻烦。 解…...
C++学习之游戏服务器开发十一DOCKER的基本使用
目录 1.多实例部署方案 2.容器的概念 3.docker初识 4.docker仓库 5.docker镜像 6.docker容器 7.docker和虚拟机的区别 8.docker命令解释 9.dockerfile构建镜像 10.离线分发镜像 1.多实例部署方案 redis 命令( redis-cli XXXX ) set key value:…...
docker学习笔记2-最佳实践
一、在容器中启动mysql的最佳实践 (一)查找目录 1、mysql的配置文件路径 /etc/mysql/conf.d 2、mysql的数据目录 /var/lib/mysql 3、环境变量 4、端口 mysql的默认端口3306。 (二)启动命令 docker run -d -p 3306:3306 …...
【TeamFlow】4.2 Yew库详细介绍
Yew 是一个用于构建高效、交互式前端 Web 应用程序的现代 Rust 框架,它借鉴了 React 和 Elm 等框架的设计理念,同时充分利用 Rust 的语言特性。 核心特性 基于组件的架构 Yew 采用组件化开发模式,类似于 React: 组件是可重用的 UI 构建块 …...
第六章.java集合与泛型
文章目录 1.集合框架1. Collection 接口存储一组不唯一,无序的对象2. Set接口存储一组唯一,无序的对象3. Map接口存储一组键值对象,提供key到value的映射 2.封装3.练习题 1.集合框架 java集合框架提供了一套性能优良,使用方便的接口和类,它们位于java.util中 1. Collection 接…...
elastic/go-elasticsearch与olivere/elastic
在 Go 语言中,与 Elasticsearch 交互的客户端库有多种选择,其中 github.com/elastic/go-elasticsearch/v8 和 github.com/olivere/elastic/v7 是两个常用的库。这两个库的功能和用途有一些差异,以下是它们的详细对比: 1. github.c…...
MYSQL之基础认识(卸载安装登录, 基本概念)
一. 卸载安装和登录 卸载 MYSQL 1. 查看有无mysql服务正在运行: ps ajx | grep mysql 2. 查看到 mysql 的服务名称: systemctl list-units --typeservice | grep mysql 3. 关闭 mysql 服务 4. 卸载 dpkg -l | grep mysql | awk {print $2} | xargs sudo apt remove --purg…...
Sentinel源码—7.参数限流和注解的实现一
大纲 1.参数限流的原理和源码 2.SentinelResource注解的使用和实现 1.参数限流的原理和源码 (1)参数限流规则ParamFlowRule的配置Demo (2)ParamFlowSlot根据参数限流规则验证请求 (1)参数限流规则ParamFlowRule的配置Demo 一.参数限流的应用场景 二.参数限流规则的属性 …...
JAVA:利用 Apache Tika 提取文件内容的技术指南
1、简述 Apache Tika 是一个强大的工具,用于从各种文件中提取内容和元数据。📄Tika 支持解析文档、📸图像、🎵音频、🎥视频文件以及其他多种格式,非常适合构建🔍搜索引擎、📂内容管理系统和📊数据分析工具。 样例代码:https://gitee.com/lhdxhl/springboot-…...
SVM(支持向量机)
SVM(支持向量机) 原理 SVM的核心目标是找到一个最大化分类间隔的超平面,将不同类别的样本分隔开。其原理可分为三部分: 线性可分情况 通过硬间隔最大化确定超平面,确保所有样本正确分类且间隔最大间隔定义为超平面到最…...
Spark,hadoop的组成
(一)Hadoop的组成 对普通用户来说, Hadoop就是一个东西,一个整体,它能给我们提供无限的磁盘用来保存文件,可以使用提供强大的计算能力。 在Hadoop3.X中,hadoop一共有三个组成部分&#…...
数据结构中的各种排序
排序之冒泡排序 原理:比较相邻的元素,将大的元素放右边,小的元素放左边。每一趟排的最后的元素一定是最大的元素,所有下一趟需要排列的元素可减少一个 public int[] bubbleSort(int[] attr) {for (int i 0; i < attr.length…...
Android 中实现 GIF 图片动画
在 Android 中,ImageView 从 Android 9.0(API 级别 28) 开始原生支持 GIF 动画,通过 AnimatedImageDrawable 类实现。在之前的版本中,ImageView 并不支持直接播放 GIF 动画,只能显示 GIF 的第一帧。 一、 …...