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

Socket编程UDP

Socket编程UDP

  • 1、V1版本——EchoServer
  • 2、网络命令
    • 2.1、ping
    • 2.2、netstat
    • 2.3、pidof
  • 3、验证UDP——Windows作为client访问Linux
  • 4、V2版本——DictServer
  • 5、V3版本——简单聊天室

1、V1版本——EchoServer

在这里插入图片描述
首先给出EchoServer目录结构:服务器的类我们实现在UdpServer.hpp中,然后在UdpServerMain.cc中启动服务器。客户端相关代码我们就直接在UdpClientMain.cc中实现了,如果有兴趣后续你可以自己在UdpClient.hpp中封装,常用的部分我们放在Common.hpp中,然后将直接写的策略模式日志拿过来方便测试,最后使用make/makefile来自动化构建项目。


1、创建套接字:

在这里插入图片描述
在这里插入图片描述
使用socket创建套接字,第一个参数domain表示域或协议家族,AF_INET表示网络通信、AF_UNIX表示本地通信,我们直接使用AF_INET。
在这里插入图片描述
第二个参数type,我们只关注两个:SOCK_STREAM和SOCK_DGRAM。
SOCK_STREAM:提供有序的、可靠的,双向的,建立连接的,面向字节流,这个就是TCP通信。
SOCK_DGRAM:提供不可靠的,无连接的,面向数据报的,这个就是UDP通信。
今天我们写的是UDP通信,所以第二个参数传SOCK_DGRAM。第三个参数表示协议,可以传,不过通过前两个参数就可以确定了,所以我们默认传0就行。

在这里插入图片描述
成功返回文件描述符,失败返回-1错误码被设置。

我们实现一个UdpServer的类,创建套接字后会返回文件描述符,所以我们需要一个int类型的sockfd变量,同时之后还需要设置服务器的IP和端口,另外我们再加一个变量判断服务器是否启动。所以代码如下:先创建套接字

// Common.hpp
#pragma once#define Die(code) do{ exit(code); }while(0)enum{USAGE_ERR = 1,SOCKET_ERR,BIND_ERR
};
// UdpServer.hpp
#pragma once#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include "Common.hpp"
#include "Log.hpp"using namespace LogModule;const static int gsockfd = -1;
const std::string gdefaultip = "127.0.0.1"; // 本地环回地址
const uint16_t gdefaultport = 8080; class UdpServer
{
public:UdpServer(const std::string& ip = gdefaultip, const uint16_t port = gdefaultport):_sockfd(gsockfd),_ip(ip),_port(port),_isrunning(false){}void InitServer(){_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){LOG(LogLevel::FATAL) << "socket: " << strerror(errno);Die(SOCKET_ERR);}LOG(LogLevel::INFO) << "socket success, sockfd is: " << _sockfd;}void Start(){}~UdpServer(){}
private:int _sockfd;        // 创建socket返回的fdstd::string _ip;    // 服务器IPuint16_t _port;     // 服务器端口号bool _isrunning;    // 服务是否启动
};

在这里插入图片描述
127.0.0.1表示本地环回地址,仅用于本地内部通信。本地环回地址(Loopback Address)是计算机网络中用于测试本地网络协议栈的保留IP地址。发送到该地址的数据包不会离开主机,而是直接返回给本地系统,常用于网络软件开发和故障排查。在构造函数中我们给了缺省值,默认端口号就是8080。

注意:云服务器需要添加防火墙规则,否则后面无法实现通信效果,以下分别是腾讯云和阿里云的添加示例:
在这里插入图片描述
在这里插入图片描述


2、填充网络信息并进行bind:

在这里插入图片描述
在这里插入图片描述
使用bind进行绑定,第一个参数sockfd就是调用socket返回的文件描述符,第二个参数const struct sockaddr*是输入型参数,我们需要在外面将一个struct sockaddr_in对象填充好传进去,第三个参数表示我们传入对象的大小。这个函数会将sockfd我们传入的struct sockaddr_in绑定。
bind成功返回0,失败返回-1错误码被设置。

而使用struct sockaddr_in这个结构我们需要包含两个头文件:
在这里插入图片描述
在这里插入图片描述
我们先看一下sockaddr_in这个结构,第一个成员变量是个宏,将sin_传过去,进行宏替换就变成了:sa_family_t sin_family表示协议家族。第二个sin_port表示的是端口号,实际上就是uint16_t,16位整数类型。第三个参数是一个结构体,里面是一个uint32_t的32位整数类型。

而进行网络通信需要保证网络字节序——大端。所以设置struct sockaddr_in结构体我们还需要以下接口:
在这里插入图片描述
htons表示主机转网络,h就是host,n就是network,s表示short16位整数,l表示long32位整数。网络转主机我们使用ntohs。

我们使用当前ip地址是字符串风格的ip地址,所以需要先将字符串风格ip地址转换成4字节的整数ip,然后再将4字节整数ip转换成网络字节序。
在这里插入图片描述
我们直接使用ient_addr函数,它可以帮助我将字符串风格的ip地址转换成四字节整数ip并且转换成网络字节序。

另外,在对struct sockaddr_in结构进行设置的时候,需要先将结构里的字段全部清0,我们使用bzero函数。
在这里插入图片描述
第一个参数我们把地址传进去,第二个参数表示该类型的大小。

下面实现设置网络信息并绑定:

// Common.hpp
#pragma once#define Die(code) do{ exit(code); }while(0)#define CONV(v) (struct sockaddr*)(v)enum{USAGE_ERR = 1,SOCKET_ERR,BIND_ERR
};
// UdpServer.hpp
void InitServer()
{// 1.创建套接字_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){LOG(LogLevel::FATAL) << "socket: " << strerror(errno);Die(SOCKET_ERR);}LOG(LogLevel::INFO) << "socket success, sockfd is: " << _sockfd;// 2.填充网络信息并绑定struct sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = ::htons(_port);local.sin_addr.s_addr = ::inet_addr(_ip.c_str());int n = ::bind(_sockfd, CONV(&local), sizeof(local));if (n < 0){LOG(LogLevel::FATAL) << "bind: " << strerror(errno);Die(BIND_ERR);}LOG(LogLevel::INFO) << "bind success";
}
// UdpServerMain.cc
#include "UdpServer.hpp"
#include <memory>int main()
{std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>();svr_uptr->InitServer();svr_uptr->Start();return 0;
}

在这里插入图片描述


3、实现Start,服务器接受客户端消息,同时把接受到的消息返回给客户端。

UDP是全双工的,既可以收又可以发。
在这里插入图片描述
在这里插入图片描述
使用recvfrom接受客户端发送过来的数据,sockfd表示创建socket的返回值文件描述符,buf表示要将数据存储到哪个数组,len表示数组大小,flags设置为0表示阻塞发送。
我们也要知道是谁给服务器发送消息,所以src_addr是输出型参数,通过这个输出型参数可以将服务器的IP和端口号带出来,addrlen表示src_addr结构的大小的地址。
成功返回接收到的字节数,失败返回-1错误码被设置。

