Netty游戏服务器之四protobuf编解码和黏包处理
我们还没讲客户端怎么向服务器发送消息,服务器怎么接受消息。
在讲这个之前我们先要了解一点就是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#中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | 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中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | 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类
1 2 3 4 5 6 7 8 9 | public class LengthDecoder extends LengthFieldBasedFrameDecoder{ public LengthDecoder( int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip) { super (maxFrameLength, lengthFieldOffset, lengthFieldLength, lengthAdjustment, initialBytesToStrip); } } |
这个功能你们可以去百度查,主要是吧接收到的二进制消息的前四个字节干掉。
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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之间的转化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 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:
1 2 3 4 5 6 7 8 9 10 11 12 | 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中,
1 2 3 4 5 6 | 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的代码中我们加入接收和发送消息的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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里面开始异步接收消息 } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | 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; } |
就行告一段落,太长了不好,读者可能吃不消。但我不鄙视长不好,终究长还是最有用的 =_=!
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步