C#实现本地服务器多客户端同频道通信

(一)需求

     在游戏中我们经常能够看到玩家在世界频道聊天,在QQ或微信中也有群聊功能。抽象成计算机网络,就是多个客户端通过服务器进行同频道通信,所有连接的客户端都可以看到其他客户端发送的消息。这种多客户端同频道通信是如何实现的呢?在本篇文章我们就来探讨一下。

(二)解决思路

       这个需求的重点部分在于网络通信,需要我们掌握基本的计算机网络通信知识,具体到每种编程语言又有对应的API。如果把这个需求抽象到计算机网络中,我们就可以理解成多个客户端向服务器发送信息,服务器接收信息后又把信息发送给所有连接的客户端。这样,在各个客户端就可以接收到其他客户端发送的信息了。

(三)设计思路

       服务器基于本地服务器开发,通过一个单独的C#控制台项目模拟,编程语言使用C#,客户端通过Unity3D构建GUI并编写客户端脚本。多客户端则通过打开多个Unity3D项目的可执行文件进行模拟,客户端的GUI需要有调试面板、客户端名称下拉菜单、连接和断开连接按钮、消息显示面板、消息输入框和消息发送按钮等。

(四)代码实现

        由于代码中引用了自定义的网络通信共享库NetShare,关于NetShare请阅读本栏目下的“C#实现网络通信共享库NetShare”。


       客户端

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using UnityEngine;
using UnityEngine.UI;
using NetShare;//自定义网络通信共享库,包括通用数据包DataPacket等
using System.Threading;

//世界频道客户端
public class WorldChannelClient : MonoBehaviour
{
    public Text BaseInfo;//显示Socket连接基本信息的文本
    public Text EchoContents;//Socket回显信息的文本
    public Text ChatContents;//聊天信息的文本
    public Dropdown ClientMenu;//客户端名称下拉菜单
    public Button Connect;//连接按钮
    public Button DisConnect;//断开连接按钮
    public InputField SendInput;//聊天消息输入框
    public Button Send;//聊天信息发送按钮

    static string ipAddressStr;//IP地址字符串
    static int port;//端口
    static IPAddress iPAddress;//IP地址对象
    static IPEndPoint iPEndPoint;//IP端点对象
    string clientName;//客户端名称
    Socket currentClientSocket;//当前客户端Socket
    bool isLockSend;//是否锁定聊天信息发送按钮
    byte[] buffer;//消息接收缓冲区
    Queue<string> echoContentQueue, chatContentQueue;//回显信息队列和聊天信息队列
    DataPacket dataPacket;//通用数据包

    //反映Socket是否与服务器有效连接的属性
    bool isConnected
    {
        get
        {
            if (currentClientSocket == null) return false;
            return !currentClientSocket.Poll(10, SelectMode.SelectRead) && currentClientSocket.Connected;
        }
    }

    void Start()
    {
        //初始化
        ipAddressStr = "8.137.8.206";
        clientName = ClientMenu.options.Count > 0 ? ClientMenu.options[0].text : "";
        port = 5500;
        iPAddress = IPAddress.Parse(ipAddressStr);
        iPEndPoint = new IPEndPoint(iPAddress, port);
        buffer = new byte[1024];
        echoContentQueue = new Queue<string>();
        chatContentQueue = new Queue<string>();

        //为UI控件添加监听事件
        ClientMenu.onValueChanged.AddListener((index) =>
        {
            clientName = ClientMenu.options[index].text;
        });
        Connect.onClick.AddListener(() =>
        {
            Thread thread = new Thread(new ThreadStart(ConnectDeal));
            thread.Start();
        });
        DisConnect.onClick.AddListener(() =>
        {
            Thread thread = new Thread(new ThreadStart(DisConnectDeal));
            thread.Start();
        });
        Send.onClick.AddListener(() =>
        {
            Thread thread = new Thread(new ThreadStart(SendDeal));
            thread.Start();
        });
    }

    void Update()
    {
        //不断更新Socket基本信息
        BaseInfo.text = $"ClientName:{clientName}" +
                        string.Format("\nSocketHashCode:{0}", currentClientSocket == null ? "None" : currentClientSocket.GetHashCode().ToString()) +
                        $"\nisLock:{isLockSend}" +
                        string.Format("\nPoll:{0}", currentClientSocket == null ? "None" : (!currentClientSocket.Poll(10, SelectMode.SelectRead)).ToString()) +
                        string.Format("\nIsConnected:{0}", currentClientSocket == null ? "False" : currentClientSocket.Connected.ToString());
        //更新回显信息
        if (echoContentQueue.Count > 0)
        {
            while (echoContentQueue.Count > 0)
            {
                SetEchoContents(echoContentQueue.Dequeue());
            }
        }
        //更新聊天信息
        if (chatContentQueue.Count > 0)
        {
            while (chatContentQueue.Count > 0)
            {
                SetChatContents(chatContentQueue.Dequeue());
            }
        }
    }

    //设置回显信息相关UI的内容
    void SetEchoContents(string text)
    {
        EchoContents.text += text;
    }

