본문 바로가기
wpf/c#, wpf 채팅프로그램

c# 채팅프로그램 만들기 - 4 - 소켓통신(TcpListener, TcpClient)을 이용한 1:N 통신을 해보자(쓰레드풀을 활용한 비동기서버)

by devjh 2020. 8. 5.
반응형

채팅프로그램 만들기는 총 12개의 게시글로 구성되어있습니다.

 

첫번째 게시글 : 1:1단발성통신(동기서버 동기클라)

 

두번째 게시글 : 1:1지속성통신(동기서버 완성본 동기 클라이언트)

 

세번째 게시글 : 1:1통신(비동기서버)

 

네번째 게시글: 1:N통신(여기서부터는 여러명을 받아야하므로 당연히 비동기서버입니다.)

 

다섯번째 게시글 : 채팅프로그램 콘솔 서버

 

여섯번째 게시글 : 채팅프로그램 콘솔 클라이언트

 

일곱번째 게시글 : wpf를 통해 View를 구현한 서버

 

여덟번째 게시글 : wpf를 통해 View를 구현한 클라이언트(메인화면 만들기)

 

아홉번째 게시글 : wpf를 통해 View를 구현한 클라이언트(로그인화면 만들기)

 

열번째 게시글 : wpf를 통해 View를 구현한 클라이언트(채팅상대 선택화면 만들기)

 

열한번째 게시글 : wpf를 통해 View를 구현한 클라이언트(채팅화면 만들기)

 

열두번째 게시글 : wpf를 통해 View를 구현한 클라이언트(로직 구현)

 


저번 게시글에서는 하나의 클라이언트와 비동기통신을 하며

 

"서버구동중"이라는 문자열을 콘솔에 출력하는 기능을하는 비동기서버를 만들어 보았습니다.

 

이번에는 비동기통신 이후 콘솔에 의미없는 데이터를 출력해 주는것이 아닌

 

비동기 통신 이후 또 다른 클라이언트를 받아서 비동기통신을 시키는

 

1:N서버를 만들어보겠습니다.

 

1. 서버

namespace AsynchronousServer
{
    class MyServer
    {
        public MyServer()
        {
            // 서버시작
            AsyncServerStart();
        }

        private void AsyncServerStart()
        {
            TcpListener listener = new TcpListener(new IPEndPoint(IPAddress.Any, 9999));
            listener.Start();
            Console.WriteLine("서버를 시작합니다.");

            // 클라이언트의 접속을 확인하면 스레드풀에서 해당클라이언트의 메시지를 읽도록 대기시키고
            // while문을 통해 다시 클라이언트의 접속을 기다립니다.
            while (true)
            {
                TcpClient acceptClient = listener.AcceptTcpClient();

                ClientData clientData = new ClientData(acceptClient);
            
                clientData.client.GetStream().BeginRead(clientData.readByteData, 0, clientData.readByteData.Length, new AsyncCallback(DataReceived), clientData);
            }
        }

        private void DataReceived(IAsyncResult ar)
        {
            ClientData callbackClient = ar.AsyncState as ClientData;
            
            int bytesRead = callbackClient.client.GetStream().EndRead(ar);

            string readString = Encoding.Default.GetString(callbackClient.readByteData,0,bytesRead);

            Console.WriteLine("{0}번 사용자 : {1}",callbackClient.clientNumber,readString);

            callbackClient.client.GetStream().BeginRead(callbackClient.readByteData, 0, callbackClient.readByteData.Length, new AsyncCallback(DataReceived), callbackClient);
        }
    }
}

 

클라이언트를 받아아서 비동기읽기 작업을 시키는 부분을

 

while문을 통해 무한루프를 걸어줍니다.

 

간단하게 로직을 정리하면

 

1. 서버는 클라이언트를 대기한다.

2. 클라이언트가접속하면 비동기읽기작업을 시킨다.(콜백메서드에서 콘솔창에 보낸 메시지 출력)

3. while루프로 인해 서버는 다시 클라이언트를 대기하며 12과정을 반복한다.

 

(접속하는 클라이언트의 숫자만큼 스레드풀의 스레드갯수가 증가합니다.)

 

 

2. 클라이언트 구분방법

이제 어떤 클라이언트가 메시지를 보낸건지 클라이언트를 구별하는 코드도 추가하겠습니다.

 

저번게시글에 이어서 나온 ClientData 클래스입니다.

 

namespace AsynchronousServer
{
    class ClientData
    {
        // 연결이 확인된 클라이언트를 넣어줄 클래스입니다.
        // readByteData는 stream데이터를 읽어올 객체입니다.
        public TcpClient client { get; set; }
        public byte[] readByteData { get; set; }
        public int clientNumber;

