개발/C#

[C#] 채팅 프로그램

훈배 2023. 4. 4. 20:56

C#을 이용하여 멀티 스레드 소캣통신 기반의 채팅 프로그램을 작성해 보겠습니다.
 

C# .Net

닷넷 프레임워크(.NetFrameWork)는 2002년 마이크로소프트에서 발표한 응용프로그램 개발 환경으로써 프로세스 가상 머신에 속합니다. C#은 이러한 .Net 환경에서 구동되는 언어들 중 대표적인 하나의 언어라고 할 수 있습니다. 따라서  C#은 닷넷을 위해 태어났고, 닷넷과 함께 발전해 나가는 관계하고 할 수 있습니다. 아마 JAVA에 익숙하신 분들이라면 .Net = JVM, C# = JAVA로 생각하시면 됩니다. C#은 윈도우 프로그램을 작성하기 좋은 플랫폼이기 때문에 C#을 이용하여 소켓통신을 구현해 볼까 합니다. 
 
<소켓통신>

소켓통신

소캣통신에 대해 알아보겠습니다. 소켓통신 먼저 소켓(Socket)은 TCP/IP 기반 네트워크 통신에서 데이터 송수신의 마지막 접점을 말합니다. TCP/IP 4계층이나 OIS 7계층을 아시는 분들은 응용 SW 계층과

hunbae.tistory.com

 

설계

구조 설계

다수의 클라이언트가 하나의 서버에 접속하여 채팅할 수 있는 환경을 구축하기 위해 멀티 스레드 기반의 프로그램으로 설계합니다. 
 

*프로세스와 스레드 
프로세스 : 운영체제로부터 자원을 할당받은 독립적인 작업(연산)의 단위
스레드 : 프로세스가 할당받은 자원을 이용하는 연산이나 코드실행흐름의 단위

 

구조 요약

클라이언트들은 TcpClient( IP, PortNumber ) 함수를 통해 서버에 있는 TcpListener()에 접속합니다. 클라이언트의 접속이 감지되어 클라이언트의 요청을 Accept 할 때 새로운 Socket을 만들어 각각의 Socket들이 각각의 클라이언트를 상대할 수 있습니다.
 

UI 설계

Server UI

 

Server UI

 

  1. 클라이언트의 접속 및 퇴장 내역과 채팅 내역을 볼 수 있는 텍스트 필드입니다.
  2. 서버의 현재 상태를 표시하는 라벨입니다.
  3. 서버를 시작하고 종료할 수 있는 버튼입니다.

 

Client UI

 

Client UI

 

  1. 채팅에서 이용할 사용자 명을 기입하는 텍스트 필드입니다.
  2. 채팅 서버에 입장 및 종료 버튼입니다. 
  3. 채팅 내역이 올라오는 텍스트 필드입니다.
  4. 채팅 내용을 기입하는 텍스트 필드입니다.

 

주요 CODE

Server

private void AcceptClient() 
        {
            Socket socketClient = null;
            while (true) // listen()
            {
                try
                {
                    socketClient = chatServer.AcceptSocket(); // Accept()

                    ClientHandler clientHandler = new ClientHandler();
                    clientHandler.ClientHandler_Setup(this, socketClient, this.txtChatMsg);
                    Thread thd_ChatProcess = new Thread(new ThreadStart(clientHandler.Chat_Process)); // 각각의 클라이언트를 대응하는 스레드
                    thd_ChatProcess.Start();
                }
                catch (System.Exception)
                {
                    Form1.clientSocketArray.Remove(socketClient);
                    break;
                }
            }

        }

AcceptClient()를 통해 무한루프를 돌며 대기를 하다가(listen) 클라이언트의 접속이 감지되면 ClientHandler를 생성하여 통신에 필요한 변수들을 세팅한 후 Chat_Process 함수의 기능을 하는 스레드를 만들어 각각의 스레드들이 클라이언트와 대응하여 통신을 합니다.   

public void SetText(string text)
        {
            // t.InvokeRequired가 true를 반환하면
            // Invoke 메소드 호출을 필요로 하는 상태고 즉 현재 스레드가 UI스레드가 아님
            // 이 때 Invoek를 시키면 UI 스레드가 델리게이트에 설정된 에소드를 실행해준다.
            // false를 반환하면 UI 스레드가 접근하는 경우로 컨트롤레 직접 접근해도 문제가 없는 상태다.
            if (this.txtChatMsg.InvokeRequired)
            {
                SetTextDelegate d = new SetTextDelegate(SetText); // 델리게이트 선언
                this.Invoke(d, new object[] { text }); // 델리게이트를 통해 글을 쓴다.
                // 이 경우 UI 스레드를 통해 SetText를 호출함
            }
            else
            {
                this.txtChatMsg.AppendText(text); // 텍스트박스에 글을 씀
            }
        }

 
우리가 만든 Work 스레드의 경우 화면의 UI에 접근할 수 없으므로 델리게이트를 이용하여 UI 스레드를 통해 텍스트 박스에 인자로 넘어온 텍스트를 화면에 표시합니다.

 public void Chat_Process()
            {
                while (true)
                {
                    try
                    {
                        // 문자열을 받음
                        string lstMessage = strReader.ReadLine();
                        if(lstMessage != null && lstMessage != "")
                        {
                            form1.SetText(lstMessage + "\r\n");
                            byte[] byteSend_Data = Encoding.Default.GetBytes(lstMessage + "\r\n");
                            lock (Form1.clientSocketArray)
                            {
                                foreach(Socket socket in Form1.clientSocketArray)
                                {
                                    NetworkStream stream = new NetworkStream(socket);
                                    stream.Write(byteSend_Data, 0, byteSend_Data.Length);
                                }
                            }
                        }
                    }
                    catch (Exception ex) 
                    {
                        MessageBox.Show("채팅 오류 :" + ex.ToString());
                        Form1.clientSocketArray.Remove(socketClient);
                        break;
                    }
                }
            }

C#에서 제공하는 StreamReader를 통해 클라이언트로부터 넘어온 데이터를 ReadLine()을 통해 읽어 서버 측 화면에 SetText()를 통해 표시합니다. 읽어 들인 메시지는 바이트로 변환하여 데이터를 주고받을 수 있는 NetworkStream 객체를 통해 Write 메서드로 전체 클라이언트에게 전송합니다. 

 private void btnStart_Click(object sender, EventArgs e)
        {
            try
            {
                //서버가 꺼져있을 경우
                if (lblMsg.Tag.ToString() == "Stop")
                {
                    chatServer.Start(); //서버 시작
                    Thread waitThread = new Thread(new ThreadStart(AcceptClient)); // 클라이언트 접속 대기 스레드 
                    waitThread.Start(); // 스레드 시작

                    lblMsg.Text = "서버 시작 됨";
                    lblMsg.Tag = "Start";
                    btnStart.Text = "서버 종료";
                }
                else
                {
                    chatServer.Stop();
                    foreach (Socket socket in Form1.clientSocketArray)
                    {
                        socket.Close(); 
                    }
                    clientSocketArray.Clear();

                    lblMsg.Text = "서버 중지 됨";
                    lblMsg.Tag = "Stop";
                    btnStart.Text = "서버 시작";
                }

            }
            catch (Exception ex)
            {
                MessageBox.Show("서버를 시작할 수 없습니다. :" + ex.Message);
            }
        }

앞서 언급된 기능들을 서버 시작 버튼을 트리거로 위와 같이 실행합니다.
 

Client

private void Message_Snd(string lstMessage, Boolean Msg)
        {
            try
            {
                //보낼 데이터를 읽어 Default 형식의 바이트 스트림으로 변환 해서 전송
                string dataToSend = lstMessage + "\r\n";
                byte[] data = Encoding.Default.GetBytes(dataToSend);
                ntwStream.Write(data, 0, data.Length);
            }
            catch (Exception Ex)
            {
                if (Msg == true)
                {
                    MessageBox.Show("서버가 Start 되지 않았거나\n\n" + Ex.Message, "Client");
                    btnConnect.Text = "입장";
                    chatHandler.ChatClose();
                    ntwStream.Close();
                    tcpClient.Close();
                }
            }
        }

Message_Snd 메서드를 통해 서버로 보낼 메시지를 바이트로 변환하여 NetworkStream 객체를 통하여 서버로 보냅니다. 이 외의 상황은 예외처리를 해줍니다.

//다른 스레드인 ChatHandler의 쓰레드에서 호출하는 함수로
        //델리게이트를 통해 채팅 문자열을 텍스트박스에 씀
        public void SetText(string text)
        {
            if (this.txtChatMsg.InvokeRequired)
            {
                SetTextDelegate d = new SetTextDelegate(SetText);
                this.Invoke(d, new object[] { text });
            }
            else
            {
                this.txtChatMsg.AppendText(text);
            }
        }

SetText 메서드에서는 델리게이트를 통해 Work 스레드에서는 접근할 수 없는 UI 텍스트 박스에 메시지를 표시합니다.

public void ChatProcess()
        {
            while (true)
            {
                try
                {
                    //문자열을 받음
                    string lstMessage = strReader.ReadLine();
                    if (lstMessage != null && lstMessage != "")
                    {
                        //SetText 메서드에서 델리게이트를 이용하여 서버에서 넘어오는 메시지를 쓴다.
                        form1.SetText(lstMessage + "\r\n");
                    }
                }
                catch (System.Exception)
                {
                    break;
                }
            }
        }

ChatProcess 메서드에서는 SetText 메서드를 이용하여 서버에서 넘어오는 데이터를 텍스트 박스에 표시합니다.

//입장 버튼 클릭
        private void btnConnect_Click(object sender, EventArgs e)
        {
            if (btnConnect.Text == "입장")
            {
                try
                {
                    tcpClient = new TcpClient();
                    tcpClient.Connect(IPAddress.Parse("127.0.0.1"), 8080);
                    ntwStream = tcpClient.GetStream();
                    chatHandler.Setup(this, ntwStream, this.txtChatMsg);

                    Thread chatThread = new Thread(new ThreadStart(chatHandler.ChatProcess));
                    chatThread.Start();
                    Message_Snd("<" + txtName.Text + "> 님께서 접속 하셨습니다.", true);
                    btnConnect.Text = "나가기";
                }
                catch (System.Exception Ex)
                {
                    MessageBox.Show("Server 오류발생 또는 Start 되지 않았거나\n\n" + Ex.Message, "Client");
                }
            }
            else
            {
                Message_Snd("<" + txtName.Text + "> 님께서 접속해제 하셨습니다.", false);
                btnConnect.Text = "입장";
                chatHandler.ChatClose();
                ntwStream.Close();
                tcpClient.Close();
            }
        }

 입장 버튼을 눌렀을 때 서버로 접속하여 채팅을 시작합니다.
 

Test

시연 영상

 잘 작동되는 것을 볼 수 있습니다!
 

ChatClient.zip
0.18MB
ChatServer.zip
0.18MB

 
 
 
 
오늘은 소켓통신을 이용한 채팅 프로그램을 작성해 봤는데요. 통신을 하려면 기본적으로 알아야 할 개념인 것 같습니다.
 

** 틀린 내용이 있을 시 지적해 주시면 감사하겠습니다.