在这里插入图片描述
在这里插入图片描述
发送数据使用sendto函数,第一个参数就是创建socket的文件描述符,buf表示要发送的缓冲区数组,len表示发送的数据大小,flags设置为0表示阻塞式发送,dest_addr表示发送目标主机的信息,addrlen表示dest_addr结构的大小。
调用成功返回发送的字节数,失败返回-1错误码被设置。

我们Start实现的逻辑就是服务器接收到数据后,将数据输出到显示器上,同时把IP和端口号都显示出来,然后再给客户端将数据发送回去。
由于要输出客户端的IP和端口号,所以需要将网络字节序转主机序列。端口号转换我们直接使用ntohs函数,IP地址转换我们使用下面这个函数:

在这里插入图片描述

// UdpServer.hpp
#pragma once#include <iostream>
#include <cstring>
#include <string>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Common.hpp"
#include "Log.hpp"using namespace LogModule;const static int gsockfd = -1;
const std::string gdefaultip = "127.0.0.1"; // 本地环回地址
const uint16_t gdefaultport = 8080; class UdpServer
{
public:UdpServer(const std::string& ip = gdefaultip, const uint16_t port = gdefaultport):_sockfd(gsockfd),_ip(ip),_port(port),_isrunning(false){}void InitServer(){// 1.创建套接字_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){LOG(LogLevel::FATAL) << "socket: " << strerror(errno);Die(SOCKET_ERR);}LOG(LogLevel::INFO) << "socket success, sockfd is: " << _sockfd;// 2.填充网络信息并绑定struct sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = ::htons(_port);local.sin_addr.s_addr = ::inet_addr(_ip.c_str());int n = ::bind(_sockfd, CONV(&local), sizeof(local));if (n < 0){LOG(LogLevel::FATAL) << "bind: " << strerror(errno);Die(BIND_ERR);}LOG(LogLevel::INFO) << "bind success";}void Start(){_isrunning = true;while (true){char inbuffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer);ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer)-1, 0, CONV(&peer), &len); if (n > 0){inbuffer[n] = 0;std::string clientip = ::inet_ntoa(peer.sin_addr);uint16_t clientport = ::ntohs(peer.sin_port);LOG(LogLevel::INFO) << clientip << ":" << clientport << "# " << inbuffer;}std::string echo_string = "echo# ";echo_string += inbuffer;::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;        // 创建socket返回的fdstd::string _ip;    // 服务器IPuint16_t _port;     // 服务器端口号bool _isrunning;    // 服务是否启动
};

4、实现客户端:

// UdpClientMain.cc
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Common.hpp"// ./client_udp serverip serverport
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]);// 1.创建套接字int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cerr << "socket error" << std::endl;Die(SOCKET_ERR);}// 2.填充服务器信息struct sockaddr_in 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());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));(void)n;struct sockaddr_in tmp;socklen_t len = sizeof(tmp);char buffer[1024];n = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, CONV(&tmp), &len);if (n > 0){buffer[n] = 0;std::cout << buffer << std::endl;}}return 0;
}

再来谈细节:
在这里插入图片描述
我们希望客户端启动的方式如mian函数上方的注释,通过命令行参数将服务器ip和端口带进来。

客户端也必须有自己的ip和port,但是客户端不需要自己显示的调用bind。而是在客户端首次调用sendto发送消息的时候由操作系统进行bind。
1、如何理解client自动随机bind端口号?假设如今有两个客户端抖音和淘宝,这两个客户端都显示绑定4000端口号,那么这样就会导致其中一个客户端进程无法启动,因为一个端口号只能被一个进程绑定,而一个进程可以绑定多个端口号。
2、如何理解server要显示的bind?因为服务器的端口号必须稳定,必须是众所周知的且不能随意改变。

接下来编译分别运行服务端和客户端程序:

在这里插入图片描述
右侧我们使用netstat -nau查看启动的udp通信,可以看到有一个本地环回地址和端口号为8080的服务,然后远端地址为全0表示可以接受来自所有ip地址的客户端消息。
可以看到本地客户端和服务器的通信就实现了。


但是我们不是要进行网络通信吗?所以下面我们将服务器程序改为类似客户端的启动方式:

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

再次运行:
在这里插入图片描述
绑定失败,这是因为云服务器禁止绑定公网IP,而虚拟机可以绑定你的任何IP。
之所以这样是因为云服务器一般会有服务在跑,一台主机可能有两张网卡,那就有多个IP地址,如果只绑定了一个IP地址,那么该进程就只能收到这个IP地址的报文,但是客户端可能通过多个IP地址同一个端口号发送数据。

所以服务器并不需要IP地址,那么填充网络信息的时候就可以这么写:
在这里插入图片描述
直接将sin_addr.s_addr设置为INADDR_ANY,INADDR_ANY本质就是0。这样表示以后不管客户端发送给哪个IP地址,只要是这个端口号的那都会接受。

下面对UdpServer.hpp和UdpServerMain.cc进行修改:

// UdpServer.hpp
#pragma once#include <iostream>
#include <cstring>
#include <string>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Common.hpp"
#include "Log.hpp"using namespace LogModule;const static int gsockfd = -1;
// const std::string gdefaultip = "127.0.0.1"; // 本地环回地址
const uint16_t gdefaultport = 8080; class UdpServer
{
public:UdpServer(const uint16_t port = gdefaultport):_sockfd(gsockfd),_port(port),_isrunning(false){}void InitServer(){// 1.创建套接字_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){LOG(LogLevel::FATAL) << "socket: " << strerror(errno);Die(SOCKET_ERR);}LOG(LogLevel::INFO) << "socket success, sockfd is: " << _sockfd;// 2.填充网络信息并绑定struct sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = ::htons(_port);// local.sin_addr.s_addr = ::inet_addr(_ip.c_str());local.sin_addr.s_addr = INADDR_ANY;int n = ::bind(_sockfd, CONV(&local), sizeof(local));if (n < 0){LOG(LogLevel::FATAL) << "bind: " << strerror(errno);Die(BIND_ERR);}LOG(LogLevel::INFO) << "bind success";}void Start(){_isrunning = true;while (true){char inbuffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer);ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer)-1, 0, CONV(&peer), &len); if (n > 0){inbuffer[n] = 0;std::string clientip = ::inet_ntoa(peer.sin_addr);uint16_t clientport = ::ntohs(peer.sin_port);LOG(LogLevel::INFO) << clientip << ":" << clientport << "# " << inbuffer;std::string echo_string = "echo# ";echo_string += inbuffer;::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;        // 创建socket返回的fd// std::string _ip;    // 服务器IPuint16_t _port;     // 服务器端口号bool _isrunning;    // 服务是否启动
};
// UdpServerMain.cc
#include "UdpServer.hpp"
#include <memory>// ./server_udp localip localport
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;
}

运行结果:
在这里插入图片描述

下面我使用另一台机器进行测试:
在这里插入图片描述
如图,实现了两台不同主机进行通信。


封装实现InetAddr,快速实现主机转网络:

