03-Netty编解码,粘包拆包及零拷贝详解

Netty编解码

Netty涉及到编解码的组件有Channel、ChannelHandler、ChannelPipe等,先大概了解下这几个组件的作用。

ChannelHandler

ChannelHandler充当了处理入站和出站数据的应用程序逻辑容器。例如,实现ChannelInboundHandler接口(或ChannelInboundHandlerAdapter),你就可以接收入站事件和数据,这些数据随后会被你的应用程序的业务逻辑处理。当你要给连接的客户端发送响应时,也可以从ChannelInboundHandler冲刷数据。你的业务逻辑通常写在一个或者多个ChannelInboundHandler中。ChannelOutboundHandler原理一样,只不过它是用来处理出站数据的。

ChannelPipeline

ChannelPipeline提供了ChannelHandler链的容器。以客户端应用程序为例,如果事件的运动方向是从客户端到服务端的,那么我们称这些事件为出站的,即客户端发送给服务端的数据会通过pipeline中的一系列ChannelOutboundHandler(ChannelOutboundHandler调用是从tail到head方向逐个调用每个handler的逻辑),并被这些Handler处理,反之则称为入站的,入站只调用pipeline里的ChannelInboundHandler逻辑(ChannelInboundHandler调用是从head到tail方向逐个调用每个handler的逻辑)。

03-Netty编解码,粘包拆包及零拷贝详解_第1张图片

编码解码器

当你通过Netty发送或者接受一个消息的时候,就将会发生一次数据转换。入站消息会被解码:从字节转换为另一种格式(比如java对象);如果是出站消息,它会被编码成字节。

Netty提供了一系列实用的编码解码器,他们都实现了ChannelInboundHadnler或者ChannelOutboundHandler接口。在这些类中,channelRead方法已经被重写了。以入站为例,对于每个从入站Channel读取的消息,这个方法会被调用。随后,它将调用由已知解码器所提供的decode()方法进行解码,并将已经解码的字节转发给ChannelPipeline中的下一个ChannelInboundHandler。

Netty提供了很多编解码器,比如编解码字符串的StringEncoder和StringDecoder,编解码对象的ObjectEncoder和ObjectDecoder等。

如果要实现高效的编解码可以用protobuf,但是protobuf需要维护大量的proto文件比较麻烦,现在一般可以使用protostuff。

protostuff是一个基于protobuf实现的序列化方法,它较于protobuf最明显的好处是,在几乎不损耗性能的情况下做到了不用我们写.proto文件来实现序列化。使用它也非常简单,代码如下:

引入依赖:


            com.dyuproject.protostuff
            protostuff-api
            1.0.10
        
        
            com.dyuproject.protostuff
            protostuff-core
            1.0.10
        
        
            com.dyuproject.protostuff
            protostuff-runtime
            1.0.10
        

protostuff使用示例:

package com.tuling.netty;

import com.dyuproject.protostuff.LinkedBuffer;
import com.dyuproject.protostuff.ProtostuffIOUtil;
import com.dyuproject.protostuff.Schema;
import com.dyuproject.protostuff.runtime.RuntimeSchema;
import com.tuling.netty.codec.User;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * protostuff 序列化工具类,基于protobuf封装
 */
public class ProtostuffUtil {
    private static Map, Schema> cachedSchema = new ConcurrentHashMap, Schema>();

    private static  Schema getSchema(Class clazz) {
        @SuppressWarnings("unchecked") Schema schema = (Schema) cachedSchema.get(clazz);
        if (schema == null) {
            schema = RuntimeSchema.getSchema(clazz);
            if (schema != null) {
                cachedSchema.put(clazz, schema);
            }
        }
        return schema;
    }

    /**
     * 序列化 * * @param obj * @return
     */
    public static  byte[] serializer(T obj) {
        @SuppressWarnings("unchecked") Class clazz = (Class) obj.getClass();
        LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
        try {
            Schema schema = getSchema(clazz);
            return ProtostuffIOUtil.toByteArray(obj, schema, buffer);
        } catch (Exception e) {
            throw new IllegalStateException(e.getMessage(), e);
        } finally {
            buffer.clear();
        }
    }

