深入了解linux网络—— UDP网络通信

前言

了解了socket编程的基础知识,IP + port标识网络中的唯一进程、源IP/端口号、目的IP/端口号以及socket相关API

传输层的两大协议:TCP/UDP

这里来使用UDP相关接口来实现网络通信

服务端

1. 初始化

我们知道,在操作系统中并不是所有的进程都有端口号,所以在服务器端,就要做相关操作来让进程绑定唯一的端口号。

创建socket

对于服务器端,要先创建socketIP + port

创建socket要用到接口socket

c 复制代码
int socket(int domain, int type, int protocol);

参数:

socket存在三个参数:int domainint typeint protocol

domain

简单来说该参数就是表面要进行的通信类型,这里我们要进行网络通信,参数就传AF_INET

type

对于这个参数也是介绍了一大堆,这里就暂且先关注UPD通信(也就是面向数据报 ),其中SOCK_DGRAM就表示面向数据报。

所有,在传参时,domainAF_INET(表示网络通信)、typeSOCK_DGRAM(表示面向数据报)就能够表示当前是UDP通信。

protocol

对于这个参数,这里暂时不做介绍,在使用时传递0即可。

返回值:

对于socket的返回值,就有意思了:


可以看到,如果调用socket创建成功,就会返回一个文件描述符 (这里体现了Linux一切皆文件)

而我们还知道,一个进程默认会打开三个文件(012文件描述符被占用),所以默认(不关闭012socket返回的文件描述符是>=3的。

这里暂且先不探究其原理,后续再深入了解。

所以,要使用UDP通信,创建套接字,调用socket就要传递AF_INETSOCK_DGRAM0

socket创建成功会返回一个文件描述符,这里就设计一个类,将该文件描述符管理起来。

cpp 复制代码
//udpserver.hpp
class UdpServer
{
public:
    UdpServer() : _sockfd(-1)
    {
    }
    ~UdpServer() {}
    void Init()
    {
        // 1. 创建socket
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0); // AF_INET 网络通信  SOCK_DGRAM  面向数据报
        if (_sockfd < 0)
        {
            // _sockfd < 0 表示创建套接字失败,这里输出一条日志然后退出
            LOG(Level::DEBUG) << "socket error";
            exit(1);
        }
        LOG(Level::DEBUG) << "socket success, sockfd : " << _sockfd;
    }
private:
    int _sockfd;
};

这样实现UdpSrever,在服务器端只需创建UdpServer对象,然后调用Init(初始化)和运行成员函数即可。

cpp 复制代码
//udpserver.cc
#include "udpserver.hpp"
int main()
{
    UdpServer usvr;
    usvr.Init();
    return 0;
}

可以看到,socket返回的文件描述符确实是从3开始的。

绑定IP/端口号

在上述操作中,通过socket创建了套接字,也获取了文件描述符;但是还没有IP和端口号啊,还是没办法在网络中找到该主机的该进程啊;

所以创建完套接字之后,还要进行绑定IP和端口号;就要调用bind方法

c 复制代码
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数:

可以看到bind应该是哪个参数,其第一个参数是sockfd,就需要传递socket返回的文件描述符。

addraddrlen

首先const struct sockaddr* addr,这里我们要进行网络通信,就要传递一个struct sockaddr_in *的指针变量;

socklen_t addrlen则表示传递的第二个参数的长度。

所以,在调用bind时,我们要进行网络通信就要先有一个struct sockaddr_in类型的结构体对象;

  • 构建struct sockaddr_in对象

我们知道sockaddr_in结构体中存在三个字段:sin_familysin_addrsin_port;分别指标志位、IP地址和端口号。

这里要进行网络通信,标志位就传递AF_INET

这里在服务器端,就由外部指定要绑定的IP和端口号。(在运行程序时由命令行参数传递)

  • IP地址转换

在我们的认知中,IP地址都是10.0.16.12这种形式的,但是在这里struct sockaddr_in结构中的IP地址类型是uint32_t,也就是4字节整数;

