基于Socket通信的小实例(二)

今天实例是在之前的基础上添加了新的功能,所以不止简单的字符串通信,可以说得上是我们联机游戏中角色的信息同步功能的缩影(只是当前例子只有简单实现而已)

实现效果

先说说咱的实现效果,其实也就是两个客户端联机,在场景中出现两个胶囊体,不同客户端操作的不同胶囊体,实现两胶囊体的位置信息同步而已,所以也就不贴图了。

效果不重要,毕竟才基础,里面的原理才是我们需要了解的~

实现原理

简单描述下原理: 每当一个客户端连接服务器的时候会获得当前客户端接入的顺序的序列号,这里可以是1、2、3... ,我这里默认只连接两个客户端就可以了,客户端一旦接入,便在场景中实例化两个胶囊体对象,且在客户端第一次连接服务器的时候就根据服务器返回的序列号进行设定当前客户端控制的胶囊体对象,这样在对胶囊体控制行为的时候就有了判断,一旦满足两个客户端接入,那么胶囊体分别能被两个客户端进行准确控制,程序一旦运行,客户端则以一定的频率将自身胶囊体位置坐标发送到服务器,服务器接收后转发当前客户端位置信息给其他接入的客户端,而其他客户端则会对信息进行解析及同步胶囊体信息,以此达到两者的信息同步~

服务端搭建

服务端跟之前的差不太多,只有个别地方的改动

Program.cs

 using System;
    class Program
        {
            static void Main(string[] args)
            {
                Server server = new Server("127.0.0.1",8899);
                server.Start();
    
                Console.ReadKey();
            }
        }

Server.cs

 using System;
    using System.Collections.Generic;
    using System.Net;
    using System.Net.Sockets;
    class Server
        {
            private Socket serverSocket;
            private Socket clientSocket;
            private IPAddress ipAddress;
            private IPEndPoint ipEndPoint;
            private List<Client> clientList = new List<Client>();
    
            public Server(string ip,int port)
            {
                //创建Socket
                serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                //声明ip地址和端口号
                this.ipAddress = IPAddress.Parse(ip);
                this.ipEndPoint = new IPEndPoint(ipAddress, port);
            }
    
            public void Start()
            {
                //Socket绑定IP和端口号
                serverSocket.Bind(ipEndPoint);
                //Socket开始监听
                serverSocket.Listen(0);
    
                Console.WriteLine("服务器已经启动");

                //客户端接入
                serverSocket.BeginAccept(AcceptCallback,null);
            }
    
            private  void AcceptCallback(IAsyncResult ar)
            {
                clientSocket = serverSocket.EndAccept(ar);
                //获取客户端名称
                string clientName = clientSocket.RemoteEndPoint.ToString();
                Client client = new Client(clientSocket, clientName);
                client.Start();
                Console.WriteLine(clientName + "已经登录。。。");
    
                //将连接上的客户端记录在案
                clientList.Add(client);
    
                //并且每连上一个客户端便给予当前客户端一个标记
                //这里的flag是给先后连上服务器的客户端进行的编号
                //输出给客户端是为了让客户端知道顺序,从而进行当前客户端来选择控制对象
                client.SendMessage(""+flag);
                flag++; 

                //继续循环监听客户端的连接
                serverSocket.BeginAccept(AcceptCallback, null);
            }
        }

Server有较小的改动就是在服务器接收客户端连接的时候会分配一个代表着客户端接入的序列号给客户端。

Client.cs

 using System;
    using System.Text;
    using System.Net.Sockets;
    class Client
        {
            private Socket clientSocket;
            private string clientName;
            private byte[] recieveData;
    
            public Client(Socket client,string clientName)
            {
                this.clientSocket = client;
                this.clientName = clientName;
                recieveData = new byte[clientSocket.ReceiveBufferSize];
            }
    
            public void Start()
            {
                clientSocket.BeginReceive(recieveData,0, clientSocket.ReceiveBufferSize,SocketFlags.None, RecieveCallback, null);
            }
    
            //接收客户端的消息
            private void RecieveCallback(IAsyncResult ar)
            {
                try
                {
                    //接收到的数据长度
                    int count = clientSocket.EndReceive(ar);
    
                    //防止客户端异常退出
                    if (count == 0)
                    {
                        clientSocket.Close();
                        return;
                    }
    
                    //对接收到的数据进行处理
                    string msgRec = Encoding.UTF8.GetString(recieveData, 0, count);
                    HandleResponse(msgRec);
    
                    //输出到控制台
                    Console.WriteLine(msgRec);
    
                    //循环接收客户端发送过来的数据
                    clientSocket.BeginReceive(recieveData, 0, clientSocket.ReceiveBufferSize, SocketFlags.None, RecieveCallback, null);
                }
                catch (Exception)
                {
                    if (clientSocket != null)
                    {
                        clientSocket.Close();
                        return;
                    }
                }
                
            }
    
            //对客户端返回过来的数据进行处理
            private void HandleResponse(string data)
            {
                 //进行数据解析的判断,如果不包含flag标记的数据不执行之后代码
                if (data.EndsWith("*")) return;
    
                //只有一个客户端的时候不同步信息
                clientList = server.clientList;
                if (clientList.Count < 2) return;
               
                //将当前客户端传送过来的同步信息转发给其他客户端进行同步
                foreach (Client item in clientList)
                {
                    if (item != this)
                    {
                        Console.WriteLine("转发消息 "+data);
                        item.SendMessage(data);
                    }
                }
            }
    
            //发送消息给客户端
            private void SendMessage(string data)
            {
                byte[] msgData = Encoding.UTF8.GetBytes(data);
                clientSocket.Send(msgData);
            }
        }

