Unity搭建简易网络服务端与客户端--基础篇

前言:本文将会以制作一个简易双端网络框架的目标,带领读者熟悉游戏开发中Socket网络编程的概念和流程,知道是怎样从零去构建一个双端的网络。


建议带着疑问去学习:

1. 什么是Socket?

2. 为什么需要用到Socket去实现网络编程?

3. Socket的实现原理是什么?

4. Socket通信的流程是什么?

5. TCP和UDP协议有什么区别?能否实现可靠的UDP协议?

并且需要了解以下知识:

0. Socket

1. 异步Socket

2. 状态检测Poll

3. 多路复用Select

服务端:

服务端的通信流程:

1. 创建socket

2. 绑定IP和端口

3. 监听

4. 接收

5. 分发消息

6. 发送

服务端的搭建流程:

NetServer: 负责创建socket和监听接收

NetSession:负责维护客户端的连接

服务端的代码实现:

服务端采用的是Select多路复用阻塞连接和接收


using System.Net;
using System.Net.Sockets;

namespace Server
{

    public class NetSession
    {
        public Socket socket;//socket对象   
        public byte[] readBuffer;//用于接收的字节数组
    }

    public class NetServer
    {

        static Socket socket;  //用于监听的全局socket

        //用字典来维护每一个Socket连接
        static Dictionary<Socket, NetSession> clients = new Dictionary<Socket, NetSession>();
        public void Init()
        {
            //创建socket对象(参数分别是地址族:Ipv4,socket类型:字节流socket,通信协议:udp
            socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Udp);

            //绑定服务器本地IP和端口
            IPAddress ipAdr = IPAddress.Parse("127.0.0.1");
            IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888);
            socket.Bind(ipEp);

            //监听,参数是最多可容纳等待接受的连接数,0表示不限制
            socket.Listen(0);
            Console.WriteLine("服务器启动成功!");
            //可读的Socket列表,用于多路复用状态检测
            List<Socket> readSockets = new List<Socket>();
            while (true)
            {
                #region 多路复用select
                //待监听的socket列表
                readSockets.Clear();
                readSockets.Add(socket);
                foreach (var kv in clients)
                {
                    readSockets.Add(kv.Value.socket);
                }
                /* 
                参数
                checkRead    检测是否有可读的Socket列表
                checkWrite   检测是否有可写的Socket列表  
                checkError   检测是否有出错的Socket列表
                m1croScconds 等待回应的时间,以微秒为单位,如果该参数为-1表示一直等待,如果为0表示非阻塞
                
                Select可以确定一个或多个Socket对象的状态,一个或多个套接字放入ILsit中。
                通过调用Select(将IList作为checkRead参数),可检查Socket是否具有可读性。
                若要检查套接字是否具有可写性,可使用checkWrite参数。若要检测错误条件,可使用checkError。 
                在调用Select之后,Select将修改ILsit列表 , 仅保留那些满足条件的套接字。
                把包含6个Socket的列表传给Select, Select方法将会阻塞 , 
                等到超时或某个(或多个)Socket可读时返同 ,并且修改checkRead列表 ,
                仅保存可诙的socket A和socket C。 当没有任何可读Socket时,程序将会阻塞 ,不占用CPU资源
                 */
                Socket.Select(readSockets, null, null, 1000);

                foreach (var kv in readSockets)
                {
                    if (kv == socket)
                    {
                        this.ReadListenfd(kv);
                    }
                    else
                    {
                        this.ReadClientfd(kv);
                    }
                }
                #endregion
                
            }
            
        }

        //当监听到客户端连接时的处理
        void ReadListenfd(Socket listen)
        {
            /如果监听到客户端的连接,则维护该连接
            Socket temp = listen.Accept();
            NetSession state = new NetSession
            {
                socket = temp
            };
            clients.Add(temp, state);
        }

        //当接收到客户端消息时的处理
        bool ReadClientfd(Socket client)
        {
            //判断字典中是否还包含该连接
            if (!clients.ContainsKey(client))
            {
                return false;
            }            
            NetSession state = clients[client];

            //Reveive
            int count = 0;
            try
            {
                //接收
                count = client.Receive(state.readBuffer);
            }
            catch (SocketException ex)
            {
                //如果发生异常则关闭并移除该连接
                client.Close();
                clients.Remove(client);
                Console.WriteLine("接收失败:" + ex.Message);
                return false;
            }
            //如果count为0,则说明客户端关闭了连接
            if (count == 0)
            {
                client.Close();
                clients.Remove(client);
                Console.WriteLine("接收失败,count为0");
                return false;
            }

            //将字节数组转换为字符串
            string readStr = System.Text.Encoding.Default.GetString(state.readBuffer, 0, count);
            Console.WriteLine("服务区接收:" + readStr);
            this.sendMessage("服务器接收成功,返回给客户端:" + readStr);
            return true;
        }
        
        //发送消息
        void sendMessage(string message)
        {
            byte[] sendMsg = System.Text.Encoding.Default.GetBytes(message);
            foreach (var kv in clients.Values)
            {
                kv.socket.Send(sendMsg);
            }
        }
        
        
    }
}

