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

【Linux网络编程】第三弹---UDP网络通信深度解析:构建服务器端、客户端,并实现两端通信的完整步骤与测试

个人主页: 熬夜学编程的小林

💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】【Linux网络编程】

目录

1、服务器端

1.1、主函数

1.2、UdpServer类 

1.2.1、基本结构

1.2.2、构造析构函数

1.2.3、InitServer() 

1.2.4、Start()

2、客户端

2.1、UdpClient

2.1.1、五个步骤 

2.1.2、两个注意 

2.1.3、代码实现

3、两端通信

3.1、UdpServer

3.1.1、测试一(固定版本)

3.1.2、测试二(传端口和IP版本) 

3.1.3、测试三(优化网络转换)


上一弹我们讲解了socket编程的基本知识,此弹设计一个基于UDP协议的网络编程代码,能够简单的回显服务器和客户端代码!!!

我们需要做到服务器与客户端进行通信,需要先分别实现服务器和客户端的代码,我们依旧使用先写主函数,再写类对象的方式实现

1、服务器端

1.1、主函数

主函数通过智能指针构造Server类,并初始化和启动服务!

注意:此处需要用到前面实现的日志类,只需将日志类文件拷贝过来即可! 

int main()
{
    EnableScreen();
    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(); // C++14标准

    usvr->InitServer(); // 初始化服务端
    usvr->Start();      // 启动服务端
    return 0;
}

1.2、UdpServer类 

表示错误的枚举类型

enum
{
    SOCKET_ERROR = 1,
    BIND_ERROR
};

全局变量 

static const int gsockfd = -1;
static const uint16_t glocalport = 8888;

UdpServer类成员变量需要文件描述符,IP,端口,运行状态初始化函数创建socket套接字,并将套接字进行绑定启动函数收客户端的消息并回复客户端!

1.2.1、基本结构

class UdpServer
{
public:
    UdpServer(uint16_t localport = glocalport);
    // 初始化
    void InitServer();
    // 启动
    void Start();
    ~UdpServer();
private:
    int _sockfd;          // 文件描述符
    uint16_t _localport;  // 端口号
    std::string _localip; // ip地址,TODO后面处理
    bool _isrunning;
};

1.2.2、构造析构函数

构造函数初始化_sockfd,_localport(默认初始化为全局端口号),_localip(传参),_isrunning(默认为false)析构函数关闭文件(前提是创建了文件)即可!

// 构造函数(可以不传参版本)
UdpServer(uint16_t localport = glocalport)
    : _sockfd(gsockfd), _localport(localport),_isrunning(false)
{
}

// 构造函数(需要传参版本)
UdpServer(const std::string &localip, uint16_t localport = glocalport)
    : _sockfd(gsockfd), _localport(localport), _localip(localip), _isrunning(false)
{
}

// 析构函数
~UdpServer()
{
    // 关闭文件
    if(_sockfd < gsockfd) ::close(_sockfd);
}

1.2.3、InitServer() 

初始化函数创建socket套接字,并将套接字进行绑定;

1、此处先测试一下socket()函数的返回值,创建成功正常会返回3,因为0,1,2,已经被占用了!

// 测试
void InitServer()
{
    _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
    if (_sockfd < 0)
    {
        LOG(FATAL, "socket error\n");
        exit(SOCKET_ERROR);
    }
    LOG(DEBUG, "socket create success,sockfd: %d\n", _sockfd); // 3
}

注意:此处的退出码用到了枚举类型,在UdpServer类最前面有具体代码!!! 

Start()

此处为了先测试,有Start()函数即可,保证能够编译通过,后面再实现函数!

void Start()
{}

运行结果 

为了防止Server类被拷贝,此处可以设计一个防止拷贝的类,并让Server类继承,此时Server类就不能被拷贝和赋值了!!!

nocopy类

class nocopy
{
public:
    nocopy(){}
    ~nocopy(){}
    nocopy(const nocopy&) = delete;
    const nocopy& operator=(const nocopy&) = delete;
};

UdpServer类 

// 继承不能拷贝的类
class UdpServer : public nocopy
{
public:
    // ... 
private:
    // ...
}

主函数

