Netty应用(五) ---- netty优化

1. 扩展序列化算法

常用的序列化json这里就不再叙述了,这里讲一下谷歌的protobuf,这个序列化方式还是有很多优点的。尤其是我们服务在与硬件设备做信息交互时,采用这种序列化算法是不错的选择。

  • 序列化后体积相比Json和XML很小,适合网络传输
  • 支持跨平台多语言
  • 序列化反序列化速度很快,快于Json的处理速速

1.1 protoc安装

1、下载proto编译器
github官网下载 我这里下载的版本是25.0,目前最新是25.1,我们下载win64.zip包,找一个目录解压即可。
在这里插入图片描述
2、配置系统环境变量path
在这里插入图片描述
3、验证版本
在这里插入图片描述

1.2 protoc文件编写

具体语法这里不细讲,作为一款序列化工具,我们掌握基本的定义就可以了,不用太过于深究。这里我将常用的语法都在message.proto文件中给出了注释。
我们在resources目录下,新建一个messagePb.proto文件。注意我们编译后生成的类名,就是取的文件名,所以我们这边不叫message.proto,否则和我们之前的messge类重名,导包会麻烦。

// 文件首行中指定该proto文件所采用的语法
// ProtocolBuffers语言版本3,简称proto3,是.proto文件最新的语法版本。相较于proto2,语法更加简化,使用更加简单
syntax = "proto3";

// 可选,指定编译后的文件路径,开发语言_package
option java_package = "com.netty.chat.pbdef";

// 定义我们的消息,命名驼峰命名,首字母大写
message Message{

  // 消息字段的格式定义为:类型 字段名 = 唯一编号
  int32 sequence_id = 1; //序列号
  int32 message_type = 2; //消息类型
  oneof data { // oneof表示下面消息只能存在一种,我们根据messageType来判断,应该解析成哪种对象
      LoginRequestMessage login = 3; // 登录消息
      ChatRequestMessage chat = 4; // 聊天消息
      GroupCreateRequestMessage group_create = 5; // 创建群
  }
}

message LoginRequestMessage {
    string user_name = 1; // 用户名
    string pass_word = 2; // 密码
}


message ChatRequestMessage {
    string content = 1; // 内容
    string to = 2; // 接收消息方
    string from = 3; // 发送消息方
}

message GroupCreateRequestMessage {
    string group_name = 1; // 群名
    repeated string members = 2; // 群成员,repeat表示可重复,即members字段是个集合
}

// 枚举类型,这里只做示例,并没用到
enum CommandType {
    HEARTBEAT = 0;
    ACK = 1;
    NACK = 2;
}

1.3 编译及依赖

注意:路径和后面的proto文件名中间要加空格,不然编译通会报错:Missing input file.
由于我直接将proto文件写在resources目录下,我这里直接到resources目录下执行以下命令。

protoc --java_out=../java/ *.proto

在这里插入图片描述

编译完后,会在当前目录下生成一个java文件,实际是MessagePb类,这里图片是最开始截的图。因为我们在proto文件中指定了option java_package = “com.netty.chat.pbdef”;
在这里插入图片描述

我们打开代码后,会发现是缺少依赖的。我们打开github官网,找到我们下载的protoc编译器的tagv25.0Tag。找到此版本的readme文档,我们要学会去找官方文档,文档中指出如何编译,如何引入maven依赖。
在这里插入图片描述
protobuf依赖

<dependency>
  <groupId>com.google.protobuf</groupId>
  <artifactId>protobuf-java</artifactId>
  <version>3.25.0</version>
</dependency>

官方提供的工具类

<dependency>
  <groupId>com.google.protobuf</groupId>
  <artifactId>protobuf-java-util</artifactId>
  <version>3.25.0</version>
</dependency>

引入并刷新依赖后,MessagePb.java如下:
在这里插入图片描述

1.5 MessageCodec改写

