zl程序教程

您现在的位置是:首页 >  云平台

当前栏目

[网络]应用层协议:HTTP / HTTPS[转载]

网络协议HTTPHTTPS 转载 应用层
2023-09-27 14:24:41 时间

1 HTTP / HTTPS 概述

1.1 什么是HTTP协议?

摘自百度百科:

超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。
所有的WWW文件都必须遵守这个标准。
目的是保证浏览器与服务器之间的通信、为了提供一种发布和接收HTML页面的方法。
HTTP的工作方式是客户端与服务器之间的请求-应答协议。

  • HTTP协议 是一个在计算机世界里专门在两点之间传输文字、图片、音频、视频等超文本数据的约定和规范
  • HTTP属于OSI网络七层协议模型中的"最上层":应用层协议。由请求和响应构成,是一个标准的客户端服务器模型。HTTP是一个无状态的协议 ( HTTP无状态协议,是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快 ) 。
  • HTTP默认端口号为80。它也可以承载在TLS和SSL之上,通过加密、认证的方式实现数据传输的安全,称为HTTPS,HTTPS默认端口号为443。
  • 早期HTTP用于传输网页HTML文件,发展到现在,应用变得广泛,客户端软件(PC,Android,iOS等)大部分通过HTTP传输数据。

1.2 从浏览器输入地址URL到呈现页面中间发生了什么事情(通信过程)?

1.2.1 简述

  • 浏览器(客户端)进行地址解析。
  • 将解析出的域名进行dns解析。
  • 通过ip寻址和arp,找到目标(服务器)地址。
  • 进行tcp三次握手,建立tcp连接。
  • 浏览器发送数据,等待服务器响应。
  • 服务器处理请求,并对请求做出响应。
  • 浏览器收到服务器响应,得到html代码。
  • 渲染页面。

通过以上这些步骤,就完成了一次完整的http请求

Step1 浏览器(客户端)进行了地址解析

当我们在浏览器中输入一个地址,按下回车后,浏览器获取到的是一个字符串。浏览器此时要对这个地址进行解析,获取协议,主机,端口,路径等信息。

URL的一般格式为(手记会自动过滤尖括号,所以只能上传图片了):
例如:

http://www.imooc.com/article/draft/id/430
这个网址缺少了一些东西,端口号,用户名,密码,query和flag都没有。
这些东西都是非必须的,甚至协议、路径都可以不要,最简洁的方式为imooc.com,浏览器会对一些默认的东西进行补齐。
例如:互联网url默认端口号为80,浏览器默认补齐功能会补齐协议http,有些还会直接在域名前面补上www。
所以,实际上,即使我们输入的是imooc.com,然而实际访问的却是http://www.imooc.com。

Step2 将解析出的域名进行dns解析

第一步地址解析中我们已经获取到服务器的域名。此时就需要将域名换成对应的ip地址,这就是dns解析。dns解析分为以下几个步骤:

  • 先查看浏览器dns缓存中是否有域名对应的ip。
  • 如果没有,则查看操作系统dns缓存中是否有对应的ip(例如windows的hosts文件)。
  • 依旧没有就对本地区的dns服务器发起请求,
  • 如果还是没有,就直接到Root Server域名服务器请求解析。
  • 这里面有几点需要关注:
  • <1>、DNS在进行区域传输的时候使用TCP协议,其它时候则使用UDP协议;
  • <2>、全球只有十三台逻辑根服务器,为什么是十三台,请参考https://www.zhihu.com/question/22587247?answer_deleted_redirect=true。其中任何一次解析成功就返回对应的ip地址。

Step3 通过ip寻址和arp,找到目标(服务器)地址

第二步获取到了ip,此时直接通过ip寻址找到ip对应的服务器,然后通过arp协议找到服务器的mac地址。

这里有几点需要注意:

  • ip地址(ipv4, 32位)。ip地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。ip地址分为A、B、C、D、E五大类:
  • A类地址:一个字节(8位)的网络地址和三个字节的主机地址。地址范围为:1.0.0.0~126.255.255.255。
  • B类地址:二个字节的网络地址和二个字节的主机地址。地址范围为:128.0.0.0~191.255.255.255。
  • C类地址:三个字节的网络地址和一个字节的主机地址。地址范围为:192.0.0.0~223.255.255.255。
  • D类地址:D类地址用于多点广播(Multicast),D类IP地址第一个字节以“lll0”开始,它是一个专门保留的地址。地址范围为:224.0.0.0~239.255.255.255。
  • E类地址:E类IP地址 以“llll0”开始,为将来使用保留。地址范围为:240.0.0.0~255.255.255.254。,255.255.255.255用于广播地址。

其中缺失了两部分,一个是0开头的,“0”表示该地址是本地主机,不能传送。一个是127开头的,127开头的是网卡自身,常用于测试。这里为什么是十进制的数字,为什么中间有‘.’,其实这都是为了方便人类而人为加上去的。转化为计算机语言就是二进制的,每一个字节八位,八位二进制能表示的最大数字就是255,这样ip地址就齐全了。可能有些人还发现ip地址为 10.170.8.61/23 ,这里涉及到局域网、保留地址和子网掩码。这里的意思是,前23位表示为该台主机的网络地址,该网络有 2^(32-23) = 512台主机。具体就不展开讲了,涉及的内容太深,太多。感兴趣的可以参考https://www.zhihu.com/question/56895036

  • IP寻址如何工作?
    ip寻址主要有两种方式,一种是同一网段,一种是不同网段。要判断两个IP地址是不是在同一个网段,就将它们的IP地址分别与子网掩码做与运算,得到的结果一网络号,如果网络号相同,就在同一子网,否则,不在同一子网。

同一网段的情况:

主机A和主机B,首先主机A通过本机的hosts表或者wins系统或dns系统先将主机B的计算机名 转换为Ip地址,然后用自己的 Ip地址与子网掩码计算出自己所出的网段,比较目的主机B的ip地址与自己的子网掩码,发现与自己是出于相同的网段,于是在自己的ARP缓存中查找是否有主机B 的mac地址,如果能找到就直接做数据链路层封装并且通过网卡将封装好的以太网帧发送有物理线路上去:如果arp缓存中没有主机B的的mac地址,主机A将启动arp协议通过在本地网络上的arp广播来查询主机B的mac地址,获得主机B的mac地址厚写入arp缓存表,进行数据链路层的封装,发送数据。

不同网段的情况:

