【Linux网络-Socket编程UDP实战】udp_echo_sercer

在实现这个实战前,先复习几个函数

inet_addr

inet_addr 是一个在 C 语言标准库 <arpa/inet.h> 中定义的函数,用于将一个以点分十进制字符串表示的 IPv4 地址(例如 “192.168.1.1”)转换为网络字节序in_addr_t 类型的整数值

参数说明:

  • cp:指向一个以点分十进制表示的 IPv4 地址字符串的指针。

返回值:

  • 如果转换成功,函数返回一个网络字节序的 in_addr_t 类型整数值,这可以存储在一个 struct in_addr 结构体中。

  • 如果输入的字符串不是一个有效的 IPv4 地址,函数返回 INADDR_NONE(通常定义为 in_addr_t 类型的 -1)。

 

inet_ntoa

inet_ntoa 是一个在 C 语言标准库 <arpa/inet.h> 中定义的函数,用于将网络字节序in_addr 结构体表示的 IPv4 地址转换成点分十进制字符串形式。

参数说明:

  • in:一个 struct in_addr 结构体,其中包含了网络字节序的 IPv4 地址。

返回值:

  • 函数返回一个指向点分十进制字符串的指针,这个字符串表示了输入的 IPv4 地址。

关于inet_ntoa

Inet_ntoa这个函数返回了一个char* ,很显然这个函数自己在内部为我们申请了一块内存来保存ip的结果,那么是否需要调用者手动释放呢?

 

man手册上说,inet_ntoa函数,是把这个返回结果放到了静态存储区,这个时候不需要我们手动进行释放

那么问题来了,如果我们调用多次这个函数,会有什么样的效果呢?

#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main() {
    struct sockaddr_in addr1;
    struct sockaddr_in addr2;

    addr1.sin_addr.s_addr = 0; 
    addr2.sin_addr.s_addr = 0xffffffff;

    char* ptr1 = inet_ntoa(addr1.sin_addr);
    char* ptr2 = inet_ntoa(addr2.sin_addr);

    printf("ptr1: %s, ptr2: %s\n", ptr1, ptr2); 

    return 0;
}

运行结果如下:

因为inet_ntoa 把结果放到自己内部的一个静态存储区,这样第二次调用时的结果会覆盖上一次的结果

思考:如果有多个线程调用 inet_ntoa 是否会出现异常情况呢?

在APUE中,明确提出inet_ntoa不是线程安全函数;

但是在centos7上测试,并没有出现问题,可能内部实现了互斥锁

在多线程环境下,推荐使用inet_ntop,这个函数由调用者提供一个缓冲区来保存结果,可以规避线程安全的问题

inet_ntop

inet_ntop 函数是用来将网络地址转换成字符串形式的。这是 POSIX 标准中定义的函数,用于替代较旧的 inet_ntoa 函数,因为它支持IPv6并且更加灵活

  • int af: 地址族。例如,AF_INET 表示IPv4,AF_INET6 表示IPv6。

  • const void *src: 指向包含原始网络地址的结构(如 struct in_addrstruct in6_addr)的指针。

  • char *dst: 指向目标缓冲区的指针,该缓冲区用于存储转换后的字符串形式的地址。

  • socklen_t size: 目标缓冲区的大小。

所以我们InetAddr.hpp中就可以这样设计

 

UdpServer.hpp

我们这里要写一个服务端

我们首先肯定需要创建套接字,绑定套接字,我们用类来封装; 下面初始化了一些参数,其中UdpServer继承了nocopy类,这个主要目的是禁止拷贝构造和赋值拷贝,使用继承可以进行解耦合,并且使用继承可以复用模版,这样就不需要为每一个类写禁止拷贝构造和赋值拷贝

class UdpServer : nocopy
{
private:
    int _sockfd;
    uint16_t _localport;
    std::string _localip;
    bool _isrunning;

public:
    UdpServer(std::string loaclip, uint16_t port = gport)
    :_localip(loaclip),
    _localport(port),
    _isrunning(false)
    {
    }
};

 nocopy.hpp

#pragma once
#include <iostream>

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

udp服务器初始化

创建套接字 和 将本地ip地址和端口号与套接字关联起来