    //设置聊天信息相关UI的内容
    void SetChatContents(string text)
    {
        ChatContents.text += text;
    }

    //执行逻辑:Socket异步连接处理
    void ConnectDeal()
    {
        echoContentQueue.Enqueue($"\n客户端{clientName}正在请求服务器连接...");
        Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        clientSocket.BeginConnect(iPEndPoint, ConnectCallback, clientSocket);
    }

    //执行逻辑:Socket异步断开连接处理
    void DisConnectDeal()
    {
        echoContentQueue.Enqueue($"\n客户端{clientName}正在断开与服务器的连接...");
        if (isConnected)
        {
            currentClientSocket.Shutdown(SocketShutdown.Both);//关闭Socket的发送和接收消息功能
            currentClientSocket.BeginDisconnect(false, DisConnectCallback, currentClientSocket);
        }
        else echoContentQueue.Enqueue($"\n客户端{clientName}未与服务器建立连接,无法进行断开连接的操作...");
    }

    //执行逻辑:Socket异步接收信息处理
    void ReceiveDeal()
    {
        echoContentQueue.Enqueue($"\n客户端{clientName}开始监听服务器响应...");
        currentClientSocket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallback, currentClientSocket);
    }

    //执行逻辑:Socket异步发送信息处理
    void SendDeal()
    {
        if (!isLockSend && !string.IsNullOrEmpty(SendInput.text))
        {
            dataPacket.mContent = SendInput.text;
            SendInput.text = string.Empty;
            byte[] bytes = dataPacket.ToBytes();
            currentClientSocket.BeginSend(bytes, 0, bytes.Length, SocketFlags.None, SendCallback, currentClientSocket);
        }
    }

    //执行逻辑:Socket异步连接处理回调
    void ConnectCallback(IAsyncResult ar)
    {
        try
        {
            Socket socket = ar.AsyncState as Socket;
            socket.EndConnect(ar);
            currentClientSocket = socket;
            if (isConnected)
            {
                dataPacket = new ClientDataPacket()
                {
                    mLocalEndPointStr = socket.LocalEndPoint.ToString(),
                    mClientName = clientName
                };
                isLockSend = false;
                echoContentQueue.Enqueue($"\n<color=orange>客户端{clientName}与服务器连接成功!</color>");
                ReceiveDeal();
            }
            else echoContentQueue.Enqueue($"\n<color=red>客户端{clientName}与服务器连接失败!</color>");
        }
        catch (SocketException se)
        {
            echoContentQueue.Enqueue($"\n<color=red>客户端{clientName}与服务器连接失败!</color>\n错误信息:{se.Message}");
        }
    }

    //执行逻辑:Socket异步断开连接处理回调
    void DisConnectCallback(IAsyncResult ar)
    {
        try
        {
            isLockSend = true;
            Socket socket = ar.AsyncState as Socket;
            socket.EndDisconnect(ar);
            dataPacket = null;
            echoContentQueue.Enqueue($"\n<color=orange>客户端{clientName}与服务器断开连接操作成功!</color>");
        }
        catch (SocketException se)
        {
            echoContentQueue.Enqueue($"\n客户端{clientName}与服务器断开连接操作失败!\n错误信息:{se.Message}");
        }
    }

    //执行逻辑:Socket异步发送信息处理回调
    void SendCallback(IAsyncResult ar)
    {
        try
        {
            Socket socket = ar.AsyncState as Socket;
            socket.EndSend(ar);
            echoContentQueue.Enqueue($"\n客户端{clientName}向服务器发送了一条消息!");
        }
        catch (SocketException se)
        {
            echoContentQueue.Enqueue($"\n客户端{clientName}向服务器发送信息操作失败!\n错误信息:{se.Message}");
        }
    }

    //执行逻辑:Socket异步接收信息处理回调
    void ReceiveCallback(IAsyncResult ar)
    {
        try
        {
            Socket socket = ar.AsyncState as Socket;
            int count = socket.EndReceive(ar);
            string res = Encoding.UTF8.GetString(buffer, 0, count);
            if (!string.IsNullOrEmpty(res)) chatContentQueue.Enqueue("\n" + res);
            //若Socket连接有效则继续接收消息
            if (isConnected)
                socket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallback, socket);
        }
        catch (SocketException se)
        {
            echoContentQueue.Enqueue($"\n客户端{clientName}接收服务器消息失败!\n错误信息:{se.Message}");
        }
    }
}

        服务器

using System.Net.Sockets;
using System.Net;
using System.Text;
using NetShare;//自定义网络通信共享库,其中包括了通用数据包DataPacket、客户端数据包ClientDataPacket等

namespace UnityServer
{
    //世界频道服务器
    internal class WorldChannelServer
    {
        private static string ipAddressStr = "127.0.0.1";//IP地址字符串
        private static int port = 5500;//端口
        private static int maxConnectCount = 20;//最大连接数
        private static byte[] buffer = new byte[1024];//消息缓冲区

        //客户端Socket合集,key为IPEndPoint字符串,value为服务器为客户端分配的Socket
        private static Dictionary<string, Socket> clients = new Dictionary<string, Socket>();

        private static Socket? serverSocket;//服务器Socket

