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

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

by devjh 2020. 7. 27.
반응형

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 


 

저번 게시글에서는 서버가 클라이언트의 메시지를 반복적으로 읽는 프로그램을 만들어보았습니다.

 

이제 나름대로 1:1통신은 가능해졌습니다.

 

그러나 치명적인 단점이 아직 존재합니다.

 

저번에 만든 서버는 GetStream().Read에서 클라이언트 message를 기다리며

 

아무것도 못하고있습니다.(블락됐다고 합니다.)

 

채팅프로그램을 만드려면 서버가 여러 클라이언트의 요청에 응답을 해야하는데

 

서버가 하나의 클라이언트에 묶여 아무것도 못하고있으면 곤란합니다.

 

그 단점을 이번게시글에서 보완하겠습니다.

 

해당 게시글은 비동기와 콜백에 대한 이해가 필요합니다.

 

예제 링크 남깁니다.

 

frozenpond.tistory.com/26

 

[c#] BeginInvoke사용법 및 예제 -1-

개요 1. Thread 2. lock 3. BeginInvoke(비동기, 스레드풀) 4. BeginInvoke2(비동기 리턴값) 5. BeginInvoke3(콜백) 6. BeginInvoke4(BeginInvoke) 이번게시글에서는 BeginInvoke에 대해 알아보겠습니다. BeginInv..

frozenpond.tistory.com

 

큰 틀로 봤을때는 두개의 스레드가 돌아갑니다.(한개는 비동기작업으로 스레드풀의 스레드입니다.)

 

하나의 스레드에서는 서버 구동여부를 체크하며 서버구동중이라는 메시지를 출력해주고

 

다른하나의 스레드(스레드풀의 스레드)에서는 접속한 클라이언트의 메시지를 대기, 출력, 대기, 출력을 반복합니다..

 

첫번째스레드가 당장은 의미가 없는 쓰레드이긴 하지만 추후 채팅서버에서는 메인문이 돌아갈 스레드이기에 따로 나눠놨으며

 

두번째 스레드(스레드풀의 스레드)는 매우매우 중요한 비동기서버의 핵심인 스레드입니다.

 

기능상으로만 본다면 저번 게시글에서 쓰레드를 하나 더 만들어 서버구동중이라는 메시지를 출력하는것과 큰 차이가 없어보일수 있지만 

 

이번에는 쓰레드를 하나 만들어서 while루프를 돌리는게아닌

 

BeginRead를 이용해서 쓰레드풀에서 클라이언트가 보낸 메시지를 처리 할 예정입니다.

 

 

 

1. Program.cs

class Program
{
    static void Main(string[] args)
    {
    	MyServer a = new MyServer();
    }

}

MyServer라는 클래스의 객체를 만듭니다.

 

2. MyServer.cs

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

    private void AsyncServerStart()
    {
        // 서버 포트설정 및 시작
        TcpListener listener = new TcpListener(new IPEndPoint(IPAddress.Any, 9999));
        listener.Start();
        Console.WriteLine("서버를 시작합니다. 클라이언트의 접속을 기다립니다..");

        // 연결을 요청한 client의 객체를 acceptClient에 저장
        TcpClient acceptClient = listener.AcceptTcpClient();
        Console.WriteLine("클라이언트 접속성공.");

        // ClientData의 객체를 생성해주고 연결된 클라이언트를 ClientData의 멤버로 설정해준다
        ClientData clientData = new ClientData(acceptClient);

        // BeginRead를 통해 비동기로 읽는다.(읽을데이터가 올때까지 대기하지않고 바로 아랫줄의 while문으로 이동한다)
        clientData.client.GetStream().BeginRead(clientData.readByteData, 0, clientData.readByteData.Length, new AsyncCallback(DataReceived), clientData);

        // 데이터를 읽든 못읽든 일단 바로 해당로직이 실행된다(비동기서버)
        while (true)
        {
            Console.WriteLine("서버 구동중");
            Thread.Sleep(1000);
        }
    }

    private void DataReceived(IAsyncResult ar)
    {
        // 콜백메서드입니다.(피호출자가 호출자의 해당 메서드를 실행시켜줍니다)
        // 즉 데이터를 읽었을때 실행됩니다.

        // 콜백으로 받아온 Data를 ClientData로 형변환 해줍니다.
        ClientData callbackClient = ar.AsyncState as ClientData;

        //실제로 넘어온 크기를 받아옵니다
        int bytesRead = callbackClient.client.GetStream().EndRead(ar);

        // 문자열로 넘어온 데이터를 파싱해서 출력해줍니다.
        string readString = Encoding.Default.GetString(callbackClient.readByteData,0,bytesRead);

        Console.WriteLine(readString);

        // 비동기서버에서 가장중요한 핵심입니다. 
        // 비동기서버는 while문을 돌리지않고 콜백메서드에서 다시 읽으라고 비동기명령을 내립니다.
        callbackClient.client.GetStream().BeginRead(callbackClient.readByteData, 0, callbackClient.readByteData.Length, new AsyncCallback(DataReceived), callbackClient);
    }
}