注意: 我们定义的messagePb.proto文件本身就是消息的定义,所以我们如果补全文件的话,相当与我们之前定义的那些消息请求,都集成到MessagePb这一个类中,并且还有对应的set/get方法。之前那些请求就可以舍弃了,但是那样之前的handler也需要改,所以简单演示下,多写一步处理,就是把protoBuf 转换成message对象(实际完全可以舍弃)。

ProtoSerializer接口

public interface ProtoSerializer<T> {

    T parse(byte[] payload);

    byte[] format(T payload);
}

MessageProtoSerializer :只用于protoBuf和byte[]之间的转换。

@Slf4j
public class MessageProtoSerializer implements ProtoSerializer<MessagePb.Message> {

    @Override
    public MessagePb.Message parse(byte[] payload) {
        try {
            return MessagePb.Message.parseFrom(payload);
        } catch (InvalidProtocolBufferException e) {
            log.error("parse fail.", e);
            return null;
        }
    }

    @Override
    public byte[] format(MessagePb.Message payload) {
        return payload.toByteArray();
    }
}

MessageCodec
如果系统支持多种序列化方式,可以将此改成配置,多种序列化方式可以根据配置切换。

@Slf4j
public class MessageCodec extends ByteToMessageCodec<Message> {

    @Override
    public void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception {
        // 1. 4个字节的魔数
        out.writeBytes(new byte[] {1, 2, 3, 4});
        // 2. 1字节的版本
        out.writeByte(1);
        // 3. 1字节的序列化方式,0-jdk 1-json
        out.writeByte(0);
        // 4. 1字节表示消息类型,比如登录、退出..
        out.writeByte(msg.getMessageType());
        // 5. 4字节表示消息请求序号
        out.writeInt(msg.getSequenceId());
        // 由于4+1+1+1+4+4=15,我们再加一字节用于补齐,无意义
        out.writeByte(0XFF);
        // 6. 4字节表示消息的长度
        // ByteArrayOutputStream os = new ByteArrayOutputStream();
        // ObjectOutputStream oos = new ObjectOutputStream(os);
        // oos.writeObject(msg);
        // byte[] bytes = os.toByteArray();

        // 将Message转成MessagePb对象
        MessagePb.Message.Builder builder = MessagePb.Message.newBuilder();
        builder.setSequenceId(msg.getSequenceId()).setMessageType(msg.getMessageType());
        if (Message.LoginRequestMessage == msg.getMessageType()) {
            LoginRequestMessage login = (LoginRequestMessage) msg;
            builder.setLogin(MessagePb.LoginRequestMessage.newBuilder()
                .setPassWord(login.getPassword())
                .setUserName(login.getUsername()));
        } else if (Message.GroupCreateRequestMessage == msg.getMessageType()) {
            GroupCreateRequestMessage groupCreate = (GroupCreateRequestMessage) msg;
            builder.setGroupCreate(MessagePb.GroupCreateRequestMessage.newBuilder()
                .setGroupName(groupCreate.getGroupName())
                .addAllMembers(groupCreate.getMembers()));
        }
        // 将MessagePb对象转成字节数组
        MessageProtoSerializer messageProtoSerializer = new MessageProtoSerializer();
        byte[] bytes = messageProtoSerializer.format(builder.build());

        out.writeInt(bytes.length);
        // 7.消息正文
        out.writeBytes(bytes);
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        int magicNum = in.readInt();
        byte version = in.readByte();
        byte serializerType = in.readByte();
        byte type = in.readByte();
        int sequenceId = in.readInt();
        in.readByte();

        int length = in.readInt();
        byte[] bytes = new byte[length];
        in.readBytes(bytes, 0, length);
//        ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(bytes));
//        Message message = (Message) objectInputStream.readObject();
        MessageProtoSerializer messageProtoSerializer = new MessageProtoSerializer();
        MessagePb.Message parse = messageProtoSerializer.parse(bytes);
        int messageType = parse.getMessageType();
        if (Message.LoginRequestMessage == messageType) {
            LoginRequestMessage message = new LoginRequestMessage();
            message.setMessageType(parse.getMessageType());
            message.setSequenceId(parse.getSequenceId());
            message.setPassword(parse.getLogin().getPassWord());
            message.setUsername(parse.getLogin().getUserName());
            out.add(message);
            log.info("message is {}",message);
        } else if (Message.GroupCreateRequestMessage == messageType) {
            GroupCreateRequestMessage message = new GroupCreateRequestMessage();
            message.setMessageType(parse.getMessageType());
            message.setSequenceId(parse.getSequenceId());
            message.setGroupName(parse.getGroupCreate().getGroupName());
            ProtocolStringList membersList = parse.getGroupCreate().getMembersList();
            message.setMembers(new HashSet<>(membersList));
            out.add(message);
            log.info("message is {}",message);
        }
//        out.add(message);
    }
}