    /**
     * 反序列化 * * @param data * @param clazz * @return
     */
    public static  T deserializer(byte[] data, Class clazz) {
        try {
            T obj = clazz.newInstance();
            Schema schema = getSchema(clazz);
            ProtostuffIOUtil.mergeFrom(data, obj, schema);
            return obj;
        } catch (Exception e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
    }

    public static void main(String[] args) {
        byte[] userBytes = ProtostuffUtil.serializer(new User(1, "zhuge"));
        User user = ProtostuffUtil.deserializer(userBytes, User.class);
        System.out.println(user);
    }
}

参见项目示例com.tuling.netty.codec包下代码

Netty粘包拆包

TCP是一个流协议,就是没有界限的一长串二进制数据。TCP作为传输层协议并不不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行数据包的划分,所以在业务上认为是一个完整的包,可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。面向流的通信是无消息保护边界的。

如下图所示,client发了两个数据包D1和D2,但是server端可能会收到如下几种情况的数据。

03-Netty编解码,粘包拆包及零拷贝详解_第2张图片

解决方案

1)消息定长度,传输的数据大小固定长度,例如每段的长度固定为100字节,如果不够空位补空格

2)在数据包尾部添加特殊分隔符,比如下划线,中划线等,这种方法简单易行,但选择分隔符的时候一定要注意每条数据的内部一定不能出现分隔符。

3)发送长度:发送每条数据的时候,将数据的长度一并发送,比如可以选择每条数据的前4位是数据的长度,应用层处理时可以根据长度来判断每条数据的开始和结束。

Netty提供了多个解码器,可以进行分包的操作,如下:

  • LineBasedFrameDecoder (回车换行分包)
  • DelimiterBasedFrameDecoder(特殊分隔符分包)
  • FixedLengthFrameDecoder(固定长度报文来分包)

自定义长度分包编解码器,参见项目示例com.tuling.netty.split包下代码

Netty心跳检测机制

所谓心跳, 即在 TCP 长连接中, 客户端和服务器之间定期发送的一种特殊的数据包, 通知对方自己还在线, 以确保 TCP 连接的有效性.

在 Netty 中, 实现心跳机制的关键是 IdleStateHandler, 看下它的构造器:

public IdleStateHandler(int readerIdleTimeSeconds, int writerIdleTimeSeconds, int allIdleTimeSeconds) { this((long)readerIdleTimeSeconds, (long)writerIdleTimeSeconds, (long)allIdleTimeSeconds, TimeUnit.SECONDS); }

这里解释下三个参数的含义:

  • readerIdleTimeSeconds: 读超时. 即当在指定的时间间隔内没有从 Channel 读取到数据时, 会触发一个 READER_IDLE 的 IdleStateEvent 事件.
  • writerIdleTimeSeconds: 写超时. 即当在指定的时间间隔内没有数据写入到 Channel 时, 会触发一个 WRITER_IDLE 的 IdleStateEvent 事件.
  • allIdleTimeSeconds: 读/写超时. 即当在指定的时间间隔内没有读或写操作时, 会触发一个 ALL_IDLE 的 IdleStateEvent 事件.

注:这三个参数默认的时间单位是秒。若需要指定其他时间单位,可以使用另一个构造方法:

IdleStateHandler(boolean observeOutput, long readerIdleTime, long writerIdleTime, long allIdleTime, TimeUnit unit)

要实现Netty服务端心跳检测机制需要在服务器端的ChannelInitializer中加入如下的代码:

pipeline.addLast(new IdleStateHandler(3, 0, 0, TimeUnit.SECONDS));

初步地看下IdleStateHandler源码,先看下IdleStateHandler中的channelRead方法:

03-Netty编解码,粘包拆包及零拷贝详解_第3张图片

红框代码其实表示该方法只是进行了透传,不做任何业务逻辑处理,让channelPipe中的下一个handler处理channelRead方法

我们再看看channelActive方法:

03-Netty编解码,粘包拆包及零拷贝详解_第4张图片

这里有个initialize的方法,这是IdleStateHandler的精髓,接着探究:

03-Netty编解码,粘包拆包及零拷贝详解_第5张图片

这边会触发一个Task,ReaderIdleTimeoutTask,这个task里的run方法源码是这样的:

