網絡通信從編解碼開始,前面的第一篇文章中,介紹過數據包的結構,這篇文章就要介紹一下拆包和組包的過程。
1. 包頭字段的設計目的
A. 起始分隔符:標明一個數據包的開始部分(裏面還隱含了小端模式的信息,這個小端模式可以忽略);
B. 協議的版本:當前版本是明文傳輸的,考慮到後期升級可能要採用密文傳輸包體,所以,設計了一個版本字段,當然,也可以用於協議內容的擴充,添加新的數據包類型時增加版本號的即可;
C. 頻道號:這個字段的作用不明顯,目前是想將包信息按照業務分類,比如:網絡連接,權限認證,聊天信息等;
D. 命令字:與頻道號、版本號聯合起來作爲包體類型的ID,參與Json串和Java對象的轉換操作;
2. 包體
要傳輸的Json字符串,二進制的形式,採用UTF-8的編碼形式;
3. 包尾
結束分隔符:標明一個數據包的結束部分,如果按照包長度解析錯誤之後,可以通過結束分隔符丟棄碎包,目前Demo沒有完成這個功能。
package houlei.net.tcp.codec;
import houlei.net.tcp.pkg.PackageVersion;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageCodec;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.List;
public class GenericPackageCodec extends ByteToMessageCodec<GenericPackage> {
public static final int HEADER_TAIL_LENGTH = 16;
public static final int PKG_MAX_LENGTH = 2048;
// 編碼過程
@Override
protected void encode(ChannelHandlerContext ctx, GenericPackage pkg, ByteBuf out) throws Exception {
switch (PackageVersion.valueOf(pkg.getVersion())) {
case V10:
encodeV10(ctx, pkg, out);
return;
default:
encodeVX(ctx, pkg, out);
return;
}
}
private void encodeVX(ChannelHandlerContext ctx, GenericPackage pkg, ByteBuf out) {
// 其他版本的編碼過程沒有實現,默認V10版本的處理過程
encodeV10(ctx, pkg, out);
}
private void encodeV10(ChannelHandlerContext ctx, GenericPackage pkg, ByteBuf out) {
String data = pkg.getData();
byte[] body = data==null ? new byte[0] : data.getBytes(Charset.forName("UTF-8"));
pkg.setLength((short)body.length);
if (out.isWritable((HEADER_TAIL_LENGTH + body.length))) {
out.writeBytes(GenericPackage.PKG_PREFIX);
out.writeShort(pkg.getVersion()).writeShort(pkg.getChannel()).writeShort(pkg.getCommand()).writeShort(body.length);
out.writeBytes(body);
out.writeBytes(GenericPackage.PKG_SUFFIX);
ctx.flush();
}
}
// 解碼過程
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
while (in.isReadable(HEADER_TAIL_LENGTH)) {
in.markReaderIndex();
byte[] buffer = new byte[GenericPackage.PKG_PREFIX.length];
in.readBytes(buffer);
if (!Arrays.equals(GenericPackage.PKG_PREFIX, buffer)) {
// 碎包片段。不進行skip,直接拋出異常,關閉連接。
throw new CodecException("PKG_PREFIX error.");
}
short version = in.readShort();
short channel = in.readShort();
short command = in.readShort();
short length = in.readShort();
if (length < 0 || length > PKG_MAX_LENGTH) {
// 長度不能小於零, 也不能超過最大長度。
throw new CodecException("PKG_length error.");
}
if (!in.isReadable(length + GenericPackage.PKG_SUFFIX.length)) {
in.resetReaderIndex();
// 接收到了不完整的包,等待下次讀取。
//TODO 應該校驗接收緩衝是否超過最大包長度。
return;
}
byte[] body = new byte[length];
in.readBytes(body);
in.readBytes(buffer);
if (!Arrays.equals(GenericPackage.PKG_SUFFIX, buffer)) {
// 碎包片段。未找到包尾分隔符。不進行skip,直接拋出異常,關閉連接。
throw new CodecException("PKG_SUFFIX error.");
}
in.resetReaderIndex();
in.skipBytes(HEADER_TAIL_LENGTH + length);
switch (PackageVersion.valueOf(version)) {
case V10:
decodeV10(ctx, version, channel, command, body, out);
break;
default:
decodeVX(ctx, version, channel, command, body, out);
break;
}
}
}
private void decodeVX(ChannelHandlerContext ctx, short version, short channel, short command, byte[] body, List<Object> out) {
// 其他版本的解碼過程沒有實現,默認V10版本的處理過程
decodeV10(ctx, version, channel, command, body, out);
}
private void decodeV10(ChannelHandlerContext ctx, short version, short channel, short command, byte[] body, List<Object> out) {
String data = new String(body, Charset.forName("UTF-8"));
GenericPackage pkg = new GenericPackage();
pkg.setVersion(version);
pkg.setChannel(channel);
pkg.setCommand(command);
pkg.setLength((short)body.length);
pkg.setData(data);
out.add(pkg);
}
}
package houlei.net.tcp.codec;
import houlei.net.tcp.pkg.PackageFactory;
import houlei.net.tcp.pkg.PackageType;
import houlei.net.tcp.utils.JsonUtil;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageCodec;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
@Component
public class GenericPackageClassifierCodec extends MessageToMessageCodec<GenericPackage, Object> {
@Resource
private PackageFactory packageFactory;
@Override
protected void encode(ChannelHandlerContext ctx, Object msg, List<Object> out) throws Exception {
PackageType type = packageFactory.getPackageType(msg.getClass());
if (type != null) {
String json = JsonUtil.toString(msg);
GenericPackage genericPackage = new GenericPackage();
genericPackage.setVersion(type.getVersion());
genericPackage.setChannel(type.getChannel());
genericPackage.setCommand(type.getCommand());
genericPackage.setData(json);
out.add(genericPackage);
} else {
//TODO 未知類型的對象,無法進行編碼操作,應該拋出異常。
}
}
@Override
protected void decode(ChannelHandlerContext ctx, GenericPackage pg, List<Object> out) throws Exception {
Class<?> klass = packageFactory.getClass(pg.getVersion(), pg.getChannel(), pg.getCommand());
if (klass != null) {
String json = pg.getData();
Object pkg = JsonUtil.fromString(json, klass);
out.add(pkg);
} else {
//TODO 未知類型的對象,無法進行解碼操作,應該進行特殊處理。
}
}
}
package houlei.net.tcp.codec;
import houlei.net.tcp.pkg.PackageType;
import houlei.net.tcp.utils.JsonUtil;
public class GenericPackage {
public final static byte[] PKG_PREFIX = new byte[]{(byte)0xFF,(byte)0xFE,0x06,0x08};
public final static byte[] PKG_SUFFIX = new byte[]{(byte)0xFF,(byte)0xFE,0x08,0x06};
private short version;
private short channel;
private short command;
private short length;
private String data;
public GenericPackage(){}
public GenericPackage(PackageType type) {
this.version = type.getVersion();
this.channel = type.getChannel();
this.command = type.getCommand();
}
@Override
public String toString() {
return JsonUtil.toString(this);
}
public short getVersion() {
return version;
}
public void setVersion(short version) {
this.version = version;
}
public short getChannel() {
return channel;
}
public void setChannel(short channel) {
this.channel = channel;
}
public short getCommand() {
return command;
}
public void setCommand(short command) {
this.command = command;
}
public short getLength() {
return length;
}
public void setLength(short length) {
this.length = length;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
}