// InetAddr.hpp
#pragma once#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Common.hpp"class InetAddr
{void PortNet2Host(){_port = ::ntohs(_net_addr.sin_port);}void IpNet2Host(){char ipbuffer[64];_ip = ::inet_ntop(AF_INET, &_net_addr.sin_addr, ipbuffer, sizeof(ipbuffer));}public:InetAddr(){}InetAddr(const 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;
};
// UdpServer.hpp
#pragma once#include <iostream>
#include <cstring>
#include <string>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"using namespace LogModule;const static int gsockfd = -1;
// const std::string gdefaultip = "127.0.0.1"; // 本地环回地址
const uint16_t gdefaultport = 8080; class UdpServer
{
public:UdpServer(const uint16_t port = gdefaultport):_sockfd(gsockfd),_addr(port),_isrunning(false){}void InitServer(){// 1.创建套接字_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){LOG(LogLevel::FATAL) << "socket: " << strerror(errno);Die(SOCKET_ERR);}LOG(LogLevel::INFO) << "socket success, sockfd is: " << _sockfd;// 2.填充网络信息并绑定// struct sockaddr_in local;// bzero(&local, sizeof(local));// local.sin_family = AF_INET;// local.sin_port = ::htons(_port);// local.sin_addr.s_addr = ::inet_addr(_ip.c_str());// local.sin_addr.s_addr = INADDR_ANY;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 inbuffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer);ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer)-1, 0, CONV(&peer), &len); if (n > 0){inbuffer[n] = 0;// std::string clientip = ::inet_ntoa(peer.sin_addr);// uint16_t clientport = ::ntohs(peer.sin_port);InetAddr cli(peer);std::string clientinfo = cli.Ip() + ":" + std::to_string(cli.Port()) + " # " + inbuffer;LOG(LogLevel::INFO) << clientinfo;std::string echo_string = "echo# ";echo_string += inbuffer;::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;        // 创建socket返回的fdInetAddr _addr;     // std::string _ip;    // 服务器IP// uint16_t _port;     // 服务器端口号bool _isrunning;    // 服务是否启动
};

在这里插入图片描述

在这里插入图片描述
上面我们网络转主机IP使用的inet_ntop,前面使用的inet_ntoa返回值是静态数组,所以如果下次再调用就会把里面的值改了,不是线程安全的。所以我们使用inet_ntop自己传入一个缓冲区,inet_ntop会将转换后的字符串IP放在传入的dst数组里面。


2、网络命令

2.1、ping

有时候连不上服务器可能就是你的网络有问题,或者有时候服务器启动了,却接受不到客户端的数据也有可能是网络的问题。所以我们可以使用ping命令来测试网络是否联通:
在这里插入图片描述
出现以上信息说明网络联通,但是这样需要ctrl c终止掉。

可以给ping命令带-c选项,可以指定次数,比如指定3次:
在这里插入图片描述




2.2、netstat

netstat是一个用来查看网络状态的重要工具。
语法:netstat [选项]
功能:查看网络状态。

1、带-u选项表示显示udp相关选项。
2、带-a选项表示显示所有选项,默认不显示LESTEN相关。

在这里插入图片描述

3、带-p选项表示显示建立相关连接的程序名
在这里插入图片描述
其他的看不到是因为其他可能是系统起的,也可能是root账户起的,如果想看可以使用sudo提权。

4、带-n选项表示拒绝显示别名,能显示数字的全部转换为数字。
在这里插入图片描述

5、带-t选项表示仅显示tcp相关选项。
6、带-l表示仅显示有在Listen的服务。

在这里插入图片描述

使用watch命令每秒执行一次netstat -tlnp:
在这里插入图片描述


2.3、pidof

语法: pidof [进程名]
功能:通过进程名,查看进程id。

在这里插入图片描述

在这里插入图片描述
使用上面这行命令可以杀掉该进行。
分析:使用pidof获取server_udp的进程PID,然后使用管道,因此kill -9的标准输入重定向成文件,现在kill -9从文件里面读。但是由于进程PID需要跟在kill -9后面,所以使用xargs,xargs表示将结果拼接到kill -9后面。


3、验证UDP——Windows作为client访问Linux

#include <iostream>
#include <string>
#include <cstring>
#include <WinSock2.h>
#include <Windows.h>#pragma warning(disable : 4996)     // 去除使用inet_addr的警告
#pragma comment(lib, "ws2_32.lib")  // 指定要链接的库std::string serverip = "47.117.157.14";  // 服务器IP
uint16_t serverport = 8080;				 // 服务器端口号int main()
{WSADATA wsd;  // 定义winsock初始化信息结构体WSAStartup(MAKEWORD(2, 2), &wsd);  // 初始化winsock库SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd == SOCKET_ERROR){std::cout << "socket error" << std::endl;return 1;}struct sockaddr_in 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());while (true){std::cout << "Please Enter@ ";std::string message;std::getline(std::cin, message);sendto(sockfd, message.c_str(), message.size(), 0, (const struct sockaddr*)&server, sizeof(server));struct sockaddr_in tmp;int len = sizeof(tmp);char buffer[1024];int n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&tmp, &len);if (n > 0){buffer[n] = 0;std::string ip = inet_ntoa(tmp.sin_addr);uint16_t port = ntohs(tmp.sin_port);std::cout << "[" << ip << ":" << port << "]" << buffer << std::endl;}}closesocket(sockfd);WSACleanup(); // 清理并释放winsock资源return 0;
}

这里有一份代码,在windows下使用vs实现的一个udp客户端。除了小部分代码不同以外,其他创建socket发送接受的代码都是一样的。

在这里插入图片描述
如图:实现了windows作为客户端访问远程服务器。由于vs和Linux编码不同的问题,如果输入中文就会乱码。


4、V2版本——DictServer

现在我们要实现一个英汉词典,相当于是客户端发送英文单词,服务器接受后查询词典是否有该单词的中文意思,如果有就获取返回给客户端,如果没有就给客户端返回None。
在这里插入图片描述
我首先在该目录下创建了一个dict.txt文件,里面存储了英文和中文的映射信息。

然后我们要实现Dictionary.hpp,Dictionary.hpp里面实现一个Dictionary类,该类存储了英文到中文的映射信息,构造函数打开dict.txt文件读取数据将英文和中文切分出来,然后通过哈希表建立映射。并实现一个翻译接口,传入英文单词查询中文意思返回。

下面是Dictionary.hpp的实现:

#pragma once#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "Log.hpp"
#include "Common.hpp"using namespace LogModule;const std::string gpath = "./";
const std::string gfilename = "dict.txt";
const std::string gsep = ": ";class Dictionary
{bool LoadDictionary(){std::string file = _path + _filename;std::ifstream in(file); // 默认打开以只读打开if (!in.is_open()){LOG(LogLevel::FATAL) << "open file " << file << "error";return false;}std::string line;while (std::getline(in, line))  // getline重载了operator bool{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 = gfilename):_path(path),_filename(filename){LoadDictionary();// Print();}std::string Translate(const std::string& word){auto iter = _dictionary.find(word);if (iter == _dictionary.end()) return "None";return iter->second;}void Print(){for (const auto& iter : _dictionary){std::cout << iter.first << ":" << iter.second << std::endl;}}~Dictionary(){}
private:std::unordered_map<std::string, std::string> _dictionary;std::string _path;std::string _filename;
};

字符串切分函数在Common.hpp中实现:

bool SplitString(const std::string& line, std::string* key, std::string* value, const 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());return true;
}

在UdpServer中添加回调函数:

#pragma once#include <iostream>
#include <cstring>
#include <string>
#include <functional>
#include <memory>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"using namespace LogModule;using func_t = std::function<std::string(const std::string&)>;const static int gsockfd = -1;
// const std::string gdefaultip = "127.0.0.1"; // 本地环回地址
const uint16_t gdefaultport = 8080; class UdpServer
{
public:UdpServer(func_t func, const uint16_t port = gdefaultport):_sockfd(gsockfd),_addr(port),_isrunning(false),_func(func){}void InitServer(){// 1.创建套接字_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){LOG(LogLevel::FATAL) << "socket: " << strerror(errno);Die(SOCKET_ERR);}LOG(LogLevel::INFO) << "socket success, sockfd is: " << _sockfd;// 2.填充网络信息并绑定// struct sockaddr_in local;// bzero(&local, sizeof(local));// local.sin_family = AF_INET;// local.sin_port = ::htons(_port);// local.sin_addr.s_addr = ::inet_addr(_ip.c_str());// local.sin_addr.s_addr = INADDR_ANY;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 inbuffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer);ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer)-1, 0, CONV(&peer), &len); if (n > 0){inbuffer[n] = 0;// std::string clientip = ::inet_ntoa(peer.sin_addr);// uint16_t clientport = ::ntohs(peer.sin_port);InetAddr cli(peer);std::string clientinfo = cli.Ip() + ":" + std::to_string(cli.Port()) + " # " + inbuffer;LOG(LogLevel::INFO) << clientinfo;std::string echo_string = _func(inbuffer);// std::string echo_string = "echo# ";// echo_string += inbuffer;::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;        // 创建socket返回的fdInetAddr _addr;     // std::string _ip;    // 服务器IP// uint16_t _port;     // 服务器端口号bool _isrunning;    // 服务是否启动func_t _func;
};

在UdpServerMain.cc中引入Dictionary.hpp头文件,并创建Dictionary对象,将回调函数通过bind或者lambda传入UdpServer中:

#include "UdpServer.hpp"
#include "Dictionary.hpp"// ./server_udp localip localport
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_sptr = std::make_shared<Dictionary>();// std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>(std::bind(&Dictionary::Translate, //     dict_sptr.get(), std::placeholders::_1), port);std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>([&dict_sptr](const std::string& word){return dict_sptr->Translate(word);}, port);svr_uptr->InitServer();svr_uptr->Start();return 0;
}

程序运行结果:
在这里插入图片描述
如图实现了单词的翻译,使用vs是因为编码不同导致的乱码。

在这里插入图片描述
如图使用bind或者lambda,在UdpServer构造函数对成员变量_func初始化,然后在接收到客户端数据后回调_func函数,并将客户端输入的英文单词传入,这时候就会去调用Dictionary里的Translate函数,然后返回中文意思,再将该字符串发送给客户端。


5、V3版本——简单聊天室

在这里插入图片描述
上面我们实现的是单进程,如果实现多人聊天的话需要将所有人IP保存起来,然后在客户端发送数据服务器接收到之后,需要转发给所有IP,但是如果是单进程来做就比较难受。如果创建子进程呢?创建子进程如果父进程收到数据要转给子进程需要实现进程间通信,有点麻烦。其实我们可以实现一个消息转发模块,服务端收到用户消息就注册到转发模块中,然后再交给线程池去处理。

在这里插入图片描述


相较于V1版本,我们需要引入之前封装的线程池,而线程池又用了互斥量、条件变量、线程模块,所以都需要引入。然后自己实现一个用户管理模块user.hpp。

// User.hpp
#pragma once#include <iostream>
#include <string>
#include <list>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "InetAddr.hpp"
#include "Log.hpp"using namespace LogModule;class UserInterface
{
public:virtual ~UserInterface() = default;virtual void SendTo(int sockfd, const std::string& message) = 0; virtual bool operator==(const InetAddr&) = 0;
};class User : public UserInterface
{
public:User(const InetAddr& id):_id(id){}virtual void SendTo(int sockfd, const std::string& message){LOG(LogLevel::INFO) << "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& id){return _id == id;}~User(){}
private:InetAddr _id;
};// 用户管理
// 观察者模式observer
class UserManager
{
public:UserManager(){}void AddUser(InetAddr& id){for (auto& user_sptr : _online_user){if (*user_sptr == id){LOG(LogLevel::INFO) << "用户已经存在";return;}}LOG(LogLevel::INFO) << "新增该用户: " << id.Addr();_online_user.push_back(std::make_shared<User>(id));}void DelUser(InetAddr& id){}void Router(int sockfd, const std::string& message){for (auto& user_sptr : _online_user){user_sptr->SendTo(sockfd, message);}}~UserManager(){}
private:std::list<std::shared_ptr<UserInterface>> _online_user;
};

首先实现了添加用户和消息转发的功能,这种设计模式为观察者模式,User添加到用户管理中,所有的用户就相当于一个观察者,一旦未来有某种事情发生了,通过router通知用户。

由于添加用户需要判断用户是否存在,遍历链表进行判断,所以抽象类和User类都需要实现operator==重载。

在这里插入图片描述
由于User类重写的虚函数SendTo在发送消息前打印了日志信息,所以InetAddr还需实现一个Addr函数,将用户IP+端口号拼接起来并返回。


在UdpServer中加入回调函数:

#pragma once#include <iostream>
#include <cstring>
#include <string>
#include <functional>
#include <memory>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"using namespace LogModule;
using namespace ThreadPoolModule;const static int gsockfd = -1;
// const std::string gdefaultip = "127.0.0.1"; // 本地环回地址
const uint16_t gdefaultport = 8080; using adduser_t = std::function<void(const InetAddr&)>;
using router_t = std::function<void(int sockfd, const std::string& message)>;
using task_t = std::function<void()>;class UdpServer
{
public:UdpServer(adduser_t adduser, router_t router, const uint16_t port = gdefaultport):_sockfd(gsockfd),_addr(port),_isrunning(false),_adduser(adduser),_router(router){}void InitServer(){// 1.创建套接字_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){LOG(LogLevel::FATAL) << "socket: " << strerror(errno);Die(SOCKET_ERR);}LOG(LogLevel::INFO) << "socket success, sockfd is: " << _sockfd;// 2.填充网络信息并绑定// struct sockaddr_in local;// bzero(&local, sizeof(local));// local.sin_family = AF_INET;// local.sin_port = ::htons(_port);// local.sin_addr.s_addr = ::inet_addr(_ip.c_str());// local.sin_addr.s_addr = INADDR_ANY;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 inbuffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer);ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer)-1, 0, CONV(&peer), &len); if (n > 0){inbuffer[n] = 0;// std::string clientip = ::inet_ntoa(peer.sin_addr);// uint16_t clientport = ::ntohs(peer.sin_port);InetAddr cli(peer);// 添加用户_adduser(cli);std::string clientinfo = "[" + cli.Addr() + "]" + inbuffer;// LOG(LogLevel::INFO) << clientinfo;ThreadPool<task_t>::GetInstance()->Equeue(std::bind(_router, _sockfd, clientinfo));// std::string echo_string = "echo# ";// echo_string += inbuffer;// ::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;        // 创建socket返回的fdInetAddr _addr;     // std::string _ip;    // 服务器IP// uint16_t _port;     // 服务器端口号bool _isrunning;    // 服务是否启动adduser_t _adduser;router_t _router;
};
#include "UdpServer.hpp"
#include "User.hpp"// ./server_udp localip localport
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<UserManager> um = std::make_unique<UserManager>();// 网络模块std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>([&um](const InetAddr& id){ um->AddUser(id); },[&um](int sockfd, const std::string& message){ um->Router(sockfd, message); },port);svr_uptr->InitServer();svr_uptr->Start();return 0;
}

在这里插入图片描述

在UdpServerMain.cc中引入用户管理模块,创建智能指针对象,通过lambda表达式传入UdpServer的构造函数初始化_adduser和_router,在Start函数内部当接收到客户端发来的消息回调_adduser并传入InetAddr对象,里面会判断用户是否已经存在,如果存在就不会添加,不存在就就会添加。然后使用bind再将回调函数_router加入任务队列中,线程就会从任务队列中取任务,然后执行回调方法将消息转发给所有用户。
在这里插入图片描述
运行程序我们发现消息确实可以发送给所有用户了,但是有个问题,我们客户端循环内是先获取用户输入,然后再发送,发送了数据才能接受到客户端转发的数据。因此,我们需要让客户端创建一个新线程来接受服务器发送的数据,然后主线程获取用户输入数据发送给服务器。


下面修改客户端创建新线程来接受服务器发送的数据:

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include "Common.hpp"int sockfd;
struct sockaddr_in server;void* Recver(void* args)
{(void)args;while (true){struct sockaddr_in temp;socklen_t len = sizeof(temp);char buff[1024];int n = ::recvfrom(sockfd, buff, sizeof(buff)-1, 0, CONV(&temp), &len);if (n > 0){buff[n] = 0;std::cout << buff << std::endl;}}return nullptr;
}// ./client_udp serverip serverport
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]);// 1.创建套接字sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cerr << "socket error" << std::endl;Die(SOCKET_ERR);}// 2.填充服务器信息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);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));(void)n;}pthread_join(tid, nullptr);return 0;
}