不同的数据链路层网络必须分配不同网段的Ip地址并且由路由器将其连接起来。和上面一样,主机A发现和主机B不在同一个网段,于是主机A将知道应该将次数据包发送给自己的缺省网关,即路由器的本地接口。主机A在自己的ARP缓存中查找是否有缺省网关的MAC地址,如果能够找到就直接做数据链路层封装并通过网卡 将封装好的以太网数据帧发送到物理线路上去,如果arp缓存表中没有缺省网关的Mac地址,主机A将启动arp协议通过在本地网络上的arp广播来查询缺省网关的mac地址,获得缺省网关的mac地址后写入arp缓存表,进行数据链路层的封装,发送数据。数据帧到达路由器的接受接口后首先解封装,变成ip数据包,对ip 包进行处理,根据目的Ip地址查找路由表,决定转发接口后做适应转发接口数据链路层协议帧的封装,并且发送到下一跳路由器,次过程继续直至到达目的的网络与目的主机。整个过程有点像dns解析,只是dns服务器换成了下一跳路由器,udp编程了tcp,其他差别不大。

  • arp。arp就是地址转化协议,也就是把ip地址转化为mac地址。和dns很像,先查缓存,然后查路由器。
  • mac地址。mac地址就是计算机的物理地址,每个网卡出厂时,被生产厂家烧制在网卡上。采用十六进制数表示,共六个字节(48位)。三个字节是由IEEE的注册管理机构RA负责给不同厂家分配的代码(高位24位),也称为“编制上唯一的标识符”(Organizationally Unique Identifier),后三个字节(低位24位)由各厂家自行指派给生产的适配器接口,称为扩展标识符(唯一性)。如何修改mac地址呢?一个方法就是直接修改网卡上烧制的mac地址,自己烧制。这个基本不靠谱,失误性也高。另一个方法就是修改注册表中的mac地址,因为网络中访问的mac地址都是访问的注册表中的mac地址,不会直接访问网卡。这个比较简单直接。
  • 为什么有了ip地址,还要mac地址?这个问题很关键,就像是我有驾驶证了你非要让我提供身份证。这个涉及一些历史问题,因为一开始没有互联网的时候就只有mac地址,还不存在ip地址。后来互联网越来越大之后,发现mac地址找起来太麻烦,并且耗时也越来越久,就发明了ip地址。并且mac地址在一个局域网中还是很有用的,所以就两个一起存在了。详细的信息,大家可以参考https://www.zhihu.com/question/21546408。

Step4 进行tcp三次握手,建立tcp连接

简述一下,第三步我们找到了目标ip,并获得了服务器ip的mac地址。
此时浏览器就会请求和服务器连接,用来传输数据。
tcp 是稳定双向面向连接的,断开时也会分两边分别断开。
面向连接不是说tcp一个双方一直开着的通道,而是维持一个连接的状态,让它看起来有连接。

Step5 浏览器发送数据,等待服务器响应

第四步已经建立了连接,此时就要发送数据了。浏览器会对请求进行包装,包装成请求报文。请求报文的格式如下:

  • 起始行:如 GET / HTTP/1.0 (请求的方法 请求的URL 请求所使用的协议)

  • 头部信息:User-Agent Host等成对出现的值

  • 请求主体

请求头部和主体之间有一个回车换行。如果是get请求,则没有主体部分,post请求有主体部分。当然里面还有些请求头部比较重要

Step6 服务器处理请求,并对请求做出响应

浏览器请求报文到达服务器之后,服务器接口会对请求报文进行处理,执行接口对应的代码,处理完成后响应客户端。由于http是无状态的,正常情况下,客户端收到响应后就会直接断开连接,然后一次http请求就完成了。但是http1.0有一个keep-alive的请求字段,可以在一定时间内不断开连接(有时时间甚至很长)。http1.1直接就默认开启了keep-alive选项。这导致了一个后果是服务器已经处理完了请求,但是客户端不会主动断开连接,这就导致服务器资源一直被占用。这时服务器就不得不自己主动断开连接,而主动断开连接的一方会出现TIME_WAIT,占用连接池,这就是产生SYN Flood攻击的原因。

此时有三种处理方式:

  • 第一是客户端主动断开连接

第一种情况,如果服务器返回的数据都有确定的content-length属性,或者客户端知道服务器返回的内容终止,则客户端主动断开连接。

  • 第二是服务器主动断开连接

第二种情况,服务器可以通过设置一个最大超市时间,可以主动断开tcp连接。

  • 第三是对tcp连接经行设置。

第三种情况,调整t三个tcp参数:

  • 第一个是:tcp_synack_retries 可以用他来减少重试次数;
  • 第二个是:tcp_max_syn_backlog,可以增大syn连接数;
  • 第三个是:tcp_abort_on_overflow 处理不过来干脆就直接拒绝连接了。

Step7 浏览器收到服务器响应,得到html代码

其实你心里有疑问,这一步有什么好说的。其实这里面有很多需要注意的点。浏览器发出请求时,请求报文如下:

你需要关注一个报文头--accept。accept代表发送端(客户端)希望接受的数据类型,这是浏览器自动封装的请求头。如果服务器返回的content-type是accept中的任何一个,浏览器都能解析,并直接展示在网页上。如果服务器返回的content-type是其他类型,此时浏览器有三种处理状态:

  • 正常显示。例如返回类型为text/javascript,浏览器能直接处理并展示。
  • 下载。例如返回类型为application/octet-stream(二进制流,不知道下载文件类型),这种浏览器不能直接处理的,会被下载。
  • 报错。当我们返回一个字符串hello world,却使用text/xml,格式时,浏览器不能正确解析,就会报错,并把报错信息呈现在网页中。

浏览器能直接处理很多种格式,并直接呈现在网页中,并不限于accept中规定的字段,具体有哪些,就需要自己亲自动手试试了。

附上一张content-type常用对照表地址:http content-type常用对照表

Step8 浏览器引擎渲染页面

