『 Linux 』利用UDP套接字简单进行网络通信

文章目录


Socket常见API

  • 创建socket文件描述符 (TCP/UDP,客户端 + 服务端)

    cpp 复制代码
    NAME
           socket - create an endpoint for communication
    
    SYNOPSIS
           #include <sys/types.h>          /* See NOTES */
           #include <sys/socket.h>
    
           int socket(int domain, int type, int protocol);
    
    RETURN VALUE
           On success, a file descriptor for the new socket is returned.  On error, -1 is returned, and errno is set appropriately.

    该函数用于创建一个新的套接字以便进行网络通信的基础函数;

    在网络编程中套接字(socket)是一个端点,它支持在不同计算机之间传输数据;

    其参数如下:

    • int domain

      该参数指定协议簇,即套接字的地址族,常用的值有:

      • AF_INET

        表示使用IPv4网络协议;

      • AF_INET6

        表示使用IPv6网络协议;

      • AF_UNIX

        表示使用域间套接字(本地通信协议,也叫UNIX域socket);

      • AF_PACKET

        表示使用底层接口(用于直接访问网络设备,通常用于编写一些网络工具,例如抓包工具等);

    • int type

      该参数为指定的套接字类型,常用的值包括:

      • SOCK_STREAM

        流式套接字,提供面向连接,可靠的数据传输(如TCP);

      • SOCK_DGRAM

        数据报套接字,提供无连接的,不保证可靠的传输(如UDP);

      • SOCK_RAM

        原始套接字,提供对底层协议的直接访问;

    • int protocol

      该参数为特定于协议簇domain的协议,设置为0时系统将会自动选择合适的协议,常用值包括:

      • IPPROTO_TCP

        TCP协议;

      • IPPROTO_UDP

        UDP协议;

    该函数调用失败时返回-1并设置全局变量errno来指示错误类型,调用成功时则返回一个新的套接字文件描述符,是一个非负整数;

    返回一个套接字文件描述符本质为在Linux中具有 "一切皆文件" 的哲学;

    该函数的常见错误码如下:

    • EACCES

      权限被拒绝;

    • EAFNOSUPPORT

      不支持指定的地址族;

    • EINVAL

      无效参数;

    • EMFILE

      超出了每个进程可打开的文件/套接字限制;

    • ENFILE

      超出了系统范围内可打开的文件/套接字限制;

    • ENOMEMENOBUFS

      表示系统内存不足;

  • 绑定端口号(TCP/UDP , 服务器)

    cpp 复制代码
    NAME
           bind - bind a name to a socket
    
    SYNOPSIS
           #include <sys/types.h>          /* See NOTES */
           #include <sys/socket.h>
    
           int bind(int sockfd, const struct sockaddr *addr,
                    socklen_t addrlen);
    
    RETURN VALUE
           On success, zero is returned.  On error, -1 is returned,
           and errno is set appropriately.

    bind函数用于将一个套接字与一个特定的地址(通常为本地地址)绑定在一起;

    绑定操作是将套接字与一个特定的IP地址和端口号关联的必要步骤;

    其参数如下:

    • int sockfd

      该参数表示传入一个socket函数返回的一个套接字文件描述符,表示需要绑定的套接字;

    • const struct sockaddr *addr

      这个参数指向一个sockaddr结构的指针;

      这个结构包含了你要绑定的地址信息,具体的结构类型取决于使用的地址族;

      • AF_INET地址族

        cpp 复制代码
        struct sockaddr_in {
            sa_family_t    sin_family; // 地址族(应设置为 AF_INET)
            in_port_t      sin_port;   // 端口号(使用 htons 转换为网络字节序)
            struct in_addr sin_addr;   // IP地址(使用 in_addr 结构表示)
            char           sin_zero[8]; // 填充字段,通常设置为 0
        };
        
        struct in_addr {
            uint32_t s_addr;  // 32位的IPv4地址(使用 htonl 转换为网络字节序)
        };
      • AF_UNIX地址族

        cpp 复制代码
        struct sockaddr_un {
            sa_family_t sun_family;   // 地址族(应设置为 AF_UNIX)
            char sun_path[108]; // 文件系统中的路径名,用作通信端点
        };

      在根据不同的地址族传入对应的结构体指针后需要对该传入的结构体指针进行强制类型转换为struct sockaddr *,否则函数调用时参数将不匹配;

      这样的实现可以看作是一种多态的实现方式,本质上struct sockaddr可以看作是一个基类,struct sockaddr_instruct sockaddr_un看作是子类;

      基类接受子类的指针,根据不同类型子类的指针访问其对应成员;

    • socklen_t addrlen

      该参数指定addr结构的长度(字节数),其中socklen_t实际上是一个无符号整型unsigned_ttypedef;

      cpp 复制代码
      #define __U32_TYPE		unsigned int
      
      __STD_TYPE __U32_TYPE __socklen_t;
      
      typedef __socklen_t socklen_t;

      通常使用sizeof(struct sockaddr_in)sizeof(struct sockaddr_in6);

    该函数调用成功时返回0表示绑定成功,调用失败时返回-1并设置全局变量errno来指示错误类型;

    该函数调用时常见的错误码包括:

    • EACCES

      表示尝试绑定到一个受保护的端口(通常为小于1024的端口,即知名端口号);

    • EADDRINUSE

      表示指定的地址已经在使用中;

    • EAADRNOTAVAIL

      表示指定的地址不可用;

    • EBADF

      表示sockfd不是有效的文件描述符;

    • EINVAL

      表示套接字已经成功被绑定;

    • ENOTSOCK

      表示所传入的sockfd不是一个套接字文件描述符;

  • 开始监听 socket (TCP,服务器)

    cpp 复制代码
    NAME
           listen - listen for connections on a socket
    
    SYNOPSIS
           #include <sys/types.h>          /* See NOTES */
           #include <sys/socket.h>
    
           int listen(int sockfd, int backlog);
    
    RETURN VALUE
           On success, zero is returned.  On error, -1 is returned,
           and errno is set appropriately.

    该函数用于在套接字上监听连接请求;

    通常与服务端编程相关的使用场景种,该函数在创建和绑定一个套接字之后调用,为的是使该套接字接受来自客户端连接请求;

    参数如下:

    • int sockfd

      该参数表示传入一个由socketbind函数创建和绑定的套接字文件描述符,表示一个服务端的套接字;

    • int backlog

      该参数表示指定等待连接队列的最大长度,如果有更多的连接请求到来但队列已经满了,这些额外的连接请求将被拒绝或者忽略;

      这是内核为尚未处理的连接建立一个等待队列,其值会影响网络性能和并发能力;

    当函数调用成功时返回0表示监听成功;

    调用失败时返回-1并设置全局变量errno来指示错误类型;

    其可能出现的错误码为如下:

    • EADDRINUSE

      表示指定的地址已经被使用且该地址没有被关闭;

      或是套接字已经被监听;

    • EBADF

      表示sockfd不是有效的文件描述符;

    • ENOTSOCK

      表示sockfd不是一个套接字文件描述符;

    • EOPNOTSUPP

      表示该套接字不支持listen操作(例如原始套接字);

  • 接受请求 (TCP , 服务器)

    cpp 复制代码
    NAME
           accept, accept4 - accept a connection on a socket
    
    SYNOPSIS
           #include <sys/types.h>          /* See NOTES */
           #include <sys/socket.h>
    
           int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    
           #define _GNU_SOURCE             /* See feature_test_macros(7) */
           #include <sys/socket.h>
    
           int accept4(int sockfd, struct sockaddr *addr,
                       socklen_t *addrlen, int flags);
    
    RETURN VALUE
           On  success, these system calls return a nonnegative integer that is
           a descriptor for the accepted socket.  On error, -1 is returned, and
           errno is set appropriately.

    accept函数和accept4函数是用于从监听状态的套接字接受连接请求的系统调用;

    通常在调用listen函数后使用这两个函数来接受和处理客户端的连接请求;

    通常使用accept接口即可;

    参数如下:

    • int sockfd

      由之前对socket,bindlisten函数的调用创建并配置好的套接字文件描述符;

      出去纳入的套接字处于监听状态;

    • struct sockaddr *addr

      为一个输出型参数;

      指向一个struct sockaddr结构体的指针;

      accept成功返回时,这个结构体将包含连接上来的客户端地址信息,可传入nullptr表示无需接受客户端地址信息;

    • socklen_t addrlen

      同样为输出型参数;

      指向一个socklen_t类型的变量,用于存储addr结构体的大小;

      调用返回时,这个变量将包含客户端地址信息的实际大小,传入nullptr表示无需接受客户端信息;

    • flags (仅accept4)

      该参数为传递给accept4的额外标志,可用来设置一下选项:

      • SOCK_NONBLOCK

        使返回的文件描述符处于非阻塞模式;

      • SOCK_CLOEXEC

        为返回的文件描述符设置O_CLOEXEC执行时关闭标志;

    这个函数调用成功使返回一个非负整数,这个非负整数是新创建的已连接套接字的文件描述符,新描述符与请求连接的客户端通信;

    调用失败时返回-1并设置全局变量errno用于指示错误类型;

    常见的错误码为:

    • EAGAINEWOULDBLOCK

      表示套接字被标记为非阻塞模式,且没有挂起的连接;

    • EBADF

      表示无效文件描述符;

    • ECONNABORTED

      表示连接被终止;

    • EFAULT

      表示指针参数无效;

    • EINTR

      表示调用被信号中断;

    • EINVAL

      表示套接字未处于监听状态;

    • EMFILE

      表示进程文件描述符已满;

    • ENFILE

      表示系统文件描述符已满;

    • ENOTSOCK

      表示文件描述符不是套接字;

    • EOPNOTSUPP

      表示套接字不支持接受(accept)操作;

  • 建立连接 (TCP , 客户端)

    cpp 复制代码
    NAME
           connect - initiate a connection on a socket
    
    SYNOPSIS
           #include <sys/types.h>          /* See NOTES */
           #include <sys/socket.h>
    
           int connect(int sockfd, const struct sockaddr *addr,
                       socklen_t addrlen);
    
    RETURN VALUE
           If the connection or binding succeeds, zero is returned.  On  error,
           -1 is returned, and errno is set appropriately.

    这个函数用于在套接字上发起到指定地址的连接,用于将客户端的套接字连接到远程服务器的地址和端口,是客户端编程中的关键一步;

    其参数如下:

    • int sockfd

      该参数表示传入一个套接字文件描述符;

      这个套接字是之前调用socket函数创建的套接字;

    • struct sockaddr *addr

      指向一个struct sockaddr结构体的指针;

      该结构体包含了要连接的远程服务器的地址信息,对于IPv4地址,通常需要传入一个struct sockaddr_in类型的指针并强制类型转换为struct sockaddr指针类型;

      对于IPv6而言需要传入struct sockaddr_in6,UNIX域则传入struct sockaddr_un,对应的也是要强制类型转换为struct sockaddr*;

    • socklen_t addrlen

      表示传入一个socklen_t类型的变量作为指定addr结构体的大小,其中单位是字节;

    该函数调用成功时返回0表示连接操作成功;

    调用失败时则返回-1并设置全局变量errno用于指示错误类型;

    常见的错误码为:

    • EACCESEPERM

      表示权限问题导致连接被拒绝;

    • EADDRINUSE

      表示本地地址已在使用中,且需要绑定该地址;

    • EAFNOSUPPORT

      表示所提供的地址族在该套接字中不被支持;

    • EAGAIN

      表示临时资源不可用;

    • EALREADY

      表示套接字是非阻塞的,目前已有操作正在进行中;

    • EBADF

      表示无效文件描述符;

    • ECONNEREFUSED

      表示目标地址没有监听或主动拒绝连接;

    • EFAULT

      表示指针参数无效;

    • EINPROGRESS

      表示非阻塞套接字正在处理连接请求;

    • EINVAL

      表示套接字已绑定到本地地址或者参数无效;

    • ENETURNREACH

      表示网络无法到达目标地址;

    • ENOTSOCK

      表示文件描述符不是套接字;