void Init()
    {
        // 1.创建套接字
        _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);//3
        if (_sockfd < 0)
        {
            LOG(FATAL,"socket error\n");
            exit(SOCKET_ERROR);
        }
        LOG(DEBUG,"socket create successs, _sockfd is %d\n",_sockfd);

        //2.将文件和网络绑定起来
        //将本地ip地址和端口号 与 _sockfd关联起来

        //填写本地信息
        struct sockaddr_in local;
        memset(&local,0,sizeof(local));
        //sin --> socker ip net
        local.sin_family = AF_INET;
        local.sin_port = htons(_localport);
        local.sin_addr.s_addr = inet_addr(_localip.c_str());

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

云服务器上我们不建议bind自己的IP,我们应该绑定能够接受任意ip的ip,那么服务器端我们应该把自己的ip绑定为0,这样服务器未来就能接收任意客户端发来的信息;

这样我们就需要修改代码,我们不需要ip了,我们只需要端口号就可以了

使用INADDR_ANY可以使服务器端,进行任意IP地址绑定

启动服务:这个服务需要一直挂着,不断进行从服务器收消息

我们是echo项目需要回显回去,所以我们收到消息后还要把消息发出去

其中这个peer是来自远方的结构体信息,存放谁发的地址信息,这样我们后续就可以通过peer再把信息发到对方

 void Start()
    {
        _isrunning = true;
        char inbuffer[1024];

        while(_isrunning)
        {   
            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)
            {   
                inbuffer[n] = 0;
                std::string echo_string = "[Udp_server_echo]### ";
                echo_string += inbuffer;
                std::cout << "[client say => ]" << echo_string << std::endl;
                
                sendto(_sockfd, echo_string.c_str(),echo_string.size(),0, (struct sockaddr*)&peer ,len);
            }
            else
            {
                std::cout << "recvfrom failed" << std::endl;
            }
            
        }
    }

 

UdpServerMain

UdpClientMain

对于客户端

客户端一定要先访问服务器 所以客户端一定要知道服务器地址和端口号

所以我们需要输入客户端的服务器地址和端口号

client的端口号,一般不让用户自己设定,而是让client OS随机选择

client 需要 bind它自己的IP和端口, 但是client 不需要 “显示” bind它自己的IP和端口,

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

 


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

// struct sockaddr头文件
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
using namespace log_ns;

// 客户端一定要先访问服务器  所以客户端一定要知道服务器地址和端口号

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

    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        LOG(FATAL, "socket create fail \n");
        exit(0);
    }
    LOG(DEBUG, "socket creat success , socketfd is %d \n", sockfd);

    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(serverip.c_str());
    server.sin_port = htons(serverport);

    while (true)
    {
        std::string line;
        std::cout << "Please Enter# ";
        std::getline(std::cin, line);

        //std::cout << line << std::endl;
        // 发消息
        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) - 1, 0, (struct sockaddr *)&temp, &len);
            if (m > 0)
            {
                buffer[m] = 0;
                std::cout << buffer << std::endl;
            }
            else
            {
                std::cout << "recvfrom error " << std::endl;
            }
        }
        else
        {
            std::cout << "sendto error" << std::endl;
        }
    }

    ::close(sockfd);
    return 0;
}

 测试及改进

我们之前说客户端不需要显示绑定自己的ip,我们来验证一下

正如我们之前所说,客户端可以自动绑定自己的网络地址信息,并且客户端的端口号是随机的

 这样的代码是没有问题但是不够优雅,未来这个模块我们可能会经常用到(将收到的地址信息转化为本地端口号和本地地址),所以我们重新设计一个类封装这个模块(网络地址的类)

InetAddr

#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:
    uint16_t _port;
    std::string _ip;
    struct sockaddr_in _addr;

public:
    InetAddr(struct sockaddr_in &addr)
    :_addr(addr)
    {
        ToHost(addr);
    }

    ~InetAddr()
    {
    }

    uint16_t Port()
    {
        return _port;
    }

    std::string Ip()
    {
        return _ip;
    }
    

private:
    void ToHost(const struct sockaddr_in &addr)
    {
        _port = ntohs(addr.sin_port);
        _ip = inet_ntoa(addr.sin_addr);
    }
};

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值