[.NET 4.8] 'SocketAsyncEventArgs'로 구현한 채팅 샘플 이해하기(1) - 문자열을 주고받는 서버 클라이언트를 구현해 보자.
'SocketAsyncEventArgs'의 이해를 위한 글을 여러 번 썼었는데.....
'.NET 5' 이후로는 'SocketAsyncEventArgs'가 이전과 살짝 다른 동작을 합니다.
그래서 '.NET 5'로 넘어가기 전에 총정리 겸 단계별 샘플을 만들었습니다.
0. 구조
각 샘플은 거의 같은 구조로 되어 있습니다.
샘플 소스 :
dang-gun/DGSocketAssist/DGSocketAssist1/
라이브러리 형태로 만들어져 있어 "DGSocketAssist_Server", "DGSocketAssist_Client"만 참조하여 서버/클라이언트 프로그램을 만들 수 있습니다.
1. 'DGSocketAssist1_Server' 이해하기
'DGSocketAssist_Server'는 'Server.cs'와 'ClientListener.cs'로 되어있습니다.
'ClientListener.cs'는 서버에 접속한 하나의 클라이언트를 관리하는 클래스고
'Server.cs'는 클라이언트 리스트를 관리하는 클래스입니다.
1-1. 'ClientListener.cs'
클라이언트가 접속되면 서버는 제일 먼저 'ClientListener'를 생성합니다.
이 클래스가 생성될 때 송신용/수신용 용도로 'SocketAsyncEventArgs'가 2개 생성됩니다.
그때그때 생성하여 사용하는 방법도 있습니다.
다만 이 방법은 가비지 컬렉터의 압력을 증가시키는 문제가 있습니다.
그래서 2개를 생성하고 재활용하는 방식을 사용합니다.
샘플에 따라 쓰레드풀을 사용하는 경우 'SocketAsyncEventArgs'를 상위 클래스에서 전달받아 사용하기도 합니다.
1-1-1. 'FirstListening' 함수
클라이언트가 연결되고 첫 메시지를 받을 준비를 하는 함수입니다.
서버에서 접속한 유저에 대한 처리가 끝나면 호출합니다.
/// <summary>
/// 연결된 클라이언트에서 전송한 첫 데이터를 읽기위해 대기한다.
/// </summary>
/// <remarks>
/// 모든 이벤트 연결이 끝난 후 호출하는 것이 좋다.
/// </remarks>
public void FirstListening()
{
//데이터 구조 생성
MessageData MsgData = new MessageData();
//커낵트용 데이터 구조 지정
this.m_saeaReceive.UserToken = MsgData;
Debug.WriteLine("첫 데이터 받기 준비");
//첫 데이터 받기 시작
this.SocketMe.ReceiveAsync(this.m_saeaReceive);
if (null != m_ValidationFunc)
{//유효성 검사용 함수가 있다.
if (false == m_ValidationFunc(this))
{//유효성 검사 실패
//접속을 끊는다.
this.Disconnect(true);
return;
}
}
//유효성 검사 함수가 없다면 검사를 하지 않는다.
}
16번 줄 : 수신용 'SocketAsyncEventArgs'를 전달하여 설정합니다.
1-1-2. 'SaeaReceive_Completed' 이벤트
수신용 'SocketAsyncEventArgs'가 전달되면 호출되는 이벤트입니다.
이 샘플에서는 별다른 방어 코드가 없지만, 원래는 버퍼 처리를 위한 추가작업을 해야 합니다.
(참고 : [.Net] SocketAsyncEventArgs - 'SocketAsyncEventArgs'의 이해)
이 이벤트 콜백은 데이터가 수신되기 시작하면 발생합니다.
/// <summary>
/// 클라리언트에서 넘어온 데이터 받음 완료
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void SaeaReceive_Completed(object sender, SocketAsyncEventArgs e)
{
//서버에서 넘어온 정보
Socket socketClient = (Socket)sender;
//서버에서 넘어온 데이터
MessageData MsgData = (MessageData)e.UserToken;
MsgData.SetLength(e.Buffer);
MsgData.InitData();
//유저가 연결 상태인지?
if (true == socketClient.Connected)
{//연결 상태이다
//넘어온 메시지 읽기
socketClient.Receive(MsgData.Data, MsgData.DataLength, SocketFlags.None);
//넘어온 메시지 전달
this.MessagedCall(MsgData.GetData());
Debug.WriteLine("전달된 데이터 : {0}", MsgData.GetData());
//다음 데이터를 기다린다.
//'Read'에서 무한루프 없이 구현하기 위해 두번째부터는 여기서 대기하도록
//구성되어 있다.
socketClient.ReceiveAsync(e);
Debug.WriteLine("데이터 받기 준비");
}
else
{//아니다
//접속 끊김을 알린다.
Disconnect(true);
}
}
이 샘플은 'SocketAsyncEventArgs'를 이해시키는 데 목적이 있고 로컬에서 테스트하니 처리하지 않았지만
'SocketAsyncEventArgs.Completed'이벤트 한 번에 한 개의 데이터를 처리하면 안 됩니다.
(참고 : [.Net] SocketAsyncEventArgs - 'SocketAsyncEventArgs'의 이해)
11번 줄 : 보내는 쪽에서 'MessageData'라는 개체를 'e.UserToken'에 담아 보내고 있습니다.
그래서 'MessageData'로 캐스팅하여 데이터를 처리합니다.
22번 줄 : 이 클래스는 메시지를 직접 처리하지 않습니다.
받은 데이터를 외부로 전달해줍니다.
28번 줄 : 수신용 'SocketAsyncEventArgs'를 다시 세팅합니다.
'SocketAsyncEventArgs e' 개체는 우리가 전달했던 'this.m_saeaReceive'와 동일한 개체입니다.
즉, 28번 줄과 같이 설정하면 'SocketAsyncEventArgs'를 재활용하게 됩니다.
이런 구현방식이 없던 예전에는 무한루프를 돌려서 버퍼를 확인하는 방법을 썼습니다.
뭐....이것도 내부적으로는 무한루프가 돌고 있긴 하겠죠? ㅎㅎㅎ
1-1-3. 'Send' 함수
연결된 클라이언트에게 메시지를 전송하기 위한 합수입니다.
/// <summary>
/// 연결된 이 클라이언트에게 메시지를 전송 한다.
/// </summary>
/// <param name="sMsg"></param>
public void Send(string sMsg)
{
MessageData mdMsg = new MessageData();
mdMsg.SetData(sMsg);
//데이터 길이 세팅
this.m_saeaSend.SetBuffer(BitConverter.GetBytes(mdMsg.DataLength), 0, 4);
//보낼 데이터 설정
this.m_saeaSend.UserToken = mdMsg;
Debug.WriteLine("데이터 전달 : {0}", sMsg);
//보내기
this.SocketMe.SendAsync(this.m_saeaSend);
}
7번 줄 : 전송요 'MessageData'개체를 만듭니다.
11번 줄 : 송신용 'SocketAsyncEventArgs'에 버퍼를 설정합니다.
12번 줄 : 송신용 'SocketAsyncEventArgs'의 'UserToken'에 데이터를 넣습니다.
제가 처음 봤던 샘플이 이렇게 돼있어서 이렇게 만들었던 코드가 남아 있는 겁니다만.....
원래 'UserToken'은 데이터 전송용이 아닙니다.
'SetBuffer'를 사용해야 하죠.
16번 줄 : 완성된 송신용 'SocketAsyncEventArgs'를 소켓에 전달합니다.
1-1-3. 'SaeaSend_Completed' 이벤트
송신이 완료되면 송신용 'SocketAsyncEventArgs'에 연결된 'Completed'이벤트가 콜백됩니다.
private void SaeaSend_Completed(object sender, SocketAsyncEventArgs e)
{
//유저 소켓
Socket socketClient = (Socket)sender;
MessageData mdMsg = (MessageData)e.UserToken;
//데이터 보내기 마무리
socketClient.Send(mdMsg.Data);
}
7번 줄 : 위에서 만든 데이터를 소켓에 다시 전달하여 전송을 마무리합니다.
이 샘플에서 가장 이해할 수 없는 코드입니다.
소켓에 다시 전달해야 전송이 마무리가 됩니다.
원례 'UserToken'를 이용한 송신이 비정상적이라 이것도 그것과 관련 있는게 아닌가 싶습니다.
1-2. 'Server.cs'
서버에 접속한 클라이언트를 관리하는 클래스입니다.
접속자를 기다리기 위한 소켓과
접속자를 관리하기 위한 리스트를 가지고 있습니다.
/// <summary>
/// 접속한 클라이언트 리스트
/// </summary>
public List<ClientListener> ClientList = new List<ClientListener>();
/// <summary>
/// 서버 소켓
/// </summary>
private Socket socketServer;
/// <summary>
/// 서버 생성
/// </summary>
/// <param name="nPort">사용할 포트</param>
public Server(int nPort)
{
//유저 리스트 생성
this.ClientList = new List<ClientListener>();
//서버 세팅
socketServer
= new Socket(AddressFamily.InterNetwork
, SocketType.Stream
, ProtocolType.Tcp);
//서버 ip 및 포트
IPEndPoint ipServer
= new IPEndPoint(IPAddress.Any, nPort);
socketServer.Bind(ipServer);
}
21번 줄 : 서버는 로컬에서 돌아가므로 IP를 별도로 설정하지 않아도 됩니다.
포트만 전달받아 클라이언트를 기다릴 소켓을 생성합니다.
1-2-1. 'Start' 함수
위에서 생성한 소켓을 수신 대기 상태로 만들어 줍니다.
/// <summary>
/// 서버 시작.
/// tcp 요청을 대기한다.
/// </summary>
public void Start()
{
Debug.WriteLine("서버 시작...");
//수신 대기 시작
//매개변수는 연결 대기 숫자.
//.NET 5 이상에서는 자동으로 설정가능하다.
//https://docs.microsoft.com/ko-kr/dotnet/api/system.net.sockets.socket.listen?view=net-6.0
socketServer.Listen(40);
this.OnStart();
Debug.WriteLine("첫번째 클라이언트 접속 대기");
//클라이언트 연결시 사용될 SocketAsyncEventArgs
SocketAsyncEventArgs saeaUser = new SocketAsyncEventArgs();
//클라이언트가 연결되었을때 이벤트
saeaUser.Completed -= ClientConnect_Completed;
saeaUser.Completed += ClientConnect_Completed;
//클라이언트 접속 대기 시작
//첫 클라이언트가 접속되기 전까지 여기서 대기를 하게 된다.
socketServer.AcceptAsync(saeaUser);
}
13번 줄 : 수신 대기 상태에 들어가면서 수신 대기 최대 수를 설정합니다.
접속자 숫자가 아니라 접속 처리가 아직 되지 않은 클라이언트의 최대 숫자입니다.
(대기 중인 클라이언트 숫자)
19번 줄 : 접속한 클라이언트가 사용할 'SocketAsyncEventArgs'를 생성합니다.
클라이언트의 접속 이벤트는 한 번에 하나씩 처리가 되므로(나머지는 대기, Listen에서 설정한 숫자보다 많아지만 거부됨) 한 개 가지고 돌려쓰게 됩니다.
26번 줄 : 클라이언트의 접속을 기다립니다.
여기서 클라이언트 접속을 기다리며 클라이언트가 접속되면 'Completed'이벤트가 콜백 됩니다.
1-2-2. 'ClientConnect_Completed' 이벤트
클라이언트가 접속되면 발생하는 이벤트입니다.
클라이언트 처리를 할 'ClientListener'클래스를 만들고 클라이언트에 관련된 정보들을 세팅해 줍니다.
/// <summary>
/// 클라이언트 접속 완료.
/// <para>하나의 클라이언트가 접속했음을 처리한다.</para>
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <exception cref="NotImplementedException"></exception>
private void ClientConnect_Completed(object sender, SocketAsyncEventArgs e)
{
Debug.WriteLine("클라이언트 접속됨 : {0}"
, ((IPEndPoint)e.AcceptSocket.RemoteEndPoint).ToString());
//유저 객체를 만든다.
ClientListener newUser = new ClientListener(e.AcceptSocket);
//각 이벤트 연결
newUser.OnValidationComplete += NewUser_OnValidationComplete;
newUser.OnDisconnect += NewUser_OnDisconnect;
newUser.OnDisconnectCompleted += NewUser_OnDisconnectCompleted;
newUser.OnMessaged += NewUser_OnMessaged;
//리스트에 클라이언트 추가
this.ClientList.Add(newUser);
//클라이언트 접속을 알림.
this.ConnectedCall(newUser);
//클라이언트의 데이터 전송을 대기한다.
newUser.FirstListening();
//다시 클라이언트 접속 대기 시작
Debug.WriteLine("클라이언트 접속 대기");
//이렇게 구성하는 이유는 'Start'에서 무한 루프 없이
//클라이언트 대기를 구현하기 위해서이다.
Socket socketServer = (Socket)sender;
e.AcceptSocket = null;
socketServer.AcceptAsync(e);
}
14번 줄 : 클라이언트 처리를 할 유저 개체를 만듭니다.
연결된 소켓 정보를 전달해야 합니다.
필요한 이벤트들도 연결해 줍니다.
27번 줄 : 클라이언트의 첫 데이터 전송을 대기합니다.
34번 줄 : 다음 클라이언트를 기다리기 위한 세팅을 합니다.
35번 줄 : 기존 소켓은 이 클라이언트가 가져갔으므로 재사용할 'SocketAsyncEventArgs'에서 소켓 정보를 제거합니다.
36번 줄 : 다음 클라이언트의 접속을 기다립니다.
2. 'DGSocketAssist1_Client' 이해하기
이 라이브러리는 'Client.cs'만 있습니다.
서버 접속을 위한 정보를 설정하고
서버와 연결된 소켓(Socket)을 관리합니다.
/// <summary>
/// 이 클라이언트가 연결된 Socket
/// </summary>
public Socket SocketMe { get; private set; }
/// <summary>
/// 서버로 전송용 SocketAsyncEventArgs
/// </summary>
private SocketAsyncEventArgs m_saeaSend = null;
/// <summary>
/// 수신용 SocketAsyncEventArgs
/// </summary>
private SocketAsyncEventArgs m_saeaReceive = null;
/// <summary>
/// 서버 주소
/// </summary>
public IPEndPoint ServerIP { get; private set; }
/// <summary>
/// 서버와 연결할 클라이언트 생성.
/// </summary>
/// <param name="sIP">서버 ip</param>
/// <param name="nPort">서버 포트</param>
public Client(string sIP, int nPort)
{
this.SocketSetting(
new IPEndPoint(
IPAddress.Parse(sIP)
, nPort));
}
/// <summary>
/// 서버와 연결할 클라이언트 생성.
/// </summary>
/// <param name="address">서버 주소</param>
public Client(IPEndPoint address)
{
this.SocketSetting(address);
}
/// <summary>
/// 이 개체가 사용할 소켓을 생성한다.
/// </summary>
/// <param name="ip">서버 주소</param>
private void SocketSetting(IPEndPoint ip)
{
//소켓 생성
SocketMe
= new Socket(AddressFamily.InterNetwork
, SocketType.Stream
, ProtocolType.Tcp);
this.ServerIP = ip;
//전송용 SocketAsyncEventArgs 세팅
this.m_saeaSend = new SocketAsyncEventArgs();
this.m_saeaSend.RemoteEndPoint = this.ServerIP;
this.m_saeaSend.Completed -= SaeaSend_Completed;
this.m_saeaSend.Completed += SaeaSend_Completed;
//수신용 SocketAsyncEventArgs 세팅
this.m_saeaReceive = new SocketAsyncEventArgs();
this.m_saeaReceive.RemoteEndPoint = this.ServerIP;
this.m_saeaReceive.SetBuffer(new Byte[SettingData.BufferFullSize], 0, SettingData.BufferFullSize);
this.m_saeaReceive.Completed -= SaeaReceive_Completed;
this.m_saeaReceive.Completed += SaeaReceive_Completed;
}
48번 줄 : 소켓을 생성하고 서버 정보를 전달합니다.
55번 줄 : 송신용 'SocketAsyncEventArgs'를 세팅합니다.
61번 줄 : 수신용 'SocketAsyncEventArgs'를 세팅합니다.
2-1. 'ConnectServer'함수
서버에 연결을 시도하는 함수입니다.
/// <summary>
/// 서버에 연결시도를 한다.
/// </summary>
public void ConnectServer()
{
//접속용 SocketAsyncEventArgs를 생성
SocketAsyncEventArgs saeaConnect = new SocketAsyncEventArgs();
saeaConnect.RemoteEndPoint = this.ServerIP;
//연결 완료 이벤트 연결
saeaConnect.Completed -= SaeaConnect_Completed;
saeaConnect.Completed += SaeaConnect_Completed;
Debug.WriteLine("서버 연결 중");
//서버 메시지 대기
this.SocketMe.ConnectAsync(saeaConnect);
}
7번 줄 : 접속용 'SocketAsyncEventArgs'를 생성합니다.
접속용은 한 번만 사용되므로 1회용 'SocketAsyncEventArgs'를 생성하여 사용합니다.
15번 줄 : 위에서 세팅한 'SocketAsyncEventArgs'를 전달하여 서버에 접속을 시도합니다.
2-2. 'SaeaConnect_Completed'이벤트
서버 접속 시도에 대한 이벤트입니다.
접속 시도가 성공하였다면 첫 번째 데이터를 보냅니다.
/// <summary>
/// 연결 완료 이벤트에 연결됨
/// <para>서버에 연결되었음에만 사용하는 이벤트이다.</para>
/// </summary>
/// <param name="sender">호출한 개체</param>
/// <param name="e">SocketAsync 이벤트</param>
private void SaeaConnect_Completed(object sender, SocketAsyncEventArgs e)
{
this.SocketMe = (Socket)sender;
if (true == this.SocketMe.Connected)
{
MessageData mdReceiveMsg = new MessageData();
//서버에 수신대기할 개체를 설정한다.
//보낼 데이터를 설정하고
this.m_saeaReceive.UserToken = mdReceiveMsg;
//첫 메시지 받기 준비
this.SocketMe.ReceiveAsync(this.m_saeaReceive);
this.ReceiveReadyCall();
Debug.WriteLine("서버 연결 성공");
//서버 연결 성공을 알림
this.ConnectionCompleteCall();
}
else
{
//접속 끊김을 알린다.
Disconnect(true);
}
}
11번 줄 : 접속 성공 여부를 판단합니다.
19번 줄 : 첫 번째 메시지를 보내고 대기합니다.
이때부터 송신용 'SocketAsyncEventArgs'가 사용됩니다.
2-3. 'SaeaReceive_Completed'이벤트
첫 접속 이후 서버로 부터 오는 데이터를 받는 이벤트입니다.
'ClientListener'와 거의 동일합니다.
/// <summary>
/// 수신 완료 이벤트 연결됨
/// <para>실제 데이터를 수신받는 이벤트이다.</para>
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void SaeaReceive_Completed(object sender, SocketAsyncEventArgs e)
{
Socket socketClient = (Socket)sender;
MessageData mdRecieveMsg = (MessageData)e.UserToken;
mdRecieveMsg.SetLength(e.Buffer);
mdRecieveMsg.InitData();
if (true == socketClient.Connected)
{
//연결이 되어 있다.
//데이터 수신
socketClient.Receive(
mdRecieveMsg.Data
, mdRecieveMsg.DataLength
, SocketFlags.None);
//메시지 수신을 알림
this.MessagedCall(mdRecieveMsg.GetData());
Debug.WriteLine("다음 데이터 받을 준비 ");
//다음 메시지를 받을 준비를 한다.
socketClient.ReceiveAsync(e);
this.ReceiveReadyCall();
}
else
{
//접속 끊김을 알린다.
Disconnect(true);
}
}
10~12번 줄 : 수신된 데이터를 받기 위한 버퍼를 설정합니다.
19번 줄 : 수신된 데이터를 새로 설정한 버퍼에 넣습니다.
29번 줄 : 다음 데이터를 기다립니다.
2-4. 'Send' 함수
서버로 데이터를 보내는 함수입니다.
이것도 'ClientListener'와 거의 동일합니다.
/// <summary>
/// 연결된 이 서버로 메시지를 전송 한다.
/// </summary>
/// <param name="sMsg"></param>
public void Send(string sMsg)
{
MessageData mdSendMsg = new MessageData();
//데이터를 넣고
mdSendMsg.SetData(sMsg);
using (SocketAsyncEventArgs saeaSendArgs = new SocketAsyncEventArgs())
{
//데이터 길이 세팅
this.m_saeaSend.SetBuffer(BitConverter.GetBytes(mdSendMsg.DataLength), 0, 4);
//보낼 데이터 설정
this.m_saeaSend.UserToken = mdSendMsg;
//보내기 시작
this.SocketMe.SendAsync(this.m_saeaSend);
}//end using saeaSendArgs
}
2-5. 'SaeaSend_Completed' 이벤트
전송 완료에 대한 이벤트 콜백입니다.
이것도 'ClientListener'와 동일합니다.
private void SaeaSend_Completed(object sender, SocketAsyncEventArgs e)
{
//유저 소켓
Socket socketClient = (Socket)sender;
MessageData mdMsg = (MessageData)e.UserToken;
//데이터 보내기 마무리
socketClient.Send(mdMsg.Data);
}
3. 테스트 서버 만들기
UI 관련 내용은 소스의 주석을 참고하면 됩니다.
여기서는 중요한 내용만 짚고 넘어갑니다.
3-1. 'User' 클래스
'ClientListener'는 접속자의 네트워크 정보를 처리하므로
실질적인 유저의 정보를 관리할 클래스가 필요합니다.
이 클래스는 서버로부터 온 데이터에서 명령어를 추출하여 실질적인 동작을 요청하는 클래스입니다.
3-1-1. 'ClientListenerMe_OnMessaged' 이벤트
서버에서 데이터가 오면 명령어를 분리하여 필요한 동작을 호출하는 함수입니다.
private void ClientListenerMe_OnMessaged(ClientListener sender, string message)
{
//구분자로 명령을 구분 한다.
string[] sData = GloblaStatic.ChatCmd.ChatCommandCut(message);
//데이터 개수 확인
if ((1 <= sData.Length))
{
//0이면 빈메시지이기 때문에 별도의 처리는 없다.
//넘어온 명령
ChatCommandType typeCommand
= GloblaStatic.ChatCmd.StrIntToType(sData[0]);
switch (typeCommand)
{
case ChatCommandType.None: //없다
break;
case ChatCommandType.Msg: //메시지인 경우
SendMeg_Main(sData[1], typeCommand);
break;
case ChatCommandType.ID_Check: //아이디 체크
SendMeg_Main(sData[1], typeCommand);
break;
case ChatCommandType.User_List_Get: //유저리스트 갱신 요청
SendMeg_Main("", typeCommand);
break;
case ChatCommandType.Login: //로그인 완료
OnLoginComplet(this);
break;
}
}
}
이 프로젝트는 모든 데이터가 문자열로 오므로
문자열에서 구분자로 명령어를 때서 처리합니다.
3-1-2. 'SendMeg_Main' 함수
이 클라이언트를 관리하는 서버에 메시지 보내기를 요청하는 함수입니다.
다른 서버에 하는 요청이나 다른 클라이언트에게 메시지를 보낼 때 사용합니다.
/// <summary>
/// 서버로 메시지를 보냅니다.
/// </summary>
/// <param name="sMag"></param>
/// <param name="typeCommand"></param>
private void SendMeg_Main(string sMag, ChatCommandType typeCommand)
{
MessageEventArgs e = new MessageEventArgs(sMag, typeCommand);
OnMessaged(this, e);
}
서버가 유저를 생성할 때 'OnMessaged'이벤트를 연결하므로
이 함수에서는 'OnMessaged'에 데이터를 담아 전달하게 됩니다.
3-1-3. 'SendMsg_User' 함수
이 클라이언트에게 메시지를 보내는 함수입니다.
/// <summary>
/// 이 유저에게 메시지를 보낸다.
/// </summary>
/// <param name="sMsg"></param>
public void SendMsg_User(string sMsg)
{
this.ClientListenerMe.Send(sMsg);
}
'ClientListener'에서 처리하므로 여기서는 데이터만 전달하면 됩니다.
3-2. 'ServerForm.cs'(WinForm)
이 프로젝트에서 서버는 WinForm으로 만들었습니다.
이 클래스의 코드는 대부분 C# 지식만 있으면 이해하는 데 문제가 없습니다.
3-2-1. 'NewUser_OnMessaged' 이벤트
'User'클래스에서 메시지가 오면 발생하는 이벤트입니다.
명령어에 맞게 처리해주는 함수입니다.
/// <summary>
/// 유저가 서버에 알리는 메시지 이벤트
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <exception cref="NotImplementedException"></exception>
private void NewUser_OnMessaged(User sender, MessageEventArgs e)
{
StringBuilder sbMsg = new StringBuilder();
switch (e.m_typeCommand)
{
case ChatCommandType.Msg: //메시지
sbMsg.Append(sender.UserID);
sbMsg.Append(" : ");
sbMsg.Append(e.m_strMsg);
Commd_SendMsg(sbMsg.ToString());
break;
case ChatCommandType.ID_Check: //id체크
Commd_IDCheck(sender, e.m_strMsg);
break;
case ChatCommandType.User_List_Get: //유저 리스트 갱신 요청
Commd_User_List_Get(sender);
break;
}
}
3-2-2. 'AllUser_Send' 함수
접속 중인 전체 유저에게 메시지를 보냅니다.
/// <summary>
/// 접속중인 모든 유저에게 메시지를 보낸다
/// </summary>
/// <param name="sMsg"></param>
private void AllUser_Send(string sMsg)
{
//모든 유저에게 메시지를 전송 한다.
foreach (User insUser in m_listUser)
{
insUser.SendMsg_User(sMsg);
}
}
리스트의 모든 유저의 'SendMsg_User'를 호출하면 됩니다.
이것을 응용하여 특정 유저를 제외하고 메시지를 보낼 수도 있습니다.
4. 테스트 클라이언트 만들기
이 프로그램에서는 'DGSocketAssist1_Client'에서 만든 'Client' 클래스를 한 개만 생성하여 관리합니다.
대부분 UI 관련 내용입니다.
'Client' 초기화 부분은 다음과 같습니다.
//유아이를 세팅하고
UI_Setting(typeState.Connecting);
string nIP = "127.0.0.1";
int nPort = Convert.ToInt32(txtPort.Text);
//클라이언트 개체 생성
GloblaStatic.Client = new Client(nIP, nPort);
GloblaStatic.Client.OnConnectionComplete += Client_OnConnectionComplete;
GloblaStatic.Client.OnDisconnect += Client_OnDisconnect;
GloblaStatic.Client.OnDisconnectCompleted += Client_OnDisconnectCompleted;
GloblaStatic.Client.OnReceiveReady += Client_OnReceiveReady;
GloblaStatic.Client.OnMessaged += Client_OnMessaged;
DisplayMsg("서버 준비 완료");
//서버 접속 시작
GloblaStatic.Client.ConnectServer();
5번 줄 : 서버 주소와 포트를 저장하고
8번 줄 : 'Client'개체를 생성합니다.
18번 줄 : 설정된 'Client'개체를 이용하여 서버에 접속합니다.
마무리
아주 오래전에 만들어두고 최근에 다시 코드만 정리한 프로젝트입니다.
지금 생각하면 '왜 저렇게 만들엇지????'라는 생각이 드는 부분이 많은 코드입니다.
그래도 'SocketAsyncEventArgs'를 이용한 서버/클라이언트가 어떻게 동작하는지는 알기 쉬운 코드라 이렇게 남겨둡니다.
이 프로젝트는 '.NET 5'로 업그레이드하면 동작하지 않습니다.