HTTP:传输大文件理论篇

引入

  • HTTP 可以传输很多种类的数据,不仅是文本,也能传输图片、音频和视频。
  • 在传输大文件时,100M 的光纤固网或者 4G 移动网络在大文件的压力下都将变成“小水管,无论是上传还是下载,都会把网络传输链路路挤的“满满当当”
  • 所以,如何在有限的带宽下高速快捷的传输大文件就成了一个重要的问题。这就好比是已经打开了冰箱门(建立连
    接),该怎么把大象(文件)塞进去再关上门(完成传输)呢?

下面就来看看HTTP协议里有哪些手段能解决这个问题

方案

数据压缩

最基本的解决方案,就是数据压缩,把大象变成小猪佩奇,再放进冰箱

  • 通常浏览器在发送请求时都会带着Accept+Encoding头字段,里面是浏览器支持的压缩格式列表,比如gzip、deflate、br 等,这样服务器就可以从中选择一种压缩算法,放进Content+Encoding响应头里,再把原数据压缩后发给浏览器
  • 如果压缩率有50%,也就是说100K的数据能够压缩成50K的大小,那么就相当于在带宽不变的情况下网速提升了一倍。
  • 不过这个解决方案也有个缺点,gzip等压缩算法通常只对文本文件由较好的压缩率,而图片、音频视频等多媒体数据本身就已经是高速压缩的,再用gzip处理也不会变小,甚至还有可能会增大一点,所以它就失效了

gzip 的压缩率通常能够超过 60%,而 br 算法是专门会 HTML 设计的,压缩效率和性能比 gzip 还要好,能够再提高 20% 的压缩密度。

分块传输

在数据压缩之外,还有什么办法来解决大文件的问题呢?

  • 压缩是把大文件整体变小,我们可以反过来思考,如果大文件整体不能变小,那就把它“拆开”,分解成多个小块,把这些小块分批发给浏览器,浏览器收到后再组装复原
  • 这样浏览器和服务器都不用在内存里保存文件的全部,每次只收发一小部分,网络也不会被大文件长时间占用,内存、带宽等资源也就节省下来了

这种“化整为零”的思路在HTTP协议中就是chunked分块传输编码,在响应报文里用字段Transfer-Encoding:chunked来表示,意思是报文里的body部分不是一次性发过来的,而是分成了很多的块(chunk)逐个发送

分块传输页可以用于“流式数据”,比如由数据库动态生成的表单页面,这种情况下body数据的长度是未知的,无法再头字段Content-Length里给出确切的长度,所以也只能用chunked分块的方式发送

Transfer-Encoding: chunked“Content-Length这两个字段是互斥的,也就是说响应报文里这两个字段不能同时出现,一个响应报文的传输要么是长度已知,要么是长度未知(chunked)

为什么分块就意味着长度未知

  • 举个例子,从github上下载源代码,github要实时压缩实时发送,而不是一下子压缩好再发送,这样body的长度一开始就是未知的
  • 所以就要用分块传输,压缩一部分,就发一部分,这部分的长度是已知的,但总长度只有压缩完才知道
  • chunked编码用在“流式”收发数据的时候,通常数据是即时生成的,也就是动态数据。

下面我们来看一下分块传输的编码规则,其实也很简单,同样采用了明文的方式,很类似响应头。

  • 每个分块包含两个部分,长度头和数据块;
  • 长度头是以 CRLF(回车换行,即\r\n)结尾的一行明文,用 16 进制数字表示长度;
  • 数据块紧跟在长度头后,最后也用 CRLF 结尾,但数据不包含 CRLF;
  • 最后用一个长度为 0 的块表示结束,即“0\r\n\r\n”

HTTP:传输大文件理论篇_第1张图片
实验环境里的 URI“/16-1”简单地模拟了分块传输,可以用 Chrome 访问这个地址看一下效果:
HTTP:传输大文件理论篇_第2张图片
不过浏览器在收到分块传输的数据后会自动按照规则去掉分块编码,重新组装出内容,所以想要看到服务器发出的原始报文形态就得用 Telnet 手工发送请求(或者用 Wireshark抓包):
在这里插入图片描述
因为 Telnet 只是收到响应报文就完事了,不会解析分块数据,所以可以很清楚地看到响应报文里的 chunked 数据格式:先是一行 16 进制长度,然后是数据,然后再是 16 进制长度和数据,如此重复,最后是 0 长度分块结束
HTTP:传输大文件理论篇_第3张图片

范围请求

有了分块传输编码,服务器就可以轻松的收发大文件了,但对于上G的超大文件,还有一些问题需要考虑。

