第9章 HTTP原理与Web服务器实战

前言

高性能的IM(即时通信)应用还需要高性能的Web应用先配合。高并发、大流量的Web应用,QPS在十万每秒甚至上千万每秒,如何使用高并发HTTP通信技术去提升内部各个节点的通信性能对于提升分布式系统整体的吞吐量有着非常重大的作用。

本章介绍一个小的HTTP服务器程序——HTTP Echo回显服务器。如果能够顺利掌握此程序,可以进入下一个阶段实战练习:疯狂创客圈的spring-boot-netty-server开源项目实战。该项目的功能是在Spring Boot、Spring Cloud应用中使用Netty来替换Tomcat、Jetty、Undertow等传统的Web容器,通过该项目可以练习比较复杂Netty服务端编程,该项目的地址为传送门

高性能Web应用架构

十万级并发的Web应用架构

QPS在十万每秒的Web应用,其架构大致如图所示。
在这里插入图片描述
十万级QPS的Web应用架构主要包括客户端层、接入层、服务层,重点是接入层和服务层。

首先看服务层,在Spring Cloud微服务技术流程之前,服务层主要是通过Tomcat集群部署的向外提供服务的独立Java应用;在微服务技术成为主流之后,服务层主要是微服务Provider实例,并通过内部网关(如Zuul)向外提供统一的访问服务。

其次看接入层,接入层可以理解为客户端层与服务层之间的一个反向代理层,利用高性能的Nginx来做反向代理:
(1)Nginx将客户端请求分发给上游的多个Web服务;Nginx向外暴露一个外网IP,Nginx和内部Web服务(如Tomcat、Zuul)之间使用内网访问。
(2)Nginx需要保障负载均衡,并且通过Lua脚本可以具备动态伸缩、动态增加Web服务节点的能力。
(3)Nginx需要保障系统的高可用(High Availability),任何一台Web服务节点挂了,Nginx都可以将流量迁移到其他Web服务节点上。

Nginx的原理与Netty很像,也是应用了Reactor模式。Nginx执行过程中主要包括一个Master进程和n(n≥1)个Worker进程,所有的进程都是单线程(只有一个主线程)的。Nginx使用了多路复用和事件通知。其中,Master进程用于接收来自外界的信号,并给Worker进程发送信号,同时监控Worker进程的工作状态。Worker进程则是外部请求真正的处理者,每个Worker请求相互独立且平等的竞争来自客户端的请求。

因为Nginx应用了Reactor模式,所以在处理高并发请求时内存消耗非常小。在30000并发连接下,开启的10个Nginx进程才消耗150(15×10=150)MB内存。

有关Nginx的原理知识和具体的使用配置,请参考笔者的另一本书《Spring Cloud、Nginx高并发核心编程》。

与Nginx类似、同样比较有名的Web服务器为Apache HTTP Server(纯Java实现)。该服务器在处理并发连接时会为每个连接建立一个单独的进程或线程,并且在网络输入/输出操作时阻塞。该阻塞式的IO将导致内存和CPU被大量消耗,因为新起一个单独的进程或线程需要准备新的运行时环境,包括堆内存和栈内存的分配,以及新的执行上下文,这些操作也会导致多余的CPU开销。最终会由于过多的上下文切换而导致服务器性能变差。因此,接入层的反向代理服务器原则上需要使用高性能的Nginx而不是Apache HTTP Server。

尽管单体的Nginx比较稳定,在长时间运行的情况下,还是存在有可能崩溃的情况。如何保障接入层的Nginx高可用呢?可以使用Nginx + KeepAlived组合模式,具体如下:
(1)使用两台(或以上)Nginx组成一个集群,分别部署上KeepAlived,设置成相同的虚IP供下游访问,从而保证Nginx的高可用。
(2)当一台Nginx挂了,KeepAlived能够探测到,并会将流量自动迁移到另一台Nginx上,整个过程对下游调用方透明。
如果流量不断增长,两台Nginx的集群模式不够,就可以使用LVS + KeepAlived组合模式实现Nginx的可扩展,并且在架构上进行升级,具体请看千万级流量的Web应用架构。

千万级高并发的Web应用架构

QPS在百万级甚至千万级的Web应用架构大致如图所示。
在这里插入图片描述

QPS在百万级甚至千万级的Web应用架构主要包括客户端层、负载均衡层、接入层、服务层,重点是客户端层和负载均衡层。

在客户端层,需要在DNS服务器上使用负载均衡的机制。DNS负载均衡的技术很简单,属于运维层面的技术,具体来说就是在DNS服务器中配置多个A记录,如表所示。

