Socket网络类移动小游戏


  实现了不同电脑的联机小游戏,游戏功能比较简单,可以看到不同客户端的物体移动,初步了解了协议的封装和解封装,熟悉了网络服务器和客户端收发消息的过程。


整理个笔记方便以后回顾。

  • 一、游戏组成

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

//异步等待连接的回调函数,当有连接时执行
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 players;
//消息list,用来存放服务器发送的消息
    List 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();
        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();
        msgList = new List();
        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](http://upload-images.jianshu.io/upload_images/4752471-d5a683c83405c230.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)


----
###最后截几个游戏图:

![连接成功](http://upload-images.jianshu.io/upload_images/4752471-4fa2c9627802960e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)


![移动](http://upload-images.jianshu.io/upload_images/4752471-9e0315812cae7478.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)



![其中一个断开连接](http://upload-images.jianshu.io/upload_images/4752471-6eb74892ebfe86ab.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

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

你可能感兴趣的:(Socket网络类移动小游戏)