C++Linux网络编程:简单的select模型运用

文章目录

    • 前言
    • 源代码
    • 部分重点解读
      • read/write与recv/send
        • 在使用上的差异

前言

这段代码来自于游双的《Linux高性能服务器编程》,在Ubuntu中对代码进行了实现,并在注释部分加上了我的个人解读

源代码

//
#include 
// 网络通讯的核心函数都在这
#include 
//
#include 
#include 
//
#include 
#include 
#include 
#include 
#include 
//
#include 
//
#include 

int main(int argc, char* argv[]){
    if(argc < 2){
        // basename这个函数在string.h中
        // 这个函数会去除文件名中的目录部分,只留下真正的文件名
        printf("usage:%s ip_address port_number\n", basename(argv[0]));
        return 1;
    }
    // 设置ip和port
    const char* ip = argv[1];
    // 这个atoi其实就是C++string中的stoi,其中的a是ASCII的缩写
    int port = atoi(argv[1]);

    // 使用ret来接收函数的返回值,以此来判断程序是否出错
    int ret = 0;
    sockaddr_in address;
    bzero(&address, sizeof(sockaddr_in));
    address.sin_family = AF_INET;

    /*
        因为我们输入的内容通常是点分十进制
        但是在网络传输的实际过程中,ip和port通常都需要在二进制的形式下进行处理

        此函数将ip转换为二进制后,将其设置为sockaddr_in.sin_addr

        这个函数在头文件arpa/inet.h中
    */
    inet_pton(AF_INET, ip, &address.sin_addr);
    address.sin_port = htons(port);

    // 创建监听套接字
    int listenfd = socket(PF_INET, SOCK_STREAM, 0);

    /*
        这是一个断言
        它判断条件是否满足
        若是满足,程序继续运行;若是不满足,程序终止运行,并输出错误信息
    */
    assert(listenfd >= 0);

    ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));
    assert(ret != -1);
    
    // 第二个参数是指:能够同时处理的最大连接数
    ret = listen(listenfd, 5);
    assert(ret != -1);

    struct sockaddr_in client_address;
    /*
        用于表示表示套接字地址结构长度的数据类型。
        是一个无符号整数类型,在套接字编程中用于指定套接字地址结构的长度。
        socklen_t类型的变量则用于指示地址结构的大小,常用于与函数accept()、bind()、connect()等配合使用。
    */
    socklen_t client_addrlength = sizeof(client_address);

    int connfd = accept(listenfd, (struct sockaddr*)&client_address, &client_addrlength);

    // 如果套接字创建失败
    if(connfd < 0){
        printf("errno is:%d", errno);
        close(listenfd);
    }

    /*
        有点不明觉厉
    */
    char buf[1024];
    fd_set read_fds;
    fd_set exception_fds;
    FD_ZERO(&read_fds);
    FD_ZERO(&exception_fds);

    while(1){
        memset(buf, '\0', sizeof(buf));
        
        /*
            每次调用select前都要重新在read_fds和expection_fdfs中设置文件描述符connfd
            因为事件发生后,文件描述符集合将被内核修改

            将文件描述符:connfd的状态进行设置
            同时对其开启“读”和“异常”处理
        */
        FD_SET(connfd, &read_fds);
        FD_SET(connfd, &exception_fds);

        /*
            这里就有个问题了,第一个参数它是什么?
            是:指定被监听文件描述符的总数
            而文件描述符是从0开始的,因此需要+1
        */
        ret = select(connfd+1, &read_fds, NULL, &exception_fds, NULL);
        if(ret < 0){
            printf("selection failure");
            break;
        }

        /*
            对于可读事件,采用普通的recv函数读取数据
        */
       if(FD_ISSET(connfd, &read_fds)){
       		/*
       			可以发现,大佬在使用recv的时候使用的是sizeof(buf)-1
       			这是因为在C格式的字符串中,存在结尾标识符,即:\0
       			而-1能够保证我们能够很完整得读取数据
       		*/
            ret = recv(connfd, buf, sizeof(buf)-1, 0);
            if(ret < 0){
                break;
            }
            printf("get%d bytes of normal data:%s\n", ret, buf);
       }

       /*
            对于异常事件,采用才MSG_OOB标志的recv函数读取带外数据
            (这个地方看得不是太懂)
       */
      else if(FD_ISSET(connfd, &exception_fds)){
      		/*
      			recv的最后一个参数就是指定传输的数据类型
      			MSG_OOB就是带外数据的意思
      			这个宏在send和recv中常常被使用
      		*/
            ret = recv(connfd, buf, sizeof(buf)-1, MSG_OOB);
            if(ret < 0){
                break;
            }
            printf("get#d bytes of oob data:%s\n", ret, buf);
      }
    }
    close(connfd);
    close(listenfd);
    
    return 0;
}

