'SocketAsyncEventArgs'의 이해를 위한 글을 여러 번 썼었는데.....
'.NET 5' 이후로는 'SocketAsyncEventArgs'가 이전과 살짝 다른 동작을 합니다.
그래서 '.NET 5'로 넘어가기 전에 총정리 겸 단계별 샘플을 만들었습니다.
각 샘플은 거의 같은 구조로 되어 있습니다.
샘플 소스 :
dang-gun/DGSocketAssist/DGSocketAssist1/
라이브러리 형태로 만들어져 있어 "DGSocketAssist_Server", "DGSocketAssist_Client"만 참조하여 서버/클라이언트 프로그램을 만들 수 있습니다.
'DGSocketAssist_Server'는 'Server.cs'와 'ClientListener.cs'로 되어있습니다.
'ClientListener.cs'는 서버에 접속한 하나의 클라이언트를 관리하는 클래스고
'Server.cs'는 클라이언트 리스트를 관리하는 클래스입니다.
클라이언트가 접속되면 서버는 제일 먼저 'ClientListener'를 생성합니다.
이 클래스가 생성될 때 송신용/수신용 용도로 'SocketAsyncEventArgs'가 2개 생성됩니다.
그때그때 생성하여 사용하는 방법도 있습니다.
다만 이 방법은 가비지 컬렉터의 압력을 증가시키는 문제가 있습니다.
그래서 2개를 생성하고 재활용하는 방식을 사용합니다.
샘플에 따라 쓰레드풀을 사용하는 경우 'SocketAsyncEventArgs'를 상위 클래스에서 전달받아 사용하기도 합니다.
클라이언트가 연결되고 첫 메시지를 받을 준비를 하는 함수입니다.
서버에서 접속한 유저에 대한 처리가 끝나면 호출합니다.
/// <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'를 전달하여 설정합니다.
수신용 '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'를 재활용하게 됩니다.
이런 구현방식이 없던 예전에는 무한루프를 돌려서 버퍼를 확인하는 방법을 썼습니다.
뭐....이것도 내부적으로는 무한루프가 돌고 있긴 하겠죠? ㅎㅎㅎ
연결된 클라이언트에게 메시지를 전송하기 위한 합수입니다.
/// <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'를 소켓에 전달합니다.
송신이 완료되면 송신용 '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'를 이용한 송신이 비정상적이라 이것도 그것과 관련 있는게 아닌가 싶습니다.
서버에 접속한 클라이언트를 관리하는 클래스입니다.
접속자를 기다리기 위한 소켓과
접속자를 관리하기 위한 리스트를 가지고 있습니다.
/// <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를 별도로 설정하지 않아도 됩니다.
포트만 전달받아 클라이언트를 기다릴 소켓을 생성합니다.
위에서 생성한 소켓을 수신 대기 상태로 만들어 줍니다.
/// <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'이벤트가 콜백 됩니다.
클라이언트가 접속되면 발생하는 이벤트입니다.
클라이언트 처리를 할 '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번 줄 : 다음 클라이언트의 접속을 기다립니다.
이 라이브러리는 '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'를 세팅합니다.
서버에 연결을 시도하는 함수입니다.
/// <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'를 전달하여 서버에 접속을 시도합니다.
서버 접속 시도에 대한 이벤트입니다.
접속 시도가 성공하였다면 첫 번째 데이터를 보냅니다.
/// <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'가 사용됩니다.
첫 접속 이후 서버로 부터 오는 데이터를 받는 이벤트입니다.
'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번 줄 : 다음 데이터를 기다립니다.
서버로 데이터를 보내는 함수입니다.
이것도 '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
}
전송 완료에 대한 이벤트 콜백입니다.
이것도 'ClientListener'와 동일합니다.
private void SaeaSend_Completed(object sender, SocketAsyncEventArgs e)
{
//유저 소켓
Socket socketClient = (Socket)sender;
MessageData mdMsg = (MessageData)e.UserToken;
//데이터 보내기 마무리
socketClient.Send(mdMsg.Data);
}
UI 관련 내용은 소스의 주석을 참고하면 됩니다.
여기서는 중요한 내용만 짚고 넘어갑니다.
'ClientListener'는 접속자의 네트워크 정보를 처리하므로
실질적인 유저의 정보를 관리할 클래스가 필요합니다.
이 클래스는 서버로부터 온 데이터에서 명령어를 추출하여 실질적인 동작을 요청하는 클래스입니다.
서버에서 데이터가 오면 명령어를 분리하여 필요한 동작을 호출하는 함수입니다.
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;
}
}
}
이 프로젝트는 모든 데이터가 문자열로 오므로
문자열에서 구분자로 명령어를 때서 처리합니다.
이 클라이언트를 관리하는 서버에 메시지 보내기를 요청하는 함수입니다.
다른 서버에 하는 요청이나 다른 클라이언트에게 메시지를 보낼 때 사용합니다.
/// <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'에 데이터를 담아 전달하게 됩니다.
이 클라이언트에게 메시지를 보내는 함수입니다.
/// <summary>
/// 이 유저에게 메시지를 보낸다.
/// </summary>
/// <param name="sMsg"></param>
public void SendMsg_User(string sMsg)
{
this.ClientListenerMe.Send(sMsg);
}
'ClientListener'에서 처리하므로 여기서는 데이터만 전달하면 됩니다.
이 프로젝트에서 서버는 WinForm으로 만들었습니다.
이 클래스의 코드는 대부분 C# 지식만 있으면 이해하는 데 문제가 없습니다.
'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;
}
}
접속 중인 전체 유저에게 메시지를 보냅니다.
/// <summary>
/// 접속중인 모든 유저에게 메시지를 보낸다
/// </summary>
/// <param name="sMsg"></param>
private void AllUser_Send(string sMsg)
{
//모든 유저에게 메시지를 전송 한다.
foreach (User insUser in m_listUser)
{
insUser.SendMsg_User(sMsg);
}
}
리스트의 모든 유저의 'SendMsg_User'를 호출하면 됩니다.
이것을 응용하여 특정 유저를 제외하고 메시지를 보낼 수도 있습니다.
이 프로그램에서는 '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'로 업그레이드하면 동작하지 않습니다.