比如,你在看当下正热播的某穿越剧,想跳过片头,直接看正片,或者有段剧情很无聊,想拖动进度条快进几分钟,这实际上是像获取一个大文件其中的片段数据,而分块传输并没有这个能力。

  • HTTP协议为了满足这样的需求,提出了范围请求(range requests)的概念,允许客户端在请求头里使用专用字段来表示只获取文件的一部分
  • 范围请求不是web服务器必备的功能,可以实现也可以不实现,所以服务器必须在响应头里使用字段Accept-Ranges:bytes明确告知客户端:“我是支持范围请求的”
  • 如果不支持的话该怎么办呢?服务器可以发送Accept-Ranges:none,或者干脆不发送Accept-Ranges字段,这可以客户端就会认为服务器没有实现范围请求的功能,只能收发整块文件了

请求头Range是HTTP范围请求的专用字段,格式是bytes=x-y,其中的x和y是以字节为单位的数据范围。

Range的格式也很灵活,起点x和终点y都可以省略,能够很方面的表示正数或者倒数的范围。假设文件是100个字节,那么:

  • “0-”表示从文档起点到文档终点,相当于“0-99”,即整个文件;
  • “10-”是从第 10 个字节开始到文档末尾,相当于“10-99”;
  • “-1”是文档的最后一个字节,相当于“99-99”;
  • “-10”是从文档末尾倒数 10 个字节,相当于“90-99”。

服务器收到Range字段后,需要做四件事:

  • 第一,它必须检查范围是否合法。比如文件只有100个字节,但请求“200-300”,这就是范围越界了;于是服务器就返回状态码416,意思是“你的范围请求有误,我无法处理,请再检查一下”
  • 第二,如果范围正确,服务器就可以根据Range头计算偏移量,读取文件的片段了,返回状态码206 Partial Content,和200的意思差不多,但表示body只是原数据的一部分
  • 第三,服务器要添加一个响应头字段Content-Range,告诉片段的实际偏移量和资源的总大小,格式是bytes x-y/length,与 Range 头区别在没有“=”,范围后多了总长度。例如,对于“0-10”的范围请求,值就是bytes 0-10/100
  • 最后剩下的就是发送数据了,直接把片段用TCP发给客户端,一个范围请求就算是处理完了。

例如下面的这个请求使用 Range 字段获取了文件的前 32 个字节:

GET /16-2 HTTP/1.1
Host: www.chrono.com
Range: bytes=0-31

得到响应结果:
HTTP:传输大文件理论篇_第4张图片

有了范围请求之后,HTTP 处理大文件就更加轻松了,看视频时可以根据时间点计算出文件的 Range,不用下载整个文件,直接精确获取片段所在的数据内容。

不仅看视频的拖拽进度需要范围请求,常用的下载工具里的多段下载、断点续传也是基于它实现的,要点是:

  • 客户端发个HEAD,看服务器是否支持范围请求,同时获取文件的大小
  • 客户端开N个线程,每个线程使用Range字段划分出各自负责下载的片段,然后请求传输数据
  • 下载意外中断也不怕,不必重头再来一遍,只要根据上次的下载记录,用Range请求剩下的那一部分就可以了

多段数据

刚才说的范围请求一次只获取一个片段,其实它还支持在Range 头里使用多个“x-y”,一次性获取多个片段数据。

这种情况需要使用一种特殊的 MIME 类型:multipart/byteranges,表示报文的 body 是由多段字节序列组成的,并且还要用一个参数boundary=xxx来区分不同的片段

  • 每一个分段必须以- -boundary开始(前面加两个“-”),
  • 之后要用Content-TypeContentRange标记这段数据的类型和所在范围
  • 然后就像普通的响应头一样以回车换行结束
  • 再后面是分段数据
  • 最后用一个- -boundary- -(前后各有两个-)表示所有的分段结束。
    HTTP:传输大文件理论篇_第5张图片
GET /16-2 HTTP/1.1
Host: www.chrono.com
Range: bytes=0-9, 20-29

响应结果如下:
HTTP:传输大文件理论篇_第6张图片

小结

  • 压缩HTML等文本文件时传输大文件最基本的方法
  • 分块传输可以流式收发数据,节约内存和带宽,使用响应头字段Transfer-Encoding: chunked来表示,分块的格式是 16 进制长度头 + 数据块;
  • 范围请求可以只获取部分数据,即“分块请求”,实现视频拖拽或者断点续传,使用请求头字段Range和响应头字段Content-Range,响应状态码必须是 206;
  • 也可以一次请求多个范围,这时候响应报文的数据类型是“multipart/byteranges”,body 里的多个部分会用boundary 字符串分隔。

