本项目使用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的聊天室项目,同学们有什么疑问可以在评论区留言。