获取到服务器相应之后,浏览器会根据相应的content-type字段对响应字符串进行解析。
能够解析并成功解析就显示,能够解析但解析错误就报错,不能解析就下载。
由于浏览器采用至上而下的方式解析,所以会先解析html,直到遇到外部样式和外部脚本。
这时会阻塞浏览器的解析,外部样式和外部脚本(在没有async、defer属性下)会并行加载,但是外部样式会阻塞外部脚本的执行,dom加载完毕,js脚本执行成功后dom树构建完成(DOMContentLoaded),之后就加载dom中引用的图片等静态资源。
(参考文章地址:http://blog.csdn.net/u014168594/article/details/52196460)

即:

html解析->外部样式、脚本加载->外部样式执行->外部脚本执行->html继续解析->dom树构建完成->加载图片->页面加载完成。

  • 情况一:如果是动态脚本(即内联脚本)则不受样式影响,在解析到它时会执行。

如果脚本是动态加载,则不会影响DOMContentLoaded时间的触发,浏览器会等css加载完成后再加载图片,因为不确定图片的样式会如何。

  • 情况二:外部样式后续外部脚本含有async属性(IE下为defer),外部样式不会阻塞该脚本的加载与执行

在外部样式执行完毕后,css附着于DOM,创建了一个渲染树(渲染树是一些被渲染对象的集)。
每个渲染对象都包含了与之对应的计算过样式的DOM对象,对于每个渲染元素来说,位置都经过计算,所以这里被叫做“布局”。
然后将“布局”显示在浏览器窗口,称之为“绘制”。

接着脚本的执行完毕后,DOM树构建完成。这时,可以触发DOMContentLoaded事件。DOMContentLoaded事件的触发条件是:在所有的DOM全部加载完毕并且JS加载执行后触发。

  • 要点一:CSS样式表会阻塞图片的加载,如果想让图片尽快加载,就不要给图片使用样式,比如宽高采用标签属性即可。
  • 要点二:脚本不会阻塞图片的加载

最后页面加载完成,页面load。

总结一下:

  • 运维人员需要处理页面缓存、cdn及keep-alive引起的连接池占用等问题;
  • 后端人员需要处理代码逻辑、缓存、传输优化、报错等问题;
  • 前端人员需要做好前端性能优化和配合运维、后端做好借口调试,缓存处理等问题。

所以,无论是前端、后台、运维都应该很清楚整个流程中的每一步,才能在配合时,得心应手,才能在出现问题时,快速准确的定位问题解决问题,才能在需要优化时,迅速完整的给出方案。

1.3 HTTP请求和响应详解

1.3.1 客户端请求消息

客户端请求消息,客户端发送一个HTTP请求到服务器的请求消息,包括4个部分:

  • 请求行(request line)

请求报文的第一行,用来说明以什么方式请求、请求的地址和HTTP版本

  • 请求头部(header)

每个头部字段都包含一个名字和值,二者之间采用“:”连接,如:Connection:Keep-Alive

  • 空行
  • 请求数据

请求的主体根据不同的请求方式请求主体不同

1.3.2 服务器响应消息

HTTP响应也由4个部分组成,分别是:

  • 状态行

由HTTP版本、响应状态码、响应状态描述;如:HTTP/1.1 200 OK

  • 消息报头(响应报文头部)

使用关键字和值表示,二者使用“:”隔开;如:Content-Type:text/html

  • 空行

  • 响应正文

请求空行之后就是请求内容

1.4 HTTP协议/版本的发展历程

  • HTTP/0.9:1991年发布,极其简单,只有一个get命令
  • HTTP/1.0:1996年5月发布,增加了大量内容

HTTP 1.0 定义了3种请求方法: GET, POST 和 HEAD方法。

  • HTTP/1.1:1997年1月发布,进一步完善HTTP协议,是目前最流行的版本

HTTP1.1 新增了五种请求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法。

  • SPDY :2009年谷歌发布SPDY协议,主要解决HTTP/1.1效率不高的问题
  • HTTP/2 :2015年借鉴SPDY的HTTP/2发布

2 HTTP/1.0 [1991]

3 HTTP/1.1 [1996]【当前浏览器与服务器的主流选择/默认选择】

3.1 HTTP/1.0 与 HTTP/1.1的区别

3.1.1 缓存处理

  • HTTP/1.0 使用 Pragma:no-cache + Last-Modified/If-Modified-Since来作为缓存判断的标准
  • HTTP/1.1 引入了更多的缓存控制策略:Cache-Control、Etag/If-None-Match等

3.1.2 错误状态管理

  • HTTP/1.1新增了24个错误状态响应码

如:

  • 409(Conflict)表示请求的资源与资源的当前状态发生冲突
  • 410(Gone)表示服务器上的某个资源被永久性的删除。

3.1.3 范围请求

  • HTTP/1.1在请求头引入了range头域,它允许只请求资源的某个部分,即返回码是206Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接,支持断点续传

3.1.4 Host头

  • HTTP1.0

HTTP1.0 认为每台服务器都绑定一个唯一的IP地址。
因此,请求消息中的URL并没有传递主机名(hostname)。
但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。

  • HTTP1.1

HTTP/1.1 的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)。
有了Host字段,就可以将请求发往同一台服务器上的不同网站,为虚拟主机的兴起打下了基础

3.1.5 持久连接与管道机制

  • HTTP1.0 需要加上keep-alive的请求首部。

否则,默认一个HTTP请求对应一个TCP连接。

  • HTTP/1.1 最大的变化就是引入了持久连接HTTP Persistent Connection / HTTP Connection Reuse / HTTP keep-alive

在HTTP/1.1 中默认开启 Connection: keep-alive
即 : TCP连接默认不关闭,可以被多个HTTP请求复用

客户端和服务器发现对方一段时间没有活动,就可以主动关闭连接。
不过,规范的做法是,客户端在最后一个请求时,发送Connection: close,明确要求服务器关闭TCP连接。

客户端的长连接不可能无限期的拿着,会有一个超时时间,服务器有时候会告诉客户端超时时间,譬如上图。
上图中的Keep-Alive: timeout=20,表示这个TCP通道可以保持20秒。
另外还可能有max=XXX,表示这个长连接最多接收XXX次请求就断开。
对于客户端来说,如果服务器没有告诉客户端超时时间也没关系,服务端可能主动发起四次握手断开TCP连接,客户端能够知道该TCP连接已经无效;
另外TCP还有心跳包来检测当前连接是否还活着,方法很多,避免浪费资源。

  • HTTP/1.1 中引入了管道机制pipelining),即在同一个TCP连接中,客户端可以同时发送多个HTTP请求

3.1.6 请求方法

  • HTTP1.0 定义了3种请求方法: GET / POST / HEAD
  • HTTP1.1 新增了5种请求方法:OPTIONS / PUT / DELETE / TRACE / CONNECT

3.2 HTTP/1.1的缺点

3.2.1 TCP队头阻塞问题

HTTP/1.1 的持久连接管道机制允许复用TCP连。
在一个TCP连接中,也可以同时发送多个请求,但是所有的数据通信都是按次序完成的,服务器只有处理完一个回应,才会处理下一个回应。

比如: 客户端需要A、B两个资源,管道机制允许浏览器同时发出A请求和B请求,但服务器还是按照顺序,先回应A请求,完成后再回应B请求,这样如果前面的回应特别慢,后面就会有很多请求排队等着,这称为"队头阻塞(Head-of-line blocking)"