客户端

客户端的通信流程:

1. 创建Socket对象

2. 连接服务器

3. 发送消息

5. 接收消息

6.关闭连接

客户端的代码实现:

客户端采用的是异步连接和接收

using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using UnityEngine;
using UnityEngine.UI;

public class Echo : MonoBehaviour
{
    public Button connectButton;//连接按钮
    public Button sendButton;//发送按钮

    public InputField input;//输入的消息
    
    private byte[] readBuffer = new byte[1024];//用于接收的字节数组
    private Socket socket;//客户端Socket
    public void Connect(){
        //创建socket
        socket = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Udp);
        //开始连接
        socket.BeginConnect("127.0.0.1",8000,ConnectCallBack,socket);
    }

    public void ConnectCallBack(IAsyncResult result){
        try{
            Socket con = result.AsyncState as Socket;
            con.EndConnect(result);//链接成功的回调方法
            con.BeginReceive(readBuffer,0,1024,0,ReceiveCallBack,con);//开始接收
            Debug.Log("Connect Success!");
            
        }
        catch(SocketException e){
            Debug.Log("Connect Error:"+e.Message);
        }
    }

    private void ReceiveCallBack(IAsyncResult ar)
    {
        try{
            Socket rec = ar.AsyncState as Socket;
            int count = rec.EndReceive(ar);
            string mes = System.Text.Encoding.Default.GetString(readBuffer,0,count);
            Debug.Log("接收服务器数据成功:"+mes);
            //接收完这窜数据后准备接收下一串
            rec.BeginReceive(readBuffer,0,1024,0,ReceiveCallBack,rec);
            
        }
        catch(SocketException ex){
            Debug.Log("Connect Error:"+ex.Message);
        }
    }

    void Send(string mes){
        string sendStr = input.text;
        byte[] bytes = System.Text.Encoding.Default.GetBytes(sendStr);
        socket.BeginSend(bytes,0,bytes.Length,0,SendCallBack,socket);
    }
    
    void SendCallBack(IAsyncResult ar){
        try{
            Socket sen = ar.AsyncState as Socket;
            int count = sen.EndSend(ar);
            Debug.Log("发送成功!count = "+count);
        }
        catch(SocketException ex){
            Debug.Log("发送失败! "+ex.Message);
        }
        
    }
}

需要说明的是,上述代码仅仅是为了熟悉基本的客户端和服务端网络通信,是一个非常简单的网络框架,建议动手亲自实战,后续将会使用protobuf协议来定义消息格式,以及添加重连、心跳检测、消息分发、消息队列等内容,搭建一个成熟的网络框架。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值