add chatserver & client

This commit is contained in:
backuppc
2025-11-12 13:57:13 +09:00
parent e19b404580
commit d977c9da83
26 changed files with 2662 additions and 118 deletions

Submodule SubProject/AmkorRestfulService deleted from cd4e1379bc

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
<section name="ChatServer.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" />
</sectionGroup>
</configSections>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
</startup>
<appSettings>
<!-- TCP 채팅 서버 포트 -->
<add key="TcpPort" value="5000" />
<!-- 최대 동시 접속자 수 -->
<add key="MaxClients" value="100" />
</appSettings>
<userSettings>
<ChatServer.Properties.Settings>
<setting name="TcpPort" serializeAs="String">
<value>5000</value>
</setting>
<setting name="MaxClients" serializeAs="String">
<value>1000</value>
</setting>
</ChatServer.Properties.Settings>
</userSettings>
</configuration>

View File

@@ -0,0 +1,174 @@
using System;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using Newtonsoft.Json;
namespace NoticeServer
{
/// <summary>
/// 연결된 채팅 클라이언트 정보 및 통신 관리
/// </summary>
public class ChatClient
{
private TcpClient tcpClient;
private NetworkStream stream;
private Thread receiveThread;
private bool isConnected;
/// <summary>클라이언트 고유 ID</summary>
public string ClientId { get; private set; }
/// <summary>닉네임</summary>
public string NickName { get; set; }
/// <summary>사원번호 (Employee ID)</summary>
public string EmployeeId { get; set; }
/// <summary>사용자 그룹</summary>
public string UserGroup { get; set; }
/// <summary>IP 주소</summary>
public string IpAddress { get; private set; }
/// <summary>호스트명</summary>
public string HostName { get; set; }
/// <summary>연결 시간</summary>
public DateTime ConnectedTime { get; private set; }
/// <summary>마지막 활동 시간</summary>
public DateTime LastActivity { get; set; }
/// <summary>메시지 수신 이벤트</summary>
public event EventHandler<ChatMessage> MessageReceived;
/// <summary>연결 해제 이벤트</summary>
public event EventHandler<string> Disconnected;
public ChatClient(TcpClient client)
{
tcpClient = client;
stream = client.GetStream();
isConnected = true;
ClientId = Guid.NewGuid().ToString();
ConnectedTime = DateTime.Now;
LastActivity = DateTime.Now;
var endpoint = client.Client.RemoteEndPoint.ToString();
IpAddress = endpoint.Split(':')[0];
StartReceiving();
}
/// <summary>
/// 메시지 수신 스레드 시작
/// </summary>
private void StartReceiving()
{
receiveThread = new Thread(ReceiveLoop)
{
IsBackground = true,
Name = $"ChatClient-{ClientId}"
};
receiveThread.Start();
}
/// <summary>
/// 메시지 수신 루프
/// </summary>
private void ReceiveLoop()
{
byte[] buffer = new byte[4096];
try
{
while (isConnected && tcpClient.Connected)
{
int bytesRead = stream.Read(buffer, 0, buffer.Length);
if (bytesRead == 0)
{
// Connection closed
break;
}
string jsonData = Encoding.UTF8.GetString(buffer, 0, bytesRead);
var message = JsonConvert.DeserializeObject<ChatMessage>(jsonData);
if (message != null)
{
LastActivity = DateTime.Now;
// Set nickname, employeeId, userGroup from Connect message
if (message.Type == MessageType.Connect && !string.IsNullOrEmpty(message.NickName))
{
NickName = message.NickName;
EmployeeId = message.EmployeeId;
UserGroup = message.UserGroup;
HostName = message.HostName;
}
MessageReceived?.Invoke(this, message);
}
}
}
catch (Exception ex)
{
Console.WriteLine($"[ERROR] Client {NickName ?? ClientId} receive error: {ex.Message}");
}
finally
{
Disconnect();
}
}
/// <summary>
/// Send message
/// </summary>
public bool SendMessage(ChatMessage message)
{
if (!isConnected || !tcpClient.Connected)
return false;
try
{
string jsonData = JsonConvert.SerializeObject(message);
byte[] data = Encoding.UTF8.GetBytes(jsonData);
stream.Write(data, 0, data.Length);
stream.Flush();
return true;
}
catch (Exception ex)
{
Console.WriteLine($"[ERROR] Client {NickName ?? ClientId} send error: {ex.Message}");
Disconnect();
return false;
}
}
/// <summary>
/// 연결 해제
/// </summary>
public void Disconnect()
{
if (!isConnected)
return;
isConnected = false;
try
{
stream?.Close();
tcpClient?.Close();
}
catch { }
Disconnected?.Invoke(this, ClientId);
}
public override string ToString()
{
return $"{NickName ?? "Unknown"} ({IpAddress})";
}
}
}

