Netty
整体架构如下
ByteBuf
Netty没有使用NIO的buffer而是用它们自己的缓冲类,作用上来看两者几乎是一致的,但是使用起来ByteBuf会比buffer简单许多,两者的结构也很相似。
源码中的注释,画出了ByteBuf的结构,包括三个指针,被三个指针分成了三个区域
具体结构如下
使用时的注意要点
-
开始时的readerIndex和writerIndex都指向0
-
ByteBuf中的数据开始时初始化为0
-
执行clear()方法时会将readerIndex和writerIndex都指向0,数据不会清空,当写时数据会覆盖
-
每当执行discardReadBytes()方法时都会直接丢弃readerIndex前的数据而且readerIndex变为0,writerIndex变为原来的数值减去readerIndex原来的数值,capacity保持不变,需要注意的是原来的writerIndex和现在的writerIndex之间的数据也不会清空
-
setZero()会将ByteBuf清零(每一个字节设为0),但是指针的位置不会变
-
ByteBuf在writerIndex指针将要超过capacity时会自动扩容
使用例程
创建一个ByteBuf
public static void main(String[] args) {
//创建一个非池化的ByteBuf,大小为10个字节
ByteBuf buffer = Unpooled.buffer(10);
System.out.println("原始ByteBuf为===================>"+buffer.toString());
System.out.println("ByteBuf中的内容为===================>"+ Arrays.toString(buffer.array()));
}
结果如下
原始ByteBuf为===================>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 0, cap: 10)
ByteBuf中的内容为===================>[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
向ByteBuf中写入数据
public static void main(String[] args) {
//创建一个非池化的ByteBuf,大小为10个字节
ByteBuf buffer = Unpooled.buffer(10);
byte[] bytes = {1,2,3,4,5};
buffer.writeBytes(bytes);
System.out.println("原始ByteBuf为===================>"+buffer.toString());
System.out.println("ByteBuf中的内容为===================>"+ Arrays.toString(buffer.array()));
}
结果如下
原始ByteBuf为===================>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 5, cap: 10)
ByteBuf中的内容为===================>[1, 2, 3, 4, 5, 0, 0, 0, 0, 0]
从ByteBuf中读数据
public static void main(String[] args) {
//创建一个非池化的ByteBuf,大小为10个字节
ByteBuf buffer = Unpooled.buffer(10);
byte[] bytes = {1,2,3,4,5};
buffer.writeBytes(bytes);
byte byte1 = buffer.readByte();
byte byte2 = buffer.readByte();
System.out.println("读到的数据是=====================>"+ Arrays.toString(new byte[]{byte1,byte2}));
System.out.println("原始ByteBuf为===================>"+buffer.toString());
System.out.println("ByteBuf中的内容为===================>"+ Arrays.toString(buffer.array()));
}
结果如下
读到的数据是=====================>[1, 2]
原始ByteBuf为===================>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 2, widx: 5, cap: 10)
ByteBuf中的内容为===================>[1, 2, 3, 4, 5, 0, 0, 0, 0, 0]
将读到的数据丢弃
public static void main(String[] args) {
//创建一个非池化的ByteBuf,大小为10个字节
ByteBuf buffer = Unpooled.buffer(10);
byte[] bytes = {1,2,3,4,5};
buffer.writeBytes(bytes);
byte byte1 = buffer.readByte();
byte byte2 = buffer.readByte();
buffer.discardReadBytes();
System.out.println("原始ByteBuf为===================>"+buffer.toString());
System.out.println("ByteBuf中的内容为===================>"+ Arrays.toString(buffer.array()));
}
结果如下
原始ByteBuf为===================>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 3, cap: 10)
ByteBuf中的内容为===================>[3, 4, 5, 4, 5, 0, 0, 0, 0, 0]
使用clear清空指针
public static void main(String[] args) {
//创建一个非池化的ByteBuf,大小为10个字节
ByteBuf buffer = Unpooled.buffer(10);
byte[] bytes = {1,2,3,4,5};
buffer.writeBytes(bytes);
byte byte1 = buffer.readByte();
byte byte2 = buffer.readByte();
buffer.discardReadBytes();
buffer.clear();
System.out.println("原始ByteBuf为===================>"+buffer.toString());
System.out.println("ByteBuf中的内容为===================>"+ Arrays.toString(buffer.array()));
}
结果如下,注意数据并没有清空
原始ByteBuf为===================>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 0, cap: 10)
ByteBuf中的内容为===================>[3, 4, 5, 4, 5, 0, 0, 0, 0, 0]
将ByteBuf清零
public static void main(String[] args) {
//创建一个非池化的ByteBuf,大小为10个字节
ByteBuf buffer = Unpooled.buffer(10);
byte[] bytes = {1,2,3,4,5};
buffer.writeBytes(bytes);
byte byte1 = buffer.readByte();
byte byte2 = buffer.readByte();
buffer.discardReadBytes();
buffer.setZero(0,buffer.capacity());
System.out.println("原始ByteBuf为===================>"+buffer.toString());
System.out.println("ByteBuf中的内容为===================>"+ Arrays.toString(buffer.array()));
}
结果如下,注意指针位置没有改变
原始ByteBuf为===================>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 3, cap: 10)
ByteBuf中的内容为===================>[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
实现自动扩容
public static void main(String[] args) {
//创建一个非池化的ByteBuf,大小为10个字节
ByteBuf buffer = Unpooled.buffer(10);
byte[] bytes = {1,2,3,4,5};
buffer.writeBytes(bytes);
byte byte1 = buffer.readByte();
byte byte2 = buffer.readByte();
buffer.discardReadBytes();
buffer.setZero(0,buffer.capacity());
byte[] bytes1 = {1,2,3,4,5,6,7,8,9,10};
buffer.writeBytes(bytes1);
System.out.println("原始ByteBuf为===================>"+buffer.toString());
System.out.println("ByteBuf中的内容为===================>"+ Arrays.toString(buffer.array()));
}
结果如下
原始ByteBuf为===================>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 13, cap: 64)
ByteBuf中的内容为===================>[0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
pipeline
如果是用Netty实现NIO,不论是客户端还是服务端都有各自的pipeline,而且每个socketchannel都有各自的pipeline,而且各自相互独立,互不影响。
pipeline的大体结构如下,不过服务端的NioServersocketchannel所属的pipeline是不需要我们维护的,我们只需要实现客户端socket和服务端接收到客户端的socket的pipeline。
pipeline都有一个head和tail,这个是固定的,是初始化的时候就存在的,我们所要做的就是将我们要执行的业务代码写成Handler放在pipeline中去。
要加入的Handler要实现其对应的接口,这类接口一般为三类
- Outbound,出栈
- Inbound,入栈
- Duplex,出栈和入栈
这样在有数据进来时就会执行继承Inbound类型的接口的Handler,需要输出数据时就会执行Outbound类型的接口的Handler。
执行的顺序和它们在pipeline中的顺序时一样的,但是它们是分别有序的,就是只有当有数据进来时才会开始执行Inbound类型的接口的Handler,同样的只有当要输出数据时才会执行Outbound类型的接口的Handler。
注意入栈处理器的顺序就是它们在pipeline中的顺序,出栈处理器的顺序与它们在pipeline中的顺序是相反的。
聊天室例程
客户端
public class TestClient {
public static void main(String[] args) {
EventLoopGroup bossGroup=new NioEventLoopGroup();
try {
Bootstrap bootstrap=new Bootstrap();
bootstrap.group(bossGroup)
.channel(NioSocketChannel.class)
.handler(new TestClientInitializer());
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1",8989).sync();
channelFuture.channel().closeFuture().sync();
}catch (Exception e){
e.printStackTrace();
}finally {
bossGroup.shutdownGracefully();
}
}
}
注意这里分别有两个sync方法,第一个是方法是客户端连接服务端,这里加上sync方法使这个方法和当前线程同步(sync可加可不加)。
第二个方法是判断当前的通道是否关闭,这里加上的sync方法同样是使方法同步,这里必须加上这个方法,因为不加上主线程就直接执行完毕了,加上会阻塞住这个方法,知道客户端的通道关闭。
这里是向客户端的pipeline上放入第一个Handler
这个Handler属于初始化pipeline的,在执行完后就会被抛弃,主要作用就是初始化pipeline,将真正业务Handler加入pipeline
public class TestClientInitializer extends ChannelInitializer<SocketChannel>{
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
/**
* 1) lengthFieldOffset //长度字段的偏差
* 2) lengthFieldLength //长度字段占的字节数
* 3) lengthAdjustment //添加到长度字段的补偿值
* 4) initialBytesToStrip //从解码帧中第一次去除的字节数
*/
//ChannelInboundHandlerAdapter
pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,0,4,0,4));
//ChannelOutboundHandlerAdapter
pipeline.addLast(new LengthFieldPrepender(4));//计算当前待发送消息的二进制字节长度,将该长度添加到ByteBuf的缓冲区头中
//ChannelInboundHandlerAdapter
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8)); //将byte数据解码成String
//ChannelOutboundHandlerAdapter
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8)); //将字符串编码成byte数据
//SimpleChannelInboundHandler
pipeline.addLast(new TestClientHandler());
}
}
客户端真正的业务Handler,属于接收数据类型的Handler
public class TestClientHandler extends SimpleChannelInboundHandler<String> {
//读取客户端数据
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
System.out.println(channelHandlerContext.channel().remoteAddress()+",client output"+s);
// channelHandlerContext.writeAndFlush("form client"+ LocalDateTime.now());
}
//通道就绪
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (int i=0;i<10;i++){
ctx.writeAndFlush("来自客户端的问候");
}
}
//有异常发生
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.channel().close();
}
}
服务端
public class TestServer {
public static void main(String[] args) {
EventLoopGroup bossGroup=new NioEventLoopGroup(1);
EventLoopGroup workGroup=new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap=new ServerBootstrap();
serverBootstrap.group(bossGroup,workGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new TestServerInitializer());
ChannelFuture channelFuture = serverBootstrap.bind(8989).sync();
channelFuture.channel().closeFuture().sync();
}catch (Exception e){
e.printStackTrace();
}finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
}
注意这里的两个NioEventLoopGroup,其实都是线程组,第一个是服务端负责接受客户端连接的线程组,不论有多少线程都只会有一个线程工作,所以为了不浪费性能一般个数设置为1。
第二个是服务端和客户端通信时的线程组,使用默认构造器就是线程数默认为cup核心数*2
这里第一个channel设置的是服务端接收客户端连接的pipeline初始化,这里固定使用NioServerSocketChannel,由netty自己来管理。
这里第二个childHandler是设置和客户端通信的socketchannel的pipeline
后面的sync方法和客户端同样的道理
同客户端一样的逻辑,初始化每一个和客户端连接的socketchannel的pipeline
public class TestServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
/**
* 1) lengthFieldOffset //长度字段的偏差
* 2) lengthFieldLength //长度字段占的字节数
* 3) lengthAdjustment //添加到长度字段的补偿值
* 4) initialBytesToStrip //从解码帧中第一次去除的字节数
*/
//ChannelInboundHandlerAdapter
pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,0,4,0,4));
//ChannelOutboundHandlerAdapter
pipeline.addLast(new LengthFieldPrepender(4)); //计算当前待发送消息的二进制字节长度,将该长度添加到ByteBuf的缓冲区头中
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8)); //ChannelInboundHandlerAdapter
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));//ChannelOutboundHandlerAdapter
pipeline.addLast(new TestServerHandler());//SimpleChannelInboundHandler
}
}
业务Handler类
public class TestServerHandler extends SimpleChannelInboundHandler<String>{
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
System.out.println(channelHandlerContext.channel().remoteAddress()+","+s);
channelHandlerContext.writeAndFlush("form server"+ UUID.randomUUID());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
粘包拆包问题
在客户端的pipeline初始化后客户端循环向服务端发送十句字符串,虽然发送时调用四个writeAndFlush()方法发送的,但是因为tcp协议的优化,在客户端接收时其实时十句字符串连在一起接受的。
这样显然不能达到我们的要求,为了将这种粘包情况拆包,我们可以使用netty为我们提供的两个Handler。
在发送数据时第二个Handler会将发送的数据的长度计算出来并且把计算出来的数值放在ByteBuf的缓冲区头,这里的参数时存放计算出来的数值的字节数。
在接收数据时第一个Handler会将接收的数据根据数据头的数值将指定的数值读出来,这样就不会出现多读或者漏读的问题,这里的参数如下
- maxFrameLength //字段长度的最大值
- lengthFieldOffset //长度字段的偏差
- lengthFieldLength //长度字段占的字节数
- lengthAdjustment //添加到长度字段的补偿值
- initialBytesToStrip //从解码帧中第一次去除的字节数
这样就可以解决粘包拆包问题
总的来说就是客户端做分包处理,加上总长度发到服务端,服务端就根据那个总长来做粘包处理,应该是这样,返回写数据的时候就反过来。
响应浏览器请求
public class TestServer {
public static void main(String[] args) {
EventLoopGroup bossGroup=new NioEventLoopGroup(1); //接收客户端连接的线程组
EventLoopGroup workGroup=new NioEventLoopGroup(); //真正处理读写事件的线程组 16
try {
ServerBootstrap serverBootstrap=new ServerBootstrap();
serverBootstrap.group(bossGroup,workGroup)
.channel(NioServerSocketChannel.class) //服务端用什么通道
.childHandler(new TestServerLnitializer()); //已经连接上来的客户端
ChannelFuture channelFuture = serverBootstrap.bind(8989).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
}
/**
* HttpRequestEncoder,将HttpRequest或HttpContent编码成ByteBuf
HttpRequestDecoder,将ByteBuf解码成HttpRequest和HttpContent
HttpResponseEncoder,将HttpResponse或HttpContent编码成ByteBuf
HttpResponseDecoder,将ByteBuf解码成HttpResponse和HttpContent
*/
//ChannelInitializer 特殊的Handler
public class TestServerLnitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast("httpServerCodec",new HttpServerCodec());
pipeline.addLast("testServerHandler",new TestServerHandler());
}
}
public class TestServerHandler extends SimpleChannelInboundHandler<HttpObject> {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, HttpObject httpObject) throws Exception {
if(httpObject instanceof HttpRequest){
HttpRequest httpRequest= (HttpRequest) httpObject;
String uri = httpRequest.uri();
System.out.println(uri);
ByteBuf byteBuf = Unpooled.copiedBuffer("helloworld", CharsetUtil.UTF_8);
FullHttpResponse fullHttpResponse=new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK,byteBuf);
fullHttpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE,"text/plain");
fullHttpResponse.headers().set(HttpHeaderNames.CONTENT_LENGTH,byteBuf.readableBytes());
channelHandlerContext.writeAndFlush(fullHttpResponse);
}
}
}