高级I/O函数(一)

引言

本篇文章是开始深入了解一些Linux的高级API,目的就是为了解决一些特别的问题,参考书目为《Linux高性能服务器编程》,所有的代码我会在github上面进行提交,我github的仓库地址是:

fengyue05/Linux-: 本仓库里面会有关于《Linux高性能服务器编程》的一些主要代码实现

pipe

int pipe(int fd2)

函数pipe创建的两个文件描述符fd0和fd1分别构成管道的读端和写端,往管道里面写入数据可以从fd0里面读出来。并且f0只能用来读取数据,这些都不可以反过来使用。如果要实现双向传输,那么就需要两个管道。默认情况下,这两个文件描述符都是阻塞的,所以如果我们使用read系统将会被阻塞,直到管道里面有数据(注意:read/write是不是阻塞完全取决于文件描述符是不是阻塞);如果我们使用write来向管道内写入数据,如果管道已经满,那么write就会被阻塞,直到管道有足够多的空闲空间来写。那么管道可以写多少数据呢?我们一般来说默认65536字节。

但是如果我们设置读和写的文件描述符为非阻塞,那么read和write就会有不同的行为。

如果管道写端文件描述符fd1引用计数为0,即没有任何进程可以往管道里面写入数据,则读取到了文件结束表示(EOF)。如果读端的文件描述符fd0引用计数为0,那么没有任何进程需要读取管道里面的数据,所以针对管道写端的文件描述符fd1的操作write将失败,并且引发SIGPIPE信号。

管道内部传输的是字节流。

dup和dup2函数

我们有时候需要把标准输入重定向到一个文件里面,或者把标准输出重定向到一个网络连接(如CGI编程),可以通过dup或者dup2函数来实现。

CGI编程是指web调用外部程序来处理HTTP请求。

dup是创建了一个新的文件描述符,该新文件描述符和原本的文件描述符指向了同一个文件,并且返回的文件描述符总是取系统当前可用最小的整数值。dup2和这个类似,只是返回的文件描述符是自己指定的,也就是newfd。

cpp 复制代码
       int dup(int oldfd);
       int dup2(int oldfd, int newfd);

我们实现一个基本的CGI服务器,其中有几行关键的代码:

cpp 复制代码
close(STDOUT_FILDNO); // 关闭标准输入
dup(connfd) // 复制一个socket文件描述符
printf("hello world"); // 打印到了客户连接的fd上
close(connfd);

我们来解释一下,首先关闭标准输入fd,也就是1,然后复制客户的fd,因为dup总是取系统当前可用最小的整数值,所以就是1,这么一来,服务器标准输出的内容(这里是"hello world")就会直接发送到客户连接的socket上,因此printf调用的输出将被客户端获得。

这个就是CGI服务器的原理~~~~

集中写和分散读

readv函数将数据从文件描述符读到分散的内存之中,这个在陈硕老师的moduo库里面涉及到过,就是buffer那个部分,如果大家感兴趣可以看一下我之前的那个文章,目的就是我们希望用更加节省的空间来处理那些动态的数据(大小不确定,可能有时候需要的空间比较少,有的空间比较大)。writev函数就是将多块分散的内存数据一并写入文件描述符内,也就是集中写。

其中有一个参数是count,也就是有多少个内存数据需要从fd中读出或者写到fd。

我们接下来就是写了一个web服务器的应答,因为一个web服务器解析完一个HTTP请求之后,如果目标文档存在且客户具有读取该文档的权限,那么它就需要发送一个HTTP应答来传输该文档。

这个HTTP应答包含1个状态行,多个头部字段,1个空行和文档的内容。其中前三个部分可能被web服务器放置在一块内存中,而文档的内容一般都被读入到另一个单独的内存,我们并不需要把这两个部分内容拼接到一起再发送,而是可以使用writev将他们同时写出。

代码

有一些很好的写法我们可以学习一下:

在里面我们涉及到了一个对于目标文件的读取也就是stat结构体,这个结构体我们通过stat()函数来对其进行赋值,然后文件的长度我们也可以就此获取。如果我们直接传入sizeof(file_buf),因为我们最初并不知道这个缓冲区的大小,所以我们定义的是一个指针,而指针的大小永远是4字节,所以我们得到的sizeof永远是4字节,而用stat就可以很好的得到这个缓冲区的大小。

然后再写http头文件的时候大家一定要记得它的格式,在拼接的时候我们要不断的对于buffer减去其已经加了的len,所以当缓存空间不够的时候就会报错。

最后在集中写的过程中就把对应的首地址传入iov_base里面,最后writev就直接传入connfd里面。

cpp 复制代码
#define BUFFER_SIZE 1024

static const char* status_line[2] = {"200 OK", "500 Internal server error"};

