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

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


建议带着疑问去学习:

  1. 什么是Socket?

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

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

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

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

并且需要了解以下知识:

  1. Socket

  2. 异步Socket

  3. 状态检测Poll

  4. 多路复用Select

服务端:

服务端的通信流程:

  1. 创建socket

  2. 绑定IP和端口

  3. 监听

  4. 接收

  5. 分发消息

  6. 发送

服务端的搭建流程:

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

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

服务端的代码实现:

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

cs 复制代码
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. 发送消息

  4. 接收消息

6.关闭连接

客户端的代码实现:

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

cs 复制代码
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协议来定义消息格式,以及添加重连、心跳检测、消息分发、消息队列等内容,搭建一个成熟的网络框架。

相关推荐
BAOYUCompany1 小时前
暴雨服务器更懂人工智能+
运维·服务器·人工智能
一只小bit1 小时前
Linux网络:阿里云轻量级应用服务器配置防火墙模板开放端口
linux·网络·阿里云
BachelorSC3 小时前
【网络工程师软考版】网络安全
网络·安全·web安全
蝶恋舞者4 小时前
怎样让阿里云服务器(centos)有界面
服务器·阿里云·centos
(Charon)4 小时前
【C语言网络编程】HTTP 客户端请求(基于 Socket 的完整实现)
网络·网络协议·http
Bryce李小白5 小时前
Kotlin实现Retrofit风格的网络请求封装
网络·kotlin·retrofit
Lovyk6 小时前
Linux网络管理
服务器·网络·php
无敌的牛7 小时前
Linux重定向的理解
linux·运维·服务器
MC皮蛋侠客7 小时前
AsyncIOScheduler 使用指南:高效异步任务调度解决方案
网络·python·fastapi
许野平7 小时前
Rust:anyhow::Result 与其他 Result 类型转换
服务器·开发语言·rust·result·anyhow