Unity Socket TCP 开发聊天室

本项目使用Unity2020.3和VS2019平台开发一个类似QQ的聊天室项目

Server端

场景部署如下:

新建脚本命名为ServerScripts,挂载到ScriptsControl物体上,脚本如下:

/**
*项目名称:Socket异步聊天室项目
*
*功能:Socket学习--服务器端
*
*Date:2024/08/10
*
*Author:WangYadong
**/
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Text;
using System;

public class ServerScripts : MonoBehaviour
{
    public InputField InpIPAddress;                         //Ip地址
    public InputField InpPort;                              //端口号
    public InputField InpDisplayInfo;                       //显示的聊天内容
    public InputField InpSendMsg;                           //要发送的消息
    public Dropdown Dd_IPList;                              //客户端的IP列表(相当于QQ聊天中好友列表)

    private Socket _SocketServer;                           //服务端套接字
    private bool _IsListenConnection = true;                       //是否正在监听
    private StringBuilder _SbDisplayInfo = new StringBuilder();//追加信息
    private string _CurrentClientIPValues = string.Empty;   //当前选择的IP地址信息
    //保存“客户端通信的套接字”(相当于“QQ列表”)
    private Dictionary<string, Socket> _DicSocket = new Dictionary<string, Socket>();
    //保存DropDown中的数据,目的为了删除信息节点
    private Dictionary<string, Dropdown.OptionData> _DicDropdown = new Dictionary<string, Dropdown.OptionData>();

    // Start is called before the first frame update
    void Start()
    {
        //控件初始化
        InpIPAddress.text = "127.0.0.1";
        InpPort.text = "1000";
        InpSendMsg.text = string.Empty;

        //下拉列表清空处理
        Dd_IPList.options.Clear();  //清空上一次残留
        Dropdown.OptionData op = new Dropdown.OptionData();
        op.text = "";
        Dd_IPList.options.Add(op);
    }

    /// <summary>
    /// 退出系统
    /// </summary>
    public void ExitSys()
    {
        //退出会话Socket
        //if (string.IsNullOrEmpty(_CurrentClientIPValues))
        //{
        //    try
        //    {
        //        _DicSocket[_CurrentClientIPValues].Shutdown(SocketShutdown.Both);
        //        _DicSocket[_CurrentClientIPValues].Close();
        //    }
        //    catch (Exception)
        //    {

        //        throw;
        //    }
        //}

        //退出监听Socket
        if (_SocketServer != null)
        {
            try
            {
                //关闭连接
                _SocketServer.Shutdown(SocketShutdown.Both);
                //清理连接资源
                _SocketServer.Close();
            }
            catch (Exception)
            {
            }            
        }
        Application.Quit();
    }

    /// <summary>
    /// 获取“当前好友”
    /// </summary>
    public void GetCurrentSelectIpInfo()
    {
        //获取下拉列表中当前选择的IP值
        _CurrentClientIPValues = Dd_IPList.options[Dd_IPList.value].text;
    }

    /// <summary>
    /// 启动服务器端
    /// </summary>
    public void EnableServerReady()
    {
        //定义IP和端口号
        IPEndPoint endpoint = new IPEndPoint(IPAddress.Parse(InpIPAddress.text), Convert.ToInt32(InpPort.text));
        //定义监听Socket
        _SocketServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        //邦定
        _SocketServer.Bind(endpoint);
        //开始监听
        _SocketServer.Listen(10);
        DisplayMsg("服务器端启动...");

        //开启后台线程(监听客户端连接),因为Unity本身就有一个主线程,所以没必要开前台线程
        Thread thClientCon = new Thread(ListenClientCon);  //Thread默认开启的就是前台线程
        thClientCon.IsBackground = true;
        thClientCon.Name = "thListenClientCon";
        thClientCon.Start();
    }

