안녕하세요. 워시기입니다.
오늘은 지난 포스팅에서 작성한 TCP 서버 / 클라이언트를 조금더 심화해서 작성을 해보도록 하겠습니다.
지난번에는 단순하게 TcpListener와 TcpClient, NetworkStream을 통해 에코서버와 클라이언트를 만들었는데
1:1의 통신이라면 크게 문제가 없겠지만 서버에 다수의 접속자가 발생하게 된다면 이전의 작업으로는 대응할 수 없을 것 입니다.
이번 포스팅에서는 다수의 접속을 처리할 수 있도록 작성을 하도록 하겠습니다.
서버가 실행을 하면 TcpListener는 접속을 기다리고 있습니다.
대기하고 있다가 클라이언트의 접속이 되면 TcpClient객체를 반환하게 됩니다.
이 객체를 CommInfo(제가 클라이언트 객체를 관리하기 위해 클래스를 만들었습니다.)라는 클래스에 관리번호를 부여하여 저장을 하고 리스트에 담도록 합니다.
처음 리스트나 딕셔너리 등의 자료구조에 데이타를 저장할때 그저 배열처럼 숫자나 스트링만 넣는것에 익숙했는데 List<int> 가 List<Class>로 생각이 확장될때 참으로 생각이 갇혀있던 내가 열린 들판으로 뛰쳐나가 노는 그런 기분이었네요.
구조는 폼이 시작되면서 하위에 통신을 담당하는 CommManager.cs를 만들고 클라이언트가 접속했을때 생성되는 TcpClient 객체를 저장하는 클래스인 CommInfo.cs를 만들었습니다.
물론 이 구조가 보다 더 잘만들 수 있을것입니다.
참고하시면서 이런 방법도 있구나 생각하시면 좋을것 같습니다.
메인폼부터 설명을 드리자면
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;
namespace TCP_Server_Multi
{
public partial class Form1 : Form
{
Thread uiThread;
CommManager CommManager;
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
uiThread = new Thread(Init);
uiThread.IsBackground = true;
uiThread.Start();
}
void Init()
{
CommManager = new CommManager(this);
}
public void UpdateLog(string msg)
{
if (Tbox_Log.InvokeRequired)
{
Tbox_Log.Invoke(new MethodInvoker(delegate () {
Tbox_Log.AppendText(msg);
}));
}
else
{
Tbox_Log.AppendText(msg);
}
}
}
}
코드는 위와 같습니다.
폼 로드가 끝나면 통신 관련 CommManager를 바로 인스턴스화 하여 쓰레드로 돌릴 예정입니다.
서버로 작성할것이기에 프로그램이 시작하면서 시작이라는 행위없이 바로 시작하게 합니다.
프로그램의 상태를 표시하여야 하기에 로그 표시화면에 업데이트하는 코드는 추가 하였습니다.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Windows.Forms;
namespace TCP_Server_Multi
{
public class CommManager
{
int manageNum = 0;
string msgStr = string.Empty;
TcpListener tcpListener;
List<CommInfo> tcpManageList = new List<CommInfo>();
Thread thread;
Form1 control;
public CommManager(Form1 control)
{
this.control = control;
Init();
}
//=====================================================================
// 초기화 서버의 시작과 클라이언트 접속 대기
//=====================================================================
void Init()
{
tcpManageList.Clear();
tcpListener = new TcpListener(IPAddress.Any, 16791);
tcpListener.Start();
UpdateLog("서버가 시작됩니다....");
while (true)
{
TcpClient tcpClient = tcpListener.AcceptTcpClient();
manageNum++;
tcpManageList.Add(new CommInfo(manageNum, tcpClient));
UpdateLog($"클라이언트 접속 {tcpClient.Client.RemoteEndPoint}");
if (tcpClient != null)
{
try
{
thread = new Thread(new ParameterizedThreadStart(CommProcess));
thread.IsBackground = true;
thread.Start(tcpManageList[tcpManageList.Count - 1]);
foreach (var item in tcpManageList)
{
Console.WriteLine($"Tcp Num : {item.ManageNum}, Tcp Obj : {item.Client}");
}
}
catch (Exception ex)
{
Console.WriteLine("Thread 종료");
}
}
}
}
//=====================================================================
// 클라이언트 데이타 수신
//=====================================================================
void CommProcess(object commInfoObj)
{
try
{
CommInfo commInfo = commInfoObj as CommInfo;
while (commInfo.Client.Connected)
{
NetworkStream stream = commInfo.Client.GetStream();
byte[] buffBytes = new byte[1024];
int recCnt = stream.Read(buffBytes, 0, buffBytes.Length);
if (recCnt > 0)
{
byte[] recBytes = new byte[recCnt];
Array.Copy(buffBytes, 0, recBytes, 0, recCnt);
string recStr = string.Empty;
recStr = Encoding.ASCII.GetString(recBytes);
if (recStr.Length > 1)
{
if (recStr.Substring(recStr.Length - 2, 2) == "\r\n")
{
msgStr += recStr;
UpdateLog($"수신 데이타 {commInfo.ManageNum}로 부터 {msgStr} 수신");
msgStr = string.Empty;
//수신 데이타 에코
stream.Write(recBytes, 0, recBytes.Length);
}
else
{
msgStr += recStr;
}
}
else
{
msgStr += recStr;
}
}
else
{
UpdateLog($"관리번호 {commInfo.ManageNum} 오류");
commInfo.Client.Close();
tcpManageList.Remove(commInfo);
Console.WriteLine($"{commInfo.ManageNum} 삭제...");
}
}
}
catch (Exception ex)
{
Console.WriteLine("종료");
}
}
//=====================================================================
// 로그 업데이트
//=====================================================================
void UpdateLog(string msg)
{
control.UpdateLog($"{DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss")} {msg}" + Environment.NewLine);
}
}
}
CommManager.cs의 코드는 위와 같습니다.
크게 메서드가 3가지가 있습니다.
- Init
서버가 시작하고 클라이언트를 접속시 해당 객체를 CommProcess에 전달하여 쓰레드를 생성하는 메서드 - CommProcess
접속된 클라이언트를 통해 만들어진 TcpClient객체를 파라미터로 받아서 수신하는 메서드 - UpdateLog
서버에 수신된 데이타를 업데이트하는 메서드
생성자에서 UI를 받아서 Control에 전달하고 바로 Init 메서드를 실행하여 서버를 실행하도록 합니다.
여기서 중요한점은 tcpListener에서 클라이언트가 접속되면 tcpClient객체를 반환받는데 이 객체를 CommInfo에 관리번호와 함께 객체를 만들어서 List 에 보관한다는 점입니다.
상황에 따라 Dictionary에 만들어도 됩니다.
그리고 일반적인 쓰레드 만드는 구분은 보셨을텐데 위코드에서는 파라미터를 받아 처리하는 쓰레드 시작 구문이 있습니다.
Thread threa = new Thread(new ParameterizedThreadStart([파라미터 수신하는 메서드]));
thread.Start(수신 파라미터);
쓰레드 처리되는 메서드( object obj)
{
...........
}
위의 구문은 파라미터를 받아서 처리해야 하는 메서드를 쓰레드에서 실행하는 구문으로 샐행되는 메서드의 파라미터가 Obejct로 수신해야한다는 점을 유의해야 합니다.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net.Sockets;
namespace TCP_Server_Multi
{
public class CommInfo
{
int manageNum;
public int ManageNum { get => manageNum; }
TcpClient client;
public TcpClient Client { get => client; }
public CommInfo(int manageNum, TcpClient client)
{
this.manageNum = manageNum;
this.client = client;
}
}
}
위의 코드는 CommInfo.cs의 코드입니다.
관리번호와 TcpClient의 객체를 받아 저장합니다.
위의 내용을 가지고 많은 참고가 되었으면 좋겠네요.
다음 포스팅에서는 클라이언트를 만들도록 하겠습니다.
여기까지 읽어주셔서 감사합니다~
이만 마치도록 하겠습니다~
'개발' 카테고리의 다른 글
C#으로 TCP 클라이언트 프로그램 만들기 (6) | 2024.12.18 |
---|---|
MAUI NU1100 Error 대처 (167) | 2024.11.30 |
.net 9.0 migration [.net 9.0 업그레이드] (12) | 2024.11.29 |
.Net Conf 2024 @BMW 다녀왔습니다~ (103) | 2024.11.28 |
C#으로 하는 Thread (116) | 2024.11.23 |