在 HTTP 协议中规定请求的第一行是类似如下的格式:
GET / HTTP/1.1
GET /img/wx_qrcode_258.jpg HTTP/1.1
很显然,它是使用空格分开的三个部分. method
,uri
和 version
, 这个是基本的 HTTP 请求格式.更多信息请参考相关的 HTTP 协议内容吧.
我们考虑的是怎么解析,首先我们需要从 TCP 连接流里面读取一行数据,我们也不用读太多, 后面功能完善的时候再继续读吧. 由于 TCP 是流协议,如果我们按常规流的读法的话,可以一个字节一个字节的读取,直到遇到换行符为止. 但是今天给大家介绍的不是这种读书.
而是回到最基本类似标准输入的编程技术, 而这个技术的基础就是, Unix 世界的一切皆是文件的抽象.
数据结构的声明和解析的函数如下:
typedef struct RequestLine {
char method[16];
char uri[256];
char version[16];
} RequestLine;
int parse_request_line(ct_socket_t clientfd, RequestLine *line) {
assert(line);
FILE *fin = fdopen(clientfd, "r+");
if (!fin) {
return -1;
}
char buf[CT_BUF_SIZE] = {0};
char *pos = fgets(buf, CT_BUF_SIZE, fin);
if (pos == NULL) {
return -1;
}
LOG_INFO("%s", buf);
int cnt = sscanf(buf, "%s %s %s\n", line->method, line->uri, line->version);
if (cnt == EOF) {
LOG_ERR("http request line parse error");
return -1;
}
return 0;
};
解析出了请求行之后,我们就可以直接使用对应的 uri
来查找对应的资源文件了. 这里使用 sprintf
充当字符串拼接的功能. 当然如果 uri
后面包含有请求字符串,就直接在后面置 \0
表示文件路径已经到此结束了.
const char *const STATIC_ROOT = "html";
void get_file_path_from_uri(char *file_path, const char *uri) {
sprintf(file_path, "%s%s", STATIC_ROOT, uri);
char *ptr = strchr(file_path, '?');
if (ptr) {
*ptr = '\0';
}
}
判断文件是否存在,一个简单的方法,就是直接使用我们之前使用过的系统调用 stat
,如果这个调用报错,说明文件或对应目录不存在,然后再判断一下是否是目录.
我们将整个上一期文章中发送文件的逻辑加上处理文件不存在的逻辑,抽象如下:
#define IS_FILE(mode) (S_ISREG(mode))
ssize_t send_static_file_or_404(ct_socket_t clientfd, const char *filename) {
struct stat st;
int ret = stat(filename, &st);
if (CHECK_FAIL(ret)) {
return write_http_error(clientfd, 404);
}
if (!IS_FILE(st.st_mode)) {
return write_http_error(clientfd, 404);
}
return send_static_file(clientfd, filename);
}
由于我们网站现在已经升级支持返回多文件了,所以支持图片则是必须的了,
暂时使用是否包含对应了后缀作为是否是图片类型的判断.
const char *guess_content_type(const char *filename) {
// TODO 优化改为使用后缀判断
if (strstr(filename, ".png")) {
return "image/png";
} else if (strstr(filename, ".jpg")) {
return "image/jpg";
} else if (strstr(filename, ".gif")) {
return "image/gif";
} else {
return "text/html;charset=utf-8";
}
}
ssize_t send_static_file(ct_socket_t clientfd, const char *filename) {
struct timespec ts_start;
struct timespec ts_end;
int ret = clock_gettime(CLOCK_THREAD_CPUTIME_ID, &ts_start);
CT_GUARD(ret);
size_t file_size = get_file_size(filename);
CT_GUARD(file_size);
const char *content_type = guess_content_type(filename);
int send_cnt = write_http_header(clientfd, content_type, file_size);
CT_GUARD(send_cnt);
send_cnt = write_http_body(clientfd, filename, file_size);
ret = clock_gettime(CLOCK_THREAD_CPUTIME_ID, &ts_end);
CT_GUARD(ret);
long ns = diff_timespec_to_nsec(ts_start, ts_end);
double ms = ns / 1000000.0;
LOG_INFO("send file %s, size:%zu bytes, cost: %0.3fms", filename, file_size,
ms);
return send_cnt;
}
如果找不到对应的资源,或者我们程序出错了,我们希望可以返回一个比较符合标准的 HTTP 响应,于是我们增加了如下的代码:
const char *http_status_code_to_text(int status_code) {
switch (status_code) {
case 200:
return "OK";
case 400:
return "Not Found";
case 500:
return "Internal Server Error";
default:
return " ";
}
}
int write_http_error(ct_socket_t clientfd, int status_code) {
const char *msg = http_status_code_to_text(status_code);
const char *header_tpl =
"HTTP/1.1 %d %s" CRLF "Content-Type:text/html;charset=utf-8" CRLF
"Content-Length:0" CRLF "Connection: close" CRLF CRLF;
char header[CT_BUF_SIZE] = {0};
size_t header_len =
snprintf(header, CT_BUF_SIZE, header_tpl, status_code, msg);
return write(clientfd, header, header_len);
}
static char *log_time() {
const ssize_t TIME_LEN = 64;
static char buf[TIME_LEN] = {0};
bzero(buf, TIME_LEN);
struct timeval tv;
gettimeofday(&tv, NULL);
struct tm *tm = localtime(&tv.tv_sec);
int millis = tv.tv_usec / 1000;
size_t pos = strftime(buf, TIME_LEN, "%F %T", tm);
snprintf(&buf[pos], TIME_LEN - pos, ".%03d", millis);
return buf;
};
#define CLEAN_ERRNO() (errno == 0 ? "None" : strerror(errno))
#define LOG_ERR(MSG, ...) \
fprintf(stderr, "([%s]%s:%s:%d: errno: %s) " MSG "\n", log_time(), __FILE__, \
__func__, __LINE__, CLEAN_ERRNO(), ##__VA_ARGS__)
#define LOG_INFO(MSG, ...) \
fprintf(stdout, "[%s] " MSG "\n", log_time(), ##__VA_ARGS__)