int main()
{
    UdpServer user1;
    UdpServer user2 = user1; // 禁止赋值
    UdpServer user3(user1);  // 禁止拷贝

    return 0;
}

运行结果  

 2、将套接字进行绑定

套接字绑定将文件描述符与 网络序列的端口号和IP绑定

需要用到IP转换函数

inet_addr()

将一个点分十进制的IPv4地址(例如 "192.168.1.1")转换为一个网络字节序(通常是大端序)的整数

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

in_addr_t inet_addr(const char *cp);

参数:

  • in_addr_t:函数返回类型,表示转换后的IPv4地址。
  • inet_addr:函数名。
  • const char *cp:函数的参数,是一个指向以null结尾的字符串的指针,该字符串表示一个点分十进制的IPv4地址。

 返回值:

  • 如果输入字符串是一个有效的IPv4地址字符串,函数返回转换后的网络字节序整数。
  • 如果输入字符串不是一个有效的IPv4地址字符串,函数返回 INADDR_NONE,这是一个特殊的常量,通常定义为 -1,用于指示错误。

InitServer()

void InitServer()
{
    // 1.创建socket文件
    _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
    if (_sockfd < 0)
    {
        LOG(FATAL, "socket error\n");
        exit(SOCKET_ERROR);
    }
    LOG(DEBUG, "socket create success,sockfd: %d\n", _sockfd); // 3

    // 2.bind
    struct sockaddr_in local;
    memset(&local, 0, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_port = htons(_localport);                  // 主机序列转网络序列
    local.sin_addr.s_addr = inet_addr(_localip.c_str()); // 1.需要4字节ip 2.需要网络序列ip

    int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
    if (n < 0)
    {
        LOG(FATAL, "bind error\n");
        exit(BIND_ERROR);
    }
    LOG(DEBUG, "socket bind success\n");
}

1.2.4、Start()

启动函数收客户端的消息并回复客户端(此处是一个死循环[根据常识])!

接收消息函数

recvfrom()

从套接字接收数据。

#include <sys/types.h>
#include <sys/socket.h>

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);

参数:

  • sockfd:已经创建并绑定的套接字的文件描述符
  • buf:指向用于存储接收到的数据的缓冲区的指针
  • len缓冲区的大小,以字节为单位。
  • flags接收操作的标志(此处设置为0即可),用于修改recvfrom的行为。常用的标志包括MSG_DONTWAIT(非阻塞模式)和MSG_WAITALL(阻塞模式,直到接收到指定大小的数据)。
  • src_addr:指向sockaddr结构体的指针,用于存储发送方的地址信息。
  • addrlen:指向整型的指针,用于指定src_addr结构体的大小,并在调用后被设置为新接收到的地址的实际大小。

返回值:

  • recvfrom 成功时返回接收到的字节数
  • 失败时返回-1,并设置全局变量errno来指示错误的原因。

接收消息是从网络里面接收,我们需要将网络的端口号和IP转成主机序列!!

网络转主机函数

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

char *inet_ntoa(struct in_addr in);

将网络字节序(通常是 IPv4 地址)转换为点分十进制字符串(即我们常见的 "x.x.x.x" 格式的 IP 地址)的函数 。

参数:

  • in一个 struct in_addr 结构体,它包含一个 uint32_t 类型的成员 s_addr,该成员以网络字节序(大端序)存储 IPv4 地址。

返回值:

  • 成功时,返回一个指向静态分配的、表示点分十进制 IP 地址字符串的指针。这个字符串不应该被修改或释放。
  • 失败时,返回 nullptr(但实际上,由于 inet_ntoa 只是一个简单的转换函数,它几乎总是能成功,除非传入的 struct in_addr 结构无效)。

发送消息函数

sendto()

向套接字发送消息。

#include <sys/types.h>
#include <sys/socket.h>

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);