1.6 测试编解码器

由于上面只写了登录请求和群聊创建请求的适配,这里只测下这两个。

public class TestMessageCodec {
    public static void main(String[] args) throws Exception {
        EmbeddedChannel embeddedChannel = new EmbeddedChannel(new LoggingHandler(LogLevel.INFO), new MessageCodec());
//        LoginRequestMessage message = new LoginRequestMessage("zhangsan", "123");
        GroupCreateRequestMessage message =
            new GroupCreateRequestMessage("群聊1", Sets.newHashSet("zhangsan", "lisi"));
        embeddedChannel.writeOutbound(message);

        ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
        new MessageCodec().encode(null, message, buffer);
        embeddedChannel.writeInbound(buffer);
    }
}

控制台
在这里插入图片描述

2. 参数

首先,我们要清楚netty中提供的option方法是给谁设置参数。
在server端,有两种设置方式:

    new ServerBootstrap().option();  // 是给ServerSocketChannel配置参数
    new ServerBootstrap().childOption();// 是给SocketChannel配置参数

在client端,只有一种:

new Bootstrap().option();// 是给SocketChannel配置参数

2.1 CONNECT_TIMEOUT_MILLIS(客户端参数)

  • 用在客户端建立连接时,如果在指定毫秒内无法连接,会抛出 timeout 异常
  • 与SO_TIMEOUT是不一样的,SO_TIMEOUT主要用在阻塞 IO,阻塞 IO 中 accept,read 等都是无限等待的,如果不希望永远阻塞,使用它调整超时时间
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,1000)

2.2 SO_BACKLOG

在这里插入图片描述
如上图,在进行三次握手的时候,如果还未完成三次握手,这时候会将连接放到半连接队列。如果完成了三次握手,会将其从半连接队列移到全连接队列中,然后,才会执行accept方法。这时,服务器会从全连接队列中获取连接并进行处理。SO_BACKLOG主要用于设置全连接队列的大小。当处理Accept的速率小于连接建立的速率时,全连接队列中堆积的连接数大于SO_BACKLOG设置的值是,便会抛出异常。

.option(ChannelOption.SO_BACKLOG, SO_BACK_LOG)

默认值,Windows为200,其他为128。
netty设置默认值的源码
我们找到抽象类ServerSocketChannel中的bind方法。

    public abstract ServerSocketChannel bind(SocketAddress local, int backlog)
        throws IOException;

在这里插入图片描述

        SOMAXCONN = AccessController.doPrivileged(new PrivilegedAction<Integer>() {
            @Override
            public Integer run() {
                // - Windows NT Server 4.0+: 200
                // - Linux and Mac OS X: 128
                int somaxconn = PlatformDependent.isWindows() ? 200 : 128;
                File file = new File("/proc/sys/net/core/somaxconn");
                BufferedReader in = null;
                try {
                    if (file.exists()) {
                        in = new BufferedReader(new FileReader(file));
                        somaxconn = Integer.parseInt(in.readLine());
                        if (logger.isDebugEnabled()) {
                            logger.debug("{}: {}", file, somaxconn);
                        }
                    } else {
                        Integer tmp = null;
                        if (SystemPropertyUtil.getBoolean("io.netty.net.somaxconn.trySysctl", false)) {
                            tmp = sysctlGetInt("kern.ipc.somaxconn");
                            if (tmp == null) {
                                tmp = sysctlGetInt("kern.ipc.soacceptqueue");
                                if (tmp != null) {
                                    somaxconn = tmp;
                                }
                            } else {
                                somaxconn = tmp;
                            }
                        }

                        if (tmp == null) {
                            logger.debug("Failed to get SOMAXCONN from sysctl and file {}. Default: {}", file,
                                         somaxconn);
                        }
                    }
                } catch (Exception e) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Failed to get SOMAXCONN from sysctl and file {}. Default: {}",
                                file, somaxconn, e);
                    }
                } finally {
                    if (in != null) {
                        try {
                            in.close();
                        } catch (Exception e) {
                            // Ignored.
                        }
                    }
                }
                return somaxconn;
            }
        });

