Netty学习之Netty介绍
前言
本周开始学习Netty,主要的参考资料是《Netty In Action》原版书,这本书写得真好,一开始学习Netty的时候,看得有些云里雾里,后面弄懂之后,再回头看一下这本书,就发现这本书真的言简意赅,精炼地将Netty的各个组件展现出来。
传统的Java网络编程
在传统的Java网络编程中,是基于阻塞形式的IO,在这种形式的IO模型中,由于当数据没有到来的时候,对应的线程会阻塞,所以,需要为每一个请求都分配一个线程(当然,可以通过线程池来复用线程,从而减少线程创建以及销毁的代价),然而,实际上需要消耗的资源还是非常巨大的,比如,如果同一时刻有1k的并发量,那么至少就需要有1k的左右的线程来处理(数据不严谨,但是足够表达意思了)。
如下代码所示
public class OIo {
private static boolean isStop = false;
private static ExecutorService executorService;
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(8080);
// 通过线程池的方式来处理
executorService = Executors.newCachedThreadPool();
// 监听多个请求
while (!isStop) {
Socket clientSocket = server.accept();
// 每个请求分配对应的线程来处理
// 由于是阻塞IO,所以如果不分配线程来处理,会导致同一时间只能处理
// 一个请求
executorService.submit(new NetworkHandler(clientSocket));
}
}
static class NetworkHandler implements Runnable {
private Socket clientSocket;
public NetworkHandler(Socket socket) {
this.clientSocket = socket;
}
public void run() {
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter writer = new PrintWriter(clientSocket.getOutputStream());
String requestContent, responseContent;
// 建立连接之后不停读取数据
while((requestContent = reader.readLine()) != null) {
if("DONE".equals(requestContent)) {
break;
}
responseContent = handleRequest(requestContent);
writer.write(responseContent);
}
writer.flush();
reader.close();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private String handleRequest(String request) {
System.out.println("request: " + request);
return "handed";
}
}
}
显然,上面的这种模式在并发比较大的时候是不太适合使用的,于是,在Java4之后,引入了Nio模式,通过选择器(selector)来监视多个channel(多个连接),当数据到达时负责与对应的channel相互,这样,在建立连接的时候,就不需要为每个连接都分配一个线程了,Nio是基于非阻塞形式的IO,所以,相对于传统的阻塞IO,有了很大的性能提升,然而,由于Java中提供的Nio过于底层,而且重复的操作比较多,所以,很多的大牛们就开始对其进行抽象,封装,Netty就是其中的一个了。
Netty介绍
Netty是一个异步的,事件驱动的网络应用框架。
具有众多优秀的特性
通用的API(支持阻塞跟非阻塞socket)
支持TCP、UDP
逻辑以及网络底层处理分隔
灵活及可扩展的事件模型
高度自定义线程模型
高吞吐量,低延迟
消耗资源低
支持SSL/TSL
社区支持
Netty官网:Netty官网
通过上面的介绍,我们应该明确的是Netty是一个网络框架,所以,它所做的事情其实就是网络的处理(建立连接、发送数据等),这一点需要搞清楚(一开始就是没有弄清楚Netty是什么,所以在学习的时候出现了混乱)。而我们使用Netty,一般是在它上面进行我们的业务操作(比如实现IM逻辑、实现RPC等等),当然,如果有精力研究其源码,也是非常不错的,据说Netty的源码也是非常值得学习的(先挖个坑,后面有机会一定学习、分析一波)
在上面Netty的介绍的时候,我们看到了异步、事件驱动这两个词,先来谈谈异步,与异步对应的还有一个词,同步
同步指的是当请求发出之后,必须等到请求回来,才完成操作,异步则不是,异步是在请求发出之后,立即结束,不用等到请求返回才结束操作。
与这两个词关联的还有阻塞跟非阻塞,阻塞是指请求发出之后,如果没有数据,则对应的操作会被阻塞(具体的就是对应的线程会进入阻塞态,等到数据能获取,再变为可运行态),非阻塞则是指即使没有数据到来,此时对应的线程可以进行其他的操作,而不会被动进入阻塞态,即不会被阻塞。
关于这四个词语的更多的解析及举例,可以参考知乎的这篇问答:怎样理解阻塞非阻塞与同步异步的区别
对于事件驱动,一般是与异步相结合的,对于同步而言,由于操作会等到出结果了才返回,所以直接调用就能获取结果了(成功/失败),然而,对于异步而言,由于调用后立即返回,所以根据返回内容是没法知道结果的,那怎么知道结果呢,一般是通过注册回调函数来实现,当有结果的时候,事件发出者/监管者会调用对应的回调函数来通知我们操作已经完成了,也就是说,整个操作的过程中,是通过事件来进行沟通,而不是通过直接调用来沟通。理清楚这一点对于学习Netty非常重要,因为Netty中是通过事件来进行消息的传递的。
Netty核心组件
Netty是由几个核心的组件构成的
Channels,NIO的基本组件,表示一个打开的连接(网络,文件),可以用于处理一个或者多个不同的I/O操作(读/写)
Callbacks,一个方法,提供给另一个方法的引用,用于后者在合适的时候调用,Netty使用回调函数处理事件,当回调函数触发时,事件能够被实现了ChannelHandler接口的处理器处理(其实就是事件发生时,回调函数被调用)(被动)
Futures,提供了另外一种通知应用操作已经完成的方法(主动),异步操作的结果占位符,通过该方式,可以在后面访问到异步操作的结果,由于JDK中的Future比较笨重,Netty提供了自己的实现ChannelFuture,用于当异步操作执行的时候。并且允许我们向ChannelFuture注册一个或者多个ChannelFutureListener对象,当操作完成时,这些对象的回调方法operationComplete()会被调用,Netty的每个outBoundI/O均返回一个ChannelFuture,也即不阻塞
Events and Handlers,Netty使用不同的事件通知我们状态的改变或者操作的结果,然后我们可以采用不同的机制来处理不同的事件,如:日志记录、数据传输、流控制、应用逻辑等,由于Netty是网络框架,所以事件是根据inbound或者outbound数据流来区分的,如:连接建立或者断开、数据读取、用户事件、错误事件;远程端口打开或者关闭、写或者刷新数据
EventLoop,连接到每个Channel用于处理事件,包括:注册事件、分发事件到ChannelHandler、调度行动,每个EventLoop本身由一个线程来驱动,用于处理一个Channel中所有的I/O事件,并且在其生命周期中不会改变
基本上理清楚上面几个组件的作用以及相互的关系,就能掌握Netty的使用了。
第一个Netty应用
为了形象地认识Netty,这里通过一个简单的小例子来理解,顺便配置下环境
首先导入Netty依赖
io.netty
netty-all
4.1.12.Final
对应的小程序
public class EchoServer {
private final int PORT = 8080;
public static void main(String[] args) {
new EchoServer().start();
}
public void start() {
// 创建事件处理器
EventLoopGroup group = new NioEventLoopGroup();
// 创建启动器
ServerBootstrap bootstrap = new ServerBootstrap();
// 配置启动器
bootstrap.group(group)
// 配置建立连接的处理器
.channel(NioServerSocketChannel.class)
// 配置连接建立后的处理器
// 当一个新连接建立后,就会新建一个childHandler
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 将逻辑处理器添加到channel对应的pipeline中
ch.pipeline().addLast(new EchoServerHandler());
}
});
try {
// 绑定地址并且建立连接,sync()表示等到连接绑定完成
ChannelFuture future = bootstrap.bind(PORT).sync();
// 阻塞直至连接关闭
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
try {
// 关闭连接,释放资源
group.shutdownGracefully().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 逻辑处理器
* 这里是直接将收到的数据发送回去
*/
class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf data = (ByteBuf) msg;
System.out.println("request: " + data.toString(CharsetUtil.UTF_8));
// 将收到的数据发送回去给客户端
ctx.writeAndFlush(data);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
// 关闭连接
ctx.close();
}
}
}
启动服务之后,直接通过telnet程序进行测试即可。
总结
本小节主要是进行Netty的介绍,包括传统阻塞IO的缺点以及Netty的几个特性,最后通过一个简单的小例子展示了Netty的基础用法,后面将详细学习Netty的几个组件及具体作用。