基于以上实现,现在已经可以进行多个人通信了。但是还有些细节:
1、客户端启动后,需要先获取用户输入的数据然后给服务器发送消息,这样服务器才会将用户信息注册到用户管理模块中,才能接收到其他人发的消息。所以我们应该在启动之后直接让客户端给服务器发送一个消息,这样服务器添加到用户管理模块中,这样哪怕用户不输入也能接收到其他人的消息。
2、我们让客户端ctrl c退出,客户端退出后服务器的用户管理模块中应该将用户信息从用户管理模块中删除掉,也就是上方的DelUser我们还没实现。我们可以对2号信号进行捕捉,然后执行自定义动作,给服务器发送QUIT信息,服务器对接收到的信息进行判断,如果是QUIT说明客户端要退出了将用户信息从管理模块中移除。
3、用户管理模块添加用户、删除用户、遍历所有用户发送消息等是多线程访问的,所以需要加锁保护,我们引入互斥锁封装Mutex.hpp保护。

// User.hpp
#pragma once#include <iostream>
#include <string>
#include <list>
#include <memory>
#include <algorithm>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "InetAddr.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&) = 0;virtual std::string Id() = 0;
};class User : public UserInterface
{
public:User(const InetAddr& id):_id(id){}virtual void SendTo(int sockfd, const std::string& message) override{LOG(LogLevel::INFO) << "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& id) override{return _id == id;}std::string Id() override{return _id.Addr();}~User(){}
private:InetAddr _id;
};// 用户管理
// 观察者模式observer
class UserManager
{
public:UserManager(){}void AddUser(InetAddr& id){LockGuard lockguard(_mutex);for (auto& user_sptr : _online_user){if (*user_sptr == id){LOG(LogLevel::INFO) << "用户已经存在";return;}}LOG(LogLevel::INFO) << "新增该用户: " << id.Addr();_online_user.push_back(std::make_shared<User>(id));PrintUser();}void DelUser(InetAddr& id){LockGuard lockguard(_mutex);auto pos = std::remove_if(_online_user.begin(), _online_user.end(), [&id](std::shared_ptr<UserInterface>& user_sptr ){ return *user_sptr == id; });_online_user.erase(pos, _online_user.end());PrintUser();}void Router(int sockfd, const std::string& message){LockGuard lockguard(_mutex);for (auto& user_sptr : _online_user){user_sptr->SendTo(sockfd, message);}}void PrintUser(){for (auto& user_sptr : _online_user){LOG(LogLevel::DEBUG) << "在线用户->" << "[" << user_sptr->Id() << "]";}}~UserManager(){}
private:std::list<std::shared_ptr<UserInterface>> _online_user;Mutex _mutex;
};
// UdpClientMain.cc
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <signal.h>
#include "Common.hpp"int sockfd;
struct sockaddr_in server;void handler(int signo)
{std::string message = "QUIT";int n = ::sendto(sockfd, message.c_str(), message.size(), 0, CONV(&server), sizeof(server));(void)n;exit(0);
}void* Recver(void* args)
{(void)args;while (true){struct sockaddr_in temp;socklen_t len = sizeof(temp);char buff[1024];int n = ::recvfrom(sockfd, buff, sizeof(buff)-1, 0, CONV(&temp), &len);if (n > 0){buff[n] = 0;std::cout << buff << std::endl;}}return nullptr;
}// ./client_udp serverip serverport
int main(int argc, char* argv[])
{if (argc != 3){std::cerr << "Usage: " << argv[0] << " serverip serverport" << std::endl;Die(USAGE_ERR);}signal(2, handler);std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 1.创建套接字sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cerr << "socket error" << std::endl;Die(SOCKET_ERR);}// 2.填充服务器信息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);std::string msg = "我来了哈!";::sendto(sockfd, msg.c_str(), msg.size(), 0, CONV(&server), sizeof(server));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));(void)n;}pthread_join(tid, nullptr);return 0;
}
// UdpServer.hpp
#pragma once#include <iostream>
#include <cstring>
#include <string>
#include <functional>
#include <memory>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"using namespace LogModule;
using namespace ThreadPoolModule;const static int gsockfd = -1;
// const std::string gdefaultip = "127.0.0.1"; // 本地环回地址
const uint16_t gdefaultport = 8080; using adduser_t = std::function<void(InetAddr&)>;
using deluser_t = std::function<void(InetAddr&)>;
using router_t = std::function<void(int sockfd, const std::string& message)>;
using task_t = std::function<void()>;class UdpServer
{
public:UdpServer(const uint16_t port = gdefaultport):_sockfd(gsockfd),_addr(port),_isrunning(false){}void InitServer(){// 1.创建套接字_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){LOG(LogLevel::FATAL) << "socket: " << strerror(errno);Die(SOCKET_ERR);}LOG(LogLevel::INFO) << "socket success, sockfd is: " << _sockfd;// 2.填充网络信息并绑定// struct sockaddr_in local;// bzero(&local, sizeof(local));// local.sin_family = AF_INET;// local.sin_port = ::htons(_port);// local.sin_addr.s_addr = ::inet_addr(_ip.c_str());// local.sin_addr.s_addr = INADDR_ANY;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, deluser_t deluser, router_t router){_adduser = adduser;_deluser = deluser;_router = router;}void Start(){_isrunning = true;while (true){char inbuffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer);ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer)-1, 0, CONV(&peer), &len); if (n > 0){inbuffer[n] = 0;// std::string clientip = ::inet_ntoa(peer.sin_addr);// uint16_t clientport = ::ntohs(peer.sin_port);InetAddr cli(peer);std::string clientinfo;if (strcmp(inbuffer, "QUIT") == 0){_deluser(cli);clientinfo = "[" + cli.Addr() + "]" + "我走了,你们聊!";}else{// 添加用户_adduser(cli);clientinfo = "[" + cli.Addr() + "]" + inbuffer;// LOG(LogLevel::INFO) << clientinfo;}ThreadPool<task_t>::GetInstance()->Equeue(std::bind(_router, _sockfd, clientinfo));// std::string echo_string = "echo# ";// echo_string += inbuffer;// ::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;        // 创建socket返回的fdInetAddr _addr;     // std::string _ip;    // 服务器IP// uint16_t _port;     // 服务器端口号bool _isrunning;    // 服务是否启动adduser_t _adduser;deluser_t _deluser;router_t _router;
};
// UdpServerMain.cc
#include "UdpServer.hpp"
#include "User.hpp"// ./server_udp localip localport
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<UserManager> um = std::make_unique<UserManager>();// 网络模块std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>(port);svr_uptr->RegisterService([&um](InetAddr& id){ um->AddUser(id); },[&um](InetAddr& id){ um->DelUser(id); },[&um](int sockfd, const std::string& message){ um->Router(sockfd, message); });svr_uptr->InitServer();svr_uptr->Start();return 0;
}

