惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Tenable Blog
博客园_首页
Vercel News
Vercel News
WordPress大学
WordPress大学
美团技术团队
G
Google Developers Blog
大猫的无限游戏
大猫的无限游戏
小众软件
小众软件
Y
Y Combinator Blog
博客园 - 【当耐特】
量子位
酷 壳 – CoolShell
酷 壳 – CoolShell
The Cloudflare Blog
T
The Blog of Author Tim Ferriss
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Google DeepMind News
Google DeepMind News
云风的 BLOG
云风的 BLOG
腾讯CDC
M
MIT News - Artificial intelligence
爱范儿
爱范儿
Recent Announcements
Recent Announcements
雷峰网
雷峰网
Last Week in AI
Last Week in AI
宝玉的分享
宝玉的分享
The Register - Security
The Register - Security
Jina AI
Jina AI
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Hugging Face - Blog
Hugging Face - Blog
P
Privacy & Cybersecurity Law Blog
Recorded Future
Recorded Future
Help Net Security
Help Net Security
N
News and Events Feed by Topic
博客园 - Franky
P
Proofpoint News Feed
L
LINUX DO - 热门话题
S
SegmentFault 最新的问题
The GitHub Blog
The GitHub Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
月光博客
月光博客
D
Docker
Google DeepMind News
Google DeepMind News
有赞技术团队
有赞技术团队
IT之家
IT之家
Security Latest
Security Latest
L
LangChain Blog
V
V2EX
阮一峰的网络日志
阮一峰的网络日志
J
Java Code Geeks

文章列表 - 她和她的猫

使用小鹤双拼一个月 折腾 Hugo PaperMod 主题 去长鹿旅游休博园 1Password 自动化登录堡垒机 深入 Hyperf:Inject 注解是如何工作的? 译|理解容器镜像层 使用 Rust 实现高性能预写日志(Write Ahead Log) 译|使用 Linux 命名空间隔离系统 深入 Hyperf:HTTP 服务是如何处理请求的? Linux 文件权限学习笔记 译|了解 Bitcask:基于日志结构的 KV 存储引擎 还原 SM2 压缩公钥的几种方法 深入 Hyperf:HTTP 服务启动时发生了什么? 一次 Hyperf 注解失效问题分析 将博客迁移到又拍云 译|PHP 7 新的 Hashtable 实现 排查 ES 查询问题:深入了解 json_encode() 函数 浅析 Redlock 分布式锁实现原理 Yar 源码阅读笔记:RPC 服务端 Yar 源码阅读笔记:客户端的并行调用 Yar 源码阅读笔记:客户端的同步调用 Yar 源码阅读笔记:数据传输模块 Yar 源码阅读笔记:消息编码模块 Yar 源码阅读笔记:RPC 通信协议 Yar 源码阅读笔记:开篇 PHP 8 新特性介绍 基于 OpenSSL 实现国密 SM4 加解密 PHP 源码阅读笔记:编译与调试 PHP 什么是惊群问题 大端模式和小端模式 基于 GitHub Actions 定时推送网址到百度站长平台 聊聊五种 I/O 模型 Workerman 源码分析:文件上传 PHP 网络编程:构建 MySQL 蜜罐获取攻击者微信 ID 使用 Workerman 接入 Bilibili 直播弹幕协议 使用 GitHub Actions 自动部署 Hexo 什么是二进制安全 【转载】PHP 程序员进阶之路 优化 Workerman 检查主进程是否存活的逻辑 基于 Redis 实现分布式锁 《PHP 实现 Base64 编码/解码》笔记 PHP 多进程下载必应壁纸 基于 Redis 实现延迟队列 【转载】PHP 实现 Base64 编码/解码 使用 MySQL FIELD() 函数自定义排序规则 Laravel 的 Facades 实现原理 C语言单链表实现约瑟夫环 数据结构学习笔记:顺序栈和链栈(C语言) 使用 QueryList + Redis 下载壁纸 在 Laravel 登录/注册中使用 mews/captcha 扩展包 使用 mews/captcha 扩展包为 Laravel 应用添加图片验证码 给 YOURLS 短网址系统编写插件《批量生成短网址》 给 YOURLS 短网址系统编写插件《Hello World!》 PHP 7.1 使用 json_encode 函数造成浮点类型数据出现精度问题 Laravel 授权策略(Policy)的基本使用 Ubuntu 系统下 WiFi 频繁掉线解决方法 Laravel 应用部署到 Nginx 服务器上的第一个坑 Ubuntu 16.04 LST 安装 Redis 和 Composer PHP 采集逐浪小说章节列表 Nginx 环境 SSL 强制 HTTP 301 跳转到 HTTPS 【工作&生活】第一周 ServiceStack.Redis 与多线程 Count the number of Duplicates C# Base64 加密及解密 ASP.NET MVC 使用动态类型传递数据 Django 使用 order_by() 对数据进行排序操作 ASP.NET + jQuery + AJAX 实现用户登录 ASP.NET 从客户端中检测到有潜在危险的 Request.Form 值 ASP.NET 使用 FormData 实现 AJAX 上传图片 Python 使用 BeautifulSoup 抓取网页 贪心算法之字符串的完美度 一言不和就造轮子之 CookieHelper ASP.NET 上传图片 DBHelper 类 C# 捕获屏幕源码 用 JavaScript 做一些有趣的事情 第一篇文章 好久不见。 如何防止非法调用 PHP 文件 清明小记。 PHP 生成随机字符串 Z-Blog PHP 程序做 301 跳转 PHP 处理数组常用的几个函数 Sublime Text 3 快捷键总结 PHP 使用 file_get_contents() 函数实现采集网页 使用 C# 编写学生信息管理系统 终于到了。 zblog报错: magic_quotes_gpc 和 safe_mode 世事无常。 如何使用 PHP 上传文件 安卓手机编写和运行 PHP 程序的软件 使用 AJAX 刷新、验证 PHP 图片验证码 PHP 生成图片验证码 PHP 之 session 的使用方法 PHP 之 cookie 的使用方法 PHP 实现分页原理详解 Thinkphp 框架中系统常量在 Js 文件中不解析的解决方法
浅入浅出 HTTP 协议
她和她的猫 · 2021-04-17 · via 文章列表 - 她和她的猫