通过在DNS服务器中配置多个A记录的方式可以在一个域名下面添加多个IP,由DNS域名服务器进行多个IP之间的负载均衡,甚至DNS服务器可以按照就近原则为用户返回最近的服务器IP地址。

DNS负载均衡虽然简单高效,但是也有不少缺点,具体如下:
(1)通常无法动态调整主机地址权重(也有支持权重配置的DNS服务器),如果多台主机性能差异较大,则不能很好地均衡负载。
(2)DNS服务器通常会缓存查询响应,以便更迅速地向用户提供查询服务。在某台主机宕机的情况下,即使第一时间移除服务器IP也无济于事。

由于DNS负载均衡无法满足高可用性要求,因此通常仅仅被用于客户端层的简单复杂均衡。为了应对百万级、千万级高并发流量,需要在客户端与接入层之间引入一个专门的负载均衡层,该层通过LVS + KeepAlived组合模式达到高可用和负载均衡的目的。负载均衡层中的LVS(Linux Virtual Server,Linux虚拟服务器)是一个虚拟的服务器集群系统,该项目在1998年5月由章文嵩博士成立,是国内最早出现的自由软件项目之一。

QPS在千万级的Web应用的高可用负载均衡层使用LVS + KeepAlived组合模式实现,具体的方案如下:
(1)使用两台(或以上)LVS组成一个集群,分别部署上KeepAlived,设置成相同的虚IP(VIP)供下游访问。KeepAlived对LVS负载调度器实现健康监控、热备切换,具体来说,对服务器池中的各个节点进行健康检查,自动移除失效节点,恢复后再重新加入,从而保证LVS高可用。
(2)在LVS系统上,可以配置多个接入层Nginx服务器集群,由LVS完成高速的请求分发和接入层的负载均衡。
LVS常常使用直接路由方式(DR)进行负载均衡,数据在分发过程中不修改IP地址,只修改MAC地址,由于实际处理请求的真实物理IP地址和数据请求目的IP地址一致,因此响应数据包可以不需要通过LVS负载均衡服务器进行地址转换,而是直接返回给用户浏览器,避免LVS负载均衡服务器网卡带宽成为瓶颈。此种方式又称作三角传输模式,具体如图所示。
在这里插入图片描述
使用三角传输模式的链路层负载均衡是目前大型网站使用最广泛的一种负载均衡手段。目前,LVS是Linux平台上最好的三角传输模式软件负载均衡开源产品。当然,除了软件产品之外,还可以使用性能更好的专用硬件产品(如F5),但是其动辄几十万的昂贵价格并不是所有Web服务提供商所能承受的。

LVS目前已经是Linux标准内核的一部分,从Linux 2.4内核以后,无须专门给内核打任何补丁,可以直接使用LVS提供的各种功能。

术业有专攻,LVS、KeepAlived的具体配置和运维更多的属于运维人员的工作,对于开发人员来说只要清楚其工作原理即可。
总之,如何抵抗十万级甚至千万级QPS访问洪峰,涉及大量的开发知识、运维知识,对于开发人员来说,并不一定需要掌握太多的操作系统层面(如LVS)的运维知识,主要原因是企业一般都会有专业的运维人员去解决系统的运行问题,对千万级QPS系统中所涉及的高并发方面的开发知识则是必须掌握的。

在十万级甚至千万级QPS的Web应用架构过程中,如何提高平台内部的接入层Nginx到服务层Tomcat(或者其他Java容器)之间的HTTP通信能力涉及高并发HTTP通信以及TCP、HTTP等基础的知识。接下来,本书从HTTP应用层协议开始为大家解读这些作为Java核心工程师、架构师所必备的基础知识。

详解HTTP应用层协议

HTTP(Hyper Text Transfer Protocol,超文本传输协议),是一个基于请求与响应、无状态的应用层的协议,是互联网上应用最为广泛的一种网络协议,所有的WWW文件都必须遵守这个标准。
应用层协议有很多,比如HTTP、FTP、TELNET等,也可以自己定义应用层协议。

HTTP简介

HTTP的主要特点可概括如下:
(1)支持客户端/服务器模式。
(2)简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET、HEAD、POST,且每种方法规定了客户与服务器联系的类型不同。HTTP简单,使得HTTP服务器的程序规模较小,因此通信速度很快。
(3)灵活:HTTP允许传输任意类型的数据对象,数据的类型由Content-Type加以标记。
(4)无连接:每次连接只处理一个请求,服务器处理完客户的请求并收到客户的应答后即断开连接。
(5)无状态:协议对于事务处理没有记忆能力。如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另外,在服务器不需要先前信息时它的应答较快。