생성자에서 AsyncServerStart를 호출하며 서버가 시작됩니다.

 

TcpClient acceptClient = listener.AcceptTcpClient();

 

이 구문에서 서버는 클라이언트가 접속할때까지 기다립니다.(블락됩니다.)

 

접속한 클라이언트는 acceptClient에 저장되며

 

해당 객체는 ClientData라는 클래스를 생성할때 생성자의 매개변수로 들어갑니다.

 

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

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

 

생성자로 들어온 acceptClient객체는 ClientData객체의 멤버로 저장됩니다.

 

 

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

 

그 다음줄에서 매개변수가 굉장히 긴 BeginRead가 등장합니다.

 

찬찬히 살펴보겠습니다.

 

 

(1) 먼저 이 구문은 비동기입니다. BeginInvoke, BeginRead, BeginWrite 등 앞에 Begin이 붙은 메서드들은 비동기입니다.

 

이전게시물의 Read는 클라이언트가 메시지를 보낼때까지 기다리지만(블락됐다고 합니다.)

 

BeginRead 해당코드를 실행해놓고 응답을 받던 못받던, 얼마의 시간이 걸리든 신경쓰지않고 바로 아랫줄로 넘어갑니다.

 

아랫줄은 while문으로 1초에 한번씩 "서버 구동중"을 출력해줍니다.

 

 

 

 

(2) BeginRead에 첫번째 두번째 세번째 매개변수까지는 저번 게시글의 Read와 크게 다르지 않습니다.

 

그러나 네번째 매개변수에 new AsyncCallback(DataReceived) 라는 구문이 등장합니다.

 

DataReceived는 제가 만든 메서드명이고, 중요한건 AsyncCallback입니다.

 

AsyncCallback의 매개변수에 콜백메서드를 등록해주면

 

클라이언트가 메시지를 보내서 서버가 해당 메시지를 읽게 됐을 때 콜백메서드가 실행됩니다.(쓰레드풀의 쓰레드가 실행합니다.)

 

해당 방법을 통해 서버는 블락되지않고 Read과정을 할수 있게 됩니다.

 

다섯번째 매개변수로는 만들어준 ClientData의 객체를 넘겨줍니다.

 

 

(3) DataReceived

 

위에서 설명했듯이 해당메서드는 피호출자(클라이언트)가 실행해 준 메서드입니다.(콜백메서드)

 

매개변수에 있는 IAsyncResult ar을 BeginRead의 마지막 매개변수로 넣은 ClientData로 형변환을 해줍니다.

 

ClientData callbackClient = ar.AsyncState as ClientData;

 

형변환이 끝났다면 저번게시물에서 했던것과 비슷하게 실제크기를 받아오고 출력을 해줍니다.

 

그다음이 비동기서버에 가장 중요한 개념입니다.

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

콜백메서드에 또 BeginRead가 들어갑니다.

 

해당구문이 없다다면

 

콜백메서드로 클라이언트가 보낸 메시지를 콜백메서드에서 한번 읽고 이 서버는 더이상 메세지를 읽을수 없게됩니다.

 

그러나 콜백메서드안에 BeginRead를 다시 넣어주므로써 또 메시지가 오면 콜백메서드가 호출 될 수 있게 해준겁니다.

 

위의 방법을 통해 비동기서버는 while루프를 돌리지않고 클라이언트의 메시지를 계속해서 받을 수 있습니다.

 

 

3. 실행화면

실행화면입니다.(클라이언트는 저번게시글의 클라이언트를 사용했습니다.)

 

 

 

 

다음 게시글에서는 1:1비동기 통신이 아닌

 

1:N 비동기 통신 게시글을 포스팅하겠습니다.

 

반응형

댓글