int main (int argc, char* argv[]) {
    if (argc <= 2) {
      return -1;
    }
    const char* ip = argv[1];
    int port = atoi(argv[2]);
    const char* file_name = argv[3];

    sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    address.sin_port = htons(port);
    inet_pton(AF_INET, ip, &address.sin_addr);

    int sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
    assert(sockfd >= 0);

    int ret = listen(sockfd, 128);
    assert(ret != -1);

    sockaddr_in client;
    socklen_t len = sizeof(client);
    int connfd = accept(sockfd, (sockaddr*)&client, &len);

    if (connfd < 0) {
        std::cout << "failed" << std::endl;
    } 
    else {
        // 用于保存HTTP应答的状态行,头部字段和一个空行的缓冲区
        char header_buf[BUFFER_SIZE];
        memset(header_buf, '\0', BUFFER_SIZE);
        // 用于存放目标文件内容的应用程序缓存
        char* file_buf;
        // 用于获取目标文件的属性,比如目录,文件大小
        struct stat file_stat;
        // 记录目标文件是否是有效文件
        bool valid = true;
        // 缓冲区header_buf目前已经使用了多少字节的空间
        int len_ret = 0;
        // 第一个参数是文件名路径,第二个参数是把文件的属性全部传入这个结构体里面
        if (stat(file_name, &file_stat) < 0) { // 目标文件不存在
            valid = false;
        } 
        else {
            // file_stat.st_mode 这个里面保存了文件的类型和权限
            if (S_ISDIR(file_stat.st_mode)) { // 目标文件是一个目录
                valid = false;
            }
            else if (file_stat.st_mode & S_IROTH) { // 当前用户有权利读取
                // 动态分配缓存区file_buf,并指定其大小为目标文件的大小file_stat.st_size 加1,然后将目标文件读入缓存区file_buf中
                int fd = open(file_name, O_RDONLY);
                file_buf = new char[file_stat.st_size + 1];
                memset(file_buf, '\0', file_stat.st_size + 1);
                if (read(fd, file_buf, file_stat.st_size + 1) < 0) {
                    valid = false;
                }
            } 
            else {
                valid = false;
            }
        }
        ret = 0;
        // 如果目标文件有效,那么发送正常的HTTP应答
        if (valid) {
            // 下面这个部分将HTTP应答的状态行,"Content-Length"头部字段和一个空行依次加入header_buf中
            ret = snprintf(header_buf, BUFFER_SIZE - 1, "%s %s\r\n", "HTTP/1.1", status_line[0]);
            len_ret += ret;

            ret = snprintf(header_buf + len, BUFFER_SIZE - 1 - len, "Content-Length: %d\r\n", static_cast<int>(file_stat.st_size));
            len += ret;

            ret = snprintf(header_buf, BUFFER_SIZE - len - 1, "%s", "\r\n");
            struct iovec iov[2];
            iov[0].iov_base = header_buf;
            iov[0].iov_len = strlen(header_buf);
            iov[1].iov_base = file_buf;
            iov[1].iov_len = file_stat.st_size;
            ret = ::writev(connfd, iov, 2);
        }
        else {
            ret = snprintf(header_buf, BUFFER_SIZE - 1, "%s %s\r\n", "HTTP/1.1", status_line[1]);
            len += ret;

            ret = snprintf(header_buf + len, BUFFER_SIZE - 1 - len, "%s", "\r\n");
            send(connfd, header_buf, strlen(header_buf), 0);
        }
        close(connfd);
        delete [] file_buf;
    }
    close(sockfd);
    return 0;
}

总结

本篇文章到这里就结束了!!!希望可以帮助大家理解~~~

相关推荐
-今昭-36 分钟前
MooseFS分布式文件系统
linux·服务器·网络
byte轻骑兵1 小时前
【BlueZ 】蓝牙 HCI 协议基础:与 BlueZ 源码的层面对应关系
linux·hci·电脑蓝牙·嵌入式蓝牙·buez
砚凝霜1 小时前
软考网络工程师|案例分析:Eth‑Trunk 链路聚合、iStack 堆叠、CSS 集群核心考点总结
前端·css·网络
万联WANFLOW1 小时前
Meta开源了能本地跑的agent
运维·服务器·网络·人工智能·业界资讯
Brilliantwxx1 小时前
【Linux】 进程(1) 从冯诺依曼体系到进程核心概念
linux·运维·服务器
城管不管2 小时前
重生——第十次面试之开源中国一面挂
java·linux·开发语言·算法·面试·职场和发展·开源
蜀道山老天师2 小时前
Zabbix监控Apache与Nginx应用实践完整指南
linux·运维·nginx·apache·zabbix
Java小白笔记2 小时前
Windows系统免软件命令激活
java·网络·人工智能·windows·ai·ai编程
王志来137944730082 小时前
从分散到集成:工控服务器机箱采购如何实现“一站式”破局
运维·服务器·人工智能·python
暖核2 小时前
Linux实战入门:从零编写第一个Bash Shell脚本
linux·运维·bash