使用Netty发送大型Java对象

使用Netty发送大型Java对象,netty,Netty,我是Netty的新手,使用版本4。在我的项目中,服务器返回客户机一个Java对象,该对象可以很大。为此,我首先使用了ObjectEncoder/解码器和NioSocketChannel。虽然它可以工作,但性能明显低于使用旧的阻塞IO时的性能。线程转储表明ObjectEncoder一直在重新分配直接缓冲区。我猜它是在直接缓冲区中序列化整个对象,然后才通过网络发送它。这很慢,如果有多个这样的请求同时运行,可能会导致OutOfMemoryError。对于快速且使用有限大小缓冲区的高效实现,您有什么建议

我是Netty的新手,使用版本4。在我的项目中,服务器返回客户机一个Java对象,该对象可以很大。为此,我首先使用了ObjectEncoder/解码器和NioSocketChannel。虽然它可以工作,但性能明显低于使用旧的阻塞IO时的性能。线程转储表明ObjectEncoder一直在重新分配直接缓冲区。我猜它是在直接缓冲区中序列化整个对象,然后才通过网络发送它。这很慢,如果有多个这样的请求同时运行,可能会导致OutOfMemoryError。对于快速且使用有限大小缓冲区的高效实现,您有什么建议?此外,服务器返回的某些(但不是全部)对象包含一个长字节数组字段。这一事实能否用于进一步提高性能

正如@MattBakaitis所问的,我正在粘贴代码示例,这是对ObjectEchoServer示例的一个轻微修改。它将一个常量大对象发送回客户端,以响应接收到的消息

public final class MyObjectEchoServer {

    static final int PORT = Integer.parseInt(System.getProperty("port", "11000"));

    public static void main(String[] args) throws Exception {

        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .handler(new LoggingHandler(LogLevel.INFO))
             .childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                public void initChannel(SocketChannel ch) throws Exception {
                    ChannelPipeline p = ch.pipeline();
                    p.addLast(
                            new ObjectEncoder(),
                            new ObjectDecoder(Integer.MAX_VALUE, ClassResolvers.cacheDisabled(null)),
                            new ObjectEchoServerHandler());
                }
             });

            // Bind and start to accept incoming connections.
            b.bind(PORT).sync().channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

public class ObjectEchoServerHandler extends ChannelInboundHandlerAdapter {
    public static class Response implements Serializable {
        public byte[] bytes;
    }

    private static Response response;