Client类中的HandleResponse函数对客户端传输过来的位置信息进行解析且转发到其他客户端

客户端创建

ClientManager.cs

 using UnityEngine;
    using System.Text;
    using System.Net.Sockets;
    using System.Net;
    using System;
    using UnityEngine.UI;
    public class ClientManager : MonoBehaviour{

        [SerializeField]
        private GameObject playerPrefab;
        [SerializeField]
        private GameObject otherPlayerPrefab;
        [HideInInspector]
        public GameObject currentPlayer;
        private Socket clientSocket;
        private byte[] recieveData;
        private PlayerMove playerMove;
        private int recieveNum;
        private string recieveStr = "";
        private bool isRecieve = false;
    
        void Start()
        {
            // 首先声明一个Socket
            clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            //申明IP和端口号
            IPAddress iPAddress = IPAddress.Parse("10.16.28.122");
            IPEndPoint iPEndPoint = new IPEndPoint(iPAddress, 8899);
            //连接服务器端
            clientSocket.Connect(iPEndPoint);
    
            //接收服务端发送过来的消息
            recieveData = new byte[clientSocket.ReceiveBufferSize];
            clientSocket.BeginReceive(recieveData, 0, clientSocket.ReceiveBufferSize, SocketFlags.None, RevieveCallback, null);
        }
    
        private void RevieveCallback(IAsyncResult ar)
        {
            int count = clientSocket.EndReceive(ar);
            string msgRec = Encoding.UTF8.GetString(recieveData, 0, clientSocket.ReceiveBufferSize);
    
            //输出信息
            isRecieve = true;
            recieveStr = msgRec;
    
            recieveData = new byte[clientSocket.ReceiveBufferSize];
            clientSocket.BeginReceive(recieveData, 0, clientSocket.ReceiveBufferSize, SocketFlags.None, RevieveCallback, null);
        }
    
        void Update()
        {
            //由于同步信息是回调回来的,所以要从主线程调用的话就采取这样的形式
            if (isRecieve)
            {
                isRecieve = false;
                RecieveMsg(recieveStr);
            }
        }
    
        public void RecieveMsg(string data)
        {
            //当连接服务器第一次的时候会返回当前客户端的flag标记
            if(recieveNum < 1)
            {
                recieveNum++;
    
                playerPrefab = GameObject.Instantiate<GameObject>(playerPrefab);
                otherPlayerPrefab = GameObject.Instantiate<GameObject>(otherPlayerPrefab);
    
                //对flag进行判断并进行当前客户端控制对象的选取
                if (int.Parse(data) == 1)
                {
                    playerMove = playerPrefab.GetComponent<PlayerMove>();
                    Destroy(otherPlayerPrefab.GetComponent<PlayerMove>());
                    playerMove.flag = "1";
                    currentPlayer = playerPrefab;
                }
                else
                {
                    playerMove = otherPlayerPrefab.GetComponent<PlayerMove>();
                    Destroy(playerPrefab.GetComponent<PlayerMove>());
                    playerMove.flag = "2";
                    currentPlayer = otherPlayerPrefab;
                }
            }
            else
            {
                //第一次之后访问服务器则进行数据的接收同步
                playerMove.RecieveData(data);
            }
            
        }
    
        public void SendMsg(string data)
        {
            byte[] sendMsg = Encoding.UTF8.GetBytes(data);
            clientSocket.Send(sendMsg);
        }
}