总之,HTTP是请求-响应模式的协议,客户端发送一个HTTP请求,服务就响应此请求,大致如图所示。
在这里插入图片描述

HTTP的请求URL

通过HTTP(或者HTTPS)协议请求的资源由统一资源标示符(Uniform Resource Identifier,URI)来标识。在Java编程中,更多的是URI的一个子类——URL(URL是一种特殊类型的URI,包含了用于查找某个资源的足够信息),其格式如下:

http://host[":"port][abs_path]

HTTP的请求报文

HTTP请求报文由3部分组成(请求行+请求头+请求体):
(1)Request Line(请求行),包含请求方法、URL地址、协议名称和版本号。
(2)Request Header(请求头),包含若干头部的字段。如有必要,客户程序还可以选择发送的请求头。大多数请求头并不是必需的,但Content-Length除外。对于POST请求来说,Content-Length必须出现。常见的请求头字段含义如下:
① Accept:客户端可接受的MIME类型。
② Accept-Charset:客户端可接受的字符集。
③ Accept-Encoding:客户端能够进行解码的数据编码方式,比如gzip。Servlet能够向支持gzip的客户端返回经gzip编码的HTML页面,许多情况下可以减少5~10倍的下载时间。
④ Accept-Language:客户端所希望的语言种类,当服务器能够提供一种以上的语言版本时要用到。
⑤ Authorization:用于设置用户身份信息,如果使用Authorization的方式进行认证,那么每次都要将认证的身份信息(如令牌)放到Authorization头部。
⑥ Content-Length:表示请求消息正文的长度。
⑦ Host:客户端通过这个头部信息告诉服务器想访问的主机名。Host头字段指定请求资源的主机和端口号,必须表示请求URL的原始服务器或网关的位置。HTTP/1.1请求必须包含主机头字段,否则系统会以400状态码返回。
⑧ If-Modified-Since:客户端通过这个头部信息告诉服务器资源的缓存时间。只有当所请求的内容在指定的时间后又经过修改才返回,否则返回304“Not Modified”应答。
⑨ Referer:客户端通过这个头部字段告诉服务器它是从哪个资源来访问服务器的(防盗链)。Referer包含一个URL,表示用户从该URL代表的页面出发访问当前请求的页面。
⑩ User-Agent:包含发出请求的用户信息。
⑪ Cookie:客户端通过这个头部信息往服务器发数据,这是最重要的请求头信息之一。
⑫ Pragma:值为“no-cache”,表示服务器必须返回一个刷新后的文档,如果服务器是代理服务器而且已经有了页面的本地缓存副本,则需要进行本地缓存副本的刷新。
⑬ From:值为请求发送者的email地址,由一些特殊的Web客户程序使用,HTTP客户端不会用到。
⑭ Connection:请求完成后是断开连接还是继续保持连接。如果值为“Keep-Alive”或者客户端使用的是HTTP 1.1(HTTP 1.1默认进行持久连接),它就可以利用持久连接的优点,当页面包含多个元素时(例如Applet,图片),会显著减少下载所需要的时间。当然,持久连接需要服务端进行配合,服务端需要在应答中发送一个ContentLength头,发送出响应内容的大小。
⑮ Range:用于请求URL资源的部分内容,单位是byte(字节),并且从0开始。如果请求头携带了Range信息,就表示客户端需要进行分批下载或者分段传输。如果服务端支持分批下载,那么服务器会返回状态码206(Partial Content)以及该部分内容。如果服务器不支持分批下载,那么服务器会返回整个资源的大小以及状态码200。
⑯ UA-Pixels、UA-Color、UA-OS、UA-CPU:由某些版本的IE浏览器所发送的非标准的请求头,表示屏幕大小、颜色深度、操作系统和CPU类型。
(3)Request Body(请求体),以文本或者其他形式组织的请求数据。若方法字段是GET,则请求体为空时表示没有请求体数据;若请求方法字段是POST,则通常来说此处放置的是要提交的数据。比如要使用POST方法提交一个表单,表单中user字段的数据为admin,password字段的数据为123456,那么这里的请求数据就是“user=admin&password=123456”,HTTP协议会使用“&”符号连接各个字段。
对HTTP请求报文进一步细分,分为以下6个部分:
(1)HTTP Method(请求方法)。HTTP/1.1定义的请求方法有8种,即GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS、TRACE,其中最常用的两种是GET和POST。如果是RESTful接口,一般会用到GET、POST、DELETE和PUT。
(2)HTTP报文的请求URL地址。它和报文头的Host属性组成完整的请求URL。URL可以传递请求参数,其方式类似于“param1=value1&param2=value2”键-值对的字符串形式。
(3)协议名称及版本号。
(4)HTTP报文的请求头。请求头包含若干个头部字段,每个字段的格式为“头部字段名:头部字段值”,服务端据此获取客户端的很多重要信息(如令牌)等。
(5)空行。它的作用是通过一个空行告诉服务器请求头到此为止。
(6)HTTP报文的请求体:可以将一个页面表单中的组件值通过“param1=value1&param2 =value2”键-值对的形式编码成一个格式化串,从而用于承载多个请求参数。

