当前位置: 首页 > news >正文

WebServer 之 http连接处理(下)

目录

✊请求报文--解析

流程图 && 状态机

状态机 -- 状态转移图

主状态机 

从状态机

http 报文解析

HTTP_CODE 含义

从状态机 逻辑

主状态机 逻辑

🐞请求报文--响应

基础API

stat

mmap

iovec

writev

流程图

HTTP_CODE  含义(2)

代码分析

do_request

process_write

http_conn::write


✊请求报文--解析

流程图 && 状态机

状态机 -- 状态转移图

从状态机 -- 读取一行

主状态机 -- 解析该行

(主状态机内部调用从状态机,从状态机驱动主状态机)

👆状态机转移图(结合以下文本理解)

主状态机 

三种状态,标识解析位置

  • CHECK_STATE_REQUESTLINE -- 解析  请求行
  • CHECK_STATE_HEADER -- 解析  请求头
  • CHECK_STATE_CONTENT -- 解析  消息体,仅用于解析  POST请求

从状态机

三种状态,标识解析一行的读取状态

  • LINE_OK,完整读取一行
  • LINE_BAD,报文语法有误
  • LINE_OPEN,读取的行不完整

http 报文解析

流程

上一篇博客,介绍了,服务器接收 http请求 的流程

也就是,浏览器发出 http连接请求,服务器 主线程创建 http对象 接收,

并将所有数据读入对应的 buffer,

将该对象插入任务队列后,工作线程从任务队列取出一个任务并处理

各子线程,通过 process() 函数,处理任务,调用 process_read() 函数 和 process_write() 函数,分别完成 报文解析 和 报文响应 

void http_conn::process()
{// 调用 process_read() 处理请求// 并返回 HTTP_CODE 枚举类型状态码HTTP_CODE read_ret = process_read();// 请求不完整,需要继续接收if (read_ret == NO_REQUEST) {// 注册并监听 读事件,等待下一次数据到来modfd(m_epollfd, m_sockfd, EPOLLLIN);return;}// 调用 process_write() 完成响应bool write_ret = process_write(read_ret);// 响应失败 -- 关闭连接if (!write_ret) close_conn();// 响应成功 -- 注册并监听 写事件,等待下一次写入响应数据modfd(m_epollfd, m_sockfd, EPOLLOUT);
}

HTTP_CODE 含义

HTTP请求的处理结果

头文件初始化了 8 种

报文解析涉及 4 种

  • NO_REQUEST
    • 请求不完整,需要继续读取请求报文数据
  • GET_REQUEST
    • 获得了完整的HTTP请求
  • BAD_REQUEST
    • 语法错误
  • INTERNAL_ERROR
    • 服务器内部错误,该结果在 主状态机 逻辑switch 的 default 下,一般不会触发

解析报文 整体流程

process_read 通过 while 循环,对主从状态机进行封装,循环处理报文每一行

  • 判断条件
    • 主状态机 转移到 CHECK_STATE_CONTENT(解析消息体)
    • 从状态机 转移到 LINE_OK(解析请求行和请求头部)
    • 两者为 或 关系,条件为真则继续循环,否则退出
  • 循环体
    • 从状态机 读取数据
    • 调用 get_line() 函数,通过 m_start_line() 将 从状态机 读取的数据间接赋给 text
    • 主状态机 解析 text
// m_start_line 是行在 buffer 起始位置
// 该位置后面的数据赋给 text
// 此时的从状态机,已提前将一行的末尾字符
// \r\n 变为 \0\0,所以text可直接取出完整的行解析
char* get_line() {return m_read_buf + m_start_line;
}http_conn::HTTP_CODE http_conn::process_read()
{// 初始化从状态机状态,HTTP请求解析结果LINE_STATUS line_status = LINE_OK;HTTP_CODE ret = NO_REQUEST;char* text = 0;// 为什么要写两个判断条件?第一个判断条件// 为什么这样写?// 具体主状态机逻辑--后面讲解// parse_line 为从状态机的具体实现while ( (m_check_state == CHECK_STATE_CONTENT &&line_status == LINE_OK) ||((line_status=parse_line()) ==LINE_OK) ) {text = get_line();// m_start_line 是每一个数据行在m_read_buf的起始位置// m_checked_edx 从状态机 在m_read_buf中读取的位置m_start_line = m_checked_idx;// 主状态机 3 种状态转移逻辑switch(m_check_state) {case CHECK_STATE_REQUESTLINE:{// 解析请求行ret = parse_request_line(text);if (ret == BAD_REQUEST)return BAD_REQUEST;break;}case CHECK_STATE_HEADER:{// 解析请求头ret = parse_headers(text);if (ret == BAD_REQUEST)return BAD_REQUEST;break;}case CHECK_STATE_CONTENT:{// 解析消息体ret = parse_content(text);// 完整解析POST请求后,跳转报文响应函数if (ret == GET_REQUEST)return do_request();// 解析完消息体即完成报文解析,避免再次进入循环// 更新 line_statusline_status = LINE_OPEN;break;}default:return INTERNAL_ERROR;}}return NO_REQUEST;
}

从状态机 逻辑

补充个基础知识👇

HTTP报文中,每一行数据由 \r\n 作为结束字符,空行只有 \r\n

因此,可以通过查找 \r\n 将报文拆解为单独的行进行解析

本项目即利用了这点

从状态机 读取 buffer 中的数据,将每行数据末尾的 \r\n 设置为 \0\0

并更新 从状态机 在 buffer 中读取的位置 m_checked_idx

