C++ TinyWebServer项目总结(6. 高级 I/O 函数)

文件描述符

文件描述符(File Descriptor, FD)是操作系统中用于访问文件的一个抽象概念。它是一个非负整数,通常由操作系统分配,用来标识被打开的文件或输入输出资源(如管道、网络连接等)。文件描述符在操作系统和应用程序之间充当桥梁,允许程序通过文件描述符来读取、写入文件或进行其他I/O操作。

文件描述符的类型

文件描述符通常分为三类标准描述符:

  1. 标准输入(Standard Input,FD 0)
  • 默认情况下与键盘关联,通常用于从用户那里接收输入数据。
  1. 标准输出(Standard Output,FD 1)
  • 默认情况下与终端窗口关联,通常用于向用户显示输出数据。
  1. 标准错误(Standard Error,FD 2)
  • 默认情况下也与终端窗口关联,但通常用于显示错误消息或诊断信息。

文件描述符的使用

在UNIX和类UNIX操作系统中,文件描述符用于各种I/O操作,包括:

  • 打开文件open() 系统调用返回一个文件描述符,表示已打开的文件。
  • 读取文件read() 系统调用使用文件描述符从文件中读取数据。
  • 写入文件write() 系统调用使用文件描述符将数据写入文件。
  • 关闭文件close() 系统调用使用文件描述符关闭文件,以释放系统资源。

文件描述符不仅限于文件,还可以用于网络套接字(socket)、管道(pipe)、设备文件等各种输入输出资源。通过文件描述符,程序可以对这些资源进行抽象的统一操作。

socket 和文件描述符的关系

socket 和文件描述符之间有着密切的关系,特别是在 UNIX 和类 UNIX 操作系统中。简而言之,socket 是一种特殊类型的文件描述符,它用于网络通信。

Socket 与文件描述符的关系

  1. Socket 是文件描述符的一种
  • 在操作系统中,socket 被抽象为文件,这意味着每个 socket 都可以通过文件描述符进行标识和操作。文件描述符不仅用于文件,还可以用于其他 I/O 资源,如 socket、管道、设备文件等。
  1. Socket 的创建与文件描述符
  • 当你使用 socket() 系统调用创建一个 socket 时,操作系统会返回一个文件描述符,这个文件描述符代表了创建的 socket。例如:
int sockfd = socket(AF_INET, SOCK_STREAM, 0);

这里,sockfd 就是一个文件描述符,后续的所有 socket 操作(如连接、发送、接收等)都将通过该文件描述符来进行。

  1. Socket 的操作类似于文件操作
  • 和普通文件一样,socket 的读写操作也是通过 read()write() 甚至是 send()recv() 等系统调用来完成的。你可以使用这些调用函数来向 socket 发送或接收数据。例如:
char buffer[1024];
int n = read(sockfd, buffer, sizeof(buffer));

在这个例子中,read() 函数通过 sockfd 文件描述符从 socket 中读取数据。

  1. Socket 的关闭
  • 当 socket 不再需要使用时,可以使用 close() 系统调用关闭它,就像关闭文件一样。关闭操作将释放与该文件描述符相关的所有资源:
close(sockfd);
  1. 重定向与 socket
  • 在某些高级应用中,可以通过重定向文件描述符来实现 socket 与其他文件描述符的交换。例如,将标准输入/输出重定向到一个 socket,从而通过网络连接来读写数据。

pipe 函数

用于创建管道,实现进程之间的通信。

#include <unistd.h>

//成功返回0,失败返回-1并设置errno
int pipe(int fd[2]);
  • fd[1]只能用于数据写入。
  • fd[0]只能用于数据读出。

socket 的基础 API 中有一个 socketpair 函数:

#include <sys/types.h>
#include <sys/socket.h>
//成功返回0,失败返回-1设置errno
int socketpair(int domain, int type, int protocol, int fd[2]);

domain只能使用UNIX本地协议族AF_UNIX,所以socketpair只能在本地使用,不过创建的这对文件描述符都是可读可写的。

dup函数和dup2函数