要注意这四种方法不是互斥的,而是可以混合起来使用,例如压缩后再分块传输,或者分段后再分块

问题

分块传输数据的时候,如果数据里含有回车换行(\r\n)是否会影响分块的处理呢?

不影响,因为分块前有数据长度说明

如果对一个被 gzip 的文件执行范围请求,比如“Range:bytes=10-19”,那么这个范围是应用于原文件还是压缩后的文件呢?

不用想,肯定是原文件。

  • 想象一下我们看视频拖动进度条,如果是应用于压缩后的文件,那么就会造成拖拽范围和响应范围不一致。
  • 再比如,有一个1M的纯文件,range请求其中的500K,然后服务器编码为gzip,压缩成200K,浏览器收到后解压缩,就得到了这部分500K数据

http交给TCP进行传输的时候本来就会分块,那么http分块还有什么意义呢?

在http层是看不到tcp的,它不知道下层协议是否会分块,下层是否分块对它来说没有意义,不关心

在http里一个报文必须是完整交付,在处理大文件的时候就很不方便,所以就要分块,在http层面方便处理

chunked主要是在http的层次来解决问题







⽂件上传原理

原理

http请求格式

文件上传是根据HTTP协议的规范和定义,完成请求消息体的封装和消息体的解析,然后将二进制内容保存到文件。

在上传⼀个⽂件时,需要把 form 标签的enctype设置为multipart/form-data,同时method必须为post⽅法。那么multipart/form-data表示什么呢?
HTTP:传输大文件理论篇_第7张图片

  • 请求头:Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryDCntfiXcSkPhS4PN 表示本次请求要上传文件,其中boundary表示分隔符,如果要上传多个表单项,就要使用boundary分割,每个表单项由———XXX 开始,以———XXX 结尾。
  • 消息体- Form Data 部分
    • 每⼀个表单项⼜由Content-TypeContent-Disposition组成。
    • Content-Disposition: form-data 为固定值,表示⼀个表单元素,name 表示表单元素的 名称,回⻋换⾏后⾯就是name的值,如果是上传⽂件就是⽂件的⼆进制内容。
    • Content-Type:表示当前的内容的 MIME 类型,是图⽚还是⽂本还是⼆进制数据。

服务器解析

客户端发送请求到服务器后,服务器会收到请求的消息体,然后对消息体进⾏解析,解析出哪是普通表单哪些是附件。

HTTP:传输大文件理论篇_第8张图片

⽂件上传类型分析

秒传

什么是秒传

通俗的说,你把要上传的东西上传,服务器会先做MD5校验,如果服务器上有一样的东西,它就直接给你个新地址,

逻辑

  • 利用redis的set方法存放文件上传状态,其中key为文件上传的md5,value为是否上传完成的标志位
  • 当标志位true为上传已经完成,此时如果有相同⽂件上传,则进⼊秒传逻辑。如果标志位为false,则说明还没上传完成,此时需要在调⽤set的⽅法,保存块号⽂件记录的路径,其中key为上传⽂件md5加⼀个固定前缀,value为块号⽂件记录路径

分片上传

什么是分⽚上传

分片上传,就是将所要上传的文件,按照一定的大小,将整个文件分割成多个数据块(part)来进行上传、上传完之后再由服务端对所有上传的文件进行汇总整合成原始的文件

分片上传的场景

  • 大文件上传
  • 网络环境不好,存在需要重传风险的场景

大文件上传

⼤⽂件上传⼀般采⽤分⽚上传的⽅式,这样可以提⾼⽂件上传的速度,前端拿到⽂件流后进⾏分⽚,然后与后端进⾏通讯传输,⼀般还会结合断点继传,这时后端⼀般提供三个接⼝:

  • 第⼀个接⼝获取已经上传的分⽚信息
  • 第⼆个接⼝将前端分⽚⽂件进⾏传输
  • 第三个接⼝是将所有分⽚上传完成后告诉后端进⾏⽂件合并

HTTP:传输大文件理论篇_第9张图片

断点续传

什么是断点续传

断点续传是在下载或者上传的时候,将下载或上传任务(⼀个⽂件或⼀个压缩包)⼈为的划分为⼏个部分,每⼀个部分采⽤⼀个线程进⾏上传或下载,如果碰到⽹络故障,可以从已经上传或下载的部分开始继续上传或者下载未完成的部分,⽽没有必要从头开始上传或者下载。

应⽤场景

断点续传可以看成是分⽚上传的⼀个衍⽣,因此可以使⽤分⽚上传的场景,都可以使⽤断点续传。

