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

网络编程套接字(一)

目录

预备知识

理解源IP地址和目的IP地址

理解源MAC地址和目的MAC地址

认识端口号

理解 "端口号" 和 "进程ID"

理解源端口号和目的端口号

认识TCP协议和UDP协议

网络字节序

socket编程接口

socket 常见API

sockaddr结构

简单的UDP网络程序

服务端创建套接字

sockaddr 结构

sockaddr_in 结构

in_addr结构

服务端绑定

字符串IP VS 整数IP

运行服务器

客户端创建套接字

关于客户端的绑定问题

启动客户端

本地测试

INADDR_ANY

网络测试

地址转换函数

​编辑

​编辑

windows客户端与linux服务器互通

使用linux终端模拟聊天室

popen函数


预备知识

理解源IP地址和目的IP地址

因特网上的每台计算机都有一个唯一的IP地址,如果一台主机上的数据要传输到另一台主机,那么对端主机的IP地址就应该作为该数据传输时的目的IP地址。但仅仅知道目的IP地址是不够的,当对端主机收到该数据后,对端主机还需要对该主机做出响应,因此对端主机也需要发送数据给该主机,此时对端主机就必须知道该主机的IP地址。因此一个传输的数据当中应该涵盖其源IP地址目的IP地址,目的IP地址表明该数据传输的目的地,源IP地址作为对端主机响应时的目的IP地址。在数据进行传输之前,会先自顶向下贯穿网络协议栈完成数据的封装,其中在网络层封装的IP报头当中就涵盖了源IP地址和目的IP地址。

理解源MAC地址和目的MAC地址

大部分数据的传输都是跨局域网的,数据在传输过程中会经过若干个路由器,最终才能到达对端主机。


源MAC地址目的MAC地址是包含在链路层的报头当中的,而MAC地址实际只在当前局域网内有效,因此当数据跨网络到达另一个局域网时,其源MAC地址和目的MAC地址就需要发生变化,因此当数据达到路由器时,路由器会将该数据当中链路层的报头去掉,然后再重新封装一个报头,此时该数据的源MAC地址目的MAC地址发生了变化

数据在传输的过程中是有两套地址:

  • 一套是源IP地址和目的IP地址,这两个地址在数据传输过程中基本是不会发生变化的(存在一些特殊情况,比如在数据传输过程中使用NET技术,其源IP地址会发生变化,但至少目的IP地址是不会变化的)。
  • 另一套就是源MAC地址和目的MAC地址,这两个地址是一直在发生变化的,因为在数据传输的过程中路由器不断在进行解包和重新封装。

认识端口号