参数:

  • sockfd套接字描述符,是通过 socket 函数创建的套接字文件描述符。
  • buf:指向要发送数据的缓冲区
  • len要发送数据的字节数
  • flags:用于控制发送行为的标志位通常设置为 0,但也可以使用以下选项之一或多个(使用按位或运算符 | 组合):
    • MSG_CONFIRM:请求确认消息数据已被发送(适用于某些特定协议)。
    • MSG_DONTROUTE:绕过路由表,直接发送数据(仅适用于某些协议)。
    • MSG_EOR:表示数据记录的结束(对于某些流协议可能有用)。
    • MSG_MORE:指示后续将发送更多数据(对于某些协议,可能会优化发送)。
    • MSG_NOSIGNAL:防止发送过程中产生 SIGPIPE 信号(如果连接已经关闭)。
  • dest_addr指向目标地址的指针,通常是一个 struct sockaddr_in(用于 IPv4)或 struct sockaddr_in6(用于 IPv6)结构体。
  • addrlen目标地址的长度,通常是 sizeof(struct sockaddr_in) 或 sizeof(struct sockaddr_in6)

返回值:

  • 成功时,返回发送的字节数。
  • 失败时,返回 -1,并设置 errno 以指示错误类型。
void Start()
{
    _isrunning = true;
    char inbuffer[1024];
    while (_isrunning)
    {
        // sleep(1);
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        // 收消息,返回值: 实际收到多少字节
        ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);
        if (n > 0)
        {
            // uint16_t peerport = ntohs(peer.sin_port); // 网络转主机
            // std::string peerip = inet_ntoa(peer.sin_addr); // 结构化转字符串

            inbuffer[n] = 0;
            std::cout << "client say# " << inbuffer << std::endl;
            // std::cout << "[" << peerip << ":" << peerport << "]# " << inbuffer << std::endl;
            std::string echo_string = "[udp_server echo] #";
            echo_string += inbuffer;

            // 发消息
            sendto(_sockfd,echo_string.c_str(),echo_string.size(),0,(struct sockaddr *)&peer,len);
        }
    }
}

2、客户端

2.1、UdpClient

Client端先向服务器发送消息(以行读取并发送),然后接收服务器的消息!

注意:我们向服务器发送消息需要知道服务器的端口和IP,因此此处使用命令行确定端口和IP!

使用形式:

// 客户端在未来一定要知道服务器的IP地址和端口号
// .udp_client server-ip server-port
// .udp_client 127.0.0.1 8888

2.1.1、五个步骤 

客户端程序分为5步:

0.读取接收端IP和端口

1.创建套接字

2.设置接收端信息

3.发消息和接收消息

4.关闭套接字

2.1.2、两个注意 

注意:

1、客户端的绑定和服务端有一些区别!!

client的端口号,一般不让用户自己设定,而是让client 所在OS随机选择?怎么选择?什么时候?

1、client 需要bind它自己的IP和端口,但是client 不需要 "显示" bind它自己的IP和端口

2、client 在首次向服务器发送数据的时候,OS会自动给client bind它自己的IP和端口

 2、设置接收端信息需要转换!

inet_addr()

将一个用点分十进制(例如“a.b.c.d”格式)表示的IP地址转换成一个长整型数(在C语言中通常为unsigned longu_long类型),这个长整型数在网络编程中代表该IP地址的网络字节序二进制值。

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

in_addr_t inet_addr(const char *cp);

2.1.3、代码实现

// 客户端在未来一定要知道服务器的IP地址和端口号
// .udp_client server-ip server-port
// .udp_client 127.0.0.1 8888
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;
        exit(0);
    }

    // 0.读取接收端IP和端口
    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 << "create socket eror\n" << std::endl;
        exit(1);
    }

    // client的端口号,一般不让用户自己设定,而是让client 所在OS随机选择?怎么选择?什么时候?
    // client 需要bind它自己的IP和端口,但是client 不需要 "显示" bind它自己的IP和端口
    // client 在首次向服务器发送数据的时候,OS会自动给client bind它自己的IP和端口

    // 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());

    // 3.发消息和接收消息
    while (true)
    {
        std::string line;
        std::cout << "Please Enter# ";
        std::getline(std::cin, line); // 以行读取消息
        // 发消息,你要知道发送给谁
        int n = sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr *)&server, sizeof(server));
        if(n > 0)
        {
            // 收消息
            struct sockaddr_in temp;
            socklen_t len = sizeof(temp);
            char buffer[1024];
            int m = recvfrom(sockfd,buffer,sizeof(buffer),0,(struct sockaddr*)&temp,&len);
            if(m > 0)
            {
                buffer[m] = 0;
                std::cout << buffer << std::endl;
            }
            else
            {
                break;
            }
        }
        else
        {
            break;
        }
    }

    // 4.关闭套接字
    ::close(sockfd);
    return 0;
}

