본문으로 건너뛰기

ChatClient

WebSocket 기반 채팅 클라이언트 코어 클래스입니다. 실제 서버 연결 및 메시지 처리를 담당합니다.

이벤트 코드

코드상수명설명
0EventChannels채널 목록
1EventSubscribe채널 구독
2EventUnSubscribe채널 구독 해제
3EventPublicMessage공개 메시지
4EventPrivateMessage비공개 메시지
5EventPlayerOnline플레이어 온라인 상태
97EventNotifyMessage알림 메시지
99EventError에러

주요 기능

메서드설명
ConnectWebSocket 서버에 연결
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
}