以此驱动 主状态机 解析

 

  • 从状态机从 m_raed_buf 中,逐字节读取,判断当前的字节是否为 \r
    • 接下来的字符是 \n,将 \r\n 修改为 \0\0,将 m_checked_idx 指向下一行的开头,则返回LINE_OK
    • 接下来到达 buffer 末尾,表示 buffer 还需要继续接收,返回 LINE_OPEN
    • 否则,语法错误,返回 LINE_BAD
  • 当前字节不是 \r,判断是否是 \n(如果上次读取到 \r 就到了 buffer 末尾,没有接收完整,再次接收会出现这个情况)
    • 如果前一个字符是 \r,则将 \r\n 修改为 \0\0,将 m_checked_idx 指向下一行开头,返回 LINE_OK
  • 当前字节,不是 \r,也不是 \n
    • 表示接收不完整,需要继续接收,返回 LINE_OPEN
// 从状态机,用于分析出一行的内容
// 返回值为行的读取状态,有:
// LINE_OK, LINE_BAD, LINE_OPEN// m_read_idx 指向缓冲区 m_read_buf 数据末尾下一字节
// m_checked_idx 指向从状态机当前分析的字节http_conn::LINE_STATUS http_conn::parse_line()
{char temp;for (; m_checked_idx < m_read_idx; ++m_checked_idx){// temp 要分析的字节temp = m_read_buf[m_checked_idx];// 如果当前是 \r,则有可能读取到完整行if (temp == '\r') {// 下一字符达到了 buffer 结尾,则接收不完整,继续接收if (m_checked_idx + 1 == m_read_idx)return LINE_OPEN;// 下一字符是 \n,将 \r\n 改为 \0\0else if (m_read_buf[m_checked_idx+1] == '\n') {m_read_buf[m_checked_idx++] = '\0';m_read_buf[m_checked_idx++] = '\0';return LINE_OK;}// 都不符合,返回 语法错误return LINE_BAD;}// 如果当前字符是 \n,也可能读取到完整的行// 一般是上次读取到 \r,就到 buffer 末尾,没有接收完整// 再次接收时,就会出现这种情况else if (temp == '\n') {// 前一字符是 \r 则接收完整if (m_checked_idx > 1 && m_read_buf[m_checked_idx-1] == '\r'){m_read_buf[m_checked_idx-1] = '\0';m_read_buf[m_checked_idx] = '\0';return LINE_OK;}return LINE_BAD;}}// 没有找到 \r\n 需要继续接收return LINE_OPEN;
}

主状态机 逻辑

(1) 处理请求行

主状态机 初始状态是 CHECK_STATE_REQUESTLINE,通过调用 从状态机 驱动 主状态机

主状态机 解析前,从状态机已经将每一行末尾的 \r\n 改为 \0\0

以便主状态机直接取出对应字符串进行处理

  • 状态(1)CHECK_STATE_REQUESTLINE
    • 主状态机 初始状态,调用 parse_request_line() 解析 请求行
    • 解析函数从 m_read_buf 中解析 HTTP请求行,获得请求方法,目标URL,HTTP版本号
    • 解析完成后,主状态机状态变为 CHECK_STATE_HEADER
// 解析http请求行,获得请求方法,目标URL,http版本号
http_conn::HTTP_CODE http_conn::parse_request_line(char *text)
{// HTTP报文中,请求行用来说明:// 请求类型,要访问的资源,所使用的HTTP版本号// 其中各个部分之间,通过 \t 或 空格 分隔// 请求行中,最先含有 空格 和 \t 任一字符的位置并返回m_url = strpbrk(text, " \t");// 如果没有 空格 或 \t,则报文格式有误if (!m_url) return BAD_REQUEST;// 该位置改为 \0,用于取出前面数据*m_url++ = '\0';// 取出数据,并通过与 GET 和 POST 比较,以确定请求方式char *method = text;if (strcasecmp(method,"GET") == 0)m_method = GET;else if (strcasecmp(method,"POST") == 0) {m_method = POST;cgi = 1;}else return BAD_REQUEST;// m_url 此时跳过了第一个空格或\t字符,但不知道之后是否还有// 将 m_url 向后偏移,通过查找,继续跳过空格和\t字符,// 指向请求资源的第一个字符m_url += strspn(m_url, " \t");// 使用与判断请求方式相同的逻辑,判断 HTTP 版本号m_version = strpbrk(m_url, " \t");if (!m_version)return BAD_REQUEST;*m_version++ = '\0';m_version += strspn(m_verison, " \t");// 仅支持 HTTP/1.1if (strcasecmp(m_verison, "HTTP/1.1") != 0)return BAD_REQUEST;// 对请求资源前 7 个字符进行判断// 这里,有些报文的请求资源会代有 http://// 要单独处理这种情况if (strncasecmp(m_url, "http://", 7) == 0) {m_url += 7;m_url = strchar(m_url, '/');}// 同样的 https 情况if (strncasecmp(m_url, "https://", 8) == 0) {m_url += 8;m_url = strchar(m_url, '/');}// 一般不会带有上述两种符号,// 而是,单独的 / 或 /后带访问资源if (!m_url || m_url[0] != '/')return BAD_REQUEST;// 当 url 为 / 时,显示欢迎界面if (strlen(m_url) == 1) strcat(m_url, "judge.html");// 请求行 处理完毕,将主状态机转移去处理 请求头m_check_state = CHECK_STATE_HEADER;return NO_REQUEST;
}

(2) 处理请求头

解析完 请求行 后,主状态机继续分析请求头

报文中,请求头 和 空行的处理,使用同一个函数

通过判断当前 text 首位,是不是 \0 字符

是 -- 当前处理的是 空行

不是 -- 当前处理的是 请求头

  • 状态(2)CHECK_STATE_HEADER
    • 调用 parse_headers() 解析 请求头
    • 判断空行 OR 请求头
      • 是空行的话,进而判断 content-length 是否为 0(不是 0,即 POST请求,那么状态转移到 CHECK_STATE_CONTENT)(是 0,说明是 GET 请求,则报文解析结束)
      • 若解析的是 请求头部字段,则主要分析 connection 字段,content-length 字段,其他字段可以直接跳过
      • connection 字段,判断是 keep_alive 还是 close,决定是长连接还是短连接
      • content-length 字段,用于读取 post 请求的 消息体长度
// 解析http请求的一个头部信息
http_conn::HTTP_CODE http_conn::parse_headers(char *text)
{// 判断 空行 还是 请求行if (text[0] == '\0') { // 空行// 判断 GET 还是 POST 请求if (m_content_length != 0) { // POST 请求// POST 需跳转到 消息体 处理状态m_check_state = CHECK_STATE_CONTENT;return NO_REQUEST;}return GET_REQUEST; // GET 请求}// 解析请求头部 连接字段else if (strncasecmp(text, "Connection:", 11) == 0) {text += 11;// 跳过 空格 和 \t 字符text += strspn(text, " \t");if (strcasecmp(text, "keep-alive") == 0) {// 如果是长连接,将 linger 标志设置为 truem_linger = true;}}// 解析请求头部 内容长度字段else if (strncasecmp(text, "Content-length:", 15) == 0) {text += 15;text += strspn(text, " \t");m_content_length = atol(text);}// 解析请求头部 HOST字段else if (strncasecmp(text, "Host:", 5) == 0) {text += 5;text += strspn(text, " \t");m_host = text;}else printf("oop! unknown header: %s\n", text);return NO_REQUEST;
}

(3)处理消息体

 如果仅仅是 GET 请求,比如项目中的欢迎界面,那么 主状态机 只设置前两个状态即可

根据之前所说,GET 和 POST 请求报文的区别:有无消息体部分。

GET 请求没有消息体,当解析完空行后,便完成了报文解析

但后续的登录和注册功能,为了避免将用户名和密码,直接暴露在URL中,我们在项目中改用了 POST 请求,将用户名和密码,添加在报文中,作为消息体进行封装

为此,我们需要在解析报文中,添加 解析消息体 的模块

while ( 
(m_check_state==CHECK_STATE_CONTENT && line_status==LINE_OK)
||
( (line_status=parse_line() )==LINE_OK) 
)

判断条件为什么写成这样呢?👆

👆解析

GET 请求报文中,每一行都是 /r/n 结尾,所以对报文进行拆解时,仅用从状态机的状态

( line_status = parse_line() ) == LINE_OK

但,在 POST 请求报文中,消息体的末尾没有任何字符,所以不能使用 从状态机 的状态

这里转而使用 主状态机 的状态,作为循环条件入口

那后面的 && line_status == LINE_OK 又为什么?👆

解析完消息体后,报文的完整解析就完成了

但此时 主状态机 的状态,还是 CHECK_STATE_CONTENT

也就是说,符合循环入口条件

还会再次进入循环,这不是我们所希望的

为此,增加了下面语句,并在完成 消息体 解析后,将 line_status 变量更改为 LINE_OPEN

此时可以跳出循环,完成报文解析任务

  • 状态(3)CHECK_STATE_CONTENT
    • 仅用于解析 POST 请求,调用 parse_content() 解析 消息体
    • 用于保存 post请求 消息体,为后面登录和注册做准备
// 判断 http请求 是否被完整读入
http_conn::HTTP_CODE http_conn::parse_content(char *text)
{// 判断 buffer 中是否读取了消息体if (m_read_idx >= (m_content_length + m_checked_idx)) {text[m_content_length] = '\0';// POST请求 中最后,是输入的用户名和密码m_string = text;return GET_REQUEST;}return NO_REQUEST;
}

状态机 和 HTTP报文解析 是 TinyWebServer 最繁琐的部分

需要 多读 + 画图 来理解

🐞请求报文--响应

本博客,上半部分,我们对 状态机 和 HTTP请求 -- 解析,作了介绍

下面,再介绍 服务器如何响应 http请求报文,并将该报文发送给浏览器

基础API

stat, mmap, iovec, writev 

为了更好的源码阅读体验,这里对源码使用的部分 API 进行介绍

stat

stat() 函数 -- 取得指定文件的文件属性,并将文件属性存储在 结构体 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; // 文件大小,字节数
};