转网络字节序

数据在进行网络传输的时候需要转成标准的网络字节序,本质原因是在数据传输的过程中网络通信的两端的字节序要相同以确保数据发送至对端时出现数据的解析错误;

  • hton 系列函数

    hton系列函数用于在网络编程中将主机字节序转换为网络字节序;

    确保数据在不同计算机体系结构之间传输时的一致性的重要步骤;

    • htons函数

      cpp 复制代码
      #include <arpa/inet.h>
      
      uint16_t htons(uint16_t hostshort);

      该函数用于将16位无符号短整数从主机字节序转换为网络字节序;

      返回值为转换后的网络字节序的16位无符号短整数;

    • htonl函数

      cpp 复制代码
      #include <arpa/inet.h>
      
      uint32_t htonl(uint32_t hostlong);

      该函数用于将32位无符号长整形从主机字节序转换位网络字节序;

      返回值为转换后的网络字节序的32位无符号长整形;

      该函数常用于将IP地址转换为网络字节序,尤其是在直接处理IP地址时,如设置套接字的IP时;

      cpp 复制代码
      struct sockaddr_in server_addr;
      server_addr.sin_addr.s_addr = htonl(INADDR_ANY);  // 将本地地址转换为网络字节序
      // 其中 INADDR_ANY 表示接收来自所有IP的数据
    • ntohs函数

      cpp 复制代码
      #include <arpa/inet.h>
      
      uint16_t ntohs(uint16_t netshort);

      该函数用于将16位无符号短整型从网络字节序转换位主机字节序;

      返回值为转换后的主机字节序的16位无符号短整型;

    • ntohl函数

      cpp 复制代码
      #include <arpa/inet.h>
      
      uint32_t ntohl(uint32_t netlong);

      该函数用于将32位无符号长整型从网络字节序转换位主机字节序;

      返回值为转换后的主机字节序的32位无符号长整型;

      该函数通常用于接收到的数据,特别是当你从网络上接收到一个以网络字节序编码的IP地址时;

      cpp 复制代码
      uint32_t ip = ntohl(server_addr.sin_addr.s_addr);  // 将网络字节序IP地址转换为主机字节序
  • inet 系列函数

    该系列函数主要用于在IP地址的字符串表示形式与其二进制表示形式之间进行转换;

    这些函数通常用于处理IP地址的格式转换和字节序的转换;

    • inet_aton函数

      cpp 复制代码
      int inet_aton(const char *cp, struct in_addr *inp);

      该函数用于将点分十进制表示的IPv4地址转换为二进制形式,同时检测输入地址的有效性;

      参数const char* cp表示点分十进制的IPv4地址字符串;

      参数struct in_addr *inp表示指向存储结果的struct in_addr;

      该函数调用成功时返回非零值,调用失败时返回0,通常为输入地址格式无效;

    • inet_addr

      cpp 复制代码
      in_addr_t inet_addr(const char *cp);

      该函数用于将点分十进制的IPv4地址转换为32位网络字节序地址(二进制形式);

      参数const char *cp表示传入点分十进制形式IPv4地址字符串;

      该函数调用成功时返回网络字节序的32位二进制表示;

      调用失败时返回INADDR_NONE(0xFFFFFFFF),通常位输入地址格式无效;

    • inet_ntoa

      cpp 复制代码
      char *inet_ntoa(struct in_addr in);

      该函数用于将二进制形式(32位网络字节序)的IPv4地址转换为点分十进制字符串表示;

      参数in表示网络字节序的IPv4地址结构;

      返回值为一个指向静态缓冲区的字符串指针,包含点分十进制的IPv4地址;

    • 其他函数

      • inet_network

        该函数主要用于返回给定IP地址网络部分(被废弃);

      • inet_makeaddr

        构造一个IP地址,通过结合网络号和主机号(用于一些特定场合);

      • inet_lnaof / inet_netof

        分别提取本地网络地址和网络字段,从实际IPv4地址中提取字部分;

      这些函数是底层操作的一部分,通常比较少直接使用;


