Linux网络编程:UDP 的echo server

目录

前言:

一、服务端的实现

1、创建socket套接字

2、绑定地址信息

3、执行启动程序

二、用户端的实现

总结:


前言:

大家好啊,前面我们介绍了一些在网络编程中的一些基本的概念知识。

今天我们就借着上节课提到的,传输层的UDP协议,来写一个简单的服务端与用户段的通信。

由于我们还有很多知识没有学习,所以这个板块的目的是为了让大家先看一下代码的,接口的使用。

这种协议的使用很多情况都是重复一样的,所以使用多了就记住了。

一、服务端的实现

1、创建socket套接字

我们一般在进行通信时,其实都是服务端与用户端的交流。我们平时使用的微信,QQ这个app,就是用户端,他会与远程的服务端口进行数据的交互。如果你是对其他用户发送的消息,就会把这个消息再传给其他用户。

由于我们目前所学还是比较简陋的,所以这里我们要实现的服务端只需要要求满足做一个echo响应就行了。因为我们没有学具体的协议,所以这里就不对发送的消息做加工处理。

首先,我们需要先定义好我们的一个服务端的头文件,这个还是很基础的,然后声明我们的命名空间,定义一个服务端的类。因为我们之后会使用日志来进行打印,所以我们就先把日志也给包含进来:

#ifndef __UDP_SERVER_HPP__
#define __UDP_SERVER_HPP__#include "log.hpp"using namespace LogModule;namespace UdpServerModule
{class UdpServer{public:UdpServer(){}~UdpServer(){}private:};
}#endif

在去试着写一下我们的Main.cc启动文件。

#include"UdpServer.hpp"using namespace UdpServerModule;
int main()
{std::unique_ptr<UdpServer> svr_ptr=std::make_unique<UdpServer>();//我们先创建一个服务器对象,并用智能指针管理它//那我们是不是要先初始化一下我们的服务器对象呢?svr_ptr->InitServer(); //假设UdpServer类有一个InitServer方法来初始化服务器//初始化好了,我们是不是应该启动我们的服务端。由于服务端一般都是启动了不会停止的,所以我们可以使用while循环svr_ptr->Start();
}

这里我们智能指针对应的头文件加入之后,我们提出了两个概念,初始化服务段与启动我们的服务端。

毫无疑问,这是需要我们在UdpServer.hpp中实现的成员方法。

我们继续来写hpp。首先,我们如何初始化我们的服务端口。就需要先创建我们的socket套接字。

创建 Socket 是网络通信的第一步,因为它是操作系统提供的网络通信端点,负责数据的发送和接收。

这里我们需要用到socket接口:

这个接口的第一个参数是domin,是协议族的意思,决定了 Socket 使用的底层协议和地址格式。

我们一般情况下都填的AF_INET表示IPv4协议族。

 第二个参数是通信类型的意思:

可以看到这连个通信类型刚好符合我们上一篇文章所说的UDP与TCP协议的特点。我们这里使用UDP,所以选择填SOCK_DGRAM。

第三个参数一般不用管,我们默认填0就行。

