Socket网络类移动小游戏

<p>
  实现了不同电脑的联机小游戏,游戏功能比较简单,可以看到不同客户端的物体移动,初步了解了协议的封装和解封装,熟悉了网络服务器和客户端收发消息的过程。
<pre>
整理个笔记方便以后回顾。
</pre>
</p>

  • 一、游戏组成

1.服务器

因为是网络游戏,所以肯定是需要服务器啦,主要负责各个客户端的消息交互,通俗说就是把某个客户端发送的消息转发给其他的客户端。代码主要包括以下几个部分:
(1)Connect类
  连接类,这个类是Socket的封装,包含了连接时需要的一些字段属性和方法,字段属性包括:socket,接受消息使用的数组,数组的长度,发送消息使用的数组,读取消息的字符串,标志位;
方法包括:初始化socket的方法Init,获取连接客户端IP端口的GetIP方法,以及关闭连接的Close方法;

(2)Server类
  服务器类,包含了,初始化连接池,服务器开启,连接客户端,接受以及发送客户端消息的整个过程。

2.客户端

使用的是unity作为客户端,通过服务器传递球体(prefab)运动的消息给其他客户端,包含了:自定义运动协议封装和解封装的消息处理,socket连接服务器,还有球体的运动操作等。

二、实现步骤

1.服务器的实现

分别实现服务器的几个类,然后开启服务即可:

  • (1)connect类:
//首先定义属性
public Socket workSocket    //和客户端连接成功后使用的socket
public byte[] readByte;    //接受客户端消息的字节数组
public int  BYTE_NUMBER = 1024;    //数组的长度
public byte[] sendByte;    //发送消息的字节数组
public string readStr;    //读取消息使用的字符串
public bool isUsed;      //标示该connect是否使用

//分别实现的方法
//构造方法,实例化的时候先初始化接收数组和标志位,false表示未使用
       public Connect()
        {
            readByte = new byte[BYTE_NUMBER];
            isUsed = false;
        }
//初始化方法当有客户端连接到改socket后,初始化以及标志位改为true
        public void Init(Socket socket)
        {
            workSocket = socket;
            isUsed = true;
        }
//返回远程客户端的IP和端口
        public string GetIP()
        {
            if (!isUsed) return "未连接成功";
            return workSocket.RemoteEndPoint.ToString();
        }
//当客户端传送数据完毕时,断开连接
         public void Close()
        {
            if (!isUsed) return;
            Console.WriteLine("和客户端“{0}”断开连接。", GetIP());
            workSocket.Shutdown(SocketShutdown.Both);
            workSocket.Close();
            isUsed = false;
        }

  • (2)Server类
      该类中包含了字段有:监听用的socket,connect连接池,最大连接的数量。
//声明监听的socket
Socket listenSocket;
//最大的连接数量
int maxCount = 50;
//声明连接池
Connect[] conns;

完成字段声明后,需要返回连接ID的方法,然后开始进行建立socket连接,接收数据,发送数据的流程步骤:初始化socket--->绑定服务器ip端口--->设置最大的连接数--->开启异步接收连接--->在回调函数中判断连接是否成功,成功后开启异步接收--->异步接收同样使用回调函数来控制接收和发送数据。(注意其中的参数意义)

//返回一个可用的连接池ID
public int GetIndex()
{
//判断连接池有没有初始化,如果没有则直接return -1
    if (conns == null) return -1;
//开始遍历连接池,寻找一个可以使用的连接(isUsed=false)
    for (int i = 0; i < conns.Length; i++)
     {
        if(conns[i]==null)
        {
            conns[i] = new Connect();
            return i;
        }
        else if(conns[i]!=null&&!conns[i].isUsed)
        {
            return i;
        }
    }
//如果没有可用的连接,则返回-1
    return -1;          
}


//服务器开启方法
public void Start(IPEndPoint ipPoint)
{
//开启之前首先实例化50个连接,为以后提供使用
    conns = new Connect[maxCount];
    for(int i=0;i<conns.Length;i++)
    {
        conns[i] = new Connect();
    }
//实例化监听sokcet
listenSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//绑定服务器端口IP
listenSocket.Bind(ipPoint);
//设置服务器最大连接数
listenSocket.Listen(maxCount);
//开启异步等待连接
 listenSocket.BeginAccept(AcceptCb, null);
Console.WriteLine("服务器开启,等待连接:");
}