部分重点解读

在这篇文章之前,我已经写完我的Linux网络编程的day01与day02,可能会有细心的读者发现:在先前我对接收到的数据进行处理使用的是read和write,而在此处我使用的是recv和send,这是因为我两部分代码的出处不同,有的是来自的Github的开源教程,有的是来自《Linux高性能网络编程》一书。
实际上,这两个函数的差别不是很大。我们都知道:在Linux中,万事万物皆文件,网络编程的本质也是对文件进行操作
read和write是使用的Linux系统提供的接口,而recv和send则是套接字提供的函数,这里就来说说它两的区别。

read/write与recv/send

在先前的代码中,我们使用read/write都需要包含头文件unistd.h,我们先来看看read/write的函数声明(Linux下的GCC编译器):

ssize_t read (int __fd,        void *__buf, size_t __nbytes);
ssize_t write (int __fd, const void *__buf, size_t __n);

这个声明可以说是一看就懂,那我也就不过多赘述了。
接下来重点说说recv/send,先前说过,这两个函数由网络通信提供,因此就在头文件,sys/socket.h中:

ssize_t recv (int __fd,       void *__buf, size_t __n, int __flags);
ssize_t send (int __fd, const void *__buf, size_t __n, int __flags);

可以发现:recv/send多提供了一个标识作为参数,这也正是它的优点之一:可以使用额外的控制标志来控制数据传输的行为


在使用上的差异

虽然它们都将文件描述符作为第一个参数,但是实际上它们是有区别的:

  • read/write的第一个参数是要处理的文件的文件描述符
  • recv/send是将已连接的套接字描述符作为参数

这个我觉得是一个容易出错的点,但是想想好像也很好理解。

错误检测:这些函数的返回值都表示读取/发送的字节数,若是返回值为-1,则说明发生了错误,同时它们会设置error,因此我们可以以此来进行异常检测。

recv/send的最后一个参数用来控制数据传输的行为,在本文中,我们已经使用了MSG_OOB用于紧急数据传输,除此之外还有很多的常用的标识,例如:

  • MSG_DONTWAIT:在非阻塞模式下进行数据传输。如果没有可用的数据或缓冲区已满,函数会立即返回,而不会阻塞等待。该标志可用于确保接收或发送操作不会阻塞应用程序。
  • MSG_PEEK:从套接字缓冲区中查看数据,但不将其从缓冲区中移除。这样可以检查下一个数据报或消息的内容,而不会实际读取它。该标志可用于预览数据而不丢失它们,或者在处理选择器时检查可读或可写事件的条件。
  • MSG_WAITALL:在接收数据时,要求函数等待,直到请求的数据完全接收到缓冲区中后再返回。如果没有足够的数据可用,则函数将阻塞等待直到所有请求的数据接收完毕。

具体的解释我们以后再说。

总的来说:recv()和send()函数更适用于处理网络连接和套接字流量控制,具有更多的控制选项,而read()和write()函数更通用。在此处我们使用了select模型,该模型需要将数据类型分类,我想,这也许就是作者使用recv而不是read的原因吧。

你可能感兴趣的:(Linux,c++,linux,网络)