所以我们就要创建一个socket套接字在我们的服务端初始化时,这个socket的返回值是一个文件描述符,所以本质上其实就像是在给我们创建一个文件。(记得加上所需的头文件:

 #include <sys/socket.h>

 #include<sys/types.h>

由于这个文件描述符我们肯定是后面经常用到的,所以我们这里可以写一个成员变量_socket来记录该文件描述符,并且可以通过这个的值判断是否申请套接字成功。(记得修改一下我们的构造函数)

如果小于0,代表创建失败,我们此时可以通过日志来打印并让服务端执行Die:

LOG(LogLevel::FATAL) << "Failed to create socket" << strerror(errno);
// 如果socket创建失败,我们记录一条FATAL级别的日志,并返回。
Die(1);
#define Die(code) do{exit(code);}while(0)

 否则我们执行LOG打印成功

 LOG(LogLevel::INFO) << "Socket created successfully, sockfd: " << _sockfd;

2、绑定地址信息

当我们创建好一个socket之后,我们必须要把他与我们的地址信息做绑定。 

Socket通信本质上是端点对端点的通信,一个完整的通信端点需要两个要素:

  • 传输协议(由socket()函数指定)

  • 地址标识(由bind()函数指定)

如果不绑定地址,Socket就相当于只有"通话功能"但没有"电话号码",其他主机无法定向发送数据。 

所以我们要介绍一下我们的绑定接口:bind:

可以看出,bind的第一个参数就是我们使用socket返回的文件描述符,他的第二个参数,大家看着是否眼熟呢?

这正是我们在上篇文章所提到的:

这个是包含地址信息的结构体指针,

  • IPv4 使用 struct sockaddr_in

  • IPv6 使用 struct sockaddr_in6

  • 本地 Unix 域套接字使用 struct sockaddr_un

我们这里自然使用的是struct sockaddr_in,我们可以看一下这个结构体的定义:

struct sockaddr_in{__SOCKADDR_COMMON (sin_);in_port_t sin_port;			/* Port number.  */struct in_addr sin_addr;		/* Internet address.  *//* Pad to size of `struct sockaddr'.  */unsigned char sin_zero[sizeof (struct sockaddr)- __SOCKADDR_COMMON_SIZE- sizeof (in_port_t)- sizeof (struct in_addr)];};

根据上面那个图我们可以知道,由上到下分别一一对应,有人说 __SOCKADDR_COMMON (sin_);是什么意思,我们可以继续看这个宏的定义:

#define	__SOCKADDR_COMMON(sa_prefix) \sa_family_t sa_prefix##family

所以转化过来就是sa_family_t sin_family;,这个就代表着我们的地址族。

继续来讲第三个参数,第三个参数就是第二个参数的大小,我们可以通过sizeof来获取。

这里值得一提的是,第二个参数是一个输入型参数,所以我们需要先定义一个sockaddr_in结构体,随后对这个结构体进行初始化填充,把我们的IP地址,端口号这些信息全部填充到这个结构体内。

但是这里有一个问题:

            // 1.创建一个socket_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);// 这里我们使用了C标准库的socket函数来创建一个UDP socket// 注意:在实际的代码中,我们需要检查socket函数的返回值,以确保// socket创建成功。if (_sockfd < 0){LOG(LogLevel::FATAL) << "Failed to create socket" << strerror(errno);// 如果socket创建失败,我们记录一条FATAL级别的日志,并返回。Die(1);}LOG(LogLevel::INFO) << "Socket created successfully, sockfd: " << _sockfd;// 2.绑定地址信息struct sockaddr_in local;local.sin_family = AF_INET; // 设置地址族为IPv4local.sin_port = ;local.sin_addr=;

我们的端口号和IP地址从哪里来?

我们这里可以定义两个默认的参数,代表默认的IP与端口地址,如果不想使用默认的,就要求你从外界传入IP与端口:

static std::string defaultip = "127.0.0.1"; // 默认的IP地址,代表本地IP地址
static uint16_t defaultport = 8080;         // 默认的端口号,用来测试

我们顺便在我们的服务端类中新增两个变量来记录IP地址与该服务端绑定的端口:
 

         UdpServer(const std::string& ip=defaultip,const uint16_t port=defaultport): _sockfd(defaultfd),_ip(ip),_port(port){}
      private:int _sockfd;std::string _ip ;// 默认IP地址uint16_t _port ; // 默认端口号

所以此时我们就给我们的sockaddr_in结构体中的成员初始化这个值:

            // 2.绑定地址信息struct sockaddr_in local;local.sin_family = AF_INET; // 设置地址族为IPv4local.sin_port = _port;// 设置端口号local.sin_addr = _ip;

这里有两个问题,我们先说第一个,首先你的_port是要发送到网络中的,协议规定端口号在报文中必须用网络字节序(大端)传输。所以你这个时候必须进行大小端转化,将主机序列转化为网络序列。

如何转化呢?

我们在上文提到过:

所以这里我们采取htons来进行转化。

但是我们还有一个新的问题,初始化ip地址时报错了,这是为什么呢?

他说类型不匹配,我们查看一下sockaddr_in结构体内部:

发现sin_addr居然是一个结构体,而C语言的结构体不支持赋值。

我们继续查看该结构体in_addr的内容:

struct in_addr{in_addr_t s_addr;};

发现里面就一个成员变量,所以我们只需要把这个成员变量显式初始化就行了。但还是有个问题,我们的IP地址时点分十进制的啊!!所以我们需要对这个ip地址进行转化。

如何将人类可读的点分十进制IP地址(如 "127.0.0.1")转换为网络字节序的二进制形式,并正确赋值给 sockaddr_in 结构体?

我们有这些接口可以使用:

大家有兴趣的可以去查一下,为了简便我们这里就使用inet_addr ,这个接口,用于将点分十进制格式的 IP 地址字符串转换为 32 位网络字节序的二进制值。

这里还有一个究极细节,为了防止结构体填充与未初始化风险,我们在设置sockaddr_in等网络结构体前先进行清零(memsetbzero):

我们这里使用bzero来完成清零的操作:

bzero(&local, sizeof(local)); // 清空结构体

完成对sockaddr_in结构体的设置后,我们就可以使用bind函数来绑定地址了:

bind(_sockfd, (struct sockaddr *)&local, sizeof(local));

 我们可以在下面进行一个if判断,并对结果进行相应的日志输出:

            int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){LOG(LogLevel::FATAL) << "bind: " << strerror(errno);Die(2);}LOG(LogLevel::INFO) << "bind success "; 

