Unity网络服务器搭建【中高级】

前言

众所周知,网络游戏中,服务器的搭建尤为重要,无论是授权服务器,还是非授权服务器,它都承担着很大一部分的数据处理。今天,给大家分享一个中高级的小服务器搭建,当然也是在Unity上实现的,可以迁移为服务,外部程序等。其中加入了对象的序列化与反序列化,数据包的封包与拆包,细细品味,别有一番滋味。

  • 服务器基础搭建

      using UnityEngine;
      using System.Collections;
      using System.Net;
      using System.Net.Sockets;
      using System.Collections.Generic;
      using System;
      using System.IO;
    
      //实时消息处理委托
      public delegate void NetEventHandler (string msg);
    
      public class NetUtility
      {
          //单例
          public static readonly NetUtility Instance = new NetUtility ();
          //消息回调
          private NetEventHandler ReceiveCallback;
          //服务器Tcp
          private TcpListener tcpServer;
          //客户端Tcp
          private TcpClient tcpClient;
          //缓冲区
          private byte[] buffer;
          //缓存数据组
          private List<byte> cache;
          //网络节点
          private IPEndPoint serverIPEndPoint;
    
          /// <summary>
          /// 设置网络节点
          /// </summary>
          /// <param name="ep">网络节点.</param>
          public void SetIpAddressAndPort (IPEndPoint ep)
          {
              //只写网络节点
              serverIPEndPoint = ep;
          }
          /// <summary>
          /// 设置委托
          /// </summary>
          /// <param name="handler">消息委托.</param>
          public void SetDelegate (NetEventHandler handler)
          {
              //只写赋值回调
              ReceiveCallback = handler;
          }
          /// <summary>
          /// Initializes a new instance of the <see cref="NetUtility"/> class.
          /// </summary>
          private NetUtility ()
          {
              //服务器实例
              tcpServer = new TcpListener (IPAddress.Any, 23456);
              //客户端实例
              tcpClient = new TcpClient (AddressFamily.InterNetwork);
              //缓冲区初始化
              buffer = new byte[1024];
              //缓存数据组实例
              cache = new List<byte> ();
              //默认网络节点
              serverIPEndPoint = new IPEndPoint (IPAddress.Parse ("127.0.0.1"), 23456);
          }
    
  • 服务器部分

      #region Server Part:
      /// <summary>
      /// 开启服务器
      /// </summary>
      public void ServerStart ()
      {
          //开启服务器
          tcpServer.Start (10);
          //服务器开启提示
          ReceiveCallback ("Server Has Init!");
          //开始异步接受客户端的连接请求
          tcpServer.BeginAcceptTcpClient (AsyncAccept, null);
      }
      /// <summary>
      /// 异步连接回调
      /// </summary>
      /// <param name="ar">Ar.</param>
      void AsyncAccept (System.IAsyncResult ar)
      {
          //接受到客户端的异步连接请求
          tcpClient = tcpServer.EndAcceptTcpClient (ar);
          //有新的客户端连接提示
          ReceiveCallback ("Accept Client :" + tcpClient.Client.RemoteEndPoint.ToString ());
          //异步接收消息
          tcpClient.Client.BeginReceive (buffer, 0, 1024, SocketFlags.None, AsyncReceive, tcpClient.Client);
          //异步接受客户端请求尾递归
          tcpServer.BeginAcceptTcpClient (AsyncAccept, null);
      }
      /// <summary>
      /// 异步接收消息回调
      /// </summary>
      /// <param name="ar">Ar.</param>
      void AsyncReceive (System.IAsyncResult ar)
      {
          //获取消息套接字
          Socket workingClient = ar.AsyncState as Socket;
          //完成接收
          int msgLength = workingClient.EndReceive (ar);
          //如果接收到了数据
          if (msgLength > 0) {
              //消息接收提示
              ReceiveCallback ("ReceiveData : " + msgLength + "bytes");
              //临时缓冲区
              byte[] tempBuffer = new byte[msgLength];
              //拷贝数据到临时缓冲区
              Buffer.BlockCopy (buffer, 0, tempBuffer, 0, msgLength);
              //数据放到缓存数据组队尾
              cache.AddRange (tempBuffer); 
              //拆包解析
              byte[] result = LengthDecode (ref cache);
              //如果已经接收完全部数据
              if (result != null) {
                  //开始反序列化数据
                  NetModel resultModel = DeSerialize (result);
                  //TODO:Object Processing!
                  //数据对象结果提示
                  ReceiveCallback ("Object Result IP : " + resultModel.senderIp);
                  ReceiveCallback ("Object Result Content : " + resultModel.content);
                  ReceiveCallback ("Object Result Time : " + resultModel.time);
              }
              //消息未接收全,继续接收
              tcpClient.Client.BeginReceive (buffer, 0, 1024, SocketFlags.None, AsyncReceive, tcpClient.Client);
          }
    
      }
      #endregion  
    
  • 客户端部分

      #region Client Part
      /// <summary>
      /// 客户端连接
      /// </summary>
      public void ClientConnnect ()
      {
          //连接到服务器
          tcpClient.Connect (serverIPEndPoint);
          //连接到服务器提示
          ReceiveCallback ("Has Connect To Server : " + serverIPEndPoint.Address.ToString ());
      }
      /// <summary>
      /// 发送消息
      /// </summary>
      /// <param name="model">Model.</param>
      public void SendMsg (NetModel model)
      {
          //将数据对象序列化
          buffer = Serialize (model);
          //将序列化后的数据加字节头
          buffer = LengthEncode (buffer);
          //拆分数据,多次发送
          for (int i = 0; i < buffer.Length/1024 + 1; i++) {
              //满发送,1KB
              int needSendBytes = 1024;
              //最后一次发送,剩余字节
              if (i == buffer.Length / 1024) {
                  //计算剩余字节
                  needSendBytes = buffer.Length - i * 1024;
              }
              //发送本次数据
              tcpClient.GetStream ().Write (buffer, i * 1024, needSendBytes);
          }
      }
      #endregion
    
  • 公共方法

      #region Public Function
      /// <summary>
      /// 数据加字节头操作
      /// </summary>
      /// <returns>数据结果.</returns>
      /// <param name="data">源数据.</param>
      byte[] LengthEncode (byte[] data)
      {
          //内存流实例
          using (MemoryStream ms = new MemoryStream()) {
              //二进制流写操作实例
              using (BinaryWriter bw = new BinaryWriter(ms)) {
                  //先写入字节长度
                  bw.Write (data.Length);
                  //再写入所有数据
                  bw.Write (data);
                  //临时结果
                  byte[] result = new byte[ms.Length];
                  //将写好的流数据放入临时结果
                  Buffer.BlockCopy (ms.GetBuffer (), 0, result, 0, (int)ms.Length);
                  //返回临时结果
                  return result;
              }
          }
      }
      /// <summary>
      /// 数据解析,拆解字节头,获取数据.
      /// </summary>
      /// <returns>源数据.</returns>
      /// <param name="cache">缓存数据.</param>
      byte[] LengthDecode (ref List<byte> cache)
      {
          //如果字节数小于4,出现异常
          if (cache.Count < 4)
              return null;
          //内存流实例
          using (MemoryStream ms = new MemoryStream(cache.ToArray())) {
              //二进制流读操作实例
              using (BinaryReader br = new BinaryReader(ms)) {
                  //先读取数据长度,一个int值
                  int realMsgLength = br.ReadInt32 ();
                  //如果未接收全数据,下次继续接收
                  if (realMsgLength > ms.Length - ms.Position) {
                      return null;
                  }
                  //接收完,读取所有数据
                  byte[] result = br.ReadBytes (realMsgLength);
                  //清空缓存
                  cache.Clear ();
                  //返回结果
                  return result;
              }
          }
      }
      /// <summary>
      /// 序列化数据.
      /// </summary>
      /// <param name="mod">数据对象.</param>
      private byte[] Serialize (NetModel mod)
      {
          try {
              //内存流实例
              using (MemoryStream ms = new MemoryStream()) {
                  //ProtoBuf协议序列化数据对象
                  ProtoBuf.Serializer.Serialize<NetModel> (ms, mod);
                  //创建临时结果数组
                  byte[] result = new byte[ms.Length];
                  //调整游标位置为0
                  ms.Position = 0;
                  //开始读取,从0到尾
                  ms.Read (result, 0, result.Length);
                  //返回结果
                  return result;
              }
          } catch (Exception ex) {
              
              Debug.Log ("Error:" + ex.ToString ());
              return null;
          }
      }
      
      /// <summary>
      /// 反序列化数据.
      /// </summary>
      /// <returns>数据对象.</returns>
      /// <param name="data">源数据.</param>
      private NetModel DeSerialize (byte[] data)
      {
          try {
              //内存流实例
              using (MemoryStream ms = new MemoryStream(data)) {
                  //调整游标位置
                  ms.Position = 0;
                  //ProtoBuf协议反序列化数据
                  NetModel mod = ProtoBuf.Serializer.Deserialize<NetModel> (ms);
                  //返回数据对象
                  return mod;
                  
              }
          } catch (Exception ex) {
              Debug.Log ("Error: " + ex.ToString ());
              return null;
          }
      }
      #endregion
    
  • 服务器实现

      using UnityEngine;
      using System.Collections;
      using System.IO;
      using System;
    
      /// <summary>
      /// 服务器实现.
      /// </summary>
      public class ServerDemo : MonoBehaviour
      {
          //临时消息接收
          string currentMsg = "";
          Vector2 scrollViewPosition;
    
          void Start ()
          {
              scrollViewPosition = Vector2.zero;
              //消息委托
              NetUtility.Instance.SetDelegate ((string msg) => {
                  Debug.Log (msg);
                  currentMsg += msg + "\r\n";
              });
              //开启服务器
              NetUtility.Instance.ServerStart ();
          }
    
          void OnGUI ()
          {
              scrollViewPosition = GUILayout.BeginScrollView (scrollViewPosition, GUILayout.Width (300), GUILayout.Height (300));
              //消息展示
              GUILayout.Label (currentMsg);
              GUILayout.EndScrollView ();
          }
      }
    
  • 客户端实现

      using UnityEngine;
      using System.Collections;
      using System.Text;
      using System;
      /// <summary>
      /// 客户端实现
      /// </summary>
      public class ClientDemo : MonoBehaviour
      {
          //待解析地址
          public string wwwAddress = "";
    
          void Start ()
          {
              //消息处理
              NetUtility.Instance.SetDelegate ((string msg) => {
                  Debug.Log (msg + "\r\n");
              });
              //连接服务器
              NetUtility.Instance.ClientConnnect ();
              //开启协程
              StartCoroutine (ServerStart ());
          }
    
          IEnumerator ServerStart ()
          {
              //加载网页数据
              WWW www = new WWW (wwwAddress);
              yield return www;
              //编码获取内容
              string content = UTF8Encoding.UTF8.GetString (www.bytes);
              //内容测试
              Debug.Log (content);
              //待发送对象
              NetModel nm = new NetModel ();
              //消息体
              nm.senderIp = "127.0.0.1";
              nm.content = content;
              nm.time = DateTime.Now.ToString ();
              //发送数据对象
              NetUtility.Instance.SendMsg (nm);
          }
      }
    
  • 结果图


    结果图
  • Demo下载:链接: http://pan.baidu.com/s/1mhgUALy 密码: u36k

结束语

文中的ProtoBuf是跨平台的,可以跨平台传输对象。本文使用的是传输层的协议,有兴趣的同学可以使用网络层协议Socket实现,本文后续还会有网络游戏传输协议的内容,同学们随时关注。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,601评论 18 139
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,587评论 18 399
  • 一. Java基础部分.................................................
    wy_sure阅读 3,793评论 0 11
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,358评论 0 17
  • 终端执行brew install redis 来安装redis 安装完成后,执行 redis-server 启动r...
    hukx_michael阅读 1,355评论 0 2