03-Netty编解码,粘包拆包及零拷贝详解_第6张图片

第一个红框代码是用当前时间减去最后一次channelRead方法调用的时间,假如这个结果是6s,说明最后一次调用channelRead已经是6s之前的事情了,你设置的是5s,那么nextDelay则为-1,说明超时了,那么第二个红框代码则会触发下一个handler的userEventTriggered方法:

如果没有超时则不触发userEventTriggered方法。

Netty心跳检测代码示例:

package com.tuling.netty.heartbeat;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
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 io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.timeout.IdleStateHandler;

import java.util.concurrent.TimeUnit;

public class HeartBeatServer {

    public static void main(String[] args) throws Exception {
        EventLoopGroup boss = new NioEventLoopGroup();
        EventLoopGroup worker = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(boss, worker)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast("decoder", new StringDecoder());
                            pipeline.addLast("encoder", new StringEncoder());
                            //IdleStateHandler的readerIdleTime参数指定超过3秒还没收到客户端的连接,
                            //会触发IdleStateEvent事件并且交给下一个handler处理,下一个handler必须
                            //实现userEventTriggered方法处理对应事件
                            pipeline.addLast(new IdleStateHandler(3, 0, 0, TimeUnit.SECONDS));
                            pipeline.addLast(new HeartBeatServerHandler());
                        }
                    });
            System.out.println("netty server start。。");
            ChannelFuture future = bootstrap.bind(9000).sync();
            future.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            worker.shutdownGracefully();
            boss.shutdownGracefully();
        }
    }
}
package com.tuling.netty.heartbeat;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.timeout.IdleStateEvent;

public class HeartBeatServerHandler extends SimpleChannelInboundHandler {

    int readIdleTimes = 0;

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String s) throws Exception {
        System.out.println(" ====== > [server] message received : " + s);
        if ("Heartbeat Packet".equals(s)) {
            ctx.channel().writeAndFlush("ok");
        } else {
            System.out.println(" 其他信息处理 ... ");
        }
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        IdleStateEvent event = (IdleStateEvent) evt;

        String eventType = null;
        switch (event.state()) {
            case READER_IDLE:
                eventType = "读空闲";
                readIdleTimes++; // 读空闲的计数加1
                break;
            case WRITER_IDLE:
                eventType = "写空闲";
                // 不处理
                break;
            case ALL_IDLE:
                eventType = "读写空闲";
                // 不处理
                break;
        }



        System.out.println(ctx.channel().remoteAddress() + "超时事件:" + eventType);
        if (readIdleTimes > 3) {
            System.out.println(" [server]读空闲超过3次,关闭连接,释放更多资源");
            ctx.channel().writeAndFlush("idle close");
            ctx.channel().close();
        }
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.err.println("=== " + ctx.channel().remoteAddress() + " is active ===");
    }
}
package com.tuling.netty.heartbeat;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.util.Random;

public class HeartBeatClient {
    public static void main(String[] args) throws Exception {
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast("decoder", new StringDecoder());
                            pipeline.addLast("encoder", new StringEncoder());
                            pipeline.addLast(new HeartBeatClientHandler());
                        }
                    });

            System.out.println("netty client start。。");
            Channel channel = bootstrap.connect("127.0.0.1", 9000).sync().channel();
            String text = "Heartbeat Packet";
            Random random = new Random();
            while (channel.isActive()) {
                int num = random.nextInt(10);
                Thread.sleep(2 * 1000);
                channel.writeAndFlush(text);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            eventLoopGroup.shutdownGracefully();
        }
    }

    static class HeartBeatClientHandler extends SimpleChannelInboundHandler {

        @Override
        protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
            System.out.println(" client received :" + msg);
            if (msg != null && msg.equals("idle close")) {
                System.out.println(" 服务端关闭连接,客户端也关闭");
                ctx.channel().closeFuture();
            }
        }
    }
}

Netty断线自动重连实现

1、客户端启动连接服务端时,如果网络或服务端有问题,客户端连接失败,可以重连,重连的逻辑加在客户端。

参见代码com.tuling.netty.reconnect.NettyClient

2、系统运行过程中网络故障或服务端故障,导致客户端与服务端断开连接了也需要重连,可以在客户端处理数据的Handler的channelInactive方法中进行重连。

