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继承该类即可。
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/75663.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

辅助查询是根据查询到的文档片段再去生成新的查询问题

&#x1f4a1; 辅助查询是怎么来的&#xff1f; 它是基于你当前查询&#xff08;query&#xff09;检索到的某个文档片段&#xff08;chunk_result&#xff09;&#xff0c;再去“反推”出新的相关问题&#xff08;utility queries&#xff09;&#xff0c;这些问题的作用是&a…

2025 年 4 月补丁星期二预测:微软将推出更多 AI 安全功能

微软正在继续构建其 AI 网络安全战略&#xff0c;并于本月宣布在 Microsoft Security Copilot 中引入新代理。 他们引入了用于网络钓鱼分类的代理、用于数据丢失预防和内部风险管理的警报分类、条件访问优化、漏洞修复和威胁情报简报。 这些代理的目标是不断从这些不同学科中…

【LLM系列】1.大模型简介

1. 基础 1.1 如何权衡模型的复杂度和性能&#xff1f; ├── a. 模型架构选择 │ ├── 简化架构 │ │ └── 选择较小的网络层数和宽度&#xff0c;降低复杂度&#xff1b; │ │ 可使用高性能基础模型如 Transformers 作为起点&#xff0c;根据需求缩放模型。 │ └──…

【leetcode】记录与查找:哈希表的题型分析

前言 &#x1f31f;&#x1f31f;本期讲解关于力扣的几篇题解的详细介绍~~~ &#x1f308;感兴趣的小伙伴看一看小编主页&#xff1a;GGBondlctrl-CSDN博客 &#x1f525; 你的点赞就是小编不断更新的最大动力 &#x1f386;那么废话不…

优选算法的妙思之流:分治——快排专题

专栏&#xff1a;算法的魔法世界 个人主页&#xff1a;手握风云 目录 一、快速排序 二、例题讲解 2.1. 颜色分类 2.2. 排序数组 2.3. 数组中的第K个最大元素 2.4. 库存管理 III 一、快速排序 分治&#xff0c;简单理解为“分而治之”&#xff0c;将一个大问题划分为若干个…

二叉树的ACM板子(自用)

package 二叉树的中序遍历;import java.util.*;// 定义二叉树节点 class TreeNode {int val; // 节点值TreeNode left; // 左子节点TreeNode right; // 右子节点// 构造函数TreeNode(int x) {val x;} }public class DMain {// 构建二叉树&#xff08;层序遍历方式&…

Linux常用命令详解:从基础到进阶

目录 一、引言 二、文件处理相关命令 &#xff08;一&#xff09;grep指令 &#xff08;二&#xff09;zip/unzip指令 ​编辑 &#xff08;三&#xff09;tar指令 &#xff08;四&#xff09;find指令 三、系统管理相关命令 &#xff08;一&#xff09;shutdown指…

Qt多线程从基础到性能优化

