目录
前言:
大家好啊,前面我们介绍了一些在网络编程中的一些基本的概念知识。
今天我们就借着上节课提到的,传输层的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; // 设置地址族为IPv4
local.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; // 设置地址族为IPv4
local.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
等网络结构体前先进行清零(memset
或bzero
):
我们这里使用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; // 设置地址族为IPv4
local.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)接收数据的系统调用,它不仅能获取数据,还能获取发送方的地址信息。
参数 | 类型 | 说明 |
---|---|---|
sockfd |
int |
套接字文件描述符 |
buf |
void* |
接收数据的缓冲区 |
len |
size_t |
缓冲区长度 |
flags |
int |
控制接收行为的标志位 |
src_addr |
struct sockaddr* |
发送方地址信息(可选) |
addrlen |
socklen_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)发送数据的系统调用,它允许指定目标地址。
参数 | 类型 | 说明 |
---|---|---|
sockfd |
int |
套接字文件描述符 |
buf |
const void* |
要发送的数据缓冲区 |
len |
size_t |
要发送的数据长度 |
flags |
int |
控制发送行为的标志位 |
dest_addr |
const struct sockaddr* |
目标地址信息 |
addrlen |
socklen_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);
}
}
}
}
总结:
希望本文对你有所帮助