最终效果:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述


最后,我们的UdpServer一般是不让拷贝的,所以可以实现一个nocopy类,让UdpServer继承该类即可。
在这里插入图片描述

相关文章:

Socket编程UDP

Socket编程UDP 1、V1版本——EchoServer2、网络命令2.1、ping2.2、netstat2.3、pidof 3、验证UDP——Windows作为client访问Linux4、V2版本——DictServer5、V3版本——简单聊天室 1、V1版本——EchoServer 首先给出EchoServer目录结构&#xff1a;服务器的类我们实现在UdpServ…...

无人机机体结构设计要点与难点!

一、无人机机体结构设计要点 1. 类型与应用场景匹配 固定翼无人机&#xff1a;需优化机翼升阻比&#xff0c;采用流线型机身降低气动阻力&#xff08;如大展弦比机翼设计&#xff09;。 多旋翼无人机&#xff1a;注重轻量化框架和对称布局&#xff08;如四轴/六轴碳纤维机…...

音视频(一)ZLMediaKit搭建部署

前言 一个基于C11的高性能运营级流媒体服务框架 全协议支持H264/H265/AAC/G711/OPUS/MP3&#xff0c;部分支持VP8/VP9/AV1/JPEG/MP3/H266/ADPCM/SVAC/G722/G723/G729 1&#xff1a;环境 ubuntu22.* ZLMediaKit downlaod:https://github.com/ZLMediaKit/ZLMediaKit or https://g…...