3、两端通信

3.1、UdpServer

服务器先接受客户端的消息,再发消息给客户端!

3.1.1、测试一(固定版本)

第一个测试先让客户端与服务端进行简单的通信,因此需要先设置服务器端的IP和端口号(此处把IP和端口号固定了,因此对这个IP和端口号才能正常通信)。

主函数

int main()
{
    uint16_t port = 8899;
    std::string ip = "127.0.0.1";

    EnableScreen();
    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(ip,port); // C++14标准

    usvr->InitServer();
    usvr->Start();
    return 0;
}

运行结果  

这个版本还有一个小问题: 每次都是固定的字符串,并不知道谁发送过来的消息!!!

可以优化一下Server类的Start()函数!!!

recvfrom()接收消息的函数中,后面有两个输出型参数,代表的是发送者的信息,我们可以读取到发送者的IP 和 端口号,但是那是网络层面的,在本地读取需要转换!!!

inet_ntoa()

// 将一个 32 位的网络字节序的 IP 地址转换为点分十进制的 IP 地址字符串
char *inet_ntoa(struct in_addr in); 

优化 

void Start()
{
    _isrunning = true;
    char inbuffer[1024];
    while (_isrunning)
    {
        // sleep(1);
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        // 收消息,返回值: 实际收到多少字节
        ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);
        if (n > 0)
        {
            uint16_t peerport = ntohs(peer.sin_port); // 网络转主机
            std::string peerip = inet_ntoa(peer.sin_addr); // 结构化转字符串

            inbuffer[n] = 0;
            // std::cout << "client say# " << inbuffer << std::endl;
            std::cout << "[" << peerip << ":" << peerport << "]# " << inbuffer << std::endl;
            std::string echo_string = "[udp_server echo] #";
            echo_string += inbuffer;

            // 发消息
            sendto(_sockfd,echo_string.c_str(),echo_string.size(),0,(struct sockaddr *)&peer,len);
        }
    }
}

运行结果  

3.1.2、测试二(传端口和IP版本) 

在第一个测试中,服务端只要启动程序就只能收到固定IP + 端口的信息(缺点),但是我们并不是每次都是固定的,我们也可以灵活一点,此处可以引入命令行参数

主函数

// .udp_client local-ip local-port
// .udp_client 127.0.0.1 8888
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << "  local-ip server-port" << std::endl; 
        exit(0);
    }

    std::string ip = argv[1];
    uint16_t port = std::stoi(argv[2]);

    EnableScreen();
    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(ip,port); // C++14标准

    usvr->InitServer();
    usvr->Start();
    return 0;
}

运行结果  

此处还可以进行优化,如果我们有了固定IP + 端口这样的方式,那么只有一个网络进程可以发送成功,我们此处可以将服务器的IP设置为0,那么只要端口一样,服务器端就能收到客户端的消息

优化

需要删除Server类的_localip成员变量,修改构造函数,无需初始化_localip,InitServer()函数时将IP设置为INADDR_ANY,并修改主函数,只需要传两个参数

Server类

class UdpServer : public nocopy
{
public:
    UdpServer(uint16_t localport = glocalport)
        : _sockfd(gsockfd), _localport(localport), _isrunning(false)
    {
    }
    void InitServer()
    {
        // 1.创建socket文件
        _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(FATAL, "socket error\n");
            exit(SOCKET_ERROR);
        }
        LOG(DEBUG, "socket create success,sockfd: %d\n", _sockfd); // 3

        // 2.bind
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_localport);                  // 主机序列转网络序列
        // local.sin_addr.s_addr = inet_addr(_localip.c_str()); // 1.需要4字节ip 2.需要网络序列ip
        local.sin_addr.s_addr = INADDR_ANY; // 服务器端,进行任意IP地址绑定[0]

        int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(FATAL, "bind error\n");
            exit(BIND_ERROR);
        }
        LOG(DEBUG, "socket bind success\n");
    }
private:
    int _sockfd;          // 文件描述符
    uint16_t _localport;  // 端口号
    // std::string _localip; // ip地址,TODO后面处理
    bool _isrunning;
};