可以将标准输入重定向到一个文件,或者标准输出重定向到一个网络连接。

#include <unistd.h>
int dup(int file_descriptor);
int dup2(int file_descriptor_one, int file_descriptor_two);

以下程序使用dup函数实现了一个基本的CGI服务器:

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <libgen.h>

int main(int argc, char *argv[]) {
    if (argc <= 2) {
        printf("usage: %s ip_address port_number\n", basename(argv[0]));
        return 1;
    }
    const char *ip = argv[1];
    int port = atoi(argv[2]);

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

    // 创建套接字
    int sock = socket(PF_INET, SOCK_STREAM, 0);
    assert(sock >= 0);

    // 绑定套接字
    int ret = bind(sock, (struct sockaddr *)&address, sizeof(address));
    assert(ret != -1);

    // 监听套接字,最大等待连接队列的长度为5
    ret = listen(sock, 5);
    assert(ret != -1);

    struct sockaddr_in client;
    socklen_t client_addrlength = sizeof(client);

    // 接受客户端连接
    int connfd = accept(sock, (struct sockaddr *)&client, &client_addrlength);
    if (connfd < 0) {
        printf("errno is: %d\n", errno);
    } else {
        // 先关闭标准输出文件描述符STDOUT_FILENO,其值为1
        close(STDOUT_FILENO);
        // 复制socket文件描述符connfd,由于dup函数总是返回系统中最小的可用文件描述符
        // 因此dup参数实际返回的是1,即之前关闭的标准输出文件描述符的值
        // 这样服务器输出到标准输出的内容会直接发送到与客户连接对应的socket上
        dup(connfd);
        printf("abcd\n");
        close(connfd);
    }

    close(sock);
    return 0;
}

代码首先关闭标准输出文件描述符STDOUT_FILENO,其值为1(由宏定义);

dup(connfd);复制客户端连接的文件描述符,并将其重定向为标准输出文件描述符1(即STDOUT_FILENO);

之后的printf("abcd\n");语句输出的内容将通过套接字发送给客户端(而非终端)。

char* a[3];char a[3];

char* a[3];char a[3]; 之间的主要区别在于:

1. 类型和结构的不同:

char a[3];:

  • 这是一个包含3个字符元素的字符数组。
  • 它占用3个字节的连续内存空间,每个元素都是一个char类型的字符。
  • 适用于存储一组字符,通常用于存储小的字符串或字符数据。

char* a[3];:

  • 这是一个包含3个元素的指针数组,每个元素都是指向char类型数据的指针。
  • 这意味着你可以在这个数组中存储3个字符指针,每个指针可以指向一个字符串或字符数组。
  • 适用于存储字符串的指针或一组字符数组的指针。

2. 内存布局的不同:

char a[3];:

  • 内存中会有3个连续的字节,每个字节存储一个字符。
  • 例如,a[0]a[1]a[2]都存储在相邻的内存地址中。

char* a[3];:

  • 内存中会有3个连续的指针,每个指针指向一个char类型的数据。
  • 这些指针本身占用空间(在32位系统中每个指针占用4个字节,在64位系统中每个指针占用8个字节),它们指向的内容可以是任意内存位置的字符或字符串。
  • 例如,a[0]a[1]a[2]存储的是指针,而不是字符本身。

3. 使用场景的不同:

char a[3];:

  • 适用于存储一小段字符数据,例如单个短字符串。
  • 例如,你可以用它来存储字符串 "Hi",并以 '\0' 结尾。

char* a[3];:

  • 适用于存储多个字符串的指针,通常用于二维字符数组或字符串数组的场景。
  • 例如:
char* a[3];
a[0] = "Hello";
a[1] = "World";
a[2] = "!";
  • 这里a[0]a[1]a[2]分别指向三个不同的字符串常量。

总结

char a[3]; 是一个存储字符的数组。

char* a[3]; 是一个存储字符指针的数组,通常用于存储字符串的指针。

readv函数和writev函数