实战 | 餐厅点餐小程序技术解析:SpringBoot + UniApp 高效开发指南

&#x1f5a5;️ 一、系统架构概览 1.1 技术选型 为了确保开发效率和系统稳定性&#xff0c;我们采用以下技术栈&#xff1a; 模块技术选型后台服务SpringBoot MyBatis-Plus MySQL用户端&#xff08;点餐小程序&#xff09;UniApp&#xff08;Vue 语法&#xff09;师傅端&…...

合并相同 patient_id 的 JSON 数据为数组

问题 select patient_id,concat({"itemText":",item_text,","itemValue":",item_value,"}) from hs_patient_groups where active 1;eef41128c47c401abb7f8885a5f9fbdf {"itemText":"旧","itemValue"…...

AI安全:构建负责任且可靠的系统

AI已成为日常生活中无处不在的助力&#xff0c;随着AI系统能力和普及性的扩展&#xff0c;安全因素变得愈发重要。从基础模型构建者到采用AI解决方案的企业&#xff0c;整个AI生命周期中的所有相关方都必须共同承担责任。 为什么AI安全至关重要&#xff1f; 对于企业而言&…...

STM32单片机入门学习——第8节: [3-4] 按键控制LED光敏传感器控制蜂鸣器

写这个文章是用来学习的,记录一下我的学习过程。希望我能一直坚持下去,我只是一个小白,只是想好好学习,我知道这会很难&#xff0c;但我还是想去做&#xff01; 本文写于&#xff1a;2025.04.02 STM32开发板学习——第8节: [3-4] 按键控制LED&光敏传感器控制蜂鸣器 前言开…...

Linux驱动入门——设备树详解

文章目录 一、设备树的引入与作用二、设备树的语法1. Devicetree格式1.1 DTS文件的格式1.2 node的格式1.3 properties的格式 2. dts文件包含dtsi文件3. 常用的属性3.1 #address-cells、#size-cells3.2 compatible3.3 model3.4 status3.5 reg 4. 常用的节点(node)4.1 根节点4.2 …...

Scala集合

Scala集合分为序列Seq、集Set、映射Map&#xff0c;都扩展自Iterable特质&#xff0c;且有可变和不可变版本。不可变集合操作后会返回新对象&#xff0c;可变集合则直接修改原对象。比如数组&#xff0c;不可变数组定义后大小不可变&#xff0c;修改会生成新数组&#xff1b;可…...

阿里云AI Studio 2.0:拖拽搭建企业级智能客服系统

一、平台能力全景 1.1 核心功能矩阵 模块子功能技术指标对话设计可视化流程编排支持50节点类型NLP引擎意图识别准确率行业TOP3&#xff08;92.6%&#xff09;知识管理多源数据接入15格式支持渠道对接全渠道覆盖8大平台SDK 1.2 企业级特性 关键优势&#xff1a; 日均对话承…...

java虚拟机---JVM

JVM JVM&#xff0c;也就是 Java 虚拟机&#xff0c;它最主要的作用就是对编译后的 Java 字节码文件逐行解释&#xff0c;翻译成机器码指令&#xff0c;并交给对应的操作系统去执行。 JVM 的其他特性有&#xff1a; JVM 可以自动管理内存&#xff0c;通过垃圾回收器回收不再…...

您的LarkXR专属顾问上线了!平行云官网新增 AI 小助手,手册同步升级!

遇到LarkXR技术问题&#xff1f;还在手动翻文档&#xff1f; Paraverse平行云官网双升级——AI小助手实时答疑 用户手册智能检索&#xff01; 助您快速定位解决方案&#xff0c;效率全面提升&#xff01; < 01 > AI 小助手—— 您的 LarkXR 智能顾问 欢迎我们的新成员…...

推导Bias² + Variance + σ²_ε

问题的背景 我们有一个真实函数 f ( x ) f(x) f(x) 和基于训练数据 D D D 训练得到的模型 f ^ ( x ; D ) \hat{f}(x;D) f^​(x;D)。对于任意输入 x x x&#xff1a; y y y 是真实的观测值&#xff0c;定义为 y f ( x ) ϵ y f(x) \epsilon yf(x)ϵ&#xff0c;其中 …...

javaSE知识梳理(一)

一.面向对象编程 1.面向对象的基本元素&#xff1a;类(class)和对象 ①类的声明 语法格式&#xff1a; [修饰符] class 类名{属性声明;方法声明; } ②对象的创建(new) 语法格式&#xff1a; //方式1&#xff1a;给创建有名对象 类名 对象名 new 类名();//方式2&#xff1…...

k8s statefulset pod重启顺序

在 Kubernetes 中&#xff0c;StatefulSet 的 Pod 重启顺序由以下规则和机制决定&#xff1a; 1. StatefulSet 的核心设计原则 StatefulSet 旨在管理有状态应用&#xff0c;其核心特性包括&#xff1a; 稳定的唯一标识&#xff1a;Pod 名称格式为 <statefulset-name>-&…...

记录学习的第十九天

现在这篇是记录一下4.1的学习。今天还没开始。 这篇是关于简单的动态规划的题目&#xff0c;思路比较清晰类似。 在这里先说一下有关动态规划的四个步骤&#xff1a; 1.确定子问题 2.确定dp数组的递推关系(dp数组也叫子问题数组) 3.确定求解的计算顺序 4.空间优化(初学者可…...

【实用技巧】电脑重装后的Office下载和设置

写在前面&#xff1a;本博客仅作记录学习之用&#xff0c;部分图片来自网络&#xff0c;如需引用请注明出处&#xff0c;同时如有侵犯您的权益&#xff0c;请联系删除&#xff01; 文章目录 前言下载设置总结互动致谢参考目录导航 前言 在数字化办公时代&#xff0c;Windows和…...

模拟集成电路设计与仿真 : Mismatch

前情提要 此為作者針對 mismatch ,進行資料統整,以便日後查詢原理 1. Mismatch (失配) random offset 靜態消除 : trimming動態消除 : auto zero ,choppingCMRRlinearity 理想差動對只有奇次諧波,沒有偶次諧波,但 mismatch 會引入殘存的偶次諧波PSRR2. Input Offset Volt…...

深度学习查漏补缺:4.数据分布的度量

一、数据分布差异的度量 1.KL散度&#xff08;Kullback-Leibler Divergence&#xff09; 什么是KL散度&#xff1f; KL散度是一种用来衡量两个概率分布之间差异的工具。你可以把它想象成一个“距离测量器”&#xff0c;但它不是传统意义上的距离&#xff08;比如两点之…...

银河麒麟V10 aarch64架构安装mysql教程

国产操作系统 ky10.aarch64 因为是arm架构&#xff0c;故选择mysql8&#xff0c;推荐安装8.0.28版本 尝试8.0.30和8.0.41版本均未成功&#xff0c;原因不明☹️ 1. 准备工作 ⏬ 下载地址&#xff1a;https://downloads.mysql.com/archives/community/ 2. 清理历史环境 不用管…...

【NLP 52、多模态相关知识】

生活应该是美好而温柔的&#xff0c;你也是 —— 25.4.1 一、模态 modalities 常见&#xff1a; 文本、图像、音频、视频、表格数据等 罕见&#xff1a; 3D模型、图数据、气味、神经信号等 二、多模态 1、Input and output are of different modalities (eg&#xff1a; tex…...

[NCTF2019]Fake XML cookbook [XXE注入]

题目源代码 function doLogin(){var username $("#username").val();var password $("#password").val();if(username "" || password ""){alert("Please enter the username and password!");return;}var data "…...

I²C总线高级特性与故障处理分析

IC总线高级特性与故障处理深度分析 目录 1. IC基础回顾 1.1 IC通信基本原理1.2 IC总线时序与协议1.3 寻址方式与读写操作 2. IC高级特性 2.1 多主机模式2.2 时钟同步与伸展2.3 高速模式与Fast-mode Plus2.4 10位寻址扩展 3. IC总线故障与锁死 3.1 断电锁死原理3.2 总线挂起与…...

【力扣hot100题】(039)二叉树的直径

这题在简单题中有点难度&#xff0c;主要是不要把边数和深度搞混&#xff08;我就这样&#xff09;。 我想了很久&#xff0c;发现如果当前节点没有右节点&#xff0c;就将它的右长度设为0&#xff0c;左节点同理&#xff0c;并且在递归是不会加一&#xff0c;而是将加一的操作…...

L2-001 紧急救援

注意题目没有说边的数量&#xff0c;实际最多有5e5条边&#xff0c;开小了第四个样例会错&#xff01;&#xff01;&#xff01; - 思路: Dijkstra 求最短路并且维护路径条数和最大人数。 #include<bits/stdc.h> using namespace std;typedef pair<int, int> pii…...

分组背包问题

与01背包的区别是&#xff0c;多了一个限制条件&#xff0c;将物品打包&#xff0c;每组物品只能用一个 #include <iostream> #include <algorithm>using namespace std;const int N 110;int v[N][N], w[N][N], s[N]; int f[N]; int n, m;int main() {cin >>…...

【工业场景】用YOLOv12实现饮料类别识别

饮料类别识别任务的意义在于帮助人们更快速地识别和区分不同类型的饮料&#xff0c;从而提高消费者的购物体验和满意度。对于商家而言&#xff0c;饮料类别识别可以帮助他们更好地管理库存、优化货架布局和预测销售趋势&#xff0c;从而提高运营效率和利润。此外&#xff0c;饮…...

宽带空时波束形成算法原理及MATLAB仿真

宽带阵列信号处理 空时波束形成、空时波束形成方向图 卫星导航空时抗干扰算法 MATLAB仿真 文章目录 前言一、空时波束形成结构二、空时波束形成方向图三、MATLAB仿真四、MATLAB源代码总结 前言 \;\;\;\;\; 空时波束形成&#xff08;Space-Time Beamforming&#xff09;是一种结…...

406. 根据身高重建队列

链接 406. 根据身高重建队列 - 力扣&#xff08;LeetCode&#xff09; 题目 思路 可能的一个思路是先对高的人进行处理&#xff0c;这样后面插入矮的人时不会影响高的人的位置。比如&#xff0c;如果先处理高个子&#xff0c;那么当插入一个矮个子的时候&#xff0c;前面的高…...

机器学习:人工智能的核心驱动力

近年来&#xff0c;机器学习&#xff08;Machine Learning&#xff0c;简称ML&#xff09;已成为人工智能&#xff08;AI&#xff09;领域的核心驱动力。它不仅广泛应用于图像识别、自然语言处理、医疗诊断等领域&#xff0c;还推动了自动驾驶、智能推荐系统等前沿技术的发展。…...

机器视觉中的传统视觉与深度视觉

工业视觉是现代制造业中一个非常重要的应用领域&#xff0c;广泛用于产品质量检测&#xff0c;尤其是在生产线上。几乎所有的工业产品在流入市场之前&#xff0c;都要经历严格的缺陷检测环节&#xff0c;以确保它们符合质量标准。而在这一过程中&#xff0c;工业视觉检测系统发…...

机器学习开发全流程详解:从数据到部署的完整指南

机器学习项目的成功不仅依赖算法选择&#xff0c;更在于系统化的开发流程。本文详细拆解各阶段的核心任务、工具及注意事项&#xff0c;并通过表格总结帮助开发者快速掌握关键要点。 1. 数据获取与准备 数据是机器学习的基础&#xff0c;需确保数据的质量与代表性。 关键任务…...

春晚魔术[蓝桥]

这里 考虑到N很大则快速幂无法处理&#xff0c;因为它并没有被取模 欧拉定理&#xff1a; 当是质数时 是质数 两边同时取mod&#xff0c; // // Created by yumo_ // #include<bits/stdc.h>using namespace std; #define scin std::cin #define scout std::cout using …...

CF580B Kefa and Company(滑动窗口)

题目描述 Sergei B., the young coach of Pokemons, has found the big house which consists of n flats ordered in a row from left to right. It is possible to enter each flat from the street. It is possible to go out from each flat. Also, each flat is connecte…...

多模态RAG实践:如何高效对齐不同模态的Embedding空间?

目录 多模态RAG实践&#xff1a;如何高效对齐不同模态的Embedding空间&#xff1f; 一、为什么需要对齐Embedding空间&#xff1f; 二、常见的对齐方法与关键技术点 &#xff08;一&#xff09;对比学习&#xff08;Contrastive Learning&#xff09; &#xff08;二&#…...

linux 时钟

chronyc sourcestats 查看所有的源以及那个比较稳定 chronyc tracking 查看当前使用的是那个 ntpstat synchronised to NTP server (119.28.183.184) at stratum 3 time correct to within 57 ms polling server every 1024 s chronyc tracking | grep "Reference ID&quo…...

【leetcode100】每日温度

1、题目描述 给定一个整数数组 temperatures &#xff0c;表示每天的温度&#xff0c;返回一个数组 answer &#xff0c;其中 answer[i] 是指对于第 i 天&#xff0c;下一个更高温度出现在几天后。如果气温在这之后都不会升高&#xff0c;请在该位置用 0 来代替。 示例 1: 输…...

华为交换综合实验——VRRP、MSTP、Eth-trunk、NAT、DHCP等技术应用

一、实验拓扑 二、实验需求 1,内网Ip地址使用172.16.0.0/16分配 2,sw1和SW2之间互为备份 3, VRRP/STP/VLAN/Eth-trunk均使用 4,所有Pc均通过DHCP获取IP地址 5,ISP只能配置IP地址 6,所有电脑可以正常访问IsP路由器环回 三、需求分析 1、设备连接需求 二层交换机&#xff08;LS…...

边缘检测技术现状初探2:多尺度与形态学方法

一、多尺度边缘检测方法 多尺度边缘检测通过在不同分辨率/平滑度下分析图像&#xff0c;实现&#xff1a; 粗尺度&#xff08;大σ值&#xff09;&#xff1a;抑制噪声&#xff0c;提取主体轮廓细尺度&#xff08;小σ值&#xff09;&#xff1a;保留细节&#xff0c;检测微观…...

【JavaScript】十四、轮播图

文章目录 实现一个轮播图&#xff0c;功能点包括&#xff1a; 自动播放鼠标经过暂时播放鼠标离开继续播放点击切换按钮手动切换 div盒子嵌套先写出静态HTML&#xff0c;再使用JS来修改样式和数据&#xff0c;渲染页面&#xff1a; <!DOCTYPE html> <html lang"…...

19信号和槽_信号和槽的基本概念

①Linux 信号 Signal 是系统内部的通知机制. 是进程间通信的方式 &#xff08;给进程发信号kill命令&#xff0c;像情景内存泄漏&#xff0c;管道一端关闭另一端还是读&#xff0c;会给进程发信号&#xff09; ②信号三要素 信号源: 谁发的信号 信号的类型: 哪种类别的信号 信…...

云端革命:数字文明的重构与新生

引言&#xff1a;算力大爆炸时代 2023年&#xff0c;当ChatGPT在全球掀起AI狂潮时&#xff0c;很少有人意识到&#xff0c;支撑这场智能革命的正是背后庞大的云计算基础设施。每天&#xff0c;全球云计算平台处理的数据量超过500EB&#xff0c;相当于5亿部高清电影&#xff1b…...

论文阅读笔记:Denoising Diffusion Implicit Models (4)

0、快速访问 论文阅读笔记&#xff1a;Denoising Diffusion Implicit Models &#xff08;1&#xff09; 论文阅读笔记&#xff1a;Denoising Diffusion Implicit Models &#xff08;2&#xff09; 论文阅读笔记&#xff1a;Denoising Diffusion Implicit Models &#xff08…...

红帽Linux怎么重置密码

完整流程 ●重启操作系统&#xff0c;进入启动界面 ●然后按进入选择项界面 ●找到linux单词开头的那一行&#xff0c;然后移动到该行末尾&#xff08;方向键移动或者使用键盘上的end&#xff09;&#xff0c;在末尾加入rd.break ●按ctrl x进入rd.break模式 ●在该模式下依次…...

关于存储的笔记

存储简介 名称适用场景常见运用网络环境备注块存储高性能、低延迟数据库局域网专业文件存储数据共享共享文件夹、非结构化数据局域网通用对象存储大数据、云存储网盘、网络媒体公网&#xff08;断点续传、去重&#xff09;海量 存储协议 名称协议块存储FC-SAN或IP-SAN承载的…...

java根据集合中对象的属性值大小生成排名

1&#xff1a;根据对象属性降序排列 public static <T extends Comparable<? super T>> LinkedHashMap<T, Integer> calculateRanking(List<ProductPerformanceInfoVO> dataList, Function<ProductPerformanceInfoVO, T> keyExtractor) {Linked…...

蓝桥杯嵌入式16届—— LED模块

使用主板 是STMG431RBT6 STMG431RBT6资源 资源配置表 跳线说明表 引脚状况 PC8~PC15分别对应着LD1~LD8 SN74HC573ADWR 是一种锁存器 当 LE&#xff08;锁存使能&#xff09;为高电平&#xff0c;输出 Q 实时跟随输入 D 的变化。​当 LE 为低电平&#xff0c;输出锁定为最后…...

【IOS webview】源代码映射错误,页面卡住不动

报错场景 safari页面报源代码映射错误&#xff0c;页面卡住不动。 机型&#xff1a;IOS13 技术栈&#xff1a;react 其他IOS也会报错&#xff0c;但不影响页面显示。 debug webpack配置不要GENERATE_SOURCEMAP。 解决方法&#xff1a; GENERATE_SOURCEMAPfalse react-app…...

206. 反转链表 92. 反转链表 II 25. K 个一组翻转链表

leetcode Hot 100系列 文章目录 一、翻转链表二、反转链表 II三、K 个一组翻转链表总结 一、翻转链表 建立pre为空&#xff0c;建立cur为head&#xff0c;开始循环&#xff1a;先保存cur的next的值&#xff0c;再将cur的next置为pre&#xff0c;将pre前进到cur的位置&#xf…...

绘制动态甘特图(以流水车间调度为例)

import matplotlib.pyplot as plt import matplotlib.animation as animation import numpy as np from matplotlib import cm# 中文字体配置&#xff08;必须放在所有绘图语句之前&#xff09; plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] Fa…...