网络数据传输的读

通常recv系列函数用于从套接字接收数据,用于读取从远程主机发送的数据;

  • recv函数

    cpp 复制代码
    #include <sys/types.h>
    #include <sys/socket.h>
    
    ssize_t recv(int sockfd, void *buf, size_t len, int flags);

    该函数用于从套接字中接收数据并将其存储在特定的缓冲区中,是一种最基本的从连接套接字(如TCP连接)接收数据的方法,适用于接收来自连接的连续字节流;

    参数如下:

    • int sockfd

      该参数表示套接字的文件描述符,从中接收数据;

    • void *buf

      指向要接收数据的缓冲区指针;

    • size_t len

      该参数表示缓冲区的长度,即最多可以接收的数据量;

    • int flags

      该参数指定接收操作的行为,常见标志包括:

      • MSG_WAITALL

        表示等待接收到所有请求的数据;

      • MSG_DONTWAIT

        表示非阻塞式接收数据;

      • MSG_PEEK

        表示查看数据但不移出队列(这里的队列指用于暂时存储通过网络接口接收到的数据的缓冲区);

    该函数调用成功时将返回接收到的数据的字节数,如果连接关闭则返回0,如果发生错误时则返回-1并设置errno全局变量来指示错误类型;

  • recvfrom函数

    cpp 复制代码
    #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);

    该函数用于从套接字中接收数据,并获取发送方的地址信息(通常用于UDP和其他无连接协议),在接收数据时同时获取对方发送的地址信息,这对于服务端处理多个客户端时非常有用;

    参数如下:

    • int sockfd

      表示套接字文件描述符,从中接收数据;

    • void *buf

      指向要接收数据的缓冲区指针;

    • size_t len

      表示缓冲区的长度,即最多可以接收的数据量;

    • int flags

      指定接收操作的行为,与上文的recv函数相同;

    • struct sockaddr *src_addr

      指向存储发送方地址的结构体指针,如果不关心发送方的地址可以传入nullptr;

    • socklen_t *addrlen

      指向存储src_addr大小的变量指针;

      在调用后,它将包含实际地址的大小;

    当函数调用成功时返回接收到的字节数,如果连接关闭则返回0,发生错误时返回-1并设置全局变量errno来指示错误类型;

  • recvmsg函数

    cpp 复制代码
    #include <sys/types.h>
    #include <sys/socket.h>
    
    ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

    该函数用于从套接字中接收数据并存储在msghdr结构中,该函数允许更复杂的接收操作,包括接收多个缓冲区的数据,辅助数据(文件描述符等)等;

    参数如下:

    • int sockfd

      表示套接字文件描述符,从中接收数据;

    • struct msghdr *msg

      指向msghdr结构体的指针,描述了接收缓冲区及相关信息;

      cpp 复制代码
      struct msghdr {
          void         *msg_name;       /* Optional address */
          socklen_t     msg_namelen;    /* Size of address */
          struct iovec *msg_iov;        /* Scatter/gather array */
          int           msg_iovlen;     /* # elements in msg_iov */
          void         *msg_control;    /* Ancillary data, see below */
          socklen_t     msg_controllen; /* Ancillary data buffer len */
          int           msg_flags;      /* Flags (unused with sendmsg) */
      };
    • int flags

      该参数指定接受操作的行为,与recv函数相同;

    该函数调用成功时返回接收到的字节数,如果连接关闭则返回0,发生错误时返回-1并设置全局变量errno用于指示错误类型;