2.3 TCP_NODELAY

正常情况下,小数据包在发送前会组合成更大的包。在发送另一个包之前,本地主机要等待远程系统对前一个包的确认。这称为Nagle算法。如果远程系统没有足够快的确认发送回本地系统, 那么依赖于小数据量信息稳定传输的应用程序会变得很慢。设置TCP_NODELAY为true可以打破这种缓冲模式,这样所有的包一旦就绪就会全部发送。

2.4 SO_REUSEADDR

一般来说,一个端口释放后会等待两分钟之后才能再被使用,不然会报端口已占用异常,SO_REUSEADDR是让端口释放后立即就可以被再次使用。

2.5 SO_KEEPALIVE

如果打开了该选项,客户端会偶尔通过一个空闲连接发送一个数据包,以确保服务器未崩溃(类似于服务器集群中的心跳机制)

2.6 ALLOCATOR

下面这个代码中,我们通过ctx.alloc().buffer()获取byteBuf对象,它默认的类型是class io.netty.buffer.PooledUnsafeDirectByteBuf。我们来看下源码中是如何决定这个类型的。

public class AllocServer {
    public static void main(String[] args) {
        new ServerBootstrap().group(new NioEventLoopGroup())
            .channel(NioServerSocketChannel.class)
            .childHandler(new ChannelInitializer<NioSocketChannel>() {
                protected void initChannel(NioSocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        @Override
                        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                            ByteBuf buffer = ctx.alloc().buffer();
                            System.out.println(buffer.getClass());
                        }
                    });
                }
            })
            .bind(8080);
    }
}

1、alloc方法跳转路径
在这里插入图片描述
2、ByteBufUtil中DEFAULT_ALLOCATOR属性的初始化
属性:io.netty.allocator.type

    static final ByteBufAllocator DEFAULT_ALLOCATOR;

    static {
    	// 1. 去找io.netty.allocator.type属性,如果配置了则按配置的,否则判断是否是安卓,是->非池化,不是->池化
        String allocType = SystemPropertyUtil.get(
                "io.netty.allocator.type", PlatformDependent.isAndroid() ? "unpooled" : "pooled");
        allocType = allocType.toLowerCase(Locale.US).trim();

        ByteBufAllocator alloc;
        // 2. 根据类型判断是创建池化还是非池化的byteBuf,默认为池化
        if ("unpooled".equals(allocType)) {
        	// 3.建立非池化分配器,详细代码请看下面的第3点
            alloc = UnpooledByteBufAllocator.DEFAULT;
            logger.debug("-Dio.netty.allocator.type: {}", allocType);
        } else if ("pooled".equals(allocType)) {
            alloc = PooledByteBufAllocator.DEFAULT;
            logger.debug("-Dio.netty.allocator.type: {}", allocType);
        } else {
            alloc = PooledByteBufAllocator.DEFAULT;
            logger.debug("-Dio.netty.allocator.type: pooled (unknown: {})", allocType);
        }

        DEFAULT_ALLOCATOR = alloc;
    }

3、UnpooledByteBufAllocator.DEFAULT
属性:io.netty.noPreferDirect
在这里插入图片描述
4.测试
我们启动AllocServer时,加下属性-Dio.netty.noPreferDirect=true -Dio.netty.allocator.type=unpooled
在这里插入图片描述

控制台
在这里插入图片描述

  • 16
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值