参见代码com.tuling.netty.reconnect.NettyClientHandler

Netty零拷贝

Netty的接收和发送ByteBuffer采用DIRECT BUFFERS,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。
如果使用传统的JVM堆内存(HEAP BUFFERS)进行Socket读写,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才能写入Socket中。JVM堆内存的数据是不能直接写入Socket中的。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
可以看下netty的读写源码,比如read源码NioByteUnsafe.read()

03-Netty编解码,粘包拆包及零拷贝详解_第7张图片

直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,某些情况下这部分内存也会被频繁地使用,而且也可能导致OutOfMemoryError异常出现。Java里用DirectByteBuffer可以分配一块直接内存(堆外内存),元空间对应的内存也叫作直接内存,它们对应的都是机器的物理内存
03-Netty编解码,粘包拆包及零拷贝详解_第8张图片

public class MemoryTest {
5
6 public static void heapAccess() {
7 long startTime = System.currentTimeMillis();
8 //分配堆内存
9 ByteBuffer buffer = ByteBuffer.allocate(1000);
10 for (int i = 0; i < 100000; i++) {
11 for (int j = 0; j < 200; j++) {
12 buffer.putInt(j);
13 }
14 buffer.flip();
15 for (int j = 0; j < 200; j++) {
16 buffer.getInt();
17 }
18 buffer.clear();
19 }
20 long endTime = System.currentTimeMillis();
21 System.out.println("堆内存访问:" + (endTime ‐ startTime));
22 }
23
24 public static void directAccess() {
25 long startTime = System.currentTimeMillis();
26 //分配直接内存
27 ByteBuffer buffer = ByteBuffer.allocateDirect(1000);
28 for (int i = 0; i < 100000; i++) {
29 for (int j = 0; j < 200; j++) {
30 buffer.putInt(j);
31 }
32 buffer.flip();
33 for (int j = 0; j < 200; j++) {
34 buffer.getInt();
35 }
36 buffer.clear();
37 }
38 long endTime = System.currentTimeMillis();
39 System.out.println("直接内存访问:" + (endTime ‐ startTime));
40 }
41
42 public static void heapAllocate() {
43 long startTime = System.currentTimeMillis();
44 for (int i = 0; i < 100000; i++) {
45 ByteBuffer.allocate(100);
46 }
47 long endTime = System.currentTimeMillis();
48 System.out.println("堆内存申请:" + (endTime ‐ startTime));
49 }
50
51 public static void directAllocate() {
52 long startTime = System.currentTimeMillis();
53 for (int i = 0; i < 100000; i++) {
54 ByteBuffer.allocateDirect(100);
55 }
56 long endTime = System.currentTimeMillis();
57 System.out.println("直接内存申请:" + (endTime ‐ startTime));
58 }
59
60 public static void main(String args[]) {
61 for (int i = 0; i < 10; i ++) {
62 heapAccess();
63 directAccess();
64 }
65
66 System.out.println();
67
68 for (int i = 0; i < 10; i ++) {
69 heapAllocate();
70 directAllocate();
71 }
72 }
73 }
74
75 运行结果:
76 堆内存访问:53
77 直接内存访问:43
78 堆内存访问:32
79 直接内存访问:21
80 堆内存访问:55
81 直接内存访问:32
82 堆内存访问:63
83 直接内存访问:48
84 堆内存访问:35
85 直接内存访问:19
86 堆内存访问:35
87 直接内存访问:19
88 堆内存访问:34
89 直接内存访问:20
90 堆内存访问:52
91 直接内存访问:28
92 堆内存访问:41
93 直接内存访问:34
94 堆内存访问:64
95 直接内存访问:23
96
97 堆内存申请:14
98 直接内存申请:37
99 堆内存申请:9
100 直接内存申请:33
101 堆内存申请:60
102 直接内存申请:44
103 堆内存申请:1
104 直接内存申请:36
105 堆内存申请:1
106 直接内存申请:69
107 堆内存申请:1
108 直接内存申请:32
109 堆内存申请:2
110 直接内存申请:25
111 堆内存申请:1
112 直接内存申请:29
113 堆内存申请:6
114 直接内存申请:27
115 堆内存申请:6
116 直接内存申请:158

