我們還沒講客戶端怎么向服務器發送消息,服務器怎么接受消息。
在講這個之前我們先要了解一點就是tcp底層存在粘包和拆包的機制,所以我們在進行消息傳遞的時候要考慮這個問題。
看了netty權威這里處理的辦法:
我決定netty采用自帶的半包解碼器LengthDecoder()的類處理粘包的問題,客戶端我是用這里的第三種思路。
消息的前四個字節是整個消息的長度,客戶端接收到消息的時候就將前4個字節解析出來,然后再根據長度接收消息。
那么消息的編解碼我用的是google的protobuf,這個在業界也相當有名,大家可以百度查查。不管你們用不用,反正我是用了。
在了解完之后,我們就來搭建這個消息編解碼的框架(當然這個只是我個人的想法,可能有很多不好的地方,你們可以指正)
首先需要下載的是支持c#的protobuf-net插件,注意google官方的是不支持c#的。
http://pan.baidu.com/s/1eQdFTmU
打開壓縮包,找到Full/Unity/protobuf-net.dll復制到我們的unity中。
在服務端呢,我用的是protobuff,這處理速度聽說和原生的相差不大。
和之前的一樣,吧這些jar包都添加到eclipse的build-path中。
好了,消息我服務器和客戶端都寫一個統一的協議SocketModel類,這樣傳送消息的時候就不會有歧義。
C#中:
using UnityEngine; using System.Collections; using System.Collections.Generic; using ProtoBuf;//注意要用到這個dll [ProtoContract] public class SocketModel{ [ProtoMember(1)] private int type;//消息類型 [ProtoMember(2)] private int area;//消息區域碼 [ProtoMember(3)] private int command;//指令 [ProtoMember(4)] private List<string> message;//消息 public SocketModel() { } public SocketModel(int type, int area, int command,List<string> message) { this.type = type; this.area = area; this.command = command; this.message = message; } public int GetType() { return type; } public void SetType(int type) { this.type = type; } public int GetArea() { return this.area; } public void SetArea(int area) { this.area = area; } public int GetCommand() { return this.command; } public void SetCommand(int command) { this.command = command; } public List<string> GetMessage() { return message; } public void SetMessage(List<string> message) { this.message = message; } }
java中:
public class SocketModel { private int type; private int area; private int command; private List<String> message; public int getType() { return type; } public void setType(int type) { this.type = type; } public int getArea() { return area; } public void setArea(int area) { this.area = area; } public int getCommand() { return command; } public void setCommand(int command) { this.command = command; } public List<String> getMessage() { return message; } public void setMessage(List<String> message) { this.message = message; } }
好了,制定好協議后,我們來動手在服務器搞出點事情來。
首先,打個包com.netty.decoder,在里面我們創建我們的解碼器類,LengthDecode和MessageDecode類
public class LengthDecoder extends LengthFieldBasedFrameDecoder{ public LengthDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip) { super(maxFrameLength, lengthFieldOffset, lengthFieldLength, lengthAdjustment, initialBytesToStrip); } }
這個功能你們可以去百度查,主要是吧接收到的二進制消息的前四個字節干掉。
public class MessageDecoder extends ByteToMessageDecoder{ private Schema<SocketModel> schema = RuntimeSchema.getSchema(SocketModel.class);//protostuff的寫法 @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> obj) throws Exception { byte[] data = new byte[in.readableBytes()]; in.readBytes(data); SocketModel message = new SocketModel(); ProtobufIOUtil.mergeFrom(data, message, schema); obj.add(message); } }
這個主要是吧接收的二進制轉化成我們的協議消息SocketModel類型。
接着是編碼器類,我們也打一個包,com.netty.encoder,里面創建一個MessageEncoder
在寫這個之前我們寫個工具類,com.netty.util,里面我么創建一個CoderUtil類,主要處理int和byte之間的轉化。
public class CoderUtil { /** * 將字節轉成整形 * @param data * @param offset * @return */ public static int bytesToInt(byte[] data, int offset) { int num = 0; for (int i = offset; i < offset + 4; i++) { num <<= 8; num |= (data[i] & 0xff); } return num; } /** * 將整形轉化成字節 * @param num * @return */ public static byte[] intToBytes(int num) { byte[] b = new byte[4]; for (int i = 0; i < 4; i++) { b[i] = (byte) (num >>> (24 - i * 8)); } return b; } }
MessageEncoder:
public class MessageEncoder extends MessageToByteEncoder<SocketModel>{ private Schema<SocketModel> schema = RuntimeSchema.getSchema(SocketModel.class); @Override protected void encode(ChannelHandlerContext ctx, SocketModel message, ByteBuf out) throws Exception { //System.out.println("encode"); LinkedBuffer buffer = LinkedBuffer.allocate(1024); byte[] data = ProtobufIOUtil.toByteArray(message, schema, buffer); ByteBuf buf = Unpooled.copiedBuffer(CoderUtil.intToBytes(data.length),data);//在寫消息之前需要把消息的長度添加到投4個字節 out.writeBytes(buf); } }
在寫完這些編解碼,我們需要將他們加到channel的pipeline中,
protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new LengthDecoder(1024,0,4,0,4)); ch.pipeline().addLast(new MessageDecoder()); ch.pipeline().addLast(new MessageEncoder()); ch.pipeline().addLast(new ServerHandler()); }
————————————————————————服務器告一段落,接着寫客戶端————————————————————————————
在我們之前寫的MainClient的代碼中我們加入接收和發送消息的方法。
private byte[] recieveData; private int len; private bool isHead; void Start() { if (client == null) { Connect(); } isHead = true; recieveData = new byte[800]; client.GetStream().BeginRead(recieveData,0,800,ReceiveMsg,client.GetStream());//在start里面開始異步接收消息 }
public void SendMsg(SocketModel socketModel) { byte[] msg = Serial(socketModel); //消息體結構:消息體長度+消息體 byte[] data = new byte[4 + msg.Length]; IntToBytes(msg.Length).CopyTo(data, 0); msg.CopyTo(data, 4); client.GetStream().Write(data, 0, data.Length); //print("send"); } public void ReceiveMsg(IAsyncResult ar)//異步接收消息 { NetworkStream stream = (NetworkStream)ar.AsyncState; stream.EndRead(ar); //讀取消息體的長度 if (isHead) { byte[] lenByte = new byte[4]; System.Array.Copy(recieveData,lenByte,4); len = BytesToInt(lenByte, 0); isHead = false; } //讀取消息體內容 if (!isHead) { byte[] msgByte = new byte[len]; System.Array.ConstrainedCopy(recieveData,4,msgByte,0,len); isHead = true; len = 0; message = DeSerial(msgByte); } stream.BeginRead(recieveData,0,800,ReceiveMsg,stream); } private byte[] Serial(SocketModel socketModel)//將SocketModel轉化成字節數組 { using (MemoryStream ms = new MemoryStream()) { Serializer.Serialize<SocketModel>(ms, socketModel); byte[] data = new byte[ms.Length]; ms.Position= 0; ms.Read(data, 0, data.Length); return data; } } private SocketModel DeSerial(byte[] msg)//將字節數組轉化成我們的消息類型SocketModel { using(MemoryStream ms = new MemoryStream()){ ms.Write(msg,0,msg.Length); ms.Position = 0; SocketModel socketModel = Serializer.Deserialize<SocketModel>(ms); return socketModel; } } public static int BytesToInt(byte[] data, int offset) { int num = 0; for (int i = offset; i < offset + 4; i++) { num <<= 8; num |= (data[i] & 0xff); } return num; } public static byte[] IntToBytes(int num) { byte[] bytes = new byte[4]; for (int i = 0; i < 4; i++) { bytes[i] = (byte)(num >> (24 - i * 8)); } return bytes; }
就行告一段落,太長了不好,讀者可能吃不消。但我不鄙視長不好,終究長還是最有用的 =_=!