实际上,有没有这么简单的框架呢?
使用 Netty 来实现异步网络通信
在 Java 中,大名鼎鼎的 Netty 框架的 API 设计就是这样的。接下来我们看一下如何使用 Netty 实现异步接收数据。
// 创建一组线性
EventLoopGroup group = new NioEventLoopGroup();
try{
// 初始化 Server
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(group);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.localAddress(new InetSocketAddress("localhost", 9999));
// 设置收到数据后的处理的 Handler
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new MyHandler());
}
});
// 绑定端口,开始提供服务
ChannelFuture channelFuture = serverBootstrap.bind().sync();
channelFuture.channel().closeFuture().sync();
} catch(Exception e){
e.printStackTrace();
} finally {
group.shutdownGracefully().sync();
}
这段代码它的功能非常简单,就是在本地 9999 端口,启动了一个 Socket Server 来接收数据。我带你一起来看一下这段代码:
- 首先我们创建了一个 EventLoopGroup 对象,命名为 group,这个 group 对象你可以简单把它理解为一组线程。这组线程的作用就是来执行收发数据的业务逻辑。
- 然后,使用 Netty 提供的 ServerBootstrap 来初始化一个 Socket Server,绑定到本地 9999 端口上。
- 在真正启动服务之前,我们给 serverBootstrap 传入了一个 MyHandler 对象,这个 MyHandler 是我们自己来实现的一个类,它需要继承 Netty 提供的一个抽象类:ChannelInboundHandlerAdapter,在这个 MyHandler 里面,我们可以定义收到数据后的处理逻辑。这个设置 Handler 的过程,就是我刚刚讲的,预先来定义回调方法的过程。
- 最后就可以真正绑定本地端口,启动 Socket 服务了。
服务启动后,如果有客户端来请求连接,Netty 会自动接受并创建一个 Socket 连接。你可以看到,我们的代码中,并没有像一些同步网络框架中那样,需要用户调用 Accept() 方法来接受创建连接的情况,在 Netty 中,这个过程是自动的。
当收到来自客户端的数据后,Netty 就会在我们第一行提供的 EventLoopGroup 对象中,获取一个 IO 线程,在这个 IO 线程中调用接收数据的回调方法,来执行接收数据的业务逻辑,在这个例子中,就是我们传入的 MyHandler 中的方法。
Netty 本身它是一个全异步的设计,我们上节课刚刚讲过,异步设计会带来额外的复杂度,所以这个例子的代码看起来会比较多,比较复杂。但是你看,其实它提供了一组非常友好 API。
真正需要业务代码来实现的就两个部分:一个是把服务初始化并启动起来,还有就是,实现收发消息的业务逻辑 MyHandler。而像线程控制、缓存管理、连接管理这些异步网络 IO 中通用的、比较复杂的问题,Netty 已经自动帮你处理好了,有没有感觉很贴心?所以,非常多的开源项目使用 Netty 作为其底层的网络 IO 框架,并不是没有原因的。
在这种设计中,Netty 自己维护一组线程来执行数据收发的业务逻辑。如果说,你的业务需要更灵活的实现,自己来维护收发数据的线程,可以选择更加底层的 Java NIO。其实,Netty 也是基于 NIO 来实现的。
使用 NIO 来实现异步网络通信
在 Java 的 NIO 中,它提供了一个 Selector 对象,来解决一个线程在多个网络连接上的多路复用问题。什么意思呢?在 NIO 中,每个已经建立好的连接用一个 Channel 对象来表示。我们希望能实现,在一个线程里,接收来自多个 Channel 的数据。也就是说,这些 Channel 中,任何一个 Channel 收到数据后,第一时间能在同一个线程里面来处理。
我们可以想一下,一个线程对应多个 Channel,有可能会出现这两种情况:
- 线程在忙着处理收到的数据,这时候 Channel 中又收到了新数据;
- 线程闲着没事儿干,所有的 Channel 中都没收到数据,也不能确定哪个 Channel 会在什么时候收到数据。
Selecor 通过一种类似于事件的机制来解决这个问题。首先你需要把你的连接,也就是 Channel 绑定到 Selector 上,然后你可以在接收数据的线程来调用 Selector.select() 方法来等待数据到来。这个 select 方法是一个阻塞方法,这个线程会一直卡在这儿,直到这些 Channel 中的任意一个有数据到来,就会结束等待返回数据。它的返回值是一个迭代器,你可以从这个迭代器里面获取所有 Channel 收到的数据,然后来执行你的数据接收的业务逻辑。
你可以选择直接在这个线程里面来执行接收数据的业务逻辑,也可以将任务分发给其他的线程来执行,如何选择完全可以由你的代码来控制。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!