您好,欢迎来到图艺博知识网。
搜索
您的当前位置:首页【计算机网络】简易UDP网络小程序

【计算机网络】简易UDP网络小程序

来源:图艺博知识网

1. socket函数:创建套接字

我们把服务封装成一个类,当我们定义出一个服务器对象后需要马上初始化服务器,而初始化服务区需要做的第一件事就是创建套接字。

  • domain:创建套接字的域或者叫做协议家族,也就是套接字的类型。该参数就相当于struct sockaddr的前十六位。如果是本地通信就设为AF_UNIX,如果是网络通信就设置为AF_INET(IPv4)或者AF_INET6(IPv6)。
  • type:创建套接字时所需的服务类型。其中最常见的套接字服务类型是SOCK_STREAMSOCK_DREAM。如果是基于UDP的网络通信,我们采用的就是SOCK_DGRAM,叫做用户数据报服务,如果是基于TCP的网络通信,我们采用的就是SOCK_STREAM,叫做流式套接字,提供的是流式服务。
  • protocol:创建套接字的协议类别。可以指明为TCP或者UDP,但该字段一般直接设置为0就可以了,设置为0表示的就是默认,此时会根据传入的前两个参数自动推导出你用的是哪种协议。

返回值说明:

  • 套接字创建成功返回文件描述符,创建失败返回-1,同时错误码会被设置。

socket属于什么接口?

网络协议栈是分层的,根据TCP/IP四层协议,自顶向下以此是应用层、传输层、网络层、数据链路层。而我们现在所写的代码都叫做用户级代码,也就是说我们是在应用层编写代码,因此我们调用的实际是下三层的接口,而传输层和网络层都是在操作系统内完成的,也就意味着我们在应用层调用的接口都叫做系统调用接口。

socker函数在底层做了什么?

socket函数是被进程所调用的,而每一个进程在系统层面上都有一个管理进程的PCB、文件描述符表(files_struct)以及对于打开的文件。而文件描述符表内包含了一个数组fd_array,其中数组的0、1、2下标默认被标准输入、标准输出以及标准错误所占用。

2. 服务端

2.1 服务端创建套接字

当我们在进行初始化服务器创建套接字时,就是调用socket函数创建套接字,创建套接字我们要传入的协议家族就是AF_INET,表明我们要进行的是网络通信,而我们需要的服务类型是SOCK_DGRAM,因为我们现在编写的UDP服务器是面向数据报的,而第三个参数设置为0即可。

#pragma once

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>

class UdpServer
{
public:
    bool InitServer()
    {
        // 创建套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            std::cerr << "socket error" << std::endl;
            return false;
        }
        std::cout << "socket create success, sockfd: " << _sockfd << std::endl;
        return true;
    }

    ~UdpServer()
    {
        if (_sockfd >= 0)
            close(_sockfd);
    }

private:
    int _sockfd; // 文件描述符
};

上面那段代码在hpp头文件中,下面我们在源文件中包含它,并运行起来,看看结果。

#include "UdpServer.hpp"

int main()
{
    UdpServer* svr = new UdpServer();
    svr->InitServer();
    return 0;
}

运行结果如下:

2.2 服务端绑定

现在套接字已经创建成功了,但只是在系统层面上打开了一个文件,操作系统并不知道是要将数据写入到磁盘还是网卡,此时该文件还没有与网络关联起来。
由于现在编写的是不面向连接的UDP服务器,所以初始化的第二件事就是绑定。

bind函数

服务端绑定用到的是bind函数

  • sockfd:绑定的文件的文件描述符。也就是我们创建套接字时获取到的文件描述符。
  • addr:网络相关的属性信息,包括协议家族、IP地址、端口号等。
  • addrlen:传入的addr结构体的长度

返回值说明:

  • 绑定成功返回0,失败返回-1,同时错误码会被设置。

struct sockaddr_in结构体

在绑定时我们需要将网络相关的属性信息填充到一个结构体当中,然后将该结构体作为bind函数的第二个参数进行传入,这实际就是struct sockaddr_in结构体。

struct sockaddr_in当中的成员如下:

  • sin_family:表示协议家族
  • sin_port:表示端口号,是一个16位的整数
  • sin_addr:表示ip地址,是一结构体,该结构体当中只有一个32位的整数,ip地址实际存储在这个整数中。

剩下的字段一般不作处理。

