Skip to main content

ChatClient

WebSocket-based chat client core class. Handles actual server connection and message processing.

Event Codes

CodeConstant NameDescription
0EventChannelsChannel list
1EventSubscribeChannel subscription
2EventUnSubscribeChannel unsubscription
3EventPublicMessagePublic message
4EventPrivateMessagePrivate message
5EventPlayerOnlinePlayer online status
97EventNotifyMessageNotification message
99EventErrorError

Key Features

MethodDescription
ConnectConnect to WebSocket server
DisconnectDisconnect from server
SubscribeSubscribe to channel (with previous message count option)
UnsubscribeUnsubscribe from channel
GetChannelsRequest channel list
GetPlayersOnlineQuery player online status
SendPublicMessageSend public message
SendPrivateMessageSend private message
SetUserNameSet username
ServiceProcess main thread callbacks

Properties

PropertyTypeDescription
IsConnectedboolConnection status
CurrentChannelstringCurrently subscribed channel

Unity C# Implementation

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
}