我们将socket的创建,绑定,监听做如下封装
#define CT_GUARD(ret) \
({ \
if (ret == -1) { \
return -1; \
} \
})
/// socket 套接字描述符
typedef int ct_socket_t;
/// 创建用于 Web 站点的 Socket (IPv4 Only)
ct_socket_t create_web_server_socket(in_port_t port) {
int serverfd = socket(AF_INET, SOCK_STREAM, 0);
CT_GUARD(serverfd);
size_t addr_len = sizeof(struct sockaddr_in);
struct sockaddr_in addr4 = {
.sin_family = AF_INET,
.sin_port = htons(port),
};
addr4.sin_addr.s_addr = htonl(INADDR_ANY);
int ret = -1;
int option = 1;
ret = setsockopt(serverfd, SOL_SOCKET, SO_REUSEPORT, &option, sizeof(option));
CT_GUARD(ret);
ret = bind(serverfd, (struct sockaddr *)&addr4, sizeof(addr4));
CT_GUARD(ret);
int backlog = 128;
ret = listen(serverfd, backlog);
CT_GUARD(ret);
return serverfd;
}
可以看到,这是典型的Linux C风格,用宏来做异常处理,Golang来写会清晰很多。
我也不喜欢在字符串里写代码
之前的返回的网页的内容是硬编码在字符串中的,在字符串里写 html
代码,这滋味不好受.
所以我决定将其放到一个文件 html/index.html
中 后面网站的静态资源就都放在 html
目录中.
然后读取整个文件然后发送给浏览器.
首先是我们的响应头.这次不能再硬编码长度了, 所以我使用了 snprintf
来帮我拼这个长度.
增加一个 write_http_header
的函数专门用于输出响应头:
响应头用单独的函数封装,响应体html资源写在文件中,读取发送
#define CRLF "\r\n"
#define CT_BUF_SIZE 1024
ssize_t write_http_header(ct_socket_t clientfd, size_t content_length) {
char header[CT_BUF_SIZE] = {0};
const char *header_tpl =
"HTTP/1.1 200 OK" CRLF "Content-Type:text/html;charset=utf-8" CRLF
"Content-Length:%d" CRLF "Connection: close" CRLF CRLF;
size_t header_len = snprintf(header, CT_BUF_SIZE, header_tpl, content_length);
return write(clientfd, header, header_len);
}
Content-Length
改为使用参数传递进来. 这样的抽象,目前来说够用也合理.
Content-Length
目前来说就是我们的文件长度,所以封装一个方法调用 stat
系统调用来获得文件的大小.
ssize_t get_file_size(const char *filename) {
struct stat st;
int ret = stat(filename, &st);
CT_GUARD(ret);
return st.st_size;
}
struct stat
结构包含了我们常用的文件元信息. 其常见的定义如下:
ssize_t write_http_body(ct_socket_t clientfd, const char *filename,
size_t size) {
char buf[CT_BUF_SIZE] = {0};
FILE *file = fopen(filename, "rb");
if (!file) {
return -1;
}
size_t total_cnt = 0;
while (true) {
size_t read_cnt = fread(buf, sizeof(char), CT_BUF_SIZE, file);
if (read_cnt > 0) {
write(clientfd, buf, read_cnt);
total_cnt += read_cnt;
}
if (read_cnt < CT_BUF_SIZE) {
if (feof(file)) {
break;
} else if (ferror(file)) {
return -1;
}
}
}
return total_cnt;
}
优化 I/O
上面的write_http_body
是一种非常低效的写法,首先我们从文件中经过层层缓冲区从内核层读取到用户层读到应用层,然后又要经过层层缓冲区写入内核区再发送出去.
操作系统通过 mmap
给我们提供了这种能力,可以直接像读内存一个读一个文件,就不用像上面这样来回的传递来传递过去就是了. mmap
签名如下:
#include <sys/mman.h>
void *
mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);
其核心原理还是在于通过虚拟内存管理机制,直接将指定的文件映射进对应的内存块 .这样简化了我们对于文件的读写,提高了性能.
使用 mmap
优化后的代码如下:
ssize_t write_http_body(ct_socket_t clientfd, const char *filename,
size_t size) {
int fd = open(filename, O_RDONLY);
CT_GUARD(fd);
void *map_mem = mmap(0, size, PROT_READ, MAP_PRIVATE, fd, 0);
if (map_mem == MAP_FAILED) {
return -1;
}
int ret = write(clientfd, map_mem, size);
printf("sendfile %s size: %d bytes\n", filename, ret);
munmap(map_mem, size);
close(fd);
return ret;
}
从代码逻辑上也简单很多,事实前一个版本的代码我是以 1024
的缓冲块来读写的, 但是一般来说缓冲大小在 4096
以上,也就是一般的页大小以上的效率才是最佳的. 读写文件缓冲区可以设置为 8192
.
不过使用 mmap
,可以让我们不必手动处理这些细节.
PS: mmap
还有很多实用的用法,比如用于进程间共享内存通信,和分配大块内存. 这里先略过.
PS2: 测试时使用 mmap
的版本,性能有显著的提升,大约45%,欢迎评论贴出你的测试数据.