一、为什么需要多线程开发 现代应用程序的性能需求 CPU多核架构的有效利用 复杂任务的解耦与响应式界面保持 二、Qt线程创建四大方式 1. 继承QThread重写run() class WorkerThread : public QThread {void run() override {// 耗时操作qDebug() << "Thread ID…

【java】在 Java 中,获取一个类的`Class`对象有多种方式

在 Java 中&#xff0c;获取一个类的Class对象有多种方式。Class对象代表了 Java 中的一个类或接口的运行时类信息&#xff0c;可以用于反射操作。以下是获取Class对象的几种常见方法&#xff1a; 1.使用.class属性 每个类都有一个.class属性&#xff0c;可以直接获取该类的Cl…

什么是RPC通信

RPC&#xff08;Remote Procedure Call&#xff0c;远程过程调用&#xff09;通信是一种允许程序像调用本地函数一样调用远程服务器上函数的通信技术。它简化了分布式系统中的网络交互&#xff0c;隐藏了底层网络通信的复杂性&#xff0c;使开发者能够专注于业务逻辑。 一、RPC…

还是主题混合程序设计

以下是针对您现有代码的完整主题化改造方案&#xff0c;实现跨QML/Qt Widgets的阴影主题系统&#xff1a; 一、主题管理系统核心 // thememanager.h #pragma once #include <QObject> #include <QColor> #include <QMap> #include <QQmlEngine>class…

BT-Basic函数之首字母T

BT-Basic函数之首字母T 文章目录 BT-Basic函数之首字母Ttabtesttest conttest monitortest on boardstest scanworkstest shortstesthead cleanuptesthead configurationtesthead istesthead power on/offtesthead statustestjet print level istestordertestplan generationth…

7-9 趣味游戏

题目解析 在某个学校的趣味游戏活动中&#xff0c;N 名同学站成一排&#xff0c;他们的年龄恰好是 1 到 N &#xff0c;需要注意的是他们并不是按照年龄的大小排列的&#xff0c;而是随机排列的。 游戏的规则是请同学们快速计算出&#xff0c;如果在这 N 名同学的小组中&…

Hugging Face模型微调训练(基于BERT的中文评价情感分析)

文章目录 学习视频地址项目地址数据集的下载模型微调的基本概念与流程加载数据集数据集格式数据集信息 制作Dataset数据集字段数据集信息 vocab字典操作词汇表文本转换 下游任务模型设计模型训练与保存数据加载优化器训练循环 最终效果评估与测试模型加载和测试 学习视频地址 …

【蓝桥杯】十五届省赛B组c++

目录 前言 握手问题 分析 排列组合写法 枚举 小球反弹 分析 代码 好数 分析 代码 R 格式 分析 代码 宝石组合 分析 代码 数字接龙 分析 代码 拔河 分析 代码 总结 前言 主播这两天做了一套蓝桥杯的省赛题目&#xff08;切实感受到了自己有多菜&#x…

必刷算法100题之计算右侧小于当前元素的个数

题目链接 315. 计算右侧小于当前元素的 个数 - 力扣&#xff08;LeetCode&#xff09; 题目解析 计算数组里面所有元素右侧比它小的数的个数, 并且组成一个数组,进行返回 算法原理 归并解法(分治) 当前元素的后面, 有多少个比我小(降序) 我们要找到第一比左边小的元素, 这…

Hyperlane框架:下一代高性能Rust Web框架 [特殊字符]

Hyperlane框架&#xff1a;下一代高性能Rust Web框架 &#x1f680; 引言 &#x1f44b; 在当今快速发展的Web开发领域&#xff0c;性能和开发效率的平衡变得越来越重要。Hyperlane作为一个新兴的Rust Web框架&#xff0c;完美地解决了这个问题。本文将带您深入了解Hyperlane…

图像处理:使用Numpy和OpenCV实现傅里叶和逆傅里叶变换

文章目录 1、什么是傅里叶变换及其基础理论 1.1 傅里叶变换 1.2 基础理论 2. Numpy 实现傅里叶和逆傅里叶变换 2.1 Numpy 实现傅里叶变换 2.2 实现逆傅里叶变换 2.3 高通滤波示例 3. OpenCV 实现傅里叶变换和逆傅里叶变换及低通滤波示例 3.1 OpenCV 实现傅里叶变换 3.2 实现逆傅…

OpenEuler/CentOS一键部署OpenGauss数据库教程(脚本+视频)

&#x1f4cc;OpenEuler/CentOS一键安装OpenGauss数据库教程 为什么需要OpenGauss一键安装脚本&#xff1f; 手动部署OpenGauss数据库时&#xff0c;环境适配、依赖冲突等问题常让开发者头疼。尤其对新人而言&#xff0c;官方文档的配置步骤可能耗时数小时甚至引发未知报错。 …

如何解决 Hive 在创建 MySQL 表时出现乱码???的问题

1.问题描述 我们启动Hive建立一个学生students表格 使用desc students;查看表格结构时 发现有出现乱码的情况 2.解决方案 打开Hive安装机器上面的MySQL 切换到Hive数据库 执行以下命令修改字段注释字符集 mysql -u root -p123456;use hive;alter table COLUMNS_V2 modify col…