前言
Github:https://github.com/yihonglei/thinking-in-netty
一 Netty概述
Netty 提供异步的、事件驱动的网络应用程序框架和工具,用以快速简单开发高性能、高可靠性的网络服务器和客户端程序。
它大大简化了网络编程,如TCP和UDP套接字服务器。“快速简单”并不意味着生成的应用程序将受到可维护性或
性能问题的影响。Netty经过精心设计,并积累了许多协议(如ftp、smtp、http)的实施经验,以及各种二进制和
基于文本的遗留协议。因此,Netty成功地找到了一种方法,可以在不妥协的情况下实现轻松的开发、性能、稳定性和灵活性。
二 Netty特性
Netty官方架构图。
设计
- 统一的API,适用于不同的协议(阻塞和非阻塞)
- 基于灵活、可扩展的事件驱动模型(SEDA)
- 高度可定制的线程模型
- 可靠的无连接数据Socket支持(UDP)
性能
- 更好的吞吐量,低延迟
- 更省资源
- 尽量减少不必要的内存拷贝
安全
- 完整的SSL/ TLS和STARTTLS的支持
易用
- 完善的Java doc,用户指南和样例
- 仅依赖于JDK1.6(netty 4.x)
三 Netty核心组件
1、Tansport Channel
对应NIO中的Channel,是NIO的基本构造,可以把Channel看成是传入(入站)或者传出(出站)数据的载体。
因此,它可以被打开或者关闭,连接或者断开连接。
2、EventLoop、ChannelHandler、EventLoopGroup
EventLoop是NIO中selector程序的抽象,消除了在NIO中手动的编写派发代码。在Netty内部,将会为每个Channel
分配一个EventLoop,用以处理所有I/O事件:
- 注册感兴趣的事件;
- 将事件派发给ChannelHandler;
- 安排进一步的处理;
每一个事件触发后将直接交由ChannelHandler接口实现处理。
EventLoop本身只由一个线程驱动,处理一个Channel的所有I/O事件,并且在该EventLoop的整个生命周期内部不会改变。
一个EventLoopGroup可以包含多个EventLoop。
3、ChannelFutue
Netty提供了另外一种在操作完成时通知应用程序的方式。在未来的某个时刻完成时提供对其结果的访问。
JDK内置包里面java.util.concurrent.Future接口提供了实现,但是操作比较麻烦,Netty提供了自己的实现,
ChannelFuture用于在执行异步操作的时候使用。ChannelFutue不会阻塞,完全是异步和事件驱动的。
4、ByteBuf
对应NIO中的ByteBuffer。
5、Bootstrap 和 ServerBootstrap
对应NIO中的Selector、ServerSocketChannel等的创建、配置、启动等。
Netty里面有很多的概念,概念说太多就迷糊了,先来个实例,体验下Netty入门编程。
四 Netty入门实战
jar包
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.44.Final</version>
</dependency>
1、服务端
1.1 EchoServer
启动服务端,等待客户端连接,将连接事件交给EchoServerHandler处理。
package com.jpeony.netty.echo;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import java.net.InetSocketAddress;
/**
* 一 Netty Server启动流程
* 1、设置服务端ServerBootStrap启动参数
* <p>
* group()方法分配一个NioEventLoopGroup实例以进行事件处理,如接受新连接以及读/写数据;
* <p>
* channel()设置通道类型;
* <p>
* localAddress()指定服务器绑定的本地的InetSocketAddress;
* <p>
* childHandler()添加一个EchoServerHandler到Channel的ChannelPipeline;
* 2、调用ServerBootStrap.bind()方法启动服务端
* 调用ServerBootStrap.bind()方法启动服务端,bind方法会在group中注册NioServerScoketChannel,
* 监听客户端的连接请求会创建一个NioServerSocketChannel实例,并将其在group中进行注册;
*
* @author yihonglei
*/
public class EchoServer {
private final int port;
public EchoServer(int port) {
this.port = port;
}
/**
* 服务器端启动方法
*
* @author yihonglei
*/
private void start() throws Exception {
// 创建EventLoopGroup
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// 创建ServerBootstrap
ServerBootstrap b = new ServerBootstrap();
// 指定EventLoopGroup以处理服务端事件,需要适用于NIO的实现。
b.group(bossGroup, workerGroup)
// 指定所使用的NIO传输Channel
.channel(NioServerSocketChannel.class)
// 使用指定的端口设置套接字地址
.localAddress(new InetSocketAddress(port))
// 添加一个EchoServerHandler到Channel的ChannelPipeline
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
// EchoServerHandler被标注为@Shareable,所以我们可以总是使用同样的实例
ch.pipeline().addLast(new EchoServerHandler());
}
});
// 异步地绑定服务器,调用sync()方法阻塞直到绑定完成
ChannelFuture f = b.bind().sync();
// 绑定Channel的CloseFuture,并且阻塞当前线程直到它完成
f.channel().closeFuture().sync();
} finally {
// 关闭EventLoopGroup并且释放所有的资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
// 设置端口值
new EchoServer(9999).start();
}
}
1.2 EchoServerHandler
EchoServerHandler负责服务端具体逻辑处理。
package com.jpeony.netty.echo;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
/**
* 服务端业务逻辑处理。
*
* @author yihonglei
*/
@ChannelHandler.Sharable //注解@ChannelHandler.Sharable表示一个ChannelHandler可以被多个Channel安全地共享。
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
/**
* 每个传入的消息都要调用该方法。
*
* @author yihonglei
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
// 将客户端发送过来的消息打印到控制台
ByteBuf in = (ByteBuf) msg;
System.out.println("server received msg from client:" + in.toString(CharsetUtil.UTF_8));
// 写一条消息响应给客户端
ByteBuf responseMsg = Unpooled.wrappedBuffer(new String("Hello Client!").getBytes());
ctx.write(responseMsg);
}
/**
* 通知ChannelInboundHandler最后一次对channelRead()的调用时当前批量读取中的最后一条消息。
*
* @author yihonglei
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
// 将未决消息冲刷到远程节点,并且关闭该Channel
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
.addListener(ChannelFutureListener.CLOSE);
}
/**
* 在读取操作期间,有异常抛出时会调用。
*
* @author yihonglei
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// 打印异常栈跟踪信息
cause.printStackTrace();
// 关闭该Channel
ctx.close();
}
}
2、客户端
2.1 EchoClient
1)连接到服务器;
2)发送一个或者多个消息;
3)对于每个消息,等待并接收从服务器发回的消息;
4)关闭连接;
客户端逻辑交由EchoClientHandler处理。
package com.jpeony.netty.echo;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import java.net.InetSocketAddress;
/**
* 1、初始化客户端,将创建一个Bootstrap实例;
* 2、为事件处理分配一个NioEventLoopGroup实例,其中事件处理包括创建新的连接以及处理入站和出站数据;
* 3、为服务器连接创建一个InetSocketAddress实例;
* 4、当连接被建立时,一个EchoClientHandler实例会被安装到ChannelPipeline中;
* 5、在一切都设置完成后,调用Bootstrap.connect()方法连接到远程节点;
*
* @author yihonglei
*/
public class EchoClient {
private final String host;
private final int port;
public EchoClient(String host, int port) {
this.host = host;
this.port = port;
}
/**
* 客户端启动方法
*
* @author yihonglei
*/
private void start() throws InterruptedException {
// 创建EventLoopGroup
EventLoopGroup group = new NioEventLoopGroup();
try {
// 创建Bootstrap
Bootstrap b = new Bootstrap();
// 指定EventLoopGroup以处理客户端事件,需要适用于NIO的实现。
b.group(group)
// 适用于NIO传输的Channel类型
.channel(NioSocketChannel.class)
// 设置服务器的InetSocketAddress
.remoteAddress(new InetSocketAddress(host, port))
// 在创建Channel时,向ChannelPipeline中添加一个EchoClientHandler实例
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new EchoClientHandler());
}
});
// 连接到远程节点,阻塞等待知道连接完成
ChannelFuture f = b.connect().sync();
// 阻塞,直到Channel关闭
f.channel().closeFuture().sync();
} finally {
// 关闭EventLoopGroup并且释放所有的资源
group.shutdownGracefully().sync();
}
}
public static void main(String[] args) throws InterruptedException {
new EchoClient("127.0.0.1", 9999).start();
}
}
2.2 EchoClientHandler
负责客户端逻辑处理。
package com.jpeony.netty.echo;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;
/**
* 客户端业务逻辑处理
*
* @author yihonglei
*/
@ChannelHandler.Sharable // 标记该类的实例可以被多个Channel共享
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
/**
* 连接服务器成功之后被调用
*
* @author yihonglei
*/
@Override
public void channelActive(ChannelHandlerContext ctx) {
// 当被通知Channel是活跃的时候,发送一条消息(向服务端发送一条消息)
String data = "Hello Server!";
ctx.writeAndFlush(Unpooled.copiedBuffer(data, CharsetUtil.UTF_8));
}
/**
* 从服务器接收到消息时被调用
*
* @author yihonglei
* @date 2019/4/20 16:27
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf in) {
// 将服务端响应的消息打印出来
System.out.println("client received msg from client:" + in.toString(CharsetUtil.UTF_8));
}
/**
* 在处理过程中抛异常时调用
*
* @author yihonglei
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// 打印异常栈跟踪信息
cause.printStackTrace();
// 关闭该Channel
ctx.close();
}
}
3、程序启动
先启动服务端EchoServer,然后再启动客户端EchoClient。
服务端接收到客户端发送过来的消息Hello Server!
客户端收到服务端响应的消息Hello Client!