网络通信编程基本常识
在开发过程中,如果类的名字有 Server 或者 ServerSocket 的,表示这个类是给服务端容纳网络 服务用的,如果类的名字只包含 Socket 的,那么表示这是负责具体的网络读写的。
ServerSocket 并不负责具体的网络读写,ServerSocket 就只是负责接收客户端连接
后,新启一个 socket 来和客户端进行沟通。这一点对所有模式的通信编程都是适用的。
在通信编程里,我们关注的其实也就是三个事情:
连接
(客户端连接服务器,服务器等
待和接收连接)、
读网络数据
、
写网络数据
,所有模式的通信编程都是围绕着这三件事情进
行的。
服务端提供
IP
和监听端口,客户端通过连接操作向服务端监听的地址发起连接请求,
通过三次握手连接,如果连接成功建立,双方就可以通过套接字进行通信。
我们后面将学习的 BIO
和
NIO
其实都是处理上面三件事,只是处理的方式不一样。
什么是 Socket?
Socket 是应用层与 TCP/IP 协议族通信的中间软件抽象层,它是一组接口,一般由操作
系统提供。
在设计模式中,
Socket 其实就是一个门面模式
,它把
复杂的 TCP/IP 协议处理
和
通信缓存管理
等等都隐藏在 Socket
接口后面,对用户来说,使用一组简单的接口就能进行
网络应用编程,让
Socket
去组织数据,以符合指定的协议。主机
A
的应用程序要能和主机
B
的
应用程序通信,必须通过
Socket
建立连接。
客户端连接上一个服务端,就会在客户端中产生一个 socket
接口实例,服务端每接受
一个客户端连接,就会产生一个
socket
接口实例和客户端的
socket
进行通信,有多个客户
端连接自然就有多个
socket
接口实例。
短连接
连接
->
传输数据
->
关闭连接
传统
HTTP
是无状态的,浏览器和服务器每进行一次
HTTP
操作,就建立一次连接,但任
务结束就中断连接。
也可以这样说:
短连接是指 SOCKET 连接后发送后接收完数据后马上断开连接。
长连接
连接
->
传输数据
->
保持连接
->
传输数据
->
。。。
->
关闭连接。
长连接指建立 SOCKET 连接后不管是否使用都保持连接。
什么时候用长连接,短连接?
长连接多用于操作频繁,点对点的通讯
。每个 TCP
连接都需要三步握手,这需要时
间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多,所以每个操作完后都
不断开,下次处理时直接发送数据包就
OK
了,不用建立
TCP
连接。例如:数据库的连接用
长连接, 如果用短连接频繁的通信会造成
socket
错误,而且频繁的
socket
创建也是对资源
的浪费。
而像 WEB 网站的 http 服务按照 Http 协议规范早期一般都用短链接
,
因为长连接对于
服务端来说会耗费一定的资源,而像 WEB 网站这么频繁的成千上万甚至上亿客户端的连接
用短连接会更省一些资源。
但是现在的
Http
协议,
Http1.1
,尤其是
Http2
、
Http3
已经开始
向长连接演化。
总之,长连接和短连接的选择要视情况而定。
原生JDK网络编程-BIO
BIO,意为 Blocking I/O,即阻塞的 I/O。
BIO 基本上就是我们上面所说的生活场景的朴素实现。
在 BIO 中类 ServerSocket 负责绑
定 IP 地址,启动监听端口,等待客户连接;
客户端 Socket 类的实例发起连接操作ServerSocket
接受连接后产生一个新的服务端 socket 实例负责和客户端 socket 实例通过输入和输出流进 行通信。
bio 的阻塞,主要体现在两个地方。
①若一个服务器启动就绪,那么主线程就一直在等待着客户端的连接,这个等待过程中
主线程就一直在阻塞。
②在连接建立之后,在读取到 socket 信息之前,线程也是一直在等待,一直处于阻塞
的状态下的。
这一点可以通过 cn.tuling.bio
下的
ServerSingle.java
服务端程序看出,启动该程序后,启
动一个
Client
程序实例,并让这个
Client
阻塞住,位置就在向服务器输出具体请求之前,再
启动一个新的
Client
程序实例,会发现尽管新的
Client
实例连接上了服务器,但是
ServerSingle
服务端程序仿佛无感知一样?为何,因为执行的主线程被阻塞了一直在等待第一个
Client
实例发送消息过来。
所以在 BIO
通信里,我们往往会在服务器的实现上结合线程来处理连接以及和客户端的
通信。
传统 BIO 通信模型:
采用 BIO 通信模型的服务端,通常
由一个独立的 Acceptor 线程负
责监听客户端的连接
,它
接收到客户端连接请求之后为每个客户端创建一个新的线程进行链
路处理
,处理完成后,通过输出流返回应答给客户端,线程销毁。即
典型的一请求一应答模
型
,同时数据的读取写入也必须阻塞在一个线程内等待其完成。
代码可见
cn.tuling.bio.Server
。
该模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程
个数和客户端并发访问数呈 1:1 的正比关系,Java 中的线程也是比较宝贵的系统资源,线程
数量快速膨胀后,系统的性能将急剧下降,随着访问量的继续增大,系统最终就死-掉-了。
为了改进这种一连接一线程的模型,我们可以使用线程池来管理这些线程,实现 1 个或
多个线程处理 N 个客户端的模型(但是底层还是使用的同步阻塞 I/O),
通常被称为“伪异
步 I/O 模型“。
我们知道,如果使用
CachedThreadPool 线程池
(不限制线程数量,如果不清楚请参考
文首提供的文章),其实除了能自动帮我们管理线程(复用),看起来也就像是
1:1
的客户
端:线程数模型,而使用
FixedThreadPool
我们就
有效的控制了线程的最大数量,保证了系
统有限的资源的控制,实现了 N:M 的伪异步 I/O 模型
。
代码可见
cn.tuling.bio.ServerPool
。
但是,正因为限制了线程数量,如果发生读取数据较慢时(比如数据量大、网络传
输慢等),大量并发的情况下,其他接入的消息,只能一直等待,这就是最大的弊端。
BIO 实战-手写 RPC 框架
为什么要有 RPC?
我们最开始开发的时候,一个应用一台机器,将所有功能都写在一起,比如说比较常见
的电商场景,服务之间的调用就是我们最熟悉的普通本地方法调用。
随着我们业务的发展,我们需要提示性能了,我们会怎么做?将不同的业务功能放到线
程里来实现异步和提升性能,但本质上还是本地方法调用。
但是业务越来越复杂,业务量越来越大,单个应用或者一台机器的资源是肯定背负不起
的,这个时候,我们会怎么做?将核心业务抽取出来,作为独立的服务,放到其他服务器上
或者形成集群。这个时候就会请出
RPC
,系统变为分布式的架构。
为什么说千万级流量分布式、微服务架构必备的 RPC
框架?和
LocalCall
的代码进行比
较,
因为引入 rpc 框架对我们现有的代码影响最小,同时又可以帮我们实现架构上的扩展。
现在的开源
rpc
框架,有什么?
dubbo
,
grpc
等等
当服务越来越多,各种 rpc
之间的调用会越来越复杂,这个时候我们会引入中间件,比
如说
MQ
、缓存,同时架构上整体往微服务去迁移,引入了各种比如容器技术
docker
,
DevOps
等等。最终会变为如图所示来应付千万级流量,但是不管怎样,
rpc
总是会占有一席之地。
什么是 RPC?
RPC
(
Remote Procedure Call
——远程过程调用),它是一种通过网络从远程计算机程
序上请求服务,而不需要了解底层网络的技术。
一次完整的 RPC 同步调用流程:
1)服务消费方(
client
)以本地调用方式调用客户端存根;
2)什么叫客户端存根?
就是远程方法在本地的模拟对象,一样的也有方法名,也有方
法参数,client stub 接收到调用后负责将方法名、方法的参数等包装,并将包装后的信息通
过网络发送到服务端;
3)服务端收到消息后,交给代理存根在服务器的部分后进行解码为实际的方法名和参
数
4)
server stub
根据解码结果调用服务器上本地的实际服务;
5)本地服务执行并将结果返回给
server stub
;
6)
server stub
将返回结果打包成消息并发送至消费方;
7)
client stub
接收到消息,并进行解码;
8)服务消费方得到最终结果。
RPC 框架的目标就是要中间步骤都封装起来,让我们进行远程方法调用的时候感觉到就
像在本地方法调用一样。
RPC 和 HTTP
rpc 字面意思就是远程过程调用,只是对不同应用间相互调用的一种描述,一种思想。
具体怎么调用?实现方式可以是最直接的
tcp
通信,也可以是
http
方式,在很多的消息中间
件的技术书籍里,甚至还有使用消息中间件来实现
RPC
调用的,我们知道的
dubbo
是基于
tcp
通信的,
gRPC
是
Google
公布的开源软件,基于最新的
HTTP2.0
协议,底层使用到了
Netty
框架的支持。所以总结来说,
rpc
和
http
是完全两个不同层级的东西,他们之间并没有什么
可比性。
实现 RPC 框架
实现
RPC
框架需要解决的那些问题
代理问题
代理本质上是要解决什么问题?要解决的是被调用的服务本质上是远程的服务,但是调
用者不知道也不关心,调用者只要结果,具体的事情由代理的那个对象来负责这件事。既然
是远程代理,当然是要用代理模式了。
代理(Proxy)
是一种设计模式
,
即通过代理对象访问目标对象
.
这样做的好处是
:
可以在目
标对象实现的基础上
,
增强额外的功能操作
,
即扩展目标对象的功能。那我们这里额外的功能
操作是干什么,通过网络访问远程服务。
jdk 的代理有两种实现方式:
静态代理
和
动态代理。
序列化问题
序列化问题在计算机里具体是什么?我们的方法调用,有方法名,方法参数,这些可能
是字符串,可能是我们自己定义的
java
的类,但是在网络上传输或者保存在硬盘的时候,
网络或者硬盘并不认得什么字符串或者
javabean
,它只认得二进制的
01
串,怎么办?要进
行序列化,网络传输后要进行实际调用,就要把二进制的
01
串变回我们实际的
java
的类,
这个叫反序列化。
java
里已经为我们提供了相关的机制