主函数

// .udp_client local-port
// .udp_client 8888
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << "  server-port" << std::endl;
        exit(0);
    }

    uint16_t port = std::stoi(argv[1]);

    EnableScreen();
    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port); // C++14标准

    usvr->InitServer();
    usvr->Start();
    return 0;
}

运行结果  

3.1.3、测试三(优化网络转换)

在服务端,为了打印客户端的IP和端口,我们需要使用转换函数,但是此处还是属于面向过程编程,此处我们可以封装成转换的类,调用该类则自动转换,想要主机的IP和端口调用成员函数即可,此时为面向对象编程!!!

InetAddr类

类成员需要本地的IP和端口,网络的结构体对象构造函数将网络数据转成主机数据,并实现获取IP和端口的成员函数

#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

class InetAddr
{
private:
    // 网络地址转本地地址 
    void ToHost(const struct sockaddr_in& addr) 
    {
        _port = ntohs(addr.sin_port); // 网络转主机
        _ip = inet_ntoa(addr.sin_addr); // 结构化转字符串
    }
public:
    InetAddr(const struct sockaddr_in& addr):_addr(addr)
    {
        ToHost(addr);
    }
    std::string Ip()
    {
        return _ip;
    }
    uint16_t Port()
    {
        return _port;
    }
    ~InetAddr()
    {}
private:
    std::string _ip;
    uint16_t _port;
    struct sockaddr_in _addr;
};

Start()

void Start()
{
    _isrunning = true;
    char inbuffer[1024];
    while (_isrunning)
    {
        // sleep(1);
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        // 收消息,返回值: 实际收到多少字节
        ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);
        if (n > 0)
        {
            // uint16_t peerport = ntohs(peer.sin_port); // 网络转主机
            // std::string peerip = inet_ntoa(peer.sin_addr); // 结构化转字符串

            InetAddr addr(peer);

            inbuffer[n] = 0;
            // std::cout << "[" << peerip << ":" << peerport << "]# " << inbuffer << std::endl;
            std::cout << "[" << addr.Ip() << ":" << addr.Port() << "]# " << inbuffer << std::endl;
            std::string echo_string = "[udp_server echo] #";
            echo_string += inbuffer;

            // 发消息
            sendto(_sockfd,echo_string.c_str(),echo_string.size(),0,(struct sockaddr *)&peer,len);
        }
    }
}

运行结果  


http://www.kler.cn/a/416846.html

相关文章:

  • 【知识科普】github开源证书等级说明
  • llamaindex实战-QueryEngine-查询引擎使用概述
  • 20241129解决在Ubuntu20.04下编译中科创达的CM6125的Android10出现找不到库文件
  • redis的应用----缓存
  • C/C++ 数据结构与算法 【时间复杂度和空间复杂度】【日常学习,考研必备】
  • 数字IC后端实现之PR工具中如何避免出现一倍filler的缝隙?
  • 【传感器技术】第6章 压电式传感器
  • [python脚本处理文件入门]-18.使用Python进行PDF文件的合并与拆分
  • 浅谈volatile
  • Mybatis:CRUD数据操作之修改数据update
  • 【QT/MinGW/.a->.lib】如何将一个用QT的MingGW编译dll项目出的dll文件导出一份.lib文件给其他项目链接动态库用
  • docker启动容器,语句名词解释
  • day21:jumpserver配置与搭建
  • 【bug】AttributeError: module ‘openai‘ has no attribute ‘error’
  • 第6章 元素应用CSS
  • 信息与网络安全笔记2
  • 常见靶场的搭建
  • 去中心化物理基础设施网络(DePIN):重塑未来的基石
  • 分析 系统滴答时钟(tickClock),设置72MHz系统周期,如何实现1毫秒的系统时间?
  • SpringBoot源码-spring boot启动入口ruan方法主线分析(二)
  • 如何解决 javax.xml.bind.MarshalException: 在 RMI 中,参数或返回值无法被编组的问题?亲测有效的解决方法!
  • spark读取hbase数据
  • XTuner 微调实践微调
  • java——Netty与Tomcat的区别
  • Android习题第7章广播
  • 【力扣热题100】[Java版] 刷题笔记-3. 无重复字符的最长子串