那我们通过命令行参数获取到的IP地址是字符串形式的(.分隔开的数字),所以在这里就要进行IP地址的转换(由字符串形式转为4字节数字

此外,我们这里的sockaddr_in在未来进行通信时,是要发送到网络的,所以在这里我们还需要将本地字节序转换为网络字节序

这里我们可以自己实现字符串IP --> 4字节数字,本地字节序 --> 网络字节序。但是这里,我们也可以调用inet_addr(将字符串形式的IP地址转换为4字节数字,再转换为网络字节序)

  • 端口号转换

这里的struct sockaddr_in是要发送到网络的,所以端口号sin_port也要由本机字节序转换为网络字节序,这里就可以调用htons来进行转换。

返回值:

成功,0被返回;失败则返回-1,并且错误码被设置。

cpp 复制代码
class UdpServer
{
public:
    UdpServer(const std::string &ip, uint16_t port) : _sockfd(-1), _ip(ip), _port(port)
    {
    }
    ~UdpServer() {}
    void Init()
    {
        // 1. 创建socket
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0); // AF_INET 网络通信  SOCK_DGRAM  面向数据报
        if (_sockfd < 0)
        {
            // _sockfd < 0 表示创建套接字失败,这里输出一条日志然后退出
            LOG(Level::DEBUG) << "socket error";
            exit(1);
        }
        LOG(Level::DEBUG) << "socket success, sockfd : " << _sockfd;
        // 2.1 构建sockaddr_in对象
        struct sockaddr_in sockin;
        sockin.sin_family = AF_INET;
        sockin.sin_addr.s_addr = inet_addr(_ip.c_str());
        sockin.sin_port = htons(_port);
        // 2.2 绑定IP、端口号
        int n = bind(_sockfd, (struct sockaddr *)&sockin, sizeof(sockin));
        if (n < 0)
        {
            // n < 0 表示绑定失败,这里输出一条日志然后退出
            LOG(Level::DEBUG) << "bind error";
            exit(2);
        }
        LOG(Level::DEBUG) << "socket success";
    }
private:
    int _sockfd;
    std::string _ip;
    uint16_t _port;
};

这样,服务器在创建UpdServer对象,调用Init初始化后才能进行通信。

cpp 复制代码
//udpserver.cc
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cout << argv[0] << " ip  port" << std::endl;
        return -1;
    }
    std::string ip = argv[1];
    uint16_t port = std::stoi(argv[2]);
    UdpServer usvr(ip, port);
    usvr.Init();
    while (true)
    {
    }
    return 0;
}

这样在程序运行起来之后,就可以在系统中查看到当前进程绑定的IP和端口号了

netstat可以用来查看网络相关信息

  • -u 表示UDP相关;
  • -p表示查看进程pidprogram name
  • -n表示用数字显示
  • -l表示查看监听状态的端口

2. 接收/发送信息

通过创建套接字、绑定IP和端口号,当前就已经具备了通信的条件,现在来实现接受和发送信息;

接受信息

对于UDP通信,接受信息要用到的接口就是recv系列的接口,这里使用recvfrom

c 复制代码
ssize_t recvfrom(int sockfd, void buf[restrict .len], size_t len,
                        int flags,
                        struct sockaddr *_Nullable restrict src_addr,
                        socklen_t *_Nullable restrict addrlen);

参数:

recvfrom存在6个参数:

  • sockfdsocket返回的文件描述符,指明通信使用的套接字(文件)
  • buflenbuf表示接受信息的缓冲区,len表示缓冲区大小
  • flags:标志为,这里传0即可**(表示阻塞式接受信息)**
  • src_addraddrlenrecvfrom不仅受到了对方发来的信息,还收到对方的struct sockaddr字段(IPport);

src_addr输出型参数,recvfrom接收到对方的struct sockaddr字段拷贝到该地址;

addrlen传参时表示src_addr的长度,(也是输出型参数,调用完成后,addrlen中的值表示实际读到的长度)

返回值:

recvfrom读取成功,返回实际读到信息的字节数;(buf)

发送信息

要发送信息,这里就要使用send系列接口,这里使用sendto

c 复制代码
ssize_t sendto(int sockfd, const void buf[.len], size_t len, int flags,
                      const struct sockaddr *dest_addr, socklen_t addrlen);