4 HTTP/2 [2015]

HTTP/2以Google发布的SPDY协议为基础,于2015年发布。
它不叫HTTP/2.0,因为标准委员会不打算再发布子版本了,下一个新版本将是HTTP/3。
HTTP/2协议只在HTTPS环境下才有效,升级到HTTP/2,必须先启用HTTPS。

4.0 HTTP/2 主要特点

HTTP/2解决了HTTP/1.1的性能问题,主要特点如下:

4.0.1 二进制分帧

  • HTTP/1.1 的头信息是文本(ASCII编码),数据体可以是文本,也可以是二进制;
  • HTTP/2 头信息和数据体都是二进制,统称为“帧”:头信息帧和数据帧;

4.0.2 多路复用(双工通信)

通过单一的 HTTP/2 连接发起多重的请求-响应消息,即在一个连接里,客户端和浏览器都可以同时发送多个请求和响应,而不用按照顺序一一对应,这样避免了“队头堵塞”。
HTTP/2 把 HTTP 协议通信的基本单位缩小为一个一个的帧,这些帧对应着逻辑流中的消息。并行地在同一个 TCP 连接上双向交换消息。

4.0.3 数据流(stream)

因为 HTTP/2 的数据包是不按顺序发送的,同一个连接里面连续的数据包,可能属于不同的回应。因此,必须要对数据包做标记,指出它属于哪个回应。

HTTP/2 将每个请求或回应的所有数据包,称为一个数据流stream)。
每个数据流都有一个独一无二的编号。数据包发送的时候,都必须标记数据流ID,用来区分它属于哪个数据流。
另外还规定,客户端发出的数据流,ID一律为奇数服务器发出的,ID为偶数。
数据流发送到一半的时候,客户端和服务器都可以发送信号(RST_STREAM帧),取消这个数据流。

HTTP/1.1 取消数据流的唯一方法,就是关闭TCP连接
这就是说,HTTP/2 可以取消某一次请求,同时保证TCP连接还打开着,可以被其他请求使用。
客户端还可以指定数据流的优先级。优先级越高,服务器就会越早回应。

4.0.4 首部压缩

HTTP 协议不带有状态,每次请求都必须附上所有信息。
所以,请求的很多字段都是重复的,一模一样的内容,每次请求都必须附带,这会浪费很多带宽,也影响速度。

HTTP/2 对这一点做了优化,引入了头信息压缩机制header compression)。
一方面,头信息压缩后再发送(SPDY 使用的是通用的DEFLATE 算法,而 HTTP/2 则使用了专门为首部压缩而设计的 HPACK 算法)。
另一方面,客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就提高速度了。

4.0.5 服务端推送

HTTP/2 允许服务器未经请求,主动向客户端发送资源,这叫做服务器推送server push)。
常见场景是客户端请求一个网页,这个网页里面包含很多静态资源。
正常情况下,客户端必须收到网页后,解析HTML源码,发现有静态资源,再发出静态资源请求。
其实,服务器可以预期到客户端请求网页后,很可能会再请求静态资源,所以就主动把这些静态资源随着网页一起发给客户端了。

4.1 HTTP/2 辉煌不在?

虽然HTTP/2标准在2015年5月就以RFC 7540正式发表了,并且多数浏览器在2015年底就支持了。
但是,真正被广泛使用起来要到2018年左右,但是也是在2018年,11月IETF给出了官方批准,认可HTTP-over-QUIC成为HTTP/3。
2018年的时候,我写过一篇文章介绍《HTTP/2到底是什么?》,那时候HTTP/2还是个新技术,刚刚开始有软件支持,短短两年过去了,现在HTTP/3已经悄然而至了。
根据W3Techs的数据,截至2019年6月,全球也仅有36.5%的网站支持了HTTP/2。所以,可能很多网站还没开始支持HTTP/2,HTTP/3就已经来了。
所以,对于很多网站来说,或许直接升级HTTP/3是一个更加正确的选择。

4.2 回顾 HTTP/2

在阅读本文之前,强烈建议大家先阅读下《HTTP/2到底是什么?》这篇文章,这里面介绍了HTTP的历史,介绍了各个版本的HTTP协议的诞生的背景。
当你读到这里的时候,我默认大家对HTTP/2有了一定的基本了解。
我们知道,HTTP/2的诞生,主要是为了解决HTTP/1.1中的效率问题,HTTP/2中最核心的技术就是多路复用技术,即允许同时通过单一的HTTP/2.0连接发起多重的请求-响应消息

同时还实现了二进制分帧header压缩服务端推送等技术。

HTTP/1.0诞生,一直到HTTP/2,在这24年里,HTTP协议已经做过了三次升级,但是有一个关键的技术点是不变的,那就是这所有的HTTP协议,都是基于TCP协议实现的。

流水的HTTP,铁打的TCP。这是因为相对于UDP协议,TCP协议更加可靠。

虽然在HTTP/1.1的基础上推出HTTP/2大大的提升了效率,但是还是有很多人认为这只是个"临时方案",这也是为什么刚刚推出没多久,业内就开始大力投入HTTP/3的研发与推广了。
而这背后的深层次原因也正是因为他还是基于TCP协议实现的。TCP协议虽然更加可靠,但是还是存在着一定的问题,接下来具体分析下。

4.3 HTTP/2 问题 : 队头阻塞问题

队头阻塞翻译自英文head-of-line blocking,这个词并不新鲜,因为早在HTTP/1.1时代,就一直存在着队头阻塞的问题。
但是很多人在一些资料中会看到有论点说HTTP/2解决了队头阻塞的问题。但是这句话只对了一半。

只能说HTTP/2解决了HTTP的队头阻塞问题,但是并没有解决TCP队头阻塞问题

如果大家对于HTTP的历史有一定的了解的话,就会知道。

4.3.1 HTTP/1.1 引入【持久连接/请求管道(keep-alive)】 => 仍遗留【HTTP队头阻塞问题】

HTTP/1.1相比较于HTTP/1.0来说,最主要的改进就是引入了持久连接(keep-alive)。
所谓的持久连接就是:

在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟。

引入了持久连接之后,在性能方面,HTTP协议有了明显的提升。

另外,HTTP/1.1允许在持久连接上使用请求管道,是相对于持久连接的又一性能优化。

所谓请求管道,就是在HTTP响应到达之前,可以将多条请求放入队列,当第一条HTTP请求通过网络流向服务器时,第二条和第三条请求也可以开始发送了。在高时延网络条件下,这样做可以降低网络的环回时间,提高性能。

但是,对于管道连接还是有一定的限制和要求的,其中一个比较关键的就是:

