WebServer 解析HTTP 响应报文

一、基础API部分,介绍stat、mmap、iovec、writev、va_list

1.1 stat​ 

作用:获取文件信息

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

// 获取文件属性,存储在statbuf中
int stat(const char *pathname, struct stat *statbuf);

struct stat {
    mode_t  st_mode;         /* 文件类型和权限 */
    off_t   st_size;         /* 文件大小,字节数 */  
};
  • 返回值:成功返回0,失败返回-1;
  • 参数:文件路径(名),struct stat 类型的结构体 

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目录啦。
​

1.2 mmap

用于将一个文件或其他对象映射到内存,提高文件的访问速度

void* mmap(void* start,size_t length,int port,int flags,int fd,off_t offset);
int munmap(void* start,size_t length);
  • start : 映射区的开始地址,设置为0时表示由系统决定映射区的起始地址
  • length : 映射区的长度
  • prot : 期望的内存保护标志,不能与文件的打开模式冲突
    • PROT_READ 表示页内容可以读取
  • flags : 指定映射对象的类型,映射选项和映射页是否可以共享
    • MAP_PRIVATE 建立一个写入时拷贝的私有映射,内存区域的写入不会影响到原文件
  • fd : 有效的文件描述符,一般是由open()函数返回
  • off_toffset : 被映射对象内容的起点

我的往期文章:  

了解Linux 的 mmap --- 笔记_呵呵哒( ̄▽ ̄)"的博客-CSDN博客icon-default.png?t=N7T8https://heheda.blog.csdn.net/article/details/132119077

1.3 iovec

定义了一个向量元素,一般把这个结构用作一个多元素的数组

struct iovec {
    void    *iov_base;    /* starting address of buffer */
    size_t  iov_len;      /* size of buffer */
};
  • iov_base 指向数据的地址
  • iov_len 表示数据的长度
  • iovec是一个结构体,里面有两个元素,指针成员iov_base指向一个缓冲区,这个缓冲区是存放的是writev将要发送的数据
  • 成员iov_len表示实际写入的长度

1.4 writev

writev函数用于在一次函数调用中写多个非连续缓冲区,有时也将这该函数称为聚集写

#include <sys/uio.h>
ssize_t writev(int filedes,const struct iovec* iov,int iovcnt);
  • filedes 表示文件描述符
  • iov 为前述io向量机制结构体 iovec
  • iovcnt 为结构体的个数

 若成功则返回已写的字节数,若出错则返回 -1

  • writev 以顺序 iov[0],iov[1] 至 iov[iovcnt - 1] 从缓冲区中聚集输出数据
  • writev 返回输出的字节总数。通常,它应等于所有缓冲区长度之和

特别注意:循环调用writev时,需要重新处理iovec中的指针和长度,该函数不会对这两个成员做任何处理

writev()函数Linux系统中的一个系统调用,其作用是将多个缓冲区的数据一起写入到文件描述符中(各个缓冲区的数据可以是不连续的)

1.5 va_list

  • va_list 是C语言中用于解决变参问题的一组宏,它定义于标准库 stdarg.h 中
  • 通过命令 $man va_arg 也可以从手册中查询到 va_list 的用法
#include <stdarg.h>

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);

对可变参数列表的处理过程:

  • va_list 定义一个可变参数列表
  • va_start 获取函数可变参数列表
  • va_arg 循环处理可变参数列表中的各个可变参数
  • va_end 结束对可变参数列表的处理 
#include <stdio.h>
#include <stdarg.h>

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
1.5.1 va_start
void va_start(va_list ap, last_arg);
  • ap: 这是一个 va_list 类型的对象,它用来存储通过 va_arg 获取额外参数时所必需的信息
  • 这个函数的作用是初始化 ap 变量,它与 va_arg 和 va_end 函数一起使用
  • last_arg 是最后一个传递给函数的已知的固定参数,即省略号之前的参数
#include <stdarg.h>
#include <stdio.h> 

// 采用可变参数,第一个参数用于标识参数数量
int sum(int num_args,...) {
    int val = 0;
    va_list ap;
    int i;

    va_start(ap,num_args);
    for(i=0;i<num_args;i++) {
        val += va_arg(ap,int);
    }
    va_end(ap);
    return val;
}

void test1() {
    printf("10、20 和 30 的和 = %d\n",  sum(3, 10, 20, 30) );
    printf("4、20、25 和 30 的和 = %d\n",  sum(4, 4, 20, 25, 30) );
}

int main() {
    test1();
    return 0;
}
heheda@heheda:~/Linux/webserver$ gcc va_list.cpp
heheda@heheda:~/Linux/webserver$ ./a.out
10、20 和 30 的和 = 60
4、20、25 和 30 的和 = 79
1.5.2 vsnprintf

作用:使用vsnprintf()用于向一个字符串缓冲区打印格式化字符串,且可以限定打印的格式化字符串的最大长度

