背景
我们知道类似HTTP协议属于一种网络规范,包括数据结构的定义和通信规则。很多情况下,业务可能基于自身的需要,实现自有的协议,比如像Dubbo的dubbo协议。
描述
本期继续netty相关内容,这次我们根据已有例子,自己定义网络协议并完成通信。主要包括协议数据结构的定义,数据的解析和反解析,网络交互规则,而交互包括认证和心跳两部分。
下表是整体的介绍,描述各个部分的功能。
类名称 | 功能 |
---|---|
Header | 协议数据的头部 |
NettyMessage | 协议数据,包括头部和数据部分 |
HeartbeatReqHandler | 客户端处理心跳消息 |
HeartbeatRespHandler | 服务端处理心跳消息 |
LoginAuthReqHandler | 客户端处理登录或者握手消息 |
LoginAuthRespHandler | 服务端处理登录或者握手消息 |
NettyMessageDecoder | 从收到的byte字节数据解码成消息体 |
NettyMessageEncoder | 对数据进行编码转换成byte字节发送 |
NettyProtocolClient | 客户端实例 |
NettyProtocolServer | 服务端实例 |
下面依次介绍下各个部分和包含的代码,内容会比较多。
代码
消息头结构
消息头的定义,如协议标记,用来识别协议,是个固定值。
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.HashMap;
import java.util.Map;
/**
* 自定义协议的消息头
*/
@NoArgsConstructor
@Data
public class Header {
//协议标记
private int crcCode = 0xabef0101;
//消息长度
private int length;
private long sessionId;
//消息类型 MessageType
private byte type;
//优先级
private byte priority;
//扩展
private Map<String, Object> attachment = new HashMap<>();
public Header(byte type) {
this.type = type;
}
//消息类型定义
@AllArgsConstructor
@Getter
public enum MessageType {
BIZ_REQ((byte) 0),
BIZ_RESP((byte) 1),
ONE_WAY_REQ((byte) 2),
HANDSHAKE_REQ((byte) 3),
HANDSHAKE_RESP((byte) 4),
HEARTBEAT_REQ((byte) 5),
HEARTBEAT_RESP((byte) 6),
;
byte value;
}
}
消息体结构
消息体结构比较简单,除了头部,剩下的数据用Object处理。
import lombok.Data;
/**
* 消息结构
*/
@Data
public class NettyMessage {
private Header header;
private Object body;
}
消息封装(编码)
这里介绍对消息进行编码,首先处理头部,从消息对象msg
中一次读取header的各项内容,然后按照顺序
写入到ByteBuf
中,注意根据类型的不同,使用的写入方法也是不同的。这里的ByteBuf
是netty对byte[]
操作的封装,如果自己实现,要麻烦很多。之后进行header.attachment
的写入,因为key是string,而value是对象,所以这里我把value先转成String,然后写入大小和内容(实际操作先写入大小,再写入内容),而且因为是对象,把对象的类型提取出来,也写到后面;同样,如果消息的数据部分body
不为空,采用和上面一致的方式,写入body
转换后的内容大小,也写入body
的类型名称。具体的也可以参照代码来处理。
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
public final class NettyMessageEncoder extends MessageToMessageEncoder<NettyMessage> {
@Override
protected void encode(ChannelHandlerContext ctx, NettyMessage msg, List<Object> out) throws Exception {
if (null == msg || msg.getHeader() == null) {
throw new Exception("encode message is null!");
}
ByteBuf sendBuf = Unpooled.buffer();
sendBuf.writeInt(msg.getHeader().getCrcCode())
.writeInt(msg.getHeader().getLength())
.writeLong(msg.getHeader().getSessionId())
.writeByte(msg.getHeader().getType())
.writeByte(msg.getHeader().getPriority())
.writeInt(msg.getHeader().getAttachment().size());
byte[] keyArray;
byte[] valueArray;
for (Map.Entry<String, Object> entry : msg.getHeader().getAttachment().entrySet()) {
//写入key,先写大小,再写值,值用Gson转成String,同时把值的类型也写入
keyArray = entry.getKey().getBytes(StandardCharsets.UTF_8);
sendBuf.writeInt(keyArray.length);
sendBuf.writeBytes(keyArray);
//写值的数据,先写大小,再写内容
valueArray = GsonUtil.toJson(entry.getValue()).getBytes(StandardCharsets.UTF_8);
sendBuf.writeInt(valueArray.length);
sendBuf.writeBytes(valueArray);
//写值的类型名称
byte[] clazzArray = entry.getValue().getClass().getName().getBytes();
sendBuf.writeInt(clazzArray.length);
sendBuf.writeBytes(clazzArray);
}
if (msg.getBody() != null) {
byte[] bodyArray = GsonUtil.toJson(msg.getBody()).getBytes(StandardCharsets.UTF_8);
sendBuf.writeInt(bodyArray.length);
sendBuf.writeBytes(bodyArray);
byte[] clazzArray = msg.getBody().getClass().getName().getBytes();
sendBuf.writeInt(clazzArray.length);
sendBuf.writeBytes(clazzArray);
}
//sendBuf.writeInt(0);
sendBuf.setInt(4, sendBuf.readableBytes());
out.add(sendBuf);
}
}
消息解析(解码)
解码就相当于编码的反操作,依次按照顺序解出内容。首先是消息头部,对于对象类型的处理,是先读取对象的大小,再取内容,再取类型的大小和名称,然后通过Gson转换回java实例对象,如下的打码,可以详细了解下注释的部分。
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
public final class NettyMessageDecoder extends LengthFieldBasedFrameDecoder {
public NettyMessageDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength) {
super(maxFrameLength, lengthFieldOffset, lengthFieldLength);
}
@Override
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
// ByteBuf frame = (ByteBuf) super.decode(ctx, in);
// if (null == frame) {
// return null;
// }
NettyMessage message = new NettyMessage();
Header header = new Header();
header.setCrcCode(in.readInt());
header.setLength(in.readInt());
header.setSessionId(in.readLong());
header.setType(in.readByte());
header.setPriority(in.readByte());
int size = in.readInt();
if (size > 0) {
Map<String, Object> attch = new HashMap<>();
int keySize;
int valueSize;
byte[] keyArray;
byte[] valueArray;
String key;
String value;
for (int i = 0; i < size; i++) {
//读取长度值
keySize = in.readInt();
keyArray = new byte[keySize];
//读取内容
in.readBytes(keyArray);
key = new String(keyArray, StandardCharsets.UTF_8);
valueSize = in.readInt();
valueArray = new byte[valueSize];
in.readBytes(valueArray);
value = new String(valueArray, StandardCharsets.UTF_8);
int clazzSize = in.readInt();
byte[] clazzArray = new byte[clazzSize];
in.readBytes(clazzArray);
String clazz = new String(clazzArray, StandardCharsets.UTF_8);
attch.put(key, GsonUtil.parse(value, Class.forName(clazz)));
}
header.setAttachment(attch);
}
//4代表一个int,超过表示还有数据
if (in.readableBytes() > 4) {
//body的长度
int bodyLength = in.readInt();
byte[] bodyArray = new byte[bodyLength];
//body的内容并转成String
in.readBytes(bodyArray);
String bodyJson = new String(bodyArray, StandardCharsets.UTF_8);
int clazzSize = in.readInt();
byte[] clazzArray = new byte[clazzSize];
in.readBytes(clazzArray);
String clazz = new String(clazzArray, StandardCharsets.UTF_8);
//根据body的内容json和类型名称,转换成对象
Object body = GsonUtil.parse(bodyJson, Class.forName(clazz));
message.setBody(body);
}
message.setHeader(header);
return message;
}
}
附加-json转换
为方便直接运行代码使用,把对应的json转换也放到这里。
import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
@Slf4j
public class GsonUtil {
private static final Gson GSON =
new GsonBuilder().disableHtmlEscaping().setDateFormat("yyyy-MM-dd HH:mm:ss").serializeNulls().addSerializationExclusionStrategy(new ExclusionStrategy() {
@Override
public boolean shouldSkipField(FieldAttributes fieldAttributes) {
return fieldAttributes.getAnnotation(GsonIgnore.class) != null;
}
@Override
public boolean shouldSkipClass(Class<?> aClass) {
return false;
}
}).create();
public static String toJson(Object src) {
return GSON.toJson(src);
}
public static <T> T parse(String str, Class<T> classOfT) {
return GSON.fromJson(str, classOfT);
}
/**
* 解析泛型
*
* @param json
* @param typeToken
* @param <T>
* @return
*/
public static <T> T parse(String json, TypeToken typeToken) {
return GSON.fromJson(json, typeToken.getType());
}
}
登录处理
- 客户端登录请求
客户端处理登录请求的代码,核心是在启动的时候在channelActive
方法中,发送一个登录注册请求。在channelRead
方法中,获取从服务端的返回,并根据协议消息的头部字段,判断类型。
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
/**
* 客户端支持登录请求
*/
public class LoginAuthReqHandler extends ChannelInboundHandlerAdapter {
private NettyMessage buildAuthReq() {
NettyMessage message = new NettyMessage();
Header header = new Header(Header.MessageType.HANDSHAKE_REQ.value);
message.setHeader(header);
return message;
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("login auth req handler active ...");
NettyMessage message = buildAuthReq();
System.out.println("[send] client send login req ---> " + message);
ctx.writeAndFlush(message);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
NettyMessage message = (NettyMessage) msg;
if (message.getHeader() != null && message.getHeader().getType() == Header.MessageType.HANDSHAKE_RESP.value) {
System.out.println("[recv] client receive login resp ---> " + message);
byte loginResult = (byte) message.getBody();
if (loginResult != (byte) 0) {
System.out.println("handshake fail!");
ctx.close();
} else {
System.out.println("handshake success.");
ctx.fireChannelRead(msg);
}
} else {
ctx.fireChannelRead(msg);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.fireExceptionCaught(cause);
}
}
- 服务端登录请求
服务端对登录请求的处理,在收到消息后,判断是否是握手消息,如果是握手消息,这里加了个IP白名单的判断,如果判断成功,构造一个握手返回请求,返回给客户端。这里用body
的值来判断是否握手成功。
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import java.net.InetSocketAddress;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 服务端处理登录握手
*/
public class LoginAuthRespHandler extends ChannelInboundHandlerAdapter {
private Map<String, Boolean> nodeCheck = new ConcurrentHashMap<>();
private String[] whiteList = new String[] {"127.0.0.1"};
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
NettyMessage message = (NettyMessage) msg;
if (message.getHeader() != null && message.getHeader().getType() == Header.MessageType.HANDSHAKE_REQ.value) {
String nodeIndex = ctx.channel().remoteAddress().toString();
NettyMessage loginResp;
if (nodeCheck.containsKey(nodeIndex)) {
loginResp = buildResponse((byte) -1);
} else {
InetSocketAddress address = (InetSocketAddress) ctx.channel().remoteAddress();
String ip = address.getAddress().getHostAddress();
boolean ok = false;
for (String e : whiteList) {
if (e.equals(ip)) {
ok = true;
break;
}
}
loginResp = ok ? buildResponse((byte) 0) : buildResponse((byte) -1);
if (ok) {
nodeCheck.put(nodeIndex, true);
}
}
System.out.println("[send] login response :" + loginResp + " , body:" + loginResp.getBody());
ctx.writeAndFlush(loginResp);
}else {
ctx.fireChannelRead(msg);
}
}
//构造握手返回请求
private NettyMessage buildResponse(byte resp) {
NettyMessage message = new NettyMessage();
Header header = new Header();
header.setType(Header.MessageType.HANDSHAKE_RESP.value);
message.setHeader(header);
message.setBody(resp);
return message;
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
nodeCheck.remove(ctx.channel().remoteAddress().toString());
ctx.close();
ctx.fireExceptionCaught(cause);
}
}
心跳处理
- 客户端心跳处理
这里看下客户端的心跳处理,有一个调度线程池,操作就是定时地发送心跳消息给服务端。同时channelRead
在负责收到消息后,判断执行的操作。这里心跳是在收到握手成功的消息后开始的。注意channelAcitve
方法是否执行和netty pipline中的channelHandler
执行顺序有关系,比较复杂感谢的同学可以额外了解下。
这里除了在收到握手消息之后开始心跳探测,后续正常收到心跳返回包,则打印日志。
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import java.time.LocalDateTime;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
/**
* 客户端处理心跳请求
*/
public class HeartbeatReqHandler extends ChannelInboundHandlerAdapter {
private volatile ScheduledFuture<?> heartbeat;
// @Override
// public void channelActive(ChannelHandlerContext ctx) throws Exception {
// System.out.println("heartbeat req channel active ...");
// heartbeat = ctx.executor().scheduleAtFixedRate(new HeartbeatTask(ctx), 0, 5, TimeUnit.SECONDS);
// }
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
NettyMessage message = (NettyMessage)msg;
if (message.getHeader() != null) {
//heartbeat 稍微调整下代码,因为握手比较少,心跳比较多。
if (message.getHeader().getType() == Header.MessageType.HEARTBEAT_RESP.value) {
System.out.println("[recv] client receive server heartbeat message ---> " + message);
} else
//login,握手成功以后开始客户端给服务端启动心跳
if (message.getHeader().getType() == Header.MessageType.HANDSHAKE_RESP.value) {
heartbeat = ctx.executor().scheduleAtFixedRate(new HeartbeatTask(ctx), 0, 5, TimeUnit.SECONDS);
System.out.println("[recv] client receive server login resp, start heartbeat ---> " + message);
}
}
ctx.fireChannelRead(msg);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
if (null != heartbeat) {
heartbeat.cancel(true);
heartbeat = null;
}
cause.printStackTrace();
ctx.fireExceptionCaught(cause);
}
private class HeartbeatTask implements Runnable {
private final ChannelHandlerContext ctx;
public HeartbeatTask(final ChannelHandlerContext ctx) {
this.ctx = ctx;
}
@Override
public void run() {
NettyMessage message = new NettyMessage();
Header header = new Header();
header.setType(Header.MessageType.HEARTBEAT_REQ.value);
message.setHeader(header);
message.setBody("heartbeat @" + LocalDateTime.now().toString() + " ...");
System.out.println("[send] client send heartbeat to server ---> " + message);
ctx.writeAndFlush(message);
}
}
}
- 服务端心跳处理
服务端的心跳处理就比较简单了,在收到心跳请求后,发送对等的包给客户端。
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import java.time.LocalDateTime;
/**
* 服务端处理心跳请求
*/
public class HeartbeatRespHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
NettyMessage message = (NettyMessage)msg;
if (message.getHeader() != null && message.getHeader().getType() == Header.MessageType.HEARTBEAT_REQ.value) {
System.out.println("[recv] server received heartbeat from client ---> " + message);
NettyMessage heartbeat = new NettyMessage();
Header header = new Header();
header.setType(Header.MessageType.HEARTBEAT_RESP.value);
heartbeat.setHeader(header);
heartbeat.setBody("heartbeat server resp @" + LocalDateTime.now());
System.out.println("[send] server send heartbeat resp to client ---> " + heartbeat);
ctx.writeAndFlush(heartbeat);
}else {
ctx.fireChannelRead(msg);
}
}
}
消息客户端
消息处理的客户端程序。注意有一个线程池用来做重试,这里这是简单的一次尝试重连,只做示例。其次,注意pipline
的添加顺序可能影响Handler
的执行结果。其他可以看下代码注释。
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.timeout.ReadTimeoutHandler;
import org.junit.Test;
import java.net.InetSocketAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class NettyProtocolClient {
private ExecutorService executor = Executors.newFixedThreadPool(1);
public void connect(int port, String host) {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast(new NettyMessageDecoder(1024 * 1024, 4, 4))
.addLast(new NettyMessageEncoder())
.addLast(new ReadTimeoutHandler(50))
.addLast(new LoginAuthReqHandler())
.addLast(new HeartbeatReqHandler());
}
});
final String localhost = "127.0.0.1";
final int localPort = 1922;
//如果加了本地地址和端口,相当于固定了本地端口,如果停止后立刻再次登录,由于之前请求会保留TIME_WAIT状态socket一段时间,会抛出Caused by: java.net.BindException: 地址已在使用
//没有设置本地端口,每次本地端口是新生成的,短时间内立即请求会消耗大量系统连接资源
System.out.println("client connect @" + host + ":" + port);
//注意用了两个sync,没有channelFuture可能不会抛出异常
ChannelFuture future =
bootstrap.connect(new InetSocketAddress(host, port), new InetSocketAddress(localhost, localPort)).sync().channel().closeFuture().sync();
future.channel().closeFuture().sync();
//不会抛异常
//bootstrap.connect(new InetSocketAddress(host, port), new InetSocketAddress(localhost, localPort)).channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
//资源释放后,清空,然后发起重连
//这里加了强制等待线程执行1分钟,否则是直接退出的。
executor.execute(() -> {
try {
int timeout = 15;
System.out.println("retry connect after :" + timeout);
TimeUnit.SECONDS.sleep(timeout);
try {
System.out.println("retry connect ...");
connect(port, host);
} catch (Exception e) {
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
});
if (!executor.isTerminated()) {
try {
executor.awaitTermination(1, TimeUnit.MINUTES);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
@Test
public void runClient() {
int port = 12342;
String host = "127.0.0.1";
connect(port, host);
}
}
消息服务端
服务端相对要简单一些,添加各个EventLoopGroup
之后,追加自定的消息解析器和协议handler
,绑定指定端口启动就可以接收情趣了。
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
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.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.ReadTimeoutHandler;
import org.junit.Test;
public class NettyProtocolServer {
private void bind(int port) throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
try {
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.handler(new LoggingHandler(LogLevel.DEBUG))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast(new NettyMessageDecoder(1024 * 1024, 4, 4))
.addLast(new NettyMessageEncoder())
.addLast(new ReadTimeoutHandler(60))
.addLast(new LoginAuthRespHandler())
.addLast(new HeartbeatRespHandler());
}
});
System.out.println("netty protocol server@" + port);
bootstrap.bind(port).channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
@Test
public void runServer() throws InterruptedException {
bind(12342);
}
}
运行结果
服务端
netty protocol server@12342
[send] login response :NettyMessage(header=Header(crcCode=-1410399999, length=0, sessionId=0, type=4, priority=0, attachment={}), body=0) , body:0
[recv] server received heartbeat from client ---> NettyMessage(header=Header(crcCode=-1410399999, length=86, sessionId=0, type=5, priority=0, attachment={}), body=heartbeat @2020-09-04T16:19:58.102 ...)
[send] server send heartbeat resp to client ---> NettyMessage(header=Header(crcCode=-1410399999, length=0, sessionId=0, type=6, priority=0, attachment={}), body=heartbeat server resp @2020-09-04T16:19:58.118)
[recv] server received heartbeat from client ---> NettyMessage(header=Header(crcCode=-1410399999, length=86, sessionId=0, type=5, priority=0, attachment={}), body=heartbeat @2020-09-04T16:20:03.089 ...)
[send] server send heartbeat resp to client ---> NettyMessage(header=Header(crcCode=-1410399999, length=0, sessionId=0, type=6, priority=0, attachment={}), body=heartbeat server resp @2020-09-04T16:20:03.091)
[recv] server received heartbeat from client ---> NettyMessage(header=Header(crcCode=-1410399999, length=86, sessionId=0, type=5, priority=0, attachment={}), body=heartbeat @2020-09-04T16:20:08.090 ...)
[send] server send heartbeat resp to client ---> NettyMessage(header=Header(crcCode=-1410399999, length=0, sessionId=0, type=6, priority=0, attachment={}), body=heartbeat server resp @2020-09-04T16:20:08.091)
客户端
client connect @127.0.0.1:12342
login auth req handler active ...
[send] client send login req ---> NettyMessage(header=Header(crcCode=-1410399999, length=0, sessionId=0, type=3, priority=0, attachment={}), body=null)
[recv] client receive login resp ---> NettyMessage(header=Header(crcCode=-1410399999, length=45, sessionId=0, type=4, priority=0, attachment={}), body=0)
handshake success.
[recv] client receive server login resp, start heartbeat ---> NettyMessage(header=Header(crcCode=-1410399999, length=45, sessionId=0, type=4, priority=0, attachment={}), body=0)
[send] client send heartbeat to server ---> NettyMessage(header=Header(crcCode=-1410399999, length=0, sessionId=0, type=5, priority=0, attachment={}), body=heartbeat @2020-09-04T16:19:58.102 ...)
[recv] client receive server heartbeat message ---> NettyMessage(header=Header(crcCode=-1410399999, length=94, sessionId=0, type=6, priority=0, attachment={}), body=heartbeat server resp @2020-09-04T16:19:58.118)
[send] client send heartbeat to server ---> NettyMessage(header=Header(crcCode=-1410399999, length=0, sessionId=0, type=5, priority=0, attachment={}), body=heartbeat @2020-09-04T16:20:03.089 ...)
[recv] client receive server heartbeat message ---> NettyMessage(header=Header(crcCode=-1410399999, length=94, sessionId=0, type=6, priority=0, attachment={}), body=heartbeat server resp @2020-09-04T16:20:03.091)
[send] client send heartbeat to server ---> NettyMessage(header=Header(crcCode=-1410399999, length=0, sessionId=0, type=5, priority=0, attachment={}), body=heartbeat @2020-09-04T16:20:08.090 ...)
[recv] client receive server heartbeat message ---> NettyMessage(header=Header(crcCode=-1410399999, length=94, sessionId=0, type=6, priority=0, attachment={}), body=heartbeat server resp @2020-09-04T16:20:08.091)
client
server
总结
以上就是本期的内容,netty确实在网络处理方面比较强大,能够比较容易地在其基础上实现各种网络组件,比自己从头开发要方便很多。本期也参考了书本和网络的知识(书之前吐槽过),在官方的github没有找到本期内容的示例,有篇文章但功能不是很完善。后面会给到所有的参考链接。
参考资料
[1]. netty example
[2]. 《netty权威指南》私有协议
网友评论