服务端必须按照与请求相同的顺序回送HTTP响应

这也就意味着:

如果一个响应返回发生了延迟,那么其后续的响应都会被延迟,直到队头的响应送达。这就是所谓的HTTP队头阻塞

4.3.2 HTTP/2 多路复用(引入【帧/消息/数据流】): 解决【HTTP队头阻塞】 => 仍依赖【TCP队头阻塞】

但是HTTP队头阻塞的问题在HTTP/2中得到了有效的解决。
HTTP/2废弃了管道化的方式,而是创新性的引入了消息数据流等概念。
客户端和服务器可以把 HTTP 消息分解为互不依赖的帧,然后乱序发送,最后再在另一端把它们重新组合起来。

因为没有顺序了,所以就不需要阻塞了,就有效的解决了HTTP队头阻塞的问题。

但是,HTTP/2仍然会存在TCP队头阻塞的问题,那是因为HTTP/2其实还是依赖TCP协议实现的。

TCP传输过程中会把数据拆分为一个个按照顺序排列的数据包,这些数据包通过网络传输到了接收端,接收端再按照顺序将这些数据包组合成原始数据,这样就完成了数据传输。

但是如果其中的某一个数据包没有按照顺序到达,接收端会一直保持连接等待数据包返回,这时候就会阻塞后续请求。这就发生了TCP队头阻塞。

HTTP/1.1管道化持久连接,也是使得同一个TCP连接可以被多个HTTP连接使用,但是HTTP/1.1中规定一个域名可以有6个TCP连接
HTTP/2中,同一个域名只是用一个TCP连接

所以,在HTTP/2中,TCP队头阻塞造成的影响会更大,因为HTTP/2多路复用技术使得多个请求其实是基于同一个TCP连接的,那如果某一个请求造成了TCP队头阻塞,那么多个请求都会受到影响。

4.4 TCP握手时长

一提到TCP协议,大家最先想到的一定是它的三次握手四次关闭的特性。

因为TCP是一种可靠通信协议,而这种可靠就是靠三次握手实现的,通过三次握手TCP在传输过程中可以保证接收方收到的数据是完整有序无差错的。

但是,问题是三次握手是需要消耗时间的,这里插播一个关于网络延迟的概念。

网络延迟又称为 RTTRound Trip Time)。

  • 它是指一个请求从客户端浏览器发送一个请求数据包到服务器,再从服务器得到响应数据包的这段时间。
  • RTT 是反映网络性能的一个重要指标。

我们知道,TCP三次握手的过程客户端和服务器之间需要交互三次,那么也就是说需要消耗1.5 RTT

另外,如果使用的是安全的HTTPS协议,就还需要使用TLS协议进行安全数据传输,这个过程又要消耗1个RTT(TLS不同版本的握手机制不同,这里按照最小的消耗来算)

那么也就是说,一个纯HTTP/2的连接,需要消耗1.5个RTT,如果是一个HTTPS连接,就需要消耗3-4个RTT。

而具体消耗的时长根据服务器和客户端之间的距离则不尽相同,如果比较近的话,消耗在100ms以内,对于用来说可能没什么感知,但是如果一个RTT的耗时达到300-400ms,那么,一次连接建立过程总耗时可能要达到一秒钟左右,这时候,用户就会明显的感知到网页加载很慢。

4.5 升级TCP是否可行?

4.5.1 网络中间设备僵化 => 协议僵化

基于上面我们提到的这些问题,很多人提出来说:既然TCP存在这些问题,并且我们也知道这些问题的存在,甚至解决方案也不难想到,为什么不能对协议本身做一次升级,解决这些问题呢?

其实,这就涉及到一个"协议僵化"的问题。

这样讲,我们在互联网上浏览数据的时候,数据的传输过程其实是极其复杂的。

我们知道的,想要在家里使用网络有几个前提,首先我们要通过运行商开通网络,并且需要使用路由器,而路由器就是网络传输过程中的一个中间设备。

中间设备是指插入在数据终端和信号转换设备之间,完成调制前或解调后某些附加功能的辅助设备。例如集线器、交换机和无线接入点、路由器、安全解调器、通信服务器等都是中间设备。

在我们看不到的地方,这种中间设备还有很多很多,一个网络需要经过无数个中间设备的转发才能到达终端用户。

如果TCP协议需要升级,那么意味着需要这些中间设备都能支持新的特性,我们知道路由器我们可以重新换一个,但是其他的那些中间设备呢?尤其是那些比较大型的设备呢?更换起来的成本是巨大的。

而且,除了中间设备之外,操作系统也是一个重要的因素,因为TCP协议需要通过操作系统内核来实现,而操作系统的更新也是非常滞后的。

所以,这种问题就被称之为"中间设备僵化",也是导致"协议僵化"的重要原因。这也是限制着TCP协议更新的一个重要原因。

所以,近些年来,由IETF标准化的许多TCP新特性都因缺乏广泛支持而没有得到广泛的部署或使用!

4.6 放弃TCP?

上面提到的这些问题的根本原因都是因为HTTP/2是基于TPC实现导致的,而TCP协议自身的升级又是很难实现的。

那么,剩下的解决办法就只有一条路,那就是放弃TCP协议。

放弃TCP的话,就又有两个新的选择,是使用其他已有的协议,还是重新创造一个协议呢?

看到这里,聪明的读者一定也想到了,创造新的协议一样会受到中间设备僵化的影响。近些年来,因为在互联网上部署遭遇很大的困难,创造新型传输层协议的努力基本上都失败了!

所以,想要升级新的HTTP协议,那么就只剩一条路可以走了,那就是基于已有的协议做一些改造和支持,UDP就是一个绝佳的选择了。

4.Y 小结

因为HTTP/2底层是采用TCP协议实现的,虽然解决了HTTP队头阻塞的问题,但是对于TCP队头阻塞的问题却无能为力。

TCP传输过程中会把数据拆分为一个个按照顺序排列的数据包,这些数据包通过网络传输到了接收端,接收端再按照顺序将这些数据包组合成原始数据,这样就完成了数据传输。

但是如果其中的某一个数据包没有按照顺序到达,接收端会一直保持连接等待数据包返回,这时候就会阻塞后续请求。这就发生了 TCP队头阻塞

另外,TCP这种可靠传输是靠三次握手实现的,TCP三次握手的过程客户端和服务器之间需要交互三次,那么也就是说需要消耗1.5 RTT。如果是HTTPS那么消耗的RTT就更多。

而因为很多中间设备比较陈旧,更新换代成本巨大,这就导致TCP协议升级或者采用新的协议基本无法实现。