参数:

参数和recvfrom一样:sockfd指创建socket时返回的文件描述符、buf表示要发送的信息,len表示信息的长度;

flag这里暂时传递0即可。

dest_addr表示要发送给谁,目的主机的struct sockaddr字段;addrlen表示dest_addr的长度(大小)。

返回值这里暂时不考虑。

所以,我们就可以让服务器端阻塞等待式的接受信息,收到信息之后,对信息稍作处理,然后再发送回来。

cpp 复制代码
class UdpServer
{
public:
    void Start()
    {
        while (true)
        {
            char buff[256];
            struct sockaddr_in peer;
            socklen_t len;
            // 接受信息
            int n = recvfrom(_sockfd, buff, sizeof(buff), 0, (struct sockaddr *)&peer, &len);
            if (n < 0)
            {
                // 读取失败
                LOG(Level::WARNING) << "recvfrom error";
                continue;
            }
            buff[n] = '\0';
            std::cout << "recv massage :" << buff << std::endl;
            // 发送信息
            int m = sendto(_sockfd, buff, sizeof(buff), 0, (struct sockaddr *)&peer, len);
            if (m < 0)
            {
                // 读取失败
                LOG(Level::WARNING) << "sendto error";
                continue;
            }
        }
    }
private:
    int _sockfd;
    std::string _ip;
    uint16_t _port;
};

这里,服务器端在接受到远端发送的信息,输出一条消息到标准输出,然后再信息发送给远端。

客户端

上面实现了服务端的代码,现在来实现客户端。

对于服务端,在通信之前,要先创建套接字,绑定IP和端口号;

那客户端呢?

客户端,只需要我们显示地去创建套接字即可,不需要显示绑定(在首次发送信息时会自动绑定IP地址和随机端口号)

所以,客户端只需要创建套接字,然后就可以给服务端发送信息了。

而发送信息,要知道给谁发吧,那就需要对方的IP地址和端口号;

这里就通过命令行参数,在执行程序时指定IP和端口号。

cpp 复制代码
//udpclient.cc
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <strings.h>
int main(int agrc, char *argv[])
{
    if (agrc != 3)
    {
        std::cout << argv[0] << " serverip  serverport" << std::endl;
        return -1;
    }
    // 创建服务端 struct sockaddr_in
    struct sockaddr_in server;
    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(argv[1]);
    server.sin_port = htons(std::stoi(argv[2]));
    // 创建套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        return -1;
    }
    // 无需显示绑定

    while (true)
    {
        std::string massage;
        std::getline(std::cin, massage);
        // 发送信息
        sendto(sockfd, massage.c_str(), massage.size(), 0, (struct sockaddr *)&server, sizeof(server));
        std::cout << "send massage : " << massage << std::endl;
        // 接受信息
        struct sockaddr_in peer;
        bzero(&peer, sizeof(peer));
        socklen_t len = sizeof(len);
        char buff[256];
        int n = recvfrom(sockfd, buff, sizeof(buff), 0, (struct sockaddr *)&peer, &len);
        if (n < 0)
        {
            // 读取失败
            std::cerr << "recvfrom error";
            continue;
        }
        buff[n] = '\0';
        std::cout << "recv : " << buff << std::endl;
    }
    return 0;
}

这里clientserver都只是简单的接受和发送数据,并没有进行数据处理。

本机IP

在上述操作中,使用的10.0.16.12IP地址是云服务器的子网IP,我们如果尝试去连接公网IP,可以发现是连不上的(服务器公司做相关保护)

而在服务器中还存一种IP地址,就是本机IP:

使用ifconfig命令可以查看到:

就是上图中的127.0.0.1,如果我们一台机器上的serverclient通信使用这种IP地址,数据就不会传输到网络,而是通过操作系统发送到对方。

这种IP 地址也通常用来测试网络代码。

可以看到也是可以通信的。

但是,如果这里一端使用10.0.16.12地址,另外一端使用127.0.0.1呢?

可以看到,client和server绑定不同的IP地址,虽然是一台主机的IP地址,但却无法完成通信。

