前面做了这几件事:
- 解析请求行
- 解析请求头
- 解析URI
- 对错误情况(404,403)进行处理,并读取了文件的inode信息,存储在结构体里面
- 处理请求头几种情况,并将请求头的处理结果存在out中
现在来处理静态文件请求
先来看下HTTP的响应报文格式。基本由三部分组成:
状态行
响应头部
响应正文
协议版本空格状态码空格状态码描述符回车符换行符
头部字段名:值回车符换行符
...
头部字段名:值回车符换行符
回车符换行符
响应正文
举例
out->mtime = sbuf.st_mtime;
获取time of last modifacition,上一次修改时的时间。
先看下out结构体
typedef struct tk_http_out{
int fd; // 连接描述符
int keep_alive; // HTTP连接状态
time_t mtime; // 文件类型
int modified; // 是否修改,这里应该不会是这个意思
int status; // 返回码
}tk_http_out_t;
它初始化函数如下:
// 初始化响应数据结构
int tk_init_out_t(tk_http_out_t* out, int fd){
out->fd = fd;
// 默认值1,保持连接不断开
out->keep_alive = 1;
out->modified = 1;
// 默认为200(success),出错时被修改
//http状态码为200,表示成功处理了请求
out->status = 200;
return 0;
}
默认长连接,默认文件已经被修改,默认成功处理了请求。
// 处理静态文件请求
serve_static(fd, filename, sbuf.st_size, out);
对应的函数是下面的。
// 处理静态文件请求
void serve_static(int fd, char *filename, size_t filesize, tk_http_out_t *out){
// 响应头缓冲(512字节)和数据缓冲(8192字节)
char header[MAXLINE];
char buff[SHORTLINE];
struct tm tm;
// 返回响应报文头,包含HTTP版本号状态码及状态码对应的短描述
sprintf(header, "HTTP/1.1 %d %s\r\n", out->status, get_shortmsg_from_status_code(out->status));
// 返回响应头
// Connection,Keep-Alive,Content-type,Content-length,Last-Modified
if(out->keep_alive){
// 返回默认的持续连接模式及超时时间(默认500ms)
sprintf(header, "%sConnection: keep-alive\r\n", header);
sprintf(header, "%sKeep-Alive: timeout=%d\r\n", header, TIMEOUT_DEFAULT);
}
if(out->modified){
// 得到文件类型并填充Content-type字段
const char* filetype = get_file_type(strrchr(filename, '.'));
sprintf(header, "%sContent-type: %s\r\n", header, filetype);
// 通过Content-length返回文件大小
sprintf(header, "%sContent-length: %zu\r\n", header, filesize);
// 得到最后修改时间并填充Last-Modified字段
localtime_r(&(out->mtime), &tm);
strftime(buff, SHORTLINE, "%a, %d %b %Y %H:%M:%S GMT", &tm);
sprintf(header, "%sLast-Modified: %s\r\n", header, buff);
}
sprintf(header, "%sServer : TKeed\r\n", header);
// 空行(must)
sprintf(header, "%s\r\n", header);
// 发送报文头部并校验完整性
size_t send_len = (size_t)rio_writen(fd, header, strlen(header));
if(send_len != strlen(header)){
perror("Send header failed");
return;
}
// 打开并发送文件
int src_fd = open(filename, O_RDONLY, 0);
char *src_addr = mmap(NULL, filesize, PROT_READ, MAP_PRIVATE, src_fd, 0);
close(src_fd);
// 发送文件并校验完整性
send_len = rio_writen(fd, src_addr, filesize);
if(send_len != filesize){
perror("Send file failed");
return;
}
munmap(src_addr, filesize);
}
用到了sprintf函数,
#include <stdio.h>
int sprintf(char *str, const char *format, ...);
存储数据到str字符串数组中。如果成功,则返回写入的字符总数,不包括字符串追加在字符串末尾的空字符。如果失败,则返回一个负数。
第一个函数,get_shortmsg_from_status_code//获取状态码
// 根据状态码返回shortmsg
const char* get_shortmsg_from_status_code(int status_code){
if(status_code == TK_HTTP_OK){
return "OK";
}
if(status_code == TK_HTTP_NOT_MODIFIED){
return "Not Modified";
}
if(status_code == TK_HTTP_NOT_FOUND){
return "Not Found";
}
return "Unknown";
}
第二个函数,const char* filetype = get_file_type(strrchr(filename, '.'));
mime_type_t tkeed_mime[] =
{
{".html", "text/html"},
{".xml", "text/xml"},
{".xhtml", "application/xhtml+xml"},
{".txt", "text/plain"},
{".rtf", "application/rtf"},
{".pdf", "application/pdf"},
{".word", "application/msword"},
{".png", "image/png"},
{".gif", "image/gif"},
{".jpg", "image/jpeg"},
{".jpeg", "image/jpeg"},
{".au", "audio/basic"},
{".mpeg", "video/mpeg"},
{".mpg", "video/mpeg"},
{".avi", "video/x-msvideo"},
{".gz", "application/x-gzip"},
{".tar", "application/x-tar"},
{".css", "text/css"},
{NULL ,"text/plain"}
};
const char* get_file_type(const char *type){
// 将type和索引表中后缀比较,返回类型用于填充Content-Type字段
for(int i = 0; tkeed_mime[i].type != NULL; ++i){
if(strcmp(type, tkeed_mime[i].type) == 0)
return tkeed_mime[i].value;
}
// 未识别返回"text/plain"
return "text/plain";
}
第三个函数,
size_t strftime(char *str, size_t maxsize, const char *format, const struct tm *timeptr)
根据 format 中定义的格式化规则,格式化结构 timeptr 表示的时间,并把它存储在 str 中。
第四个函数,将响应报文头部写入连接套接字。
size_t send_len = (size_t)rio_writen(fd, header, strlen(header));
ssize_t rio_writen(int fd, void *usrbuf, size_t n)
{
size_t nleft = n;
ssize_t nwritten;
char *bufp = (char *)usrbuf;
while(nleft > 0){
if((nwritten = write(fd, bufp, nleft)) <= 0){
if (errno == EINTR)
nwritten = 0;
else{
return -1;
}
}
nleft -= nwritten;
bufp += nwritten;
}
return n;
}
第三个函数,mmap映射。将读取到的文件写入连接套接字。
void * mmap(void * addr ,size_t length ,int prot ,int flags ,
int fd ,off_t offset );
mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。mmap除了可以减少read,write等系统调用以外,还可以减少内存的拷贝次数,比如在read调用时,一个完整的流程是操作系统读磁盘文件到页缓存,再从页缓存将数据拷贝到read传递的buffer里,而如果使用mmap之后,操作系统只需要将磁盘读到页缓存,然后用户就可以直接通过指针的方式操作mmap映射的内存,减少了从内核态到用户态的数据拷贝。
addr指向欲映射的内存起始地址,通常设为 NULL,代表让系统自动选定地址,映射成功后返回该地址。
length代表将文件中多大的部分映射到内存。
prot映射区域的保护方式。PROT_READ 映射区域可被读取。
flags影响映射区域的各种特性。在调用mmap()时必须要指定MAP_SHARED 或MAP_PRIVATE。
fd要映射到内存中的文件描述符。
offset文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是分页大小的整数倍。
第四个函数,munmap,
#include <sys/mman.h>
int munmap(void *addr, size_t len);
The munmap() function shall remove any mappings for those entire
pages containing any part of the address space of the process
starting at addr and continuing for len bytes.