Linux网络编程:UDP 的echo server

发布于:2025-08-01 ⋅ 阅读:(12) ⋅ 点赞:(0)

目录

前言:

一、服务端的实现

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; // 设置地址族为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等网络结构体前先进行清零(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;                     // 设置地址族为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);
                    }
                }
            }
        }

 

总结:

希望本文对你有所帮助


网站公告

今日签到

点亮在社区的每一天
去签到