PlayerMove.cs

 using UnityEngine;

    public class PlayerMove : MonoBehaviour {
    
        public string flag;
        private string tempFlag = "";
        private ClientManager clientManager;
        private Rigidbody myRigibody;
        private Vector3 playerInput;
        [SerializeField]
        private float movementSpeed = 5.0f; 
        [SerializeField]
        private float turnSpeed = 1000f;
        //同步频率
        private int syncRate = 20;
        private bool isRemotePlayer = false;
        private Vector3 pos;
        private Vector3 rotation;
        private Transform currentPlayerTransform;
    
        void Start () {
            
            myRigibody = GetComponent<Rigidbody>();
    
            clientManager = GameObject.Find("ClientManager").GetComponent<ClientManager>();
    
            currentPlayerTransform = clientManager.currentPlayer.transform;
    
            //持续间隔对数据的同步
            InvokeRepeating("SyncPlayerTransform", 3f, 1f / syncRate);
        }
        
    
        void Update () {
    
            //判断当前客户端控制的对象来同步其他客户端的对象信息
            if (clientManager.currentPlayer.name.Equals("OtherPlayer(Clone)"))
            {
                SyncRemoteTransform(GameObject.Find("Player(Clone)").transform);
            }
            else
            {
                SyncRemoteTransform(GameObject.Find("OtherPlayer(Clone)").transform);
            }
    
        }
    
        //同步自身控制对象的位置信息给服务器
        void SyncPlayerTransform()
        {
            string data = string.Format("{0},{1},{2}*{3}", currentPlayerTransform.localPosition.x,
                currentPlayerTransform.localPosition.y, currentPlayerTransform.localPosition.z,flag);
    
            clientManager.SendMsg(data);
        }
    
        //同步其他客户端的控制对象位置信息
        void SyncRemoteTransform(Transform remotePlayerTransform)
        {
            remotePlayerTransform.position = pos;
            //remotePlayerTransform.eulerAngles = rotation;
        }
    
        public void RecieveData(string data)
        {
         
            if (!string.IsNullOrEmpty(data)&& !data.Equals("")){
                //获取到其他控制器的数据信息并进行解析
                string[] strs = data.Split('*');
                pos = Parse(strs[0]);
                //rotation = Parse(strs[1]);
                tempFlag = strs[1];
            }
            
        }
    
        void FixedUpdate()
        {
            //如果当前控制对象不属于此客户端则不控制
            if (clientManager.currentPlayer != this.gameObject)
            {
                return;
            }
                
            //设置 坐标的值
            playerInput.Set(Input.GetAxis("Horizontal"), 0f, Input.GetAxis("Vertical"));
    
            //当用户停止输入 让当前的物体不做任何的操作且待在原地
            if (playerInput == Vector3.zero)
                return;
    
            //创建一个可以朝向运动的方向
            Quaternion newRotation = Quaternion.LookRotation(playerInput);
    
            
            //如果当前物体并没有朝向运动方向则将当前物体转向运动方向
            if (myRigibody.rotation != newRotation)
                myRigibody.rotation = Quaternion.RotateTowards(myRigibody.rotation, newRotation, turnSpeed * Time.deltaTime);
    
            //通过我们的输入得到单位的方向向量,再乘以移动速度以及时间得到需要移动的距离Vector3(此时是以原点为基准的)
            //再和此前物体的坐标相加可以得到当前物体需要移动到的新坐标
            Vector3 newPosition = transform.localPosition + playerInput.normalized * movementSpeed * Time.deltaTime;
    
            //将我们的物体移动到最新的坐标
            myRigibody.MovePosition(newPosition);
        }
    
        //对字符串数据进行解析
         Vector3 Parse(string data)
        {
            string[] strs = data.Split(',');
            float x = float.Parse(strs[0]);
            float y = float.Parse(strs[1]);
            float z = float.Parse(strs[2]);
    
            return new Vector3(x,y,z);
        }
    }


在ClientManager中进行Socket的初始化,负责对服务器的连接,数据的发送和数据的接收以及数据的解析功能,而PlayerMove脚本是挂载到场景中的胶囊体上的,主要是控制胶囊体的运动,当然还附加胶囊体数据的同步功能(功能分配有点乱,当前实例以实现功能为主,并不care如何设计类会更加灵活233333)

源码地址

源码工程已上传Github了,在这里

总结

初步了解了下联机游戏中数据同步的概念以及简单的实现流程,加深Socket的基础认识。

就这么多,白了个白~

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,638评论 18 139
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,934评论 6 13
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,870评论 25 707
  • 1.OkHttp源码解析(一):OKHttp初阶2 OkHttp源码解析(二):OkHttp连接的"前戏"——HT...
    隔壁老李头阅读 20,826评论 24 176
  • 最近有点乱,没方向,没定位。走的很累,想的多,做的少。改变从现在开始,每天看看书,学习。多写写文字,写着写着就会有...
    lyy2026阅读 171评论 0 0