《Unity3D网络游戏实战》学习与实践

news2025/2/28 17:36:33

纸上得来终觉浅,绝知此事要躬行~

Echo

网络上的两个程序通过一个双向的通信连接实现数据交换,这个连接的一端称为一个Socket

端口”是英文port的意译,是设备与外界通信交流的出口。每台计算机可以分配0到65535共65536个端口

每一条Socket连接代表着本地Socket→本地端口→网络介质→远程端口→远程Socket的链路

Socket通信的基本流程

  • 开启一个连接之前,需要创建一个Socket对象(使用API Socket)​,然后绑定本地使用的端口(使用API Bind)​。对客户端而言,连接时(使用API Connect)会由系统分配端口,可以省去绑定步骤。
  • 对客户端而言,连接时(使用API Connect)会由系统分配端口,可以省去绑定步骤。
  • 客户端连接服务器(使用API Connect)
  • 服务器接受连接(使用API Accept)
  • 客户端和服务端通过Send和Receive等API收发数据,操作系统会自动完成数据的确认、重传等步骤,确保传输的数据准确无误。
  • 某一方关闭连接(使用API Close)​,操作系统会执行“四次挥手”的步骤,关闭双方连接
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using System.Net.Sockets;
    using UnityEngine.UI;
    public class Echo : MonoBehaviour {
        //定义套接字
        Socket socket;
        //UGUI
        public InputField InputFeld;
        public Text text;

        //点击连接按钮
        //客户端通过socket.Connect(远程IP地址,远程端口)连接服务端。Connect是一个阻塞方法,程        
        //序会卡住直到服务端回应(接收、拒绝或超时)​。
        public void Connection()
        {
            //Socket
            socket = new Socket(AddressFamily.InterNetwork,
                SocketType.Stream, ProtocolType.Tcp);
            //这一行用于创建一个Socket对象,它的三个参数分别代表地址族、套接字类型和协议。
            //Connect
            socket.Connect("127.0.0.1", 8888);
        }

        //点击发送按钮
        //客户端通过socket.Send发送数据,这也是一个阻塞方法。该方法接受一个byte[​]类型的参数指明    
        //要发送的内容。Send的返回值指明发送数据的长度(例子中没有使用)​。程序用 
        //System.Text.Encoding.Default.GetBytes(字符串)把字符串转换成byte[​]数组,然后发送给服 
        //务端。
        public void Send()
        {
            //Send
            string sendStr = InputFeld.text;
            byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);
            socket.Send(sendBytes);
            //Recv
            byte[] readBuff = new byte[1024];
            //客户端通过socket.Receive接收服务端数据。Receive也是阻塞方法,没有收到服务端数据 
            //时,程序将卡在Receive不会往下执行。Receive带有一个byte[​]类型的参数,它存储接收到 
            //的数据。Receive的返回值指明接收到数据的长度。之后使用System.Text.Encoding. 
            //Default.GetString(readBuff,0, count)将byte[​]数组转换成字符串显示在屏幕上。
            int count = socket.Receive(readBuff);
            string recvStr = System.Text.Encoding.Default.GetString(readBuff, 0, count);
            text.text = recvStr;
            //Close
            socket.Close();
        }
    }

此时运行游戏点击连接会出现

因为我们还没有启动服务器,所以属于正常现象

创建服务端程序

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

internal class Program
{
    private static void Main(string[] args)
    {
        Console.WriteLine("Hello, World!");
        //Socket
        Socket listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        IPAddress ipAdr = IPAddress.Parse("127.0.0.1");
        IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888);
        listenfd.Bind(ipEp);
        //Listen
        listenfd.Listen(0);
        Console.WriteLine("[服务器]启动成功");
        while (true)
        {
            //Accept
            Socket connfd = listenfd.Accept();
            Console.WriteLine("[服务器]Accept");
            //Receive
            byte[] readBuff = new byte[1024];
            int count = connfd.Receive(readBuff);
            string readStr = System.Text.Encoding.Default.GetString(readBuff, 0, count);
            Console.WriteLine("[服务器接受]" + readStr);
            //Send
            byte[] sendBytes = System.Text.Encoding.Default.GetBytes(readStr);
            connfd.Send(sendBytes);
        }
    }
}

绑定 Bind

listenfd.Bind(ipEp)将给listenfd套接字绑定IP和端口。程序中绑定本地地址“127.0.0.1”和8888号端口。127.0.0.1是回送地址,指本地机,一般用于测试。读者也可以设置成真实的IP地址,然后在两台计算机上分别运行客户端和服务端程序

监听 Listen

服务端通过listenfd.Listen(backlog)开启监听,等待客户端连接。参数backlog指定队列中最多可容纳等待接受的连接数,0表示不限制。

应答 Accept