mmap

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

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

iovec

定义一个 向量 元素,用作一个 多元素数组

  • iov_base 指向数据的地址
  • iov_len 表示数据长度
struct iovec {void *iov_base; // starting address of buffersize_t iov_len; // size of buffer
};

writev

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

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

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

writev 以顺序 iov[0],iov[1] 到 iov[iovcnt - 1] 从缓冲区中聚集输出数据

writev 返回输出的字节总数,通常,等于所有缓冲区长度之和

特别注意

循环调用 writev() 时,需要重新处理 iovec 中的指针 和 长度

该函数不会对这两个成员做任何处理

writev() 的返回值为 已写字节数,但这个返回值的实用性不高

因为参数传入的是 iovec 数组,计量单位是 iovcnt,而不是字节数

还需要通过遍历 iovec 来计算新的基址

另外,写入数据的 “结束点” 可能位于一个 iovec 中间的某个位置

因此需要调整临界的 iovec 的 io_base 和 io_len

流程图

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

其中,服务器 子线程 完成报文的解析与响应;

主线程监测 独写事件,调用 read_once 和 http_conn::write 完成数据的 读取与发送

HTTP_CODE  含义(2)

表示 HTTP请求 的处理结果

头文件初始化了 8 种

报文 解析与响应 用到 7 种

  • NO_REQUEST

    • 请求不完整,需要继续读取请求报文数据

    • 跳转主线程继续监测读事件

  • GET_REQUEST

    • 获得了完整的HTTP请求

    • 调用do_request完成请求资源映射

  • NO_RESOURCE

    • 请求资源不存在

    • 跳转process_write完成响应报文

  • BAD_REQUEST

    • HTTP请求报文有语法错误或请求资源为目录

    • 跳转process_write完成响应报文

  • FORBIDDEN_REQUEST

    • 请求资源禁止访问,没有读取权限

    • 跳转process_write完成响应报文

  • FILE_REQUEST

    • 请求资源可以正常访问

    • 跳转process_write完成响应报文

  • INTERNAL_ERROR

    • 服务器内部错误,该结果在主状态机逻辑switch的default下,一般不会触发