#include <stdarg.h>
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
  • 第四个参数,可变参数arg,需要用va_start初始化
 1.5.3 fprintf
int fprintf(FILE *stream, const char *format, ...);
#include <cassert>
#include <cstring>
#include <stdlib.h>
#include <stdarg.h>
#include <stdio.h> 
#include <cerrno>
#include <ctime>

#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

二、 HTTP请求和响应步骤

2.1 HTTP 响应报文格式

响应行\r\n
响应头\r\n
空行\r\n
响应体\r\n

提示: 每项信息之间都要有一个\r\n进行分割
2.2 流程图

浏览器端发出HTTP请求报文,服务器端接收该报文并调用 process_read 对其进行解析,根据解析结果 HTTP_CODE,进入相应的逻辑和模块

  • 服务器子线程完成报文的解析与响应;
  • 主线程监测读写事件,调用 read_oncehttp_conn::write 完成数据的读取与发送

2.3 HTTP_CODE 含义 
 2.4 do_request

do_request 函数的返回值是对请求的文件分析后的结果,一部分是语法错误导致的 BAD_REQUEST,一部分是 do_request 的返回结果(NO_RESOURCE、FORBIDDEN_REQUEST、FILE_REQUEST)

do_request 函数:

  • 将网络将网站根目录 和 url 文件拼接
  • 接着通过 stat 判断该文件属性
  • 为了提高访问速度,可通过mmap进行映射,将普通文件映射到内存逻辑地址
// 网站的根目录
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;
    }
}
 2.5 process_write

根据 do_request 的返回状态,服务器子线程调用 process_write m_write_buf 中写入响应报文

  • add_status_line函数,添加状态行: http/1.1 状态码 状态消息
  • add_headers函数添加消息报头,内部调用add_content_length add_linger函数
    • content-length 记录响应报文长度,用于浏览器端判断服务器是否发送完的数据
    • connection 记录连接状态,用于告诉浏览器端保持长连接
  • add_blank_line添加空行

add_status_line、add_headers、add_content_length、add_linger、add_blank_line这5个函数均是内部调用 add_response 函数更新 m_write_idx 指针缓冲区 m_write_buf 中的内容

2.5.1 add_response
// 往写缓冲中写入待发送的数据
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;
}
2.5.2 add_status_line
// 添加状态行
bool http_conn::add_status_line( int status, const char* title ) {
    return add_response( "%s %d %s\r\n", "HTTP/1.1", status, title );
}
2.5.3 add_headers
// 添加消息报头,具体的添加文本长度
bool http_conn::add_headers(int content_len) {
    add_content_length(content_len);
    add_content_type();
    add_linger();
    add_blank_line();
}
2.5.3 add_headers
// 添加消息报头,具体的添加文本长度
bool http_conn::add_headers(int content_len) {
    add_content_length(content_len);
    add_content_type();
    add_linger();
    add_blank_line();
}
2.5.4 add_content_length
// 添加Content-Length,表示响应报文的长度
bool http_conn::add_content_length(int content_len) {
    return add_response( "Content-Length: %d\r\n", content_len );
}
2.5.5 add_linger
// 添加连接状态,通知浏览器端是保持连接还是关闭
bool http_conn::add_linger()
{
    return add_response( "Connection: %s\r\n", ( m_linger == true ) ? "keep-alive" : "close" );
}
2.5.6 add_blank_line
// 添加空行
bool http_conn::add_blank_line()
{
    return add_response( "%s", "\r\n" );
}
2.5.7 add_content
// 添加文本content
bool http_conn::add_content( const char* content )
{
    return add_response( "%s", content );
}
2.5.8 add_content_type
// 添加文本类型,这里是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 = "<html><body></body></html>";
				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;
}


2.6 write 写事件

该函数实现了分散写的功能,也就是把要发送的数据分为多个部分进行传输

① 首先,在函数开始时,会判断是否存在字节需要发送,如果没有,则结束此次响应

② 通过writev()函数进行分散写操作,将 m_iv 数组中保存的多个缓冲区的数据写入到套接字中。

temp : 表示本次写入的字节数量

  • bytes_have_send : 记录已经发送的字节数  
  • bytes_to_send : 表示剩余需要发送的字节数
// 写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;
            }
        }

    }
}
2.7 总结:生成响应报文的具体流程(来自这篇文章
  1. 进行请求报文的时候,进行URL分析。使用一个文件或其他对象映射到内存,不能映射则报错!
  2. 根据状态码,若为错误状态码,则提取对应的报错介绍和报错信息,作为响应头和响应正文;若为正确的状态码,则提取成功信息作为响应头,提取的内存映射作为响应正文。根据当前的响应头和响应正文的情况,对写缓冲区进行相应的写入(状态行、响应头)
  3. 将写事件写入epoll事件表,之后主线程会自动将写缓冲区中的前半部分报文和响应正文通过分散写传回给客户端
2.8 演示效果:

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博客