View File

@@ -0,0 +1,76 @@
using System;
namespace NoticeServer
{
/// <summary>
/// 채팅 메시지 타입
/// </summary>
public enum MessageType
{
/// <summary>클라이언트 연결</summary>
Connect,
/// <summary>클라이언트 연결 해제</summary>
Disconnect,
/// <summary>일반 채팅 메시지</summary>
Chat,
/// <summary>서버 공지</summary>
Notice,
/// <summary>귓속말</summary>
Whisper,
/// <summary>사용자 목록 요청</summary>
UserListRequest,
/// <summary>사용자 목록 응답</summary>
UserListResponse,
/// <summary>핑 (연결 유지)</summary>
Ping,
/// <summary>퐁 (핑 응답)</summary>
Pong
}
/// <summary>
/// 채팅 메시지 프로토콜
/// </summary>
[Serializable]
public class ChatMessage
{
/// <summary>메시지 타입</summary>
public MessageType Type { get; set; }
/// <summary>발신자 닉네임</summary>
public string NickName { get; set; }
/// <summary>발신자 사원번호 (Employee ID)</summary>
public string EmployeeId { get; set; }
/// <summary>발신자 IP</summary>
public string IpAddress { get; set; }
/// <summary>발신자 호스트명</summary>
public string HostName { get; set; }
/// <summary>메시지 내용</summary>
public string Content { get; set; }
/// <summary>수신자 사원번호 (1:1 채팅용)</summary>
public string TargetEmployeeId { get; set; }
/// <summary>수신자 닉네임 (귓속말용, null이면 전체)</summary>
public string TargetNickName { get; set; }
/// <summary>전송 시간</summary>
public DateTime Timestamp { get; set; }
/// <summary>사용자 그룹</summary>
public string UserGroup { get; set; }
public ChatMessage()
{
Timestamp = DateTime.Now;
}
public override string ToString()
{
return $"[{Timestamp:HH:mm:ss}] {NickName}: {Content}";
}
}
}

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{8E9A4B1C-6D5F-4E2A-9F3B-1C8D7E6A5B4F}</ProjectGuid>
<OutputType>Exe</OutputType>
<RootNamespace>NoticeServer</RootNamespace>
<AssemblyName>NoticeServer</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>x86</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.ServiceProcess" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Program.cs" />
<Compile Include="NoticeService.cs">
<SubType>Component</SubType>
</Compile>
<Compile Include="Properties\Settings.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
<DependentUpon>Settings.settings</DependentUpon>
</Compile>
<Compile Include="TcpChatServer.cs" />
<Compile Include="ChatClient.cs" />
<Compile Include="ChatMessage.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
<None Include="packages.config" />
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@@ -0,0 +1,116 @@
using System;
using System.ServiceProcess;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;
namespace NoticeServer
{
public partial class NoticeService : ServiceBase
{
private CancellationTokenSource _cancellationTokenSource;
private Task _serviceTask;
private static TcpChatServer _tcpServer;
public NoticeService()
{
InitializeComponent();
ServiceName = "EETGWNoticeService";
CanStop = true;
CanPauseAndContinue = false;
AutoLog = true;
}
protected override void OnStart(string[] args)
{
try
{
EventLog.WriteEntry(ServiceName, "Notice 서비스가 시작됩니다.", EventLogEntryType.Information);
_cancellationTokenSource = new CancellationTokenSource();
_serviceTask = Task.Run(() => DoWork(_cancellationTokenSource.Token));
EventLog.WriteEntry(ServiceName, "Notice 서비스가 성공적으로 시작되었습니다.", EventLogEntryType.Information);
}
catch (Exception ex)
{
EventLog.WriteEntry(ServiceName, $"서비스 시작 중 오류 발생: {ex.Message}", EventLogEntryType.Error);
throw;
}
}
protected override void OnStop()
{
try
{
EventLog.WriteEntry(ServiceName, "Notice 서비스를 중지합니다.", EventLogEntryType.Information);
_cancellationTokenSource?.Cancel();
// Stop server
_tcpServer?.Stop();
if (_serviceTask != null)
{
// 최대 30초 대기
if (!_serviceTask.Wait(TimeSpan.FromSeconds(30)))
{
EventLog.WriteEntry(ServiceName, "서비스 중지 시간 초과", EventLogEntryType.Warning);
}
}
EventLog.WriteEntry(ServiceName, "Notice 서비스가 중지되었습니다.", EventLogEntryType.Information);
}
catch (Exception ex)
{
EventLog.WriteEntry(ServiceName, $"서비스 중지 중 오류 발생: {ex.Message}", EventLogEntryType.Error);
}
finally
{
_cancellationTokenSource?.Dispose();
_serviceTask?.Dispose();
}
}
private void DoWork(CancellationToken cancellationToken)
{
EventLog.WriteEntry(ServiceName, "Notice 서비스 작업을 시작합니다.", EventLogEntryType.Information);
try
{
// Read settings
int tcpPort = Properties.Settings.Default.TcpPort;
int maxClients = Properties.Settings.Default.MaxClients;
// Start server
_tcpServer = new TcpChatServer(tcpPort, maxClients);
_tcpServer.Start();
EventLog.WriteEntry(ServiceName, $"Notice 서버가 포트 {tcpPort}에서 시작되었습니다.", EventLogEntryType.Information);
// Wait for cancellation
while (!cancellationToken.IsCancellationRequested)
{
if (cancellationToken.WaitHandle.WaitOne(1000))
{
break; // 취소 요청 시 종료
}
}
}
catch (Exception ex)
{
EventLog.WriteEntry(ServiceName, $"서버 실행 중 오류 발생: {ex.Message}\n{ex.StackTrace}", EventLogEntryType.Error);
}
EventLog.WriteEntry(ServiceName, "Notice 서비스 작업이 종료되었습니다.", EventLogEntryType.Information);
}
private void InitializeComponent()
{
//
// NoticeService
//
this.ServiceName = "EETGWNoticeService";
}
}
}