代码分析

do_request

process_read() 返回值是,对请求文件分析后的结果

一部分是语法错误导致的 BAD_REQUEST

一部分是 do_request() 返回的结果

该函数将 网站根目录 和 url文件 拼接,再通过 stat 判断该文件属性

另外,为了提高访问速度,通过 mmap 进行映射,将 普通文件 映射到 内存逻辑地址

为了更好的理解请求资源的 访问流程

这里介绍各种 页面跳转机制

浏览器网址栏的字符,即 url,可以抽象成 ip:prot/xxx

xxx 通过 html 文件的 action 属性设置

m_url -- 请求报文中解析出的 请求资源,以 / 开头,也就是 /xxx

TinyWebServer 中解析后的 m_url 有 8 种情况

  • /
    • GET 请求,跳转到 judge.html(欢迎页面)
  • /0
    • POST 请求,跳转到 register.html(注册页面)
  • /1
    • POST 请求,跳转到 log.html(登陆页面)
  • /2CGISQL.cgi
    • POST 请求,进行登录校验
    • 验证成功 -- 跳转 welcome.html(资源请求成功页面)
    • 验证失败 -- 跳转 logError.html(登录失败页面)
  • /3CGISQL.cgi
    • POST 请求,进行注册校验
    • 成功 -- 跳转 log.html(登录页面)
    • 失败 -- 跳转 registerError.html(注册失败页面)
  • /5
    • POST 请求,跳转 picture.html(图片请求页面)
  • /6
    • POST 请求,跳转 vedio.html(视频请求页面)
  • /7
    • POST 请求,跳转 fans.html(关注页面)
// 网站根目录,文件夹内存放 请求资源 和跳转的 html 文件
const char* doc_root = "/home/qgy/github/ini_tinywebserver/root";http_conn::HTTP_CODE http_conn::do_request()
{// 网站根目录doc_root的内容 复制到 m_real_filestrcpy(m_real_file, doc_root);int len = strlen(doc_root);// 找到 m_url 中 / 的位置const char *p = strrchr(m_url, '/');// 实现 登录和注册 校验if (cgi == 1 && (*(p+1) == '2' || *(p+1) == '3') ) {// 根据标志,判断 登录 OR 注册 检测// 同步线程登录校验// CGI多进程登录校验}// 请求资源为 /0,表示跳转 注册页面if (*(p+1) == '0') {char *m_url_real = (char *)malloc(sizeof(char) * 200);strcpy(m_url_real, "/register.html");// 将 网站目录 和 /register.html 拼接// 更新到 m_real_filestrncpy(m_real_file + m_url_real, strlen(m_url_real));free(m_url_real);}// 请求资源为 /1,表示跳转 登录页面else if (*(p+1) == '1') {char *m_url_real = (char *)malloc(sizeof(char) * 200);strcpy(m_url_real, "/log.html");// 网站目录 和 /log.html 拼接// 更新到 m_real_filestrncpy(m_real_file + len, m_url_real, strlen(m_url_real));free(m_url_real);}// 既不是登录,也不是注册,直接将 url 与 网站根目录 拼接// 这里是 welcome 界面,请求服务器的一个图片else strncpy(m_real_file + len, m_url, FILENAME_LEN - len - 1);// 通过 stat 获取 请求资源文件信息,成功 则将信息更新到// m_file_stat 结构体// 失败 返回 NO__RESOURCE 状态,表示 资源不存在if (stat(m_real_file, &m_file_stat) < 0)return NO_RESOURCE;// 判断文件权限,是否可读,不可读 则返回 FORBIDDEN_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);m_file_address = (char*)mmap(0, m_file_stat.st_size,PROT_READ, MAP_PRIVATE, fd, 0);// 避免文件描述符的浪费和占用close(fd);// 请求文件存在,且可以访问return FILE_REQUEST;
}

process_write

根据 do_request() 的返回状态,服务器子线程调用 process_wirte() 向 m_write_buf 

写入响应报文

  • add_status_line() -- 添加状态行:http/1.1  状态码  状态消息
  • add_headers() -- 添加消息报头,内部调用 add_content_length() 和 add_linger() 函数
    • content_length -- 响应报文长度,用于 浏览器 判断 服务器 是否发送完数据
    • connection -- 连接状态,用于告诉 浏览器 保持长连接
  • 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_dix)) {va_end(arg_list);return false;}// 更新 m_write_idx 位置m_write_idx += len;// 清空可变参数列表va_end(arg_list);return true;
}//添加 状态行
bool http::connadd_status_line(int status, const char* title)
{return add_address("%s %d %s\r\n", "HTTP/1.1", status, title);}// 添加 消息报头,具体的,添加 文本长度,连接状态,空行
bool http_conn::add_headers(int content_len)
{add_content_length(content_len);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);
}// 添加 文本类型,这里是 html
bool http_conn::add_content_type()
{return add_response("Content-Type:%s\r\n", "text/html");
}// 添加 连接状态,通知浏览器 保持连接 还是 关闭
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);
}

响应报文分 2 种

一种是 请求文件存在,通过 io 向量机制 iovec

声明两个 iovec,第一个指向 m_write_buf,第二个指向 mmap 的地址 m_file_address

另一种是 请求出错,此时,只申请一个 iovec,指向 m_write_buf 

  • iovec 是一个结构体,里面有 2 个元素,指针成员 iov_base 指向一个缓冲区,这个缓冲区存放 writev 要发送的数据
  • 成员 iov_len 表示 实际写入的长度
