vlambda博客
学习文章列表

Http以及TCP/IP、RPC等实践问题汇总

Http以及TCP/IP、RPC等实践问题汇总

我们从一个基本的问题开始:

现在开发人员一般都用过Netty框架,既然Netty是可以自启动的框架,为什么还要把应用程序打包成war等形式发布到tomcat中?

实际上这是两个问题。

Tomcat属于应用服务器还是web服务器,两者之间的区别是什么?

实际上现在这两个概念算是混淆使用,也没有大问题。


简单来说可参见上图。

应用程序选择Tomcat的优势是什么,为什么不直接使用Netty启动?

当然是因为Tomcat本身的功能,在运行一个应用层面具有非常大的优势:

  1. Tomcat本身已经实现了守护进程,不需要开发者使用Netty自己实现。

  2. Tomcat允许热部署(只要没有使用hibernate等不支持热部署的框架)

  3. Tomcat可以提供监控的 Admin UI,以及监控工具,帮助监控应用运行情况。

  4. Tomcat可以帮助管理数据源、JNDI等功能

  5. Tomcat可以通过配置化支持SSL等能力,如果使用Netty就需要自己配置开发这类与实际业务无关的代码

  6. Tomcat已经封装解决了TCP/IP的粘包等问题

使用Tomcat外部化很多技术工作,还有很多优势,最主要的是让开发人员从技术细节解脱出来,专业业务开发交付,而付出的代价仅仅是部署一个独立的tomcat进程而已。

另外,Netty等框架,虽然也能直接接收服务,但是其本质是一个通讯框架。

粘包、拆包问题:

1、什么是粘包/拆包一般所谓的TCP粘包是在一次接收数据不能完全地体现一个完整的消息数据。TCP通讯为何存在粘包呢?主要原因是TCP是以流的方式来处理数据,再加上网络上MTU的往往小于在应用处理的消息数据,所以就会引发一次接收的数据无法满足消息的需要,导致粘包的存在。处理粘包的唯一方法就是制定应用层的数据通讯协议,通过协议来规范现有接收的数据是否满足消息数据的需要2、解决办法2.1、消息定长,报文大小固定长度,不够空格补全,发送和接收方遵循相同的约定,这样即使粘包了通过接收方编程实现获取定长报文也能区分。2.2、包尾添加特殊分隔符,例如每条报文结束都添加回车换行符(例如FTP协议)或者指定特殊字符作为报文分隔符,接收方通过特殊分隔符切分报文区分。2.3、将消息分为消息头和消息体,消息头中包含表示信息的总长度(或者消息体长度)的字段

3、Netty中提供了FixedLengthFrameDecoder定长解码器可以帮助我们轻松实现第一种解决方案,定长解码报文。如果原始数据的长度不够定长,需要增加空格来达到定长。

4、加入FixedLengthFrameDecoder定长解码器  

4.1 在服务端,加入FixedLengthFrameDecoder定长解码器

下面我们进一步的去了解一下相关的知识。

1

工作必备基础概念回顾

五层模型

这个问题我们不再概述,一般的计算机网络书籍就已经介绍清楚,类下图:

Http以及TCP/IP、RPC等实践问题汇总


长链接

HTTP的长连接和短连接本质上是TCP长连接和短连接。

HTTP属于应用层协议,在传输层使用TCP协议,在网络层使用IP协议。IP协议主要解决网络路由和寻址问题,TCP协议主要解决如何在IP层之上可靠的传递数据包,使在网络上的另一端收到发端发出的所有包,并且顺序与发出顺序一致。TCP有可靠,面向连接的特点。

HTTP长连接(持久连接)是指,客户端和服务端建立一次连接之后,可以在这条连接上进行多次请求/响应操作。持久连接可以设置过期时间,也可以不设置。

Http以及TCP/IP、RPC等实践问题汇总


优点

(1)省时间:在多次通信中可以避免进行多次连接,从而节省了连接建立和关闭连接的开销,并且从总体上来看,进行多次数据传输的总耗时更少,减少网络阻塞的影响。

(2)当发生错误时,可以在不关闭连接的情况下进行提示

(3)节约资源:减少CPU及内存的使用,因为不需要经常的建立及关闭连接。

缺点

(1)连接数过多时,影响服务端的性能和并发数量。