网络数据传输的写

通常send系列函数用于写入数据到套接字中;

  • send函数

    cpp 复制代码
    #include <sys/types.h>
    #include <sys/socket.h>
    
    ssize_t send(int sockfd, const void *buf, size_t len, int flags);

    该函数用于向套接字发送数据,将缓冲区中的数据发送到已连接的套接字中,用于向TCP连接发送数据,是最常用的发送函数之一;

    参数如下:

    • int sockfd

      套接字文件描述符,表示目标连接;

    • const void *buf

      指向包含待发送数据缓冲区的指针;

    • size_t len

      缓冲区中待发送数据的长度;

    • int flags

      指定发送操作的行为,常用标志包括:

      • MSG_DONTWAIT

        表示非阻塞式发送数据;

      • MSG_NOSIGNAL

        表示不产生SIGPIPE信号;

  • sendto函数

    cpp 复制代码
    #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);

    用于发送数据到指定的地址,即使为建立连接,主要用于无连接协议(如UDP),在未经建立连接的情况下直接将数据包发送到目标地址;

    参数如下:

    • int sockfd

      套接字文件描述符;

    • const void *buf

      指向包含待发送数据缓冲区的指针;

    • size_t len

      缓冲区中待发送数据的长度;

    • int flags

      指定发送操作行为(同上);

    • const struct sockaddr *dest_addr

      指向包含目标地址的结构体指针;

    • socklen_t addrlen

      目标地址结构体的长度;

    函数调用成功是返回实际发送的字节数;

    发生错误时返回-1,并设置全局变量errno来指示错误类型;

  • sendmsg函数

    cpp 复制代码
    #include <sys/types.h>
    #include <sys/socket.h>
    
    ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

    使用消息结构体msghdr向套接字发送数据,支持高级功能如多缓冲区和辅助数据(如文件描述符),其结构如上(recvmsg函数中对该结构体有解释);

    参数如下:

    • int sockfd

      套接字文件描述符;

    • const struct msghdr *msg

      指向包含待发送信息的msghdr结构的指针;

    • int flags

      指定发送操作的行为(同上);

    该函数调用成功时返回实际发送的字节数,发生错误时返回-1并设置全局变量errno来指示错误类型;


