1.1 stat
作用:获取文件信息
#include
#include
#include
// 获取文件属性,存储在statbuf中
int stat(const char *pathname, struct stat *statbuf);
struct stat {
mode_t st_mode; /* 文件类型和权限 */
off_t st_size; /* 文件大小,字节数 */
};
struct stat 结构体详解:
struct stat
{
dev_t st_dev; /* ID of device containing file */文件使用的设备号
ino_t st_ino; /* inode number */ 索引节点号
mode_t st_mode; /* protection */ 文件对应的模式,文件,目录等
nlink_t st_nlink; /* number of hard links */ 文件的硬连接数
uid_t st_uid; /* user ID of owner */ 所有者用户识别号
gid_t st_gid; /* group ID of owner */ 组识别号
dev_t st_rdev; /* device ID (if special file) */ 设备文件的设备号
off_t st_size; /* total size, in bytes */ 以字节为单位的文件容量
blksize_t st_blksize; /* blocksize for file system I/O */ 包含该文件的磁盘块的大小
blkcnt_t st_blocks; /* number of 512B blocks allocated */ 该文件所占的磁盘块
time_t st_atime; /* time of last access */ 最后一次访问该文件的时间
time_t st_mtime; /* time of last modification */ /最后一次修改该文件的时间
time_t st_ctime; /* time of last status change */ 最后一次改变该文件状态的时间
};
stat结构体中的st_mode 则定义了下列数种情况:
S_IFMT 0170000 文件类型的位遮罩
S_IFSOCK 0140000 套接字
S_IFLNK 0120000 符号连接
S_IFREG 0100000 一般文件
S_IFBLK 0060000 区块装置
S_IFDIR 0040000 目录
S_IFCHR 0020000 字符装置
S_IFIFO 0010000 先进先出
S_ISUID 04000 文件的(set user-id on execution)位
S_ISGID 02000 文件的(set group-id on execution)位
S_ISVTX 01000 文件的sticky位
S_IRUSR(S_IREAD) 00400 文件所有者具可读取权限
S_IWUSR(S_IWRITE)00200 文件所有者具可写入权限
S_IXUSR(S_IEXEC) 00100 文件所有者具可执行权限
S_IRGRP 00040 用户组具可读取权限
S_IWGRP 00020 用户组具可写入权限
S_IXGRP 00010 用户组具可执行权限
S_IROTH 00004 其他用户具可读取权限
S_IWOTH 00002 其他用户具可写入权限
S_IXOTH 00001 其他用户具可执行权限
上述的文件类型在POSIX中定义了检查这些类型的宏定义:
S_ISLNK (st_mode) 判断是否为符号连接
S_ISREG (st_mode) 是否为一般文件
S_ISDIR (st_mode) 是否为目录
S_ISCHR (st_mode) 是否为字符装置文件
S_ISBLK (s3e) 是否为先进先出
S_ISSOCK (st_mode) 是否为socket
若一目录具有sticky位(S_ISVTX),则表示在此目录下的文件只能被该文件所有者、此目录所有者或root来删除或改名,在linux中,最典型的就是这个/tmp目录啦。
用于将一个文件或其他对象映射到内存,提高文件的访问速度
void* mmap(void* start,size_t length,int port,int flags,int fd,off_t offset);
int munmap(void* start,size_t length);
我的往期文章:
了解Linux 的 mmap --- 笔记_呵呵哒( ̄▽ ̄)"的博客-CSDN博客https://heheda.blog.csdn.net/article/details/132119077
定义了一个向量元素,一般把这个结构用作一个多元素的数组
struct iovec {
void *iov_base; /* starting address of buffer */
size_t iov_len; /* size of buffer */
};
writev函数用于在一次函数调用中写多个非连续缓冲区,有时也将这该函数称为聚集写
#include
ssize_t writev(int filedes,const struct iovec* iov,int iovcnt);
若成功则返回已写的字节数,若出错则返回 -1
特别注意:循环调用writev时,需要重新处理iovec中的指针和长度,该函数不会对这两个成员做任何处理
writev()函数是Linux系统中的一个系统调用,其作用是将多个缓冲区的数据一起写入到文件描述符中(各个缓冲区的数据可以是不连续的)。
#include
void va_start(va_list ap, last);
type va_arg(va_list ap, type);
void va_end(va_list ap);
void va_copy(va_list dest, va_list src);
对可变参数列表的处理过程:
#include
#include
static void show_numbers(int num,...) {
va_list va;
/* 初始化va,让va指向num后面的参数*/
va_start(va,num);
while (num--) {
/* 通过while循环依次获取遍历后面的参数 */
printf("%d\n",va_arg(va,int));
}
va_end(va);
}
int main(void) {
show_numbers(3,110,111,112);
return 0;
}
heheda@heheda:~/Linux/webserver$ gcc va_list.cpp
heheda@heheda:~/Linux/webserver$ ./a.out
110
111
112
void va_start(va_list ap, last_arg);
ap
变量,它与 va_arg 和 va_end 函数一起使用#include
#include
// 采用可变参数,第一个参数用于标识参数数量
int sum(int num_args,...) {
int val = 0;
va_list ap;
int i;
va_start(ap,num_args);
for(i=0;i
heheda@heheda:~/Linux/webserver$ gcc va_list.cpp
heheda@heheda:~/Linux/webserver$ ./a.out
10、20 和 30 的和 = 60
4、20、25 和 30 的和 = 79
作用:使用vsnprintf()
用于向一个字符串缓冲区打印格式化字符串,且可以限定打印的格式化字符串的最大长度
#include
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
- 功能:将可变参数格式化输出到一个字符数组
- 参数:
- str:把生成的格式化的字符串存放在这里
- size:str可接受的最大字符数
- format:指定输出格式的字符串,它决定了你需要提供的可变参数的类型、个数和顺序
- ap:va_list变量
- 返回值:
- 成功:返回最终生成字符串的长度
- 失败:返回负值
n-1
个(留位置给\0
)%d:%s
)va_start
初始化int fprintf(FILE *stream, const char *format, ...);
#include
#include
#include
#include
#include
#include
#include
#define DEBUG 0
#define NOTICE 1
#define WARNING 2
#define FATAL 3
const char* log_level[] = {"DEBUG","NOTICE","WARNING","FATAL"};
// 采用可变参数列表
void logging(int level,const char* format,...) {
assert(level >= DEBUG && level <= FATAL);
char* name = getenv("USER"); // 获取环境变量中的用户(执行命令的用户)
char logInfo[1024];
// 获取可变参数列表
va_list ap;// ap -> char*
va_start(ap,format);
vsnprintf(logInfo,sizeof(logInfo),format,ap);
va_end(ap);// ap = NULL;
// 根据日志等级选择打印到 stderr/stdout
FILE *out = (level == FATAL) ? stderr:stdout;
// 格式化打印到文件中
fprintf(out, "%s | %u | %s | %s\n", \
log_level[level], \
(unsigned int)time(nullptr),\
name == nullptr ? "unknow":name,\
logInfo);
}
int main() {
logging(DEBUG, "socket create success: %d", 114514);
logging(FATAL, "socket:%s:%d", strerror(errno), 11234);
return 0;
}
heheda@heheda:~/Linux/webserver$ g++ 利用可变参数实现log类.cpp
heheda@heheda:~/Linux/webserver$ ./a.out
DEBUG | 1694175430 | heheda | socket create success: 114514
FATAL | 1694175430 | heheda | socket:Success:11234
响应行\r\n
响应头\r\n
空行\r\n
响应体\r\n
提示: 每项信息之间都要有一个\r\n进行分割
浏览器端发出HTTP请求报文,服务器端接收该报文并调用 process_read 对其进行解析,根据解析结果 HTTP_CODE,进入相应的逻辑和模块
do_request 函数的返回值是对请求的文件分析后的结果,一部分是语法错误导致的 BAD_REQUEST,一部分是 do_request 的返回结果(NO_RESOURCE、FORBIDDEN_REQUEST、FILE_REQUEST)
do_request 函数:
// 网站的根目录
const char* doc_root = "/home/nowcoder/webserver/resources";
// 当得到一个完整、正确的HTTP请求时,我们就分析目标文件的属性,
// 如果目标文件存在、对所有用户可读,且不是目录,则使用mmap将其
// 映射到内存地址m_file_address处,并告诉调用者获取文件成功
http_conn::HTTP_CODE http_conn::do_request()
{
// "/home/nowcoder/webserver/resources"
// 将初始化的m_real_file赋值为网站根目录
strcpy( m_real_file, doc_root );
int len = strlen( doc_root );
strncpy( m_real_file + len, m_url, FILENAME_LEN - len - 1 );
// 获取m_real_file文件的相关的状态信息,-1失败,0成功
// 通过stat获取请求资源文件信息,成功则将信息更新到m_file_stat结构体
// 失败返回NO_RESOURCE状态,表示资源不存在
if ( stat( m_real_file, &m_file_stat ) < 0 ) {
return NO_RESOURCE;
}
// 判断文件的权限,是否可读,不可读则返回FORBINDDEN_REQUEST状态
if ( ! ( m_file_stat.st_mode & S_IROTH ) ) {
return FORBIDDEN_REQUEST;
}
// 判断文件类型,如果是目录,则返回BAD_REQUEST,表示请求报文有误
if ( S_ISDIR( m_file_stat.st_mode ) ) {
return BAD_REQUEST;
}
// 以只读方式获取文件描述符,通过mmap将该文件映射到内存中
int fd = open( m_real_file, O_RDONLY );
// 创建内存映射
// 通过mmap将该文件映射到内存中
m_file_address = ( char* )mmap( 0, m_file_stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0 );
// 避免文件描述符的浪费和占用
close( fd );
// 表示请求文件存在,且可以访问
return FILE_REQUEST;
}
unmap 取消映射
// 对内存映射区执行munmap操作
void http_conn::unmap() {
if( m_file_address )
{
munmap( m_file_address, m_file_stat.st_size );
m_file_address = 0;
}
}
根据 do_request 的返回状态,服务器子线程调用 process_write 向 m_write_buf 中写入响应报文
add_status_line、add_headers、add_content_length、add_linger、add_blank_line这5个函数,均是内部调用 add_response 函数更新 m_write_idx 指针和缓冲区 m_write_buf 中的内容
// 往写缓冲中写入待发送的数据
bool http_conn::add_response( const char* format, ... ) {
// 如果写入内容超出m_write_buf大小则报错
if( m_write_idx >= WRITE_BUFFER_SIZE ) {
return false;
}
// 定义可变参数列表
va_list arg_list;
// 将变量arg_list初始化为传入参数
va_start( arg_list, format );
// 将数据format从可变参数列表写入缓冲区写,返回写入数据的长度
int len = vsnprintf( m_write_buf + m_write_idx, WRITE_BUFFER_SIZE - 1 - m_write_idx, format, arg_list );
// 如果写入的数据长度超过缓冲区剩余空间,则报错
if( len >= ( WRITE_BUFFER_SIZE - 1 - m_write_idx ) ) {
va_end( arg_list );
return false;
}
// 更新m_write_idx位置
m_write_idx += len;
// 清空可变参列表
va_end( arg_list );
return true;
}
// 添加状态行
bool http_conn::add_status_line( int status, const char* title ) {
return add_response( "%s %d %s\r\n", "HTTP/1.1", status, title );
}
// 添加消息报头,具体的添加文本长度
bool http_conn::add_headers(int content_len) {
add_content_length(content_len);
add_content_type();
add_linger();
add_blank_line();
}
// 添加消息报头,具体的添加文本长度
bool http_conn::add_headers(int content_len) {
add_content_length(content_len);
add_content_type();
add_linger();
add_blank_line();
}
// 添加Content-Length,表示响应报文的长度
bool http_conn::add_content_length(int content_len) {
return add_response( "Content-Length: %d\r\n", content_len );
}
// 添加连接状态,通知浏览器端是保持连接还是关闭
bool http_conn::add_linger()
{
return add_response( "Connection: %s\r\n", ( m_linger == true ) ? "keep-alive" : "close" );
}
// 添加空行
bool http_conn::add_blank_line()
{
return add_response( "%s", "\r\n" );
}
// 添加文本content
bool http_conn::add_content( const char* content )
{
return add_response( "%s", content );
}
// 添加文本类型,这里是html
bool http_conn::add_content_type() {
return add_response("Content-Type:%s\r\n", "text/html");
}
响应报文分为两种
io
向量机制iovec
,声明两个iovec
m_write_buf(包含响应首行和响应头部)
mmap
的地址m_file_address
;(响应体)iovec
,指向m_write_buf(包含响应首行、响应头部、响应体)
>>process_write : 根据服务器处理HTTP请求的结果,决定返回给客户端的内容
// 定义HTTP响应的一些状态信息
const char* ok_200_title = "OK";
const char* error_400_title = "Bad Request";
const char* error_400_form = "Your request has bad syntax or is inherently impossible to satisfy.\n";
const char* error_403_title = "Forbidden";
const char* error_403_form = "You do not have permission to get file from this server.\n";
const char* error_404_title = "Not Found";
const char* error_404_form = "The requested file was not found on this server.\n";
const char* error_500_title = "Internal Error";
const char* error_500_form = "There was an unusual problem serving the requested file.\n";
// 根据服务器处理HTTP请求的结果,决定返回给客户端的内容
bool http_conn::process_write(HTTP_CODE ret) {
switch (ret)
{
// 内部错误,500
case INTERNAL_ERROR:
// 状态行
add_status_line( 500, error_500_title );
// 消息头
add_headers( strlen( error_500_form ) );
if ( ! add_content( error_500_form ) ) {
return false;
}
break;
// 报文语法有误,400
case BAD_REQUEST:
add_status_line( 400, error_400_title );
add_headers( strlen( error_400_form ) );
if ( ! add_content( error_400_form ) ) {
return false;
}
break;
// 请求资源不存在,404
case NO_RESOURCE:
add_status_line( 404, error_404_title );
add_headers( strlen( error_404_form ) );
if ( ! add_content( error_404_form ) ) {
return false;
}
break;
// 资源没有访问权限,403
case FORBIDDEN_REQUEST:
add_status_line( 403, error_403_title );
add_headers(strlen( error_403_form));
if ( ! add_content( error_403_form ) ) {
return false;
}
break;
// 请求资源可以正常访问,200
case FILE_REQUEST:
add_status_line(200, ok_200_title );
// 如果请求的资源存在
if(m_file_stat.st_size !=0 ) {
add_headers(m_file_stat.st_size);
// 第一个iovec指针指向响应报文缓冲区,长度指向m_write_idx
m_iv[ 0 ].iov_base = m_write_buf;
m_iv[ 0 ].iov_len = m_write_idx;
// 第二个iovec指针指向mmap返回的文件指针,长度指向文件大小
m_iv[ 1 ].iov_base = m_file_address;
m_iv[ 1 ].iov_len = m_file_stat.st_size;
m_iv_count = 2;
// 发送的全部数据为响应报文头部信息和文件大小
bytes_to_send = m_write_idx + m_file_stat.st_size;
return true;
}
else {
// 如果请求的资源大小为0,则返回空白html文件
const char* ok_string = "";
add_headers(strlen(ok_string));
if(!add_content(ok_string))
return false;
}
default:
return false;
}
// 除FILE_REQUEST状态之外,其余状态只申请一个iovec,指向响应报文缓冲区
m_iv[ 0 ].iov_base = m_write_buf;
m_iv[ 0 ].iov_len = m_write_idx;
m_iv_count = 1;
bytes_to_send = m_write_idx;
return true;
}
该函数实现了分散写的功能,也就是把要发送的数据分为多个部分进行传输
① 首先,在函数开始时,会判断是否存在字节需要发送,如果没有,则结束此次响应
② 通过writev()函数进行分散写操作,将 m_iv 数组中保存的多个缓冲区的数据写入到套接字中。
temp : 表示本次写入的字节数量
// 写HTTP响应
// 写事件是指服务器向客户端“写”数据,也就是服务器向客户端发送数据(即HTTP响应报文)
bool http_conn::write()
{
int temp = 0;
// 若要发送的数据长度为0
// 表示响应报文为空,一般不会出现这种情况
if ( bytes_to_send == 0 ) {
// 将要发送的字节为0,这一次响应结束。
modfd( m_epollfd, m_sockfd, EPOLLIN );
// 重新初始化HTTP对象
init();
return true;
}
while(1) {
// 分散写
// 将响应报文的状态行、消息头、空行和响应正文发送给浏览器端
temp = writev(m_sockfd, m_iv, m_iv_count);
if ( temp <= -1 ) {
// 判断缓冲区是满了
// 如果TCP写缓冲没有空间,则等待下一轮EPOLLOUT事件,虽然在此期间,
// 服务器无法立即接收到同一客户的下一个请求,但可以保证连接的完整性
if( errno == EAGAIN ) {
// 重新注册写事件
modfd( m_epollfd, m_sockfd, EPOLLOUT );
return true;
}
// 如果发送失败,但不是缓冲区问题,取消映射
unmap();
return false;
}
bytes_have_send += temp;// 已发送的字节数
// 更新已发送字节数
bytes_to_send -= temp;// 剩余需要发送的字节数
// 第一个iovec头部信息的数据已发送完,发送第二个iovec数据
if (bytes_have_send >= m_iv[0].iov_len) {
// 不再继续发送头部信息
m_iv[0].iov_len = 0;
m_iv[1].iov_base = m_file_address + (bytes_have_send - m_write_idx);
m_iv[1].iov_len = bytes_to_send;
}
// 继续发送第一个iovec头部信息的数据
else
{
m_iv[0].iov_base = m_write_buf + bytes_have_send;
m_iv[0].iov_len = m_iv[0].iov_len - temp;
}
// 判断条件,数据已全部发送完
if (bytes_to_send <= 0)
{
// 取消映射
unmap();
// 在epoll树上重置EPOLLONESHOT事件
modfd(m_epollfd, m_sockfd, EPOLLIN);
// 浏览器的请求为长连接
if (m_linger)
{
// 重新初始化HTTP对象
init();
return true;
}
else
{
return false;
}
}
}
}
heheda@heheda:~/Linux/heheda_test/webserver$ g++ main.cpp -pthread
heheda@heheda:~/Linux/heheda_test/webserver$ ./a.out
按照如下格式运行:a.out port_number
heheda@heheda:~/Linux/heheda_test/webserver$ ./a.out 9999
create the 0th thread
create the 1th thread
create the 2th thread
create the 3th thread
create the 4th thread
create the 5th thread
create the 6th thread
create the 7th thread
heheda read
读取到了数据:GET /index.html HTTP/1.1
Host: 192.168.90.131:9999
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.69
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
got 1 http line : GET /index.html HTTP/1.1
got 1 http line : Host: 192.168.90.131:9999
got 1 http line : Connection: keep-alive
got 1 http line : Upgrade-Insecure-Requests: 1
oop!unknow header Upgrade-Insecure-Requests: 1
got 1 http line : User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.69
oop!unknow header User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.69
got 1 http line : Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
oop!unknow header Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
got 1 http line : Accept-Encoding: gzip, deflate
oop!unknow header Accept-Encoding: gzip, deflate
got 1 http line : Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
oop!unknow header Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
got 1 http line :
write....
heheda read
读取到了数据:GET /images/image1.jpg HTTP/1.1
Host: 192.168.90.131:9999
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.69
Accept: image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Referer: http://192.168.90.131:9999/index.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
got 1 http line : GET /images/image1.jpg HTTP/1.1
got 1 http line : Host: 192.168.90.131:9999
got 1 http line : Connection: keep-alive
got 1 http line : User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.69
oop!unknow header User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.69
got 1 http line : Accept: image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
oop!unknow header Accept: image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
got 1 http line : Referer: http://192.168.90.131:9999/index.html
oop!unknow header Referer: http://192.168.90.131:9999/index.html
got 1 http line : Accept-Encoding: gzip, deflate
oop!unknow header Accept-Encoding: gzip, deflate
got 1 http line : Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
oop!unknow header Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
got 1 http line :
write....
heheda read
读取到了数据:GET /favicon.ico HTTP/1.1
Host: 192.168.90.131:9999
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.69
Accept: image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Referer: http://192.168.90.131:9999/index.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
got 1 http line : GET /favicon.ico HTTP/1.1
got 1 http line : Host: 192.168.90.131:9999
got 1 http line : Connection: keep-alive
got 1 http line : User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.69
oop!unknow header User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.69
got 1 http line : Accept: image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
oop!unknow header Accept: image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
got 1 http line : Referer: http://192.168.90.131:9999/index.html
oop!unknow header Referer: http://192.168.90.131:9999/index.html
got 1 http line : Accept-Encoding: gzip, deflate
oop!unknow header Accept-Encoding: gzip, deflate
got 1 http line : Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
oop!unknow header Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
got 1 http line :
write....
推荐和参考文章:
超详细的HTTP协议请求报文、响应报文教程! - 知乎 (zhihu.com)
“Web 服务器” 笔记04 ------ 生成、写HTTP响应_http响应报文为啥用writev_CV发烧友的博客-CSDN博客
writev函数 - lypbendlf - 博客园 (cnblogs.com)
最新版Web服务器项目详解 - 06 http连接处理(下) (qq.com)
【webserver】 第8节 响应报文的生成_webserver中http处理类中写缓冲区大小小于读缓冲区_几日行云的博客-CSDN博客