Tiny Httpd
CSAPP中实现了一个Robust I/O package,它在文件读写IO函数(文件IO函数也称为不带缓冲的IO函数)上做出的改进:
- short cut(不足值)的自动处理;
- 提供了带缓冲的IO包装函数。
short cut的处理
先看看两个文件读写IO函数:
ssize_t read(int filedes, void *buf, size_t nbytes);
ssize_t write(int filedes, void *buf, size_t nbytes);
在进行读|写文件操作时,实际读|写的字节数(函数的返回值)和期望读的字节数可能不同,这两个值的差就是不足值。
在读操作中很多情况会产生这种不足值:
- 读到了文件尾;
- 从终端、网络或管道读时,由于缓冲机制的影响或传输的数据量未达到要求;
- 被信号中断。
在写操作中,返回值通常遇期望写的字节数相同,当写操作被信号中断时也会出现不足值的情况。
在实际的网络通信中,这种不足值并不代表错误,为了能够自动处理这些不足值的情况,实现read()和write()的包装函数rio_readn(),rio_writen()。rio_readn()包装函数的实现:
ssize_t rio_readn(int filedes, void *buf, size_t nbytes) {
ssize_t nread;
size_t left = nbytes;
char *bufp = buf;
while (left) {
nread = read(filedes, bufp, left);
if (-1 == nread) {
// 当read()被信号中断时,重启read()
if (errno == EINTR)
continue;
return -1;
} else if (0 == EINTR) {
break;
} else {
bufp += nread;
left -= nread;
}
}
return nbytes-left;
}
具体实现这种处理的机制:
- 返回的情况:read()系统调用出错|read()读到EOF|read()读到所需字节数
- 当read()被信号中断时,自动重启read()。read是一个慢系统调用,慢指的是它是可能会永远阻塞,当它被信号中断时,有些系统不会在信号处理程序返回后重启这个系统调用,而是直接返回-1。
带缓冲的IO
考虑网络服务器的一个服务场景,当客户端telnet到服务器时,服务器需要逐行获取请求内容,这样可以通过每次通过read()读入一个字节,判断它是否是'\n'来实现。但这样实现存在一个很大的问题:每次系统调用只读了一个字节,效率很低,一种更好的实现方式是每次读取很多字节到一个应用级缓冲,在从缓冲中逐个读入字符,直到'\n'。
基于这种考虑,需要实现了编写带缓冲的IO函数。
一个缓冲区和一个文件描述符对应,这样的缓冲区的结构:
typedef struct {
char rio_buf[BUF_SIZE];
char *rio_bufptr; // 指向下个未读的字符
int rio_fd;
int rio_cnt; // 剩余未读的字节数
} rio_buf_t;
带缓冲的读包装函数的实现:
ssize_t rio_buf_read(rio_buf_f *rp, void *buf, size_t nbytes) {
ssize_t nread;
size_t read_bytes;
while (0 == rp->rio_cnt) {
nread = read(rp->rio_fd, rp->rio_buf, BUF_SIZE);
if (-1 == nread) {
if (errno = EINTR)
continue;
return -1;
} else if (0 == nread) {
return 0;
} else {
rp->rio_bufp = rp->rio_buf;
rp->rio_cnt = nread;
}
}
read_bytes = rp->rio_cnt > nbytes : nbytes, rp->rio_cnt;
memcpy(buf, rp->rio_bufp, read_bytes);
rp->rio_cnt -= read_bytes;
rp->rio_bufp += read_bytes;
return read_bytes;
}
当缓冲区为空时,rio_buf_read()会请求BUF_SIZE大小的数据,读取到数据之后再降缓冲区中的数据返回给调用者。rio_buf_read()没有去处理short cut的情况,这种设计是为了保持read()和rio_buf_read()语义的一致性,当需要用带缓冲的io函数代替不带缓冲的io函数时只需要将read()替换成rio_buf_read()即可。
再rio_buf_read()的基础上实现带缓冲的rio_buf_readline()和rio_buf_readn()就很简单了。
rio_readline()调用rio_buf_read()每次请求一个字节,判断它是不是换行符;rio_buf_readn()和rio_readn()的实现类似,只需要将其中的read()替换成rio_buf_read()。
在同一个fd上不能同时使用带缓冲的IO函数和不带缓冲的IO函数
由于带缓冲的IO函数每次可能请求多于实际返回的数据,也就是在缓冲区可能还会保存一部分未读数据,这个时候调用不带缓冲的IO函数可能会造成不可预知的结果
tiny-webserver
实现一个非常简单的httpd:
- 当前只支持POST和GET两种方式;
- 对于每个服务请求,创建一个线程来为其服务;
- 支持静态文件请求和动态请求(cgi-bin下的可执行文件)。
和常见的服务器客户端编程模式一样,服务器创建、绑定和监听套接字后,执行一个循环,从监听套接字上接收一个服务请求,创建一个线程来为其服务。
客户端请求的第一行的格式为: METHOD URI HTTP_PROCOTOL
METHOD说明了请求的方式,URI中包含了请求的文件以及可能包含的参数。程序根据URI来区分动态请求和静态请求:如果URI中包含子串"cgi-bin",则判定这是一个动态请求,否则它是一个静态请求。
对于静态请求,读取文件返回给客户端。
对于动态请求,创建一个子进程来执行cgi-bin中的可执行文件,若请求方法为POST,父进程需要将请求的content通过管道传送给子进程。子进程通过管道将执行的输出传递给父进程,父进程再将其返回给客户端。
代码托管于Github