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

c# 채팅프로그램 만들기 - 6 - 소켓통신(TcpListener, TcpClient)을 이용한 채팅프로그램을 만들어보자(콘솔 채팅클라이언트)

by devjh 2020. 8. 20.
반응형

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 


이번 게시글에서는 콘솔 클라이언트를 포스팅하겠습니다.

 

1. 서버의 주요기능

총 3개의 스레드 + 접속한 클라이언트의 수만큼의 스레드풀의 스레드(3명이 접속하였다면 3 + 3 =6 개)가 돌아갑니다.

 

같은스레드는 같은 번호를 묶었고 스레드풀에서 구동되는것은 빨간색으로 구분하였습니다.

 

1. 포트를 설정하고 서버를 구동합니다.

 

1-2. 클라이언트의 연결을 대기하고(블락) 연결된 클라이언트의 객체를 받아옵니다.

 

1-3. 받아온 클라이언트의 리소스를 저장해놓은후, 해당 클라이언트가 본낸 메시지를 스레드풀로 읽을준비를 시킵니다.(1-2, 1-3번과정 반복, 같은스레드에서 돌아가므로 같은번호로 묶었습니다.)

 

2. 1-3번에서 저장해놓은 클라이언트들에게서 "수신자<메시지>"가 오면, 스레드풀에서 송신자이름으로 수신자에게 메시지를 보내주고 로그를 남겨줍니다.(스레드풀을 활용한 비동기서버)

 

3. 하트비트 스레드를 돌려 현재 접속한 유저를 수시로 확인하고 접속을 종료한 클라이언트에게 메시지를 보내는것을 방지해줍니다.

 

4. 콘솔창으로 UI를 출력해줍니다.(메인스레드)

 

 

2. 클라이언트의 주요 기능

클라이언트는 두개의 스레드로 구성되어있습니다.

 

1. 메인스레드는 콘솔창을 돌리며 사용자가 여러가지 기능에 접근하도록 합니다.(서버접속,이름설정,메시지보내기 등)

 

2. 사용자가 서버에 접속을 하면 서버가 보낸 메시지를 읽을 스레드를 돌려줍니다.

 

1. Program.cs

namespace ChattingServiceConsoleClient
{
    class Program
    {
        static void Main(string[] args)
        {
            ConsoleClient a = new ConsoleClient();
            a.Run();
        }
    }
}

 

2. ConsoleClient.cs

namespace ChattingServiceConsoleClient
{
    class ConsoleClient
    {
        TcpClient client = null;
        Thread receiveMessageThread = null;
        ConcurrentBag<string> sendMessageListToView = null;
        ConcurrentBag<string> receiveMessageListToView = null;
        private string name = null;

        // 서버를 구동합니다.
        public void Run()
        {
            sendMessageListToView = new ConcurrentBag<string>();
            receiveMessageListToView = new ConcurrentBag<string>();

            receiveMessageThread = new Thread(receiveMessage);
            while (true)
            {
                Console.WriteLine("==========클라이언트==========");
                Console.WriteLine("1.서버연결");
                Console.WriteLine("2.Message 보내기");
                Console.WriteLine("3.보낸 Message확인");
                Console.WriteLine("4.받은 Message확인");
                Console.WriteLine("0.종료");
                Console.WriteLine("==============================");

                string key = Console.ReadLine();
                int order = 0;


                if (int.TryParse(key, out order))
                {
                    switch (order)
                    {
                        case StaticDefine.CONNECT:
                            {
                                if (client != null)
                                {
                                    Console.WriteLine("이미 연결되어있습니다.");
                                    Console.ReadKey();
                                }
                                else
                                {
                                    Connect();
                                }

                                break;
                            }
                        case StaticDefine.SEND_MESSAGE:
                            {
                                if (client == null)
                                {
                                    Console.WriteLine("먼저 서버와 연결해주세요");
                                    Console.ReadKey();
                                }
                                else
                                {
                                    SendMessage();
                                }
                                break;
                            }
                        case StaticDefine.SEND_MSG_VIEW:
                            {
                                SendMessageView();
                                break;    
                            }
                        case StaticDefine.RECEIVE_MSG_VIEW:
                            {
                                ReceiveMessageVIew();
                                break;
                            }

                        case StaticDefine.EXIT:
                            {
                                if(client != null)
                                {
                                    client.Close();
                                }
                                receiveMessageThread.Abort();
                                return;
                            }
                    }
                }

                else
                {
                    Console.WriteLine("잘못 입력하셨습니다.");
                    Console.ReadKey();
                }
                Console.Clear();
                Thread.Sleep(50);
            }
        }

        // 사용자로부터 받은 메시지를 확인하는 기능입니다.
        private void ReceiveMessageVIew()
        {
            if (receiveMessageListToView.Count == 0)
            {
                Console.WriteLine("받은 메시지가 없습니다.");
                Console.ReadKey();
                return;
            }

            foreach (var item in receiveMessageListToView)
            {
                Console.WriteLine(item);
            }
            Console.ReadKey();
        }

        // 사용자에게 보낸 메시지를 확인하는 기능입니다.
        private void SendMessageView()
        {
            if (sendMessageListToView.Count == 0)
            {
                Console.WriteLine("보낸 메시지가 없습니다.");
                Console.ReadKey();
                return;
            }
            foreach (var item in sendMessageListToView)
            {
                Console.WriteLine(item);
                
            }
            Console.ReadKey();
        }

        // 서버에서 보낸 메시지를 읽어주는 메서드이며 스레드를 생성해 돌려줍니다. 
        private void receiveMessage()
        {
            string receiveMessage = "";
            List<string> receiveMessageList = new List<string>();
            while (true)
            {
                byte[] receiveByte = new byte[1024];
                client.GetStream().Read(receiveByte, 0, receiveByte.Length);
                
                receiveMessage = Encoding.Default.GetString(receiveByte);

                string[] receiveMessageArray = receiveMessage.Split('>');
                foreach (var item in receiveMessageArray)
                {
                    if (!item.Contains('<'))
                        continue;
                    // 관리자<TEST>는 서버에서 보내는 하트비트 메시지이니 무시해줍니다. 
                    if (item.Contains("관리자<TEST")) 
                        continue;
                    receiveMessageList.Add(item);

                }
                ParsingReceiveMessage(receiveMessageList);
                
                Thread.Sleep(500);
            }
        }