如何理解绑定?

class UdpServer
{
public:
    UdpServer(std::string ip, int port)
        : _sockfd(-1), _port(port), _ip(ip)
    {};

    bool InitServer()
    {
        // 创建套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            std::cerr << "socket error" << std::endl;
            return false;
        }
        std::cout << "socket create success, sockfd: " << _sockfd << std::endl;
        return true;
    }

    ~UdpServer()
    {
        if (_sockfd >= 0)
            close(_sockfd);
    }

private:
    int _sockfd; // 文件描述符
    int _port; // 端口号
    std::string _ip; // IP地址
};

服务端绑定

需要注意的是,在发送到网络之前需要将端口号设置为网络序列,由于端口号是16位的,因此我们需要使用前面说到的htons函数将端口号转为网络序列。此外,由于网络中传输的是整数IP,我们需要调用inet_addr函数将字符串转换成整数IP,然后再将转换后的整数IP进行设置。(inet_addr函数同时做了将字符串转换为整数,以及将主机字节序转换为网络字节序两件事)。

    bool InitServer()
    {
        // 创建套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            std::cerr << "socket error" << std::endl;
            return false;
        }
        std::cout << "socket create success, sockfd: " << _sockfd << std::endl;

        // 填充网络相关信息
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = inet_addr(_ip.c_str());

        // 绑定
        int ret = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
        if (ret < 0)
        {
            std::cerr << "bind error" << std::endl;
            return false;
        }
        std::cout << "bind success" << std::endl;
        
        return true;
    }

2.3 字符串IP和整数IP

整数IP存在的意义

inet_addr函数

在进行字符串IP和网络IP的转换时,系统为我们提供了相应的转换函数,我们直接调用即可。

inet_ntoa函数

将整数IP转换为字符串IP函数为inet_ntoa

2.4 运行服务器

UDP服务器的初始化只需要创建套接字和绑定,当服务器初始化完毕之后就可以启动服务了。

服务器实际上就是在周而复始地为我们提供某种服务,服务器在运行起来之后就是一个死循环。由于UDP服务器是不面向连接的,因此只要UDP服务器启动后,就可以直接读取客户端发来的数据。

recvfrom

UDP服务器读取数据的函数叫做recvfrom

参数说明:

  • sockfd:对应操作的文件描述符,表示从该文件描述符索引的文件当中读取数据。
  • buf:读取数据的存放位置
  • len:最大读取数据的字节数
  • flags:读取方式,一般设置为0,表示阻塞读取
  • src_addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等
  • addrlen:调用时传入期望读取的src_addr结构体长度,返回时代表实际读取到的src_addr结构体的长度,这是一个输入输出型参数。

返回值说明:

  • 读取成功时返回实际读取到的字节数,读取失败返回-1,同时错误码会被设置

注意:

  • 由于UDP是不面向连接的,因此我们除了读取到数据意外还需要获取到对端网络相关的属性信息,包括IP地址和端口号。
  • 在调用recvfrom读取数据时,必须将addrlen设置为你要读取的结构体对应的大小。
  • 由于recvfrom函数提供的参数也是struct sockaddr* 类型的,因此我们传入结构体地址时需要将struct sockaddr_in* 进行强转。

代码实现

    void start()
    {
#define SIZE 1024
        char buffer[SIZE];
        while (1) 
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t size = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
            if (size > 0)
            {
                buffer[size] = 0;
                int port = ntohs(peer.sin_port);
                std::string ip = inet_ntoa(peer.sin_addr);
                std::cout << ip << " : " << port << "# " << buffer << std::endl;
            }
            else
            {
                std::cerr << "recvfrom error" << std::endl;
            }
        }
    }

注意:如果调用recvform函数读取数据失败,我们可以打印一条提示信息,但是不要让服务器退出,服务器不能因为读取某一个客户端的数据失败就退出。

引入命令行参数

int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " port" << std::endl;
        return 1;
    }
    std::string ip = "127.0.0.1"; // 本地环回
    int port = atoi(argv[1]);
    UdpServer* svr = new UdpServer(ip, port);
    svr->InitServer();
    svr->start();
    return 0;
}

此时运行程序,就可以看到套接字创建成功、绑定成功了,现在服务器就是在等待客户端发送数据。

我们可以通过netstat命令查看当前网络的状态