readv函数将数据从文件描述符读到分散的内存块中,即分散读;writev函数将多块分散的内存数据一并写入文件描述符中,即集中写:

#include <sys/uio.h>

//成功返回读写的字节数,失败返回-1并设置errno
ssize_t readv(int fd, const struct iovec* vector, int count); 	//分散读
ssize_t writev(int fd, const struct iovec* vector, int count); 	//集中写

结构体 iovec

struct iovec{
	void *iov_base;		//内存块起始地址
	size_t iov_len;		//内存块长度
};

sendfile函数

sendfile函数在两个文件描述符之间直接传递数据,完全在内核中操作,避免内核缓冲区和用户缓冲区之间的数据拷贝,效率高。这被称为零拷贝。

#include <sys/sendfile.h>

//成功返回传输的字节数,失败返回-1并设置errno
ssize_t sendfile(int out_fd, int in_fd, off_t* offset, size_t count);

参数:

  • in_fd:待读出内容的文件描述符
  • out_fd:待写入内容的文件描述符
  • offset:指定从读入文件流的哪个位置开始读,如果为空,则使用默认起始位置
  • count:指定in_fdout_fd之间传输的字节数。

mmap函数和munmap函数

mmap用于申请一段内存空间,这段内存可以用于进程间通信的共享内存,也可以直接将文件映射到其中,munmap用于释放这段空间。

#include <sys/mman.h>

//成功返回指向目标区域的指针,失败返回MAP_FAILED((void*) -1),并设置errno
void* mmap(void *start, size_t length, int port, int flags, int fd, off_t offset);
//成功返回0,失败返回-1并设置errno
int munmap(void *start, size_t length);

参数:

  • start :允许用户使用某一个特定的地址作为这段内存的起始地址,如果是设置为NULL,则系统自动分配。
  • length:指定这段内存的长度
  • port:设置内存段的访问权限。
    • PROT_READ,内存段可读。
    • PROT_WRITE,内存段可写。
    • PROT_EXEC,内存段可执行。
    • PROT_NONE,内存段不能被访问。
  • flags:控制内存段内容被修改后 程序的行为。
  • fd:是被映射文件的文件描述符,一般通过open系统调用获取
  • offset:设置从文件的何处开始映射。

splice函数

用于两个文件描述符之间移动数据,也是零拷贝操作。

#include <fcntl.h>
ssize_t splice(int fd_in, loff_t* off_in, int fd_out, loff_t* off_out, size_t len, unsigned int flags);

参数:

  • fd_in:是输入数据的文件描述符,用于数据的读出。
  • fd_outoff_out含义类似,用于输出数据流(数据写入)。
  • len:指定移动数据的长度。
  • flags:控制数据如何移动。

tee函数

tee函数在两个管道文件描述符之间复制数据,也是零拷贝操作,不消耗数据,而splice从管道中读取数据,也就是消耗数据。

#include <fcntl.h>
ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);

参数:

  • fd_infd_out必须都是管道文件描述符

fcntl函数

fcntl函数,正如其名字(file control)描述的那样,提供了对文件描述符的各种控制,另一个常见的控制文件描述符属性和行为的系统调用是ioctl,且ioctl函数比fcntl函数能执行更多的控制,但控制文件描述符的常用属性和操作,fcntl函数是由 POSIX 规范指定的首选方法:

#include <fcntl.h>

//失败返回-1并设置errno
int fcntl(int fd, int cmd, ...);

参数:

  • fd:被操作的文件描述符
  • cmd:指定执行何种类型的操作
  • 由于操作类型的不同,可能需要第三个可选参数 arg

fcntl支持的常用操作:

在网络编程中,fcntl 函数常用于把文件描述符设置为非阻塞的:

int setnonblocking(int fd) {
    
	// 获取文件描述符状态标志
	int old_option = fcntl(fd, F_GETFL);
    
	// 设置非阻塞标志
	int new_option = old_option | O_NONBLOCK;
	fcntl(fd, F_SETFL, new_option);

    // 返回fd旧的状态标志,以便日后恢复该状态标志
	return old_option;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

红茶川

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值