所以,HTTP/3选择了一种新的技术方案,那就是基于UDP做改造,这种技术叫做QUIC

那么问题来了,HTTP/3是如何使用的UDP呢?做了哪些改造?如何保证连接的可靠性?UDP协议就没有僵化的问题了吗?

4.X 参考文献

K HTTPS

K.1 简述

HTTPS可以说是安全版的HTTP,HTTPS基于安全SSL/TLS(安全套接层Secure Sockets Layer/安全传输层Transport Layer Security)层
即: 在传统的HTTP和TCP之间加了一层用于加密解密的SSL/TLS层。HTTP默认使用80端口,HTTPS默认使用443端口。

不使用SSL/TLS的HTTP通信,所有信息明文传播,会带来三大风险:

  • 窃听风险:第三方可以获取通信内容;

  • 篡改风险:第三方可以修改通信内容;

  • 冒充风险:第三方可以冒充他人进行通信。

SSL/TLS协议是为了解决这三大风险而设计的,以期达到:

  • 信息加密传输:第三方无法窃听;
  • 校验机制:一旦被篡改,通信双方会立刻发现;
  • 身份证书:防止身份被冒充。

K.2 SSL/TLS发展

  • SSL/1.0:1994年NetScape公司设计,未发布;
  • SSL/2.0:1995年NetScape公司发布,但存在严重漏洞;
  • SSL/3.0:1996年NetScape公司发布,得到大规模应用;
  • TLS/1.0:1999年互联网标准化组织(ISOC)接替NetScape公司,发布SSL的升级版TLS/1.0;
  • TLS/1.1:2006年发布;
  • TLS/1.2:2008年发布;
  • TLS/1.2修订版:2011年发布。

目前,应用最广泛的是 TLS/1.0 和 SSL/3.0,且主流浏览器已实现 TLS/1.2的支持。

K.3 SSL/TLS运行机制

SSL/TLS的基本思路是公钥加密法

  • 客户端先向服务器索要并验证公钥,
  • 然后,客户端用公钥加密传输来协商生成“对话秘钥”(非对称加密),双方采用“对话秘钥”进行加密通信(对称加密)。

通信过程如下:

  • 客户端发出请求:给出支持的协议版本、支持的加密方法(如RSA公钥加密)以及一个客户端生成的随机数(Client random);
  • 服务端回应:确认双方通信的协议版本、加密方法,并给出服务器证书以及一个服务器生成的随机数(Server random);
  • 客户端回应:客户端确认证书有效,取出证书中的公钥,然后生成一个新的随机数(Premaster secret),使用公钥加密这个随机数,发送给服务端;
  • 服务端回应:服务端使用自己的私钥解密客户端发来的随机数(Premaster secret),客户端和服务端根据约定的加密方法,使用三个随机数,生成“对话秘钥”;
  • 会话通信:客户端和服务端使用“对话秘钥”加密通信,这个过程完全使用普通的HTTP协议,只不过用“会话秘钥”加密内容。

前四步称为握手阶段,用于客户端和服务端建立连接交换参数

K.4 HTTPS协议的特点

  • 缓存:只要在HTTP头中使用特定命令,就可以缓存HTTPS

  • 延迟:

  • HTTP耗时 = TCP握手
  • HTTPS耗时 = TCP握手 + SSL握手。

SSL握手耗时大概是TCP握手耗时的三倍左右。

Q 相关面试问题

Q2 HTTP 与 HTTPS 的区别?

Q3 常用HTTP状态码?

HTTP状态码表示客户端HTTP请求的返回结果、标识服务器处理是否正常、表明请求出现的错误等。
状态码的类别:

Q4 GET和POST区别?

[1] HTTP 协议的请求方法
HTTP协议中定义了浏览器和服务器进行交互的不同方法,基本方法有4种,分别是GET,POST,PUT,DELETE。这四种方法可以理解为,对服务器资源的查,改,增,删。

  • GET:从服务器上获取数据,也就是所谓的查,仅仅是获取服务器资源,不进行修改。
  • POST:向服务器提交数据,这就涉及到了数据的更新,也就是更改服务器的数据。
  • PUT:英文含义是放置,也就是向服务器新添加数据,就是所谓的增。
  • DELETE:从字面意思也能看出,这种方式就是删除服务器数据的过程。

[1] HTTP 协议中GETPOST的区别?

  • Get是不安全的。

因为在传输过程,数据被放在请求的URL中;Post的所有操作对用户来说都是不可见的。 但是这种做法也不时绝对的,大部分人的做法也是按照上面的说法来的,但是也可以在get请求加上 request body,给 post请求带上 URL 参数。

  • Get请求提交的url中的数据最多只能是2048字节

这个限制是浏览器或者服务器给添加的,http协议并没有对url长度进行限制,目的是为了保证服务器和浏览器能够正常运行,防止有人恶意发送请求。Post请求则没有大小限制。

  • Get限制Form表单的数据集的值必须为ASCII字符;而Post支持整个ISO10646字符集
  • Get执行效率却比Post方法好。Get是form提交的默认方法。
  • GET产生一个TCP数据包;POST产生2个TCP数据包

对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);
而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。

Q5 什么是对称加密与非对称加密?

  • 对称密钥加密是指加密和解密使用同一个密钥的方式。这种方式存在的最大问题就是密钥发送问题,

即:如何安全地将密钥发给对方

  • 非对称加密是指使用一对非对称密钥,即公钥私钥,公钥可以随意发布,但私钥只有自己知道。发送密文的一方使用对方的公钥进行加密处理,对方接收到加密信息后,使用自己的私钥进行解密。
    由于非对称加密的方式不需要发送用来解密的私钥,所以可以保证安全性;但是和对称加密比起来,非常的慢

Q6 什么是HTTP2?

HTTP2 可以提高了网页的性能。

HTTP1 中浏览器限制了同一个域名下的请求数量(Chrome 下一般是六个),当在请求很多资源的时候,由于队头阻塞当浏览器达到最大请求数量时,剩余的资源需等待当前的六个请求完成后才能发起请求。

HTTP2 中引入了多路复用的技术,这个技术可以只通过一个 TCP 连接就可以传输所有的请求数据。

多路复用可以绕过浏览器限制同一个域名下的请求数量的问题,进而提高了网页的性能。

Q7 Session、Cookie和Token的主要区别?

  • HTTP协议本身是无状态的。什么是无状态呢,即服务器无法判断用户身份。

  • 什么是cookie?

cookie是由Web服务器保存在用户浏览器上的小文件(key-value格式),包含用户相关的信息。
客户端向服务器发起请求,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie。
客户端浏览器会把Cookie保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie,以此来辨认用户身份。

  • 什么是Session?