简单的UDP网络程序服务端

基本结构

cpp 复制代码
/*
	UdpServer.hpp 文件 
	用于实现服务端
*/
#ifndef UDPSERVER_HPP
#define UDPSERVER_HPP

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

#include <cstring>
#include <iostream>
#include <memory>
#include <string>

#include "log.hpp"  // 引用日志插件

#define BUF_SIZE 1024

Log log_; // 实例化了一个日志对象

enum { SOCK_CREATE_FAIL = 1, SOCK_BIND_FAIL };

class UdpServer {
 public:
    // 构造函数初始化成员变量
  UdpServer(const uint16_t &port = defaultport,
            const std::string &ip = defaultip)
      : port_(port), ip_(ip), isrunning_(false) {}

  ~UdpServer() {
      if(sockfd_>0)
      close(sockfd_); // 关闭套接字文件描述符
  }

  void Init() {
  	// 用于实现服务端的初始化
      /*
      	1. 创建UDP socket
      	2. bind 绑定
      */
  }

  void Run() {
    // 用于服务端的运行
      /*
      	服务端需要一直处于运行状态
      	
      	1. 接收客户端传入的套接字信息
      	2. 处理用户所传入的数据
      */
  }

 private:
  int sockfd_;      // 套接字文件描述符
  uint16_t port_;   // 服务器进程端口号
  std::string ip_;  // IP 地址
  bool isrunning_;  // 表明服务器的运行状态

  static const std::string defaultip;  // 设置 ip 初始值
  static const uint16_t defaultport;   // 设置端口初始值
};

/*
  为静态的默认IP与端口设置初始值 (定义)
*/
const std::string UdpServer::defaultip = "0.0.0.0";
const uint16_t UdpServer::defaultport = 8080;

#endif

在该实现中引用了之前的Log日志插件,具体参考[Gitee - MyLogPlug];

引入了一系列的头文件用于支持后续需要的实现;

定义了一系列的成员变量包括服务端的套接字文件描述符sockfd_,服务端进程端口号port_,服务端IP地址ip_,以及表明服务端是否运行的标识符isrunning_;

其中设置了默认的服务端进程端口号与服务端的IP地址;

构造函数用于初始化服务端的成员变量,析构函数调用close关闭对应的套接字文件描述符;

声名了两个函数作为服务端的主要功能:

  • Init()

    该函数用于实现服务端中套接字的初始化以及将套接字绑定到指定的IP地址和端口号;

  • Run()

    该函数用于实现服务端的运行以及处理客户端所发的数据包;


Init() 服务端的初始化

服务端的初始化主要分为两个步骤,即创建服务端的套接字以及将套接字绑定到指定的IP地址和端口号;

  • 服务端的套接字创建

    cpp 复制代码
    void Init() {
        /*
          1. 创建 UDP socket
        */
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);  // 表示使用IPv4 UDP协议
        // SOCK_DGRAM 表示允许一个面向数据报 无连接 不可靠的数据传输
    
        if (sockfd_ < 0) {
          log_(FATAL, "socket create fail , the errornum : %d\n",
               sockfd_);  // 打印日志信息确认套接字创建是否成功
    
          exit(SOCK_CREATE_FAIL);  // 套接字创建失败时退出
        }
        log_(INFO, "socket create sucess , sockfd : %d", sockfd_);
    
        /*
          2. bind 绑定
        */
    	// ...
      }

    该函数中调用了socket()函数创建了一个套接字;

    cpp 复制代码
    sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);

    其中参数AF_INET表示使用IPv4协议,SOCK_DGRAM表示该套接字允许一个面向数据报且无连接同时不保证可靠性的数据传输;

    cpp 复制代码
    if (sockfd_ < 0)...

    当套接字创建失败时会返回-1,当套接字创建失败时接下来的操作都将不可进行否则可能会出现未定义行为,此时需要打印日志信息并退出进程;

  • 服务端的套接字绑定

    cpp 复制代码
    void Init() {
        /*
          1. 创建 UDP socket
        */
        //...
        
        
        /*
          2. bind 绑定
        */
    
        // 2.1 类似于设置阻塞信号集 此处只是设置了一个变量
        struct sockaddr_in localsr;
        bzero(&localsr, sizeof(localsr));
        localsr.sin_family = AF_INET;     // 表明结构体地址类型
        localsr.sin_port = htons(port_);  // 表明需要绑定的端口
        // localsr.sin_addr.s_addr = inet_addr(ip_.c_str());  //
        // 表明需要绑定的IP地址
        localsr.sin_addr.s_addr = INADDR_ANY;
        // bind 地址为0时表示可收到来自所有主机的数据 是一种比较推荐的做法
    
        /*
          其中端口号和IP地址必须是网络字节序的
          (IP与端口必定是客户端和服务端互相发送的)
          使用 htons 用于 uint16_t 的转网络字节序
          sin_addr是一个结构体 该结构体的s_addr成员才是需要填入的IP
          使用 inet_addr 用于 const char* 类型的IP 转 uint16_t(网络字节序)
        */
        // 2.2 进行 bind
        if (bind(sockfd_, (const struct sockaddr *)&localsr, sizeof(localsr))) {
          log_(FATAL, "socket bind fail, err string :%s", strerror(errno));
          exit(SOCK_BIND_FAIL);
        }
        // bind 成功
        log_(INFO, "socket bind sucess , sockfd : %d", sockfd_);
      }

    绑定的操作流程与设置阻塞信号集相似,需要创建一个对应地址族的结构体,为该结构体进行初始化而后才能进行绑定;

    cpp 复制代码
        struct sockaddr_in localsr;
        bzero(&localsr, sizeof(localsr));

    创建了一个sockaddr_in类型的对象localsr并调用bzero初始化该结构体;

    同样的该结构体需要表明结构体地址类型与表明需要绑定的端口以及表明需要绑定的IP地址;

    cpp 复制代码
        localsr.sin_family = AF_INET;     // 表明结构体地址类型
        localsr.sin_port = htons(port_);  // 表明需要绑定的端口
        // localsr.sin_addr.s_addr = inet_addr(ip_.c_str());  //
        // 表明需要绑定的IP地址
        localsr.sin_addr.s_addr = INADDR_ANY;
        // bind 地址为0时表示可收到来自所有主机的数据 是一种比较推荐的做法

    云服务器不允许被直接绑定,bind云服务器的公网IP时将会bind error;

    通常情况下服务端绑定的IP地址为0(INADDR_ANY)是比较推荐的做法,表示接收任何主机发送过来的数据包;

    当对应的结构体设置完毕后可进行bind绑定;

    cpp 复制代码
    if (bind(sockfd_, (const struct sockaddr *)&localsr, sizeof(localsr))) ... 

    此处在绑定时还判断了一次绑定是否成功,若绑定未成功则打印对应日志消息并退出进程(绑定失败为致命操作);