(2)我们就需要担心各种问题:比如端对端连接的维护,连接的保活等。需要花费额外的精力来保持这个连接一直是可用的,因为网络抖动、服务器故障等都会导致这个连接不可用,甚至是由于防火墙的原因。

应用场景:高频请求的场景。高频、服务端主动推送和有状态

(1)数据库的连接就是采用TCP长连接.

(2)RPC,远程服务调用,在服务器,一个服务进程频繁调用另一个服务进程,可使用长连接,减少连接花费的时间。

一些监控或者实时报价类系统,比如股票软件,它需要在几秒之内刷新最新的价格。

心跳

既然是长连接,为了防止连接断开或者说判断连接是否依然存在,就需要有一种机制来进行判断,这种机制就是心跳机制(HeartBeat)。

心跳:就像人的心跳一样,人的心跳是用来续命的,通信中的心跳是用来检测一个系统是否存活或者网络链路是否通畅的一种方式,其一般做法是定时向被检测系统发送心跳包,被检测系统收到心跳包进行回复,收到回复说明对方存活。

心跳能够给长连接提供保活功能,能够检测长连接是否正常(这里所说的保活一方面可以理解为维持长连接,另一方面来说是一旦链路死了,不可用了,能够尽快知道,然后做些其他的高可用措施,来保证系统的正常运行)。

心跳包机制心跳包之所以叫心跳包是因为:它像心跳一样每隔固定时间发一次,以此来告诉服务器,这个客户端还活着。

事实上这是为了保持长连接,至于这个包的内容,是没有什么特别规定的,不过一般都是很小的包,或者只包含包头的一个空包。

在TCP的机制里面,本身是存在有心跳包的机制的,也就是TCP的选项:SO_KEEPALIVE。系统默认是设置的2小时的心跳频率。

但是它检查不到机器断电、网线拔出、防火墙这些断线。而且逻辑层处理断线可能也不是那么好处理。一般,如果只是用于保活还是可以的。心跳包一般来说都是在逻辑层发送空的echo包来实现的。下一个定时器,在一定时间间隔下发送一个空包给客户端,然后客户端反馈一个同样的空包回来,服务器如果在一定时间内收不到客户端发送过来的反馈包,那就只有认定说掉线了。其实,要判定掉线,只需要send或者recv一下,如果结果为零,则为掉线。但是,在长连接下,有可能很长一段时间都没有数据往来。理论上说,这个连接是一直保持连接的,但是实际情况中,如果中间节点出现什么故障是难以知道的。更要命的是,有的节点(防火墙)会自动把一定时间之内没有数据交互的连接给断掉。

在这个时候,就需要我们的心跳包了,用于维持长连接,保活。在获知了断线之后,服务器逻辑可能需要做一些事情,比如断线后的数据清理呀,重新连接呀……当然,这个自然是要由逻辑层根据需求去做了。总的来说,心跳包主要也就是用于长连接的保活和断线处理。一般的应用下,判定时间在30-40秒比较不错。如果实在要求高,那就在6-9秒。心跳检测步骤:1 客户端每隔一个时间间隔发生一个探测包给服务器2 客户端发包时启动一个超时定时器3 服务器端接收到检测包,应该回应一个包4 如果客户机收到服务器的应答包,则说明服务器正常,删除超时定时器5 如果客户端的超时定时器超时,依然没有收到应答包,则说明服务器挂了

对于心跳包的应用,一些SOCKET协议也有较多的应用。

Netty框架中,实现了心跳的插件,可以直接使用。

HTTP短连接(非持久连接)是指,客户端和服务端进行一次HTTP请求/响应之后,就关闭连接。所以,下一次的HTTP请求/响应操作就需要重新建立连接。

Http以及TCP/IP、RPC等实践问题汇总


优点:

(1)管理简单,不需要操心连接状态的管理。

(2)由于每次使用的连接都是新建的,所以基本上只要能够建立连接,数据就大概率能送达到对方。并且哪怕这次传输出现异常也不用担心影响后续新的数据传输,因为届时又是一个新的连接。

缺点

(1)频繁的连接就会浪费时间:每个连接都需要经过三次握手和四次握手的过程,耗时大大增加。

(2)致命的缺点:短时间内的大量连接导致端口数不够用。