开启监听后,服务器调用listenfd.Accept()接收客户端连接。本例使用的所有Socket方法都是阻塞方法,也就是说当没有客户端连接时,服务器程序卡在listenfd.Accept()不会往下执行,直到接收了客户端的连接。Accept返回一个新客户端的Socket对象,对于服务器来说,它有一个监听Socket(例子中的listenfd)用来监听(Listen)和应答(Accept)客户端的连接,对每个客户端还有一个专门的Socket(例子中的connfd)用来处理该客户端的数据。

IPAddress 和 IPEndPoint

使用IPAddress指定IP地址,使用IPEndPoint指定IP和端口。

System.Text.Encoding.Default.GetString

Receive方法将接收到的字节流保存到readBuff上,readBuff是byte型数组。GetString方法可以将byte型数组转换成字符串。同理,System.Text.Encoding.Default.GetBytes可以将字符串转换成byte型数组。

测试 :

Socket类的一些常用方法

公网和局域网

把宽带连接到家里的路由器,再由路由器分发到多台计算机(校园网、公司局域网同理)​,在这种情况下,路由器会有公网和局域网两个IP

比如:路由器的公网IP是123.207.111.220,局域网IP为192.168.0.1,连接路由器的计算机只有内网IP,它们分别是192.168.0.10和192.168.0.12。。如果将服务端放到连接路由器的某台计算机上,因为它只有局域网IP,所以只有局域网内的计算机可以连接上。如果拥有路由器的控制权,可以使用一种叫“端口映射”的技术,即设置路由器,将路由器IP地址的一个端口映射到内网中的一台计算机,提供相应的服务。当用户访问该IP的这个端口时,路由器自动将请求映射到对应局域网内部的计算机上

异步和多路复用

上面的程序全部使用阻塞API(Connect、Send、Receive等)​,可称为同步Socket程序

一个简单的异步程序示例:

        using System.Collections;
        using System.Collections.Generic;
        using UnityEngine;
        using System.Threading;

        public class Async : MonoBehaviour {
            // Use this for initialization
            void Start () {
                //创建定时器
                Timer timer = new Timer(TimeOut, null, 5000, 0);
                //其他程序代码
                //……
            }

            //回调函数
            private void TimeOut(System.Object state){
                Debug.Log("铃铃铃");
            }
        }

异步Connect

每一个同步API(如Connect)对应着两个异步API,分别是在原名称前面加上Begin和End(如BeginConnect和EndConnect)

BeginConnect的函数原型如下:

public IAsyncResult BeginConnect( string host, int port, AsyncCallback requestCallback, object state )

修改代码:

        using System;

        //点击连接按钮
        public void Connection()
        {
            //Socket
            socket = new Socket(AddressFamily.InterNetwork,
                SocketType.Stream, ProtocolType.Tcp);
            //Connect
            socket.BeginConnect("127.0.0.1", 8888, ConnectCallback, socket);
        }

        //Connect回调
        public void ConnectCallback(IAsyncResult ar){
            try{
                Socket socket = (Socket) ar.AsyncState;
                socket.EndConnect(ar);
                Debug.Log("Socket Connect Succ");
            }
            catch (SocketException ex){
                Debug.Log("Socket Connect fail" + ex.ToString());
            }
        }

说明:

  • 由BeginConnect最后一个参数传入的socket,可由ar.AsyncState获取到。

异步Receive

public IAsyncResult BeginReceive ( byte[] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback, object state )

public int EndReceive( IAsyncResult asyncResult )  它的返回值代表接收到的字节数

修改客户端代码:

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

public class Echo : MonoBehaviour
{
    //UGUI
    public InputField inputField;
    public Text text;
    //定义套接字
    Socket socket;

    //接收缓冲区
    byte[] readBuff = new byte[1024];
    string recvStr = "";

    public void Connection()
    {
        socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        socket.BeginConnect("127.0.0.1", 8888,ConnectCallback,socket);
    }

    //Connect回调
    public void ConnectCallback(IAsyncResult ar)
    {
        try
        {
            Socket socket = (Socket)ar.AsyncState;
            socket.EndConnect(ar);
            Debug.Log("Socket Connect Succ");
            socket.BeginReceive(readBuff,0,1024,0,ReceiveCallback,socket);
        }
        catch (SocketException ex)
        {
            Debug.Log("Socket Connect fail" + ex.ToString());
        }
    }

    public void ReceiveCallback(IAsyncResult ar) {
        try
        {
            Socket socket = (Socket) ar.AsyncState;
            int count = socket.EndReceive(ar);
            recvStr = System.Text.Encoding.Default.GetString(readBuff,0,count);
            socket.BeginReceive(readBuff,0,1024,0,ReceiveCallback,socket);
            Debug.Log("ReceiveCallback" + recvStr);
        }
        catch (SocketException ex)
        {
            Debug.Log("Socket Receive fail" + ex.ToString());
        }
    }