//异步等待连接的回调函数,当有连接时执行
void AcceptCb(IAsyncResult ar)
{
    try
    {
//有连接接入时,声明新的socket来接收连接
        Socket socket = listenSocket.EndAccept(ar);
//获取一个可用的连接ID
        int index = GetIndex();
//判断是否有可用的ID
        if (index < 0)
        {
            Console.WriteLine("连接已满,请稍后重试!");
            return;
        }
//获得连接ID后,初始化连接并打印连接成功的消息
    conns[index].Init(socket);
    Console.WriteLine("连接成功,客户端地址为:{0}", conns[index].GetIP());

//连接成功后,开始异步接收
    conns[index].workSocket.BeginReceive(conns[index].readByte, 0
    , conns[index].BYTE_NUMBER, SocketFlags.None, ReceiveCB, conns[index]);


//一个连接完成后,开始进行递归等待新连接
    listenSocket.BeginAccept(AcceptCb, null);
    }
    catch(Exception e)
    {
        throw e;
    }
}

//异步接收的回调函数,接收完毕后执行
void ReceiveCB(IAsyncResult ar)
{
//声明一个connect连接来接收异步接收传递过来的连接(通过最后一个参数)
    Connect conn = ar.AsyncState as Connect;
//判断接收的数量
    int count = conn.workSocket.EndReceive(ar);
//如果为0,则表示接收完毕,可以断开连接
    if(count<=0)
    {
    Console.WriteLine("从“{0}”接收完毕,断开连接!", conn.GetIP());
//断开连接后,广播发送消息给其他客户端,告知该客户端断开连接,做相应的处理操作
    string leave = "LEAVE" + " " + conn.GetIP();
    conn.sendByte = System.Text.Encoding.UTF8.GetBytes(leave);
    for (int i = 0; i < conns.Length; i++)
    {
        if (conns[i].isUsed)
        {
            conns[i].workSocket.Send(conn.sendByte);
        }
    }
    conn.Close();
    return;
}
//讲接收到的字节消息转换成成字符串
conn.readStr = System.Text.Encoding.UTF8.GetString(conn.readByte, 0, count);
//在服务器打印收到的消息
Console.WriteLine("服务器收到从“{0}”的消息:{1}", conn.GetIP(), conn.readStr);

//收到消息后广播给其他客户端
conn.sendByte = System.Text.Encoding.UTF8.GetBytes(conn.readStr);
for(int i=0;i<conns.Length;i++)
{
if (conns[i].isUsed)
{
conns[i].workSocket.Send(conn.sendByte);
}
}

//接收消息完毕后,递归接收消息
conn.workSocket.BeginReceive(conn.readByte, 0, conn.BYTE_NUMBER, SocketFlags.None, ReceiveCB, conn);
 }
 }

(3)完成后,在主函数当中声明服务器类,通过Star方法开启服务器,等待客户端连接。

2.客户端的实现

(1)移动的物体
 使用unity中的3d球体加上文本组成的prefab,比较简单:

1

(2)然后在unity上创建一个空对象,用来挂控制脚本:

2

(3)码控制脚本

  • 属性:
//用来连接服务器的socket相关属性
    Socket socket;
    int BUFF_NUMBER;
    byte[] readByte;
    byte[] sendByte;
    string readStr;
//玩家列表字典,存放玩家的信息
    Dictionary<string, GameObject> players;
//消息list,用来存放服务器发送的消息
    List<string> msgList;
//玩家的预设,本次使用的是一个3d球体
    public GameObject prefab;
//本机玩家
    GameObject player;
//本机玩家的id字段
    string m_id;
  • 方法
    先实现所有的方法,完成后再在Awake,Start,Update中调用相应的方法。
//连接方法,和服务器的连接类似
    void ConnetServer()
    {
        socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

        socket.Connect(IPAddress.Parse("127.0.0.1"), 3344);
        m_id = socket.LocalEndPoint.ToString();

        socket.BeginReceive(readByte, 0, BUFF_NUMBER, SocketFlags.None, ReceiveCB, null);

    }
//异步接收的回调函数
    void ReceiveCB(IAsyncResult ar)
    {
        try
        {
            int count = socket.EndReceive(ar);
            if(count<=0)
            {
                return;
            }
            readStr = System.Text.Encoding.UTF8.GetString(readByte, 0, count);
//转化成字符串消息后,加入消息列表,统一处理
            msgList.Add(readStr);
//递归的调用异步接收
            socket.BeginReceive(readByte, 0, BUFF_NUMBER, SocketFlags.None, ReceiveCB, null);
        }
        catch (Exception e)
        {
            throw e;
        }
    }


//封装位置消息,发送给服务器,在每次移动的时候调用
    void SendPos()
    {
//通过id找到本机的玩家
        player = players[m_id];
        Vector3 pos = player.transform.position;
//封装玩家的位置信息,格式为:POS+ID+X+Y+Z,通过空格分割。
        string msgPos = "POS"+" "+ m_id + " " + pos.x+" "+ pos.y + " "+ pos.z ;
//转化成字节数组发送
        sendByte = System.Text.Encoding.UTF8.GetBytes(msgPos);
        socket.Send(sendByte);
    }


