目录
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
控制台