    public void Send()
    {
        string sendStr = inputField.text;
        byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);
        socket.Send(sendBytes);
    }

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        text.text = recvStr;
    }
}

说明:

BeginReceive的参数

上述程序中,BeginReceive的参数为(readBuff, 0, 1024, 0, ReceiveCallback,socket)。第一个参数readBuff表示接收缓冲区;第二个参数0表示从readBuff第0位开始接收数据,这个参数和TCP粘包问题有关,第三个参数1024代表每次最多接收1024个字节的数据

BeginReceive的调用位置

程序在两个地方调用了BeginReceive:一个是ConnectCallback,在连接成功后,就开始接收数据,接收到数据后,回调函数ReceiveCallback被调用。另一个是BeginReceive内部,接收完一串数据后,等待下一串数据的到来

Update和recvStr

在Unity中,只有主线程可以操作UI组件。由于异步回调是在其他线程执行的,如果在BeginReceive给text.text赋值,Unity会弹出“get_isActiveAndEnabled can onlybe called from the main thread”的异常信息,所以程序只给变量recvStr赋值,在主线程执行的Update中再给text.text赋值

异步Send

Socket使用的协议、IP、端口属于用户层面的属性,可以直接修改;操作系统层面拥有“发送”和“接收”两个缓冲区,当调用Send方法时,程序将要发送的字节流写入到发送缓冲区中,再由操作系统完成数据的发送和确认

如果缓冲区满,那么Send就会阻塞,直到缓冲区的数据被确认腾出空间

值得注意的是,Send过程只是把数据写入到发送缓冲区,然后由操作系统负责重传、确认等步骤。Send方法返回只代表成功将数据放到发送缓存区中,对方可能还没有收到数据。

异步Send不会卡住程序,当数据成功写入输入缓冲区(或发生错误)时会调用回调函数。异步Send方法BeginSend的原型如下。

public IAsyncResult BeginSend( byte[] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback, object state )

public int EndSend ( IAsyncResult asyncResult )

修改客户端代码,使用异步发送:

    public void Send()
    {
        string sendStr = inputField.text;
        byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);
        for(int i = 0;i < 10000;i++) {
        socket.BeginSend(sendBytes,0,sendBytes.Length,0,SendCallback,socket);
        }
    }

    public void SendCallback(IAsyncResult ar) {
        try
        {
            Socket socket = (Socket) ar.AsyncState;
            int count = socket.EndSend(ar);
            Debug.Log("Socket Send succ" + count);
        }
        catch (SocketException ex)
        {
            Debug.Log("Socket Send fail" + ex.ToString());
        }
    }

异步服务端

上面的同步服务端程序同一时间只能处理一个客户端的请求,因为它会一直阻塞,等待某一个客户端的数据,无暇接应其他客户端。使用异步方法,可以让服务端同时处理多个客户端的数据,及时响应

管理客户端

定义一个名为ClientState的类,用于保存一个客户端信息。ClientState包含TCP连接所需Socket,以及用于填充BeginReceive参数的读缓冲区readBuff

        //数据结构
        class ClientState {

            public Socket socket;

            public byte[] readBuff = new byte[1024];

        }
        static Dictionary<Socket, ClientState> clients =
            new Dictionary<Socket, ClientState>();

异步Accept

public IAsyncResult BeginAccept( AsyncCallback callback, object state )

public Socket EndAccept( IAsyncResult asyncResult )

程序结构:

修改代码:

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

class ClientState {
    public Socket socket;
    public byte[] readBuff = new byte[1024];
}

internal class Program
{
    //监听Socket
    static Socket listenfd;
    //客户端Socket及状态信息
    static Dictionary<Socket,ClientState> clients = new Dictionary<Socket, ClientState>();