先粘一段来自 MDN 的解释:

HTTP 消息是服务器和客户端之间交换数据的方式。有两种类型的消息︰ 请求(requests)--由客户端发送用来触发一个服务器上的动作;响应(responses)--来自服务器的应答。

HTTP消息由采用 ASCII 编码的多行文本构成。

说白了就是客户端与服务端通信的协议,就像人与人之间进行交流,大家都要用相同的语言(协议)才能进行沟通(通信)。

GET 请求

来看一个简单的 GET 请求协议的数据:

GET /index.html?name=her-cat HTTP/1.1\r\n
Host: 127.0.0.1:8081\r\n
Connection: keep-alive\r\n
Accept: text/html\r\n
\r\n

这里通过多行展示只是为了看起来更清晰,实际上不会有视觉上的换行

第一行是 请求行(Request Line),由请求方法、请求地址(包含请求参数)、HTTP 协议版本三部分组成,通过空格分隔;服务端只需要解析该行就可以知道客户端通过什么方式取哪里的数据。

第二、三行是 请求头(Request Header),每行以 字段: 值 方式组成,用来携带一些请求信息。比如 Host 就是目标主机的地址和端口号,Connection 就是连接方式。

第四行是一个 CRLF(又称回车换行符:\r\n),用来表示请求头信息到此结束。

在传输过程中协议的数据实际是这样的:

GET /index.html?name=her-cat HTTP/1.1\r\nHost: 127.0.0.1:8081\r\nConnection: keep-alive\r\nAccept: text/html\r\n\r\n

可以看到最后有两个连续的 CRLF,第一个是上一行的结束标识,第二个是请求头结束标识。

通过判断数据中是否包含两个连续的 CRLF 来确定 HTTP 请求头是否读取完毕。

下面是 Workerman 的实现:

// source: Workerman/Protocols/Http.php

/**
 * 返回值为 0 有两种情况:
 *   1) 如果断开了连接,表示不再接收数据
 *   2) 如果未断开连接,表示需要继续接收数据
 *
 * 返回值大于 0 表示 HTTP 协议包的大小或请求体的大小
 *
 */
public static function input($recv_buffer, TcpConnection $connection)
{
    // 判断收到的数据中是否包含两个连续的 CRLF
    $crlf_pos = \strpos($recv_buffer, "\r\n\r\n");
    // false 说明不包含,需要继续接收数据
    if (false === $crlf_pos) {
        // 判断请求头数据长度是否超限
        if ($recv_len = \strlen($recv_buffer) >= 16384) {
            // 超限则断开连接并响应 413 状态码
            $connection->close("HTTP/1.1 413 Request Entity Too Large\r\n\r\n");
            return 0;
        }
        return 0;
    }

    // 请求头长度,+4 是将两个 CRLF 的长度算上
    $head_len = $crlf_pos + 4;
    // 请求方式
    $method = \strstr($recv_buffer, ' ', true);

    if ($method === 'GET' || $method === 'OPTIONS' || $method === 'HEAD' || $method === 'DELETE') {
        // 这几种方法不需要接收请求体,直接返回请求头长度
        return $head_len;
    } else if ($method !== 'POST' && $method !== 'PUT') {
        // 非法的请求方式
        $connection->close("HTTP/1.1 400 Bad Request\r\n\r\n", true);
        return 0;
    }

    // 到这里说明本次是 POST 或 PUT 请求,在后面的 POST 请求中讲解。
}

POST 请求

POST 请求其实跟 GET 差不多,只不过请求方法是 POST,多了一个请求体。

POST / HTTP/1.1\r\n
Host: 127.0.0.1:8080\r\n
Connection: keep-alive\r\n
Content-Length: 28\r\n
Accept: text/html\r\n
Content-Type: application/x-www-form-urlencoded\r\n
\r\n
name=her-cat&password=123456

name=her-cat&password=123456 就是请求体的内容。