总的来说,HTTP请求报文格式就如图所示:
在这里插入图片描述

HTTP的响应报文

客户端向HTTP服务端发送请求之后,如果服务器能够正常处理并进行响应,就会向客户端发送HTTP响应。
在这里插入图片描述
(1)HTTP响应行:一般由协议版本、状态码及其描述组成,比如“HTTP/1.1 200 OK”。其中,协议版本为HTTP/1.1或者HTTP/1.0,200是状态码,OK为描述。
(2)响应头:用于描述服务器的基本信息和数据描述,服务器通过这些数据的描述信息可以通知客户端如何处理等一会它回送的数据。
设置HTTP响应头时往往和状态码结合起来。例如,有好几个表示“文档位置已经改变”的状态代码都伴随着一个Location头,而401状态代码则必须伴随一个“WWW-authenticate”头部来表示未授权(Unauthorized)。响应头可以用来完成设置Cookie、指定修改日期、指示浏览器按照指定的间隔刷新页面、声明文档的长度以便利用持久HTTP连接等许多其他任务。

常见的响应头字段大致如下:
① Allow:服务器支持哪些请求方法(如GET、POST等)。
② Content-Encoding:文档的编码(Encode)类型,如gzip压缩格式。客户端只有在解码之后才可以得到Content-Type头指定的内容类型。由于服务端返回gzip压缩文档能够显著地减少HTML文档的下载时间,因此服务端应该通过查看Accept-Encoding请求头检查客户端是否支持gzip,为支持gzip的客户端返回经gzip压缩的HTML页面,而为不支持gzip的其他客户端返回普通页面。
③ Content-Length:表示内容长度,只有当客户端使用持久HTTP连接时才需要这个数据。
④ Content-Type:表示后面的文档属于什么MIME类型。Servlet程序默认为text/plain,但通常需要显式地指定为text/html。由于经常要设置Content-Type,Servlet程序可以通过调用HttpServletResponse提供了一个专用的setContentType()方法去完成。
⑤ Date:当前的GMT时间,例如“Date:Mon,31Dec200104:25:57GMT”。Date描述的时间表示世界标准时,换算成本地时间,需要知道用户所在的时区。可以用setDateHeader来设置Date,以避免转换时间格式的麻烦。
⑥ Expires:告诉客户端把回送的资源缓存多长时间,-1或0表示不缓存。
⑦ Last-Modified:文档的最后改动时间。和客户端请求头配合使用,客户可以通过请求头If-Modified-Since提供一个起始时间,该请求头将被视为一个条件GET,只有改动时间迟于指定起始时间的文档才会返回,否则返回一个304(Not Modified)状态。
⑧ Location:配合302状态码使用,用于重定向接收者到一个新URI地址,表示客户应当到哪里去提取重定向文档。
⑨ Refresh:告诉客户端隔多久刷新一次,以秒计。
⑩ Server:服务器通过这个头告诉客户端服务器的类型。Server响应头包含处理请求的原始服务器的软件信息。
⑪ Set-Cookie:设置和页面关联的Cookie。
⑫ Transfer-Encoding:告诉客户端数据的传送格式。
⑬ WWW-Authenticate:告诉客户端应该在Authorization请求头中提供什么类型的授权信息。如果响应状态码为401(Unauthorized),则应答中这个头是必需的。
(3)响应体:响应的消息体,可以是文本内容或者二进制内容,比如JSON、HTML等都属于纯文本内容。

HTTP中GET和POST的区别

  1. 二者请求数据的放置位置不同:对于GET请求,请求的数据将会附在URL之后。对于POST请求,提交的数据将被放置在HTTP请求报文的请求体中。
  2. 二者所能传输数据的大小不同:对于GET请求,特定浏览器和服务器对URL长度有限制,例如IE对URL长度的限制是2083字节。对于POST请求,不是通过URL传值的,理论上数据不受限。实际上各个Web服务器会通过自定义设置对POST提交数据大小进行限制,Tomcat、Apache、IIS6都有各自的配置。
  3. 二者传输数据的安全性不同:POST的安全性要比GET的安全性高。通过GET提交数据,用户名和密码将明文出现在URL上,通过查看浏览器的历史记录,就可以拿到其他用户的账号和密码了。

HTTP的演进

HTTP是如今互联网的基石,其演进从侧面反映了互联网技术的快速发展。
在这里插入图片描述

HTTP的1.0版本

第一个版本的HTTP是HTTP 0.9,组成极其简单,只允许客户端发送GET这一种请求,且不支持请求头。由于没有协议头,因此HTTP 0.9
协议只支持一种内容,即纯文本。不过网页仍然支持用HTML语言格式化,同时无法插入图片。

HTTP的第二个版本为1.0版本,也是第一个在通信中指定版本号的HTTP版本,至今仍被广泛采用。相对于HTTP 0.9版本,HTTP 1.0版本增加了如下主要特性:
(1)请求与响应支持头部字段。
(2)响应对象以一个响应状态行开始。
(3)响应对象不只限于超文本。
(4)开始支持客户端通过POST方法向Web服务器提交数据,支持GET、HEAD、POST方法。
(5)支持长连接,但默认使用短连接,缓存机制,以及身份认证。
(6)请求行必须在尾部添加协议版本字段(HTTP 1.0),并且必须包含头部消息。

HTTP 1.0版本支持的请求方式为GET、POST和HEAD;请求访问的资源不再局限于上一个版本的HTML格式,可以根据Content-Type设置访问的格式;同时也开始支持Cache,当客户端在规定时间内访问同一URL资源时,直接访问Cache即可。
与HTTP 0.9版本相比,HTTP 1.0版本请求和回应的格式也变了。

除了数据部分,每次通信都必须包括响应头信息(HTTP header),用来描述一些元数据。
HTTP 1.0版本使用Content-Type字段来表示客户端请求服务端的数据是什么格式,或者说客户端使用Content-Type来表示具体请求中的媒体类型信息,服务端使用Content-Type来表示具体的响应体中的媒体类型信息。媒体类型(MediaType)的全称为互联网媒体类型(Internet Media Type),也叫作MIME(多用途互联网邮件扩展)类型。

HTTP的1.1版本

HTTP的第三个版本是HTTP 1.1,是目前使用最广泛的协议版本,也是目前主流的HTTP版本。

HTTP 1.1版本引入了许多关键技术进行传输性能的优化,主要包括持久连接(Persistent Connection)、管道机制(Pipelining)、分块传输编码(Chunked Transfer Encoding,CTE)、字节范围(Range)请求等。
HTTP 1.1版本的最大变化就是引入了持久连接(Persistent Connection),即下层的TCP连接默认不关闭,可以被多个请求复用,而且报文不用声明“Connection: keep-alive”头部值。在HTTP 1.1版本中,默认情况下一个TCP连接可以允许多个HTTP请求。

TCP连接如何关闭呢?客户端和服务器都可以进行通信监测,如果发现对方在一段时间没有活动,就可以主动关闭TCP连接。不过,相对规范的做法是,客户端在最后一个请求时发送带“Connection:close”请求头的HTTP报文,明确要求服务器关闭TCP连接。

Connection: close

HTTP 1.1版本加入了管道机制,在同一个TCP连接里允许多个请求同时发送,增加了并发性,进一步改善了HTTP协议的效率。举例来说,客户端需要请求两个资源:以前的做法是,在同一个TCP连接里面先发送A请求,然后等待服务器做出回应,收到后再发出B请求;管道机制则是允许浏览器同时发出A请求和B请求,但是服务器还是按照顺序先回应A请求,完成后再回应B请求。
在这里插入图片描述

HTTP 1.1版本支持传送内容的一部分,也就是“字节范围请求”。当客户端已经拥有请求资源的一部分后,只需与服务器请求另外的部分资源即可。“字节范围请求”是支持文件断点续传的基础。

具体来说,“字节范围请求”是通过Range头部实现的,HTTP 1.0版本每次传送文件都是只能从文件头开始,即0字节处开始。在HTTP1.1版本中,客户端通过“Range:bytes=XX”的请求头部值表示要求服务器从文件的“XX”字节处开始传送,也就是断点续传。其对应的部分内容的响应码不是200,而是使用专门的响应码206(Partial Content)。

HTTP 1.1版本支持分块(Chunked)传输编码。分块传输编码(Chunked Transfer Encoding,CTE)是一种新数据传输机制,允许服务端将数据分成多个部分发送到客户端。普通的服务端响应会将响应数据的长度通过Content-Length字段告诉客户端。

HTTP的2.0版本

HTTP的2.0版本(或者说HTTP/2.0协议)是一个二进制协议。二进制更易于Frame(帧、数据包)的传输。HTTP 1.x版本在应用层以纯文本的形式进行通信,HTTP 2.0将所有的传输信息分割为更小的消息和数据帧,并对它们采用二进制格式编码。这样,客户端和服务端都需要引入新的二进制编码和解码的机制,就像本书前面编写的Protobuf聊天数据帧的编码器和解码器一样。

HTTP/2.0协议引入了新的通信单位:帧、消息、流。分帧有什么好处?服务器单位时间接收到的请求数变多,可以提高并发数。最重要的是,为多路复用提供了底层支持。HTTP/2.0协议之所以叫HTTP/2.0版本而不是HTTP/1.2版本,关键在于新增的二进制分帧传输在传输的方式上发生了重大变化。

HTTP/2协议的主要特点有首部压缩、多路复用、并行双向传输、服务端推送等。

有数据表明,全球排名1000万个网站只有12%左右支持HTTP/2.0协议。目前所有新版本的浏览器包括Firefox、Safari、Chrome以及其他
基于Blink核心的浏览器已完全支持HTTP/2.0协议。虽然目前HTTP/1.1协议还是主流,但是相信不久的将来HTTP/2.0协议会大行其道。

基于Netty实现简单的Web服务器

Netty天生是异步事件驱动的架构,无论是在性能上还是在可靠性上都表现优异,非常适合作为Web服务器使用,相比于传统的Tomcat、
Jetty等Web容器,基于Netty的Web服务器具有更加轻量和小巧、灵活性和定制性更好的特点。

先介绍一下本节实现的演示服务器示例——HttpEchoServer,这是一个简单基于Netty的HTTP回显服务器。
HttpEchoServer的功能是:当通过HTTP客户端(如Postman工具、浏览器等)向演示服务器发起HTTP请求时,服务器会回显该HTTP请求的请求方法、请求参数、请求URI、请求头、请求体等内容。

基于Netty的HTTP服务器演示实例

基于Netty的HTTP回显服务器的处理器流水线:
在这里插入图片描述

基于Netty的HTTP请求的处理流程

通常HTTP协议通信过程中,客户端和服务端的交互过程如下:
(1)客户端(如Postman工具、浏览器、Java程序等)向服务端发送HTTP请求。
(2)服务端对HTTP请求进行解析。
(3)服务端向客户端发送HTTP响应报文。
(4)客户端解析HTTP响应的应用层协议内容。

在以上交互过程中,服务端将涉及HTTP请求的解码处理和HTTP响应的编码处理。不过,Netty已经内置了这些解码和编码的处理器,大致如下:
(1)HttpRequestDecoder:HTTP请求编码器,是一个入站处理器,间接地继承了ByteToMessageDecoder,将ByteBuf缓冲区解码成代
表请求的HttpRequest首部实例和HttpContent内容实例,并且HttpRequestDecoder在解码时会处理好分块(Chunked)类型和固定长度(Content-Length)类型的HTTP请求报文。
(2)HttpResponseEncoder:HTTP响应编码器,把代表响应的HttpResponse首部实例和HttpContent内容实例编码成ByteBuf字节流,是一个出站处理器。
(3)HttpServerCodec:HTTP的编解码器是HttpRequestDecoder解码器和HttpResponseEncoder编码器的结合体。
(4)HttpObjectAggregator:是HttpObject实例聚合器,也是一个入站处理器。通过HttpObject实例聚合器,可以把HttpMessage首部实例和一个或多个HttpContent内容实例最终聚合成一个FullHttpRequest实例。上文中涉及的与HTTP相关的HttpMessage、HttpRequest、HttpContent、FullHttpRequest等类型都是HttpObject的子类。
(5)QueryStringDecoder:把HTTP的请求URI分割成Path路径和Key-Value参数键-值对,同一次请求,该解码器仅能使用一次。

基于Netty的HTTP请求的处理流程大致如下:
(1)二进制的HTTP数据包从Channel通道入站后,首先进入Pipeline流水线的是ByteBuf字节流。
(2)HttpRequestDecoder首先将ByteBuf缓冲区中的请求行(Request Line)和请求头Header解析成HttpRequest首部对象,传入到HttpObjectAggregator。然后将HTTP数据包的请求体Body解析出HttpContent对象(可能是多个),传入到HttpObjectAggregator聚合器。解码完成之后,如果没有更多的请求体内容,HttpRequestDecoder会传递一个LastHttpContent结束实例到聚合器HttpObjectAggregator,表示HTTP请求数据已经解析完成。
(3)当HttpObjectAggregator发现有入站包为LastHttpContent实例入站时,代表HTTP请求数据协议解析完成,此时会将所收到的全部HttpObject实例封装成一个FullHttpRequest整体请求实例发送给下一站,这里的下一站基本上为业务处理器。

Netty的HTTP请求的处理流程大致如图所示。
在这里插入图片描述
在请求体Request Body处理过程中会涉及Content-Length和Trunked两种类型请求体,但是其处理差异被HttpRequestDecoder协议解码器所屏蔽,它们的最终出站对象是一致的,通过聚合器HttpObjectAggregator处理之后,输出的都是FullHttpRequest实例。HTTP服务端的业务处理器(如EchoHandler)可以通过该FullHttpRequest实例获取到所有与HTTP请求的内容。

总体来说,如果要进行HTTP请求报文的读取,只需要在Netty的流水线上配置好两个内置处理器HttpRequestDecoder和HttpObjectAggregator即可。
以本节的HttpEchoServer演示实例的服务端处理器为例,大致的流水线装配代码如下:

ChannelPipeline pipeline = ch.pipeline();
//请求的解码器
pipeline.addLast(new HttpRequestDecoder());
//请求聚合器
pipeline.addLast(new HttpObjectAggregator(65535));
//响应的编码器
pipeline.addLast(new HttpResponseEncoder());
//自定义的业务Handler
pipeline.addLast(new HttpEchoHandler());

Netty内置的HTTP报文解码流程

通过内置处理器HttpRequestDecoder和HttpObjectAggregator对HTTP请求报文进行解码之后,Netty会将HTTP请求封装成一个FullHttpRequest实例,然后发送给下一站。
在这里插入图片描述
Netty内置的与HTTP请求报文相对应的类大致有如下几个:
(1)FullHttpRequest:包含整个HTTP请求的信息,包含对HttpRequest首部和HttpContent请求体的组合。
(2)HttpRequest:请求首部,主要包含对HTTP请求行和请求头的组合。
(3)HttpContent:对HTT请求体进行封装,本质上就是一个ByteBuf缓冲区实例。如果ByteBuf的长度是固定的,则请求体过大,可能包含多个HttpContent。解码的时候,最后一个解码返回对象为LastHttpContent(空的HttpContent),表示对请求体的解码已经结束。
(4)HttpMethod:主要是对HTTP请求方法的封装。
(5)HttpVersion:对HTTP版本的封装,该类定义了HTTP/1.0和HTTP/1.1两个协议版本。
(6)HttpHeaders:包含对HTTP报文请求头的封装及相关操作。

以上清单中的类与HTTP请求报文各部分的对应关系大致如下图所示。
在这里插入图片描述
Netty的HttpRequest首部类中有一个String uri成员,主要是对请求URI的封装,该成员包含了HTTP请求的Path路径与跟随在其后的请求参数。

接下来介绍本节的重点:Netty的HTTP报文拆包方案。
一般来说,服务端收到的HTTP字节流可能被分成多个ByteBuf包。Netty服务端如何处理HTTP报文的分包问题呢?大致有如下几种策略:
(1)定长分包策略:接收端按照固定长度进行数据包分割,发送端按照固定长度发送数据包。
(2)长度域分包策略:比如使用LengthFieldBasedFrameDecoder长度域解码器在接收端分包,而在发送端先发送4个字节表示消息的长度,紧接着发送消息的内容。
(3)分隔符分割:比如使用LineBasedFrameDecoder解码器通过换行符进行分包,或者使用DelimiterBasedFrameDecoder通过特定的分隔符进行分包。

Netty结合使用上面第(2)种和第(3)种策略完成HTTP报文的拆包:对于请求头,应用了分隔符分包策略,以特定分隔符(“\r\n”)进行拆包;对于HTTP请求体,应用长度字段中的分包策略,按照请求头中的内容长度进行内容拆包。

Netty总体的HTTP拆包方案具体如下:
(1)处理HTTP请求行,由于请求行的边界是CRLF(“\r\n”),如果读取到CRLF,则意味着请求行的信息已经读取完成。
(2)开始处理请求头部分,由于Header的边界是CRLF,每遇到一个CRLF,则表示一个请求头读取完成;如果连续读取到两个CRLF,则
意味着全部Header的信息读取完成。
(3)请求体的长度一般由请求头Content-Length来进行确定。如果请求头中没有Content-Length头部,则属于“块编码”报文,具体的解析方式请参考Trunked协议。

为了减少内存复制,Netty使用了CompositeByteBuf。例如,Netty聚合各个HttpObject实例的FullHttpMessage实现类,内部就是一个CompositeByteBuf实例,该组合缓冲区会将HttpRequest内部的更多IT书籍请关注:www.cmsblogs.cnByteBuf、HttpContent内部的ByteBuf都组合在一起,作为最终的HTTP报文缓冲区,从而避免数据拷贝(也就是内存复制),具体如图所示:
在这里插入图片描述

基于Netty的HTTP响应编码流程

Netty的HTTP响应的处理流程只需在流水线装配HttpResponseEncoder编码器即可。该编码器是一个出站处理器,有以下特点:
(1)该编码器输入的是FullHttpResponse响应实例,输出的是ByteBuf字节缓冲器。后面的处理器会将ByteBuf数据写入Channel,最终被发送到HTTP客户端。
(2)该编码器按照HTTP对入站FullHttpResponse实例的请求行、请求头、请求体进行序列化,通过请求头去判断是否含有Content-Length头或者Trunked头,然后将请求体按照相应的长度规则对内容进行序列化。

Netty的HTTP响应的编码流程具体如图所示。
在这里插入图片描述

HttpEchoHandler回显业务处理器的实战案例

基于Netty的HttpEchoHandler回显业务处理器,将来自客户端的HTTP客户端的请求方法、URI请求参数、请求体数据、请求头字段回显到客户端(写回到客户端)。回显业务处理器主要对GET请求、Form表单POST请求、JSON类型的POST请求进行处理,所涉及的可以回显处理的客户端请求大致如图所示。
在这里插入图片描述
HttpEchoHandler.java

运行HttpEchoServer的main()方法,正式启动HTTP回显服务。同时,为了抓取和查看报文,可以开启Fiddler抓包程序,在一切都准备妥当之后,可以在Postman中输入一个带参数的URI,去访问回显服务器。服务端所返回的回显结果大致如下图所示。

使用Postman发送多种类型的请求体

接下来的演示通过Postman发送多种类型的POST请求体。按照Content-Type进行划分,POST请求体有很多种编码类型,以下为常见的几种:
(1)text/plain:请求体以普通文本形式编码,其中不含任何控件或格式字符。
(2)application/json:请求体以JSON格式编码。
(3)application/x-www-form-urlencoded:请求体被编码为“名称=值”对相连的形式,这是标准的表单项编码格式。
(4)multipart/form-data:请求体被编码为多部分,每个表单数据对应到消息中的一部分。
(5)application/octet-stream:从字面意思得知,请求体只可以发送二进制数据,通常用来上传文件。因为没有键的名称,所以该类型请求体一次只能上传一个文件。

实验1: 发送application/x-www-form-urlencoded编码类型的请求体

首先演示和介绍application/x-www-form-urlencoded类型的请求体编码形式。该类型的请求体会将表单的每个表单项名称和值转换为“名称=值”的形式,然后用“&”符号连在一起,最终将整个表单编码后的字符串作为POST请求的请求体发送出去。如果是GET请求,则将编码后的字符串追加到URI后面发送出去。在Postman提交该类型的POST请求到回显服务器,具体的请求URL如下:

http://crazydemo.com:18899/postrequest

服务端返回的回显结果大致如图所示。
在这里插入图片描述
通过Fiddler查看到以上POST请求的应用层HTTP协议数据包,具体如图所示。
在这里插入图片描述

实验2:发送multipart/form-data编码类型的请求体

这里介绍一下multipart/form-data类型的请求体编码形式。在Postman提交该类型的POST请求到回显服务器,具体如图所示。
在这里插入图片描述
浏览器对于multipart/form-data类型的POST请求的报文编码稍微有点复杂。它在将表单项编码成请求体时,会将每一个表单项分开进行编码。每个表单项都有一个Content-disposition来说明表单项的类型,表单Field字段的类型值为form-data(数据)、File字段的类型值为file(文件)。紧跟在Content-disposition属性的后面,每个表单项都有一个name属性,其值为表单项的名称。在name名字之后是两个“\r\n”,然后是表单项的值,如果是上传文件,则此处为文件的内容;每个表单项的末尾都有一段boundary分隔字符串,隔开自己和下一个表单项。
在这里插入图片描述
编码之后的Form表单项被同一个boundary分隔符分开,boundary分隔符的值则被包含在请求的Content-Type请求头的后半部分,处于“multipart/form-data”的后面,具体如图所示。
在这里插入图片描述

实验3:发送text/plain、application/json等编码类型的请求体

除了上面介绍的请求体的两种表单编码类型,使用Postman还可以提交Content-Type为text/plain、application/json等类型的POST请求体。在这种情况下,需要在其操作界面的Body类型选项中选择原始请求体类型,然后进一步选择Text、JSON或其他细分类型的原始内容类型,具体如图所示。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值