    /// <summary>
    /// (后台线程)监听客户端连接
    /// </summary>
    private void ListenClientCon()
    {
        Socket sockMsgServer = null;
        try
        {
            while (_IsListenConnection)
            {
                //等待接收客户端连接,这里会产生阻塞,所以需要另起线程
                sockMsgServer = _SocketServer.Accept();
                //获取远程(客户端)节点信息(IP,Port)
                string strClientIPAndPort = sockMsgServer.RemoteEndPoint.ToString();

                //把“远程节点”信息,添加到DropDown控件中
                Dropdown.OptionData op = new Dropdown.OptionData();
                op.text = strClientIPAndPort;
                Dd_IPList.options.Add(op);

                //把DropDown控件添加到控件字典中
                _DicDropdown.Add(strClientIPAndPort, op);

                //把会话Socket添加到字典集合中(为了后面发送信息使用)
                _DicSocket.Add(strClientIPAndPort, sockMsgServer);

                //控件显示,有客户端连接
                DisplayMsg("有客户端连接...");

                //开启后台线程,接收客户端会话消息
                Thread thClientMsg = new Thread(ReceiveMsg);
                thClientMsg.IsBackground = true; //设置为后台线程
                thClientMsg.Name = "thSocketMsg";
                thClientMsg.Start(sockMsgServer);

            }
        }
        catch (Exception)
        {
            _IsListenConnection = false;
            //关闭会话Socket
            if (sockMsgServer != null)
            {
                sockMsgServer.Shutdown(SocketShutdown.Both); //关闭连接,发送和接收都不再进行
                sockMsgServer.Close(); //清理资源
            }
            //关闭监听Socket
            if (_SocketServer != null)
            {
                _SocketServer.Shutdown(SocketShutdown.Both);
                _SocketServer.Close();
            }
        }

    }

    /// <summary>
    /// (后台线程)接收客户端会话消息
    /// </summary>
    /// <param name="sockMsg"></param>
    private void ReceiveMsg(object sockMsg)
    {
        Socket clientSocketMsg = sockMsg as Socket;
        try
        {
            while (true)
            {
                //定义一个1M的缓存空间
                byte[] byteArray = new byte[1024 * 1024];
                //接收客户端发来的套接字消息
                int trueLengthMsg = clientSocketMsg.Receive(byteArray); //返回接收到消息的真实长度
                                                                        //转为string类型
                string strMsg = Encoding.UTF8.GetString(byteArray, 0, trueLengthMsg);
                //显示消息
                DisplayMsg("客户端消息: " + strMsg);
            }
        }
        catch (Exception)
        {
        }
        finally
        {
            DisplayMsg("有客户端断开连接了: " + clientSocketMsg.RemoteEndPoint.ToString());
            //字典中移除断开连接的客户端信息
            _DicSocket.Remove(clientSocketMsg.RemoteEndPoint.ToString());
            //客户端列表中移除
            if (_DicDropdown.ContainsKey(clientSocketMsg.RemoteEndPoint.ToString()))
            {
                Dd_IPList.options.Remove(_DicDropdown[clientSocketMsg.RemoteEndPoint.ToString()]);
            }
            //清理连接资源
            clientSocketMsg.Shutdown(SocketShutdown.Both);
            //关闭Socket
            clientSocketMsg.Close();
        }

    }

    /// <summary>
    /// 向客户端发送消息    
    /// </summary>
    public void SendMsg()
    {
        //参数检查
        _CurrentClientIPValues = _CurrentClientIPValues.Trim();
        if (string.IsNullOrEmpty(_CurrentClientIPValues))
        {
            DisplayMsg("请选择要聊天的用户名称");
            return;
        }

        //判断服务器端是否存在指定的客户端IP和Socket
        if (_DicSocket.ContainsKey(_CurrentClientIPValues))
        {
            //获取到发送的信息
            string strSendMsg = InpSendMsg.text;
            strSendMsg = strSendMsg.Trim();
            if (!string.IsNullOrEmpty(strSendMsg))
            {
                byte[] byteMsg = Encoding.UTF8.GetBytes(strSendMsg);
                Socket clientMsgSocket = _DicSocket[_CurrentClientIPValues]; //用指定的客户端Socket向客户端发消息
                clientMsgSocket.Send(byteMsg);
                //记录发送的数据
                DisplayMsg("已发送: " + strSendMsg);
                //控件置空
                InpSendMsg.text = string.Empty;
            }
            else
            {
                DisplayMsg("发送的信息不能为控!");
            }
        }
        else
        {
            DisplayMsg("请选择合法的聊天用户,请重新选择!");
        }
    }