Run() 服务端的运行

服务端是需要一直运行的,所以在启动服务端后需要有一个对应的标识服务器是否启动的标识;

服务端的运行主要为维持服务端的运行,对数据进行处理;

cpp 复制代码
  void Run() {
    // 服务器需要一直运行
    isrunning_ = true;
    char inbuf[BUF_SIZE] = {0};
    while (isrunning_) {
      struct sockaddr_in client;
      socklen_t len = sizeof(client);
      bzero(&client, len);
      // 使用 recvfrom 接收来自客户端发送的消息
      size_t n = recvfrom(sockfd_, inbuf, sizeof(inbuf) - 1, 0,
                          (struct sockaddr *)&client, &len);
      // client 与 len 保存着客户端发送过来的套接字信息

      if (n < 0) {
        log_(WARNING, "recvfrom fail, err string :%s", strerror(errno));
        continue;
      }
      inbuf[n] = 0;  // 当字符串进行打印

      // 充当数据处理
      std::string info = inbuf;  // 组合字符串
      std::string echo_string = "server echo# " + info;

      sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0,
             (const struct sockaddr *)&client, len);
      // 将数据发回给客户端
    }
  }

该程序为一个简单的echo打印程序,主要逻辑为获取客户端发来的数据并进行简单处理,再将处理后的数据发回客户端进行显示;

定义了一个缓冲区char inbuf[BUF_SIZE] = {0};;

在接收客户端的数据时同样需要一个相同地址族的结构体,与对应的长度信息,两个信息作为输出型参数用于接收客户端发送的套接字信息;

cpp 复制代码
 while (isrunning_) {
      struct sockaddr_in client;
      socklen_t len = sizeof(client);
      bzero(&client, len);
     //...
 }

调用bzero对结构体进行初始化;

调用recvfrom接收来自客户端发送过来的数据包并存储进对应的缓冲区中;

cpp 复制代码
      // 使用 recvfrom 接收来自客户端发送的消息
size_t n = recvfrom(sockfd_, inbuf, sizeof(inbuf) - 1, 0,
                          (struct sockaddr *)&client, &len);

将数据简单进行组合作为数据的简单处理随后将数据发回客户端;

  • 服务端运行与数据处理的解耦合

    该段程序中对于数据处理和服务端运行(接收数据)耦合度过高,可以使用传递回调的方式进行解耦合;

    cpp 复制代码
    using func_t = std::function<std::string(const std::string &)>;
    // 使用function包装器包装一个函数类型 用于接收传入的处理数据的函数
    
    class UdpServer {
     public:
      UdpServer(const uint16_t port = defaultport)
          : sockfd_(0), port_(port), isrunning_(false) {}
    
      ~UdpServer() {
          // ...
      }
    
      void Init() {
      	// ...  
      }
    
      void Run(func_t fun) { // 传入一个回调函数
       // ...
          size_t n = recvfrom(sockfd_, inbuf, sizeof(inbuf) - 1, 0,
                              (struct sockaddr *)&client, &len);
          inbuf[n] = 0; 
    
          // 充当数据处理
          std::string info = inbuf;  // 组合字符串
          std::string echo_string = fun(inbuf); // 调用回调函数进行数据处理
          std::cout << echo_string << std::endl;
          // 打印处理好的数据...
        }
      }
    
     private:
    // 成员变量
    };

    对应的main函数传入一个函数进行回调即可;


服务端启动及测试

cpp 复制代码
#include <iostream>

#include "UdpServer.hpp"

using namespace std;
void Usage(std::string proc) {
    // 使用手册
  std::cout << "\n\tUsage: " << proc << " port[1024+]\n" << std::endl;
}

string func(const string& str) { // 定义一个函数用于数据的处理
  std::string echo_string = "server echo# " + str;
  return echo_string;
}

int main(int argc, char* argv[]) {
  if (argc != 2) {
      // 判断传入参数是否符合使用标准
    Usage(argv[0]);
    exit(0);
  }

  std::unique_ptr<UdpServer> svr(new UdpServer(stoi(argv[1])));  // 守卫智能指针
  svr->Init();
  svr->Run(func); // 传入处理数据函数
  return 0;
}