然而一台计算机最多只能开启 65535 个端口,如果现在两个进程之间需要通信,作为服务端的 IP 和端口必然是固定的,因此单个客户端理论上最多只能与服务端同时建立 65535 个 socket 连接。如果除去操作系统和其它进程所占用的端口,实际还会更少。所以,一旦使用不当,在很短的时间内建立了大量连接,端口很容易被占用完。这不但会导致自身无法正常工作,还会影响到同一台计算机上的其它进程。我们在项目中大多数情况使用的是短连接的方式,因为这对我们编程来说可以少考虑很多问题,潜在的这些缺点可能是你没有遇到或者意识到而已。

使用场景并发量大,请求频率低的场景:低频、无状态

通常浏览器访问服务器的时候就是短连接。

对于服务端来说,长连接会耗费服务端的资源,而且用户用浏览器访问服务端相对而言不是很频繁的如果有几十万,上百万的连接,服务端的压力会非常大,甚至会崩溃。所以对于并发量大,请求频率低的,建议使用短连接。

Http中的长链接、短链接

Http1.1协议中,对于连接的参数设置如下:

设置HTTP短连接在首部字段中设置 Connection:close ,则在一次请求/响应之后,就会关闭连接。

设置HTTP长连接,有过期时间在首部字段中设置 Connection:keep-alive 和 Keep-Alive: timeout=60 ,表明连接建立之后,空闲时间超过60秒之后,就会失效。如果在空闲第58秒时,再次使用此连接,则连接仍然有效,使用完之后,重新计数,空闲60秒之后过期。

设置HTTP长连接,无过期时间在首部字段中只设置 Connection:keep-alive ,表明连接永久有效。

一个非常重要点:connection字段只有服务端设置才有效

尤其是使用HttpClient这个开源包的同学要注意。

Tomcat中的短连接与长连接

我们刚才说,长连接、短连接等内容,实际上是技术层面的内容,最好交给中间件去解决。

在Tomcat中,怎么配置?

短连接配置:

<!-- tomcat 1w 并发测试 短连接-->

<Connector port="8080" protocol="HTTP/1.1"

connectionTimeout="5000"

maxThreads="10000"

minSpareThreads="100"

maxSpareThreads="10000"

acceptCount="5000"

URIEncoding="UTF-8"

redirectPort="8443" />


长连接配置:

<!-- bio keepAliveTimeout 长连接使用时间 maxKeepAliveRequests 长连接使用格式 1 表示禁用 -1 表示不限制 一般100-200 enableLookups 是否禁用dns查询 dns查询耗费网络 -->

<Connector port="8080" protocol="HTTP/1.1"

connectionTimeout="20000"

URIEncoding="UTF-8"

redirectPort="8443"

maxKeepAliveRequests="-1"

keepAliveTimeout="15000"

enableLookups="false" />

socket连接和TCP连接

首先,我们再回忆一下SOCKET套接字的基本概念。

套接字(socket)是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。

应用层通过传输层进行数据通信时,TCP会遇到同时为多个应用程序进程提供并发服务的问题。

多个TCP连接或多个应用程序进程可能需要通过同一个 TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了套接字(Socket)接口。

应用层可以和传输层通过Socket接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。

其次,建立socket连接。

建立Socket连接至少需要一对套接字,其中一个运行于客户端,称为ClientSocket ,另一个运行于服务器端,称为ServerSocket 。

套接字之间的连接过程分为三个步骤:服务器监听,客户端请求,连接确认。

服务器监听:服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态,等待客户端的连接请求。

连接确认:当服务器端套接字监听到或者说接收到客户端套接字的连接请求时,就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,双方就正式建立连接。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。

第三,创建Socket连接时,可以指定使用的传输层协议,Socket可以支持不同的传输层协议(TCP或UDP),当使用TCP协议进行连接时,该Socket连接就是一个TCP连接。

第四,socket连接与HTTP连接。

由于通常情况下Socket连接就是TCP连接,因此Socket连接一旦建立,通信双方即可开始相互发送数据内容,直到双方连接断开。但在实际网络应用中,客户端到服务器之间的通信往往需要穿越多个中间节点,例如路由器、网关、防火墙等,大部分防火墙默认会关闭长时间处于非活跃状态的连接而导致 Socket 连接断连,因此需要通过轮询告诉网络,该连接处于活跃状态。

而HTTP连接使用的是“请求—响应”的方式,不仅在请求时需要先建立连接,而且需要客户端向服务器发出请求后,服务器端才能回复数据。

