프로그래밍/C#, .NET

[.NET 4.8] 'SocketAsyncEventArgs'로 구현한 채팅 샘플 이해하기(1) - 문자열을 주고받는 서버 클라이언트를 구현해 보자.

당근천국 2022. 6. 29. 15:30

'SocketAsyncEventArgs'의 이해를 위한 글을 여러 번 썼었는데.....

'.NET 5' 이후로는 'SocketAsyncEventArgs'가 이전과 살짝 다른 동작을 합니다.

그래서 '.NET 5'로 넘어가기 전에 총정리 겸 단계별 샘플을 만들었습니다.

 

연관글 영역

 

 

0. 구조

각 샘플은 거의 같은 구조로 되어 있습니다.

샘플 소스 :

dang-gun/DGSocketAssist/

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'로 업그레이드하면 동작하지 않습니다.