4.HTTP协议详解有关问题
4.1 HTTP请求
一个HTTP请求报文由请求行、请求头部、空行和请求数据四个部分组成。
1.请求行
请求行中有请求方法字段、URL字段和HTTP协议版本3个字段组成。例如 GET /index.html HTTP/1.1.
1.1 请求行-请求方法字段
(1)GET请求: 最常见的一种请求方法,当点击网页上的链接或者通过在游览器的地址栏输入网址来游览网页。
使用GET方法时,请求参数和对应的值会被附加在URL后面,并利用一个问号("?")代表URL的结尾和请求参数的开始。例如:/index.html?id=1&key=1&value=1。这种传递方式不适合传送私密数据,且不同游览器对地址的字符限制有所不同,一般最多只能1024个字符。
GET /search?hl=zh-CN&source=hp&q=domety&aq=f&oq= HTTP/1.1
(2)POST请求: POST请求方式会将请求参数封装到HTTP请求数据当中,以key/value的形式出现,可以传输大量数据,且POST方式对传送的数据大小没有限制。
hl=zh-CN&source=hp&q=domety
面试题:GET请求和POST请求的区别是什么呢?
从HTTP层面上来看:
- GET请求把参数放到URL里面,即HTTP中的请求行当中;而POST请求把参数放到HTTP中的请求数据当中。且GET数据对数据有所限制最多为2048个字节,即1024个字符,目的是为了保证服务器和游览器能够正常运行,防止有人恶意发送请求;POST请求则没有大小限制;(放置位置以及大小方面)
- GET请求会产生一个TCP数据包,而POST会产生两个TCP数据包;对于GET请求,游览器会把http header和data一起发送出去,服务器响应200;对于POST请求,游览器先发送header,服务器响应100 continue,游览器在发送data,服务器响应200ok。(请求)
- GET限制FORM表单的数据集的值必须要ASCII字符;而post支持整个ISO10646字符集,没有限制;
从数据库层面:GET符合幂等性;POST不符合。幂等性指的是同一个请求执行多次和执行一次的效果上完全等同;即GET在游览器回退时是无害的,而POST会再次提交请求;
从游览器缓存角度: GET请求会被游览器主动缓存下来,留下历史记录;而POST默认不会缓存;
了解下XMLHttpRequest:提供了客户端和服务器之间传输数据的功能,不会整个页面刷新,XMLHttpRequest在ajax中被大量使用,在使用xmlhttprequest的post方法时,游览器会先发送header再发送data。
(3) HEAD
HEAD请求通常用来判断资源是否存在。HEAD就像GET,只不过服务器端接收到的HEAD请求后只返回响应头,而不会发送响应内容。
当我们只需要查看某个页面的状态的时候,使用HEAD是非常高效的,因为在传输的过程中省去了页面内容。
(4)PUT 更新资源
(5)DELETE 删除资源
1.2 请求行-请求URL字段
1.3 请求行-HTTP协议版本字段
通常是用HTTP1.1 协议,而HTTP1.0和HTTP1.1最大的区别就是keep-alive.即短连接和长连接。keep-alive不会永久保持连接,它由一个保持时间。HTTP协议的长连接和短连接,实质上就是TCP协议的长连接和短连接。(多个http请求会复用这样一个tcp的连接)
TCP长连接,指的是TCP连接之后可以传送多个请求和响应,避免了频繁建立和销毁连接,降低了消耗和延迟。
TCP短连接:client向server发起连接请求,server接到请求,双方建立连接。 client向server发送消息,server回应client,然后依次读写就完成了,这时候双方任何一个都可以发起close操作,不过一般都是client先发起close操作。
TCP长连接:client向server发起连接,server接收client连接,双方建立连接。client与server完成一次读写之后,它们之间的连接并不会主动关闭,后续的读写操作也会继续使用这个连接。而保持长连接,就需要用到TCP的保活功能。 TCP保活功能:主要为服务器应用提供,服务器应用希望知道客户主机是否崩溃,从而代表客户是否使用资源。
扩展:
长轮询和短轮询:
短轮询:客户端每隔一段时间就主动向服务器请求数据,服务器被动推送消息。
长轮询:客户端发出请求,服务器端不会立即返回数据,服务器会阻塞该请求,直到服务器端有数据更新 或者连接超时才返回。之后客户端再重新发送请求。
SSE(Server-Sent Events): HTTP协议无法做到服务器主动推送消息,但有一个变通方法,就是服务器向客户端声明,接下来要发送的是流信息。例如:当前网站在线的实时人数,实时汇率,成交额;
Websocket:websocket是全双工通信,sse是单双工通信;websocket协议是跟http协议等价的一个协议,都是应用层协议,都是基于TCP协议。websocket是一种长连接的模式,一旦websocket连接后,除非client或者server有一端主动断开连接,否则每次数据传输之前都不需要HTTP那样请求数据。
2 请求行-消息报头
请求头部由关键字/值对组成,每行一对,关键字和值用英文冒号“:”分隔。请求头部通知服务器有关客户端请求的信息,
a.通用的首部字段(请求报文和相应报文都会使用的首部字段)
Date:创建报文的时间
Connection:连接的管理
Cache-Controll:缓存的控制
Transfer-Encoding:报文主体的传输编码格式
b.请求首部字段:(请求报文会使用的首部字段)
HOST:请求资源所在的服务器
ACCEPT:可处理的媒体类型
Accept-Charset:接收到的字符集;
Accept-Encoding:可接受到的内容编码;
Accept-Language:可接受的自然语言
c.实体首部字段:(请求报文与响应报文的实体部分使用到的首部字段)
- Allow:资源可支持的HTTP方法
- Content-Type:实体主类的类型
- Content-Encoding:实体主体适用的编码方式
- Content-Language:实体主体的自然语言
- Content-Length:实体主体的的字节数
- Content-Range:实体主体的位置范围,一般用于发出部分请求时使用
典型的请求头有:
User-Agent:产生请求的游览器类型
Host:请求的主机名,允许多个域名同处一个IP地址,即虚拟主机;
cookie:游览器告诉服务器带来的cookie是什么?
Context-Type:文本类型
Context-length:文本长度
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-excel, application/vnd.ms-powerpoint,
application/msword, application/x-silverlight, application/x-shockwave-flash, */*
Referer: <a href="http://www.google.cn/">http://www.google.cn/</a>
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; TheWorld)
Host: <a href="http://www.google.cn">www.google.cn</a>
Connection: Keep-Alive
Cookie: PREF=ID=80a06da87be9ae3c:U=f7167333e2c3b714:NW=1:TM=1261551909:LM=1261551917:S=ybYcq2wpfefs4V9g;
NID=31=ojj8d-IygaEtSxLgaJmqSjVhCspkviJrB6omjamNrSm8lZhKy_yMfO2M4QMRKcH1g0iQv9u-2hfBW7bUFwVh7pGaRUb0RnHcJU37y-
FxlRugatx63JLv7CWMD6UB_O_r
cookie和session的区别是什么?
HTTP是一种无状态的协议,保存信息就是通过cookie和session来记录信息。
cookie是服务器发送给客户端的特殊信息,以文本的形式存放在客户端;客户端再次请求时,会把cookie回发;服务器接收到后会解析cookie生成与客户端相应的应用;
session是服务器端的机制,在服务器上保存的信息;
cookie数据存放在客户端的游览器,session存放在服务器上;session相对于cookie安全,若考虑减轻服务器负担,应用使用cookie。cookie一般用来保存用户信息,自动登录; session用来记录用户的状态,购物车。
什么是分布式session呢?
HTTP协议的特点是无状态性的,通常我们使用游览器上网时,游览器和服务器都是通过HTTP协议交互的。当我们请求页面时,这一个请求和上一个请求没有任何关系,这就是无状态性。无状态性使得交互更加快速,但会带来一个问题,比如我们在商品详情页面登录后,等到了订单页面,仍然需要登录,这是我们无法忍受的。但是这是两个不同的页面,也就是两个不同的HTTP请求,HTTP请求是无状态的,页面间无法关联。
为了解决这个问题,cookie就出现了,它弥补了HTTP协议无状态的不足, 即少量信息存储在客户端,访问不同页面时,可用从客户端读取相关信息并添加到请求中发送给客户端,从而解决页面间无法关联的问题。cookie这种解决方案快速方便,但不安全,因为存储在客户端中,用户是可见的,可随意修改。
为了弥补cookie的不足,新的存储会话机制session出现,session将信息存储在服务端,解决了cookie的不安全问题。
但如果只有一台tomcat时,不会出现上面问题。但如果是A,B两台服务器呢?请求1先访问到了A,A中存储了1的信息,当请求1访问B时,B中并没有1的信息。如何让A,B共享同一session呢?
常用的解决方案就是:将session信息存放到第三方,客户端和服务端在通信时,去第三方存取session信息。redis常作为第三方,主要是由于:
- redis读写速度快, redis是直接操作内存数据的。而之前的cookie和session都是以文件形式存储;
- redis更好地设置过期时间。文件形式保存信息,有时并不能及时删除,占用磁盘空间;
- 更好的分布式同步。设置redis主从同步,可快速同步session到各台web服务器;
3 请求行-请求正文
请求数据不再GET方法中使用,而是在POST方法中使用。与请求数据相关的最常使用的请求头的是Context-Type和Context-length,是在请求头部中的,即游览器想知道编码格式就是response.setContextType();
4.2 HTTP响应
HTTP响应由三个部分组成,分别是:响应行、消息报头和响应正文。
1.响应行
HTTP响应中唯一区别在于第一行用响应行代替了请求头。
响应行的格式如下:
HTTP-Version Status-Code Reason-Phrase CRLF
其中,HTTP-Version表示服务器HTTP协议的版本;Status-Code表示服务器发回的响应状态代码;Reason-Phrase表示状态码的文本表示。
- 1xx:指示信息-表示请求已接受,继续处理;
- 2xx:成功-表示请求已被成功接受、理解;
- 3xx:重定向-要完成请求必须进行更进一步的操作;
- 4xx:客户端错误-请求有语法错误或请求无法实现;
- 5xx:服务器端错误-服务器未能实现合法的请求。
常见的状态代码、状态描述的说明如下:
- 200OK: 客户端请求成功;
- 400 Bad Request:客户端请求有语法错误;
- 401 Unauthorized:请求未经授权;
- 404 Not Found:请求资源不存在;
- 500Internal Server Error:服务器发生不可预期的错误;
面试题目:说一下HTTP协议中的302状态?
重定向,HTTP响应中302表示重定向;
Location字段-utl:这种情况下,服务器返回的头部信息中会包含一个location字段,内容是重定向的url。
那么重定向和转发的区别是什么?
重定向是由游览器端进行的页面跳转。请求->响应 请求 响应
转发是服务器端进行页面跳转的,请求->服务器端forword转发->响应
- 请求次数上:重定向是至少请求两次,转发请求一次;
- 地址栏是否不同:重定向发生变化,转发不会;
- 是否共享数据:重定向两次请求不共享数据,转发一次请求共享数据;
- 跳转限制:重定向可以跳转到任意的url,转发只能跳转本站点资源;
- 发生行为不同:重定向是客户端行为,转发是服务器端行为;
2.消息报头
响应首部字段(响应报文会使用的首部字段)
Acceptt-Ranges:可接受的字节返回
Server:HTTP服务器的安装信息;
Location:令客户端重新定位到的URL;
3.响应正文
4.3 HTTP题目
1.HTTP协议中的请求报头由什么构成?
- 请求头:请求方法 url http版本信息
- 请求首部字段:
- 请求内容实体:
2.HTTP协议中的响应报头由什么构成?
- 状态行:包含HTTP版本、状态码、状态码的原因短语
- 响应首部字段;
- 响应内容实体;
3.说一下网络的传输过程?
应用层(http数据)->传输层(tcp)->网络层(IP首部)->链路层(以太网首部)
4.HTTP协议中的http1.0和1.1的区别是什么?
在http1.0中,当建立连接后,客户端发送一个请求,服务器端返回一个消息就关闭连接。当游览器下次请求时候又要建立连接,显然这种不断建立连接的方式,会造成很多问题;
在http1.1中,引入了长连接的概念,通过这种连接,游览器可以建立一个连接,发送请求并得到返回消息,然后继续发送请求再次等到返回消息。也就是说客户端可以持续发送多个请求,而不用等待每一个响应的内容。
5.常见HTTP协议状态?(高频)
200 ok:成功
302:重定向,表示服务器要求游览器重新再发一个请求;
304:当用户第一次请求index.html时,服务器会给其添加一个last-modified响应头,这个头说明了index.html最后修改时间。游览器会把index.html和最后响应时间缓存下来。当用户第二次请求时,会加入一个if-modified-since请求头,会告诉服务端我游览器缓存的 index.html最后修改时间是这个,你看一下对么,如果相同服务器会发响应码304,表示index.html与游览器上次缓存的相同,无需再次发送。如果不同说明index.html已经做了修改,服务器会响应200。
403:请求的对应资源禁止被访问
404:服务器无法找到对应的资源;
500:服务器内部出现错误;
6.HTTP与HTTPS优缺点?
http通信使用明文传输不加密,内容可能被窃听,也就是被抓包分析;此外可能被伪装,不验证通信方身份,可能遭到伪装。
7.HTTP协议的特征有哪些?
- 支持客户/服务器模式;
- 简单快速、灵活
- 无连接
- 无状态
8.HTTP优化
HTTP Cache。所谓缓存就是将已经请求得到的内容放在一个就近的仓库存放起来,下次请求可以不再向服务器请求,而是直接从这个缓存仓库获取;
- 减少网络延迟,加快了页面响应速度;
- 减少了网络带宽的消耗;
- 减少了服务器压力;
8.HTTP的短连接和长连接
当游览器访问一个包含多张图片的HTML页面时,除了请求访问的HTML页面资源,还会请求图片资源。如果每进行一次HTTP通信就要新建一个TCP连接,那么开销就很大。
长连接是只需要建立一次TCP连接就能进行多次HTTP通信。HTTP/1.1默认是长连接Connection:Keep-Alive。HTTP/1.0默认是短连接。
9.代理
代理服务器分为正向代理和反向代理两种:
正向代理的话:用户察觉得到正向代理的存在;
反向代理的话:位于内部网络,用户察觉不到。
10.HTTPS的加密
对称密钥加密:客户端和服务器端 加密和解密都使用同一 密钥;
非对称加密:加密和解密使用不同的秘钥
https:是先使用非对称秘钥,获取到了之后,产生公钥加密解密
但需要保证首次传输的私钥的,所以有了CA进行通信认证。
问题:1.怎么获取这个公钥?2.大家都有公钥、私钥、无法确认公钥到底是谁的?
问题一、公钥如何获取?
(1)提供一个下载公钥的地址,回话前让客户端去下载(缺点:下载地址有可能是假的;客户端每次在回话前都要先去下载公钥也很麻烦);
(2)回话开始前,服务器把公钥发给客户端(缺点:黑客冒充服务器,发送给客户端假的公钥);
那有木有一种方式既可以安全的获取公钥,又能防止黑客冒充呢?那就需要用到终极武器了:SSL证书(申购)。
举例说明:客户端在接受到服务端发来的SSL证书时,会对这个证书的真伪进行校验,以游览器为例:
- 首先游览器读取证书中的证书所有者、有效期等信息进行一一校验;
- 游览器开始查找操作系统中已内置的受信任的证书发布机构CA,与服务器发来的证书中的颁布者CA对比,用于校验证书是否为合法机构颁发的;
- 如果找不到,游览器就会报错,说明服务器发来的证书是不可信任的;
- 如果找到,那么游览器就会从操作系统中取出颁发者CA的公钥,然后对服务器发来的证书里面的签名进行解密;
- 游览器使用相同的hash算法计算出服务器发来的证书的hash值,将这个计算的hash值与证书中签名做对比;
- 对比结果一致,则证明服务器发来的证书合法,没有被冒充;
- 此时游览器就可以读取证书中的公钥,用于后续加密了;
通过发送SSL证书的形式,即解决了公钥获取问题,又解决了黑客冒充问题,HTTPS加密过程也就此形成。
问题二、证书生成过程
公司ABC 自己生成一对公钥、私钥以及对应的加密算法,还有公司相关信息,提供给证书颁发机构CA。CA根据提供的信息生成证书(或者自己生成,这里不是重点)
一个证书中含有三个部分:“证书内容,散列算法,加密密文”,证书内容会被散列算法hash计算出hash值,然后使用CA机构提供的私钥进行RSA加密。
当客户端发起请求时,服务器将该数字证书发送给客户端,客户端通过CA机构提供的公钥对加密密文进行解密获得散列值(数字签名),同时将证书内容使用相同的散列算法进行Hash得到另一个散列值,比对两个散列值,如果两者相等则说明证书没问题。
11.HTTP1.0和HTTP2.0的区别是什么
(1) 二进制分帧层
HTTP/2.0将报文分为HEADERS帧和DATA帧,它们都是二进制格式的。
在通信过程中,只会有一个TCP连接存在,它承载了任意数量的双向数据流(Stream)。
- 一个数据流(Stream)都有一个唯一标识符和可选的优先级信息,用于承载双向信息;
- 消息(Message)是与逻辑请求或响应对应的完整的一系列帧;
- 帧(Frame)是最小的通信单位,来自不同数据流的帧可以交错发送,然后再根据每个帧头的数据流标识符重新组装。
(2) 服务器端推送
HTTP/2.0在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。例如客户端请求page.html页面,服务端就把script.js和style.css等与之相关的资源一起发给客户端。
(3) 首部压缩
HTTP/1.0的首部带有大量信息,而且每次都要重复发送。
HTTP/2.0要求客户端和服务器同时维护和更新一个包含之前见过的首部字段表,从而避免了重复传输。
不仅如此,HTTP/2.0也是用Huffman编码对首部字段进行压缩。
4.4 HTTP文件传输
HTTP协议用于文件传输时,一般把文件内容放到消息体中。作为TCP之上的流式传输协议,发送端和接收端可以对大文件进行流式的发送和接收。
(1)确定大小的文件传输
消息头部的content-length字段表示文件的长度,用于接收端确定文件的传输。
(2)chunked编码
当文件大小无法事先确定时,无法设置Content-Length字段。此时可以用分块传输的方式,将文件分成多个部分进行发送。在分块发送下,头部增加Transfer-Encoding:chunked,存在这个头部时不允许再加上content-length头,即使有也会 被忽略。
Chunked模式下,消息体分块发送,每一块头部存储数据长度,跟上CRLF,然后是具体的实现,块与块之间也是CRLF分隔。当长度头尾0时,表示块的结束。
(3) 使用multipart/form-data上传文件
原始的POST请求消息体中是URL编码后的表单,格式为key=value,不同的key、value之间用&分隔。上传二进制的文件时,可以用multipart/form-data的方式。
在这种方式下,基础的请求仍然是POST请求,文件内容放在消息体,只是Content-Type字段的值为multipart/form-data,并随机选择一个字符串作为分隔符(理论上需要这个分隔符不在文件内容中出现,一般随机选择的字符串出现在正文中的概率非常小,如果真的出现会导致POST失败,需要另外发起一次请求重新选择随机字符串),然后,每个字段之间用”–分隔符”进行分隔,最后一个”–分隔符–”表示结束。每个字段中都可以包含头部和消息体,头部的内容可以包含文件名称、文件路径等,也可以是文件的二进制内容本身。
(4)断点续传与多线程传输
这里仍然是运用分块传输的思想:如果传输中途中断,接下来可以从中断的地方重新开始避免从头开始的浪费;在多线程程序中,各个线程可以分别负责传输一个文件块,然后将他们合并恢复成为原始文件。
分块传输就需要确定块的边界,这里采用的是Range字段,表示从某一字节开始,如Range:bytes=100-,表示请求的是从文件的100字节开始到文件末尾,返回消息为206 Partial Content,头部字段增加Content-Range: bytes100-199/200,表示返回文件100-199字节内容,文件一共200字节。
多线程传输时,每个线程请求文件中不同的range,传输完成后由应用作合并。
(5) RandomAccessFile大文件多线程读写
package com.lcz.thread;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
public class Test17 {
/**
* 测试利用多线程进行文件的写操作
*/
public static void main(String[] args) throws Exception {
// 预分配文件所占的磁盘空间,磁盘中会创建一个指定大小的文件
RandomAccessFile raf = new RandomAccessFile("D://abc.txt", "rw");
raf.setLength(1024*1024); // 预分配 1M 的文件空间
raf.close();
// 所要写入的文件内容
String s1 = "第一个字符串";
String s2 = "第二个字符串";
String s3 = "第三个字符串";
String s4 = "第四个字符串";
String s5 = "第五个字符串";
// 利用多线程同时写入一个文件
// 利用多线程同时写入一个文件
new FileWriteThread(1024*1,s1.getBytes()).start(); // 从文件的1024字节之后开始写入数据
new FileWriteThread(1024*2,s2.getBytes()).start(); // 从文件的2048字节之后开始写入数据
new FileWriteThread(1024*3,s3.getBytes()).start(); // 从文件的3072字节之后开始写入数据
new FileWriteThread(1024*4,s4.getBytes()).start(); // 从文件的4096字节之后开始写入数据
new FileWriteThread(1024*5,s5.getBytes()).start(); // 从文件的5120字节之后开始写入数据
}
// 利用线程在文件的指定位置写入指定数据
static class FileWriteThread extends Thread{
private int skip;
private byte[] content;
public FileWriteThread(int skip,byte[] content){
this.skip = skip;
this.content = content;
}
public void run(){
RandomAccessFile raf = null;
try {
raf = new RandomAccessFile("D://abc.txt", "rw");
raf.seek(skip);
raf.write(content);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
try {
raf.close();
} catch (Exception e) {
}
}
}
}
}
4.5 HTTP流式传输
业务场景:将大文件通过HTTP协议传输到服务端。无法一次加载到内存中,组装到Request的body中。针对这样的问题,应该怎么解决呢?最简单的就是将大文件分成小文件上传,HTTP流式传输就提供了相应的解决方案。
(1)KeepAlive模式
HTTP是无状态无连接的;
HTTP协议采用“请求-应答”模式,当使用普通模式时,即非KeepAlive模式时,每个请求/应答客户和服务器都要新建一个连接,完成之后立即断开连接;
后来的web的世界越来越精彩,一个网页中可能嵌套了多种资源,比如图片、视频,为了解决频繁建立TCP连接带来的性能损耗,提出了Keep-Alive模式。当使用Keep-Alive时,Keep-Alive功能使客户端到服务器端的连接持续有效,当出现对服务器的后继请求时,Keep-Alive功能避免了建立或者重新建立连接。
TCP的底层实现中包含一个KeepAlive定时器,当一条数据流中没有数据通过 后,服务器端每隔一段时间会向客户端发送一个不带数据的ACK请求,如果收到Client回复,表明连接依然存在。如果没有收到恢复,Server会多次ACK,达到一定次数以后还没有收到回复,默认此连接关闭。
解决了不必重复频繁建立连接的问题,第二个问题随之而来,怎么判断数据流的结束?
在请求-响应模式下,每一次HTTP请求发送结束后,Client都会主动关闭连接,Server端在读取完所有的Body数据后,就认为此次请求已经结束完毕了,开始在服务端进行处理。但是在Keep-Alive模式下,这个问题显然就没有这么简单了。举个例子,比如一条Keep-Alive的HTTP连接,通过连接底层的TCP通道连续发送了两张图片,对于服务器来说,如何判断这是两张图片?而不是把它们当做同一个文件里面的数据进行处理呢?
(2)判断数据流结束的方法
HTTP为我们提供了两种方式:
- Content-Length
这是一个很直观的方式,在要传输的数据前增加一个信息,来告知对端要传输多少数据,这样在另一侧读取到这个长度的数据后,可以认为接受已经完成了。
如果无法提前预知Content-Length呢?比如数据源还在不断的生成当中,不知道什么时候会结束呢?
- 使用消息字段Header字段,Transfer-Encoding:chunk
如果要一边产生数据,一边发给客户端,服务器就需要使用“transfer-encoding:chunked”这样的方式
chunk编码将数据分成一块一块的。chunked编码将使用若干个chunk串连而成,由一个标明长度为0的chunk标识结束。每个chunk分为头部和正文两部分头部内容指定正文的字符数量和数量单位,正文部分就是指定长度的实际内容,两部分之间用回车换行(CRLF)隔开。在最后一个长度为-的chunk中的内容是尾巴。