    private static void Main(string[] args)
    {
        Console.WriteLine("Hello, World!");
        //Socket
        listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        IPAddress ipAdr = IPAddress.Parse("127.0.0.1");
        IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888);
        listenfd.Bind(ipEp);
        //Listen
        listenfd.Listen(0);
        Console.WriteLine("[服务器]启动成功");
        listenfd.BeginAccept(AcceptCallback,listenfd);
        //等待
        Console.ReadLine();
        // while (true)
        // {
        //     //Accept
        //     Socket connfd = listenfd.Accept();
        //     Console.WriteLine("[服务器]Accept");
        //     //Receive
        //     byte[] readBuff = new byte[1024];
        //     int count = connfd.Receive(readBuff);
        //     string readStr = System.Text.Encoding.Default.GetString(readBuff, 0, count);
        //     Console.WriteLine("[服务器接受]" + readStr);
        //     //Send
        //     string sendStr = System.DateTime.Today.ToString();
        //     byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);
        //     connfd.Send(sendBytes);
        // }
    }

    //Accept回调
    public static void AcceptCallback(IAsyncResult ar) {
        try
        {
            Console.WriteLine("[服务器]Accept");
            Socket listenfd = (Socket) ar.AsyncState;
            Socket clientfd = listenfd.EndAccept(ar);
            //clients列表
            ClientState state = new ClientState();
            state.socket = clientfd;
            clients.Add(clientfd,state);
            //接收数据的BeginReceive
            clientfd.BeginReceive(state.readBuff,0,1024,0,ReceiveCallback,state);
            //继续Accept
            listenfd.BeginAccept(AcceptCallback,listenfd);
        }
        catch (SocketException ex)
        {
            Console.WriteLine("Socket Accept fail" + ex.ToString());
        }
    }

    //Receive回调
    public static void ReceiveCallback(IAsyncResult ar) {
        try
        {
            ClientState state = (ClientState) ar.AsyncState;
            Socket clientfd = state.socket;
            int count = clientfd.EndReceive(ar);
            //客户端关闭
            if(count == 0) {
                clientfd.Close();
                clients.Remove(clientfd);
                Console.WriteLine("Socket Close");
                return;
            }
            string recvStr = System.Text.Encoding.Default.GetString(state.readBuff,0,count);
            byte[] sendBytes = System.Text.Encoding.Default.GetBytes("echo" + recvStr);
            clientfd.Send(sendBytes); //减少代码量,不用异步
            clientfd.BeginReceive(state.readBuff,0,1024,0,ReceiveCallback,state);
            //注意BeginReceive的最后一个参数,这里以ClientState代替了原来的Socket。
        }
        catch (SocketException ex)
        {
            Console.WriteLine("Socket Receive fail" + ex.ToString());
            throw;
        }
    }
}

AcceptCallback是BeginAccept的回调函数,它处理了三件事情:

  • 给新的连接分配ClientState,并把它添加到clients列表中;
  • 异步接收客户端数据;
  • 再次调用BeginAccept实现循环。

ReceiveCallback是BeginReceive的回调函数,它也处理了三件事情:

  • 服务端收到消息后,回应客户端;
  • 如果收到客户端关闭连接的信号“if(count == 0)”​,断开连接;
  • 继续调用BeginReceive接收下一个数据。

当Receive返回值小于等于0时,表示Socket连接断开,可以关闭Socket。

聊天室

在聊天室中,某个客户端发送聊天消息,所有在线的客户端都会收到这条消息。也就是会遍历在线的客户端,然后推送消息

修改服务端代码:

        //Receive回调
        public static void ReceiveCallback(IAsyncResult ar){
            try {
                ……
                  string recvStr = System.Text.Encoding.Default.GetString(state.readBuff,
    0, count);
                string sendStr = clientfd.RemoteEndPoint.ToString() + ":" + recvStr;
                byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);
                foreach (ClientState s in clients.Values){
                    s.socket.Send(sendBytes);
                }
                clientfd.BeginReceive( state.readBuff, 0, 1024, 0,
                    ReceiveCallback, state);
            }
            catch (SocketException ex){
                ……
            }
        }

服务端整体代码:

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

class ClientState {
    public Socket socket;
    public byte[] readBuff = new byte[1024];
}

internal class Program
{
    //监听Socket
    static Socket listenfd;
    //客户端Socket及状态信息
    static Dictionary<Socket,ClientState> clients = new Dictionary<Socket, ClientState>();

    private static void Main(string[] args)
    {
        Console.WriteLine("Hello, World!");
        //Socket
        listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        IPAddress ipAdr = IPAddress.Parse("127.0.0.1");
        IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888);
        listenfd.Bind(ipEp);
        //Listen
        listenfd.Listen(0);
        Console.WriteLine("[服务器]启动成功");
        listenfd.BeginAccept(AcceptCallback,listenfd);
        //等待
        Console.ReadLine();
    }

    //Accept回调
    public static void AcceptCallback(IAsyncResult ar) {
        try
        {
            Console.WriteLine("[服务器]Accept");
            Socket listenfd = (Socket) ar.AsyncState;
            Socket clientfd = listenfd.EndAccept(ar);
            //clients列表
            ClientState state = new ClientState();
            state.socket = clientfd;
            clients.Add(clientfd,state);
            //接收数据的BeginReceive
            clientfd.BeginReceive(state.readBuff,0,1024,0,ReceiveCallback,state);
            //继续Accept
            listenfd.BeginAccept(AcceptCallback,listenfd);
        }
        catch (SocketException ex)
        {
            Console.WriteLine("Socket Accept fail" + ex.ToString());
        }
    }

    //Receive回调
    public static void ReceiveCallback(IAsyncResult ar) {
        try
        {
            ClientState state = (ClientState) ar.AsyncState;
            Socket clientfd = state.socket;
            int count = clientfd.EndReceive(ar);
            //客户端关闭
            if(count == 0) {
                clientfd.Close();
                clients.Remove(clientfd);
                Console.WriteLine("Socket Close");
                return;
            }
            string recvStr = System.Text.Encoding.Default.GetString(state.readBuff,0,count);
            string sendStr = clientfd.RemoteEndPoint.ToString() + ":" + recvStr;
            byte[] sendBytes = System.Text.Encoding.Default.GetBytes("echo" + sendStr);
            foreach(ClientState s in clients.Values) {
                s.socket.Send(sendBytes);
            }
            clientfd.BeginReceive(state.readBuff,0,1024,0,ReceiveCallback,state);
        }
        catch (SocketException ex)
        {
            Console.WriteLine("Socket Receive fail" + ex.ToString());
            throw;
        }
    }
}