        private static void Main(string[] args)
        {
            Thread thread = new Thread(new ThreadStart(ServerDeal));
            thread.Start();
            Console.ReadLine();
        }

        //判断Socket是否进行有效连接
        private static bool IsConnected(Socket socket)
        {
            if (socket == null) return false;
            return !socket.Poll(10, SelectMode.SelectRead) && socket.Connected;
        }

        //执行逻辑:服务器处理
        private static void ServerDeal()
        {
            serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            IPAddress v_ipAddress = IPAddress.Parse(ipAddressStr);
            serverSocket.Bind(new IPEndPoint(v_ipAddress, port));
            serverSocket.Listen(maxConnectCount);
            Console.WriteLine($"开启服务器[{serverSocket.LocalEndPoint}]...");

            serverSocket.BeginAccept(AcceptCallback, null);
        }

        //执行逻辑:Socket异步接收消息
        private static void ReceiveDeal(object? clientSocket)
        {
            Console.WriteLine("********************");
            if (clientSocket == null) return;
            Socket? v_clientSocket = clientSocket as Socket;
            if (v_clientSocket == null) return;
            Console.WriteLine("接收到客户端的连接请求!");

            if (IsConnected(v_clientSocket))
                v_clientSocket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallback, v_clientSocket);
        }

        //添加客户端Socket到客户端Socket合集
        private static void AddClient(Socket clientSocket)
        {
            if (clientSocket == null) return;
            EndPoint? endPoint = clientSocket.RemoteEndPoint;
            if (endPoint != null)
            {
                string? v_endPointStr = endPoint.ToString();
                if (v_endPointStr != null) clients[v_endPointStr] = clientSocket;
            }
        }

        //向所有客户端发送指定信息
        private static void SendToAll(string? content)
        {
            if (string.IsNullOrEmpty(content)) return;
            byte[] bytes = Encoding.UTF8.GetBytes(content);
            foreach (Socket clientSocket in clients.Values)
            {
                if (IsConnected(clientSocket))
                {
                    Thread thread = new Thread(() =>
                    {
                        clientSocket.BeginSend(bytes, 0, bytes.Length, SocketFlags.None, SendCallback, clientSocket);
                    });
                    thread.Start();
                }
            }
        }

        //Socket监听请求回调
        private static void AcceptCallback(IAsyncResult ar)
        {
            try
            {
                if (serverSocket != null)
                {
                    Socket clientSocket = serverSocket.EndAccept(ar);
                    AddClient(clientSocket);
                    Thread thread = new Thread(new ParameterizedThreadStart(ReceiveDeal));
                    thread.Start(clientSocket);
                    serverSocket.BeginAccept(AcceptCallback, null);
                }
            }
            catch (SocketException se)
            {
                Console.WriteLine("AcceptException:" + se.Message);
            }
        }

        //Socket发送信息回调
        private static void SendCallback(IAsyncResult ar)
        {
            try
            {
                Socket? clientSocket = ar.AsyncState as Socket;
                if (clientSocket != null) clientSocket.EndSend(ar);
            }
            catch (SocketException se)
            {
                Console.WriteLine("SendException:" + se.Message);
            }
        }

        //Socket接收信息回调
        private static void ReceiveCallback(IAsyncResult ar)
        {
            try
            {
                Socket? clientSocket = ar.AsyncState as Socket;
                if (clientSocket != null)
                {
                    int bytesCount = clientSocket.EndReceive(ar);
                    ClientDataPacket? dataPacket = DataPacket.ToObject<ClientDataPacket>(buffer.Take(bytesCount).ToArray());
                    if (dataPacket != null)
                    {
                        string v_content = $"客户端{dataPacket.mClientName}:{dataPacket.mContent}";
                        SendToAll(v_content);
                    }
                    if (IsConnected(clientSocket))
                        clientSocket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallback, clientSocket);
                }
            }
            catch (SocketException se)
            {
                Console.WriteLine("ReceiveException:" + se.Message);
            }
        }
    }
}

(五)测试

       测试流程大概是先启动服务器,然后启动三个客户端,三个客户端分别以A、B、C的名称作为客户端名称与服务器建立连接,连接后再由客户端A、B、C分别向服务器发送信息,通过观察三个客户端的消息面板来确定测试结果,具体测试流程请观看下列视频:

本地服务器多客户端通信

(六)总结

       在服务器端,我们通过一个C#控制台项目来模拟服务器后台,服务器与客户端具有类似的功能,同样具有发送、接收消息的功能,不同的是服务器具有监听客户端连接的功能,而客户端具有向服务器发送连接请求的功能,本质上这些都是通过Socket实现的功能,人为划分成服务器端和客户端。在客户端我们通过GUI将用户的操作进行可视化构建,实现了回显、客户端名称选择、连接、断开连接、发送和显示消息等基本交互。

       为了模拟多客户端并发操作,所有功能我们都采用了异步的方式启动,对于真正的网络通信而言,这对我们来说才刚刚开始,不过通过这个案例也让我们了解了基本的网络通信流程。

如果这篇文章对你有帮助,请给作者点个赞吧!

  • 11
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值