所以我们服务端目前的代码如下,大家可以借助注释理解:

#ifndef __UDP_SERVER_HPP__
#define __UDP_SERVER_HPP__#include "log.hpp"
#include <string.h>
#include <string>
#include <memory>#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>using namespace LogModule;#define Die(code)   \do              \{               \exit(code); \} while (0)static int defaultfd = -1;
static std::string defaultip = "127.0.0.1"; // 默认的IP地址,代表本地IP地址
static uint16_t defaultport = 8080;         // 默认的端口号,用来测试namespace UdpServerModule
{class UdpServer{public:UdpServer(const std::string &ip = defaultip, const uint16_t port = defaultport): _sockfd(defaultfd),_ip(ip),_port(port){}~UdpServer(){}void InitServer(){// 1.创建一个socket_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);// 这里我们使用了C标准库的socket函数来创建一个UDP socket// 注意:在实际的代码中,我们需要检查socket函数的返回值,以确保// socket创建成功。if (_sockfd < 0){LOG(LogLevel::FATAL) << "Failed to create socket" << strerror(errno);// 如果socket创建失败,我们记录一条FATAL级别的日志,并返回。Die(1);}LOG(LogLevel::INFO) << "Socket created successfully, sockfd: " << _sockfd;// 2.绑定地址信息struct sockaddr_in local;bzero(&local, sizeof(local)); // 清空结构体// 这里我们使用了bzero函数来清空local结构体,确保没有残留数据,垃圾值local.sin_family = AF_INET;                     // 设置地址族为IPv4local.sin_port = htons(_port);                         // 设置端口号local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 将IP地址转换为网络字节序int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){LOG(LogLevel::FATAL) << "bind: " << strerror(errno);Die(2);}LOG(LogLevel::INFO) << "bind success "; }void Start(){}private:int _sockfd;std::string _ip; // 默认IP地址uint16_t _port;  // 默认端口号};
}#endif
#include"UdpServer.hpp"using namespace UdpServerModule;
int main()
{std::unique_ptr<UdpServer> svr_ptr=std::make_unique<UdpServer>();//我们先创建一个服务器对象,并用智能指针管理它//那我们是不是要先初始化一下我们的服务器对象呢?svr_ptr->InitServer(); //假设UdpServer类有一个InitServer方法来初始化服务器//初始化好了,我们是不是应该启动我们的服务端。由于服务端一般都是启动了不会停止的,所以我们可以使用while循环svr_ptr->Start();
}


3、执行启动程序

目前为止,我们的初始化工作的代码就已经完成了。

目前为止的这些代码都是套路,我们这里就先看一下,把写出来。后面我们讲网络原理这些会懂。

接下来就来实现一下我们的start启动功能。

首先,服务端一般都是启动之后就不会停止的,就像抖音一样,你晚上可以使用,白天也能使用。

所以我们这里就可以使用while循环,由于我们今天的目标只是实现一个简单的echo server,所以我们只需要在服务端接收到用户端的消息,随后打印消息结果,并返回就行了。

在写while循环前我们应该再增加一个成员变量:is_running,表示目前服务器的运行状态。一开始默认为false,在我们执行start后变为true。

        void Start(){is_running = true;while(is_running){}}

我们要接受从用户端发来的消息,应该如何接收呢?

这就要拜托给我们的recvfrom了,recvfrom() 是用于无连接套接字(如 UDP)接收数据的系统调用,它不仅能获取数据,还能获取发送方的地址信息。

参数类型说明
sockfdint套接字文件描述符
bufvoid*接收数据的缓冲区
lensize_t缓冲区长度
flagsint控制接收行为的标志位
src_addrstruct sockaddr*发送方地址信息(可选)
addrlensocklen_t*地址结构体长度指针

 所以为了接收消息,我们需要先自己定义一个缓冲区,以及存储我们发送方地址信息的结构体和长度。这个flag我们使用默认的0就行。

注意最后两个参数是一个输出型参数。

        void Start(){is_running = true;while(is_running){char buffer[1024];struct sockaddr_in peer;//输出型参数socklen_t len =sizeof(peer);//也是一个输出型参数ssize_t n=::recvfrom(_sockfd,buffer,sizeof(buffer),0,(struct sockaddr*)&peer,&len);if(n>0){buffer[n] = '\0'; // 确保字符串以null结尾LOG(LogLevel::INFO) << "client say: " << buffer;}}}

我们这里的sockfd既可以用来发消息,也可以用来收消息,这就是全双工的特性。

所以我们还可以在后面添加一个返回消息的逻辑,这里的返回消息我们用的是 sendto,sendto() 是用于无连接套接字(如 UDP)发送数据的系统调用,它允许指定目标地址。

参数类型说明
sockfdint套接字文件描述符
bufconst void*要发送的数据缓冲区
lensize_t要发送的数据长度
flagsint控制发送行为的标志位
dest_addrconst struct sockaddr*目标地址信息
addrlensocklen_t地址结构体长度

在sendto函数中,后面两个参数是我们要发送的目标的地址信息与参数,而这个参数,我们在使用recvrom的时候就已经获取到peer里了。

        void Start(){is_running = true;while (is_running){char buffer[1024];struct sockaddr_in peer;      // 输出型参数socklen_t len = sizeof(peer); // 也是一个输出型参数ssize_t n = ::recvfrom(_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &len);if (n > 0){buffer[n] = '\0'; // 确保字符串以null结尾LOG(LogLevel::INFO) << "client say: " << buffer;std::string echo_str = "echo:"; // 我们要给客户端回显一条消息echo_str += buffer;// 发送回显消息ssize_t m = ::sendto(_sockfd, echo_str.c_str(), echo_str.size(), 0, (struct sockaddr *)&peer, len);if (m < 0){LOG(LogLevel::ERROR) << "sendto error: " << strerror(errno);}}}}

那么到目前为止,我们的服务端接口就简单的写完了。

我们可以运行一下服务端的接口,随后在新的shell中输入以下指令:

netstat -nuap

可以看见我们的服务端进程已经启动起来了,并且地址就是我们一开始设置的127.0.0.8080.


二、用户端的实现

我们的服务端已经建立起来了,接下来我们要实现的就是用户端的代码。

其实,二者的代码极具相似度,由于时间原因我选择直接复制粘贴一下代码(这也就是为什么我说那些代码都是套路的原因,使用方法顺序几乎一致)

值得一提的是,用户端的类我们要求初始化时必须有的目的地的IP地址与端口号(也就是服务端)

随后我们新增一个类成员变量_server负责记录我们会使用的sockaddr_in结构体数据,并且,在InitClient中我们需要对sockaddr_in进行初始化,这是为了方便我们后面的使用。

最后去除我们InitClient中的bind相关的代码,这是为什么呢?

这是因为客户端不需要自己显示的调用bind!!

客户端首次sendto消息的时候,由OS自动进行bind!!此时操作系统会随机分配一个空闲的端口号!

我们在start中的改变还是比较多的,首先我们需要让客户端先输入消息,也就是可以创建一个string字符串,使用getline接受消息,并且通过sendto发送到目的IP与端口,这里所使用的sockaddr_in结构体正是我们的成员变量_server。

之后,由于服务端调用了sendto,所以我们可以在后面进行recvfrom的使用。

代码整体如下,大家可以看注释帮助理解:

#ifndef __UDP_CLIENT_HPP__
#define __UDP_CLIENT_HPP__#include "log.hpp"
#include <string.h>
#include <string>
#include <memory>#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>using namespace LogModule;#define Die(code)   \do              \{               \exit(code); \} while (0)static int defaultfd = -1;namespace UdpClientModule
{class UdpClient{public:UdpClient(const std::string &ip, const uint16_t port): _sockfd(defaultfd),_ip(ip),_port(port){}~UdpClient(){}void InitClient(){// 1.创建一个socket_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);// 这里我们使用了C标准库的socket函数来创建一个UDP socket// 注意:在实际的代码中,我们需要检查socket函数的返回值,以确保// socket创建成功。if (_sockfd < 0){LOG(LogLevel::FATAL) << "Failed to create socket" << strerror(errno);// 如果socket创建失败,我们记录一条FATAL级别的日志,并返回。Die(3);}// 如果socket创建成功,我们记录一条INFO级别的日志,表示socket创建成功。LOG(LogLevel::INFO) << "Socket created successfully, sockfd: " << _sockfd;// 2.绑定地址信息memset(&_server, 0, sizeof(_server)); // 清空结构体// 这里我们使用了bzero函数来清空local结构体,确保没有残留数据,垃圾值_server.sin_family = AF_INET;                     // 设置地址族为IPv4_server.sin_port = htons(_port);                  // 设置端口号_server.sin_addr.s_addr = inet_addr(_ip.c_str()); // 将IP地址转换为网络字节序// client必须也要有自己的ip和端口!但是客户端,不需要自己显示的调用bind!!// 而是,客户端首次sendto消息的时候,由OS自动进行bind// 1. 如何理解client自动随机bind端口号? 一个端口号,只能被一个进程bind// 2. 如何理解server要显示的bind?服务器的端口号,必须稳定!!必须是众所周知且不能改变轻易改变的!// 如果服务端改变,那么他所服务对接的众多客户端都无法正常运行}void Start(){while (true){std::cout << "Please input your message: ";std::string message;getline(std::cin, message); // 从标准输入读取一行消息// 发送消息ssize_t m = ::sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&_server, sizeof(_server));struct sockaddr_in temp;socklen_t len = sizeof(temp);char buffer[1024];ssize_t n = ::recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)(&temp), &len);if (n > 0){buffer[n] = 0;std::cout << buffer << std::endl;}}}private:int _sockfd;                // socket文件描述符std::string _ip;            // IP地址uint16_t _port;             // 端口号struct sockaddr_in _server; // 我们的类初始化时必须传入目的地的IP与端口};
}#endif

那我们的main.cc文件如何实现呢?我们的客户端的要求运行时必须传入目的地的IP与端口,所以我们需要用到系统学到的知识命令行参数。

#include "UdpClient.hpp"using namespace UdpClientModule;int main(int argc, char *argv[])
{if (argc != 3) // 客户端必须传入我们要发送的目的地的IP和端口号{std::cout << "Usage: ./client ip port" << std::endl;return 1;}std::string ip = argv[1];uint16_t port = std::stoi(argv[2]);std::unique_ptr<UdpClient> client = std::make_unique<UdpClient>(ip, port);client->InitClient();client->Start();return 0;
}

我们先运行服务端,随后运行客户端:

可以看到,我们已经能够实现简单的本地通信了。


三、优化

我们服务端的代码虽然能够正常运行了,但是我们还是觉得不够优美。所以我们可以在优化一下。

首先就是我们的那一大串的各种转换了。

我们想要快速的显示IP地址主机转网络网络转主机,快速得到端口号。

该怎么做呢?

首先我们可以定义一个新的头文件:

InetAddr.hpp,表示我们将在这个头文件中实现一个类,这个类中必须实现我们的各种转换,所以就会包含各种成员函数调用接口。即把源代码中的:

这一部分给优化到一个类中。

所以我们的这个类成员变量必须包含一个sockaddr_in类型的结构体:

#pragma once
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>class InetAddr
{
public:InetAddr(){}~InetAddr(){}private:struct sockaddr_in addr; // 用于存储IP地址和端口号的结构体
};

但是我们实际上看的是点分十进制的IP地址与端口,所以我们还可以新建两个成员变量来表示:

    std::string _ip;uint16_t _port;

我们想要在服务端收消息时知道客户端的IP地址,所以我们重载一个构造函数,使得其支持从外部传进来一个sockaddr_in的结构体给我们的addr初始化,并且在这里面调用相关接口,实现我们的网络端口与IP的网络转主机。

这样一来如果我们在start里的sendto之前,就可以通过struct sockaddr_in初始化的新建一个InetAddr变量,调用此构造函数对我们的IP地址以及端口进行自动化处理,随后我们在通过一些返回调用就能在打印出来。

class InetAddr
{private:void PortNet2Host(){_port=::ntohs(_addr.sin_port);}void IpNet2Host(){char ip[64];::inet_ntop(AF_INET, &_addr.sin_addr, ip, sizeof(ip));_ip = std::string(ip);}
public:InetAddr(){}InetAddr(const struct sockaddr_in &addr):_addr(addr){PortNet2Host(); // 将网络字节序的端口转换为主机字节序IpNet2Host();   // 将网络字节序的IP地址转换为主机字节序}~InetAddr(){}private:struct sockaddr_in _addr; // 用于存储IP地址和端口号的结构体std::string _ip;uint16_t _port;
};

不同于之前,我们在这里将IP地址转换为主机字节序时用到的接口是:

inet_ntop

我们之前所使用的是inet_addr。

这个接口并不是很安全,因为返回值是一个char*指针,在多线程中有错误的风险。

但是inet_ntop就比较安全了,因为我们需要自己创建一个区域来存储地址。在多线程中,名义上是规定了线程的栈区资源是不共享的。 

我们打印是要用到IP地址与端口,所以可以写调用来返回:

    std::string GetIp(){return _ip;}uint16_t GetPort(){return _port;}
  void Start(){is_running = true;while (is_running){char buffer[1024];struct sockaddr_in peer;      // 输出型参数socklen_t len = sizeof(peer); // 也是一个输出型参数ssize_t n = ::recvfrom(_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &len);if (n > 0){InetAddr temp(peer); // 通过上面的peer来进行初始化,这样以来我们就能获取到相关ip地址与端口并打印buffer[n] = '\0';    // 确保字符串以null结尾LOG(LogLevel::INFO) << "client ip: " << temp.GetIp() << ", port: " << temp.GetPort() << "client say: " << buffer;std::string echo_str = "echo:"; // 我们要给客户端回显一条消息echo_str += buffer;// 发送回显消息ssize_t m = ::sendto(_sockfd, echo_str.c_str(), echo_str.size(), 0, (struct sockaddr *)&peer, len);if (m < 0){LOG(LogLevel::ERROR) << "sendto error: " << strerror(errno);}}}}

 这样一来我们也能看见客户端的IP地址了。


由于我们设定的监控任意接口,所以我们在UdpServer.hpp里不需要IP来给我们的成员变量初始化,只需要端口。

所以我们就再重载一个端口版的构造函数 

    InetAddr(const uint16_t port):_port(port),_ip(""){_addr.sin_family=AF_INET; // 设置地址族为IPv4_addr.sin_port=htons(port); // 将端口转换为网络字节序_addr.sin_addr.s_addr=INADDR_ANY; // 设置IP地址为任}

至此,我们只需要在服务端的成员变量中新增InetAddr类型,便可注释掉我们之前的成员变量port与ip,这个端口版的构造函数主要是给我们的成员变量中新增InetAddr类型进行初始化使用的。

要代替的正是我们Init里面的对sockaddr_in结构体进行初始化的代码。

        InetAddr local; // 本地地址信息// std::string _ip; // 默认IP地址// uint16_t _port;  // 默认端口号

另外由于使用bind的时候要获取结构体地址与长度,所以我们可以新加接口在内部返回地址与长度。

 

    struct sockaddr* Getsockaddr(){return (struct sockaddr*)&_addr;}size_t GetSockaddrLen(){return sizeof(_addr);}

代码总体如下:

#pragma once
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>class InetAddr
{private:void PortNet2Host(){_port=::ntohs(_addr.sin_port);}void IpNet2Host(){char ip[64];::inet_ntop(AF_INET, &_addr.sin_addr, ip, sizeof(ip));_ip = std::string(ip);}
public:InetAddr(){}InetAddr(const struct sockaddr_in &addr):_addr(addr){PortNet2Host(); // 将网络字节序的端口转换为主机字节序IpNet2Host();   // 将网络字节序的IP地址转换为主机字节序}InetAddr(const uint16_t port):_port(port),_ip(""){_addr.sin_family=AF_INET; // 设置地址族为IPv4_addr.sin_port=htons(port); // 将端口转换为网络字节序_addr.sin_addr.s_addr=INADDR_ANY; // 设置IP地址为任}~InetAddr(){}struct sockaddr* Getsockaddr(){return (struct sockaddr*)&_addr;}size_t GetSockaddrLen(){return sizeof(_addr);}std::string GetIp(){return _ip;}uint16_t GetPort(){return _port;}private:struct sockaddr_in _addr; // 用于存储IP地址和端口号的结构体std::string _ip;uint16_t _port;
};

UdpServer.hpp涉及到的更改的地方如下:

添加成员变量

  private:int _sockfd;    // socket文件描述符InetAddr local; // 本地地址信息// std::string _ip; // 默认IP地址// uint16_t _port;  // 默认端口号bool is_running; // 服务器是否在运行

 构造函数修改:
 

         UdpServer(const std::string &ip = defaultip, const uint16_t port = defaultport): _sockfd(defaultfd),local(port), // 初始化本地地址信息is_running(false){}

Init函数省略优化:

        void InitServer(){// 1.创建一个socket_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);// 这里我们使用了C标准库的socket函数来创建一个UDP socket// 注意:在实际的代码中,我们需要检查socket函数的返回值,以确保// socket创建成功。if (_sockfd < 0){LOG(LogLevel::FATAL) << "Failed to create socket" << strerror(errno);// 如果socket创建失败,我们记录一条FATAL级别的日志,并返回。Die(1);}// 如果socket创建成功,我们记录一条INFO级别的日志,表示socket创建成功。LOG(LogLevel::INFO) << "Socket created successfully, sockfd: " << _sockfd;// 2.绑定地址信息// struct sockaddr_in local;// bzero(&local, sizeof(local)); // 清空结构体// // 这里我们使用了bzero函数来清空local结构体,确保没有残留数据,垃圾值// local.sin_family = AF_INET;    // 设置地址族为IPv4// local.sin_port = htons(_port); // 设置端口号// // local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 将IP地址转换为网络字节序// local.sin_addr.s_addr = INADDR_ANY; // 绑定到任意IP地址,这样服务器可以接收来自任何IP的消息int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){// 如果bind函数返回小于0,表示绑定失败,我们记录一条FATAL级别的日志,并返回。// 这里我们使用了strerror函数来获取错误信息,并将其记录到日志中。LOG(LogLevel::FATAL) << "bind: " << strerror(errno);Die(2);}// 如果绑定成功,我们记录一条INFO级别的日志,表示绑定成功。LOG(LogLevel::INFO) << "bind success ";}

 statr新增打印客户端IP地址信息:
 

       void Start(){is_running = true;while (is_running){char buffer[1024];struct sockaddr_in peer;      // 输出型参数socklen_t len = sizeof(peer); // 也是一个输出型参数ssize_t n = ::recvfrom(_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &len);if (n > 0){InetAddr temp(peer); // 通过上面的peer来进行初始化,这样以来我们就能获取到相关ip地址与端口并打印buffer[n] = '\0';    // 确保字符串以null结尾LOG(LogLevel::INFO) << "client ip: " << temp.GetIp() << ", port: " << temp.GetPort() << "client say: " << buffer;std::string echo_str = "echo:"; // 我们要给客户端回显一条消息echo_str += buffer;// 发送回显消息ssize_t m = ::sendto(_sockfd, echo_str.c_str(), echo_str.size(), 0, (struct sockaddr *)&peer, len);if (m < 0){LOG(LogLevel::ERROR) << "sendto error: " << strerror(errno);}}}}

 

总结:

希望本文对你有所帮助

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

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

相关文章

AI+金融,如何跨越大模型和场景鸿沟?

文&#xff5c;白 鸽编&#xff5c;王一粟当AI大模型已开始走向千行百业之时&#xff0c;备受看好的金融行业&#xff0c;却似乎陷入了落地瓶颈。打开手机银行想查下贷款额度&#xff0c;对着屏幕说了半天&#xff0c;AI客服却只回复 “请点击首页贷款按钮”&#xff1b;客户经…

深度解析:从零构建跨平台对象树管理系统(YongYong框架——QT对象树机制的现代化替代方案)

一、技术背景与核心价值 1.1 QT对象树的局限性 在Qt框架中&#xff0c;QObject通过对象树机制实现了革命性的对象管理&#xff1a; #mermaid-svg-SvqKmpFjg76R02oL {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-Sv…

力扣46:全排列

力扣46:全排列题目思路代码题目 给定一个不含重复数字的数组 nums &#xff0c;返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。 思路 看到所有可能首先想到的就是回溯。 回溯的结束条件也很好写&#xff0c;用数组的长度来判断即可。这道题的难点主要是如何进行判…

mac环境配置rust

rustup 是一个命令行工具&#xff0c;用于管理 Rust 编译器和相关工具链 sh 体验AI代码助手 代码解读复制代码curl --proto ‘https’ --tlsv1.2 -sSf https://sh.rustup.rs | sh使得 Rust 的安装在当前 shell 环境中生效 如果你使用的是 bash, zsh 或其他类似的 shell&#xf…

脚手架搭建React项目

脚手架搭建项目 1. 认识脚手架工具 1.1. 前端工程的复杂化 1.1.1. 如果只是开发几个小的demo程序&#xff0c;那么永远不要考虑一些复杂的问题&#xff1a; 比如目录结构如何组织划分&#xff1b;比如如何关键文件之间的相互依赖&#xff1b;比如管理第三方模块的依赖&#xff…

Golang 调试技巧:在 Goland 中查看 Beego 控制器接收的前端字段参数

&#x1f41b; Golang 调试技巧&#xff1a;在 Goland 中查看 Beego 控制器接收的前端字段参数 在使用 Beego 开发 Web 项目时&#xff0c;我们常常会在控制器中通过 c.GetString()、c.GetInt() 等方法获取前端页面传过来的字段值。而在调试过程中&#xff0c;如何在 Goland 中…

sqli-labs:Less-2关卡详细解析

1. 思路&#x1f680; 本关的SQL语句为&#xff1a; $sql"SELECT * FROM users WHERE id$id LIMIT 0,1";注入类型&#xff1a;数值型提示&#xff1a;参数id无需考虑闭合问题&#xff0c;相对简单 2. 手工注入步骤&#x1f3af; 我的地址栏是&#xff1a;http://l…

TRAE 软件使用攻略

摘要TRAE 是一款集成了人工智能技术的开发工具&#xff0c;旨在为开发者提供高效、智能的编程体验。它包括三个主要组件&#xff1a;TRAE IDE、TRAE SOLO 和 TRAE 插件。无论是编程新手还是经验丰富的开发者&#xff0c;都可以通过 TRAE 提高工作效率和代码质量。标题一&#x…

将开发的软件安装到手机:环境配置、android studio设置、命令行操作

将开发的软件安装到手机环境配置android studio4.1.2安装命令行操作环境配置 注意&#xff1a;所有的工具的版本都需要根据当下自己的软件需要的。 Node&#xff1a;14.16.0 &#xff08;如果安装了npm&#xff0c;可以使用npm进行当前使用node版本的更改&#xff09; &#x…

Jmeter 命令行压测、HTML 报告、Jenkins 配置目录

Jmeter 命令行压测 & 生成 HTML 测试报告 通常 Jmeter 的 GUI 模式仅用于调试&#xff0c;在实际的压测项目中&#xff0c;为了让压测机有更好的性能&#xff0c;多用 Jmeter 命令行来进行压测。 官方&#xff1a;Jmeter 最佳实践 同时&#xff0c;JMeter 也支持生成 HT…

记录几个SystemVerilog的语法——覆盖率

1. 前言 通常说的覆盖率有两种类型&#xff1a;code coverage(代码覆盖率)和functional coverage(功能覆盖率)。代码覆盖率是使用EDA工具自动从设计代码里提取出来的&#xff0c;功能覆盖率是用户指定的&#xff0c;用于衡量测试设计意图和功能进展。因此&#xff0c;功能覆盖…

深度学习基础—2

第一章、参数初始化 我们在构建网络之后&#xff0c;网络中的参数是需要初始化的。我们需要初始化的参数主要有权重和偏置&#xff0c;偏重一般初始化为 0 即可&#xff0c;而对权重的初始化则会更加重要&#xff0c;我们介绍在 PyTorch 中为神经网络进行初始化的方法。 1.1 常…

PyTorch深度学习快速入门学习总结(三)

现有网络模型的使用与调整 VGG — Torchvision 0.22 documentation VGG 模型是由牛津大学牛津大学&#xff08;Oxford University&#xff09;的 Visual Geometry Group 于 2014 年提出的卷积神经网络模型&#xff0c;在 ImageNet 图像分类挑战赛中表现优异&#xff0c;以其简…

是否需要买一个fpga开发板?

纠结要不要买个 FPGA 开发板&#xff1f;真心建议搞一块&#xff0c;尤其是想在数字电路、嵌入式领域扎根的同学&#xff0c;这玩意儿可不是可有可无的摆设。入门级的选择不少&#xff0c;全新的像 Cyclone IV、Artix 7 系列&#xff0c;几百块就能拿下&#xff0c;要是去二手平…

【模型细节】MHSA:多头自注意力 (Multi-head Self Attention) 详细解释,使用 PyTorch代码示例说明

MHSA:使用 PyTorch 实现的多头自注意力 (Multi-head Self Attention) 代码示例&#xff0c;包含详细注释说明&#xff1a;线性投影 通过三个线性层分别生成查询(Q)、键(K)、值(V)矩阵&#xff1a; QWq⋅x,KWk⋅x,VWv⋅xQ W_qx, \quad K W_kx, \quad V W_vxQWq​⋅x,KWk​⋅x…

PGSQL运维优化:提升vacuum执行时间观测能力

本文是 IvorySQL 2025 生态大会暨 PostgreSQL 高峰论坛上的演讲内容&#xff0c;作者&#xff1a;NKYoung。 6 月底济南召开的 HOW2025 IvorySQL 生态大会上&#xff0c;我在内核论坛分享了“提升 vacuum 时间观测能力”的主题&#xff0c;提出了新增统计信息的方法&#xff0c…

神奇的数据跳变

目的 上周遇上了一个非常奇怪的问题,就是软件的数据在跳变,本来数据应该是158吧,数据一会变成10,一会又变成158,数据在不断地跳变,那是怎么回事?? 这个问题非常非常的神奇,让人感觉太不可思议了。 这是这段时间,我遇上的最神奇的事了,没有之一,最神奇的事,下面…

【跨国数仓迁移最佳实践3】资源消耗减少50%!解析跨国数仓迁移至MaxCompute背后的性能优化技术

本系列文章将围绕东南亚头部科技集团的真实迁移历程展开&#xff0c;逐步拆解 BigQuery 迁移至 MaxCompute 过程中的关键挑战与技术创新。本篇为第3篇&#xff0c;解析跨国数仓迁移背后的性能优化技术。注&#xff1a;客户背景为东南亚头部科技集团&#xff0c;文中用 GoTerra …

【MySQL集群架构与实践3】使用Dcoker实现读写分离

目录 一. 在Docker中安装ShardingSphere 二 实践&#xff1a;读写分离 2.1 应用场景 2.2 架构图 2.3 服务器规划 2.4 启动数据库服务器 2.5. 配置读写分离 2.6 日志配置 2.7 重启ShardingSphere 2.8 测试 2.9. 负载均衡 2.9.1. 随机负载均衡算法示例 2.9.2. 轮询负…