修改客户端代码,显示历史聊天:

        //Receive回调
        public void ReceiveCallback(IAsyncResult ar){
            try {
                Socket socket = (Socket) ar.AsyncState;
                int count = socket.EndReceive(ar);
                string s = System.Text.Encoding.Default.GetString(readBuff, 0, count);
                recvStr = s + "\n" + recvStr;

                socket.BeginReceive( readBuff, 0, 1024, 0,
                    ReceiveCallback, socket);
            }
            catch (SocketException ex){
                Debug.Log("Socket Receive fail" + ex.ToString());
            }
        }

客户端整体代码:

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

public class Echo : MonoBehaviour
{
    //UGUI
    public InputField inputField;
    public Text text;
    //定义套接字
    Socket socket;

    //接收缓冲区
    byte[] readBuff = new byte[1024];
    string recvStr = "";

    public void Connection()
    {
        socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        socket.BeginConnect("127.0.0.1", 8888,ConnectCallback,socket);
    }

    //Connect回调
    public void ConnectCallback(IAsyncResult ar)
    {
        try
        {
            Socket socket = (Socket)ar.AsyncState;
            socket.EndConnect(ar);
            Debug.Log("Socket Connect Succ");
            socket.BeginReceive(readBuff,0,1024,0,ReceiveCallback,socket);
        }
        catch (SocketException ex)
        {
            Debug.Log("Socket Connect fail" + ex.ToString());
        }
    }

    public void ReceiveCallback(IAsyncResult ar) {
        try
        {
            Socket socket = (Socket) ar.AsyncState;
            int count = socket.EndReceive(ar);
            string s = System.Text.Encoding.Default.GetString(readBuff,0,count);
            recvStr = s + "\n" + recvStr;
            socket.BeginReceive(readBuff,0,1024,0,ReceiveCallback,socket);
            Debug.Log("ReceiveCallback" + recvStr);
        }
        catch (SocketException ex)
        {
            Debug.Log("Socket Receive fail" + ex.ToString());
        }
    }

    public void Send()
    {
        string sendStr = inputField.text;
        byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);
        socket.BeginSend(sendBytes,0,sendBytes.Length,0,SendCallback,socket);

    }

    public void SendCallback(IAsyncResult ar) {
        try
        {
            Socket socket = (Socket) ar.AsyncState;
            int count = socket.EndSend(ar);
            Debug.Log("Socket Send succ" + count);
        }
        catch (SocketException ex)
        {
            Debug.Log("Socket Send fail" + ex.ToString());
        }
    }

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        text.text = recvStr;
    }
}

效果: 

状态检测Poll

什么是Poll

处理阻塞的代码:

        if(socket有可读数据){
            socket.Receive()
        }

        if(socket缓冲区可写){
            socket.Send()
        }

        if(socket发生程序){
            错误处理
        }

public bool Poll ( int microSeconds, SelectMode mode )

防止单线程卡住程序的Poll方法