Content-Length 字段用来表示请求体的大小,如果请求体为空,那么该值为 0。

在接收 HTTP 协议数据时,可以通过 Content-Length 字段判断请求数据是否接收完毕。

当 Content-Length 等于已接收的字节数时,说明已经读取完毕了。

public static function input($recv_buffer, TcpConnection $connection)
{
    ...上面解析 GET 请求的逻辑...

    // 获取请求头的内容
    $header = \substr($recv_buffer, 0, $crlf_pos);
    // 请求体的长度
    $length = false;

    // 解析请求体的长度
    if ($pos = \strpos($header, "\r\nContent-Length: ")) {
        $length = $head_len + (int)\substr($header, $pos + 18, 10);
    } else if (\preg_match("/\r\ncontent-length: ?(\d+)/i", $header, $match)) {
        $length = $head_len + $match[1];
    }

    if ($length !== false) {
        // 返回请求体的长度
        return $length;
    }

    // 未解析到 Content-Length 字段则断开连接并响应 400 状态码
    $connection->close("HTTP/1.1 400 Bad Request\r\n\r\n", true);
    return 0;
}

Content-Type 字段表示请求体的类型,这个值可以通过 form 表单的 enctype 属性指定,有 application/x-www-form-urlencodedmultipart/form-data 两种类型,下面分别介绍一下两种类型的区别。

application/x-www-form-urlencoded

当 enctype 为 application/x-www-form-urlencoded 时,在发送到服务端之前,会先将表单的数据转为 名称=值 的格式,再通过 & 符号将它们拼接起来。

如果名称或值有特殊字符,则会先将它们进行 urlencode(又叫百分号编码),编码方法很简单,在该字节 ASCII 码的 16 进制字符前面加 %

比如 & 字符,ASCII 码是 38,对应 16 进制是 26,那么 urlencode 编码结果是 %26。

用 PHP 实现该过程:

$form_params = [
    '&name' => 'her-cat=',
    'password' => '123456&',
];

$data = [];
foreach ($form_params as $name => $value) {
    $data[] = urlencode($name).'='.urlencode($value);
}

$body = implode('&', $data);

echo $body;

// 输出:%26name=her-cat%3D&password=123456%26

PHP 服务端接收到该数据后,可以使用 parse_str 函数得到表单数据。

注意,application/x-www-form-urlencoded 类型下,请求体末尾没有 CRLF。

multipart/form-data

当 enctype 为 multipart/form-data 时,收到的 HTTP 数据:

POST / HTTP/1.1\r\n
Host: 127.0.0.1:8080\r\n
Connection: keep-alive\r\n
Content-Length: 243\r\n
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarySyX8l4XxjtjHAusG\r\n
\r\n
------WebKitFormBoundarySyX8l4XxjtjHAusG\r\n
Content-Disposition: form-data; name="name"\r\n
\r\n
her-cat\r\n
------WebKitFormBoundarySyX8l4XxjtjHAusG\r\n
Content-Disposition: form-data; name="file"; filename="code.txt"
Content-Type: text/plain\r\n
\r\n
hello her-cat\r\n
------WebKitFormBoundarySyX8l4XxjtjHAusG--\r\n

相对于上一种类型,multipart/form-data 要复杂一些,因为这种类型不仅仅可以用来提交表单数据,本文的重点“文件上传”也是用的该类型,只不过它们的格式有一点点不同。

可以看到 Content-Type 中除了 multipart/form-data,还多了 boundary=—-WebKitFormBoundarySyX8l4XxjtjHAusG。

boundary 翻译过来就是边界的意思,它表示了数据块在请求体中的边界,后面的值就是分割请求体中数据块的边界值。

在边界值的前面添加 -- 才是在请求体中实际的边界值。

  ----WebKitFormBoundarySyX8l4XxjtjHAusG // 请求头中的边界值
------WebKitFormBoundarySyX8l4XxjtjHAusG // 请求体中的边界值

boundary 的值并非是固定的,可以是 1 到 70 个字符组成的随机字符串,不同的浏览器中 boundary 的生成规则也不一样。

Content-Disposition

Content-Disposition 存储了数据块的内容信息,form-data 表示是表单数据,name 字段表示该数据块的名称,如果有 filename 字段说明该数据块是用来存储文件上传的数据,filename 字段存储的是文件名称。

Content-Type

Content-Type 字段只会在文件上传时才会存在,用于表示数据块的内容类型。比如 text/plain 表示纯文本、image/jpeg 表示 jpg 图片、video/mp4 表示视频。

详见:MIME 类型

紧接着一个空白行(实际上是 CRLF)后面的就是数据块的内容。如果数据块是普通的表单数据,这里就是它的值,如果是文件上传,那么这里就是文件的内容了。

最后,通过在边界值的后面添加 -- 表示请求体结束。

总结

这篇文章其实是 “Workerman 源码分析:基于 HTTP 协议实现文件上传” 的前半部分,写的时候发现大部分都是在讲 HTTP 协议,索性分成了两篇文章。

本文对于 HTTP 协议的描述仅仅是一小部分,如果想要深入的了解,推荐阅读 MDN 的文档。

参考链接: