跳转到主要内容

ChatClient

基于 WebSocket 的聊天客户端核心类。负责实际的服务器连接和消息处理。

事件代码

代码常量名说明
0EventChannels频道列表
1EventSubscribe频道订阅
2EventUnSubscribe频道取消订阅
3EventPublicMessage公开消息
4EventPrivateMessage私密消息
5EventPlayerOnline玩家在线状态
97EventNotifyMessage通知消息
99EventError错误

主要功能

方法说明
Connect连接到 WebSocket 服务器
Disconnect断开服务器连接
Subscribe订阅频道(可设置历史消息数量)
Unsubscribe取消订阅频道
GetChannels请求频道列表
GetPlayersOnline查询玩家在线状态
SendPublicMessage发送公开消息
SendPrivateMessage发送私密消息
SetUserName设置用户名
Service主线程回调处理

属性

属性类型说明
IsConnectedbool连接状态
CurrentChannelstring当前订阅的频道

Unity C# 实现

using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;
using WebSocketSharp;

public class ChatClient
{
// Event Codes
private const byte EventChannels = 0;
private const byte EventSubscribe = 1;
private const byte EventUnSubscribe = 2;
private const byte EventPublicMessage = 3;
private const byte EventPrivateMessage = 4;
private const byte EventPlayerOnline = 5;
private const byte EventNotifyMessage = 97;
private const byte EventError = 99;

public bool IsConnected => _isConnected;
public string CurrentChannel => _currentChannel;

private readonly IChatListener _listener;
private WebSocket _webSocket;
private readonly Queue<Action> _mainThreadActions = new Queue<Action>();
private readonly string _userUniqueId;
private string _userName;
private string _currentChannel;
private bool _isConnected;

public ChatClient(IChatListener listener, string userName = "")
{
_listener = listener;
_userName = userName;
_userUniqueId = DataManager.Instance.UUID;
}

#region Public API

public void Connect(ChatServerItem server)
{
if (_isConnected) return;
string protocol = server.Secure == "Y" ? "wss" : "ws";
ConnectToServer(BuildServerUrl($"{protocol}://{server.Addr}:{server.Port}"));
}

public void Disconnect()
{
if (_webSocket == null) return;
_isConnected = false;
_webSocket.CloseAsync();
_webSocket = null;
}

public void Subscribe(string channel, int prevMessageCount = 0)
{
_currentChannel = channel;
Send(EventSubscribe, channel, null, null, prevMessageCount);
}

public void Unsubscribe(string channel) => Send(EventUnSubscribe, channel);

public void GetChannels() => Send(EventChannels, "");

public void GetPlayersOnline(string[] userIds)
{
if (_webSocket == null || !_webSocket.IsAlive)
{
_listener?.OnError("NOT_CONNECTED", "WebSocket is not connected");
return;
}

var msg = new ChatMessageModel
{
mid = Guid.NewGuid().ToString(),
type = EventPlayerOnline,
gameId = HttpClient.GetGameId,
serviceKey = HttpClient.GetServiceKey,
userUniqueId = _userUniqueId,
userName = _userName,
onlinePlayers = userIds,
ipAddr = GetLocalIP(),
createdAt = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss")
};

_webSocket.Send(JsonUtility.ToJson(msg));
}

public void SendPublicMessage(string channel, string text)
{
Send(EventPublicMessage, channel, text);
}

public void SendPrivateMessage(string targetUserId, string text) => Send(EventPrivateMessage, _currentChannel, text, targetUserId);

public void SetUserName(string userName) => _userName = userName;

public void Service()
{
lock (_mainThreadActions)
{
while (_mainThreadActions.Count > 0)
_mainThreadActions.Dequeue()?.Invoke();
}
}

#endregion

#region Server Connection

private string BuildServerUrl(string baseUrl)
{
string hash = ComputeHash();
return $"{baseUrl}?gameId={HttpClient.GetGameId}&uuid={_userUniqueId}&hash={UnityWebRequest.EscapeURL(hash)}";
}

private void ConnectToServer(string url)
{
try
{
_webSocket = new WebSocket(url);
_webSocket.OnOpen += (s, e) => { _isConnected = true; RunOnMainThread(() => _listener?.OnConnected()); };
_webSocket.OnMessage += (s, e) => RunOnMainThread(() => ProcessMessage(e.Data));
_webSocket.OnError += (s, e) => RunOnMainThread(() => _listener?.OnError("WEBSOCKET_ERROR", e.Message));
_webSocket.OnClose += (s, e) => { _isConnected = false; RunOnMainThread(() => _listener?.OnDisconnected()); };
_webSocket.ConnectAsync();
}
catch (Exception e)
{
_listener?.OnError("CONNECTION_FAILED", e.Message);
}
}

#endregion

#region Message Handling

private void Send(byte type, string channel, string message = null, string privateToUserId = null, int prevMessageCount = 0)
{
if (_webSocket == null || !_webSocket.IsAlive)
{
_listener?.OnError("NOT_CONNECTED", "WebSocket is not connected");
return;
}

var msg = new ChatMessageModel
{
mid = Guid.NewGuid().ToString(),
type = type,
gameId = HttpClient.GetGameId,
serviceKey = HttpClient.GetServiceKey,
channelId = channel,
userUniqueId = _userUniqueId,
userName = _userName,
message = message,
privateToUserUniqueId = privateToUserId,
prevMessageCount = prevMessageCount,
ipAddr = GetLocalIP(),
createdAt = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss")
};

_webSocket.Send(JsonUtility.ToJson(msg));
}

private void ProcessMessage(string json)
{
try
{
var msg = JsonUtility.FromJson<ChatMessageModel>(json);
var user = new ChatUserInfo { visitorId = msg.userUniqueId, visitorName = msg.userName };

switch (msg.type)
{
case EventChannels: _listener?.OnChannels(msg.channels); break;
case EventSubscribe: _listener?.OnSubscribed(user); break;
case EventUnSubscribe: _listener?.OnUnSubscribed(user); break;
case EventPublicMessage: _listener?.OnPublicMessage(user, msg.message); break;
case EventPrivateMessage: _listener?.OnPrivateMessage(user, msg.message); break;
case EventPlayerOnline: _listener?.OnPlayerOnline(msg.players); break;
case EventNotifyMessage: _listener?.OnNotifyMessage(user, msg.message); break;
case EventError: _listener?.OnError(msg.error.ToString(), msg.message); break;
}
}
catch (Exception e)
{
Debug.LogError($"[Chat] ProcessMessage error: {e.Message}");
}
}

private void RunOnMainThread(Action action)
{
lock (_mainThreadActions)
_mainThreadActions.Enqueue(action);
}

#endregion

#region Utilities

private string ComputeHash()
{
string message = $"{HttpClient.GetGameId}{HttpClient.GetServiceKey}{_userUniqueId}";
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(HttpClient.GetSecretKey)))
return Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(message)));
}

private string GetLocalIP()
{
try
{
foreach (var ip in System.Net.Dns.GetHostEntry(System.Net.Dns.GetHostName()).AddressList)
if (ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
return ip.ToString();
}
catch { }
return "0.0.0.0";
}

#endregion
}