    static {
        int len = 256 * 1024 * 1024;
        response = new Response();
        response.bytes = new byte[len];
    }


    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        System.out.println("Received: msg=" + msg);
        // Echo back the received object to the client.
        System.out.println("Sending response. length: " + response.bytes.length);
        ctx.write(response);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        System.out.println("Flushing");
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

如果您正在编写一个大对象,正如您在问题中提到的,可能是多个内存拷贝恰好扩展了输出缓冲区。要解决该问题,您可以覆盖
ObjectEncoder
中的
allocateBuffer()
方法(
messagetobytecoder
正确),并分配具有更高初始容量的缓冲区。例如:

@Override
protected ByteBuf allocateBuffer(
        ChannelHandlerContext ctx, Object msg, boolean preferDirect) {

    return ctx.alloc().heapBuffer(1048576);
}
为了进一步减少内存拷贝数,我建议使用直接缓冲区(即
ctx.alloc().directBuffer(1048576)
)和
pooledbytebuf分配器

但是,这并不能解决由于许多对等方交换一个大对象而导致在负载下获取
OutOfMemoryError
的问题。Java对象序列化从未实现为与非阻塞连接一起工作;它总是假定流有数据。如果不重新实现对象序列化,就不可能不将整个对象保留在缓冲区中。实际上,这适用于仅使用
InputStream
OutputStream
的其他对象序列化实现


或者,您可以实现一个用于交换大数据流的替代协议,并通过使用对数据流的一些引用来减小对象的大小。

几年前,我遇到了一个类似的问题,然后我开始检查通过有线发送的对象。我发现大多数实例变量都是原语,只有5%是另一个对象。 我尝试了JAVA序列化,提取了这些对象的字节[],然后将其包装到ByteBuf并发送到wire。示例代码可以是:

try {

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(obj);
        oos.close();
        return bos.toByteArray();
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
然后进行反序列化:

ByteArrayInputStream ins = new ByteArrayInputStream(b);
    ObjectInputStream ois = new ObjectInputStream(ins);
    return ois.readObject();
对象的平均大小约为800字节,使用了1GBPS的链接。这对于每秒数千个事务都可以正常工作。现在,如果您想将速度提高到每秒一些LAC,您需要付出一些努力,并通过自己的方式序列化这些对象

下面是这样做的示例:

我的对象->

public class MyObject{

private int i;
private String key;

private AnotherObject obj;

private boolean toUpdate;

public MyObject(int i,String key, AnotherObject obj, boolean toUpdate) {
    this.i=i;
    this.key = key;
    this.obj= obj;
    this.toUpdate = toUpdate;
}

/**
 * Decode in order: 
  int i
  String key 
  AnotherObject obj 
  boolean toUpdate
 * 
 */

public Object decodeAll(ByteBuf b) {
    this.i = readInt(b);
    this.key = readString(b);
    if (b.readableBytes() > 1) {
        AnotherObject obj= new AnotherObject ();
        this.value = AnotherObject .decodeAll(b);
    }
    byte[] bool = new byte[1];
    b.readBytes(bool);
    this.toUpdate = decodeBoolean(bool);
    return this;
}

/**
 * Encode in order: 
  int i
  String key 
  AnotherObject obj 
  boolean toUpdate
 * 
 */


public ByteBuf encodeAll() {
    ByteBuf buffer = allocator.buffer();
    //First the int
    writeInt(buffer, this.i);
   // String key
    writeString(buffer, this.key);
    // AnotherObject 
    if (this.value != null) {
        ByteBuf rel = this.value.encodeAll();
        buffer.writeBytes(rel);
        rel.release();
    }
    // boolean toUpdate
    buffer.writeBytes(encodeBoolean(this.toUpdate));
    return buffer;
}
}

在这里,我使用sun.misc.Unsafe API来序列化/反序列化:

protected byte[] encodeBoolean(final boolean value) {
    byte[] b = new byte[1];
    unsafe.putBoolean(b, byteArrayOffset, value);
    return b;
}

public static boolean decodeBoolean(final byte[] b) {
    boolean value = unsafe.getBoolean(b, byteArrayOffset);

    return value;
}
protected byte[] encodeInt(final int value) {
    byte[] b = new byte[4];
    unsafe.putInt(b, byteArrayOffset, value);

    return b;

}
protected void writeString(ByteBuf buffer, String data) {
    if (data == null) {
        data = new String();
    }
    // For String , first write the length as integer and then actual data
    buffer.writeBytes(encodeInt(data.getBytes().length));
    buffer.writeBytes(data.getBytes());
}
public static String readString(ByteBuf data) {
    byte[] temp = new byte[4];
    data.readBytes(temp);
    int len = decodeInt(temp);
    if (len == 0) {
        return null;
    } else {
        temp = new byte[len];
        data.readBytes(temp);
        return new String(temp);
    }
}
同样,您也可以对任何java数据结构进行编码/解码。i、 e

protected void writeArrayList(ByteBuf buffer, ArrayList<String> data) {
    if (data == null) {
        data = new ArrayList<String>();
    }
    // Write the number of elements and then all elements one-by-one
    buffer.writeBytes(encodeInt(data.size()));
    for (String str : data) {
        writeString(buffer, str);
    }
}

protected ArrayList<String> readArrayList(ByteBuf data) {
    byte[] temp = new byte[4];
    data.readBytes(temp);
    int noOfElements = decodeInt(temp, 0);
    ArrayList<String> arr = new ArrayList<>(noOfElements);
    for (int i = 0; i < noOfElements; i++) {
        arr.add(readString(data));
    }

    return arr;
}
protectedvoid writeArrayList(字节缓冲区、数组列表数据){
如果(数据==null){
数据=新的ArrayList();
}
//写下元素的数量,然后逐个写下所有元素
writeBytes(encodeInt(data.size());
for(字符串str:data){
writeString(缓冲区,str);
}
}
受保护的ArrayList readArrayList(ByteBuf数据){
字节[]临时=新字节[4];
数据读取字节(temp);
int noOfElements=decodeInt(温度,0);
ArrayList arr=新的ArrayList(noOfElements);
for(int i=0;i

现在,您只需将ByteBuf写入通道,即可获得每秒事务的LAC。

如果您可以包含您认为导致错误的代码或配置,您将获得更好的结果和答案。这个问题相当广泛,可能很难回答。@MattBakaitis,我用代码示例和线程堆栈更新了这篇文章。谢谢@trustin。allocateBuffer是4.0.22中的一种新方法。我使用的是4.0.20,其中的代码隐藏在write方法中。我现在明白了为什么ObjectOutputStream比AbstractByteBuf对大型对象的序列化工作得更快。ObjectOutputStream每次需要增长时都会将底层数组的大小增加一倍,而AbstractByteBuf则采取更谨慎的内存策略,如果旧数组的大小超过4MB阈值,则会将其大小增加4MB。这会导致在出现大型对象时产生大量内存拷贝。
protected void writeArrayList(ByteBuf buffer, ArrayList<String> data) {
    if (data == null) {
        data = new ArrayList<String>();
    }
    // Write the number of elements and then all elements one-by-one
    buffer.writeBytes(encodeInt(data.size()));
    for (String str : data) {
        writeString(buffer, str);
    }
}

protected ArrayList<String> readArrayList(ByteBuf data) {
    byte[] temp = new byte[4];
    data.readBytes(temp);
    int noOfElements = decodeInt(temp, 0);
    ArrayList<String> arr = new ArrayList<>(noOfElements);
    for (int i = 0; i < noOfElements; i++) {
        arr.add(readString(data));
    }

    return arr;
}