View File

@@ -0,0 +1,288 @@
using System;
using System.Configuration;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.ServiceProcess;
using System.Threading;
namespace NoticeServer
{
partial class Program
{
static TcpChatServer tcpServer;
static bool isRunning = true;
static void Main(string[] args)
{
// 명령행 인수 처리
if (args.Length > 0)
{
string command = args[0].ToLower();
switch (command)
{
case "-install":
case "/install":
InstallService();
return;
case "-uninstall":
case "/uninstall":
UninstallService();
return;
case "-console":
case "/console":
RunAsConsole();
return;
case "-help":
case "/help":
case "/?":
ShowHelp();
return;
}
}
// 서비스로 실행
if (Environment.UserInteractive)
{
// 대화형 모드에서는 콘솔로 실행
RunAsConsole();
}
else
{
// 서비스로 실행
ServiceBase[] ServicesToRun;
ServicesToRun = new ServiceBase[]
{
new NoticeService()
};
ServiceBase.Run(ServicesToRun);
}
}
private static void ShowHelp()
{
Console.WriteLine("EETGW Notice Server (Chat Server)");
Console.WriteLine("사용법:");
Console.WriteLine(" NoticeServer.exe - 서비스로 실행 (또는 대화형 모드에서 콘솔 실행)");
Console.WriteLine(" NoticeServer.exe -console - 콘솔 모드로 실행");
Console.WriteLine(" NoticeServer.exe -install - 서비스 설치");
Console.WriteLine(" NoticeServer.exe -uninstall - 서비스 제거");
Console.WriteLine(" NoticeServer.exe -help - 도움말 표시");
}
private static void InstallService()
{
try
{
string servicePath = $"\"{System.Reflection.Assembly.GetExecutingAssembly().Location}\"";
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "sc.exe",
Arguments = $"create EETGWNoticeService binPath= \"{servicePath}\" DisplayName= \"EETGW Notice Service\" start= auto",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using (Process process = Process.Start(startInfo))
{
process.WaitForExit();
if (process.ExitCode == 0)
{
Console.WriteLine("서비스가 성공적으로 설치되었습니다.");
Console.WriteLine("서비스를 시작하려면 다음 명령을 실행하세요:");
Console.WriteLine("net start EETGWNoticeService");
}
else
{
string error = process.StandardError.ReadToEnd();
Console.WriteLine($"서비스 설치 실패: {error}");
}
}
}
catch (Exception ex)
{
Console.WriteLine($"서비스 설치 중 오류 발생: {ex.Message}");
}
}
private static void UninstallService()
{
try
{
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "sc.exe",
Arguments = "delete EETGWNoticeService",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using (Process process = Process.Start(startInfo))
{
process.WaitForExit();
if (process.ExitCode == 0)
{
Console.WriteLine("서비스가 성공적으로 제거되었습니다.");
}
else
{
string error = process.StandardError.ReadToEnd();
Console.WriteLine($"서비스 제거 실패: {error}");
}
}
}
catch (Exception ex)
{
Console.WriteLine($"서비스 제거 중 오류 발생: {ex.Message}");
}
}
private static void RunAsConsole()
{
// 중복실행 방지 체크
if (!CheckSingleInstance())
{
return; // 프로그램 종료
}
Console.OutputEncoding = System.Text.Encoding.UTF8;
Console.Title = "EETGW Notice Server (Chat Server)";
Console.WriteLine("========================================");
Console.WriteLine(" EETGW Notice Server v1.0 (콘솔 모드)");
Console.WriteLine("========================================\n");
// Read settings
int tcpPort = Properties.Settings.Default.TcpPort;
int maxClients = Properties.Settings.Default.MaxClients;
// Get local IP address
string localIP = GetLocalIPAddress();
string hostName = Dns.GetHostName();
Console.WriteLine($"Server IP: {localIP}");
Console.WriteLine($"Host Name: {hostName}");
Console.WriteLine($"TCP Port: {tcpPort}");
Console.WriteLine($"Max Clients: {maxClients}\n");
// Start server
tcpServer = new TcpChatServer(tcpPort, maxClients);
tcpServer.Start();
Console.WriteLine("\nServer started successfully.");
Console.WriteLine("Commands: status, clear, quit, help");
Console.WriteLine("종료하려면 Ctrl+C를 누르거나 'quit'를 입력하세요.\n");
// Ctrl+C handler
Console.CancelKeyPress += (sender, e) =>
{
Console.WriteLine("\n서버를 종료합니다...");
e.Cancel = true;
isRunning = false;
};
// Command processing loop
while (isRunning)
{
string command = Console.ReadLine()?.Trim().ToLower();
switch (command)
{
case "status":
case "s":
tcpServer.PrintStatus();
break;
case "clear":
case "cls":
Console.Clear();
Console.WriteLine("EETGW Notice Server - Commands: status, clear, quit\n");
break;
case "quit":
case "exit":
case "q":
isRunning = false;
break;
case "help":
case "h":
case "?":
PrintHelp();
break;
case "":
break;
default:
Console.WriteLine($"Unknown command: {command}");
Console.WriteLine("Available commands: status, clear, quit, help");
break;
}
Thread.Sleep(100);
}
// Server shutdown
Console.WriteLine("\nShutting down server...");
tcpServer.Stop();
Console.WriteLine("Server stopped.");
Thread.Sleep(1000);
}
/// <summary>
/// 중복실행 방지 체크
/// </summary>
/// <returns>단일 인스턴스인 경우 true, 중복실행인 경우 false</returns>
static bool CheckSingleInstance()
{
string processName = Process.GetCurrentProcess().ProcessName;
Process[] processes = Process.GetProcessesByName(processName);
if (processes.Length > 1)
{
// 중복실행 감지
string message = $"⚠️ 프로그램이 이미 실행 중입니다!\n\n" +
"동시에 여러 개의 프로그램을 실행할 수 없습니다.\n";
Console.WriteLine(message);
return false;
}
return true; // 단일 인스턴스
}
static string GetLocalIPAddress()
{
try
{
var host = Dns.GetHostEntry(Dns.GetHostName());
var ipAddress = host.AddressList
.FirstOrDefault(ip => ip.AddressFamily == AddressFamily.InterNetwork);
if (ipAddress != null)
return ipAddress.ToString();
}
catch { }
return "127.0.0.1";
}
static void PrintHelp()
{
Console.WriteLine("\n========================================");
Console.WriteLine("Available Commands:");
Console.WriteLine("========================================");
Console.WriteLine("status (s) - Show server status and client list");
Console.WriteLine("clear (cls) - Clear screen");
Console.WriteLine("quit (q) - Stop server");
Console.WriteLine("help (h) - Show this help");
Console.WriteLine("========================================\n");
}
}
}