bool http_conn::process_write(HTTP_CODE ret)
{switch(ret){// 内部错误  500case INTERNAL_ERROR:{// 状态行add_status_line(500, error_500_title);// 消息报头add_headers(strlen(error_500_form));if (!add_content(error_500_form))return falsebreak;}// 报文语法有误,404case BAD_REQUEST:{add_status_line(404, error_404_tile);add_headers(strlen(error_404_form));if (!add_content(error_404_form))return false;break;}// 资源没有访问权限,403case FORBIDDEN_REQUEST:{add_status_line(403, error_403_title);add_headers(strlen(error_403_form));if(!add_content(error_403_form))return false;break;}// 文件存在,200case 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_dixm_iv[0].iov_base = m_write_buf;m_iv[0].iov_len = m_write_idx;// 第二个iovec指针指向mmap返回的文件指针,长度指向文件大小m_iv[0].iov_base = m_write_buf;m_iv[0].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;return true;
}

http_conn::write

服务器子线程调用 process_write() 完成 响应报文,随后注册 epollout 事件

服务器主线程监测 写事件,并调用 http_conn::write() 函数,将响应报文发送给浏览器

具体逻辑👇

生成响应报文时初始化 byte_to_send,包括 头部信息 和 文件数据大小

通过 writev() 函数,循环发送响应报文数据,根据返回值更新 byte_have_send 和 iovec 结构体指针和长度,并判断响应报文整体是否发送成功

  • 若 writev() 单次发送成功,更新 byte_to_send 和 byte_have_send 大小;
    若响应报文整体发送成功,则取消 mmap 映射,并判断 是否长连接
    • 长连接 -- 重置 http 类实例,注册读事件,不关闭连接
    • 短链接 -- 直接关闭连接
  • 若 writev() 单次发送不成功,判断 是否 缓冲区满了
    • 若不是因为缓冲区满了失败,取消 mmap 映射,关闭连接
    • 若 eagain 则缓冲区满了,更新 iovec 结构体的指针和长度,并注册写事件,等待下一次写事件触发
      (当写缓冲区从不可写变为可写,触发 epollout)
      在这期间,无法立即接收同一用户的下一请求,但可以保证连接的完整性
bool http_conn::wirte()
{int temp = 0;int newadd = 0;// 若要发送的数据长度为 0// 表示响应报文为空,一般不会出现该情况if (bytes_to_send == 0){modfd(m_epollfd, m_sockfd, EPOLLIN);init();return true;}while (1){// 将响应报文的状态行,消息头,空行,响应正文// 发送给浏览器temp = writev(m_sockfd, m_iv, m_iv_count);// 正常发送,temp 为发送的字节数if (temp > 0) {// 更新已发送字节bytes_have_send += temp;// 偏移文件 iovec 的指针newadd = bytes_have_send - m_write_idx;}if (temp <= -1) {// 判断缓冲区是否满了if (errno == EAGAIN) {// 第一个iovec头部信息的数据已发送完,发送第二个iovecif (bytes_have_send >= m_iv[0].iov_len) {// 不再继续发送头部信息m_iv[0].iov_len = 0;m_iv[1].iov_base = m_file_address + newadd;m_iv[1].iov_len = bytes_to_send;}// 继续发送第一个iovec头部信息的数据else {m_iv[0].iov_base = m_write_buf + bytes_to_send;m_iv[0].iov_len = m_iv[0].iov_len - bytes_have_send;}// 重新注册写事件modfd(m_epollfd, m_sockfd, EPOLLOUT);return true;}// 发送失败,但不是缓冲区问题,取消映射unmap();return false;}// 更新已发送 字节数bytes_to_send -= temp;// 判断条件,数据已全部发送完if (bytes_to_send <= 0) {ummap();// 在 epoll 树上重置 EPOLLONESHOT 事件modfd(m_epollfd, m_sockfd, EPOLLIN);// 浏览器的请求为 长连接if (m_linger) {// 重新初始化 HTTP 对象init();return true;}elsereturn false;}}
}

《Linux高性能服务器》中, http_conn::write() 函数不够严谨,这里对其中的 BUG 进行了修复

-->👇

可以正常传输大文件

相关文章:

WebServer 之 http连接处理(下)

目录 ✊请求报文--解析 流程图 && 状态机 状态机 -- 状态转移图 主状态机 从状态机 http 报文解析 HTTP_CODE 含义 从状态机 逻辑 主状态机 逻辑 &#x1f41e;请求报文--响应 基础API stat mmap iovec writev 流程图 HTTP_CODE 含义(2) 代码分析 …...

Android电量相关知识

关于作者&#xff1a;CSDN内容合伙人、技术专家&#xff0c; 从零开始做日活千万级APP。 专注于分享各领域原创系列文章 &#xff0c;擅长java后端、移动开发、商业变现、人工智能等&#xff0c;希望大家多多支持。 目录 一、导读二、概览三、 查看耗电情况3.1 注册广播 ACTION…...

【Java多线程】线程中几个常见的属性以及状态

目录 Thread的几个常见属性 1、Id 2、Name名称 3、State状态 4、Priority优先级 5、Daemon后台线程 6、Alive存活 Thread的几个常见属性 1、Id ID 是线程的唯一标识&#xff0c;由系统自动分配&#xff0c;不同线程不会重复。 2、Name名称 用户定义的名称。该名称在各种…...

鸿蒙OS跨进程IPC与RPC通信

一、IPC与RPC通信概述 基本概念 IPC&#xff08;Inter-Process Communication&#xff09;与RPC&#xff08;Remote Procedure Call&#xff09;用于实现跨进程通信&#xff0c;不同的是前者使用Binder驱动&#xff0c;用于设备内的跨进程通信&#xff0c;后者使用软总线驱动…...

Effective Objective-C 学习(三)

理解引用计数 Objective-C 使用引用计数来管理内存&#xff1a;每个对象都有个可以递增或递减的计数器。如果想使某个对象继续存活&#xff0c;那就递增其引用计数&#xff1a;用完了之后&#xff0c;就递减其计数。计数变为 0时&#xff0c;就可以把它销毁。 在ARC中&#xf…...

蓝桥杯备赛攻略

背景 第十五届蓝桥杯大赛快要到比赛的时间了&#xff0c;按照惯例省赛就在4月9号开赛。有很多的小伙伴都报名了这次比赛&#xff0c;也有很多的同学问我应该怎么训练&#xff0c;什么水平可以拿奖。我自己也已经参加过两届蓝桥杯大赛了&#xff0c;拿到过国赛三等奖&#xff0…...

react反向代理

http-proxy-middleware 使用npm安装 npm i -D http-proxy-middleware 文档 点击查看 关键代码 const { createProxyMiddleware } require(http-proxy-middleware);module.exports function(app) {app.use(/api, // api开头的地址的请求createProxyMiddleware({target: ht…...

债券专题二:可转债估值-二叉树模型

1. 模型背景 由于可转债自身的属性较多&#xff0c;因此对其定价的难度也会加大&#xff0c;在诸多影响因素中&#xff0c;未来的股价占比最高。由于股价的不可预测性&#xff0c;导致了可转债的定价在实际交易中作用非常有限。随着可转债发行数量和规模的增大&#xff0c;越…...

【闲谈】开源软件的崛起与影响

随着信息技术的快速发展&#xff0c;开源软件已经成为软件开发的趋势&#xff0c;并产生了深远的影响。开源软件的低成本、可协作性和透明度等特点&#xff0c;使得越来越多的企业和个人选择使用开源软件&#xff0c;促进了软件行业的繁荣。然而&#xff0c;在使用开源软件的过…...

【教程】Linux使用aria2c多线程满速下载

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhang.cn] 安装aria2c&#xff1a; sudo apt-get install aria2多线程下载&#xff1a; aria2c -x 16 -s 16 <url> 比如&#xff1a; aria2c -x 16 -s 16 http://images.cocodataset.org/zips/test2017.zip...

【漏洞复现】蓝网科技临床浏览系统信息泄露漏洞

Nx01 产品简介 蓝网科技临床浏览系统是一个专门用于医疗行业的软件系统&#xff0c;主要用于医生、护士和其他医疗专业人员在临床工作中进行信息浏览、查询和管理。 Nx02 漏洞描述 蓝网科技临床浏览系统存在信息泄露漏洞&#xff0c;攻击者可以利用该漏洞获取敏感信息。 Nx03…...

JSON转换List<Map<String, Object>>、Map<String, Object>

废话就不说了 早上10点研究到现在 获取redis的JSON字符串 String getPalletListNew redisService.getRedis(“getPalletListNew”, abroad “” goodsLevel “” startPort “” destinationPort “” maxTon “” minTon); 转换Map<String,Object> public …...

单主模式和多主模式切换

1 组复制模式切换注意点 组复制有两种运行模式&#xff0c;一种是单主模式&#xff0c;一种是多主模式。这个模式是在整个组中设置的&#xff0c;由 group_replication_single_primary_mode 这个系统变量指定&#xff0c;而且在所有成员上必须保持一致。ON 表示单主模式&#…...

petalinux2018.3安装步骤

1、虚拟机安装ubuntu-16.04.7-desktop-amd64.iso &#xff08;注意&#xff1a;安装ubuntu-18.04.6-desktop-amd64.iso和ubuntu-16.04.6-desktop-i386.iso会报以下错误&#xff09; environment: line 314: ((: 10 #15~1 > 10 #3: syntax error in expression (error toke…...

ubuntu22.04下使用conda安装pytorch(cpu及gpu版本)

本文介绍了conda下安装cpu、gpu版本的pytorch&#xff1b;并介绍了如何设置镜像源 ubuntu环境安装pytorch的CPU版本与GPU版本 系统&#xff1a;ubuntu22.04 显卡&#xff1a;RTX 3050 依赖工具&#xff1a;miniconda 确认环境 lsb_release -a No LSB modules are available.…...

突破编程_C++_高级教程(模板编程的基础知识)

1 模板编程的基本概念 C 的模板编程是一种编程技术&#xff0c;它允许程序员编写处理不同类型数据的通用代码。通过使用模板&#xff0c;可以创建与特定数据类型无关的函数或类&#xff0c;这些函数或类在编译时可以根据需要生成特定数据类型的版本。这增加了代码的复用性、灵…...

胆小勿入!AI创作恐怖电影宣传片《生化危机:重生》

胆小勿入&#xff01;AI创作恐怖电影宣传片《生化危机&#xff1a;重生》 "The city is falling, and the dead walk among us." "In the shadow of the apocalypse, the fight for survival begins." "The streets are silent, but the nightmare …...

HTTP 超文本传送协议

1 超文本传送协议 HTTP HTTP 是面向事务的 (transaction-oriented) 应用层协议。 使用 TCP 连接进行可靠的传送。 定义了浏览器与万维网服务器通信的格式和规则。 是万维网上能够可靠地交换文件&#xff08;包括文本、声音、图像等各种多媒体文件&#xff09;的重要基础。 H…...

MySQL导入/导出数据

MySQL导入/导出数据 文章目录 MySQL导入/导出数据一、MySQL 导入数据1、mysql 命令导入2、source 命令导入3、使用 LOAD DATA 导入数据4、使用 mysqlimport 导入数据4.1、mysqlimport的常用选项介绍 二、MySQL 导出数据1、使用 SELECT ... INTO OUTFILE 语句导出数据2、mysqldu…...

Matplotlib初探:认识数据可视化与Matplotlib

Matplotlib初探&#xff1a;认识数据可视化与Matplotlib Fig.1 利用Matplotlib进行数据可视化( 可视化代码见文末) &#x1f335;文章目录&#x1f335; &#x1f333;引言&#x1f333;&#x1f333;一、数据可视化简介&#x1f333;&#x1f333;二、Matplotlib库简介&#x…...

LeetCode 0987.二叉树的垂序遍历:遍历时存节点信息,遍历完自定义排序

【LetMeFly】987.二叉树的垂序遍历&#xff1a;遍历时存节点信息&#xff0c;遍历完自定义排序 力扣题目链接&#xff1a;https://leetcode.cn/problems/vertical-order-traversal-of-a-binary-tree/ 给你二叉树的根结点 root &#xff0c;请你设计算法计算二叉树的 垂序遍历…...

TCP 和 UDP的区别

文章目录 概述区别UDPTCPTCP与UDP的选择UDP和TCP编程区别 概述 TCP&#xff08;Transmission Control Protocol&#xff0c;传输控制协议&#xff09;和 UDP&#xff08;User Datagram Protocol&#xff0c;用户数据报协议&#xff09;是互联网中两种最常用的传输层协议 总的来…...

Python 将一维数组或矩阵变为三维

Python 将一维数组或矩阵变为三维 正文 正文 话不多说直接上代码&#xff1a; import numpy as npsampling_points 10001arr np.linspace(0, 2, sampling_points) arr_3D arr.reshape(1, 1, -1) print(arr_3D) """ result: [[[0.0000e00 2.0000e-04 4.0000…...

Python如何实现定时发送qq消息

因为生活中老是忘记各种事情&#xff0c;刚好又在学python&#xff0c;便突发奇想通过python实现提醒任务的功能&#xff08;尽管TIM有定时功能&#xff09;&#xff0c;也可定时给好友、群、讨论组发送qq消息。其工作流程是&#xff1a;访问数据库提取最近计划——>根据数据…...

支付方式接入:支付宝、微信支付、微软支付

支付方式接入&#xff1a;支付宝、微信支付、微软支付 1、微信支付-接入指引 2、支付宝-接入指引 3、微软支付-接入指引 3.1、使用visual studio打包应用&#xff08;发布到微软市场&#xff09;&#xff1a;Package a desktop app from source code using Visual Studio -…...

C++中的互斥量

互斥量是一个类&#xff0c;互斥量的使用必须引入头文件#include <mutex>。互斥量就如同一把锁&#xff0c;在同一时间&#xff0c;多个线程都可以调用lock成员函数尝试给这把锁头加锁&#xff0c;但是只有一个线程可以成功给这把锁加锁&#xff0c;其他没有加锁成功的线…...

盲盒小程序开发

现如今&#xff0c;盲盒已经成为了市场上不可忽视的新型消费模式&#xff0c;并且也逐渐遍布在全球各地中。盲盒的种类商品也逐渐丰富完善&#xff0c;不在局限于性价比高的盲盒玩具、手办等&#xff0c;也发展到了美妆、电子、食品等行业&#xff0c;具有较大的实用性和收藏价…...

安装 Windows 10

1.镜像安装 镜像安装:安装Windows 10 2.安装过程(直接以图的形式呈现) 选择专业版的 等待安装即可...

C++文件操作->文本文件(->写文件、读文件)、二进制文件(->写文件、读文件)

#include<iostream> using namespace std; #include <fstream>//头文件包含 //文本文件 写文件 void test01() { //1.包含头文件 fstream //2.创建流对象 ofstream ofs; //3.指定打开方式 ofs.open("test.txt", ios::out); //4.写…...

Mac相关问题

Mac 更新node版本 第一步&#xff0c;先查看本机node.js版本&#xff1a; node -v 第二步&#xff0c;清除node.js的cache&#xff1a; sudo npm cache clean -f 第三步&#xff0c;安装 n 工具&#xff0c;这个工具是专门用来管理node.js版本的&#xff0c;别怀疑这个工具…...

Python爬虫之Splash详解

爬虫专栏&#xff1a;http://t.csdnimg.cn/WfCSx Splash 的使用 Splash 是一个 JavaScript 渲染服务&#xff0c;是一个带有 HTTP API 的轻量级浏览器&#xff0c;同时它对接了 Python 中的 Twisted 和 QT 库。利用它&#xff0c;我们同样可以实现动态渲染页面的抓取。 1. 功…...

Deep深度系统下载安装Beyond compare4

Beyond Compare 4下载和安装 1、在线安装 Debian, Ubuntu安装命令&#xff1a; wget https://www.scootersoftware.com/bcompare-4.4.6.27483_amd64.deb sudo apt update sudo apt install ./bcompare-4.4.6.27483_amd64.deb Redhat Enterprise Linux, Fedora, CentOS安装命令…...

Qt 使用QScintilla 编辑lua 脚本

需求&#xff1a; 利用QScintilla 编辑lua 脚本 步骤&#xff1a; 1&#xff0c;下载 QScintilla Riverbank Computing | Download 2, 打开 src/qscintilla.pro 文件 编译出 dll库 3&#xff0c;工程中引入这个库 注意debug 模式 必须加载debug 版本编译的库&#xff0…...

2022长安杯复现

案件情况 某地警方接到受害人报案称其在某虚拟币交易网站遭遇诈骗&#xff0c;该网站号称使用“USTD 币”购买所谓的“HT 币”&#xff0c;受害人充 值后不但“HT 币”无法提现、交易&#xff0c;而且手机还被恶意软件锁定 勒索。警方根据受害人提供的虚拟币交易网站调取了对应…...

Netty Review - NioEventLoopGroup源码解析

文章目录 概述类继承关系源码分析小结 概述 EventLoopGroup bossGroup new NioEventLoopGroup(1); EventLoopGroup workerGroup new NioEventLoopGroup();这段代码是在使用Netty框架时常见的用法&#xff0c;用于创建两个不同的EventLoopGroup实例&#xff0c;一个用于处理连…...

团队配置管理规范浅见

在一段时间的工作过程中配置管理工作确实对我们的生产活动产生了巨大的工作量&#xff0c;现在就这个工作来进行梳理一下。 本文主要分为两部分&#xff1a; 1、借用软件系统分析师的配置管理部分内容来介绍配置管理的工作&#xff08;原谅时间精力有限&#xff0c;原文基本已…...

「算法」二分查找1:理论细节

&#x1f387;个人主页&#xff1a;Ice_Sugar_7 &#x1f387;所属专栏&#xff1a;算法详解 &#x1f387;欢迎点赞收藏加关注哦&#xff01; 二分查找算法简介 这个算法的特点就是&#xff1a;细节多&#xff0c;出错率高&#xff0c;很容易就写成死循环有模板&#xff0c;但…...

【网络安全】什么样的人适合学?该怎么学?

有很多想要转行网络安全或者选择网络安全专业的人在进行决定之前一定会有的问题&#xff1a; 什么样的人适合学习网络安全&#xff1f;我适不适合学习网络安全&#xff1f; 当然&#xff0c;产生这样的疑惑并不奇怪&#xff0c;毕竟网络安全这个专业在2017年才调整为国家一级…...

从零开始学习数据结构—【链表】—【探索环形链的设计之美】

环形链表 文章目录 环形链表1.结构图2.具体实现2.1.环形链表结构2.2.头部添加数据2.2.1.具体实现2.2.2.测试添加数据 2.3.尾部添加数据2.3.1.具体实现2.3.2.添加测试数据 2.4.删除头部数据2.4.1.具体实现2.4.2.测试删除数据 2.5.删除尾部数据2.5.1.具体实现2.5.2.测试删除数据 …...

AJAX——HTTP协议

1 HTTP协议-请求报文 HTTP协议&#xff1a;规定了浏览器发送及服务器返回内容的格式 请求报文&#xff1a;浏览器按照HTTP协议要求的格式&#xff0c;发送给服务器的内容 1.1 请求报文的格式 请求报文的组成部分有&#xff1a; 请求行&#xff1a;请求方法&#xff0c;URL…...

java面试微服务篇

目录 目录 SpringCloud Spring Cloud 的5大组件 服务注册 Eureka Nacos Eureka和Nacos的对比 负载均衡 负载均衡流程 Ribbon负载均衡策略 自定义负载均衡策略 熔断、降级 服务雪崩 服务降级 服务熔断 服务监控 为什么需要监控 服务监控的组件 skywalking 业务…...

JS进阶——垃圾回收机制以及算法

版权声明 本文章来源于B站上的某马课程&#xff0c;由本人整理&#xff0c;仅供学习交流使用。如涉及侵权问题&#xff0c;请立即与本人联系&#xff0c;本人将积极配合删除相关内容。感谢理解和支持&#xff0c;本人致力于维护原创作品的权益&#xff0c;共同营造一个尊重知识…...

【快速解决】python项目打包成exe文件——vscode软件

目录 操作步骤 1、打开VSCode并打开你的Python项目。 2、在VSCode终端中安装pyinstaller&#xff1a; 3、运行以下命令使用pyinstaller将Python项目打包成exe文件&#xff1a; 其中your_script.py是你的Python脚本的文件名。 4、打包完成后&#xff0c;在你的项目目录中会…...

数据结构——lesson3单链表介绍及实现

目录 1.什么是链表&#xff1f; 2.链表的分类 &#xff08;1&#xff09;无头单向非循环链表&#xff1a; &#xff08;2&#xff09;带头双向循环链表&#xff1a; 3.单链表的实现 &#xff08;1&#xff09;单链表的定义 &#xff08;2&#xff09;动态创建节点 &#…...

中科大计网学习记录笔记(八):FTP | EMail

前言&#xff1a; 学习视频&#xff1a;中科大郑烇、杨坚全套《计算机网络&#xff08;自顶向下方法 第7版&#xff0c;James F.Kurose&#xff0c;Keith W.Ross&#xff09;》课程 该视频是B站非常著名的计网学习视频&#xff0c;但相信很多朋友和我一样在听完前面的部分发现信…...

QPaint绘制自定义坐标轴组件00

最终效果 1.创建一个ui页面&#xff0c;修改背景颜色 鼠标右键->改变样式表->添加颜色->background-color->选择合适的颜色->ok->Apply->ok 重新运行就可以看到widget的背景颜色已经改好 2.创建一个自定义的widget窗口小部件类&#xff0c;class MyChart…...

MATLAB|基于改进二进制粒子群算法的含需求响应机组组合问题研究(含文献和源码)

目录 主要内容 模型研究 1.改进二进制粒子群算法&#xff08;BPSO&#xff09; 2.模型分析 结果一览 下载链接 主要内容 该程序复现《A Modified Binary PSO to solve the Thermal Unit Commitment Problem》&#xff0c;主要做的是一个考虑需求响应的机组组合…...

JDBC核心技术

第1章 JDBC概述 第2章 获取数据库连接 第3章 使用PreparedStatement实现CRUD操作 第4章 操作BLOB类型字段 第5章 批量插入 第6章 数据库事务 第7章 DAO及相关实现类 第8章 数据库连接池 第9章 Apache-DBUtils实现CRUD操作图像 小部件...

【天幕系列 02】开源力量:揭示开源软件如何成为技术演进与社会发展的引擎

文章目录 导言01 开源软件如何推动技术创新1.1 开放的创新模式1.2 快速迭代和反馈循环1.3 共享知识和资源1.4 生态系统的建设和扩展1.5 开放标准和互操作性 02 开源软件的商业模式2.1 支持和服务模式2.2 基于订阅的模式2.3 专有附加组件模式2.4 开源软件作为平台模式2.5 双重许…...

“挖矿”系列:细说Python、conda 和 pip 之间的关系

继续挖矿&#xff0c;挖“金矿”&#xff01; 1. Python、conda 和 pip&#xff08;挖“金矿”工具&#xff09; Python、conda 和 pip 是在现代数据科学和软件开发中常用的工具&#xff0c;它们各自有不同的作用&#xff0c;但相互之间存在密切的关系&#xff1a; Python&…...