创建了一个函数Usag用于当用户传入的环境变量参数不符合标准时调用该函数展示对应的使用手册;

定义了一个函数用于数据的处理,该函数将作为可调用对象传入Run成员函数中;

使用防拷贝智能指针unique实例化一个服务端对象指针;

随后调用服务端的Init()Run()进行初始化与启动服务端;

运行结果为:

bash 复制代码
$ ./udpserver 8080
[INFO][2024-08-14 23:44:22] socket create sucess , sockfd : 3
[INFO][2024-08-14 23:44:22] socket bind sucess , sockfd : 3

程序正常运行,可调用netstat -naup查看对应网络信息;

bash 复制代码
$ netstat -naup
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
udp        0      0 0.0.0.0:68              0.0.0.0:*                           -                   
udp        0      0 127.0.0.1:323           0.0.0.0:*                           -                   
udp        0      0 0.0.0.0:8080            0.0.0.0:*                           12440/./udpserver   
udp6       0      0 ::1:323                 :::*                                -                   

其中12440/./udpserver表示运行成功;

启动服务端后可用nc工具向对应的服务端发送数据测试服务端是否有效;

bash 复制代码
# 服务端所在会话 - 启动服务端
$ ./udpserver 8000
[INFO][2024-08-15 14:36:40] socket create sucess , sockfd : 3
[INFO][2024-08-15 14:36:40] socket bind sucess , sockfd : 3


# 客户端所在会话(非同一网络的其他主机) - 通过 nc 工具发送消息给服务端和 server
$ echo "Hello, Server!" | nc -u xxx.xxx.xxx.xxx 8000 # xxx... 表示IP地址


# 服务端所在会话
[INFO][2024-08-15 14:51:29] recvfrom sucess
server echo# Hello, Server!

简单的UDP网络程序客户端

cpp 复制代码
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

#include <cstring>
#include <iostream>
#include <memory>
#include <string>

void Usage(std::string proc) {
  // 使用手册
  std::cout << "\n\tUsage: " << proc << " serverip serverport\n" << std::endl;
}

int main(int argc, char* argv[]) {
  // 判断环境变量参数是否符合使用标准
  if (argc != 3) {
    Usage(argv[0]);
    exit(0);
  }

  /*
    分解环境变量参数并进行转换为 IP 和端口
  */
  std::string serverip = argv[1];            // IP
  uint16_t serverport = std::stoi(argv[2]);  // 端口

  sockaddr_in local;             // 创建对应的地址族结构体
  bzero(&local, sizeof(local));  // 为结构体清零

  local.sin_family = AF_INET;  // 设置地址族为 IPv4

  local.sin_port = htons(serverport);  // 将端口号转换为网络字节序

  local.sin_addr.s_addr =
      inet_addr(serverip.c_str());  // 将IP地址转换为网络字节序并赋值

  socklen_t locallen = sizeof(local);  // 获取地址结构体的大小

  int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
  // 创建UDP套接字并保存文件描述符

  if (sockfd < 0) {
    // 判断套接字是否创建成功
    std::cout << "socket fail" << std::endl;
    exit(-1);
  }
  std::cout << "socket success" << std::endl;

  // 通常客户端不需要显式绑定自己的IP和端口
  // 一个端口号只能被一个进程bind,客户端端口号只需保证唯一性

  std::string message;      // 用于保存发送给服务端的数据
  char buffer[1024] = {0};  // 用于接收服务端返回的数据

  while (true) {
    std::cout << "Please Enter@";  // 打印提示信息

    getline(std::cin, message);  // 读取用户输入的消息

    int sdebug = sendto(sockfd, message.c_str(), message.size(), 0,
                        (struct sockaddr*)&local, locallen);
    // 调用 sendto 向服务端发送数据 // sdebug 为debug

    if (sdebug < 0) {
      std::cout << "sendto fail, err: " << strerror(errno) << std::endl;
    }
    std::cout << "sendto success" << std::endl;  // 发送成功

    // -------------------------------

    struct sockaddr_in temp;     // 用于存储服务端的地址信息
    bzero(&temp, sizeof(temp));  // 清空结构体
    socklen_t len = sizeof(temp);
    ssize_t n =
        recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);
    // 阻塞接收服务端的回应数据
    if (n > 0) {
      buffer[n] = 0;  // 将接收到的数据作为字符串处理
      std::cout << buffer << std::endl;
    }
  }

  close(sockfd);  // 关闭套接字文件描述符
  return 0;
}

客户端并未进行封装;

客户端主要进行两个操作,一个是向服务端发送数据,一个是接收服务端返回的处理后的数据包;

同样的定义了一个函数用于当参数传入不同时展现其使用手册;

cpp 复制代码
 std::string serverip = argv[1];            // IP
  uint16_t serverport = std::stoi(argv[2]);  // 端口

将传入的IP与端口号进行分离,便于后期sendto向服务端发送数据时使用;

cpp 复制代码
 sockaddr_in local;             // 创建对应的地址族结构体
  bzero(&local, sizeof(local));  // 为结构体初始化
  local.sin_family = AF_INET;  // 设置地址族为 IPv4
  local.sin_port = htons(serverport);  // 将端口号转换为网络字节序
  local.sin_addr.s_addr =
      inet_addr(serverip.c_str());  // 将IP地址转换为网络字节序并赋值
  socklen_t locallen = sizeof(local);  // 获取地址结构体的大小