        public ClientData(TcpClient client)
        {
            this.client = client;
            this.readByteData = new byte[1024];

            // 아래부분이 1:1비동기서버에서 추가된 부분입니다.
            // 127.0.0.1:9999에서 포트번호 직전 마지막번호를 클라이언트 번호로 지정해줍니다.
            string clientEndPoint = client.Client.LocalEndPoint.ToString();
            char[] point = { '.', ':' };
            string[] splitedData = clientEndPoint.Split(point);
            this.clientNumber = int.Parse(splitedData[3]);
            Console.WriteLine("{0}번 사용자 접속성공",clientNumber);
        }
    }
}

 

 

ClientData클래스에 멤버변수로 ClientNumber가 추가되었습니다.

 

TcpClient.Client.LocalEndPoint와 TcpcCient.Client.RemoteEndPoint를 사용하면

 

현재 연결된 클라이언트의 ip주소를 받아올 수 있습니다.

 

RemoteEndPoint는 접속한 클라이언트의 원격끝점(실제 서버에서 클라이언트의 Ip를 받아올때 사용)을

 

LocalEndPoint는 접속한 클라이언트의 로컬끝점(클라이언트가 접속할때 입력한 루프백 아이피)를 받아올 수 있습니다.

 

또한 127.0.0.1, 127.0.0.2, 127.0.0.x는 모두 루프백 IP로 사용가능합니다.

 

해당방법으로 하나의 PC에서 서버에서 구별할 수 있는 여러개의 클라이언트를 접속 시킬 수 있게됩니다.

 

저는 디버깅용이므로 루프백 아이피로 접속을 했으니 서버에서는 LocalEndPoint를 사용해서 입력한 루프백 아이피를 받아오겠습니다.

 

접속한 사람들은 모두 네번째 옥텟인 호스트ID가 다르므로 저는 호스트ID로 클라이언트를 구별할 예정입니다.

 

(실제로 여러 PC에서 사용하려면 RemoteEndPoint로 받아와야합니다.)

 

추가로 윈도우 + R키를 눌러 cmd를 입력한 후 ipconfig를 입력해서 ipv4주소를 찾아보세요

 

192.168.x.y 로 예로 들면

 

192로 시작하는 ipv4주소를 C클래스 주소라고하며 192.168.x까지를 네트워크ID

 

y를 호스트ID라고 부릅니다.

 

ipv4는 총 네부분으로 구성되며 4개의 옥텟으로 구성됐다고 얘기합니다.

 

(각각의 옥텟은 8비트로 구성되어  0부터 255(11111111)까지 지정이 가능하며 255와 127은 특수한 기능을 하고있습니다.)

 

여기에 127.0.0.1대신 192.168.x.y를 입력해도 서버와의 통신이 똑같이 가능합니다.

 

공인ip 사설ip 등등 구글링해서 개념을 잡아줍니다.

 

192.168로 시작하는건 공유기의 ISP에서 제공하는 공인ip를 나눠쓰는 사설ip입니다.

 

해당게시글은 사설ip에서 사용하는걸 목표로 작성하였습니다. 

 

3. 클라이언트

예전에 만든 클라이언트를 보겠습니다.

 

위에도 설명했지만 LocalEndPoint를 사용하면 클라이언트에서 입력한 루프백 아이피를 얻어올 수 있습니다.

 

클라이언트를 실행시킬 때

 

하나는 127.0.0.1로 연결, 다른 하나는 127.0.0.2로 연결시키고

 

서버에서 네번째 옥텟으로 clientnumber를 저장해주면

 

127.0.0.1은 1번사용자가, 127.0.0.2는 2번사용자가 됩니다.

 

(클라이언트의 Connect 메서드를 이렇게 수정합니다.)

 

private void Connect()
{
    client = new TcpClient();
    client.Connect("127.0.0.2", 9999);
    Console.WriteLine("서버연결 성공 이제 Message를 입력해주세요");
    Console.ReadKey();
}

클라이언트가 접속할때 ip별로 사용자를 구분해주고

 

콜백메서드에 메시지를 보낸사용자가 누구인지 ClientNumber를 함께 출력해주면 

 

1:N 통신(비동기서버) 완성입니다.

 

Console.WriteLine("{0}번 사용자 : {1}",callbackClient.clientNumber,readString);

 

4. 결과

 

이번 게시글에서 1:N 비동기서버를 구현해봤습니다.

 

현재 서버의 기능은 여러 클라이언트들의 접속을 받아오고

 

각각의 클라이언트가 보낸 메시지를 콘솔에 출력해주고 있습니다.

 

다음게시글에서는 클라이언트가 서버에게 수신자를 설정하여 메시지를 보내면

 

서버는 해당 클라이언트에게 메시지를 보내주는 채팅서버를 구현하겠습니다.

반응형

댓글