netstat命令常用选项说明:

  • -n:以数字格式显示网络地址和端口,而不进行主机名或服务名的解析
  • -l:显示监控中服务器的socket
  • -t:显示TCP传输协议的连线情况
  • -u:显示UDP协议的连线情况
  • -p:显示正在使用socket的程序识别码和程序名称
  • Proto:表示协议的类型
  • Recv-Q:表示网络接收队列
  • Send-Q:表示网络发送队列
  • Local Address:表示本地地址
  • Foreign Address:表示外部地址
  • State:表示当前的装填
  • PID:表示该进程的进程ID
  • Program name:表示该进程的程序名称

3. 客户端

3.1 客户端创建套接字

同样地,我们把客户端也封装成一个类,当我们定义出一个客户端对象后也是需要对其进行初始化,而客户端在初始化是也需要创建套接字,之后客户端发送数或接收数据也就是对这个套接字进行操作。

客户端创建套接字时选择的协议家族也是AF_INET,需要的服务类型也是SOCK_DGRAM,当客户端被析构时也可以选择对应的套接字。与服务端不同的是,客户端在初始化时只需要创建套接字就行了,不需要程序员进行绑定。

class UdpClient
{
public:
    bool InitClient()
    {
        // 创建套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            std::cerr << "socket error" << std::endl;
            return false;
        }
        return true;
    }

    ~UdpClient()
    {
        if (_sockfd > 0)
            close(_sockfd);
    }
private:
    int _sockfd;
}

客户端要不要进行bind?

client必须要进行bind,要有自己的IP和port,说不要bind是错的,只不过client不需要程序员去进行bind。服务端的port为了客户端更快地找到,它是固定不变的,但是客户端有很多,如果都要让客户处去显式地绑定一个port,程序员可能选的port是一样的。为了避免重复,port由操作系统自动生成并隐式地进行绑定。
所以,在UDP通信中写客户端时,只需要创建套接字即可,不需要bind,bind由操作系统自己完成!

3.2 启动客户端

class UdpClient
{
public:
    UdpClient(std::string server_ip, int server_port)
        : sockfd(-1), _server_ip(server_ip), _server_port(server_port)
    {}

    bool InitClient()
    {
        // 创建套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            std::cerr << "socket error" << std::endl;
            return false;
        }
        return true;
    }

    ~UdpClient()
    {
        if (_sockfd > 0)
            close(_sockfd);
    }
private:
    int _sockfd;
    int _server_port; // 服务端端口号
    std::string _server_ip // 服务端IP地址
}

sendto函数

UDP客户端发送数据的函数叫做sendto

参数说明:

  • sockfd:对应操作的文件描述符,表示将数据写入该文件描述符索引的文件当中
  • buf:待写入数据的存放位置
  • len:期望写入数据的字节数
  • flags:写入的方式。一般设置为0,表示阻塞写入。
  • dest_addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
  • addlen:传入dest_addr结构体的长度

返回值说明:

  • 写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。

注意:

  • 由于UDP是不面向连接的,因此除了待发送的数据以外还需要指明对端网络相关的信息,包括IP地址和端口号等。
  • 由于sendto函数提供的参数也是struct sockaddr* 类型的,因此我们在传入结构体地址时需要将struct sockaddr_in* 类型进行强转

启动客户端函数

现在客户端要发送数据给服务端,我们可以让客户端获取用户输入,不断将用户输入的数据发送给服务端。

    void start()
    {
        std::string msg;
        struct sockaddr_int peer;
        memset(&peer, 0, sizeof(peer));
        peer.sin_family = AF_INET;
        peer.sin_port = htons(_server_port);
        peer.sin_addr.s_addr = inet_addr(_server_ip.c_str());

        while (1)
        {
            std::cout << "Please Enter# ";
            getline(std::cin, msg);
            sendto(_sockfd, msg.c_str(), msg.size(), 0, (struct sockaddr*)&peer, sizeof(peer));
        }
    }

引入命令行参数

int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
        return 1;
    }
    std::string server_ip = argv[1];
    int server_port = atoi(argv[2]);

    UdpClient* clt = new UdpClient(server_ip, server_port);
    clt->InitClient();
    clt->start();

    return 0;
}

4. 本地测试

5. INADDR_ANY

绑定INADDR_ANY的好处

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- huatuoyibo.net 版权所有 湘ICP备2023021910号-2

违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务