Poll方法将会检查Socket的状态。如果指定mode参数为SelectMode.SelectRead,则可确定Socket是否为可读;指定参数为SelectMode.SelectWrite,可确定Socket是否为可写;指定参数为SelectMode.SelectError,可以检测错误条件。Poll将在指定的时段(以微秒为单位)内阻止执行,如果希望无限期地等待响应,可将microSeconds设置为一个负整数;如果希望不阻塞,可将microSeconds设置为0。

        //省略各种using
        public class Echo : MonoBehaviour {

            //定义套接字
            Socket socket;
            //UGUI
            public InputField InputFeld;
            public Text text;

            //点击连接按钮
            public void Connection()
            {
                //Socket
                socket = new Socket(AddressFamily.InterNetwork,
                    SocketType.Stream, ProtocolType.Tcp);
                //Connect
                socket.Connect("127.0.0.1", 8888);
            }

            //点击发送按钮
            public void Send(){……//略}

            public void Update(){
                if(socket == null) {
                    return;
                }

                if(socket.Poll(0, SelectMode.SelectRead)){
                    byte[] readBuff = new byte[1024];
                    int count = socket.Receive(readBuff);
                    string recvStr =
                        System.Text.Encoding.Default.GetString(readBuff, 0, count);
                    text.text = recvStr;
                }
            }
        }

服务端代码:

        class MainClass
        {
            //监听Socket
            static Socket listenfd;
            //客户端Socket及状态信息
            static Dictionary<Socket, ClientState> clients =
                new Dictionary<Socket, ClientState>();
            public static void Main (string[] args)
            {
                //Socket
                listenfd = new Socket(AddressFamily.InterNetwork,
                    SocketType.Stream, ProtocolType.Tcp);
                //Bind
                IPAddress ipAdr = IPAddress.Parse("127.0.0.1");
                IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888);
                listenfd.Bind(ipEp);
                //Listen
                listenfd.Listen(0);
                Console.WriteLine("[服务器]启动成功");
                //主循环
                while(true){
                    //检查listenfd
                    if(listenfd.Poll(0, SelectMode.SelectRead)){
                        ReadListenfd(listenfd);
                    }
                    //检查clientfd
                    foreach (ClientState s in clients.Values){
                        Socket clientfd = s.socket;
                        if(clientfd.Poll(0, SelectMode.SelectRead)){
                            if(! ReadClientfd(clientfd)){
                                break;
                            }
                        }
                    }
                    //防止CPU占用过高
                    System.Threading.Thread.Sleep(1);
                }
            }
        }

        //读取Listenfd
        public static void ReadListenfd(Socket listenfd){
            Console.WriteLine("Accept");
            Socket clientfd = listenfd.Accept();
            ClientState state = new ClientState();
            state.socket = clientfd;
            clients.Add(clientfd, state);
        }

        //读取Clientfd
        public static bool ReadClientfd(Socket clientfd){
            ClientState state = clients[clientfd];
            //接收
            int count = 0;
            try{
                count = clientfd.Receive(state.readBuff);
            }catch(SocketException ex){
                clientfd.Close();
                clients.Remove(clientfd);
                Console.WriteLine("Receive SocketException " + ex.ToString());
                return false;
            }
            //客户端关闭
            if(count == 0){
                clientfd.Close();
                clients.Remove(clientfd);
                Console.WriteLine("Socket Close");
                return false;
            }
            //广播
            string recvStr =
                System.Text.Encoding.Default.GetString(state.readBuff, 0, count);
            Console.WriteLine("Receive" + recvStr);
            string sendStr = clientfd.RemoteEndPoint.ToString() + ":" + recvStr;
            byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);
            foreach (ClientState cs in clients.Values){
                cs.socket.Send(sendBytes);
            }
            return true;
        }

多路复用Select(重点)

多路复用,就是同时处理多路信号,比如同时检测多个Socket的状态。

解决Poll服务端中CPU占用率过高的方法,那就是:同时检测多个Socket的状态。在设置要监听的Socket列表后,如果有一个(或多个)Socket可读(或可写,或发生错误信息)​,那就返回这些可读的Socket,如果没有可读的,那就阻塞。

public static void Select( IList checkRead, IList checkWrite, IList checkError, int microSeconds )

如图所示:把包含6个Socket的列表传给Select, Select方法将会阻塞,等到超时或某个(或多个)Socket可读时返回,并且修改checkRead列表,仅保存可读的socket A和socket C。当没有任何可读Socket时,程序将会阻塞,不占用CPU资源。

Select 服务端

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

        class ClientState
        {
            public Socket socket;
            public byte[] readBuff = new byte[1024];
        }

        class MainClass
        {
            //监听Socket
            static Socket listenfd;
            //客户端Socket及状态信息
            static Dictionary<Socket, ClientState> clients =
                new Dictionary<Socket, ClientState>();

            public static void Main (string[] args)
            {
                //Socket
                listenfd = new Socket(AddressFamily.InterNetwork,
                    SocketType.Stream, ProtocolType.Tcp);
                //Bind
                IPAddress ipAdr = IPAddress.Parse("127.0.0.1");
                IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888);
                listenfd.Bind(ipEp);
                //Listen
                listenfd.Listen(0);
                Console.WriteLine("[服务器]启动成功");
                //checkRead
                List<Socket> checkRead = new List<Socket>();
                //主循环
                while(true){
                    //填充checkRead列表
                    checkRead.Clear();
                    checkRead.Add(listenfd);
                    foreach (ClientState s in clients.Values){
                        checkRead.Add(s.socket);
                    }
                    //select
                    Socket.Select(checkRead, null, null, 1000);
                    //检查可读对象
                    foreach (Socket s in checkRead){
                        if(s == listenfd){
                            ReadListenfd(s);
                        }
                        else{
                            ReadClientfd(s);
                        }
                    }
                }
            }
        }
  • 将监听Socket(listenfd)和客户端Socket(遍历clients列表)添加到待检测Socket可读状态的列表checkList中。
  • 调用Select,程序中设置超时时间为1秒,若1秒内没有任何可读信息,Select方法将checkList列表变成空列表,然后返回。
  • 对Select处理后的每个Socket做处理,如果监听Socket(listenfd)可读,说明有客户端连接,需调用Accept。如果客户端Socket可读,说明客户端发送了消息(或关闭)​,将消息广播给所有客户端。