session是·依赖Cookie·实现的。session是服务器端的用户会话对象
session 是浏览器和服务器会话过程中,服务器分配的一块储存空间。
服务器默认为浏览器在cookie中设置 sessionid,浏览器在向服务器请求过程中传输 cookie 包含 sessionid ,服务器根据 sessionid 获取出会话中存储的信息,然后确定会话的身份信息。

  • cookie与session区别?
  • 存储位置与安全性:cookie数据存放在客户端上,安全性较差,session数据放在服务器上,安全性相对更高;
  • 存储空间:单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie,session无此限制
  • 占用服务器资源:session一定时间内保存在服务器上,当访问增多,占用服务器性能,考虑到服务器性能方面,应当使用cookie。

Q8 什么是Token?

Token的引入:Token是在客户端频繁向服务端请求数据,服务端频繁的去数据库查询用户名和密码并进行对比,判断用户名和密码正确与否,并作出相应提示,在这样的背景下,Token便应运而生。

Token的定义:Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。

使用Token的目的:Token的目的是为了减轻服务器的压力,减少频繁的查询数据库,使服务器更加健壮。

Token 是在服务端产生的。如果前端使用用户名/密码向服务端请求认证,服务端认证成功,那么在服务端会返回 Token 给前端。前端可以在每次请求的时候带上 Token 证明自己的合法地位

Q9 session与token区别?

  • session机制存在服务器压力增大,CSRF跨站伪造请求攻击,扩展性不强等问题;
  • session存储在服务器端,token存储在客户端
  • token提供认证和授权功能,作为身份认证,token安全性比session好;
  • session这种会话存储方式方式只适用于客户端代码和服务端代码运行在同一台服务器上,token适用于项目级的前后端分离(前后端代码运行在不同的服务器下)

Q10 Servlet是线程安全的吗?

Servlet不是线程安全的,多线程并发的读写会导致数据不同步的问题。
解决的办法是尽量不要定义name属性,而是要把name变量分别定义在doGet()doPost()方法内。虽然使用synchronized(name){}语句块可以解决问题,但是会造成线程的等待,不是很科学的办法。

注意:多线程的并发的读写Servlet类属性会导致数据不同步。但是如果只是并发地读取属性而不写入,则不存在数据不同步的问题。因此Servlet里的只读属性最好定义为final类型的。

Q11 Servlet接口中有哪些方法?

在Java Web程序中,Servlet主要负责接收用户请求HttpServletRequest,在doGet(),doPost()中做相应的处理,并将回应HttpServletResponse反馈给用户。Servlet可以设置初始化参数,供Servlet内部使用。

Servlet接口定义了5个方法,其中前三个方法与Servlet生命周期相关:

  • void init(ServletConfig config) throws ServletException
  • void service(ServletRequest req, ServletResponse resp) throws ServletException, java.io.IOException
  • void destory()
  • java.lang.String getServletInfo()
  • ServletConfig getServletConfig()

Q12 Servlet的生命周期?

  • Web容器加载Servlet并将其实例化后,Servlet生命周期开始,容器运行其init()方法进行Servlet的初始化;

  • 请求到达时调用Servlet的service()方法,service()方法会根据需要调用与请求对应的doGetdoPost等方法;

  • 当服务器关闭或项目被卸载时服务器会将Servlet实例销毁,此时会调用Servlet的destroy()方法。

init方法和destory方法只会执行一次,service方法客户端每次请求Servlet都会执行。
Servlet中有时会用到一些需要初始化与销毁的资源。因此,可以把初始化资源的代码放入init方法中,销毁资源的代码放入destroy方法中,这样就不需要每次处理客户端的请求都要初始化与销毁资源。

  • Cookie 与 Session,一般认为是两个独立的东西,Session采用的是在服务器端保持状态的方案,而Cookie采用的是在客户端保持状态的方案。
  • 但为什么禁用Cookie就不能得到Session呢?因为Session是用Session ID来确定当前对话所对应的服务器Session,而Session ID是通过Cookie来传递的,禁用Cookie相当于失去了Session ID,也就得不到Session了。

假定用户关闭Cookie的情况下使用Session,其实现途径有以下几种:

  • 手动通过URL传值、隐藏表单传递Session ID
  • 用文件、数据库等形式保存Session ID,在跨页过程中手动调用。

H 源码案例

H.1 案例 HttpConnectionTest


import cn.johnnyzen.dataservice.common.utils.DatetimeUtil;
import com.alibaba.fastjson.JSONObject;
import lombok.SneakyThrows;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

/**
 * @author johnny-zen
 * @version v1.0
 * @create-time 2023/4/10 16:42
 * @description
 * @reference-doc
 *  [1] HTTP请求报错异常!java.net.SocketTimeoutException: Read timed out - CSDN - https://blog.csdn.net/weixin_44411569/article/details/100007013
 *  [2] Java利用 URLConnection发起Post请求并携带参数的方法 - CSDN - https://blog.csdn.net/m0_48324758/article/details/122860625
 *  [3] HttpUrlConnection使用简介 - CSDN - https://blog.csdn.net/lck_csdn/article/details/125800854
 *  [4] HttpURLConnection(http 1.1) 用法、状态码、状态描述 - bbsmax - https://www.bbsmax.com/A/RnJWDn3g5q/
 *  [5] Http——HttpURLConnection详解 - CSDN - https://blog.csdn.net/weixin_42602900/article/details/127264765
 *  [6] HTTP协议和URLConnection使用 - CSDN - https://blog.csdn.net/qq_42402854/article/details/108063928
 *  [7] Http持久连接与HttpClient连接池 - 博客园 - https://www.cnblogs.com/kingszelda/p/8988505.html
 */
public class HttpConnectionTest implements Runnable {
    private static final Logger logger = LoggerFactory.getLogger(HttpConnectionTest.class);

    private static final String CHARSET = "UTF-8";

