Java Nio(六)Java Nio实现高性能HTTP/HTTPS客户端

由以前的文章得知,http本质上是TCP连接,然后发送http约定的字节。既然是TCP那么TCP的一些限制也会影响http的性能,接下来介绍下我们访问网络的一些流程。

网络访问流程

IPv4/6

  我们现在常使用的IP地址是IPv4地址,由四组0-255的十进制数字组成,中间以小数点分隔。Internet上的每一台主机或者路由器都至少有一个IP地址。IP地址(IPv4地址,下文IP地址默认指IPv4)的长度是32位。被写成4组,每组8位,总数为2的32次方,大约43亿个。43亿IP地址假如全球每人分配一个IP就需要75亿。

  IPv6地址的二进制位数是128位,这128位地址通常被写成8组,每组为4个十六进制数的形式。ipv6是互联网协议的第六版,是下一代互联网协议。IPv6采用128位地址长度,其地址数量总数可达2的128次方个,当然这个还未普及。

  为了快速解决IP地址匮乏的这个问题,NAT技术诞生了。

NAT

  中文名唤作网络地址转换,诞生于IP地址匮乏的时代。

  NAT的基本思想是ISP(Internet服务提供商)为每个家庭或者公司分配一个IP地址,这个IP地址用作Internet流量的传输,也就是大家常说的外网IP地址或者公网IP地址。在客户网络的内部,每台计算机有唯一一个IP地址,即内网IP地址,这些地址主要用于路由内部流量。当一个数据包离开客户网络发送至其他ISP时,需要进行地址转换,把唯一的内网IP地址转换成外网的IP地址。

  这种地址转化使用IP地址的三个范围,这些地址已被声明私有化,任何内网中的设备可以任意使用这些地址,但是在这三个范围内的IP地址不允许出现在Internet(外网)上,这三个保留的地址范围是:
  10.0.0.0~10.255.255.255/8

  172.16.0.0~172.31.255.255/12

  192.168.0.0~192.168.255.255/16

  他们分别可以容纳16777216、1048576、65536台主机。一般家里用无线路由器,就用到了网络地址转换技术,我们连上wifi后分配的IP地址一般是以172或192为开头。学校或者大企业里面的网络可能会用到10开头的地址范围。

  NAT将内网外网划分好之后,是如何使内网的设备访问外网的呢?

  如下图,当计算机A在内网(假设IP为10.0.0.1)想去访问一个Internet上的网站S(假设IP地址为54.223.189.245)时,A的数据包需要先经过一个NAT盒子(NAT box),这个盒子先将A的IP源地址转换成外网的真实IP地址(假设IP为121.0.0.2),然后将转换后的数据包发送至Internet。

Java Nio(六)Java Nio实现高性能HTTP/HTTPS客户端

  于是问题来了,当网站S收到这个数据包后,会处理请求,并发送响应的数据包,然而这个数据包的目标地址是121.0.0.2(外网IP),数据包如何返回内网中的A呢?

  

  这里要先介绍一下源端口(Source Port)和目标端口(Destination Port)的概念。当一个进程希望与另一个进程建立TCP连接时,它把自己绑定到一个本机尚未被占用的TCP端口上,这个端口称为源端口,该TCP连接中所有入境的数据包都要被发送至这个端口。同时进程还需要提供一个目标端口,指明数据包到达远程主机后送至哪一个端口处理。每一个出境的TCP数据包都包括一个源端口和目标端口。

  举例来说,如下图,网站服务器S(IP地址为54.223.189.245)的HTTP服务运行在80端口上,公网上的计算机D(IP地址为121.141.56.23)想去访问网站S,于是把自己绑定到本机的33121端口上,并发送请求的数据包,这个数据包中就包含了计算机D的源端口33121和目标端口80。网站S收到请求后,发送响应的数据包,这个数据包中包含了服务器S的源端口80和目标端口3312。

Java Nio(六)Java Nio实现高性能HTTP/HTTPS客户端

  上面的例子是外网中的一台计算机访问一个网站。在内网中计算机发送的数据包同样存在着源端口和目标端口。NAT盒子做的事情就是对出入境数据包的端口进行修改。

  回到最初举的例子,假设内网计算机A(IP地址为10.0.0.1)发送的请求包的源端口是45421,目标端口是80,请求访问网站服务器S(IP地址为54.223.189.245)。

  当这个出境数据包经过NAT盒子时,其源地址被修改成公网的真实IP(121.0.0.2),源端口被修改一个索引值(假设为50002),这个索引值指向NAT盒子的地址转化表中的某一项,这一表项保存了计算机A的内网源地址和源端口。最后NAT盒子将重新生成的数据包发送出去。

  当网站S响应的入境数据包到达NAT盒子时,数据包经过处理,目的地址由公网IP(121.0.0.2)还原为计算机A的内网IP(10.0.0.1),目标端口由索引值(50002)还原为计算机A的源端口(45421)。还原后数据包可以正常的在内网路由。

  这个过程基本如下图所示。

Java Nio(六)Java Nio实现高性能HTTP/HTTPS客户端

网络访问的限制

  由上面可以看出,我们所有的内网ip都会映射到同一个外网ip,所有内网机器对外网的访问都要通过同一个外网ip。对于同一个外网ip来说,端口、socket存储等都是有限的,如果同一时间我们的请求越多,建立的TCP连接和NAT需要的资源也就越多,那么NAT的压力也越大,致使连接没有在规定的时间内收发数据,导致服务器断开连接,大量的请求超时或者其他异常。

提高性能的方法

1.连接池

  既然连接多会导致性能降低,那我们构造一个连接池不就得了。构造一个池子,维护连接数量,如果连接不足就创建连接,如果连接满了,就删除一个空闲连接然后再创建连接(删除策略可以优先删除最早的连接或者使用LRU策略),如果没有空闲连接就把请求添加到一个数组里面等待有空闲连接,保证连接数在一个上限下面。

2.复用连接  

  从HTTP/1.1起,默认开启了Keep-Alive,保持连接特性,客户端和服务器都能选择随时关闭连接,则请求头中为connection:close。简单地说,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的TCP连接。但是Keep-Alive不会永久保持连接,它有一个保持时间。我们可以用这个特性来实现连接复用(毕竟TCP三次握手也挺费时间的,尤其是https,ssl握手交换密钥需要的时间会更长)。只需要在上面的连接池里面构造一个以host和端口位key,连接数组为value的map即可,每次使用连接的时候先取同一目标地址和目标端口的空闲连接,取到后直接发送请求数据。如果没有再根据1里面的策略进行选取。

3.压缩算法

  访问那些返回的数据量大的地址时候,数据传输就成了影响性能的大头,毕竟大量的数据传输也需要很长时间,这个时候我们就可以通过压缩数据来将大数据变成小数据。比如前几篇文章提到过的gzip算法等。

 

具体的实现代码比较多,就不在这里写了,感兴趣的可以去github上 https://github.com/cxsummer/net-nio 看。

上一篇:亚马逊Listing变“狗”怎么办?这7大解决方案仅供参考!


下一篇:关于代码审查的一些探讨和总结