很多情况下,需要服务器端主动向客户端推送数据,保持客户端与服务器数据的实时与同步。此时若双方建立的是Socket连接,服务器就可以直接将数据传送给客户端;

若双方建立的是HTTP连接,则服务器需要等到客户端发送一次请求后才能将数据传回给客户端,因此,客户端定时向服务器端发送连接请求,不仅可以保持在线,同时也是在“询问”服务器是否有新的数据,如果有就将数据传给客户端。

我们回忆一下,网络传输的基本流程:

Http以及TCP/IP、RPC等实践问题汇总


1)Socket是一个针对TCP和UDP编程的接口,你可以借助它建立TCP连接等等。而TCP和UDP协议属于传输层 。而http是个应用层的协议,它实际上也建立在TCP协议之上。(HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。

2)Socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用TCP/IP协议。Socket的出现只是使得程序员更方便地使用TCP/IP协议栈而已,是对TCP/IP协议的抽象,从而形成了我们知道的一些最基本的函数接口。

Linux服务器最大连接数是65535吗?

问题有一个概念上的误解,错误的把TCP端口号的上限65535理解成了TCP连接数的上限,进而认为Linux无法实现超过65,535个的并发任务,实际上端口号数量和TCP连接数确实有关联,但并非一一对应的关系。

在Linux系统中,表示端口号(port)的变量占16位,这就决定了端口号最多有2的16次方个,即65,536个,另外端口0有特殊含义不给使用,这样每个服务器最多就有65,535个端口可用。

因此,65,535代表Linux系统支持的TCP端口号数量,在TCP建立连接时会使用。

Linux服务器在交互时,一般有两种身份:客户端或者服务器端。

典型的交互场景是:

(1)服务器端主动创建监听的socket,并绑定对外服务端口port,然后开始监听

(2)客户端想跟服务器端通信时,就开始连接服务器的端口port

(3)服务端接受客户端的请求,然后再生成新的socket

(4)服务器和客户端在新的socket里进行通信可以看到,端口port主要用在服务器和客户端的“握手认识”过程,一旦互相认识了,就会生成的的socket进行通信,这时候port就不再需要了,可以给别的socket通信去使用,所以很明显TCP连接的数量可以大于TCP端口号的数量65,535。

事实上,真正影响TCP连接数量的,是服务器的内存以及允许单一进程同时打开文件的数量,因为每创建一个TCP连接都要创建一个socket句柄,每个socket句柄都占用一部分系统内存,当系统内存被占用殆尽,允许的TCP并发连接数也就到了上限。

一般来讲,通过增加服务器内存、修改最大文件描述符个数等,可以做到单台服务器支持10万+的TCP并发。

单工、半双工和双工通信

一、单工通信(simplex)

单工通信只支持信号在一个方向上传输(正向或反向),任何时候不能改变信号的传输方向。为保证正确传送数据信号,接收端要对接收的数据进行校验,若校验出错,则通过监控信道发送请求重发的信号。

二、半双工通信(half-duplex)半双工通信允许信号在两个方向上传输,但某一时刻只允许信号在一个信道上单向传输。

因此,半双工通信实际上是一种可切换方向的单工通信。此种方式适用于问讯、检索、科学计算等数据通信系统;传统的对讲机使用的就是半双工通信方式。由于对讲机传送及接收使用相同的频率,不允许同时进行。因此一方讲完后,需设法告知另一方讲话结束(例如讲完后加上’OVER’),另一方才知道可以开始讲话。

三、全双工(full-duplex)

Http以及TCP/IP、RPC等实践问题汇总


3次握手与4次挥手

3次握手:

Http以及TCP/IP、RPC等实践问题汇总


第一次握手:建立连接时,客户端发送syn包(syn=x)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。

第二次握手:服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(syn=y),即SYN+ACK包,此时服务器进入SYN_RECV状态;

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。

4次挥手:

Http以及TCP/IP、RPC等实践问题汇总


1)客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。

2)服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。

3)客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。

4)服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。

5)客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。

6)服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。

2

Http Post请求相关问题

Http以及TCP/IP、RPC等实践问题汇总


双向认证

双向认证和单向认证原理基本差不多,只是除了客户端需要认证服务端以外,增加了服务端对客户端的认证。 