而发送到127.0.0.110.0.16.12的信息都是发送给该主机的啊,按理来说应该是能够收到的。

这里,要想让服务端接收到发送给该主机的所有信息,就不能让server去绑定某个IP地址,而是让server绑定INADDR_ANY

INADDR_ANY本质上就是0

所以,在server端,就不需要通过命令行参数传递IP地址,也不需要存储IP地址了,直接绑定INADDR_ANY即可。

cpp 复制代码
class UdpServer
{
public:
    // UdpServer(const std::string &ip, uint16_t port) : _sockfd(-1), _ip(ip), _port(port)
    // {
    // }
    UdpServer(uint16_t port) : _sockfd(-1), _port(port)
    {
    }
    ~UdpServer() {}
    void Init()
    {
        // 1. 创建socket
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0); // AF_INET 网络通信  SOCK_DGRAM  面向数据报
        if (_sockfd < 0)
        {
            // _sockfd < 0 表示创建套接字失败,这里输出一条日志然后退出
            LOG(Level::DEBUG) << "socket error";
            exit(1);
        }
        LOG(Level::DEBUG) << "socket success, sockfd : " << _sockfd;
        // 2.1 构建sockaddr_in对象
        struct sockaddr_in sockin;
        bzero(&sockin, sizeof(sockin));
        sockin.sin_family = AF_INET;
        // sockin.sin_addr.s_addr = inet_addr(_ip.c_str());
        sockin.sin_addr.s_addr = INADDR_ANY;//绑定INADDR_ANY 接受发送给该主机的所有信息
        sockin.sin_port = htons(_port);
        // 2.2 绑定IP、端口号
        int n = bind(_sockfd, (struct sockaddr *)&sockin, sizeof(sockin));
        if (n < 0)
        {
            // n < 0 表示绑定失败,这里输出一条日志然后退出
            LOG(Level::DEBUG) << "bind error";
            exit(2);
        }
        LOG(Level::DEBUG) << "socket success";
    }
    void Start()
    {
        while (true)
        {
            char buff[256];
            struct sockaddr_in peer;
            socklen_t len;
            // 接受信息
            int n = recvfrom(_sockfd, buff, sizeof(buff), 0, (struct sockaddr *)&peer, &len);
            if (n < 0)
            {
                // 读取失败
                LOG(Level::WARNING) << "recvfrom error";
                continue;
            }
            buff[n] = '\0';
            std::cout << "recv massage :" << buff << std::endl;
            // 发送信息
            int m = sendto(_sockfd, buff, sizeof(buff), 0, (struct sockaddr *)&peer, len);
            if (m < 0)
            {
                // 读取失败
                LOG(Level::WARNING) << "sendto error";
                continue;
            }
        }
    }
private:
    int _sockfd;
    // std::string _ip;
    uint16_t _port;
};

到这里本篇文章内容就结束了,感谢各位大佬的支持

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2oul0hvapjsws

相关推荐
左师佑图3 小时前
Apache POI 在 Linux 无图形界面环境下因字体配置问题导致Excel导出失败的解决方案
linux·apache·excel
前端世界3 小时前
鸿蒙网络优化实战:从智能切换到缓存加速的完整指南
网络·缓存·harmonyos
手握风云-3 小时前
JavaEE 初阶第二十五期:IP协议,网络世界的 “身份通行证”(一)
网络
深思慎考8 小时前
LinuxC++项目开发日志——基于正倒排索引的boost搜索引擎(2——Parser解析html模块)
linux·c++·搜索引擎
冠希陈、9 小时前
云锁客户端连不上服务器处理
linux·运维·服务器
似水এ᭄往昔9 小时前
【Linux】--入门、基础命令
linux·服务器
做运维的阿瑞9 小时前
Linux 企业级备份体系实战:cron/anacron/restic/rclone 对比与脚本总结
linux·运维·服务器·后端·学习·系统架构·centos
喜欢吃燃面9 小时前
Linux:基本指令(一)
linux·学习
ziyue75759 小时前
idea终端添加git-bash,支持linux的shell语法
linux·git·bash·idea·软件