zoukankan      html  css  js  c++  java
  • 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#中:

    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;
    	}
    

      

    就行告一段落,太长了不好,读者可能吃不消。但我不鄙视长不好,终究长还是最有用的 =_=!

  • 相关阅读:
    SpringBoot中获取上下文
    @Import
    SpringBoot集成Swagger-Bootstrap-UI(已改名为Knife4j)
    SpringFox 3.0.0(包含springfox-swagger2-3.0.0)——无法访问/swagger-ui.html解决方案
    容器编排技术 docker compose 20210816
    SC Nacos 服务注册和发现202107
    GitHub Desktop报错 Authentication failed. Some common reasons include
    arcgis esriGeometryType
    小程序H5接口测试整理(JMeter)
    python+检查图像文件是否损坏,是,删除
  • 原文地址:https://www.cnblogs.com/CaomaoUnity3d/p/4610183.html
Copyright © 2011-2022 走看看