閒言:關於mesos的學習,因爲某些原因(我的性能好的臺式暫時不可用了)而擱置,擱置時間不定
最近在對接某個第三方的socket接口的時候,對與這種寫socket服務完全不考慮粘包拆包的操作完全絕望了,所以寫一下粗淺的關於如何自定義協議,以及編寫可處理粘包拆包的編解碼器。關於tcp粘包拆包的概念請查閱其它博客
個人認爲想能解決拆包和粘包問題,根本的途徑就是知道消息有多長。大致常用的方法就是消息有指定的分隔符,或者在前面固定大小的消息頭裏存儲剩餘消息主體的大小
示例如下,實例基於netty編寫,大致想法相同,不過netty免去了自己管理緩存
package protocol;
import java.io.UnsupportedEncodingException;
/**
* Created by tangjiaqi on 2018/5/17.
*/
public class Message {
public enum MessageType{
BASE(1),
DISCONNECT(2);
int value;
MessageType(int i) {
value = i;
}
public int getValue(){
return value;
}
public static MessageType valueOf(int i){
MessageType[] types = MessageType.values();
MessageType result = null;
for (MessageType x: types){
if (x.getValue() == i){
result = x;
break;
}
}
return result;
}
}
// 協議頭,標記協議從什麼時候開始
private final static int HEADER = 0x7788b;
// 消息類型
private MessageType messageType;
// 字符集的字符串長度(考慮不同編碼的數據)
private int charsetLength;
// 內容長度
private int contentLength;
// 內容
private byte[] charset;
// 字符集
private byte[] content;
public Message() {
}
public Message(MessageType messageType, String content, String charset) throws UnsupportedEncodingException {
this.messageType = messageType;
this.content = content.getBytes(charset);
this.charset = charset.getBytes();
this.contentLength = this.content.length;
this.charsetLength = this.charset.length;
}
public MessageType getMessageType() {
return messageType;
}
public void setMessageType(MessageType messageType) {
this.messageType = messageType;
}
public int getCharsetLength() {
return charsetLength;
}
public int getContentLength() {
return contentLength;
}
public byte[] getContent() {
return content;
}
public void setContent(String content, String charset) throws UnsupportedEncodingException {
this.content = content.getBytes(charset);
this.charset = charset.getBytes();
this.contentLength = this.content.length;
this.charsetLength = this.charset.length;
}
public byte[] getCharset() {
return charset;
}
public static int getHEADER() {
return HEADER;
}
}
Encoder:
package codce;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import protocol.Message;
/**
* Created by tangjiaqi on 2018/5/17.
*/
public class MyProtocolEncoder extends MessageToByteEncoder<Message> {
public MyProtocolEncoder(){
}
protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception {
out.writeInt(msg.getHEADER());
out.writeInt(msg.getMessageType().getValue());
out.writeInt(msg.getCharsetLength());
out.writeInt(msg.getContentLength());
out.writeBytes(msg.getCharset());
out.writeBytes(msg.getContent());
}
}
encoder較爲簡單,按順序寫入ByteBuf就好了
Decoder:
package codce;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import protocol.Message;
import protocol.Message.MessageType;
import java.util.List;
/**
* Created by tangjiaqi on 2018/5/17.
*/
public class MyProtocolDecoder extends ByteToMessageDecoder {
public MyProtocolDecoder(){
}
// 協議頭+消息類型+charsetLength+contentLength的長度
private final static int BASE_LENGTH = 16;
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
System.out.println("start decode");
int beginIndex = in.readerIndex();
int messageTypeValue = 0, charsetLength = 0, contentLength = 0;
boolean flag = false;
while (in.readableBytes() > BASE_LENGTH){
beginIndex = in.readerIndex();
int tmp = in.readInt();
// 協議開始
if (tmp == Message.getHEADER()){
messageTypeValue = in.readInt();
charsetLength = in.readInt();
contentLength = in.readInt();
flag = true;
break;
}
}
if (!flag){
return;
}else {
// 剩餘可讀數據小於所需,返回等待下次數據的到來
if (in.readableBytes() < (charsetLength + contentLength)){
// 記住將讀索引歸位
in.readerIndex(beginIndex);
return;
}else {
byte[] charset = new byte[charsetLength];
byte[] content = new byte[contentLength];
in.readBytes(charset);
in.readBytes(content);
String charsetStr = new String(charset);
String contentStr = new String(content, charsetStr);
Message message = new Message(MessageType.valueOf(messageTypeValue), contentStr, charsetStr);
out.add(message);
}
}
System.out.println("end decode");
}
}
關於解碼器部分其實就幾個注意點
- 首先要讀取到你自定義的包頭的開始字段
- 一次性讀取你定義的固定長度的頭,從而能從頭中獲取到剩餘字段的大小
達成以上兩點大致就能做到解決拆包粘包問題了,關於這個協議還做了一個測試,測試大致就是一次性傳輸內容較大,然後查看接受到的內容以及解碼過程
關於對這個自定義協議的demo應用詳見: https://github.com/ncuwaln/protocol-demo
ps: 這個自定義協議沒有考慮任何傳輸內容的減少的優化以及其它七七八八的各種細節,很簡單的那種