안녕하세요!


이번 글에서는 이번 주제의 마지막 장인 1:N 비동기 채팅 프로그램 - 클라이언트 부분을 작성하도록 하겠습니다.

이전 글에서 윈폼 디자인이나 서버쪽을 만들면서 거의 대부분의 작업을 설명드렸기 때문에 이번 글의 내용은 상당히 짧습니다.


이전 글:


시작합니다!!


클라이언트에서 서버와 연결하고 통신하기 위해 해야 할 역할들은 다음과 같습니다.

  • 서버의 주소와 포트를 이용하여 연결을 시도해야 한다.
  • 비동기적으로 들어오는 데이터(텍스트)를 텍스트박스에 표시해야 한다.
  • 서버가 데이터를 보내지 않았어도 클라이언트가 원할 때 데이터를 보낼 수 있어야 한다.



서버의 주소와 포트를 이용하여 연결을 시도해야 한다.

서버 또는 특정 소켓에 연결하기 위해서는 정말로 간단하게 Socket.Connect 메서드를 호출하시면 됩니다.

원래는 DNS 형태 또는 점-10진 표기법으로 표기된 주소는 컴퓨터가 이해하지 못하기 때문에 변환 과정을 거쳐야 하는데, .NET의 소켓에서는 문자열을 넘겨주면 변환 과정을 내부에서 처리해주기 때문에 정말 간단하게 연결 작업을 수행할 수 있습니다.


