Netty是什么
定义
Netty是一种高性能,异步的,基于事件驱动的网络应用框架
核心架构
- 核心:可扩展事件,统一的通信API,零拷贝机制和字节缓冲区
- 传输服务:Socket,http通道,In-VM通道
- 协议支持:http、SSL、Google protobuf、zlib/gzip、RTSP
Netty优势
- 基于NIO实现,统一封装了各种传输类型和协议实现的API。
- 简化开发,提高效率,开发人员只需关注业务即可
- 可定制线程模型
- 只依赖JDK底层API,低耦合。
- 减少了内存考虑,提高了性能。
- 快速迭代。
Netty的版本
Netty目前常用的3.X和4.X,而5.X不建议使用,因为性能没有大的提升,而是使得Netty的维护更加复杂。
为什么使用Netty而不直接使用NIO进行网络编程
- NIO的类库和API比较复杂
- 需要自己实现Reactor模型,开发需要学习额外的技能。
- 使用NIO进行网络编程,实现比较简单,但是可靠性较差,功能补齐工作量很大。
- JDK的NIO存在epoll(bug)。
业界通常使用Netty进行网络编程,最主要的原因是Netty的高性能,低延迟,而Netty为什么有更高的性能呢?主要原因是由于它的高性能设计。
Netty的高性能的原因
Netty的高性能源于它的设计模型。了解他的设计模型,需要先了解IO模型和Reactor模型。
IO模型
BIO
在JDK1.4 以前java的IO都是BIO(Blocking IO),即阻塞型IO。
BIO模型解读:
- 客户端的请求和后端线程数1:1,导致在高并发下,大量创建和销毁线程,开销非常大。甚至可能会发生OOM。
- 创建连接后,会创建一个线程,当改线程没有任何操作时候,该线程会一直阻塞,浪费资源。
NIO模型
NIO模型解读:
Buffer:缓冲区,底层通过数组实现,每一种java基本类型都有对应缓冲区。
channel:基于通道实现,和流不同的是,通道是双向的,因此,一个通道可以实现读写操作。
selector:多路复用器/选择器,NIO会启动一个单线程来运行Selector,selector会不断轮询channel,但channel中有事件的时候,就会被selector挑选出来,获取它的SelectionKey集合,SelectionKey中包含不同的事件类型,根据不同的事件类型(OP_ACCEPT, OP_READ, OP_WRITE)进行不同的操作。
NIO的优点:
- selector运行在单线程上,处理多个通道,避免了多线程上下文切换带来的系统开销。
- 一个channel并没有开启一个线程,而是channel中真正有事件的时候,才会开启线程进行读写操作,而BIO为每一个连接都开启了一个线程。
代码:
public class SelectorDemo {
/**
* 注册时间
*/
private Selector getSelector() throws Exception {
// 获取Selector对象
Selector selector = Selector.open();
// 获取通道,设置非阻塞
ServerSocketChannel channel = ServerSocketChannel.open();
channel.configureBlocking(false);
// 获取服务端套接字,绑定端口号
ServerSocket socket = channel.socket();
socket.bind(new InetSocketAddress(5566));
// 注册通道感兴趣的时间
channel.register(selector, SelectionKey.OP_ACCEPT);
return selector;
}
/**
* 监听事件
*/
public void listen() throws Exception {
Selector selector = this.getSelector();
while (true) {
selector.select(); // 多路复用器轮询通道,直到有一个通道有事件(阻塞)
Set<SelectionKey> selectionKeys = selector.selectedKeys(); // 将事件的SelectionKey都获取出来
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
process(selectionKey, selector);
iterator.remove();
}
}
}
private void process(SelectionKey key, Selector selector) throws Exception {
if (key.isAcceptable()) { // 新连接请求
ServerSocketChannel server = (ServerSocketChannel) key.channel(); // 获取服务端通道
SocketChannel channel = server.accept(); // 建立连接(阻塞)
channel.configureBlocking(false); // 设置通道非阻塞
channel.register(selector, SelectionKey.OP_READ); // 注册read事件
} else if(key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel(); // 获取通道
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);// 获取字节缓冲区
channel.read(byteBuffer); // 从通道中读取数据
System.out.println("from 客户端 " + new String(byteBuffer.array(), 0, byteBuffer.position()));
}
}
public static void main(String[] args) throws Exception {
new SelectorDemo().listen();
}
}
AIO模型
AIO是真正的异步IO,它将BIO中阻塞的步骤优化成了非阻塞的,但是目前AIO模型存在不足,它依赖于操作系统,window下的AIO性能很高,但是AIO在linux系统下却不完善。
Reactor模型
Reactor线程模型是一种并发编程思想,而不是一种技术,不是一种语言专有的。
Reactor模型中的三个角色:
- Reactor:负责监听和分配事件,将事件分配给对应的Handler。
- Acceptor:处理客户端的新连接,并分派请求到处理器链中。
- Handler:将自身与事件绑定,执行非阻塞的读写,完成channel中的读写操作。
常见的三种Reactor线程模型
- 单Reactor单线程模型:
模型解读:
Reactor充当selector,监听多路连接的请求
如果是新连接通过Acceptor完成,其他的交由Handler完成
Handler完成业务逻辑的处理:Read -》 业务处理 -》Send。
优点: 结构简单
缺点: 性能低,高并发下消息堆积,可靠性差。
-
单Reactor多线程模型
优点:Handler不会处理业务,而是交由其他线程完成,降低Reactor性能开销。
缺点:高并发下涉及共享数据的互斥和保护,Reactor承担事件的监听和响应,只在主线程中运行,还是存在性能问题。 -
主从Reactor多线程模型
模型解读:
Main Reactor负责监听,用来处理新连接的建立,然后将建立的channel注册给Sub Reactor。
Sub Reactor完成数据交互和业务逻辑的处理。
相当于Main负责接收任务, sub负责真正处理任务。
优点:响应快,可扩展性强,复用性高。
Netty模型
Netty在Reactor模型的基础上,进行优化,对于三种Reactor线程模型都有很好的支持。
BossGroup:相当于Main Reactor,负责处理新连接。
NioEventLoop:表示不断循环处理任务的线程,它监听了绑定在它上面的的读写事件。
Pipeline(管道):真正执行业务逻辑的处理,Pipeline中会有多个channelHandler。
Netty快速入门
服务端代码
依赖
<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.50.Final</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- java编译插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
MyRpcServer
public class MyRpcServer {
public void start(int port) throws Exception {
// BossGroup: 只接受客户端请求
EventLoopGroup boss = new NioEventLoopGroup(1);
// workerGroup
EventLoopGroup worker = new NioEventLoopGroup(2);
// 服务启动类
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(boss, worker) // 设置线程组
.channel(NioServerSocketChannel.class) // 配置server通道
.childHandler(new MyChannelInitializer()); // worker线程处理器
// 通道绑定端口
ChannelFuture future = serverBootstrap.bind(port).sync();
System.out.println("服务器启动完成, 端口号:" + port);
// 等待服务端监听端口关闭
future.channel().closeFuture().sync();
} finally {
// 关闭线程组
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
MyChannelInitializer
public class MyChannelInitializer extends ChannelInitializer<SocketChannel> {
/**
* 将处理操作的Handler添加到管道中
*
* @param socketChannel
* @throws Exception
*/
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new MyChannelHandler());
}
}
MyChannelHandler
public class MyChannelHandler extends ChannelInboundHandlerAdapter {
/**
* 获取客户端发来的数据
*
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 读取客户端发来的数据
ByteBuf buf = (ByteBuf) msg; // ByteBuffer不好用, netty自己实现了ByteBuf
String msgStr = buf.toString(CharsetUtil.UTF_8);
System.out.println("客户端发来数据:" + msgStr);
// 向客户端发送响应
ctx.writeAndFlush(Unpooled.copiedBuffer("ok" , CharsetUtil.UTF_8));
}
/**
* 异常处理
*
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.fireExceptionCaught(cause);
}
}
测试
public class TestServer {
@Test
public void testServer() throws Exception {
MyRpcServer server = new MyRpcServer();
server.start(5566);
}
}
测试结果
服务器启动完成, 端口号:5566
客户端发来数据:123
客户端发来数据:456
客户端代码
MyRpcClient
public class MyRpcClient {
public void start(String host, int port) throws Exception {
// 客户端只需要发送请求, 因此, 只需要工作线程组
EventLoopGroup worker = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(worker) // 设置线程组
.channel(NioSocketChannel.class) // 配置客户端通道
.handler(new MyClientChannelHandler());
// 开启连接
ChannelFuture future = bootstrap.connect(host, port).sync();
// 关闭
future.channel().closeFuture().sync();
} finally {
worker.shutdownGracefully();
}
}
}
MyClientChannelHandler
public class MyClientChannelHandler extends SimpleChannelInboundHandler<ByteBuf> {
/**
* 读取服务端的数据
*
* @param channelHandlerContext
* @param byteBuf
* @throws Exception
*/
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {
System.out.println("接收到服务端数据:" + byteBuf.toString(CharsetUtil.UTF_8));
}
/**
* 向服务端发送数据
*
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
String msg = "test";
ctx.writeAndFlush(Unpooled.copiedBuffer(msg, CharsetUtil.UTF_8));
}
/**
* 异常处理
*
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
测试
public class TestDemo {
@Test
public void testServer() throws Exception {
MyRpcServer server = new MyRpcServer();
server.start(5566);
}
@Test
public void testClient() throws Exception {
MyRpcClient client = new MyRpcClient();
client.start("127.0.0.1", 5566);
}
}
测试结果
服务端:
服务器启动完成, 端口号:5566
客户端发来数据:test
客户端:
接收到服务端数据:ok
Netty核心组件
Channel
Channel理解为socket理解,Netty的Channel接口提供了很多的API,降低了直接使用socket的不便。
- NioSocketChannel,NIO的客户端 TCP Socket 连接。
- NioServerSocketChannel,NIO的服务器端 TCP Socket 连接。
- NioDatagramChannel, UDP 连接。
- NioSctpChannel,客户端 Sctp 连接。
- NioSctpServerChannel,Sctp 服务器端连接,这些通道涵盖了 UDP 和 TCP ⽹络 IO 以及⽂件IO。
EventLoop,EventLoopGroup
说明:
入站:服务器接收消息
出站:服务器发出消息
在 Netty 中每个 Channel 都会被分配到⼀个 EventLoop。⼀个 EventLoop 可以服务于多个 Channel,每个 EventLoop 会占⽤⼀个 Thread。
EventLoopGroup 是⽤来⽣成 EventLoop 的。
上图关系为:
- ⼀个 EventLoopGroup 包含⼀个或者多个 EventLoop;
- ⼀个 EventLoop 在它的⽣命周期内只和⼀个 Thread 绑定;
- 所有由 EventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理;
- ⼀个 Channel 在它的⽣命周期内只注册于⼀个 EventLoop;
- ⼀个 EventLoop 可能会被分配给⼀个或多个 Channel。
ChannelPipeline
数据传输过程中,往往有很多业务逻辑要处理,比如:编解码,读写操作等等。
每种操作都需要有对应的channelHandler处理。因此,每个Channel都维护了一个ChannelPipeline,一个ChannelPipeline中维护了一个ChannelHandler链表。
Bootstrap
Bootstrap是引导的意思,它的作⽤是配置整个Netty程序,将各个组件都串起来,最后绑定端⼝、启动
Netty服务。
Netty中提供了2种类型的引导类,⼀种⽤于客户端(Bootstrap),⽽另⼀种(ServerBootstrap)⽤于服务
器。
区别:
- ServerBootstrap 将绑定到⼀个端⼝,⽽ Bootstrap 则是由想要连接到远程节点的客户端应⽤程序所使⽤的。
- 引导⼀个客户端只需要⼀个EventLoopGroup,但是⼀个ServerBootstrap则需要两个。因为服务器需要两组不同的 Channel。
- 第⼀组将只包含⼀个 ServerChannel,代表服务器⾃身的已绑定到某个本地端⼝的正在监听
的套接字。 - 第⼆组将包含所有已创建的⽤来处理传⼊客户端连接。
- 第⼀组将只包含⼀个 ServerChannel,代表服务器⾃身的已绑定到某个本地端⼝的正在监听
Future
Future提供了⼀种在操作完成时通知应⽤程序的⽅式。Netty提供了它自己的实现ChannelFuture。
Netty的字节缓冲区ByteBuf
ByteBuf结构说明:
Netty的ByteBuf替代了JDK NIO中的ByteBuffer,原因是ByteBuffer的使用过于负责,不利于开发。
ByteBuf中存在两个索引,readerIndex和writerIndex。
- 当从ByteBuf中读取数据时候,readerIndex 会随着读取的字节数的增加而递增。
- 当将数据写入ByteBuf中时,writerIndex会随着写入的字节数的增加而递增。
- 当readerIndex > writerIndex,会抛出IndexOutOfBoundsException。
基本使用
读取操作
public class TestBuf {
@Test
public void testRead() {
//构造
ByteBuf byteBuf = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8);
System.out.println("byteBuf的容量为:" + byteBuf.capacity());
System.out.println("byteBuf的可读容量为:" + byteBuf.readableBytes());
System.out.println("byteBuf的可写容量为:" + byteBuf.writableBytes());
while (byteBuf.isReadable()){ // 法 :内部通过移动readerIndex进 读取
System.out.println((char)byteBuf.readByte());
}
// 法 :通过下标直接读取
// for (int i = 0; i < byteBuf.readableBytes(); i++) {
// System.out.println((char)byteBuf.getByte(i));
// }
// 法三:转化为byte[]进 读取
// byte[] bytes = byteBuf.array();
// for (byte b : bytes) {
// System.out.println((char)b);
//
// }
}
}
写入操作
@Test
public void testWrite() {
//构造空的字节缓冲区,初始⼤⼩为10,最⼤为20
ByteBuf byteBuf = Unpooled.buffer(10,20);
System.out.println(“byteBuf的容量为:” + byteBuf.capacity());
System.out.println(“byteBuf的可读容量为:” + byteBuf.readableBytes());
System.out.println(“byteBuf的可写容量为:” + byteBuf.writableBytes());
for (int i = 0; i < 5; i++) {
byteBuf.writeInt(i); //写⼊int类型,⼀个int占4个字节
}
System.out.println(“ok”);
System.out.println(“byteBuf的容量为:” + byteBuf.capacity());
System.out.println(“byteBuf的可读容量为:” + byteBuf.readableBytes());
System.out.println(“byteBuf的可写容量为:” + byteBuf.writableBytes());
while (byteBuf.isReadable()){
System.out.println(byteBuf.readInt());
}
}
丢弃已读字节
@Test
public void testDiscardReadByte() {
ByteBuf byteBuf = Unpooled.copiedBuffer("hello world",
CharsetUtil.UTF_8);
System.out.println("byteBuf的容量为:" + byteBuf.capacity());
System.out.println("byteBuf的可读容量为:" + byteBuf.readableBytes());
System.out.println("byteBuf的可写容量为:" + byteBuf.writableBytes());
byteBuf.clear(); //重置readerIndex 、 writerIndex 为0
System.out.println("byteBuf的容量为:" + byteBuf.capacity());
System.out.println("byteBuf的可读容量为:" + byteBuf.readableBytes());
System.out.println("byteBuf的可写容量为:" + byteBuf.writableBytes());
}
使用模式
分类
-
堆缓冲区(HeapByteBuf)
内存分配和回收速度比较快,可以被GC回收,但是如果进行socket的IO读写,需要额外多做一次 内存拷贝,性能有所下降 -
直接缓冲区(DirectByteBuf)
非堆内存,内存分配和回收的速度会慢一些,而且需要手动回收,但是进行socket的IO的读写,会减少一次内存考虑,性能会有所提升。 -
复合缓冲区
两类缓冲区聚合在⼀起。Netty 提供了⼀个 CompsiteByteBuf,可以将堆缓冲区和直接缓冲区的数据放在⼀起,让使⽤更加⽅便。
Netty默认使用的是DirectByteBuf,如果需要使用HeapByteBuf模式,需要设置参数;
System.setProperty("io.netty.noUnsafe", "true");
serverBootstrap.childOption(ChannelOption.ALLOCATOR,UnpooledByteBufAllocator.DEFAULT);
ByteBuf的内存分配
PooledByteBufAllocator :ByteBuf的对象的池化,可以提高性能,减少内存碎片
UnpooledByteBufAllocator:没有实现对象的池化,每次都会创建对象的实例。
Netty模式使用了PooledByteBufAllocator 。
//可以在引导类中设置⾮池化模式
serverBootstrap.childOption(ChannelOption.ALLOCATOR, UnpooledByteBufAllocator.DEFAULT);
ByteBuf的释放
- 手动释放:ReferenceCountUtil.release(byteBuf); (不推荐)
- 自动释放:
- 入站的TailHandler释放:ctx.fireChannelRead(msg); //将接受的ByteBuf向下传递
- 继承SimpleChannelInboundHandler
- HeadHandler的出站释放: 在出站流程开始的时候,通过调⽤ ctx.writeAndFlush(msg)
小结:
1. 入站流程中,如果对元消息不处理,调用ctx.fireChannelReal(msg); 由TaliHandler完成自动释放
2. 如果在入站流程中,截断了,可以继承SimpleChannelInboundHandler,完成释放
3. 入站处理中,如果将原消息转化为新的消息并调用ctx.fireChannelRead(newMsg)往下传,那必须把原消息release掉;
4. 入站处理中, 如果已经不再调⽤ ctx.fireChannelRead(msg) 传递任何消息,也没有继承SimpleChannelInboundHandler 完成⾃动释放,那更要把原消息release掉;
5. 出站处理过程中,申请分配到的 ByteBuf,通过 HeadHandler 完成⾃动释放。
Netty编解码器
什么是编解码器
网络传输过程中,以字节流方式传递。客户端将数据转化为字节,称之为编码。服务端获取字节流将字节转化为原来的数据格式,称之为解码。编码器负责出站数据操作,解码器负责入站数据操作。
解码器
Netty中提供了ByteToMessageDecoder的抽象实现,⾃定义解码器只需要继承该类,实现decode()即
可。常见的Netty提供的解码器:
- RedisDecoder 基于Redis协议的解码器
- XmlDecoder 基于XML格式的解码器
- JsonObjectDecoder 基于json数据格式的解码器
- HttpObjectDecoder 基于http协议的解码器
Netty也提供了MessageToMessageDecoder,将⼀种格式转化为另⼀种格式的解码器,也提供了⼀些实现:
- StringDecoder 将接收到ByteBuf转化为字符串
- ByteArrayDecoder 将接收到ByteBuf转化字节数组
- Base64Decoder 将由ByteBuf或US-ASCII字符串编码的Base64解码为ByteBuf。
编码器
Netty提供了MessageToByteEncoder的抽象实现,它实现了ChannelOutboundHandler接⼝,本质上也是ChannelHandler,常见的Netty的编码器:
- ObjectEncoder 将对象(需要实现Serializable接⼝)编码为字节流
- SocksMessageEncoder 将SocksMessage编码为字节流
- HAProxyMessageEncoder 将HAProxyMessage编码成字节流
Netty也提供了MessageToMessageEncoder,将⼀种格式转化为另⼀种格式的编码器,也提供了⼀些
实现:
- RedisEncoder 将Redis协议的对象进⾏编码
- StringEncoder 将字符串进⾏编码操作
- Base64Encoder 将Base64字符串进⾏编码操作
Demo(Http服务器)
Server
public class MyRpcServer {
public void start(int port) throws Exception {
// BossGroup: 只接受客户端请求
EventLoopGroup boss = new NioEventLoopGroup(1);
// workerGroup
EventLoopGroup worker = new NioEventLoopGroup(2);
// 服务启动类
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(boss, worker) // 设置线程组
.channel(NioServerSocketChannel.class) // 配置server通道
.childHandler(new MyChannelInitializer()); // worker线程处理器
// 通道绑定端口
ChannelFuture future = serverBootstrap.bind(port).sync();
System.out.println("服务器启动完成, 端口号:" + port);
// 等待服务端监听端口关闭
future.channel().closeFuture().sync();
} finally {
// 关闭线程组
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
MyChannelInitializer
public class MyChannelInitializer extends ChannelInitializer<SocketChannel> {
/**
* 将处理操作的Handler添加到管道中
*
* @param socketChannel
* @throws Exception
*/
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline()
.addLast(new HttpRequestDecoder()) // 添加http解码器
// 将http请求中的uri以及请求体聚合成⼀个完整的FullHttpRequest对象
.addLast(new HttpObjectAggregator(1024 * 128))
.addLast(new HttpResponseEncoder()) // http服务器需要给客户端响应,所以添加http响应编码器
.addLast(new ChunkedWriteHandler()) // ⽀持异步的⼤⽂件传输,防⽌内存溢出
.addLast(new MyChannelHandler());
}
}
MyChannelHandler
public class MyChannelHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
// 解析request获取参数
Map<String, String> param = new RequestParser(request).parse(); // 解析请求获取request中的参数
String name = param.get("name");
// 构造响应
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=utf-8");
String responseStr = "<h1>" + "你好," + name + "</h1>";
response.content().writeBytes(Unpooled.copiedBuffer(responseStr, CharsetUtil.UTF_8));
ctx.writeAndFlush(response) // 给客户端响应
.addListener(ChannelFutureListener.CLOSE); // 因为http是一个短连接,所以在操作完成后需要将channel关闭
}
/**
* 异常处理
*
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.fireExceptionCaught(cause);
}
}
RequestParser
public class RequestParser {
private FullHttpRequest fullReq;
/**
* 构造⼀个解析器
* @param req
*/
public RequestParser(FullHttpRequest req) {
this.fullReq = req;
}
/**
* 解析请求参数
* @return 包含所有请求参数的键值对, 如果没有参数, 则返回空Map
*
* @throws Exception
*/
public Map<String, String> parse() throws Exception {
HttpMethod method = fullReq.method();
Map<String, String> paramMap = new HashMap<>();
if (HttpMethod.GET == method) {
// 是GET请求
QueryStringDecoder decoder = new QueryStringDecoder(fullReq.uri());
decoder.parameters().entrySet().forEach( entry -> {
// entry.getValue()是⼀个List, 只取第⼀个元素
paramMap.put(entry.getKey(), entry.getValue().get(0));
});
} else if (HttpMethod.POST == method) {
// 是POST请求
HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(fullReq);
decoder.offer(fullReq);
List<InterfaceHttpData> paramList = decoder.getBodyHttpDatas();
for (InterfaceHttpData param : paramList) {
Attribute data = (Attribute) param;
paramMap.put(data.getName(), data.getValue());
}
} else {
// 不⽀持其它⽅法
throw new RuntimeException("不⽀持其它⽅法"); // 这是个⾃定义的异常, 可删掉这⼀⾏
}
return paramMap;
}
}
测试
Demo(对象的编解码)
我们在实际开发过程中,最常用的是将一个对象转换成一个字节流进行传输,Netty也支持Object对象的编解码,只需要java bean实现java.io.Serializable接⼝。
Client
MyClientInitializer
public class MyClientInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline()
.addLast(new ObjectEncoder())
.addLast(new MyClientChannelHandler());
}
}
MyClientChannelHandler
public class MyClientChannelHandler extends SimpleChannelInboundHandler<ByteBuf> {
/**
* 读取服务端的数据
*
* @param channelHandlerContext
* @param byteBuf
* @throws Exception
*/
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {
System.out.println("接收到服务端数据:" + byteBuf.toString(CharsetUtil.UTF_8));
}
/**
* 向服务端发送数据
*
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
User user = new User("张三", 18, "男");
ctx.writeAndFlush(user);
}
/**
* 异常处理
*
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
Server
MyChannelInitializer
public class MyChannelInitializer extends ChannelInitializer<SocketChannel> {
/**
* 将处理操作的Handler添加到管道中
*
* @param socketChannel
* @throws Exception
*/
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline()
.addLast(new ObjectDecoder(ClassResolvers.weakCachingResolver(this.getClass().getClassLoader())))
.addLast(new MyChannelHandler());
}
}
MyChannelHandler
public class MyChannelHandler extends SimpleChannelInboundHandler<User> {
/**
* 获取客户端发来的数据
*
* @param ctx
* @param user
* @throws Exception
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, User user) throws Exception {
//获取到user对象
System.out.println(user);
ctx.writeAndFlush(Unpooled.copiedBuffer("ok", CharsetUtil.UTF_8));
}
/**
* 异常处理
*
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.fireExceptionCaught(cause);
}
}
User
public class User implements Serializable {
private static final long serialVersionUID = -89217070354741790L;
private String name;
private Integer age;
private String sex;
public User(String name, Integer age, String sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", sex='" + sex + '\'' +
'}';
}
}
测试结果
虽然JDK序列化使用比较方便,但是性能较差,所以我们通常会采用第三方序列化框架,例如:Hessian。
TCP粘包/拆包问题
什么是TCP粘包/拆包问题
TCP传递的是流,一个没有界限的数据。服务端在接收到客户端发出的数据的时候,不知道这是一条数据,还是多条数据。因此,服务端和客户端需要约定好拆包的规则,客户端按照此规则粘包,服务端按照此规则拆包。
TCP粘包/拆包问题的解决方法
- 数据包中添加头,头中存储了数据的大小。服务端根据投中数据的大小,来拆包。
- 客户端发送定长的数据,不足的补0.
- 数据包之间设置边界,如在边界处添加特殊字符,服务端根据特殊字符来确定边界,进行拆包。
以上三种方法中,最常用的是第一种。
Demo
编解码器
编码器
public class MyProtocolEncoder extends MessageToByteEncoder<MyProtocol> {
@Override
protected void encode(ChannelHandlerContext ctx, MyProtocol myProtocol, ByteBuf byteBuf) throws Exception {
byteBuf.writeInt(myProtocol.getLength()); // 将数据长度写入
byteBuf.writeBytes(myProtocol.getBody()); // 发送数据
}
}
解码器
public class MyProtocolDecoder extends ReplayingDecoder<Void> {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf byteBuf, List<Object> list) throws Exception {
int length = byteBuf.readInt(); // 读取头中的数据的长度
byte[] body = new byte[length];
byteBuf.readBytes(body); // 读取长度为length的字节数据
MyProtocol myProtocol = new MyProtocol();
myProtocol.setBody(body);
myProtocol.setLength(length);
list.add(myProtocol);
}
}
自定义规则
public class MyProtocol {
private int length;
private byte[] body;
public int getLength() {
return length;
}
public void setLength(int length) {
this.length = length;
}
public byte[] getBody() {
return body;
}
public void setBody(byte[] body) {
this.body = body;
}
}
服务端
MyChannelHandler
public class MyChannelHandler extends SimpleChannelInboundHandler<MyProtocol> {
private int count;
/**
* 获取客户端发来的数据
*
* @param ctx
* @param msg
* @throws Exception
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, MyProtocol msg) throws Exception {
System.out.println("服务端接收到消息:" + new String(msg.getBody(),
CharsetUtil.UTF_8));
System.out.println("服务端接收到消息数量:" + (++count));
//获取到user对象
byte[] data = "ok".getBytes(CharsetUtil.UTF_8);
MyProtocol myProtocol = new MyProtocol();
myProtocol.setLength(data.length);
myProtocol.setBody(data);
ctx.writeAndFlush(myProtocol);
}
/**
* 异常处理
*
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.fireExceptionCaught(cause);
}
}
MyChannelInitializer
public class MyChannelInitializer extends ChannelInitializer<SocketChannel> {
/**
* 将处理操作的Handler添加到管道中
*
* @param socketChannel
* @throws Exception
*/
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline()
.addLast(new MyProtocolDecoder())
.addLast(new MyProtocolEncoder())
.addLast(new MyChannelHandler());
}
}
客户端
MyClientChannelHandler
public class MyClientChannelHandler extends SimpleChannelInboundHandler<MyProtocol> {
private int count;
/**
* 读取服务端的数据
*
* @param channelHandlerContext
* @param msg
* @throws Exception
*/
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, MyProtocol msg) throws Exception {
System.out.println("接收到服务端数据:" + new String(msg.getBody(), CharsetUtil.UTF_8));
System.out.println("接收到服务端的消息数量:" + (++count));
}
/**
* 向服务端发送数据
*
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (int i = 0; i < 10; i++) {
byte[] body = "from client message!".getBytes(CharsetUtil.UTF_8);
MyProtocol protocol = new MyProtocol();
protocol.setLength(body.length);
protocol.setBody(body);
ctx.writeAndFlush(protocol);
}
}
/**
* 异常处理
*
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
MyClientInitializer
public class MyClientInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline()
.addLast(new MyProtocolDecoder())
.addLast(new MyProtocolEncoder())
.addLast(new MyClientChannelHandler());
}
}
注意:
TCP是基于流的,只保证接收到数据包分⽚顺序,⽽不保证接收到的数据包每个分⽚⼤⼩。因此在使⽤
ReplayingDecoder时,即使不存在多线程,同⼀个线程也可能多次调⽤decode()⽅法。在decode中修
改ReplayingDecoder的类变量时必须⼩⼼谨慎。
//这是⼀个错误的例⼦:
//消息中包含了2个integer,代码中decode⽅法会被调⽤两次,此时队列size不等于2,这段代码达不到期望结果。
public class MyDecoder extends ReplayingDecoder<Void> {
private final Queue<Integer> values = new LinkedList<Integer>();
@Override
public void decode(ByteBuf buf, List<Object> out) throws Exception {
// A message contains 2 integers.
values.offer(buf.readInt());
values.offer(buf.readInt());
assert values.size() == 2;
out.add(values.poll() + values.poll());
}
}
//正确的做法:
public class MyDecoder extends ReplayingDecoder<Void> {
private final Queue<Integer> values = new LinkedList<Integer>();
@Override
public void decode(ByteBuf buf, List<Object> out) throws Exception {
// Revert the state of the variable that might have been changed
// since the last partial decode.
values.clear();
// A message contains 2 integers.
values.offer(buf.readInt());
values.offer(buf.readInt());
// Now we know this assertion will never fail.
assert values.size() == 2;
out.add(values.poll() + values.poll());
}
}
Netty源码解析
我们自己在分析Netty的源码的时候主要看服务端的启动过程和连接请求的过程。
服务端启动过程
创建服务端Channel
- 查看服务端的源码,入口在ServerBootstrap的bind()方法。
- AbstractBootstrap中的initAndRegister()进⾏创建Channel,创建Channel的⼯作由ReflectiveChannelFactory反射类中的newChannel()⽅法完成。
- NioServerSocketChannel中的构造⽅法中,通过jdk nio底层的SelectorProvider打开ServerSocketChannel。
- 在AbstractNioChannel的构造⽅法中,设置channel为⾮阻塞:ch.configureBlocking(false);
- 通过的AbstractChannel的构造⽅法,创建了id、unsafe、pipeline内容。
- 通过NioServerSocketChannelConfig获取tcp底层的⼀些参数
Netty的优化建议
1. 零拷贝
Netty的零拷贝体现在三个方面
- ByteBuf默认使用的池化的DirectByteBuf(堆外内存)
- CompositeByteBuf将多个ByteBuf封装成一个ByteBuf,在添加ByteBuf不需要进程拷贝
- 文件传输类DefaultFileRegion的transferTo方法将文件送给目标channel,不需要进行循环拷贝。
2. 使用EventLoop的任务调度
在EventLoop的⽀持线程外使⽤channel:
channel.eventLoop().execute(new Runnable() {
@Override
public void run() {
channel.writeAndFlush(data)
}
});
⽽不是直接使⽤channel.writeAndFlush(data);
前者会直接放⼊channel所对应的EventLoop的执⾏队列,⽽后者会导致线程的切换。
3. 减少ChannelHandler的创建
如果channelhandler是⽆状态的(即不需要保存任何状态参数),那么使⽤Sharable注解,并在
bootstrap时只创建⼀个实例,减少GC。否则每次连接都会new出handler对象。
@ChannelHandler.Sharable
public class StatelessHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) {}
}
public class MyInitializer extends ChannelInitializer<Channel> {
private static final ChannelHandler INSTANCE = new StatelessHandler();
@Override
public void initChannel(Channel ch) {
ch.pipeline().addLast(INSTANCE);
}
4. 减少ChannelPipline的调⽤⻓度
5. 一些参数的配置
ServerBootstrap启动时,通常bossGroup只需要设置为1即可,因为ServerSocketChannel在初始化阶
段,只会注册到某⼀个eventLoop上,⽽这个eventLoop只会有⼀个线程在运⾏,所以没有必要设置为
多线程。⽽ IO 线程,为了充分利⽤ CPU,同时考虑减少线上下⽂切换的开销,通常设置为 CPU 核数的
两倍,这也是 Netty 提供的默认值。
在对于响应时间有⾼要求的场景,使⽤.childOption(ChannelOption.TCP_NODELAY, true)
和.option(ChannelOption.TCP_NODELAY, true)来禁⽤nagle算法,不等待,⽴即发送。