端口号(port)是传输层协议的内容.

  • 端口号是一个2字节16位的整数;
  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
  • 一个端口号只能被一个进程占用;

    理解 "端口号" 和 "进程ID"

    端口号(port)的作用唯一标识 一台主机上的某个进程,进程ID(PID)的作用也是唯一标识一台主机上的某个进程,那在进行网络通信时为什么不直接用PID来代替port呢?

    1.不是所有的进程都需要网络通信,但是所有的进程都需要pid

    2.从技术角度,使用pid来充当端口号是决对可以的,pid属于系统模块,若使用pid来充当端口号,系统部分只要出现问题,网络部分就一定会受影响,属于强耦合,所以就有了端口号的出现,其目的就是为了解耦合

    底层如何通过port找到对应进程的?

    实际底层采用哈希的方式,每个元素都是进程PCB的指针,建立和端口号的映射关系,当底层拿到端口号时就可以直接执行对应的哈希算法,然后就能够找到该端口号对应的进程。

    理解源端口号和目的端口号

    现在通过IP地址MAC地址已经能够将数据发送到对端主机了,但实际我们是想将数据发送给对端主机上的某个服务进程,此外,数据的发送者也不是主机,而是主机上的某个进程,比如当我们用浏览器访问数据时,实际就是浏览器进程向对端服务进程发起的请求。

    socket通信的本质

    现在通过IP地址和MAC地址已经能够将数据发送到对端主机了,但实际我们是想将数据发送给对端主机上的某个服务进程,此外,数据的发送者也不是主机,而是主机上的某个进程,比如当我们用浏览器访问数据时,实际就是浏览器进程向对端服务进程发起的请求

    也就是说,socket通信本质上就是两个进程之间在进行通信,只不过这里是跨网络的进程间通信。比如逛淘宝和刷抖音的动作,实际就是手机上的淘宝进程和抖音进程在和对端服务器主机上的淘宝服务进程和抖音服务进程之间在进行通信。

    因此进程间通信的方式除了管道、消息队列、信号量、共享内存等方式外,还有套接字,只不过前者是不跨网络的,而后者是跨网络的

    在应用层,网路通信的本质:就是基于网络协议栈的进程间通信

    tip:不管进程有多少个,一台主机上的网路协议栈只有一套

    理解socket这个名字

    socket在英文上有“插座”的意思,插座上有不同规格的插孔,我们将插头插入到对应的插孔当中就能够实现电流的传输。在进行网络通信时,客户端就相当于插头,服务端就相当于一个插座,但服务端上可能会有多个不同的服务进程(多个插孔),因此当我们在访问服务时需要指明服务进程的端口号(对应规格的插孔),才能享受对应服务进程的服务。

    认识TCP协议和UDP协议

    网络协议栈是贯穿整个体系结构的,在应用层、操作系统层和驱动层各有一部分。当我们使用系统调用接口实现网络数据通信时,不得不面对的协议层就是传输层,而传输层最典型的两种协议就是TCP协议和UDP协议。

    TCP协议

    TCP协议叫做传输控制协议(Transmission Control Protocol),TCP协议是一种面向连接的可靠的、基于字节流的传输层通信协议。

    TCP协议是面向连接的,如果两台主机之间想要进行数据传输,那么必须要先建立连接,当连接建立成功后才能进行数据传输。其次,TCP协议是保证可靠的协议,数据在传输过程中如果出现了丢包、乱序等情况,TCP协议都有对应的解决方法。

    UDP协议

    UDP协议叫做用户数据报协议(User Datagram Protocol),UDP协议是一种无需建立连接的不可靠的面向数据报的传输层通信协议。

    使用UDP协议进行通信时无需建立连接,如果两台主机之间想要进行数据传输,那么直接将数据发送给对端主机就行了,但这也就意味着UDP协议是不可靠的,数据在传输过程中如果出现了丢包、乱序等情况,UDP协议本身是不知道的。

    既然UDP协议是不可靠的,那为什么还要有UDP协议的存在?

    TCP协议是一种可靠的传输协议,使用TCP协议能够在一定程度上保证数据传输时的可靠性,而UDP协议是一种不可靠的传输协议,UDP协议的存在有什么意义?

    首先,可靠是需要我们做更多的工作的,TCP协议虽然是一种可靠的传输协议,但这一定意味着TCP协议在底层需要做更多的工作,因此TCP协议底层的实现是比较复杂的,我们不能只看到TCP协议面向连接可靠这一个特点,我们也要能看到TCP协议对应的缺点。

    同样的,UDP协议虽然是一种不可靠的传输协议,但这一定意味着UDP协议在底层不需要做过多的工作,因此UDP协议底层的实现一定比TCP协议要简单,UDP协议虽然不可靠,但是它能够快速的将数据发送给对方,虽然在数据在传输的过程中可能会出错。

    编写网络通信代码时具体采用TCP协议还是UDP协议,完全取决于上层的应用场景。如果应用场景严格要求数据在传输过程中的可靠性,此时我们就必须采用TCP协议,如果应用场景允许数据在传输出现少量丢包,那么我们肯定优先选择UDP协议,因为UDP协议足够简单。

    注意: 一些优秀的网站在设计网络通信算法时,会同时采用TCP协议和UDP协议,当网络流畅时就使用UDP协议进行数据传输,而当网速不好时就使用TCP协议进行数据传输,此时就可以动态的调整后台数据通信的算法。

    网络字节序

    我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分,那么如何定义网络数据流的地址呢?

    • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出
    • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存
    • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址
    • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节
    • 不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据
    • 如果当前发送主机是小端,就需要先将数据转成大端,否则就忽略直接发送即可

    为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络 字节序和主机字节序的转换。

    • 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数
    • 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送
    • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回 
    • 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回

    socket编程接口

    socket 常见API

    sockaddr结构 socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同.

    // 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)int socket(int domain, int type, int protocol);// 绑定端口号 (TCP/UDP, 服务器)int bind(int socket, const struct sockaddr *address, socklen_t address_len);// 开始监听socket (TCP, 服务器)int listen(int socket, int backlog);// 接收请求 (TCP, 服务器)int accept(int socket, struct sockaddr* address, socklen_t* address_len);// 建立连接 (TCP, 客户端)int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

    sockaddr结构

    sockaddr结构的出现

    套接字不仅支持跨网络的进程间通信,还支持本地的进程间通信(域间套接字)。在进行跨网络通信时我们需要传递的端口号和IP地址,而本地通信则不需要,因此套接字提供了sockaddr_in结构体和sockaddr_un结构体,其中sockaddr_in结构体是用于跨网络通信的,而sockaddr_un结构体是用于本地通信的。

    为了让套接字的网络通信和本地通信能够使用同一套函数接口,于是就出现了sockeaddr结构体,该结构体与sockaddr_insockaddr_un的结构都不相同,但这三个结构体头部的16个比特位都是一样的,这个字段叫做协议家族。


    此时当我们在传递在传参时,就不用传入sockeaddr_insockeaddr_un这样的结构体,而统一传入sockeaddr这样的结构体。在设置参数时就可以通过设置协议家族这个字段,来表明我们是要进行网络通信还是本地通信,在这些API内部就可以提取sockeaddr结构头部的16位进行识别,进而得出我们是要进行网络通信还是本地通信,然后执行对应的操作。此时我们就通过通用sockaddr结构,将套接字网络通信和本地通信的参数类型进行了统一。

    注意: 实际我们在进行网络通信时,定义的还是sockaddr_in这样的结构体,只不过在传参时需要将该结构体的地址类型进行强转为sockaddr*罢了。

    套接字编程的种类:

    1.域间套接字编程

    2.原始套接字编程

    3.网络套接字编程

    为什么会有这么多本地进程间通信的方式?

    本地进程间通信的方式已经有管道、消息队列、共享内存、信号量等方式了,现在在套接字这里又出现了可以用于本地进程间通信的域间套接字,为什么会有这么多通信方式,并且这些通信方式好像并不相关?

    实际是因为早期有很多不同的实验室都在研究通信的方式,由于是不同的实验室,因此就出现了很多不同的通信方式,比如常见的有System V标准的通信方式和POSIX标准的通信方式。

    • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型,16位端口号和32位IP地址。
    • IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6。这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。
    • socket API可以都用struct sockaddr* 类型表示,在使用的时候需要强制转化成sockaddr_in;这样的好处是程序的通用性,可以接收IPv4、IPv6,以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数。

    为什么没有用void*代替struct sockaddr*类型?

    我们可以将这些函数的struct sockaddr*参数类型改为void*,此时在函数内部也可以直接指定提取头部的16个比特位进行识别,最终也能够判断是需要进行网络通信还是本地通信,那为什么还要设计出sockaddr这样的结构呢?

    实际在设计这一套网络接口的时候C语言还不支持void*,于是就设计出了sockaddr这样的解决方案。并且在C语言支持了void*之后也没有将它改回来,因为这些接口是系统接口,系统接口是所有上层软件接口的基石,系统接口是不能轻易更改的,否则引发的后果是不可想的,这也就是为什么现在依旧保留sockaddr结构的原因。

    简单的UDP网络程序

    服务端创建套接字

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

    sockaddr 结构

    /* Structure describing a generic socket address.  */
    struct sockaddr{__SOCKADDR_COMMON (sa_);	/* Common data: address family and length.  */char sa_data[14];		/* Address data.  */};

    sockaddr_in 结构

    /* Structure describing an Internet socket address.  */
    struct sockaddr_in{__SOCKADDR_COMMON (sin_);in_port_t sin_port;			/* Port number.  */struct in_addr sin_addr;		/* Internet address.  *//* Pad to size of `struct sockaddr'.  */unsigned char sin_zero[sizeof (struct sockaddr)- __SOCKADDR_COMMON_SIZE- sizeof (in_port_t)- sizeof (struct in_addr)];};

    可以看到,Struct sockaddr_in当中的成员如下:

    • sin_family:表示协议家族。
    • sin_port:表示端口号,是一个16位的整数。
    • sin_addr:表示IP地址,是一个32位的整数。

    剩下的字段一般不做处理,当然你也可以进行初始化。

    虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主 要有三部分信息: 地址类型, 端口号, IP地址.

    in_addr结构

    /* Internet address.  */
    typedef uint32_t in_addr_t;
    struct in_addr{in_addr_t s_addr;};

    in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数;

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

    socket函数

    创建套接字的函数叫做socket,该函数的函数原型如下:

    int socket(int domain, int type, int protocol);
    

    参数说明:

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

    返回值说明:

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

    socket函数属于什么类型的接口?

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

    socket函数是被谁调用的?

    socket这个函数是被程序调用的,但并不是被程序在编码上直接调用的,而是程序编码形成的可执行程序运行起来变成进程,当这个进程被CPU调度执行到socket函数时,然后才会执行创建套接字的代码,也就是说socket函数是被进程所调用的。

    socket函数底层做了什么?

    当我们调用socket函数创建套接字时,实际相当于我们打开了一个“网络文件”,打开后在内核层面上就形成了一个对应的struct file结构体,同时该结构体被连入到了该进程对应的文件双链表,并将该结构体的首地址填入到了fd_array数组当中下标为3的位置,此时fd_array数组中下标为3的指针就指向了这个打开的“网络文件”,最后3号文件描述符作为socket函数的返回值返回给了用户。

    其中每一个struct file结构体中包含的就是对应打开文件各种信息,比如文件的属性信息、操作方法以及文件缓冲区等。其中文件对应的属性在内核当中是由struct inode结构体来维护的,而文件对应的操作方法实际就是一堆的函数指针(比如read*write*)在内核当中就是由struct file_operations结构体来维护的。而文件缓冲区对于打开的普通文件来说对应的一般是磁盘,但对于现在打开的“网络文件”来说,这里的文件缓冲区对应的就是网卡。


    对于一般的普通文件来说,当用户通过文件描述符将数据写到文件缓冲区,然后再把数据刷到磁盘上就完成了数据的写入操作。而对于现在socket函数打开的“网络文件”来说,当用户将数据写到文件缓冲区后,操作系统会定期将数据刷到网卡里面,而网卡则是负责数据发送的,因此数据最终就发送到了网络当中。

    服务端创建套接字

    #include <cstring>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include "log.hpp"Log lg;const uint16_t defaultport = 8080;
    const std::string defaultip = "113.45.60.212";class UdpServer
    {
    public:UdpServer(const uint16_t &defaultport, const std::string &defaultip): _sockfd(0){}void init(){_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if(socket < 0){lg(FATAL, "socket fail, errno: %d, strerror: %s", errno, strerror(errno));exit(0);}lg(INFO, "socket success, errno: %d, strerror: %s", errno, strerror(errno));}~UdpServer(){if(_sockfd > 0) close(_sockfd);}
    private:int _sockfd;
    };

    注意: 当析构服务器时,我们可以将sockfd对应的文件进行关闭,但实际上不进行该操作也行,因为一般服务器运行后是就不会停下来的。

    测试套接字是否创建成功:

    #include "UdpServer.hpp"
    #include <memory>int main()
    {std::unique_ptr<UdpServer> Udp(new UdpServer);Udp->init();return 0;
    }

    服务端绑定

    我们只是在系统层面上打开了一个文件,操作系统将来并不知道是要将数据写入到磁盘还是刷到网卡,此时该文件还没有与网络关联起来。

    由于现在编写的是不面向连接的UDP服务器,所以初始化服务器要做的第二件事就是绑定。

    bind函数

    原型如下:

    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    

    参数说明:

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

    返回值说明:

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

    在进行绑定的时候需要将IP地址和端口号告诉对应的网络文件,此时就可以改变网络文件当中文件操作函数的指向,将对应的操作函数改为对应网卡的操作方法,此时读数据和写数据对应的操作对象就是网卡了,所以绑定实际上就是将文件和网络关联起来。

    由于绑定时需要知道是那台主机上的进程,所以需要用到IP地址和端口号,因此我们需要在服务器类当中引入IP地址和端口号,在创建服务器对象时需要传入对应的IP地址和端口号,此时我们就可以根据传入的IP地址和端口号对对应的成员进行初始化。

    服务端绑定

    套接字创建完毕后我们就需要进行绑定了,但在绑定之前我们需要先定义一个struct sockaddr_in结构,将对应的网络属性信息填充到该结构当中。由于该结构体当中还有部分选填字段,因此我们最好在填充之前对该结构体变量里面的内容进行清空,然后再将协议家族、端口号、IP地址等信息填充到该结构体变量当中。

    需要注意的是,在发送到网络之前需要将端口号设置为网络序列,由于端口号是16位的,因此我们需要使用前面说到的htons函数将端口号转为网络序列。此外,由于网络当中传输的是整数IP,我们需要调用inet_addr函数将字符串IP转换成整数IP,然后再将转换后的整数IP进行设置。

    当网络属性信息填充完毕后,由于bind函数提供的是通用参数类型,因此在传入结构体地址时还需要将struct sockaddr_in*强转为struct sockaddr*类型后再进行传入。

    #include <string>
    #include <cstring>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include "log.hpp"Log lg;const uint16_t defaultport = 8080;
    const std::string defaultip = "0.0.0.0";class UdpServer
    {
    public:UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip) : _ip(ip), _port(port), _sockfd(0), _isrunning(false){}void init(){_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if(socket < 0){lg(FATAL, "socket fail, errno: %d, strerror: %s", errno, strerror(errno));exit(0);}lg(INFO, "socket success, sockfd: %d, errno: %d, strerror: %s", _sockfd, errno, strerror(errno));struct sockaddr_in Server;Server.sin_family = AF_INET;Server.sin_addr.s_addr = inet_addr(_ip.c_str()); //将字符串风格的ip转化为uint32_t类型的整型Server.sin_port = htons(_port);socklen_t len = sizeof(Server);if(bind(_sockfd, (struct sockaddr *)&Server, len) < 0){lg(FATAL, "bind fail, errno: %d, strerror: %s", errno, strerror(errno));exit(0);}lg(INFO, "bind success, errno: %d, strerror: %s", errno, strerror(errno));}~UdpServer(){if(_sockfd > 0) close(_sockfd);}
    private:uint16_t _port;std::string _ip;int _sockfd;
    };

    字符串IP VS 整数IP

    IP地址的表现形式有两种:

    • 字符串IP:类似于192.168.66.88这种字符串形式的IP地址,叫做基于字符串的点分十进制IP地址
    • 整数IP:IP地址在进行网络传输时所用的形式,用一个32位的整数来表示IP地址。

    整数IP存在的意义

    如果我们在网络传输时直接以基于字符串的点分十进制IP的形式进行IP地址的传送,那么此时一个IP地址至少就需要15个字节,但实际并不需要耗费这么多字节。
    P地址实际可以划分为四个区域,其中每一个区域的取值都是0~255,而这个范围的数字只需要用8个比特位就能表示,因此我们实际只需要32个比特位就能够表示一个IP地址。其中这个32位的整数的每一个字节对应的就是IP地址中的某个区域,我们将IP地址的这种表示方法称之为整数IP,此时表示一个IP地址只需要4个字节。

    因为采用整数IP的方案表示一个IP地址只需要4个字节,并且在网络通信也能表示同样的含义,因此在网络通信时就没有用字符串IP而用的是整数IP,因为这样能够减少网络通信时数据的传送。

    字符串IP和整数IP相互转换的方式

    先创建一个struct ip的结构体,其中有四个成员,part1,part2,part3,part4都是uint8_t类型的

    inet_addr函数

    实际在进行字符串IP和整数IP的转换时,我们不需要自己编写转换逻辑,系统已经为我们提供了相应的转换函数,我们直接调用即可。

    将字符串IP转换成整数IP的函数叫做inet_addr,该函数的函数原型如下:

    in_addr_t inet_addr(const char *cp);
    

    inet_ntoa函数

    将整数IP转换成字符串IP的函数叫做inet_ntoa,该函数的函数原型如下:

    char *inet_ntoa(struct in_addr in);
    

    需要注意的是,传入inet_ntoa函数的参数类型是in_addr,因此我们在传参时不需要选中in_addr结构当中的32位的成员传入,直接传入in_addr结构体即可。

    运行服务器

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

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

    recvfrom函数

    UDP服务器接收数据的函数叫做recvfrom,该函数的函数原型如下:

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

    返回值说明:

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

    注意:

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

    启动服务器函数

    由于云服务器的原因,后面实际不需要传入IP地址,因此在运行服务器的时候我们只需要传入端口号即可,目前我们就手动将IP地址设置为127.0.0.1。IP地址为127.0.0.1实际上等价于localhost表示本地主机,我们将它称之为本地环回,相当于我们一会先在本地测试一下能否正常通信,然后再进行网络通信的测试。

    int main(int argc, char* argv[])
    {if(argc != 2){Usage(argv[0]);exit(0);}uint16_t port = std::stoi(argv[1]);std::unique_ptr<UdpServer> udp(new UdpServer(port, "127.0.0.1"));udp->init();udp->Run(fun);return 0;
    }
    

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

    虽然现在客户端代码还没有编写,但是我们可以通过netstat命令来查看当前网络的状态,这里我们可以选择携带nlup选项。

    netstat常用选项说明:

    • -n:直接使用IP地址,而不通过域名服务器。
    • -l:显示监控中的服务器的Socket。
    • -t:显示TCP传输协议的连线状况。
    • -u:显示UDP传输协议的连线状况。
    • -p:显示正在使用Socket的程序识别码和程序名称。

    此时你就能查看到对应网络相关的信息,在这些信息中程序名称为./UdpServer的那一行显示的就是我们运行的UDP服务器的网络信息。

    你可以尝试去掉-n选项再查看,此时原本显示IP地址的地方就变成了对应的域名服务器。

    其中netstat命令显示的信息中,Proto表示协议的类型,Recv-Q表示网络接收队列,Send-Q表示网络发送队列,Local Address表示本地地址,Foreign Address表示外部地址,State表示当前的状态,PID表示该进程的进程ID,Program name表示该进程的程序名称。

    其中Foreign Address写成0.0.0.0:*表示任意IP地址、任意的端口号的程序都可以访问当前进程。

    客户端创建套接字

    关于客户端的绑定问题

    首先,由于是网络通信,通信双方都需要找到对方,因此服务端和客户端都需要有各自的IP地址和端口号,只不过服务端需要进行端口号的绑定,而客户端不需要。

    因为服务器就是为了给别人提供服务的,因此服务器必须要让别人知道自己的IP地址和端口号,IP地址一般对应的就是域名,而端口号一般没有显示指明过,因此服务端的端口号一定要是一个众所周知的端口号,并且选定后不能轻易改变,否则客户端是无法知道服务端的端口号的,这就是服务端要进行绑定的原因,只有绑定之后这个端口号才真正属于自己,因为一个端口只能被一个进程所绑定,服务器绑定一个端口就是为了独占这个端口。

    而客户端在通信时虽然也需要端口号,但客户端一般是不进行绑定的,客户端访问服务端的时候,端口号只要是唯一的就行了,不需要和特定客户端进程强相关。

    如果客户端绑定了某个端口号,那么以后这个端口号就只能给这一个客户端使用,就是这个客户端没有启动,这个端口号也无法分配给别人,并且如果这个端口号被别人使用了,那么这个客户端就无法启动了。所以客户端的端口只要保证唯一性就行了,因此客户端端口可以动态的进行设置,并且客户端的端口号不需要我们来设置,当我们调用类似于sendto这样的接口时,操作系统会自动给当前客户端获取一个唯一的端口号。

    也就是说,客户端每次启动时使用的端口号可能是变化的,此时只要我们的端口号没有被耗尽,客户端就永远可以启动。

    启动客户端

    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <cstring>
    #include "log.hpp"Log lg;
    const int size = 4096;void Usage(std::string proc)
    {std::cout << "\n\rUsage: " << proc << " port ip" << std::endl;
    }int main(int argc, char *argv[])
    {// ./UdpClient port ipif(argc != 3){Usage(argv[0]);exit(0);}int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (socket < 0){lg(FATAL, "socket fail, errno: %d, strerror: %s", errno, strerror(errno));exit(0);}lg(INFO, "socket success, sockfd: %d, errno: %d, strerror: %s", sockfd, errno, strerror(errno));struct sockaddr_in Server;bzero(&Server, sizeof(Server));Server.sin_family = AF_INET;Server.sin_port = htons(stoi(argv[1]));Server.sin_addr.s_addr = inet_addr(argv[2]);socklen_t len = sizeof(Server); char buffer[size];std::string message;while(true){std::cout << "Client Say@ ";getline(std::cin, message);sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&Server, len);struct sockaddr Client;socklen_t len = sizeof(Client);ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&Client, &len);}close(sockfd);return 0;
    }

    需要注意的是,argv数组里面存储的是字符串,而端口号是一个整数,因此需要使用stoi或者atoi函数将字符串转换成整数。然后我们就可以用这个IP地址和端口号来构造客户端了,客户端构造完成并初始化后就可以调用Start函数启动客户端了。

    本地测试

    现在服务端和客户端的代码都已经编写完毕,我们可以先进行本地测试,此时服务器没有绑定外网,绑定的是本地环回。现在我们运行服务器时指明端口号为8080,再运行客户端,此时客户端要访问的服务器的IP地址就是本地环回127.0.0.1,服务端的端口号就是8080。

    此时我们再用netstat命令查看网络信息,可以看到服务端的端口是8080,客户端的端口是59248。这里客户端能被netstat命令查看到,说明客户端也已经动态绑定成功了,这就是我们所谓的网络通信。

    INADDR_ANY

    现在我们已经通过了本地测试,接下来就需要进行网络测试了,那是不是直接让服务端绑定我的公网IP,此时这个服务端就能够被外网访问了呢?

    理论上确实是这样的,就比如我的服务器的公网IP是113.45.60.212,这里用ping命令也是能够ping通的。


    现在我将服务端设置的本地环回改为我的公网IP,此时当我们重新编译程序再次运行服务端的时候会发现服务端绑定失败。

    由于云服务器的IP地址是由对应的云厂商提供的,这个IP地址是虚拟的,这个IP地址是不能直接被绑定的,如果需要让外网访问,此时我们需要bind 0。系统当当中提供的一个INADDR_ANY,这是一个宏值,它对应的值就是0。

    因此如果我们需要让外网访问,那么在云服务器上进行绑定时就应该绑定INADDR_ANY,此时我们的服务器才能够被外网访问。

    绑定INADDR_ANY的好处

    当一个服务器的带宽足够大时,一台机器接收数据的能力就约束了这台机器的IO效率,因此一台服务器底层可能装有多张网卡,此时这台服务器就可能会有多个IP地址,但一台服务器上端口号为8080的服务只有一个。这台服务器在接收数据时,这里的多张网卡在底层实际都收到了数据,如果这些数据也都想访问端口号为8080的服务。此时如果服务端在绑定的时候是指明绑定的某一个IP地址,那么此时服务端在接收数据的时候就只能从绑定IP对应的网卡接收数据。而如果服务端绑定的是INADDR_ANY,那么只要是发送给端口号为8080的服务的数据,系统都会可以将数据自底向上交给该服务端。

    因此服务端绑定INADDR_ANY这种方案也是强烈推荐的方案,所有的服务器具体在操作的时候用的也就是这种方案。

    当然,如果你既想让外网访问你的服务器,但你又指向绑定某一个IP地址,那么就不能用云服务器,此时可以选择使用虚拟机或者你自定义安装的Linux操作系统,那个IP地址就是支持你绑定的,而云服务器是不支持的。

    网络测试

    静态编译客户端

    我们可以将生成的客户端的可执行程序发送给你的其他朋友,进行网络级别的测试。为了保证程序在你们的机器是严格一致的,可以选择在编译客户端时携带-static选项进行静态编译。

    此时由于客户端是静态编译的,可以看到生成的客户端的可执行程序要比服务端大得多。

    分发客户端

    此时我们可以先使用sz命令将该客户端可执行程序下载到本地机器,然后将该程序发送给你的朋友。而我们分发客户端的过程实际上就是我们在网上下载各种PC端软件的过程,我们下软件下的实际上就是客户端的可执行程序,而与之对应的服务端就在Linux服务器上部署着。

    当你的朋友收到这个客户端的可执行程序后,可以通过rz命令或拖拽的方式将这个可执行程序上传到他的云服务器上,然后通过chmod命令给该文件加上可执行权限。

    地址转换函数

    基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP地址但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示和in_addr表示之间转换;

    字符串转in_addr的函数:

    in_addr转字符串的函数:

    其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void *addrptr。

    关于inet_ntoa

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

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

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

    因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果,所以该函数是存在线程安全的,但是在ubentu上测试, 并没有出现问题, 可能内部的实现加了互斥锁,所以系统还提供了inet_ntop函数,该函数也是进行整型转字符串的,但是维护数据的缓冲区由用户自己提供

    多线程调用inet_ntoa代码示例

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <pthread.h>void *Func1(void *p)
    {struct sockaddr_in *addr = (struct sockaddr_in *)p;while (1){char *ptr = inet_ntoa(addr->sin_addr);printf("addr1: %s\n", ptr);}return NULL;
    }void *Func2(void *p)
    {struct sockaddr_in *addr = (struct sockaddr_in *)p;while (1){char *ptr = inet_ntoa(addr->sin_addr);printf("addr2: %s\n", ptr);}return NULL;
    }int main()
    {pthread_t tid1 = 0;struct sockaddr_in addr1;struct sockaddr_in addr2;addr1.sin_addr.s_addr = 0;addr2.sin_addr.s_addr = 0xffffffff;pthread_create(&tid1, NULL, Func1, &addr1);pthread_t tid2 = 0;pthread_create(&tid2, NULL, Func2, &addr2);pthread_join(tid1, NULL);pthread_join(tid2, NULL);return 0;
    }

    windows客户端与linux服务器互通

    不同系统进程可能不一样,但是所有系统的网络体系都是一样的,linux上客户端的代码windows也能使用

    Windows客户端代码:与linux不同的是需要引入一个静态库,初始化网络环境,清理Winsock资源,其他基本一样

    #include <iostream>
    #include <string.h>
    #include <string>
    #include <WinSock2.h>
    #include <windows.h>#pragma warning(disable:4996)
    #pragma comment(lib,"ws2_32.lib")const std::string Serverip = "127.0.0.1";
    const uint16_t Serverport = 8080;int main()
    {// 0. 初始化网络环境WSADATA wsaData;if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0){printf("初始化Winsock失败\n");return -1;}printf("初始化Winsock成功\n");// 此处放置网络通信代码...SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){perror("main::socket");exit(0);}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());char buffer[4096];std::string message;while (true){std::cout << "Client Say@ ";getline(std::cin, message);sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&Server, sizeof(Server));struct sockaddr Client;int len = sizeof(Client);int s = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&Client, &len);if (s > 0){buffer[s] = 0;}}closesocket(sockfd);// 清理Winsock资源WSACleanup();return 0;
    }

    linux服务端代码:

    #include <iostream>
    #include <functional>
    #include <string>
    #include <unordered_map>
    #include <cstring>
    #include <strings.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include "log.hpp"Log lg;typedef std::function<std::string(const std::string &)> func_t;const uint16_t defaultport = 8080;
    const std::string defaultip = "0.0.0.0";enum
    {SOCKET_ERR = 1,BIND_ERR
    };class UdpServer
    {
    public:UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip) : _sockfd(0), _port(port), _ip(ip), _isrunning(false){}void init(){// 1.创建udp socket// 第一个参数: 套接字种类(使用IPV4网络协议) 第二个参数: sockets所对应的类型: 流式套接字/用户数据报套接字(udp) 第三个参数:协议类型: 默认设置为0即可_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){lg(FATAL, "socket create fail, sockfd: %d", _sockfd);exit(SOCKET_ERR);}lg(INFO, "socket create success, sockfd: %d", _sockfd);// 2. bind socket// 设置struct socketaddr_in结构体信息struct sockaddr_in local;bzero(&local, sizeof(local));// 结构体只能整体赋值不能整体初始化local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = inet_addr(_ip.c_str());// 将你刚才在用户级别设置好的数据绑定进内核(网络)if (bind(_sockfd, (const struct sockaddr *)&local, sizeof(local)) < 0){lg(FATAL, "bind fail, errno: %d, strerror: %s", errno, strerror(errno));exit(BIND_ERR);}lg(INFO, "bind success, errno: %d, strerror: %s", errno, strerror(errno));}void Run(){_isrunning = true;// 接收客户端发来的数据char buffer[SIZE];// 服务器一直运行while (_isrunning){// 还需要知道是那个主机发过来的struct sockaddr_in Client;socklen_t len = sizeof(Client);// 接收数据ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&Client, &len);if (n < 0){lg(WARNING, "recvfrom fail, errno: %d, strerror: %s\n", errno, strerror(errno));// 由于服务器需要一直运行,收不到客户端请求则继续等待// 结束本次循环continue;}buffer[n] = 0; // 设置结束符// 获取客户端ip portstd::cout << buffer << std::endl;// 处理数据std::string str = buffer;std::string echo_string = fun(str);// 响应sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (const struct sockaddr *)&Client, len);}}~UdpServer(){if (_sockfd > 0)close(_sockfd);}private:int _sockfd;uint16_t _port;std::string _ip;bool _isrunning;
    };

    运行测试:由于window和linux上的编码可能不同,所以会导致两边信息不一致,这属于正常情况

    使用linux终端模拟聊天室

    客户端代码:

    #include <cstdio>
    #include <cstring>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include "Terminal.hpp"
    #include "log.hpp"Log lg;enum
    {SOCKET_ERR = 1,};struct ThreadData
    {struct sockaddr_in Server;std::string Serverip;int sockfd;
    };void Usage(std::string str)
    {std::cout << "\n\rUsage: " << str << " Clientport Clientip !" << std::endl;
    }void *send_message(void *args)
    {std::string message;ThreadData* td = static_cast<ThreadData*>(args);socklen_t len = sizeof(td->Server);std::string welcome = td->Serverip;welcome += " comming...";sendto(td->sockfd, welcome.c_str(), welcome.size(), 0, (struct sockaddr *)&(td->Server), len);while (true){std::cerr << "Client Say@ ";// 我们在聊天室进行多人聊天时每个人,就算我不发消息我也是可以收到别人的消息的,但是以下单进程是是做不到的// 单线程,如果键盘资源没就绪,该进程就会处于阻塞状态,执行不了后续的代码,你就收不到后续别人发送的信息了getline(std::cin, message);// std::cout << message << std::endl;// 发送数据时,系统就绑定服务器的端口号和ip// 发给谁sendto(td->sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)(&td->Server), len);}
    }void *recv_message(void *args)
    {// OpenTerminal();ThreadData *td = static_cast<ThreadData*>(args);char buffer[SIZE];// 客户端启动while (true){memset(buffer, 0, sizeof(buffer));// 接收服务端响应数据struct sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t s = recvfrom(td->sockfd, buffer, SIZE, 0, (struct sockaddr *)&temp, &len);if (s > 0){buffer[s] = 0;std::cout << buffer << std::endl;}}
    }int main(int argc, char *argv[])
    {// ./UdpClient Clientport Clientipif (argc != 3){Usage(argv[0]);exit(0);}ThreadData *td = new ThreadData;uint16_t ServerPort = std::stoi(argv[1]);std::string ServerIp = argv[2];bzero(&td->Server, sizeof(td->Server));td->Server.sin_family = AF_INET; // 网络通信类型 网络通信td->Server.sin_addr.s_addr = inet_addr(ServerIp.c_str());td->Server.sin_port = htons(ServerPort);// 创建套接字, 返回一个网络套接字描述符td->sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (td->sockfd < 0){lg(FATAL, "socket create fail, errno: %d, strerror: %s\n", errno, strerror(errno));exit(SOCKET_ERR);}lg(INFO, "socket create success, errno: %d, strerror: %s\n", errno, strerror(errno));pthread_t render, recver;pthread_create(&render, nullptr, send_message, td);pthread_create(&recver, nullptr, recv_message, td);pthread_join(render, nullptr);pthread_join(recver, nullptr);close(td->sockfd);return 0;
    }
    #include <iostream>
    #include <functional>
    #include <string>
    #include <unordered_map>
    #include <cstring>
    #include <strings.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include "log.hpp"Log lg;typedef std::function<std::string(const std::string &)> func_t;const uint16_t defaultport = 8080;
    const std::string defaultip = "0.0.0.0";enum
    {SOCKET_ERR = 1,BIND_ERR
    };class UdpServer
    {
    public:UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip) : _sockfd(0), _port(port), _ip(ip), _isrunning(false){}void init(){// 1.创建udp socket// 第一个参数: 套接字种类(使用IPV4网络协议) 第二个参数: sockets所对应的类型: 流式套接字/用户数据报套接字(udp) 第三个参数:协议类型: 默认设置为0即可_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){lg(FATAL, "socket create fail, sockfd: %d", _sockfd);exit(SOCKET_ERR);}lg(INFO, "socket create success, sockfd: %d", _sockfd);// 2. bind socket// 设置struct socketaddr_in结构体信息struct sockaddr_in local;bzero(&local, sizeof(local));// 结构体只能整体赋值不能整体初始化local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = inet_addr(_ip.c_str());// local.sin_addr.s_addr = htonl(INADDR_ANY); // 将ip地址设置为0.0.0.0,让它能接收任意ip地址,有些机器网卡可能有多个// 将你刚才在用户级别设置好的数据绑定进内核(网络)if (bind(_sockfd, (const struct sockaddr *)&local, sizeof(local)) < 0){lg(FATAL, "bind fail, errno: %d, strerror: %s", errno, strerror(errno));exit(BIND_ERR);}lg(INFO, "bind success, errno: %d, strerror: %s", errno, strerror(errno));}// 查看用户是否存在,不存在就放入unordered_map中void CheckUser(struct sockaddr_in &Client, const uint16_t &Clientport, const std::string &Clientip){auto iter = online_user.find(Clientip);if (iter == online_user.end()){online_user.insert({Clientip, Client});std::cout << "[" << Clientport << " : " << Clientip << "]" << "add to online user" << std::endl;}}// 将响应发送给所有客户端void broadcast(const std::string info, uint16_t &Clientport, const std::string &Clientip){for (const auto &user : online_user){std::string message;message += "[";message += Clientip;message += " : ";message += std::to_string(Clientport);message += "]# ";message += info;socklen_t len = sizeof(user.second);sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&user.second, len);}}void Run(){_isrunning = true;// 接收客户端发来的数据char buffer[SIZE];// 服务器一直运行while (_isrunning){// 还需要知道是那个主机发过来的struct sockaddr_in Client;socklen_t len = sizeof(Client);// 接收数据ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&Client, &len);if (n < 0){lg(WARNING, "recvfrom fail, errno: %d, strerror: %s\n", errno, strerror(errno));// 由于服务器需要一直运行,收不到客户端请求则继续等待// 结束本次循环continue;}buffer[n] = 0; // 设置结束符// 获取客户端ip portstd::string Clientip = inet_ntoa(Client.sin_addr);uint16_t Clientport = ntohs(Client.sin_port);CheckUser(Client, Clientport, Clientip);std::string info = buffer;broadcast(info, Clientport, Clientip);}}
    
    #include "log.hpp"
    #include "UdpServer.hpp"
    #include <memory>void Usage(std::string proc)
    {std::cout << "\n\rUsage: " << proc << "ServerPort" << std::endl;
    }std::string fun(const std::string& str)
    {std::string ret = "Server Say@ " + str;std::cout << ret << std::endl;return ret;
    }int main(int argc, char* argv[])
    {// ./UdpServer ServerPortif(argc != 2){Usage(argv[0]);exit(0);}uint16_t port = std::stoi(argv[1]);std::unique_ptr<UdpServer> udp(new UdpServer(port));udp->init();udp->Run();return 0;
    }
    

    日志打印系统:

    #pragma once#include <iostream>
    #include <time.h>
    #include <stdarg.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <stdlib.h>using namespace std;#define SIZE 4096enum LogLevel
    {INFO,DEBUG,WARNING,ERROR,FATAL
    };class Log
    {
    public:string LevelToString(int level) //需要传入枚举类型,我们使用的是枚举类进行进行判断{switch(level){case INFO :return "INFO";case DEBUG :return "DEBUG";case WARNING :return "WARNING";case ERROR :return "ERROR";case FATAL :return "FATAL";default : return "none";}return "none";}void operator()(int Log_Level, const char *format, ...){//左半部分 日志等级 + 日志时间time_t t = time(nullptr);struct tm* pt = localtime(&t);char leftbuffer[SIZE];snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]: ",LevelToString(Log_Level).c_str(),pt->tm_year + 1900, pt->tm_mon + 1, pt->tm_mday,pt->tm_hour, pt->tm_min, pt->tm_sec);//右半部分 自定义部分va_list s;va_start(s, format);char rightbuffer[SIZE];//第三个参数不能使用%s或者其他格式控制,需要放入format进行格式错误,不然容易造成段错误vsnprintf(rightbuffer, sizeof(rightbuffer), format, s); va_end(s);//进行打印char buffer[SIZE * 2];snprintf(buffer, sizeof(buffer), "%s %s", leftbuffer, rightbuffer);printf("%s\n", buffer);return;}
    };
    

    运行测试:

    popen函数

    popen是C语言中标准库中的一个函数,在内部它会帮你fork()创建一个子进程来执行一个外部命令,并建立父子进程的管道,然后通过管道将子进程的运行结果返回给调用方。在大多数主流的操作系统中都有实现。

    popen原型如下:

    FILE *popen(const char *command, const char *type);

    command:需要执行的指令

    type:只能是读或者写中的一种,得到的返回值(标准 I/O 流)也具有和 type 相应的只读或只写类型。如果 type 是 “r” 则文件指针连接到 command 的标准输出;如果 type 是 “w” 则文件指针连接到 command 的标准输入。

    std::string Excutecommand(const std::string cmd)
    {// 第一个参数:我们需要执行的指令,第二个参数:我们需要对执行之后的信息进行读取操作//popen会帮你fork()一个子进程,帮你给父子进程建立一个管道,然后将子进程执行完的指令信息放入管道中//通过设置第二个参数,对管道中的数据进行读取FILE *fp = popen(cmd.c_str(), "r"); if (fp == nullptr){perror("main::popen");exit(0);}std::string result;char buffer[4096];while(true){char *ok = fgets(buffer, sizeof(buffer), fp);if(ok == nullptr) break;result += buffer;}pclose(fp);return result;
    }

    例如,我们通过执行ls -l命令来获取当前目录下所有文件的详细信息。popen函数使用第一个参数"ls -l"来执行这个命令,而第二个参数"r"表示我们要读取这个命令的输出。执行命令后,通过fgets函数逐行读取输出,并打印到控制台上。最后使用pclose函数来关闭popen打开的文件流。

    需要注意的是,在使用popen函数的时候,尽量不要执行不可信任的命令,避免造成安全问题。

    http://www.xdnf.cn/news/293149.html

    相关文章:

  • PriorityQueue
  • 使用 Semantic Kernel 快速对接国产大模型实战指南(DeepSeek/Qwen/GLM)
  • Web前端开发:Grid 布局(网格布局)
  • ts学习(1)
  • 2024年408真题及答案
  • C++ 外观模式详解
  • php8 枚举使用教程
  • 稀疏性预测算法初步
  • 健康养生:从微小改变开始
  • 【YOLO11改进】改进Conv、颈部网络STFEN、以及引入PIOU用于小目标检测!
  • 基于Vue3开发:打造高性能个人博客与在线投票平台
  • 【MATLAB例程】基于RSSI原理的Wi-Fi定位程序,N个锚点(数量可自适应)、三维空间,轨迹使用UKF进行滤波,附代码下载链接
  • 反射-探索
  • CASS 3D使用等高线修改插件导致修后等高线高程变化的问题
  • 当前人工智能领域的主流高级技术及其核心方向
  • 10.施工测量
  • 引领变革的“Vibe Coding”:AI辅助编程的崛起与挑战
  • 某信服EDR3.5.30.ISO安装测试(一)
  • printf的终极调试大法
  • 分析 Docker 磁盘占用
  • FTP/TFTP/SSH/Telnet
  • FastMCP - 快速、Pythonic风格的构建MCP server 和 client
  • [人机交互]交互设计
  • Qwen3的“混合推理”是如何实现的
  • Kotlin-空值和空类型
  • 【AI提示词】SCAMPER法专家
  • 【最新Python包管理工具UV的介绍和安装】
  • SIFT算法详细原理与应用
  • 嵌入式模数转换原理与程序设计
  • 新型深度神经网络架构:ENet模型