소켓을 이용한 연결 (C#)
string address = "localhost"; // "127.0.0.1" 도 가능
int port = 12345;
mainSock.Connect(address, port);




비동기적으로 들어오는 데이터(텍스트)를 텍스트박스에 표시해야 한다.

이 주제는 서버를 다루는 글에서도 소개된 내용입니다.

Socket.BeginReceive 함수를 사용하여 비동기적으로 들어오는 데이터를 받을 수 있으며, 이전 글에서 소개된 AsyncObject 클래스가 추가 데이터로 사용됩니다.


비동기적으로 들어오는 데이터를 받기 위한 준비 (C#)
AsyncObject ao = new AsyncObject(4096);
ao.WorkingSocket = mainSock;
mainSock.BeginReceive(ao.Buffer, 0, ao.BufferSize, 0, DataReceived, ao);



그리고 받은 데이터를 표시해주는 작업을 해줘야 합니다.

이 작업은 서버와 동일하게 Cross-Thread 문제가 있기 때문에 아래 글을 꼭 읽어보시기 바랍니다.

(링크) '크로스 스레드 작업이 잘못되었습니다' - 넌 왜 나타나서 날 괴롭게 하니..


UI Thread에서의 컨트롤 작업 & 비동기로 수신한 데이터 처리 (C#)
delegate void AppendTextDelegate(Control ctrl, string s);
AppendTextDelegate _textAppender;

void AppendText(Control ctrl, string s) {
    // 텍스트를 추가해주는 대리자가 null이면 개체를 생성한다.
    if (_textAppender == null) _textAppender = new AppendTextDelegate(AppendText);
    
    // 컨트롤이 생성된 UI Thread가 아니라면 InvokeRequired 속성의 값이 true로 설정된다.
    // 따라서, Invoke를 통한 대리자 호출로 AppendText 메서드가 UI Thread에서 실행되도록 해줘야 한다.
    if (ctrl.InvokeRequired) ctrl.Invoke(_textAppender, ctrl, s);
    else {
        string source = ctrl.Text;
        ctrl.Text = source + Environment.NewLine + s;
    }
}

static void DataReceived(IAsyncResult ar) {
    // BeginReceive에서 추가적으로 넘어온 데이터를 AsyncObject 형식으로 변환한다.
    AsyncObject obj = (AsyncObject) ar.AsyncState;

    // 데이터 수신을 끝낸다.
    int received = obj.WorkingSocket.EndReceive(ar);

    // 받은 데이터가 없으면(연결끊어짐) 끝낸다.
    if (received <= 0) {
        obj.WorkingSocket.Close();
        return;
    }

    // UTF8 인코더를 사용하여 바이트 배열을 문자열로 변환한다.
    string text = Encoding.UTF8.GetString(obj.Buffer);

    // 0x01 기준으로 짜른다.
    // tokens[0] - 보낸 사람 IP
    // tokens[1] - 보낸 메세지
    string[] tokens = text.Split('\x01');
    string ip = tokens[0];
    string msg = tokens[1];

    // 텍스트박스에 추가해준다.
    // 비동기식으로 작업하기 때문에 폼의 UI 스레드에서 작업을 해줘야 한다.
    // 따라서 대리자를 통해 처리한다.
    AppendText(txtHistory, string.Format("[받음]{0}: {1}", ip, msg));
    
    // 클라이언트에선 데이터를 전달해줄 필요가 없으므로 바로 수신 대기한다.
    // 데이터를 받은 후엔 다시 버퍼를 비워주고 같은 방법으로 수신을 대기한다.
    obj.ClearBuffer();

    // 수신 대기
    obj.WorkingSocket.BeginReceive(obj.Buffer, 0, 4096, 0, DataReceived, obj);
}




서버가 데이터를 보내지 않았어도 클라이언트가 원할 때 데이터를 보낼 수 있어야 한다.

복잡하게 보일 수도 있지만, 정말 간단합니다.

앞에서 BeginReceive를 통해서 비동기식으로 데이터를 받도록 해줬기 때문에 데이터가 오기만을 무한정 대기할 필요가 없어진 것이죠.


그래서 이 주제에선 간단하게 텍스트를 바이트로 변환한 후 소켓으로 보내주기만 하면 끝입니다.


데이터 보내기 (C#)
void OnSendData(object sender, EventArgs e) {
    // 서버가 대기중인지 확인한다.
    if (!mainSock.IsBound) {
        MsgBoxHelper.Warn("서버가 실행되고 있지 않습니다!");
        return;
    }

    // 보낼 텍스트
    string tts = txtTTS.Text.Trim();
    if (string.IsNullOrEmpty(tts)) {
        MsgBoxHelper.Warn("텍스트가 입력되지 않았습니다!");
        txtTTS.Focus();
        return;
    }

    // 서버 ip 주소와 메세지를 담도록 만든다.
    IPEndPoint ip = (IPEndPoint) mainSock.LocalEndPoint;
    string addr = ip.Address.ToString();

    // 문자열을 utf8 형식의 바이트로 변환한다.
    byte[] bDts = Encoding.UTF8.GetBytes(addr + '\x01' + tts);

    // 서버에 전송한다.
    mainSock.Send(bDts);

    // 전송 완료 후 텍스트박스에 추가하고, 원래의 내용은 지운다.
    AppendText(txtHistory, string.Format("[보냄]{0}: {1}", addr, tts));
    txtTTS.Clear();
}



여기까지가 클라이언트의 전부입니다.

서버에서 거의 모든 내용과 개념을 설명하여 코드 제외하고 내용이 별로 없지만, 소켓 통신이란 것이 원래 이렇게 간단합니다.

내부적인 메커니즘까지 고려하면 정말 복잡하지만, .NET Framework에서 기본적으로 제공하는 소켓 클래스를 사용하면 단 몇 줄만에 서버-클라이언트 또는 Peer-to-Peer 통신이 가능해지죠.


그리고 대망의 최종 결과물입니다.

2개의 클라이언트와 한 개의 서버를 놓고 서로 채팅을 테스트하는 화면입니다.




마지막으로 클라이언트와 서버가 포함된 솔루션 파일을 올렸으니, 다운로드 받으셔서 직접 실행해보시고 확인해보시기 바랍니다 :)

여기까지 긴 글 읽어주시느라 고생하셨습니다.


MultiChat.zip


감사합니다.


p.s. 부족한 글이지만, 흥미를 갖고 읽어주시는 불꽃중년님께 감사드리고, 늦게 마무리해서 죄송합니다.

+ Recent posts