远程通信技术重点是通信方式、序列化协议和透明化RPC协议。
2.1 通信方式
OSI(Open System Interconnection)开放系统互联: 定义了了不同计算机之间实现互联的标准,是网络通信的基本框架。
ISO:国际标准化组织。
OSI复杂,没有TCP/IP模型引用广泛。TCP/IP可以看做是OSI模型的浓缩版本,它将OSI模型的七层抽象为四层。
2.1.1 通信协议
应用层(应用层+表示层+会话层):HTTP, FTTP, Telnet, NTP, DHCP, PING
传输层(传输层):TCP UDP
网络层(网络层):IP, ARP, ICMP, IGMP
网络接口层(数据链路层 + 物理层):Ethernet
传输层协议:
1 TCP(Transportation Control Protocol)传输控制协议。面向连接的协议(因为显示的方式确认连接的创建和终止),提供可靠的双向字节流。TCP通过三次握手的连接创建机制确保连接的可靠性。
TCP的开销高于UDP,性能低于UDP,但是可以保证数据的正确性、顺序性和不可重复性。对于业务间的通信而言,TCP是更适合选择。
Socket:通过IP和一个端口确定一个网络环境中唯一的通信句柄。 socket是TCP/IP协议的API。
Java的Socket编程API封装了TCP的三次握手。
我们编程不会直接去调用tcp
udp
,而是通过他们封装好的接口socket
去通信。可以说,现在几乎网络上所有的通信,底层都是通 过socket
完成的,一切皆Socket
。
java Socket 可以实现TCP也可以实现UDP通信
2 UDP(User Datagram Protocol)用户数据报文协议
UDP是一种无连接,面向数据报文的协议。每一个数据报文都是一个独立的信息,包括完整的源地址或目的地址,数据报文是否能到达目的地,到达的时间以及传送的内容是否正确,这些都不能保证。
UDP不要求通信时保持统一的连接,不需要建立连接。
UDP可能产生网络丢包,并且无法保证传输的原有顺序,但在性能方面更有优势,更加适用允许数据被部分丢弃的业务场景,如视频会
议流等。
Java使用DatagramSocket用于开发UDP。
应用层协议:
包含所有的高层协议,如FTP,SMTP,DNS和HTTP
1 HTTP
无连接(这个无连接是指每次连接只处理一个请求。)
无状态的:协议对于业务事务处理没有记忆能力。
2 长连接与短连接
HTTP的长连接和短连接本质上是TCP长连接和短连接。
HTTP属于应用层协议,在传输层使用TCP协议,在网络层使用IP协议。
IP协议主要解决网络路由和寻址问题,TCP协议主要解决如何在IP层之上可靠的传递数据包,使在网络上的另一端收到发端发出的
所有包,并且顺序与发出顺序一致。
TCP有可靠,面向连接的特点。
在HTTP/1.0中,默认使用的是短连接
从 HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头有加入这行代码:
Connection:keep-alive |
在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的 TCP连接不会关闭,如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。实现长连接要客户端和服务端都支持长连接。
HTTP协议的长连接和短连接,实质上是TCP协议的长连接和短连接。
长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况,。每个TCP连接都需要三步握手,这需要时间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多,所以每个操作完后都不断开,次处理时直接发送数据包就OK了,不用建立TCP连接。例如:数据库的连接用长连接, 如果用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费。
而像WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源,如果用长连接,而且同时有成千上万的用户,如果每个用户都占用一个连接的话,那可想而知吧。所以并发量大,但每个用户无需频繁操作情况下需用短连好。 HTTP分为长连接和短连接,其实本质上是说的TCP连接。TCP连接是一个双向的通道,它是可以保持一段时间不关闭的,因此TCP连接才有真正的长连接和短连接这一说。HTTP协议说到底是应用层的协议,而TCP才是真正的传输层协议,只有负责传输的这一层才需要建立连接。长连接,短连接都是指的传输层的TCP连接,而不是应用层的HTTP协议。HTTP属于应用层协议,在传输层使用TCP协议,在网络层使用IP协议。IP协议主要解决网络路由和寻址问题,TCP协议主要解决如何在IP层之上可靠的传递数据包,使在网络上的另一端收到发端发出的所有包,并且顺序与发出顺序一致。TCP有可靠,面向连接的特点。使用长连接的HTTP协议,会在响应头有加入这行代码:
Connection:keep-alive 服务器和客户端都要设置。
在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的 TCP连接不会关闭,如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,
它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。
在实际使用中,HTTP头部有了Keep-Alive这个值并不代表一定会使用长连接,客户端和服务器端都可以无视这个值
Keep-Alive: timeout=20,表示这个TCP通道可以保持20秒。另外还可能有max=XXX,表示这个长连接最多接收XXX次请求就断开。
对于客户端来说,如果服务器没有告诉客户端超时时间也没关系,服务端可能主动发起四次握手断开TCP连接,客户端能够知道该TCP连接已经无效;
另外TCP还有心跳包来检测当前连接是否还活着,方法很多,避免浪费资源。
HTTP首部的Connection: Keep-alive是HTTP1.0浏览器和服务器的实验性扩展,当前的HTTP1.1 RFC2616文档没有对它做说明,因为它所需要的功能已经默认开启,无须带着它,但是实践中可以发现,浏览器的报文请求都会带上它。如果HTTP1.1版本的HTTP请求报文不希望使用长连接,则要在HTTP请求报文首部加上Connection: close。
2.1.2 I/O模型
对于高性能、高并发的应用系统来说,如何回避I/O瓶颈从而提升性能,这一点至关重要。
一般,I/O模型可以分为阻塞与非阻塞、同步与异步。
1.老王用水壶煮水,并且站在那里,不管水开没开,每隔一定时间看看水开了没。同步阻塞
老王想了想,这种方法不够聪明。
2.老王还是用水壶煮水,不再傻傻的站在那里看水开,跑去寝室上网,但是还是会每隔一段时间过来看看水开了没有,
水没有开就走人。同步非阻塞
3.老王这次使用高大上的响水壶来煮水,站在那里,但是不会再每隔一段时间去看水开,而是等水开了,水壶会自动的通知他。异步阻塞
老王想了想,不会呀,既然水壶可以通知我,那我为什么还要傻傻的站在那里等呢,嗯,得换个方法。
4.老王还是使用响水壶煮水,跑到客厅上网去,等着响水壶自己把水煮熟了以后通知他。异步非阻塞
通过以上例子发现: 同步异步,在于是轮询查看response,还是response主动回调; 阻塞和非阻塞关键是看是否让出CPU去干别的事。
2.1.3 Java中的I/O
IO不仅仅针对文件的操作,网络编程socket的通信,也是IO操作。
Java对于I/O的封装分为BIO、NIO和AIO。 有很多第三方的I/O处理框架,如Netty, Mina等。
BIO:阻塞读取直到完成,不会缓存,适用于连接数少并发不高的场景。
NIO:性能提升明显。适用于连接数目多 连接比较短的轻量级操作架构。
NIO通过事件模型的异步机制,
一些核心概念:Buffer 是读取或写入的数据的缓冲区。
Channel 双向数据读写通道。
Selector通过轮询注册在其上Channel来选择并分发已处理就绪的事件。
AIO:和NIO类似,比NIO晚,没有性能提升,不普及。
2.2 序列化
希望在JVM停止运行后持久化保持对象。就需要把内存对象转化成一定的格式化数据,这个过程就是序列化。
JVM重启后,读取数据并重新转化为对象的过程就是反序列化。如果要将对象通过网络传输,同样需要序列化/反序列化。
序列化协议是一种结构化数据格式。
序列化的衡量指标: 序列化后字节占的大小; 序列化和反序列化的性能;序列化工具自身的性能。
2.2.1 文本序列化
JSON和XML这两种格式。
JSON格式配置简单,文本所占空间更小,而且对于JavaScript支持更加好,一种轻量级数据交换格式,独立于编程语言的文本格式来存储和表示数据。
虽然JavaScript可以无障碍将JSON文本和对象转换,但是同样的操作无法在java中实现,因此有很多第三方处理JSON的序列化框架。
流式解析JSON序列化框架Jackson: 流式增量的处理方式解析JSON,因此在解析数据量较大的JSON文本时有优势。
轻量级JSON序列化框架Gson:Google开源的框架。
FasTJSON:阿里开源的高性能。
2.2.2 二进制Java序列化
基于JSON或XML的文本序列化方式简单清晰,并且文本传输对于异构语言有天然优势。
但是文本格式未经压缩,其内容所占的空间较大,解析较慢。所以对于性能要求高的场景,二进制序列化更加受青睐。
对于用JAVA开发的同构系统,仅针对java的二进制序列化方式可以很好的和Java语言结合,给开发工作带来很大的便利。
Java原生序列化:
只要一个类实现了java.io.Serializable接口,那么这个类就可以序列化。使用java对象序列化保存对象时,会将对象状态转化为字节数组,
当某个字段被声明为transient后,序列化机制会忽略该字段。并且序列化不会保存静态变量,只会保存对象的成员变量。
java原生序列化使用serialVersionUID来控制兼容性(怎么控制的呢?如果不指定这个值,java运行时会自动生成,修改源码再重新编译的话,这个值会变化,导致序列化反序列化时抛出异常,这样就导致不兼容的问题;如果希望可以兼容,就可以自己显式的指定这个值。)
(补充: public interface Serializable {} // 是一个空接口。
类通过实现java.io.Serializable接口可以启用其序列化功能。未实现其接口的类无法使其任何状态序列化或反序列化。可序列化类
的所有子类型本身都是可序列化的。序列化接口没有方法或字段,仅用于标识可序列化的语义。
Java的"对象序列化"能让你将一个实现了Serializable接口的对象转换成byte流,这样日后要用这个对象时候,你就能把这些byte 数据恢复出来,并据此重新构建那个对象了。
要想序列化对象,你必须先创建一个OutputStream,然后把它嵌进ObjectOutputStream。这时,你就能用writeObject()方法把对 象写入OutputStream了。
)
Java原生序列化方式虽然简单,但是性能是“灾难”,而且序列化后的字节过于臃肿,不方便网络传输。因此不是理想方案。
高性能序列化框架Kyro:
由于java原生的序列化方案性能低下,出现了很多第三方序列化框架。
2.2.3 二进制异构语言序列化
Google开源的高性能异构语言序列化框架-----Protobuf
(在那些浏览器和JavaScript并不直接消耗数据的服务,Protobuf比JSON更高效)
Protocol Buffers在内部服务的数据传输方面和JSON相比具有多项优势。虽然不能完全取代JSON,尤其对于浏览器需要直接面向服务数据的时候,但是Protocol Buffers在其他方面比如像编解码速度、传输数据所占空间大小等都具有更好的表现。
2.3 远程调用(RPC)
允许程序调用其他服务器的方法或函数。客户端应用可以像调用本地方法一样直接调用另一台服务器上的服务端应用的对象方法。
2.3.1 核心概念
客户端和服务端需要互相熟悉相同的业务方法接口。服务端需要将远程接口导出给客户端,客户端需要将该接口导入,这样客户端才能像调用本地方法一样调用远程方法。对于一个完整的RPC调用,核心是通信、序列化和透明化调用。可以使用Netty和Kryo可以实现RPC框架。
2.3.2 Java远程方法调用
远程方法调用(Remote method Invocaton, RMI) 是最初用于实现透明远程调用的重要组成部分。它能让
开发RMI的步骤:开发服务接口(继承Remote接口)-> 实现服务的业务逻辑() -> 发布服务 -> 客户端使用服务
局限性:性能低(阻塞IO,java原生序列化),灵活性差(客户和服务端直接建立连接,没有分布式中的负载均衡,熔断,限流等,不适合分布式系统),缺乏对异构语言从支持(两端都是java)。
2.3.3 异构语言RPC框架gRPC
在跨语言的RPC解决方案中,RESTFUL API是热门的选择,但未必是互联网高并发下的合理选择。
gRPC采用更高效的序列化方式(Proto协议),并且跨语言。性能高。
总结: 远程调用框架除了性能好,支持跨语言这两点,还有能够将服务治理与远程调用集中于一体的框架,比如Dubbo。
开发者可以根据自己需要定制不同的通信协议与序列化协议搭配的方案。
对于仅使用java的开发来说,RMI已经不再适用,可以选择Dubbo和RESTFUL API。
Dubbo的远程通信性能极高,支持多种通信和序列化协议,还有服务治理的能力。
基于HTTP的RESTful API在性能上逊色,但是调试和适配能力出色。
对于一般的需求来说,适用Dubbo有些“重”,而RESTful API性能不足,因此采用Netty与Kryo的通信和序列化组合自行实现的框架也是不错的选择。
对于跨语言的场景,RESTful API依然是可行选择之一。gRPC在性能上更加出色。