Netty编程
NIO编程
关于NIO相关的文章网上也有很多,这里不打算详细深入分析,下面简单描述一下NIO是如何解决BIO的线程资源受限,线程切换效率低下,以字节为单位三个问题的。
1. 解决线程资源受限
NIO编程模型中,新来一个连接不再创建一个新的线程,而是可以把这条连接直接绑定到某个固定的线程,然后这条连接所有的读写都由这个线程来负责,那么他是怎么做到的?我们用一幅图来对比一下IO与NIO
如上图所示,IO模型中,一个连接来了,会创建一个线程,对应一个while死循环,死循环的目的就是不断监测这条连接上是否有数据可以读,大多数情况下,1w个连接里面同一时刻只有少量的连接有数据可读,因此,很多个while死循环都白白浪费掉了,因为读不出啥数据。
而在NIO模型中,他把这么多while死循环变成一个死循环,这个死循环由一个线程控制,那么他又是如何做到一个线程,一个while死循环就能监测1w个连接是否有数据可读的呢? 这就是NIO模型中selector的作用,一条连接来了之后,现在不创建一个while死循环去监听是否有数据可读了,而是直接把这条连接注册到selector上,然后,通过检查这个selector,就可以批量监测出有数据可读的连接,进而读取数据,下面我再举个非常简单的生活中的例子说明IO与NIO的区别。
在一家幼儿园里,小朋友有上厕所的需求,小朋友都太小以至于你要问他要不要上厕所,他才会告诉你。幼儿园一共有100个小朋友,有两种方案可以解决小朋友上厕所的问题:
- 每个小朋友配一个老师。每个老师隔段时间询问小朋友是否要上厕所,如果要上,就领他去厕所,100个小朋友就需要100个老师来询问,并且每个小朋友上厕所的时候都需要一个老师领着他去上,这就是IO模型,一个连接对应一个线程。
- 所有的小朋友都配同一个老师。这个老师隔段时间询问所有的小朋友是否有人要上厕所,然后每一时刻把所有要上厕所的小朋友批量领到厕所,这就是NIO模型,所有小朋友都注册到同一个老师,对应的就是所有的连接都注册到一个线程,然后批量轮询。
2. 解决线程切换效率低下
由于NIO模型中线程数量大大降低,线程切换效率因此也大幅度提高
3. 解决IO读写以字节为单位
NIO解决这个问题的方式是数据读写不再以字节为单位,而是以字节块为单位。IO模型中,每次都是从操作系统底层一个字节一个字节地读取数据,而NIO维护一个缓冲区,每次可以从这个缓冲区里面读取一块的数据, 这就好比一盘美味的豆子放在你面前,你用筷子一个个夹(每次一个),肯定不如要勺子挖着吃(每次一批)效率来得高。
简单讲完了JDK NIO的解决方案之后,我们接下来使用NIO的方案替换掉IO的方案,我们先来看看,如果用JDK原生的NIO来实现服务端,该怎么做。
Netty编程
1.netty简介
用一句简单的话来说就是:Netty封装了JDK的NIO,让你用得更爽,你不用再写一大堆复杂的代码了。 用官方正式的话来说就是:Netty是一个异步事件驱动的网络应用框架,用于快速开发可维护的高性能服务器和客户端。
下面是我总结的使用Netty不使用JDK原生NIO的原因
- 使用JDK自带的NIO需要了解太多的概念,编程复杂,一不小心bug横飞
- Netty底层IO模型随意切换,而这一切只需要做微小的改动,改改参数,Netty可以直接从NIO模型变身为IO模型
- Netty自带的拆包解包,异常检测等机制让你从NIO的繁重细节中脱离出来,让你只需要关心业务逻辑
- Netty解决了JDK的很多包括空轮询在内的bug
- Netty底层对线程,selector做了很多细小的优化,精心设计的reactor线程模型做到非常高效的并发处理
- 自带各种协议栈让你处理任何一种通用协议都几乎不用亲自动手
- Netty社区活跃,遇到问题随时邮件列表或者issue
- Netty已经历各大rpc框架,消息中间件,分布式通信中间件线上的广泛验证,健壮性无比强大
2.netty的使用
- 首先引入maven依赖
也可以 fail ->project Structure -> Modules ->dependencies 右侧的+号
Library ->new Library -> from maven 输入 io.netty:netty-all
-
服务端的实现:
NettyServer.java
package java_netty.simple;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class NettyServer {
public static void main(String[] args) throws InterruptedException {
//创建BossGroup 和WokerGroup
//说明:
//1.创建两个线程组 bossgroup和workergroup
//2.BossGroup 只是处理连接请求
//3.wokergroup 真正和客户端业务处理
//4.两个都是无限循环
//5. bossGroup, 和 workerGroup 含有的子线程(NioEventLoop)的个数
// 默认实际( cpu核数 * 2)
EventLoopGroup bossGroup=new NioEventLoopGroup(1);
EventLoopGroup workerGroup=new NioEventLoopGroup();
try {
//创建服务器端的启动对象,配置参数
ServerBootstrap bootstrap = new ServerBootstrap();
//使用链式编程进行设置
bootstrap.group(bossGroup,workerGroup)//设置两个线程组
.channel(NioServerSocketChannel.class)//使用NioSocketChannel 作为服务器的通道实现
.option(ChannelOption.SO_BACKLOG,128)//设置线程队列得到连接个数
.childOption(ChannelOption.SO_KEEPALIVE,true)//设置保持活动连接状态
.childHandler(new ChannelInitializer<SocketChannel>() {//创建一个通道测试对象(匿名类)
//给pipeline 设置处理器
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new NettyServerHandler());//给管道的最后添加一个处理器(即写的NettyServerHandler)
}
});// 给我们的workergroup 的EventLoopGroup 对应的管道设置处理器
System.out.println("服务器 is ready");
//绑定一个端口,并且同步处理,生成了一个ChannelFuture对象。
//相当于启动服务器并把端口端口
ChannelFuture channelFuture = bootstrap.bind(6668).sync();
//对关闭通道进行监听(当有关闭操作的时候会进行监听
channelFuture.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();//优雅的关闭
workerGroup.shutdownGracefully();//优雅的关闭
}
}
}
-
boos
对应了IOServer.java
中的接收新连接线程,主要负责创建新连接 -
worker
对应IOClient.java
中的负责读取数据的线程,主要用于读取数据以及业务逻辑处理<!--说明:--> <!--1.创建两个线程组 bossgroup和workergroup--> <!--2.`BossGroup` 只是处理连接请求--> <!--3.`wokergroup` 真正和客户端业务处理--> <!--4.两个都是无限循环--> <!--5. bossGroup`, 和 workerGroup 含有的子线程(NioEventLoop)的个数--> <!--( cpu核数 * 2)-->
-
服务端的handler处理器
NettyServerHandler.java
package java_netty.simple;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
/*
说明:
1.自定义一个Handler 需要继续netty, 规定好某个HandlerAdapter
2.这时的Handler 才能算一个Hander
*/
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
//读取数据实际,(这里我们可以读取客户端发送的信息)
/*
1.ChannelHandlerContext ctx: 上下文对象,含有管道 pipeline ,通道channel 地址
2.Object msg 客户端发送的数据,以对象的形式传递,默认是Obj类
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("服务器读取线程 : "+Thread.currentThread().getName());
System.out.println("Server ctx=="+ctx+" mgs=="+msg);
//将mgs 转换成一个ByteBuf
//ByteBuf 是Netty 提供的 ,不是Nio的ByteBuffer
ByteBuf buf =(ByteBuf)msg;
System.out.println("客户端发送的信息是:"+buf.toString(CharsetUtil.UTF_8));
System.out.println("客户端地址:"+ctx.channel().remoteAddress());
}
//数据读取完毕:
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// writeAndFlush 是write 和 flush 的合并,写到缓冲区再刷新
//一般来讲,对这个发送的数据进行编码
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端",CharsetUtil.UTF_8));
}
//处理异常 ,出现异常关闭通道
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
- 客户端
NettyClient.java
package java_netty.simple;
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;
public class NettyClient {
public static void main(String[] args) throws InterruptedException {
//客户端需要一个事件循环组
EventLoopGroup group = new NioEventLoopGroup();
try {
//创建客户端启动对象
//注意客户端使用的是Bootstrp,而服务端是ServerBootstrp
Bootstrap bootstrap = new Bootstrap();
//链式编程 设置相关参数
bootstrap.group(group)//设置线程组
.channel(NioSocketChannel.class)// 设置客户端通道的实现类(反射)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new NettyClientHandler());//加入自己的处理器
}
});
System.out.println("客户端ok..");
//启动客户端去连接服务器,
// 关于ChannelFuture 要分析 涉及到netty的异步模型。
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();
//给关闭通道 进行一个监听
channelFuture.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
}
客户端程序中, group
对应了我们 IOClient.java
中main函数起的线程。
-
客户端的handler处理器
NettyClientHandler.java
package java_netty.simple;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
// Inbound 是入栈的操作
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
//当通道就绪时 就会触发该方法
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("Clinet "+ctx);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,server", CharsetUtil.UTF_8));
}
//当通道有读取事件时会触发
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
System.out.println("服务器回复的信息:"+buf.toString(CharsetUtil.UTF_8));
System.out.println("服务器的地址 :"+ctx.channel().remoteAddress());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
Netty对NIO封装得如此完美,写出来的代码非常优雅,另外一方面,使用Netty之后,网络通信这块的性能问题几乎不用操心。
Netty粘包拆包问题:
1. 粘包问题:
TCP是一个 流 的协议,所谓流就是没有界限的遗传数据,大家可以想象一下,如果河里的水相当于数据,他们是连成一片的,没有分界线,TCP底层并不了解上层的业务数据具体含义,他会根据TCP缓冲区的实际情况进行包的划分,也就是说在业务上,我们一个完整的包可能会被TCP分成多个包进行发送,也可能把多个小包封装成一个大的数据包发送出去,这就是所谓的粘包问题。
例如在tcp包里 运行客户端向服务端发送10条信息"Hello ,Server"。
10条信息 被分成了6个包发送:
这就是粘包问题
2.拆包
这里使用自定义协议包+编码器+解压器解决:
具体代码实现拆包:
核心代码: 协议包,编解码器
目录结构
- 服务端 (MyServer.java)
package java_netty.protocoltcp;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class MyServer {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup bossGroup=new NioEventLoopGroup(1 );
EventLoopGroup workerGroup=new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup,workerGroup)//设置两个线程组
.channel(NioServerSocketChannel.class)//使用NioSocketChannel 作为服务器的通道实现
.childHandler(new MyServerInitalizer());//自定义初始化类
ChannelFuture channelFuture = bootstrap.bind(7000).sync();
channelFuture.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();//优雅的关闭
workerGroup.shutdownGracefully();//优雅的关闭
}
}
}
- 服务端的handler
package java_netty.protocoltcp;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import java.nio.charset.Charset;
import java.util.UUID;
// 处理业务的 handler
public class MyServerHandler extends SimpleChannelInboundHandler<MessageProtocotcp> {
private int count;
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, MessageProtocotcp msg) throws Exception {
//接收到数据,并处理
int len = msg.getLen();
byte[] content = msg.getContent();
System.out.println("服务器接收到的信息如下:");
System.out.println("长度:"+len);
System.out.println("内容:"+new String(content,Charset.forName("utf-8")));
System.out.println("服务器接收到消息包(协议包)数量"+(++this.count));
//回复消息
String responseContent = "你好客户端,你发送的信息已经收到";
int responselen =responseContent.getBytes("utf-8").length;
// int responselen =responseContent.length;
byte[] responseContentBytes = responseContent.getBytes("utf-8");
//构建一个协议包
MessageProtocotcp messageProtocotcp =new MessageProtocotcp();
messageProtocotcp.setLen(responselen);
messageProtocotcp.setContent(responseContentBytes);
//构建完协议包 即可发送
//但需要在ServerHandler中加一个编码器 ClientHandler中加入解码器
channelHandlerContext.writeAndFlush(messageProtocotcp);
}
}
- 自定义的初始化类(MyServerInitalizer.java):给服务器的启动对象配置handler,编码器,解码器
package java_netty.protocoltcp;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
public class MyServerInitalizer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new MyMessageDecoder());// 解码器
pipeline.addLast(new MyMessageEncoder());// 编码器
pipeline.addLast(new MyServerHandler());
}
}
- 客户端:
package java_netty.protocoltcp;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
public class MyClient {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup groups = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(groups).channel(NioSocketChannel.class)// 设置客户端通道的实现类(反射)
.handler(new MyClientInitializer());//自定义一个初始化类
ChannelFuture channelFuture = bootstrap.connect("localhost", 7000).sync();
//给关闭通道 进行一个监听
channelFuture.channel().closeFuture().sync();
}finally {
groups.shutdownGracefully();
}
}
}
- 客户端的handler
package java_netty.protocoltcp;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import java.nio.charset.Charset;
public class MyClientHandler extends SimpleChannelInboundHandler<MessageProtocotcp> {
private int count;
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//使用客户端发送5条数据 呵呵 给服务端,
for(int i=0;i<5;i++){
String mes=" 呵呵服务端,你好";
byte[] content = mes.getBytes(Charset.forName("utf-8"));
int length =mes.getBytes(Charset.forName("utf-8")).length;
//创建协议包对象;
MessageProtocotcp messageProtocotcp=new MessageProtocotcp();
messageProtocotcp.setLen(length);
messageProtocotcp.setContent(content);
ctx.writeAndFlush(messageProtocotcp);
}
}
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, MessageProtocotcp msg) throws Exception {
int len = msg.getLen();
byte[] content = msg.getContent();
System.out.println("客户端接收到的信息如下:");
System.out.println("长度:"+len);
System.out.println("内容:"+new String(content,Charset.forName("utf-8")));
System.out.println("服务器接收到消息包(协议包)数量"+(++this.count));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("异常信息: "+cause.getMessage());
ctx.close();
}
}
- 客户端的初始化类
package java_netty.protocoltcp;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
public class MyClientInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new MyMessageEncoder()); // 加入编码器
pipeline.addLast(new MyMessageDecoder()); // 加入解码器
pipeline.addLast(new MyClientHandler());
}
}
编码器(Decoder):
- 编码器
package java_netty.protocoltcp;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
//编码器
public class MyMessageEncoder extends MessageToByteEncoder<MessageProtocotcp> {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, MessageProtocotcp messageProtocotcp, ByteBuf byteBuf) throws Exception {
System.out.println("MyMessageEncoder encode 方法被调用");
byteBuf.writeInt(messageProtocotcp.getLen());
byteBuf.writeBytes(messageProtocotcp.getContent());
}
}
解码器(Encoder)
- 解码器(Encoder) 这里用readInt()方法可以自动获取length长度
package java_netty.protocoltcp;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ReplayingDecoder;
import java.util.List;
// 解码器
public class MyMessageDecoder extends ReplayingDecoder {
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
System.out.println(" MyMessageDecoder decoder 被调用");
//需要将得到的二进制字节码 -> MessageProtocol 数据包(对象)
int length = byteBuf.readInt(); //自动获取length长度
byte [] content=new byte[length];
byteBuf.readBytes(content);
//封装成 MessageProtocol 对象, 放入 list 传递下一个handler 业务处理
MessageProtocotcp messageProtocotcp = new MessageProtocotcp();
messageProtocotcp.setLen(length);
messageProtocotcp.setContent(content);
list.add(messageProtocotcp);
}
}
协议包
- 协议包(MessageProtocotcp)
(定义长度和发送的数据(一般用字节数组))
package java_netty.protocoltcp;
// 协议包
public class MessageProtocotcp {
//定义长度 ,关键
private int len;
//发送的数据 一般用 字节数组
private byte[] content;
public int getLen() {
return len;
}
public void setLen(int len) {
this.len = len;
}
public byte[] getContent() {
return content;
}
public void setContent(byte[] content) {
this.content = content;
}
}