Select客户端

            public void Update(){
                if(socket == null) {
                    return;
                }
                //填充checkRead列表
                checkRead.Clear();
                checkRead.Add(socket);
                //select
                Socket.Select(checkRead, null, null, 0);
                //check
                foreach (Socket s in checkRead){
                    byte[] readBuff = new byte[1024];
                    int count = socket.Receive(readBuff);
                    string recvStr =
                        System.Text.Encoding.Default.GetString(readBuff, 0, count);
                    text.text = recvStr;
                }
            }

        }

为了不卡住客户端,Select的超时时间设置为0,永不阻塞

参考书籍:《Unity3D网络游戏实战(第2版)》 (豆瓣) (douban.com)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1980082.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java | Leetcode Java题解之第322题零钱兑换

题目&#xff1a; 题解&#xff1a; public class Solution {public int coinChange(int[] coins, int amount) {int max amount 1;int[] dp new int[amount 1];Arrays.fill(dp, max);dp[0] 0;for (int i 1; i < amount; i) {for (int j 0; j < coins.length; j)…

基于springboot+vue+uniapp的智慧物业平台小程序

开发语言&#xff1a;Java框架&#xff1a;springbootuniappJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#…

c++ 容器 vector

vector的意思就是向量&#xff0c;就是一个顺序表的意思&#xff0c;这个顺序表可以存任意的类型&#xff0c;因为其线性的内存特点&#xff0c;所以在stl里是经常被使用的存在。 vector vector既然要能储存任意的变量&#xff0c;那么就必须使用模板: 这里的T就是变量类型&a…

【QT】鼠标按键事件 - QMouseEvent QKeyEvent

qt 事件 事件1. 事件概念2. 事件的处理3. 按键事件&#xff08;1&#xff09;单个按键&#xff08;2&#xff09;组合按键 4. 鼠标事件&#xff08;1&#xff09;鼠标单击事件&#xff08;2&#xff09;鼠标释放事件&#xff08;3&#xff09;鼠标双击事件&#xff08;4&#x…

【多线程-从零开始-叁】线程的核心操作

一、创建一个线程-start() start 和 run 的区别&#xff1a;&#xff08;经典面试题&#xff09; run 描述了线程要执行的任务&#xff0c;也可以称为“线程的入口”此处 start 会根据不同的系统&#xff0c;分别调用不同的 API&#xff0c;来执行系统函数&#xff0c;在系统…

原生多模态跟GPT聊天部分测试,大家都用他来做什么;字节推出AI音乐产品-海绵音乐,可以媲美Udio和Suno

✨ 1: 跟GPT聊天 原生多模态跟GPT聊天部分测试&#xff0c;大家都用他来做什么。 说各个国家的语言&#xff0c;例如普通话&#xff0c;或者是广东话等。 ChatGPT担任富有激情的足球比赛解说员 使用新的高级语音模式 视觉&#xff0c;进行实时日语翻译&#xff01; 地址&…

Java编程规范 空格

