引言
本篇文章是开始深入了解一些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;
}
总结
本篇文章到这里就结束了!!!希望可以帮助大家理解~~~