        // 서버가 보낸 메시지를 역캡슐화하는 과정입니다.
        private void ParsingReceiveMessage(List<string> messageList)
        {
            foreach (var item in messageList)
            {
                string sender = "";
                string message = "";

                if (item.Contains('<'))
                {
                    string[] splitedMsg = item.Split('<');

                    sender = splitedMsg[0];
                    message = splitedMsg[1];

                    if (sender == "관리자")
                    {
                        string userList = "";
                        string[] splitedUser = message.Split('$');
                        foreach (var el in splitedUser)
                        {
                            if (string.IsNullOrEmpty(el))
                                continue;
                            userList += el + " ";
                        }
                        Console.WriteLine(string.Format("[현재 접속인원] {0}", userList));
                        messageList.Clear();
                        return;
                    }

                    Console.WriteLine(string.Format("[메시지가 도착하였습니다] {0} : {1}", sender, message));
                    receiveMessageListToView.Add(string.Format("[{0}] Sender : {1}, Message : {2}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), sender, message));
                }
            }
                    messageList.Clear();
        }

        // 사용자가 메시지를 보내는 기능입니다.
        private void SendMessage()
        {
            string getUserList = string.Format("{0}<GiveMeUserList>", name);
            byte[] getUserByte = Encoding.Default.GetBytes(getUserList);
            client.GetStream().Write(getUserByte, 0, getUserByte.Length);

            Console.WriteLine("수신자를 입력해주세요");
            string receiver = Console.ReadLine();

            Console.WriteLine("보낼 message를 입력해주세요");
            string message = Console.ReadLine();

            if(string.IsNullOrEmpty(receiver) || string.IsNullOrEmpty(message))
            {
                Console.WriteLine("수신자와 보낼 message를 확인해주세요");
                Console.ReadKey();
                return;
            }

            string parsedMessage = string.Format("{0}<{1}>", receiver, message);

            byte[] byteData = new byte[1024];
            byteData = Encoding.Default.GetBytes(parsedMessage);

            client.GetStream().Write(byteData, 0, byteData.Length);
            sendMessageListToView.Add(string.Format("[{0}] Receiver : {1}, Message : {2}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),receiver,message));
            Console.WriteLine("전송성공");
            Console.ReadKey();
        }

        // 서버에 접속하는 메서드입니다.
        private void Connect()
        {
            Console.WriteLine("이름을 입력해주세요");

            name = Console.ReadLine();

            string parsedName = "%^&" + name;
            if (parsedName == "%^&")
            {
                Console.WriteLine("제대로된 이름을 입력해주세요");
                Console.ReadKey();
                return;
            }

            client = new TcpClient();
            // 하나의 PC에서 사용하므로 루프백IP를 사용하였습니다.
            // 여러개의 PC에서 사용하려면 서버PC의 실제 IP를 입력해주셔야됩니다.
            client.Connect("127.0.0.1", 9999);

            byte[] byteData = new byte[1024];
            byteData = Encoding.Default.GetBytes(parsedName);
            client.GetStream().Write(byteData, 0, byteData.Length);

            // 서버에 접속하고 서버의 메시지를 받아주는 스레드를 돌려줍니다.
            receiveMessageThread.Start();

            Console.WriteLine("서버연결 성공 이제 Message를 보낼 수 있습니다.");
            Console.ReadKey();
        }
    }
}

 

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

 

서버에 접속할때 서버의 IP주소를 직접 입력해서 접속해도 접속이 가능하지만

 

서버에서 LocalEndPoint로 클라이언트의 IP를 받아오면

 

클라이언트의 IP주소가아닌 접속할때 사용한 루프백IP를 받아올 수 있으며

 

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

 

여러대의 PC에서 사용하고 싶다면 실제IP를 받아오는 RemoteEndPoint를 사용하면 됩니다.

 

3. StaticDefine.cs

namespace ChattingServiceConsoleClient
{
    class StaticDefine
    {
        public const int CONNECT = 1;
        public const int SEND_MESSAGE = 2;
        public const int SEND_MSG_VIEW = 3;
        public const int RECEIVE_MSG_VIEW = 4;
        public const int EXIT = 0;
    }
}

 스위치문의 편의성을 위해 작성되었습니다.

 

4. 결과화면

클라이언트는 접속할때 이름을 입력합니다.

 

서버는 접속기록을 확인할수 있습니다.

 

클라이언트가 메시지보내기를할때 서버는 현재접속인원을 클라이언트에게 보내줍니다.

 

오른쪽(이성계)클라이언트가 왼쪽(을지문덕)클라이언트에게 메시지를 보낸 화면입니다.

 

채팅로그입니다.

 

클라이언트도 받은 메시지와 보낸 메시지를 확인할 수 있습니다.

 

왼쪽(을지문덕)클라이언트를 종료시켰습니다. 하트비트스레드로 서버에서 바로 확인이 가능합니다.

 

 

5. 콘솔 채팅프로그램 완성

 

채팅프로그램은 1차 완결이 났습니다

 

제가 예전에 C# 채팅프로그램을 만드려고 검색을 하다보니 

 

타 블로그들에 좋은 내용도 많지만,

 

채팅프로그램은 단발성 통신만 있거나.. 1:1 서버 클라 통신하는 소스만 있거나, 뭔지모를 완성소스만 있거나.. 등

 

뭔가 초보자입장에서 따라가기 힘들어 고생했던 기억이 있습니다.

 

혹시 누군가 채팅프로그램을 만들고 싶어서 이 블로그에 들어오면

 

끝까지 보고 따라할 수 있게 만들어보자

 

라는 생각에 해당 프로젝트를 포스팅하기 시작하였는데요.

 

포스팅한걸 다시 읽어보니 가독성도 떨어지고 부족한점이많아 차차 수정해 나가야 할 것 같습니다

 

궁금한 점이 있다면 댓글 남겨주시면 계속 응답하겠습니다.

 

다음게시글에서는 2차 버전을 포스팅하겠습니다.

 

2차버전에서는 여러가지기능(그룹채팅)을 추가하고 WPF를 활용해 VIEW를 구현하겠습니다.

 

콘솔채팅프로그램 끝.

반응형

댓글