//发送离开消息,在游戏推出时发送(本次没有使用,采用的方式是在服务器上处理。)
   void SendLeave()
    {
        player = players[m_id];
        Vector3 pos = player.transform.position;
//封装的格式为:Leave+id
        string msgLeave = "Leave" + " " + m_id;
        sendByte = System.Text.Encoding.UTF8.GetBytes(msgLeave);
        socket.Send(sendByte);
    }

//处理玩家离开消息
    void HandleLeave(string id)
    {
        if(players.ContainsKey(id)&&id!=m_id)
        {
            GameObject.Destroy(players[id]);
            players[id] = null;
        }        
    }

//消息处理方法,处理放在消息list中的消息,分割后,通过字符串数组的第一个来确定消息类型并处理
    void HandleMsg()
    {
//如果消息列表为空,则不处理
        if (msgList.Count == 0) return;
//通过空格分割消息,存放到数组中
        string[] arg = msgList[0].Split(' ');
//处理一条消息后,从list中删除这条消息
        msgList.RemoveAt(0);
//位置消息的处理
        if(arg[0]=="POS")
        {
            HandlePos(arg[1], arg[2], arg[3], arg[4]);
        }
//离开消息的处理
        else if(arg[0]=="LEAVE")
        {
            HandleLeave(arg[1]);
        }
    }

//处理位置消息,负责处理其他玩家的位置移动
    void HandlePos(string id,string x,string y,string z)
    {
//如果消息的id是本机的话,则不处理
        if (id == m_id) return;
//存放位置信息
        Vector3 pos = new Vector3(float.Parse(x), float.Parse(y), float.Parse(z));
//做判断,如果该玩家已经存在,则移动该玩家的位置,如果不存在,则生成一个新的玩家。
        if(players.ContainsKey(id))
        {
            players[id].transform.position = pos;
        }
        else
        {
            AddPlayer(id, pos);
        }
    }

//生成玩家方法
    void AddPlayer(string id,Vector3 pos)
    {
        GameObject newPlayer = Instantiate(prefab, pos, Quaternion.identity);
        TextMesh mesh = newPlayer.GetComponentInChildren<TextMesh>();
        mesh.text = id;
        players.Add(id, newPlayer);
    }

//本机玩家移动的方法
    void Move()
    {
//通过id确定本机的玩家
        player = players[m_id];
//玩家的移动速度
        float spped = 0.1f;
//通过左右上下控制移动,每次移动都会通过SendPos方法发送位置消息
        if (Input.GetKey(KeyCode.LeftArrow))
        {
            player.transform.position += new Vector3(-spped,0,0);
            SendPos();
        }
        else if(Input.GetKey(KeyCode.RightArrow))
        {
            player.transform.position += new Vector3(spped, 0, 0);
            SendPos();
        }
        else if (Input.GetKey(KeyCode.UpArrow))
        {
            player.transform.position += new Vector3(0, spped, 0);
            SendPos();
        }
        else if (Input.GetKey(KeyCode.DownArrow))
        {
            player.transform.position += new Vector3(0, -spped, 0);
            SendPos();
        }
    }

//首先初始化字段
    private void Awake()
    {
        players = new Dictionary<string, GameObject>();
        msgList = new List<string>();
        BUFF_NUMBER = 1024;
        readByte = new byte[1024];
        sendByte = new byte[1024];
    }
//初始化字段后,开始建立连接,并生成本机玩家
    void Start () {
        ConnetServer();
        AddPlayer(m_id, new Vector3(0, 0, 0));  
    }
    
//在update中一直处理从服务器发送的消息以及控制自身移动
    void FixedUpdate () {
        HandleMsg();
        Move(); 
    }
}

三、总结

写到这游戏就算差不多完成啦,可以体验一下啦,首先先开启服务器,然后在开启客户端(客户端可以通过unity build生成客户端,同时运行几个即可),另外,注意几点:

  • (1)在退出方面,本次由于unity中UI没有建立一个退出按钮,所以没法由客户端主动发送退出消息,所以改成在服务器发送,后续可以改进。

  • (2)服务器端,使用连接来封装socket,最后组成连接池,这样的好处在于可以在服务器开启前就先建立好连接池,不用等到有接入在生成连接,提高了效率。

  • (3)在码代码中出现的几个问题:
    a.服务器在获取可用连接id时,注意判断返回id时的条件,一定要写明清楚,不要使用else,会造成得到的连接占用。
    b.客户端异步接收和发送的时候,需要使用两个不同的字节数组来处理,不然会有冲突,导致只会接收一次消息,不能递归。
    c.服务器在处理异步接收的时候,如果使用connect中封装的count来接收endReceive的话,会造成客户端收到的消息有误,这个没找到原因。


    3

最后截几个游戏图:

连接成功
移动
其中一个断开连接

(tips,一开始其他客户端没有移动的时候是不会新建球体的。)

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

推荐阅读更多精彩内容