从程序运行结果看出直接内存申请较慢,但访问效率高。在java虚拟机实现上,本地IO会直接操作直接内存(直接内存=>系统调用=>硬盘/网卡),而非直接内存则需要二次拷贝(堆内存=>直接内存=>系统调用=>硬盘/网卡)。

直接内存分配源码分析:

package‐private
7 super(‐1, 0, cap, cap);
8 boolean pa = VM.isDirectMemoryPageAligned();
9 int ps = Bits.pageSize();
10 long size = Math.max(1L, (long)cap + (pa ? ps : 0));
11 //判断是否有足够的直接内存空间分配,可通过‐XX:MaxDirectMemorySize=参数指定直接内存最大可分配空间,如果不指定默认为最
大堆内存大小,
12 //在分配直接内存时如果发现空间不够会显示调用System.gc()触发一次full gc回收掉一部分无用的直接内存的引用对象,同时直接内存也会
被释放掉
13 //如果释放完分配空间还是不够会抛出异常java.lang.OutOfMemoryError
14 Bits.reserveMemory(size, cap);
15
16 long base = 0;
17 try {
18 // 调用unsafe本地方法分配直接内存
19 base = unsafe.allocateMemory(size);
20 } catch (OutOfMemoryError x) {
21 // 分配失败,释放内存
22 Bits.unreserveMemory(size, cap);
23 throw x;
24 }
25 unsafe.setMemory(base, size, (byte) 0);
26 if (pa && (base % ps != 0)) {
27 // Round up to page boundary
28 address = base + ps ‐ (base & (ps ‐ 1));
29 } else {
30 address = base;
31 }
32
33 // 使用Cleaner机制注册内存回收处理函数,当直接内存引用对象被GC清理掉时,
34 // 会提前调用这里注册的释放直接内存的Deallocator线程对象的run方法
35 cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
36 att = null;
37 }
38
39
40 // 申请一块本地内存。内存空间是未初始化的,其内容是无法预期的。
41 // 使用freeMemory释放内存,使用reallocateMemory修改内存大小
42 public native long allocateMemory(long bytes);
43
44 // openjdk8/hotspot/src/share/vm/prims/unsafe.cpp
45 UNSAFE_ENTRY(jlong, Unsafe_AllocateMemory(JNIEnv *env, jobject unsafe, jlong size))
46 UnsafeWrapper("Unsafe_AllocateMemory");
47 size_t sz = (size_t)size;
48 if (sz != (julong)size || size < 0) {
49 THROW_0(vmSymbols::java_lang_IllegalArgumentException());
50 }
51 if (sz == 0) {
52 return 0;
53 }
54 sz = round_to(sz, HeapWordSize);
55 // 调用os::malloc申请内存,内部使用malloc这个C标准库的函数申请内存
56 void* x = os::malloc(sz, mtInternal);
57 if (x == NULL) {
58 THROW_0(vmSymbols::java_lang_OutOfMemoryError());
59 }
60 //Copy::fill_to_words((HeapWord*)x, sz / HeapWordSize);
61 return addr_to_java(x);
62 UNSAFE_END

使用直接内存的优缺点:
优点:
不占用堆内存空间,减少了发生GC的可能
java虚拟机实现上,本地IO会直接操作直接内存(直接内存=>系统调用=>硬盘/网卡),而非直接内存则需要二次拷贝(堆内
存=>直接内存=>系统调用=>硬盘/网卡)
缺点:
初始分配较慢
没有JVM直接帮助管理内存,容易发生内存溢出。为了避免一直没有FULL GC,最终导致直接内存把物理内存被耗完。我们可以指定直接内存的最大值,通过-XX:MaxDirectMemorySize来指定,当达到阈值的时候,调用system.gc来进行一次FULL GC,
间接把那些没有被使用的直接内存回收掉。
有道云笔记:文档:03-VIP-Netty编解码,粘包拆包及零拷贝详解
链接:http://note.youdao.com/noteshare?
id=b8970e44473486a48178193d68929008&sub=2FBCDAF79D794F8DBBD027A5F2C29249

你可能感兴趣的:(03-Netty编解码,粘包拆包及零拷贝详解)