Netty源码剖析与应用
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.3 Netty客户端的运用

Netty除了可以编写高性能服务端,还有配套的非阻塞I/O客户端,关于客户端与服务端的通信,涉及多线程数据交互,并运用了JDK的锁和多线程。

1.3.1 Java多线程交互

本小节编写了一个Java多线程实例,为后续介绍Netty客户端做铺垫。此实例模拟1条主线程循环写数据,另外99条子线程,每条子线程模拟睡眠1s,再给主线程发送结果,最终主线程阻塞获取99条子线程响应的结果数据。

此实例有以下几个类。

第一个类FutureMain:主线程,拥有启动main()方法和实例的总体处理逻辑(请求对象的生成、子线程的构建和启动、获取子线程的响应结果)。

第二个类RequestFuture:模拟客户端请求类,主要用于构建请求对象(拥有每次的请求id,并对每次请求对象进行了缓存),最核心的部分在于它的同步等待和结果通知方法。

第三个类SubThread:子线程,用于模拟服务端处理,根据主线程传送的请求对象RequestFuture构建响应结果,等待1s后,调用RequestFuture的响应结果通知方法将结果交互给主线程。

第四个类Response:响应结果类,拥有响应id和结果内容。只有响应id和请求id一致,才能从请求缓存中获取当前响应结果的请求对象。

如图1-2为多线程交互数据UML图。

图1-2 多线程交互数据UML图

具体实现代码如下。

(1)FutureMain类:

(2)RequestFuture类:

(3)SubThread类:

(4)Response类:

运行FutureMain的main()方法,控制台会打印出子线程SubThread返回的Response消息。感兴趣的读者可通过以下两个问题对代码进行相应的修改。

(1)使用ReentrantLock与Condition更换同步关键词,以及Object类的notify()和wait()方法。

(2)将主线程for循环构建请求而阻塞同步发送修改成线程池Executors.newFixedThreadPool生成100条线程异步请求。

1.3.2 Netty客户端与服务端短连接

Netty是一个异步网络处理框架,使用了大量的Future机制,并在Java自带Future的基础上增加了Promise机制,从而使异步编程更加方便简单。本小节采用Netty客户端与服务端实现短连接异步通信的方式,加深读者对多线程的灵活运用的认识,并帮助读者初步了解Netty客户端。

Netty客户端的线程模型比服务端的线程模型简单一些,它只需一个线程组,底层采用Java的NIO,通过IP和端口连接目标服务器,请求发送和接收响应结果数据与Netty服务端编程一样,同样需要经过一系列Handler。请求发送从TailContext到编码器Handler,再到HeadContext;接收响应路径从HeadContext到解码器Handler,再到业务逻辑Handler(此处提到的数据流的编码和解码会在第5章进行详细讲解)。TCP网络传输的是二进制数据流,且会源源不断地流到目标机器上,如果没有对数据流进行一些额外的加工处理,那么将无法区分每次请求的数据包。编码是指在传输数据前,对数据包进行加工处理,解码发生在读取数据包时,根据加工好的数据的特点,解析出正确的数据包。客户端与服务端的交互流程如图1-3所示。

图1-3中共有8个处理流程,分别如下。

①Netty客户端通过IP和端口连接服务端,并准备好JSON数据包。

②JSON数据包发送到网络之前需要经过一系列编码器,并最终被写入Socket中,发送给Netty服务端。

③Netty服务端接收到Netty客户端发送的数据流后,先经过一系列解码器,把客户端发送的JSON数据包解码出来,然后传递给ServerHandler实例。

④ServerHandler实例模拟业务逻辑处理。

⑤Netty服务端把处理后的结果返回Netty客户端。

⑥Netty服务端同样需要经过一系列编码器,最终将响应结果发送到网络中。

⑦Netty客户端解码器接收响应结果字节流并对其进行解码,然后把响应的JSON结果数据返回给ClientHandler。

⑧由于ClientHandler运行在NioEventLoop线程上,所以结果数据在返回主线程时需要用到Netty的Promise机制,以实现多线程数据交互。

图1-3 客户端与服务端的交互流程

Netty服务端的Netty服务类在1.2节的基础上新增了长度编码器和解码器,具体代码如下:

服务端业务逻辑处理类ServerHandler的channelRead()方法需要返回Response对象,同时获取请求id,具体变动代码如下:

客户端启动类NettyClient和Netty服务相似,辅助类用Bootstrap替代ServerBootstrap,同时引入了异步处理类DefaultPromise,用于异步获取服务端的响应结果,具体实现代码如下:

客户端业务逻辑处理ClientHandler,接收服务端的响应数据,并运用Promise唤醒主线程,实现代码如下:

通过以上的介绍,对Netty客户端会有一个基本的了解,能正常发送与接收数据。上述用法看起来没什么问题,但性能偏弱。因为在每次发送请求时都需要创建连接,还不如直接使用普通Socket作为客户端。可以运用连接池优化解决,把短连接先放入连接池,然后从连接池中获取每次请求的连接,感兴趣的读者可以采用Apache Commons Pool重构上述代码。

1.3.3 Netty客户端与服务端长连接

本小节使用长连接解决1.3.2小节中的性能问题,通常各公司内部的RPC通信一般会选择长连接通信模式。在改造代码前,请先思考以下两个问题。

(1)改造成长连接后,ClientHandler不能每次都在main()方法中构建,promise对象无法通过主线程传送给ClientHandler,那么此时主线程如何获取NioEventLoop线程的数据呢?

(2)主线程每次获取的响应结果对应的是哪次请求呢?

• 通过多线程交互数据实例的学习,很显然第一个问题可以通过多线程数据交互来解决。首先对Netty客户端创建的连接进行静态化处理,以免每次调用时都需要重复创建;然后在给服务端发送请求后运用RequestFuture的get()方法同步等待获取响应结果,以替代Netty的Promise同步等待;最后用RequestFuture.received替代ClientHandler的Promise异步通知。

• 第二个问题的解决方案:每次请求带上自增唯一的id,客户端需要把每次请求先缓存起来,同时服务端在接收到请求后,会把请求id放入响应结果中一起返回客户端。

NettyClient的连接需要进行静态化处理,同时,改成RequestFutrue的get()方法获取异步响应结果。具体的改造后的NettyClient代码如下:

下面对RequestFuture类也进行一些改动,新增了一个id自增属性AtomicLong aid,在构造方法上,将自增aid赋给请求id,同时把当前请求对象加入全局缓存futures中,代码如下:

将ClientHandler的channelRead()方法中的promise改为RequestFuture.received,代码如下:

服务端ServerHandler类将返回的结果加上对应的请求id,客户端与服务端长连接响应输出如图1-4所示。通过图1-4发现,服务器响应的结果有序地输出在客户端控制台上。不妨思考,要如何修改代码,才能让客户端输出的结果是无序的呢?

图1-4 客户端与服务端长连接响应输出