    @Test
    public void test() throws IOException, InterruptedException {
        //new Thread(new HttpConnectionTest()).start();
        String contextUri = "http://127.0.0.1:9527";
        String apiUrl = contextUri + "/bdp/public/api/V2/bdp-data-service/100010/v1.0?nonce&timestamp&appKey&sign";

        URL url = new URL(apiUrl);
        /**
         * HttpURLConnection 是 HTTP 1.1 协议的实现
         * 原因:
         *  1) HTTP1.0定义了三种请求方法: GET, POST 和 HEAD方法。
         *  2) HTTP1.1新增了五种请求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法;默认支持持久连接
         *  3) HttpURLConnection (JDK 1.8)
         *      支持 OPTIONS, PUT, DELETE, TRACE 和 CONNECT
         *      默认支持持久连接(连接时长: 60S)
         */
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setRequestProperty("Content-Type", "application/json");
        connection.setRequestProperty("charset", "utf-8");

        /** Content-Disposition响应头 - CSDN - https://blog.csdn.net/CHS007chs/article/details/84140285 **/
        //connection.setRequestProperty("Content-Disposition", "inline");
        //connection.setRequestProperty("Content-Disposition", "inline;filename=f.txt");
        //connection.setRequestProperty("Content-Disposition", "attachment;");
        //connection.setRequestProperty("Content-Disposition", "attachment;filename=f.txt");
        //String fileName = "f";
        //connection.setRequestProperty("Content-Disposition", "form-data; name="+fileName+"; filename=" + fileName +".jpg");

        // 启用持久连接 [HTTP 1.1 默认支持]
        connection.setRequestProperty("Connection", "keep-alive");
        //connection.setRequestProperty("Authorization", "Bearer {YOUR_ACCESS_TOKEN}");

        // 设置连接超时时间 , 设置为0表示不超时 单位毫秒
        Integer connectionTimeout = 1000;
        Integer readTimeout = 1000;
        long sleepTime = 120000; // 单位: 毫秒
        connection.setConnectTimeout(connectionTimeout);
        // 设置读超时时间 , 设置为0表示不超时 单位毫秒
        connection.setReadTimeout(readTimeout);
        // JDK 1.5以前的版本,只能通过设置这两个系统属性来控制网络超时。
        System.setProperty("sun.net.client.defaultConnectTimeout", Integer.toString(connectionTimeout));
        System.setProperty("sun.net.client.defaultReadTimeout", Integer.toString(readTimeout));
        // 设置请求方法 (默认: GET)
        connection.setRequestMethod("POST");
        // Post 请求不能使用缓存
        connection.setUseCaches(false);
        // 是否允许重定向
        connection.setInstanceFollowRedirects(false);
        // 设置是否向 connection 输出 | 因为这个是 post 请求,参数要放在 http 正文内 => 需设为 true
        connection.setDoOutput(true);
        connection.setDoInput(true);

        //设置请求体
        String requestParams = "{ \"index\": 1, \"size\" : 10, \"params\": { \"startTime\": \"1667836800000\", \"endTime\": 1667923199999, \"top\": 10 } }";
        Map<String, Object> map = new HashMap<>(); //new Gson().fromJson( requestParams, Map.class);
        //map.put("key","value"); //key-value的形式设置请求参数
        map.put("index", new Integer(1));
        map.put("size", new Integer(10));
        Map<String, Object> dynamicParams = new HashMap<>();
        dynamicParams.put("startTime", 1667836800000L);
        dynamicParams.put("endTime", 1667923199999L);
        dynamicParams.put("top", 10);
        map.put("params", dynamicParams);
        String requestJsonData = JSONObject.toJSONString(map);

        /**
         * 建立连接
         * @description
         *  1. 所有的参数,必须在建立连接之前设置,否则会报如下错误:  java.lang.IllegalStateException: Already connected
         *  2. 调用打开连接, 调用此方法,只是建立一个连接,并不会发送数据
         */
        connection.connect();
        logger.info("success to build connection with server at {}", DatetimeUtil.longToString(System.currentTimeMillis(),"yyyy-MM-dd HH:mm:ss.SSS"));
        long startTime = System.currentTimeMillis();
        logger.info("thread sleep start at {}", DatetimeUtil.longToString(startTime,"yyyy-MM-dd HH:mm:ss.SSS"));
        //Thread.sleep(30000);
        while( (System.currentTimeMillis()-startTime) < sleepTime ){
            ;
        }
        logger.info("thread sleep end at {}", DatetimeUtil.longToString(System.currentTimeMillis(),"yyyy-MM-dd HH:mm:ss.SSS"));
        //发送请求数据,获取返回值
        String result = postRequest(connection, requestJsonData);
        logger.info("result: {}", result);

        //断开连接
        logger.info("prepare to disconnect with server at {}", DatetimeUtil.longToString(System.currentTimeMillis(),"yyyy-MM-dd HH:mm:ss.SSS"));
        //connection.disconnect();
    }

    /**
     * post 请求的方法重载
     * @description
     *  超时读取时: ProtocolException: Cannot write output after reading input.
     * @param connection
     * @param requestJsonData
     * @return
     */
    public static String postRequest(HttpURLConnection connection,String requestJsonData) throws IOException {
        try{
            StringBuffer buffer = new StringBuffer();
            byte[] bytes = requestJsonData.getBytes();
            // 获取 URLConnection 对象对应的输出流
            // PrintWriter outWriter = new PrintWriter(connection.getOutputStream());
            // outWriter.print(requestJsonData);
            // outWriter.flush();
            OutputStream outputStream = connection.getOutputStream();
            outputStream.write(bytes);
            // 刷新输出流的缓冲
            outputStream.flush();
            outputStream.close();

            // 获得响应状态码的返回值 responseCode
            int responseCode = connection.getResponseCode();
            logger.info("responseCode after build connection and send request data: {}", responseCode);
            if (responseCode == 200) { // 正常响应
//            String msg = "";
//            // 从流中读取响应信息
//            BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
//            String line = null;
//            while ((line = reader.readLine()) != null) { // 循环从流中读取
//                msg += line + "\n";
//            }
//            reader.close(); // 关闭流
                //将返回的输入流转换成字符串
                InputStream inputStream = connection.getInputStream();
                InputStreamReader inputStreamReader = new InputStreamReader(inputStream, CHARSET);
                BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
                String str = null;
                while ((str = bufferedReader.readLine()) != null) {
                    buffer.append(str);
                }
                String result = buffer.toString();
                return result;
            }
        }catch (Exception e){
            logger.error("postUrlConnection 出错",e);
        }
        return null;
    }

    /*请求 url 获取返回的内容*/
    public static String getRequest(HttpURLConnection connection) throws IOException {
        StringBuffer buffer = new StringBuffer();
        //将返回的输入流转换成字符串
        try(InputStream inputStream = connection.getInputStream();
            InputStreamReader inputStreamReader = new InputStreamReader(inputStream, CHARSET);
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);){
            String str = null;
            while ((str = bufferedReader.readLine()) != null) {
                buffer.append(str);
            }
            String result = buffer.toString();
            return result;
        }
    }

    @SneakyThrows
    @Override
    public void run() {
        logger.info("start");
    }
}

X 参考文献