根据地址族创建对应的地址族结构体并为这个地址族结构体进行初始化;

  • 客户端不需要显式bind端口号

    通常情况下客户端不需要显式bind端口号,原因是防止特别的端口号或者被其他进程绑定了的端口号被占用(一个端口号只能被一个进程bind);

    当客户端在进行sendto操作时操作系统将自行为进程动态分配端口号;

    这个端口号一般是由操作系统自由随机选择的;

此处的客户端模拟循环操作,即一个循环的会话,客户端可向服务端发送数据,服务端进行打印;

cpp 复制代码
 std::string message;      // 用于保存发送给服务端的数据
  char buffer[1024] = {0};  // 用于接收服务端返回的数据

  while (true) {
    std::cout << "Please Enter@";  // 打印提示信息
    getline(std::cin, message);  // 读取用户输入的消息
      ...
      ...
  }

创建一个套接字用于接收服务端处理并返回的数据,通过recvfrom函数接收;

cpp 复制代码
struct sockaddr_in temp;     // 用于存储服务端的地址信息
    bzero(&temp, sizeof(temp));  // 清空结构体
    socklen_t len = sizeof(temp);
    ssize_t n =
        recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);

可以在循环中设置特定操作用于退出客户端;


服务端客户端相互通信测试

分别在不同的主机上运行服务端和客户端;

bash 复制代码
# 服务端启动
$ ./udpserver 8000
[INFO][2024-08-15 15:26:10] socket create sucess , sockfd : 3
[INFO][2024-08-15 15:26:10] socket bind sucess , sockfd : 3

# 客户端启动并发送信息
$ ./udpclient xxx.xxx.xxx.xxx 8000
Please Enter@hello server!
server echo# hello server!

# 服务端接收信息
[INFO][2024-08-15 15:26:49] recvfrom sucess
server echo# hello server!

网络通信成功;


服务端通过传入命令处理实现远程命令执行

可以通过实现命令解析并处理的功能实现不同网络情况下对对端主机进行命令处理;

在此之前服务端的运行与数据的处理已经进行了解耦合;

现在只需要实现命令的解析及处理并传入服务端成员函数Run中就可以实现该功能;

cpp 复制代码
std::string HandlerCommand(const std::string& cmd) {
  // 打开管道,执行命令
  FILE* fp = popen(cmd.c_str(), "r");
  if (!fp) {
    perror("popen");
    return "error";
  }

  std::string ret;
  char buffer[4096];

  // 循环读取命令输出
  while (true) {
    char* res = fgets(buffer, sizeof(buffer), fp);
    if (res == nullptr) break;   // 到达文件末尾,或出错
    ret += std::string(buffer);  // 将命令输出追加到返回字符串中
  }

  // 关闭管道,并获取命令执行的返回值
  int status = pclose(fp);
  if (status == -1) {
    perror("pclose");
    return "error";
  }

  // 返回命令执行结果
  return ret;
}

通过使用popen打开管道执行命令;

使用fgets循环读取fp指针中的命令执行内容,将命令输出追加到返回字符串ret中;

最后使用结束后调用pclose关闭管道,随后返回命令执行结果;

在使用该数据处理函数时只需要将该可调用对象传入Run成员函数即可;

cpp 复制代码
int main(int argc, char* argv[]) {
  if (argc != 2) {
    Usage(argv[0]);
    exit(0);
  }

  std::unique_ptr<UdpServer> svr(new UdpServer(stoi(argv[1])));
  svr->Init();
  svr->Run(HandlerCommand); // 传入 HandlerCommand
  return 0;
}

测试如下:

bash 复制代码
# 服务端启动
$ ./udpserver 8000
[INFO][2024-08-15 15:52:45] socket create sucess , sockfd : 3
[INFO][2024-08-15 15:52:45] socket bind sucess , sockfd : 3

# 客户端启动并使用命令
Please Enter@ls
log.hpp
Main.cc
Makefile
noMyUDP
udpclient
UdpClient.cc
udpserver
UdpServer.hpp

Please Enter@

# 服务端接收
[INFO][2024-08-15 15:55:38] recvfrom sucess
log.hpp
Main.cc
Makefile
noMyUDP
udpclient
UdpClient.cc
udpserver
UdpServer.hpp

参考代码

[Gitee - 半介莽夫 / Dio夹心小面包]

相关推荐
Lary_Rock1 小时前
RK3576 LINUX RKNN SDK 测试
linux·运维·服务器
云飞云共享云桌面3 小时前
8位机械工程师如何共享一台图形工作站算力?
linux·服务器·网络
Peter_chq3 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
一坨阿亮4 小时前
Linux 使用中的问题
linux·运维
dsywws5 小时前
Linux学习笔记之vim入门
linux·笔记·学习
幺零九零零6 小时前
【C++】socket套接字编程
linux·服务器·网络·c++
wclass-zhengge6 小时前
Docker篇(Docker Compose)
运维·docker·容器
李启柱7 小时前
项目开发流程规范文档
运维·软件构建·个人开发·设计规范
小林熬夜学编程8 小时前
【Linux系统编程】第四十一弹---线程深度解析:从地址空间到多线程实践
linux·c语言·开发语言·c++·算法
力姆泰克8 小时前
看电动缸是如何提高农机的自动化水平
大数据·运维·服务器·数据库·人工智能·自动化·1024程序员节