    /// <summary>
    /// 控件显示消息
    /// </summary>
    /// <param name="str">需要显示的消息</param>
    private void DisplayMsg(string str)
    {
        Loom.QueueOnMainThread((param) => {
            str = str.Trim();
            if (!string.IsNullOrEmpty(str))
            {
                _SbDisplayInfo.Append(System.DateTime.Now.ToString());
                _SbDisplayInfo.Append("   ");
                _SbDisplayInfo.Append(str);
                _SbDisplayInfo.Append("\r\n");
                InpDisplayInfo.text = _SbDisplayInfo.ToString();
            }
        },null);              
    }
    
}

下面对代码中方法逐一讲解:

Start():该函数中进行控件初始化和下拉列表清空

ExitSys():该函数退出Socket连接,断开并释放连接资源 

GetCurrentSelectIpInfo():该函数作用选择下拉好友(IP地址及端口号)列表 

EnableServerReady():该函数是启动服务器,做Socket初始化及监听作用,同时开启了一个后台线程用来监听客户端的连接 (这里必须另起线程,因为这里是实时监听,会阻塞)

ListenClientCon():监听客户端连接(后台线程),①、使用while死循环进行实时监听;②、将连接到的客户端信息(IP+端口号)添加到下拉列表中;③、再开启一个后台线程用来接收客户端发来的消息

SendMsg():该方法是向客户端发送消息 

DisplayMsg():该函数是用来在控件中显示聊天信息,使用StringBuilder进行消息组合连接

重要的一点:

这里引入了一个Loom.cs脚本,Unity中另起线程,在该线程里不能使用Unity 中自带的函数、控件,而 DisplayMsg() 函数在新启的线程中被调用了,该函数中涉及到了Unity中控件的调用 ,所以Loom.cs脚本就可以解决这个难题, Loom.QueueOnMainThread((param) => {(param)=>{这里写你自己的逻辑内容},null}。

该脚本网上到处都是,已是开源代码,同学们可以自己去下载,下面我贴上源码:

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

public class Loom : MonoBehaviour
{
    public static int maxThreads = 8;
    static int numThreads;

    private static Loom _current;
    //private int _count;
    public static Loom Current
    {
        get
        {
            Initialize();
            return _current;
        }
    }

    //用于初始化一次,在程序入口调用一次
    public void StartUp()
    { }

    static bool initialized;

    public static void Initialize()
    {
        if (!initialized)
        {

            if (!Application.isPlaying)
                return;
            initialized = true;
            var g = new GameObject("Loom");
            _current = g.AddComponent<Loom>();
#if !ARTIST_BUILD
            UnityEngine.Object.DontDestroyOnLoad(g);
#endif
        }

    }
    public struct NoDelayedQueueItem
    {
        public Action<object> action;
        public object param;
    }

    private List<NoDelayedQueueItem> _actions = new List<NoDelayedQueueItem>();
    public struct DelayedQueueItem
    {
        public float time;
        public Action<object> action;
        public object param;
    }
    private List<DelayedQueueItem> _delayed = new List<DelayedQueueItem>();

    List<DelayedQueueItem> _currentDelayed = new List<DelayedQueueItem>();

    public static void QueueOnMainThread(Action<object> taction, object tparam)
    {
        QueueOnMainThread(taction, tparam, 0f);
    }
    public static void QueueOnMainThread(Action<object> taction, object tparam, float time)
    {
        if (time != 0)
        {
            lock (Current._delayed)
            {
                Current._delayed.Add(new DelayedQueueItem { time = Time.time + time, action = taction, param = tparam });
            }
        }
        else
        {
            lock (Current._actions)
            {
                Current._actions.Add(new NoDelayedQueueItem { action = taction, param = tparam });
            }
        }
    }

    public static Thread RunAsync(Action a)
    {
        Initialize();
        while (numThreads >= maxThreads)
        {
            Thread.Sleep(100);
        }
        Interlocked.Increment(ref numThreads);
        ThreadPool.QueueUserWorkItem(RunAction, a);
        return null;
    }

    private static void RunAction(object action)
    {
        try
        {
            ((Action)action)();
        }
        catch
        {
        }
        finally
        {
            Interlocked.Decrement(ref numThreads);
        }

    }


    void OnDisable()
    {
        if (_current == this)
        {

            _current = null;
        }
    }



    // Use this for initialization
    void Start()
    {

    }

    List<NoDelayedQueueItem> _currentActions = new List<NoDelayedQueueItem>();

    // Update is called once per frame
    void Update()
    {
        if (_actions.Count > 0)
        {
            lock (_actions)
            {
                _currentActions.Clear();
                _currentActions.AddRange(_actions);
                _actions.Clear();
            }
            for (int i = 0; i < _currentActions.Count; i++)
            {
                _currentActions[i].action(_currentActions[i].param);
            }
        }

        if (_delayed.Count > 0)
        {
            lock (_delayed)
            {
                _currentDelayed.Clear();
                _currentDelayed.AddRange(_delayed.Where(d => d.time <= Time.time));
                for (int i = 0; i < _currentDelayed.Count; i++)
                {
                    _delayed.Remove(_currentDelayed[i]);
                }
            }

            for (int i = 0; i < _currentDelayed.Count; i++)
            {
                _currentDelayed[i].action(_currentDelayed[i].param);
            }
        }
    }
}

 在场景中新建一个空物体,将该脚本挂载在该物体上即可,如下图所示:

Client 端

场景部署如下:

新建脚本ClientScripts.cs,并挂载在ScriptsControl物体上,如上图所示:

/**
*项目名称:Socket异步聊天室项目
*
*功能:Socket学习--客户端
*
*Date:2024/08/10
*
*Author:WangYadong
**/
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System.Threading;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System;

public class ClientScripts : MonoBehaviour
{
    public InputField InpIPAddress;                         //Ip地址
    public InputField InpPort;                              //端口号
    public InputField InpDisplayInfo;                       //显示的聊天内容
    public InputField InpSendMsg;                           //要发送的消息

    private Socket _SocketClient;                           //客户端套接字
    private IPEndPoint endPoint;
    private bool _isSendDataConnection = true;              //发送数据
    private StringBuilder _SbDisplayInfo = new StringBuilder();//追加信息
    // Start is called before the first frame update
    void Start()
    {
        InpIPAddress.text = "127.0.0.1";
        InpPort.text = "1000";
    }

    /// <summary>
    /// 退出系统
    /// </summary>
    public void ExitSystem()
    {
        if (_SocketClient != null)
        {
            try
            {
                _SocketClient.Shutdown(SocketShutdown.Both);
            }
            catch (Exception)
            {
            }
            _SocketClient.Close();
        }
        //退出系统
        Application.Quit();
    }

    /// <summary>
    /// 启动客户端连接
    /// </summary>
    public void EnableClientCon()
    {
        //通讯IP地址和端口号
        endPoint = new IPEndPoint(IPAddress.Parse(InpIPAddress.text), Convert.ToInt32(InpPort.text));
        //定义客户端Socket
        _SocketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        try
        {
            //建立连接
            _SocketClient.Connect(endPoint);
            //监听服务器端发来的信息
            Thread thListenMsgFromServer = new Thread(ListenMsgInfoFromServer);
            thListenMsgFromServer.IsBackground = true;
            thListenMsgFromServer.Name = "thListenMsgFromServer";
            thListenMsgFromServer.Start();


        }
        catch (Exception)
        {
        }
        //显示客户端连接成功
        DisplayMsg("连接服务器成功!");

    }

    /// <summary>
    /// (后台线程)监听服务器发来的信息
    /// </summary>
    private void ListenMsgInfoFromServer()
    {
        try
        {
            while (true)
            {
                //开辟一个1M的内存空间
                byte[] byteArray = new byte[1024 * 1024];
                //接收服务器端返回的数据
                int trueMsgLength = _SocketClient.Receive(byteArray);
                //转字符串
                string strMsg = Encoding.UTF8.GetString(byteArray, 0, trueMsgLength);
                //显示接收的消息
                if (!string.IsNullOrEmpty(strMsg))
                    DisplayMsg("服务器返回的消息: " + strMsg);

            }
        }
        catch (Exception)
        {
            DisplayMsg("服务器断开连接!");
            //关闭连接
            _SocketClient.Disconnect(false);
            //清理连接
            _SocketClient.Close();
        }

    }


    /// <summary>
    /// 客户端发送数据到服务器端
    /// </summary>
    public void SendMsg()
    {
        string strSendMsg = InpSendMsg.text;
        if (!string.IsNullOrEmpty(strSendMsg))
        {
            //字节转换
            byte[] byteArray = System.Text.Encoding.UTF8.GetBytes(strSendMsg);
            //发送
            _SocketClient.Send(byteArray);
            //显示发送的内容
            DisplayMsg("我:" + strSendMsg);

            //控件清空
            InpSendMsg.text = string.Empty;
        }
        else
        {
            DisplayMsg("提示:发送的数据不能为空!,请输入发送信息");
        }
    }

    /// <summary>
    /// 控件显示消息
    /// </summary>
    /// <param name="str">需要显示的消息</param>
    private void DisplayMsg(string str)
    {
        Loom.QueueOnMainThread((param) =>
        {
            str = str.Trim();
            if (!string.IsNullOrEmpty(str))
            {
                _SbDisplayInfo.Append(System.DateTime.Now.ToString());
                _SbDisplayInfo.Append("   ");
                _SbDisplayInfo.Append(str);
                _SbDisplayInfo.Append("\r\n");
                InpDisplayInfo.text = _SbDisplayInfo.ToString();
            }
        }, null);
    }
}

下面对代码进行逐一讲解:

Start():该函数中对IP和端口号进行赋值,127.0.0.1是指本机,即本机自己是客户端,上面服务端也是127.0.0.1也是自己,本机自己即是服务端也是客户端

ExitSystem():该函数是断开Socket连接,断开并释放连接资源

EnableClientCon():该函数  ①、Socket初始化并与服务端建立连接   ②、另起一个后台线程监听服务端发过来的消息

ListenMsgInfoFromServer():监听服务端发过来的消息(后台线程) 

SendMsg():客户端向服务端发送消息 

DisplayMsg():在控件中显示聊天记录

同样的这里引用了Loom脚本,因为该函数中在另起的线程中被调用了,而该函数中涉及到了Unity控件的调用 。

以上就是聊天室项目的服务端和客户端全部代码,下面进行发布测试。

发布测试 

将该项目发布成两个包:

客户端包:

服务端包:

同时起两个包进行通信,如下:

两个包同时启动后,第一步先点击服务端的 Start 按钮,启动服务端的Socket;第二步再点击客户端的Connection按钮,启动客户端的Socket去连接服务端。

然后就可以愉快的聊天了:

注意:客户端连接成功后,服务端要发送消息前,要先选中要聊天的客户端对象,该项目是一对多的(一个服务器可以有多个客户端连接)

以上就是在前两章节的基础上开发的类似QQ的聊天室项目,同学们有什么疑问可以在评论区留言。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值