实现断点续传的核⼼逻辑

在分片上传的过程中,如果因为系统崩溃或者网络中断等异常因素导致上传中断,这时候客户端需要记录上传的进度。在之后支持再次上传时,可以继续从上传上传中断的地方进行继续上传。

为了避免客户端在上传之后的进度数据被删除而导致重新开始从头上传的问题,服务端可以提供相应的接口便于客户端对已经上传的分片数据进行查询,从而使得客户端知道已经上传的分片数据,从而从下一个分片数据开始继续上传

实现流程步骤

(1)方案一,常规步骤

  • 将需要上传的文件按照一定的分割规则,分割成相同大小的数据块
  • 初始化一个分片上传任务,返回本次分片上传唯一标识
  • 按照一定的策略(串行或并行)发送各个分片数据的数据块
  • 发送完成后,服务端根据判断数据上传是否完整,如果完整,则进行数据快合成得到原始文件

(2)⽅案⼆、实现的步骤

  • 客户端需要根据固定⼤⼩对⽂件进⾏分⽚,请求服务端时要带上分⽚序号和⼤⼩
  • 服务端创建conf⽂件⽤来记录分块位置,conf⽂件⻓度为总分⽚数,每上传⼀个分块即向conf⽂件中写⼊⼀个127,那么没上传的位置就是默认的0,已上传的就是Byte.MAX_VALUE 127
  • 服务器按照请求数据中给的分⽚序号和每⽚分块⼤⼩(分⽚⼤⼩是固定且⼀样的)算出开始位置,与读取到的⽂件⽚段数据,写⼊⽂件。

⽂件下载原理

⽂件下载⽐⽂件上传容易的多。
对于HTTP协议,向服务器请求某个⽂件时,只要发送类似如下的请求即可:

GET /Path/FileName HTTP/1.0
Host: www.baidu.com:80
Accept: */*
User-Agent: GeneralDownloadApplication
Connection: close
  • 第⼀⾏中的GET是HTTP协议⽀持的⽅法之⼀
  • 除第⼀⾏以外,其余⾏都是HTTP头的字段部分
    • Host字段表示主机名和端⼝号,如果端⼝号是默认的80则可以不写。
    • Accept字段中的*/*表示接收任何类型的数据
    • User-Agent表示⽤户代理,这个字段可有可⽆,但强烈建议加上,因为它是服务器统计、追踪以及识别客户端的依据。
    • Connection字段中的close表示使⽤⾮持久连接。

如果服务器成功收到该请求,并且没有出现任何错误,则会返回类似下⾯的数据:

HTTP/1.0 200 OK
Content-Length: 13057672
Content-Type: application/octet-stream
Last-Modified: Wed, 10 Oct 2005 00:56:34 GMT
Accept-Ranges: bytes
ETag: "2f38a6cac7cec51:160c"
Server: Microsoft-IIS/6.0
X-Powered-By: ASP.NET
Date: Wed, 16 Nov 2005 01:57:54 GMT
Connection: close
  • 第⼀⾏是协议名称及版本号等
  • 第⼆⾏Content-Length字段是⼀个⽐较重要的字段,它标明了服务器返回数据的⻓度,这个⻓度是不包含HTTP头⻓度的。换句话说,我们的请求中并没有Range字段,表示我们请求的是整个⽂件,所以Content-Length就是整个⽂件的⼤⼩。其余各字段是⼀些关于⽂件和服务器的属性信息。
  • 这段返回数据同样是以最后⼀⾏的结束标志(回⻋换⾏)和⼀个额外的回⻋换⾏作为结束,即“\r\n\r\n”。⽽“\r\n\r\n”后⾯紧接的就是⽂件的内容了,这样我们就可以找到“\r\n\r\n”,并从它后⾯的第⼀个字节开始,源源不断的读取,再写到⽂件中了。

以上就是通过HTTP协议实现⽂件下载的全过程。但还不能实现断点续传,⽽实际上断点续传的实现⾮常简单,只要在请求中加⼀个Range字段就可以了。

假如⼀个⽂件有1000个字节,那么其范围就是0-999,则:

  • Range: bytes=500- 表示读取该⽂件的500-999字节,共500字节
  • Range: bytes=500-599 表示读取该⽂件的500-599字节,共100字节。

如果HTTP请求中包含Range字段,那么服务器会返回206(Partial Content),同时HTTP头中也会有⼀个相应的Content-Range字段,类似下⾯的格式:

Content-Range: bytes 500-999/1000

你可能感兴趣的:(计算机理论与基础,http,服务器,网络协议)