public static void main(String[] args) { // 缩进4 个空格 String say "hello"; // 运算符的左右必须有一个空格 int flag 0; // 关键词if 与括号之间必须有一个空格&#xff0c;括号内的f与左括号&#xff0c;0与右括号不需要空格 if (flag 0) { System…

秃姐学AI系列之:模型选择 | 欠拟合和过拟合 | 权重衰退

目录 训练误差 泛化误差 验证数据集和测试数据集 验证数据集 Validation Dataset&#xff1a; 测试数据集&#xff1a; K-则交叉验证 总结 过拟合和欠拟合 模型容量 模型容量的影响 估计模型容量 数据复杂度 总结 权重衰退 weight decay 使用均方范数作为硬性…

【八】Zookeeper3.7.1集成Hadoop3.3.4集群安装

文章目录 1.基本原理2.下载并解压ZooKeeper3.配置环境变量4.配置ZooKeeper5.创建数据目录并初始化myid6.启动ZooKeeper7.配置ZooKeeper集成到Hadoop8.重启Hadoop9.ZooKeeper状态检查 1.基本原理 ZooKeeper 是一个分布式协调服务&#xff0c;用于分布式系统中管理配置信息、命名…

51单片机—智能垃圾桶(定时器)

一. 定时器 1. 简介 C51中的定时器和计数器是同一个硬件电路支持的&#xff0c;通过寄存器配置不同&#xff0c;就可以将他当做定时器或者计数器使用。 确切的说&#xff0c;定时器和计数器区别是致使他们背后的计数存储器加1的信号不同。当配置为定时器使用时&#xff0c;每…

vue3 手写日历组件

找了很久vue3的element样式一直没办法修改实现。只能手写日历了。借鉴了一些大佬的代码 调用&#xff1a; 再要使用的地方引入 import calendarelement from ./calendarelement.vue //日历组件 <div > <calendarelement /> //日历</div> 效果&#…

押金原路退回系统在医院中应用,一键操作秒到账 押金+身份证+电子押金单

一、医院押金管理必要性 保障医疗服务的连续性&#xff1a;患者缴纳押金能够确保在治疗过程中&#xff0c;医院有足够的资金来提供必要的医疗服务、药品和设备&#xff0c;不会因为费用问题而中断治疗。例如&#xff0c;在紧急手术或需要持续使用昂贵药物的情况下&#xff0c;…

【Vue3】组件通信之$attrs

【Vue3】组件通信之$attrs 背景简介开发环境开发步骤及源码总结 背景 随着年龄的增长&#xff0c;很多曾经烂熟于心的技术原理已被岁月摩擦得愈发模糊起来&#xff0c;技术出身的人总是很难放下一些执念&#xff0c;遂将这些知识整理成文&#xff0c;以纪念曾经努力学习奋斗的…

开发在线客服系统新的宣传推广站【微客客服】

打造一个软件宣传官网&#xff0c;这事儿可不简单。咱们得先搞清楚&#xff0c;这个网站要给谁看&#xff0c;要传达啥信息&#xff0c;需要哪些功能。 我们网站是宣传【在线客服系统】的&#xff0c;所以需要把主要功能展示清楚 在线网址&#xff1a;https://weikefu.com.cn 然…

Python面试宝典第27题:全排列

题目 给定一个不含重复数字的数组nums&#xff0c;返回其所有可能的全排列 。备注&#xff1a;可以按任意顺序返回答案。 示例 1&#xff1a; 输入&#xff1a;nums [1,2,3] 输出&#xff1a;[[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]] 示例 2&#xff1a; 输…

Qt之Gui

组件依赖关系 应用 #mermaid-svg-GADicZtZJRVVUeiF {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-GADicZtZJRVVUeiF .error-icon{fill:#552222;}#mermaid-svg-GADicZtZJRVVUeiF .error-text{fill:#552222;stroke:#…

【Spark计算引擎----第三篇(RDD)---《深入理解 RDD:依赖、Spark 流程、Shuffle 与缓存》】

前言&#xff1a; &#x1f49e;&#x1f49e;大家好&#xff0c;我是书生♡&#xff0c;本阶段和大家一起分享和探索大数据技术Spark—RDD&#xff0c;本篇文章主要讲述了&#xff1a;RDD的依赖、Spark 流程、Shuffle 与缓存等等。欢迎大家一起探索讨论&#xff01;&#xff0…

【Gold菜鸟】Linux知识回忆(8)——进程和计划任务

前言 这部分让我们来继续了解Linux中进程和计划任务的相关知识吧~ 相关技术交流欢迎添加VX: wenjinworkon 目录 进程和内存管理 什么是进程 进程结构 进程相关概念 物理地址空间和虚拟地址空间 用户和内核空间 进程使用内存问题 进程状态 内存淘汰数据机制&#xff1a;…

数学建模评价类—Topsis法

目录 文章目录 前言 切记&#xff1a;以下内容仅用于参考理解&#xff0c;不可用于数模竞赛&#xff01;&#xff01;&#xff01; 一、Topsis的基本原理 二、Topsis的建模过程 1.判断矩阵是否需要正向化 2.原始矩阵正向化 3.矩阵标准化 4.计算距离&#xff0c;给出得…

Can Large Language Models Provide Feedback to Students? A Case Study on ChatGPT

文章目录 题目摘要相关工作方法结果讨论意义 题目 大型语言模型能为学生提供反馈吗&#xff1f;ChatGPT 案例研究 论文地址&#xff1a;https://ieeexplore.ieee.org/abstract/document/10260740 摘要 摘要——教育反馈已被广泛认为是提高学生学习能力的有效方法。然而&#x…