(1)客户端向服务端发送SSL协议版本号、加密算法种类、随机数等信息。 

(2)服务端给客户端返回SSL协议版本号、加密算法种类、随机数等信息,同时也返回服务器端的证书,即公钥证书。 

(3)客户端使用服务端返回的信息验证服务器的合法性,包括:    证书是否过期     发型服务器证书的CA是否可靠     返回的公钥是否能正确解开返回证书中的数字签名     服务器证书上的域名是否和服务器的实际域名相匹配     验证通过后,将继续进行通信,否则,终止通信  

(4)服务端要求客户端发送客户端的证书,客户端会将自己的证书发送至服务端

(5)验证客户端的证书,通过验证后,会获得客户端的公钥  

(6)客户端向服务端发送自己所能支持的对称加密方案,供服务器端进行选择

(7)服务器端在客户端提供的加密方案中选择加密程度最高的加密方式

(8)将加密方案通过使用之前获取到的公钥进行加密,返回给客户端

(9)客户端收到服务端返回的加密方案密文后,使用自己的私钥进行解密,获取具体加密方式,而后,产生该加密方式的随机码,用作加密过程中的密钥,使用之前从服务端证书中获取到的公钥进行加密后,发送给服务端  

(10)服务端收到客户端发送的消息后,使用自己的私钥进行解密,获取对称加密的密钥,在接下来的会话中,服务器和客户端将会使用该密码进行对称加密,保证通信过程中信息的安全。

遇到的一个问题HTTP POST参数丢失数据问题

通过post form表单提交的请求,在body大于2M的情况下,某些参数字段值在server端获取不到。

Tomcat的maxpostsize配置问题,默认是2M,body大了会随即丢失内容。

在server.xml中:

<Connector port="8080" protocol="HTTP/1.1" 
          connectionTimeout="2000"
          redirectPort="8443"
          URIEncoding="UTF-8"
          maxThreads="3000"
          compression="on" compressableMimeType="text/html,text/xml"
          maxPostSize="10240"/>

其中参数maxPostSize="10240"是限制post请求参数的大小,将值改为0代表不限制。

Http chunked,以及遇到的生产问题

Http以及TCP/IP、RPC等实践问题汇总


需要在报文头中设置报文长度,即可处理。

3

概念辨析

正向代理、反向代理

一、正向代理(Forward Proxy)


正向代理(forward)是一个位于客户端【用户A】和原始服务器(origin server)【服务器B】之间的服务器【代理服务器Z】,为了从原始服务器取得内容,用户A向代理服务器Z发送一个请求并指定目标(服务器B),然后代理服务器Z向服务器B转交请求并将获得的内容返回给客户端。

客户端必须要进行一些特别的设置才能使用正向代理,例如指定代理的IP/PORT。

二、反向代理(reverse proxy)

反向代理正好与正向代理相反,对于客户端而言代理服务器就像是原始服务器,并且客户端不需要进行任何特别的设置。客户端向反向代理的命名空间(name-space)中的内容发送普通请求,接着反向代理将判断向何处(原始服务器)转交请求,并将获得的内容返回给客户端。



反向代理的话,客户端是感觉不到的,直接访问反向代理即可。

4

Apache Httpclient 与 Netty

NoHttpResponseException

A系统调用B系统的时候,规律性的出现失败,发现了NoHttpResponseException。

首先是按照官网说法,查询对方服务是否过载,但是并没有,然后找网络人员进行网络抓包,但是说找不到请求报文。

一个初步的更改方案是对NoHttpResponseException异常进行捕获,然后进行重试。

但是这种治标不治本。

安装wireshark进行抓包,发现建立TCP连接之后,第一次发送数据结束,server方就主动发起了FIN断开连接申请。

抓包发现,返回的Http头中支持keep-alive,而A平台使用的HttpClient会根据这个响应,来确定是否建立长链接。

这样就导致,当server段发起了关闭TCP连接的请求FIN后,客户端这边还有数据发送,所以没有发出FIN请求,导致HttpClient的连接池中的连接,还没有关闭,还在继续发送,但这样的话,招标通发来了 RST 指令,说明连接异常结束,这样的话,导致客户端报错NoHttpResponseException。

解决方案:

1)服务端,既然不支持长连接,一定要保证http响应头中不要返回keep alive。

2)客户端,对NoHttpResponseException的异常进行捕获,进行重试处理