View File

@@ -0,0 +1,33 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// 어셈블리에 대한 일반 정보는 다음 특성 집합을 통해
// 제어됩니다. 어셈블리와 관련된 정보를 수정하려면
// 이러한 특성 값을 변경하세요.
[assembly: AssemblyTitle("ChatServer")]
[assembly: AssemblyDescription("GroupWare Chat Server - TCP/UDP 기반 다중 사용자 채팅 서버")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("ChatServer")]
[assembly: AssemblyCopyright("Copyright © 2025")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// ComVisible을 false로 설정하면 이 어셈블리의 형식이 COM 구성 요소에
// 표시되지 않습니다. COM에서 이 어셈블리의 형식에 액세스하려면
// 해당 형식에 대해 ComVisible 특성을 true로 설정하세요.
[assembly: ComVisible(false)]
// 이 프로젝트가 COM에 노출되는 경우 다음 GUID는 typelib의 ID를 나타냅니다.
[assembly: Guid("8e9a4b1c-6d5f-4e2a-9f3b-1c8d7e6a5b4f")]
// 어셈블리의 버전 정보는 다음 네 가지 값으로 구성됩니다.
//
// 주 버전
// 부 버전
// 빌드 번호
// 수정 버전
//
[assembly: AssemblyVersion("25.11.12.1300")]
[assembly: AssemblyFileVersion("25.11.12.1300")]

View File

@@ -0,0 +1,50 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 이 코드는 도구를 사용하여 생성되었습니다.
// 런타임 버전:4.0.30319.42000
//
// 파일 내용을 변경하면 잘못된 동작이 발생할 수 있으며, 코드를 다시 생성하면
// 이러한 변경 내용이 손실됩니다.
// </auto-generated>
//------------------------------------------------------------------------------
namespace ChatServer.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.9.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default {
get {
return defaultInstance;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("5000")]
public int TcpPort {
get {
return ((int)(this["TcpPort"]));
}
set {
this["TcpPort"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("1000")]
public int MaxClients {
get {
return ((int)(this["MaxClients"]));
}
set {
this["MaxClients"] = value;
}
}
}
}

View File

@@ -0,0 +1,12 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)" GeneratedClassNamespace="ChatServer.Properties" GeneratedClassName="Settings">
<Profiles />
<Settings>
<Setting Name="TcpPort" Type="System.Int32" Scope="User">
<Value Profile="(Default)">5000</Value>
</Setting>
<Setting Name="MaxClients" Type="System.Int32" Scope="User">
<Value Profile="(Default)">1000</Value>
</Setting>
</Settings>
</SettingsFile>

View File

@@ -0,0 +1,421 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace NoticeServer
{
/// <summary>
/// TCP 채팅 서버
/// </summary>
public class TcpChatServer
{
private TcpListener tcpListener;
private Thread acceptThread;
private bool isRunning;
private int tcpPort;
private int maxClients;
private readonly object clientsLock = new object();
private List<ChatClient> clients = new List<ChatClient>();
public TcpChatServer(int tcpPort, int maxClients = 100)
{
this.tcpPort = tcpPort;
this.maxClients = maxClients;
}
/// <summary>
/// TCP 채팅 서버 시작
/// </summary>
public void Start()
{
if (isRunning)
return;
try
{
tcpListener = new TcpListener(IPAddress.Any, tcpPort);
tcpListener.Start();
isRunning = true;
acceptThread = new Thread(AcceptLoop)
{
IsBackground = true,
Name = "TcpAcceptThread"
};
acceptThread.Start();
Console.WriteLine($"[TCP] Chat server started (Port: {tcpPort})");
Console.WriteLine($"[TCP] Max clients: {maxClients}");
}
catch (Exception ex)
{
Console.WriteLine($"[ERROR] Failed to start TCP server: {ex.Message}");
isRunning = false;
}
}
/// <summary>
/// 클라이언트 연결 수락 루프
/// </summary>
private void AcceptLoop()
{
while (isRunning)
{
try
{
TcpClient tcpClient = tcpListener.AcceptTcpClient();
lock (clientsLock)
{
if (clients.Count >= maxClients)
{
Console.WriteLine("[REJECTED] Maximum clients exceeded");
tcpClient.Close();
continue;
}
}
var client = new ChatClient(tcpClient);
client.MessageReceived += OnClientMessageReceived;
client.Disconnected += OnClientDisconnected;
lock (clientsLock)
{
clients.Add(client);
}
Console.WriteLine($"[CONNECTED] New client: {client.IpAddress} (ID: {client.ClientId})");
}
catch (SocketException)
{
// Server shutdown
break;
}
catch (Exception ex)
{
Console.WriteLine($"[ERROR] Client accept error: {ex.Message}");
}
}
}
/// <summary>
/// 클라이언트 메시지 수신 이벤트 핸들러
/// </summary>
private void OnClientMessageReceived(object sender, ChatMessage message)
{
var client = sender as ChatClient;
if (client == null)
return;
Console.WriteLine($"[RECEIVED] {client.NickName ?? client.IpAddress}: {message.Type} - {message.Content}");
switch (message.Type)
{
case MessageType.Connect:
HandleConnect(client, message);
break;
case MessageType.Chat:
HandleChat(client, message);
break;
case MessageType.Whisper:
HandleWhisper(client, message);
break;
case MessageType.UserListRequest:
HandleUserListRequest(client);
break;
case MessageType.Ping:
HandlePing(client);
break;
case MessageType.Disconnect:
client.Disconnect();
break;
}
}
/// <summary>
/// 연결 처리
/// </summary>
private void HandleConnect(ChatClient client, ChatMessage message)
{
// Check employee ID duplication
lock (clientsLock)
{
if (!string.IsNullOrEmpty(message.EmployeeId) &&
clients.Any(c => c.ClientId != client.ClientId && c.EmployeeId == message.EmployeeId))
{
var errorMsg = new ChatMessage
{
Type = MessageType.Notice,
Content = "Employee ID already connected.",
Timestamp = DateTime.Now
};
client.SendMessage(errorMsg);
client.Disconnect();
return;
}
}
Console.WriteLine($"[LOGIN] {message.NickName} ({message.EmployeeId}, {message.UserGroup}) from {client.IpAddress}, {message.HostName}");
// Welcome message
var welcomeMsg = new ChatMessage
{
Type = MessageType.Notice,
Content = $"Welcome to the chat server, {message.NickName}!",
Timestamp = DateTime.Now
};
client.SendMessage(welcomeMsg);
// Notify other users
var noticeMsg = new ChatMessage
{
Type = MessageType.Notice,
Content = $"{message.NickName} ({message.EmployeeId}) has joined the chat.",
Timestamp = DateTime.Now
};
BroadcastMessage(noticeMsg, client.ClientId);
}
/// <summary>
/// 채팅 메시지 처리 (1:1 messaging)
/// </summary>
private void HandleChat(ChatClient client, ChatMessage message)
{
// Set sender information
message.NickName = client.NickName;
message.EmployeeId = client.EmployeeId;
message.UserGroup = client.UserGroup;
message.IpAddress = client.IpAddress;
message.HostName = client.HostName;
message.Timestamp = DateTime.Now;
// Send to target recipient (1:1 messaging)
if (!string.IsNullOrEmpty(message.TargetEmployeeId))
{
lock (clientsLock)
{
// Find target by employee ID
var targetClient = clients.FirstOrDefault(c => c.EmployeeId == message.TargetEmployeeId);
if (targetClient != null)
{
// Send to target
targetClient.SendMessage(message);
// Echo back to sender
client.SendMessage(message);
Console.WriteLine($"[CHAT] {client.EmployeeId} -> {message.TargetEmployeeId}: {message.Content}");
}
else
{
var errorMsg = new ChatMessage
{
Type = MessageType.Notice,
Content = $"User '{message.TargetEmployeeId}' not found or offline.",
Timestamp = DateTime.Now
};
client.SendMessage(errorMsg);
}
}
}
else
{
// No target specified, send error
var errorMsg = new ChatMessage
{
Type = MessageType.Notice,
Content = "Please select a recipient.",
Timestamp = DateTime.Now
};
client.SendMessage(errorMsg);
}
}
/// <summary>
/// Whisper message handler
/// </summary>
private void HandleWhisper(ChatClient client, ChatMessage message)
{
message.NickName = client.NickName;
message.IpAddress = client.IpAddress;
message.Timestamp = DateTime.Now;
lock (clientsLock)
{
var targetClient = clients.FirstOrDefault(c => c.NickName == message.TargetNickName);
if (targetClient != null)
{
targetClient.SendMessage(message);
Console.WriteLine($"[WHISPER] {client.NickName} -> {message.TargetNickName}: {message.Content}");
}
else
{
var errorMsg = new ChatMessage
{
Type = MessageType.Notice,
Content = $"User '{message.TargetNickName}' not found.",
Timestamp = DateTime.Now
};
client.SendMessage(errorMsg);
}
}
}
/// <summary>
/// 사용자 목록 요청 처리
/// </summary>
private void HandleUserListRequest(ChatClient client)
{
lock (clientsLock)
{
// Format: "employeeId1:nickName1:userGroup1,employeeId2:nickName2:userGroup2,..."
// Filter: dev can see all, others can only see same group
var filteredClients = clients
.Where(c => !string.IsNullOrEmpty(c.NickName) && !string.IsNullOrEmpty(c.EmployeeId))
.Where(c => client.UserGroup == "dev" || c.UserGroup == client.UserGroup);
var userListStr = string.Join(",", filteredClients
.Select(c => $"{c.EmployeeId}:{c.NickName}:{c.UserGroup ?? "default"}"));
var response = new ChatMessage
{
Type = MessageType.UserListResponse,
Content = userListStr,
Timestamp = DateTime.Now
};
client.SendMessage(response);
Console.WriteLine($"[USER_LIST] Sent to {client.EmployeeId} ({client.UserGroup}): {userListStr}");
}
}
/// <summary>
/// Ping 처리
/// </summary>
private void HandlePing(ChatClient client)
{
var pongMsg = new ChatMessage
{
Type = MessageType.Pong,
Timestamp = DateTime.Now
};
client.SendMessage(pongMsg);
}
/// <summary>
/// 클라이언트 연결 해제 이벤트 핸들러
/// </summary>
private void OnClientDisconnected(object sender, string clientId)
{
ChatClient disconnectedClient = null;
lock (clientsLock)
{
disconnectedClient = clients.FirstOrDefault(c => c.ClientId == clientId);
if (disconnectedClient != null)
{
clients.Remove(disconnectedClient);
}
}
if (disconnectedClient != null)
{
Console.WriteLine($"[DISCONNECTED] {disconnectedClient.NickName ?? disconnectedClient.IpAddress} (ID: {clientId})");
if (!string.IsNullOrEmpty(disconnectedClient.NickName))
{
var noticeMsg = new ChatMessage
{
Type = MessageType.Notice,
Content = $"{disconnectedClient.NickName} has left the chat.",
Timestamp = DateTime.Now
};
BroadcastMessage(noticeMsg);
}
}
}
/// <summary>
/// 모든 클라이언트에게 메시지 전송
/// </summary>
private void BroadcastMessage(ChatMessage message, string excludeClientId = null)
{
lock (clientsLock)
{
foreach (var client in clients.ToList())
{
if (excludeClientId != null && client.ClientId == excludeClientId)
continue;
if (!string.IsNullOrEmpty(client.NickName))
{
client.SendMessage(message);
}
}
}
}
/// <summary>
/// 서버 상태 출력
/// </summary>
public void PrintStatus()
{
Console.WriteLine("\n========================================");
Console.WriteLine($"Current Clients: {clients.Count}/{maxClients}");
Console.WriteLine("========================================");
lock (clientsLock)
{
foreach (var client in clients)
{
if (!string.IsNullOrEmpty(client.NickName))
{
Console.WriteLine($"- {client.NickName} ({client.EmployeeId}, {client.UserGroup})");
Console.WriteLine($" IP: {client.IpAddress}, Host: {client.HostName}");
Console.WriteLine($" Connected: {client.ConnectedTime:yyyy-MM-dd HH:mm:ss}");
Console.WriteLine($" Last Activity: {client.LastActivity:HH:mm:ss}");
}
}
}
Console.WriteLine("========================================\n");
}
/// <summary>
/// TCP 서버 중지
/// </summary>
public void Stop()
{
if (!isRunning)
return;
isRunning = false;
// Disconnect all clients
lock (clientsLock)
{
foreach (var client in clients.ToList())
{
client.Disconnect();
}
clients.Clear();
}
try
{
tcpListener?.Stop();
}
catch { }
Console.WriteLine("[TCP] Chat server stopped");
}
}
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net48" />
</packages>