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

c#,wpf 채팅프로그램 만들기 - 7 - 소켓통신(TcpListener, TcpClient)을 이용한 채팅프로그램을 만들어보자(wpf를 활용한 서버)

by devjh 2020. 9. 18.
반응형

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 


 

이번 게시글에서는 WPF를 통해 뷰가 구현된 서버를 만들어보겠습니다.,

 

wpf에 익숙하지 않은 분들도 쉽게 접할 수 있도록

 

제 코드에서 사용되는 대부분의 내용을 블로그에 예제화 하여 포스팅하였습니다.

 

frozenpond.tistory.com/49?category=1126752

 

[wpf] Gird, StackPanel 레이아웃 사용법 및 예제(구글메인 페이지 따라 만들기)

WPF에서 레이아웃을 구성하는 방법에는 Grid, StackPanel, DockPanel Canvas 등이 있습니다. 이번 게시글에서는 제가 자주 사용하는 Grid와 StackPanel 사용법에 대해 알아보겠습니다. 1. Grid Grid는 보통 화면을.

frozenpond.tistory.com

frozenpond.tistory.com/44?category=1126752

 

[wpf] 화면 이동하기 -2- Window 사용법, 예제(사용자 입력 받기)

화면 이동하기는 총 네개의 게시글로 구성되어 있습니다. 1. Page 2. Window 3. TabControl 4. UserControl 이번 게시글에서는 Window를 사용하여 화면을 이동해보겠습니다. Window는 화면을 이동하는게 아니라 �

frozenpond.tistory.com

 

frozenpond.tistory.com/52?category=1126752

 

[wpf] MessageBox 사용법 및 예제

이번 게시글에서는 MessageBox 사용법에 대해 포스팅하겠습니다. MessageBox는 사용자에게 정보를 주거나 양자택일을 선택시킬때 주로 사용합니다. 저번 window로 직접 창을 만들어 커스터마이징 하여

frozenpond.tistory.com

 

frozenpond.tistory.com/4?category=1126752frozenpond.tistory.com/4

 

[WPF] WPF ListView 사용법 및 예제 - 1 -

ListView 사용법 및 예제는 총 4개의 게시글로 구성되어있습니다. 1. ListView 사용법 및 예제 -1- 2. ListView 사용법 및 예제 -2-(ObservableCollection 사용하기) 3. ListView 사용법 및 예제 -2-(INotifyPrope..

frozenpond.tistory.com

 

 

1. MainWindow.cs

namespace ChattingServiceServer
{
    /// <summary>
    /// MainWindow.xaml에 대한 상호 작용 논리
    /// </summary>
    public partial class MainWindow : Window
    {
        private object lockObj = new object();
        private ObservableCollection<string> chattingLogList = new ObservableCollection<string>();
        private ObservableCollection<string> userList = new ObservableCollection<string>();
        private ObservableCollection<string> AccessLogList = new ObservableCollection<string>();
        Task conntectCheckThread = null;

        public MainWindow()
        {
            InitializeComponent();
            MainServerStart();
            ClientManager.messageParsingAction += MessageParsing;
            ClientManager.ChangeListViewAction += ChangeListView;
            ChattingLogListView.ItemsSource = chattingLogList;
            UserListView.ItemsSource = userList;
            AccessLogListView.ItemsSource = AccessLogList;
            conntectCheckThread = new Task(ConnectCheckLoop);
            conntectCheckThread.Start();
        }

        private void ConnectCheckLoop()
        {
            while (true)
            {
                foreach (var item in ClientManager.clientDic)
                {
                    try
                    {
                        string sendStringData = "관리자<TEST>";
                        byte[] sendByteData = new byte[sendStringData.Length];
                        sendByteData = Encoding.Default.GetBytes(sendStringData);

                        item.Value.tcpClient.GetStream().Write(sendByteData, 0, sendByteData.Length);
                    }
                    catch (Exception e)
                    {
                        RemoveClient(item.Value);
                    }
                }
                Thread.Sleep(1000);
            }
        }

        private void RemoveClient(ClientData targetClient)
        {
            ClientData result = null;
            ClientManager.clientDic.TryRemove(targetClient.clientNumber, out result);
            string leaveLog = string.Format("[{0}] {1} Leave Server", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), result.clientName);
            ChangeListView(leaveLog, StaticDefine.ADD_ACCESS_LIST);
            ChangeListView(result.clientName, StaticDefine.REMOVE_USER_LIST);
        }

        

        private void ChangeListView(string Message, int key)
        {
            switch (key)
            {
                case StaticDefine.ADD_ACCESS_LIST:
                    {
                        Dispatcher.Invoke(DispatcherPriority.Normal, new Action(() =>
                        {
                            AccessLogList.Add(Message);
                        }));
                        break;
                    }
                case StaticDefine.ADD_CHATTING_LIST:
                    {
                        Dispatcher.Invoke(DispatcherPriority.Normal, new Action(() =>
                        {
                            chattingLogList.Add(Message);
                        }));
                        break;
                    }
                case StaticDefine.ADD_USER_LIST:
                    {
                        Dispatcher.Invoke(DispatcherPriority.Normal, new Action(() =>
                        {
                            userList.Add(Message);
                        }));
                        break;
                    }
                case StaticDefine.REMOVE_USER_LIST:
                    {
                        Dispatcher.Invoke(DispatcherPriority.Normal, new Action(() =>
                        {
                            userList.Remove(Message);
                        }));
                        break;
                    }
                default:
                    break;
            }
        }

        private void MessageParsing(string sender, string message)
        {
            lock(lockObj)
            {
                List<string> msgList = new List<string>();

                string[] msgArray = message.Split('>');
                foreach (var item in msgArray)
                {
                    if (string.IsNullOrEmpty(item))
                        continue;
                    msgList.Add(item);
                }
                SendMsgToClient(msgList, sender);
            }
        }

        private void SendMsgToClient(List<string> msgList, string sender)
        {
            string parsedMessage = "";
            string receiver = "";

            int senderNumber = -1;
            int receiverNumber = -1;

            foreach (var item in msgList)
            {
                string[] splitedMsg = item.Split('<');

                receiver = splitedMsg[0];
                parsedMessage = string.Format("{0}<{1}>",sender, splitedMsg[1]);

                if (parsedMessage.Contains("<GroupChattingStart>")) 
                {
                    string[] groupSplit = receiver.Split('#');

                    foreach (var el in groupSplit)
                    {
                        if (string.IsNullOrEmpty(el))
                            continue;
                        string groupLogMessage = string.Format(@"[{0}] [{1}] -> [{2}] , {3}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), groupSplit[0], el, splitedMsg[1]);
                        ChangeListView(groupLogMessage, StaticDefine.ADD_CHATTING_LIST);

                        receiverNumber = GetClinetNumber(el);

                        parsedMessage = string.Format("{0}<GroupChattingStart>", receiver);
                        byte[] sendGroupByteData = Encoding.Default.GetBytes(parsedMessage);
                        ClientManager.clientDic[receiverNumber].tcpClient.GetStream().Write(sendGroupByteData, 0, sendGroupByteData.Length);
                    }
                    return;
                }

                if (receiver.Contains('#'))
                {
                    string[] groupSplit = receiver.Split('#');

                    foreach (var el in groupSplit)
                    {
                        if (string.IsNullOrEmpty(el))
                            continue;
                        if (el == groupSplit[0])
                            continue;
                        string groupLogMessage = string.Format(@"[{0}] [{1}] -> [{2}] , {3}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), groupSplit[0], el, splitedMsg[1]);
                        ChangeListView(groupLogMessage, StaticDefine.ADD_CHATTING_LIST);

                        receiverNumber = GetClinetNumber(el);

                        parsedMessage = string.Format("{0}<{1}>", receiver, splitedMsg[1]);
                        byte[] sendGroupByteData = Encoding.Default.GetBytes(parsedMessage);
                        ClientManager.clientDic[receiverNumber].tcpClient.GetStream().Write(sendGroupByteData, 0, sendGroupByteData.Length);
                    }
                    return;
                }


                senderNumber = GetClinetNumber(sender);
                receiverNumber = GetClinetNumber(receiver);


                if (senderNumber == -1 || receiverNumber == -1)
                {
                    return;
                }

                byte[] sendByteData = new byte[parsedMessage.Length];
                sendByteData = Encoding.Default.GetBytes(parsedMessage);

                if (parsedMessage.Contains("<GiveMeUserList>"))
                {
                    string userListStringData = "관리자<";
                    foreach (var el in userList)
	                {
		                userListStringData += string.Format("${0}",el);
	                }
                    userListStringData += ">";
                    byte[] userListByteData = new byte[userListStringData.Length];
                    userListByteData = Encoding.Default.GetBytes(userListStringData);
                    ClientManager.clientDic[receiverNumber].tcpClient.GetStream().Write(userListByteData,0,userListByteData.Length);
                    return;
                }

                


                string logMessage = string.Format(@"[{0}] [{1}] -> [{2}] , {3}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), sender, receiver, splitedMsg[1]);
                ChangeListView(logMessage, StaticDefine.ADD_CHATTING_LIST);

                if (parsedMessage.Contains("<ChattingStart>"))
                {
                    parsedMessage = string.Format("{0}<ChattingStart>", receiver);
                    sendByteData = Encoding.Default.GetBytes(parsedMessage);
                    ClientManager.clientDic[senderNumber].tcpClient.GetStream().Write(sendByteData, 0, sendByteData.Length);

                    parsedMessage = string.Format("{0}<ChattingStart>", sender);
                    sendByteData = Encoding.Default.GetBytes(parsedMessage);
                    ClientManager.clientDic[receiverNumber].tcpClient.GetStream().Write(sendByteData, 0, sendByteData.Length);

                    return;
                }

                

                if(parsedMessage.Contains(""))

                ClientManager.clientDic[receiverNumber].tcpClient.GetStream().Write(sendByteData, 0, sendByteData.Length);
            }
        }

        private int GetClinetNumber(string targetClientName)
        {
            foreach (var item in ClientManager.clientDic)
            {
                if (item.Value.clientName == targetClientName)
                {
                    return item.Value.clientNumber;
                }
            }
            return -1;
        }

        private void MainServerStart()
        {
            MainServer a = new MainServer();
        }
    }
}

 

이번에 새로 등장한 개념 입니다.

ChattingLogListView.ItemsSource = chattingLogList; 
UserListView.ItemsSource = userList; 
AccessLogListView.ItemsSource = AccessLogList;

WPF의 XAML파일에서 생성한 ListView 객체의 아이템소스를

 

observableCollection(List와 유사합니다)의 객체로 지정해놨습니다.

 

또한 #기호는 추후 클라이언트에서 구현할 그룹채팅을 위한 구분자로

 

여러명의 유저들에게 메시지를 보내주는 기능이 추가되었습니다.

 

GroupChattingStart, ChattingStart, GiveMeUserList는 클라이언트와 약속된 프로토콜입니다.

 

해당 메시지가 오면 클라이언트끼리 연결해주는 방식으로 추후 클라이언트편에 소개하겠습니다.

 

 

2. MainWindow.xaml

<Window x:Class="ChattingServiceServer.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="550" Width="900">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="50"></ColumnDefinition>
            <ColumnDefinition Width="270"></ColumnDefinition>
            <ColumnDefinition Width="*"></ColumnDefinition>
        </Grid.ColumnDefinitions>

        <Grid Grid.Column="0">
            <StackPanel>
                <Border BorderThickness="1,1,1,0" BorderBrush="Black">
                    <TextBlock Text="User" TextAlignment="Center" Height="20"/>
                </Border>
                
                <Border BorderThickness="1,1,1,1" BorderBrush="Black" Height="500">
                    <ListView x:Name="UserListView" BorderThickness="0"/>
                </Border>
            </StackPanel>
        </Grid>

        <Grid Grid.Column="1">
            <StackPanel>
                <Border BorderThickness="0,1,1,0" BorderBrush="Black">
                    <TextBlock Text="AccessLog" TextAlignment="Center" Height="20"/>
                </Border>

                <Border BorderThickness="0,1,1,1" BorderBrush="Black" Height="500">
                    <ListView x:Name="AccessLogListView" BorderThickness="0"/>
                </Border>
            </StackPanel>


        </Grid>

        <Grid Grid.Column="2">
            <StackPanel>
                <Border BorderThickness="0,1,1,0" BorderBrush="Black">
                    <TextBlock Text="MainLog" TextAlignment="Center" Height="20"/>
                </Border>
                
                <Border BorderThickness="0,1,1,1" BorderBrush="Black" Height="500">
                    <ListView x:Name="ChattingLogListView" BorderThickness="0"/>
                </Border>
            </StackPanel>


        </Grid>
    </Grid>
</Window>

 

현재접속자와 접속로그, 채팅로그를 보여주는 기능을 보여주도록 만들었습니다.

 

Gird, StackPanel , ListView의 사용법에 대해서는 추후 포스팅하는대로 링크를 걸어놓겠습니다.

 

 

3. MainServer.cs

namespace ChattingServiceServer
{
    public class MainServer
    {
        ClientManager _clientManager = new ClientManager();

        public MainServer()
        {
            Task serverStart = Task.Run(() =>
            {
                ServerRun();
            });
        }
        private void ServerRun()
        {
            TcpListener listener = new TcpListener(new IPEndPoint(IPAddress.Any, 708));
            listener.Start();

            while (true)
            {
                Task<TcpClient> acceptTask = listener.AcceptTcpClientAsync();

                acceptTask.Wait();

                TcpClient newClient = acceptTask.Result;

                _clientManager.AddClient(newClient);
            }
        }
    }
}

 

이전게시글과 같은 내용입니다.

 

 

4. ClientManager.cs 

namespace ChattingServiceServer
{
    class ClientManager
    {
        public static ConcurrentDictionary<int, ClientData> clientDic = new ConcurrentDictionary<int, ClientData>();
        public static event Action<string, string> messageParsingAction = null;
        public static event Action<string, int> ChangeListViewAction = null;

        public void AddClient(TcpClient newClient)
        {
            ClientData currentClient = new ClientData(newClient);

            try
            {
                newClient.GetStream().BeginRead(currentClient.readBuffer, 0, currentClient.readBuffer.Length, new AsyncCallback(DataReceived), currentClient);
                clientDic.TryAdd(currentClient.clientNumber, currentClient);
            }

            catch (Exception e)
            {
               // RemoveClient(currentClient);
            }
        }

        

        private void DataReceived(IAsyncResult ar)
        {
            ClientData client = ar.AsyncState as ClientData;

            try
            {
                int byteLength = client.tcpClient.GetStream().EndRead(ar);

                string strData = Encoding.Default.GetString(client.readBuffer, 0, byteLength);

                client.tcpClient.GetStream().BeginRead(client.readBuffer, 0, client.readBuffer.Length, new AsyncCallback(DataReceived), client);

                if (string.IsNullOrEmpty(client.clientName))
                {
                    if (ChangeListViewAction != null)
                    {
                        if (CheckID(strData))
                        {
                            string userName = strData.Substring(3);
                            client.clientName = userName;
                            ChangeListViewAction.Invoke(client.clientName, StaticDefine.ADD_USER_LIST);
                            string accessLog = string.Format("[{0}] {1} Access Server", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), client.clientName);
                            ChangeListViewAction.Invoke(accessLog, StaticDefine.ADD_ACCESS_LIST);
                            File.AppendAllText("AccessRecored.txt", accessLog + "\n");
                            return;
                        }
                    }
                }


                if (messageParsingAction != null)
                {
                    messageParsingAction.BeginInvoke(client.clientName, strData, null, null);
                }

            }
            catch (Exception e)
            {
                //RemoveClient(client);
            }
        }

        private bool CheckID(string ID)
        {
            if (ID.Contains("%^&"))
                return true;

            File.AppendAllText("IDErrLog.txt", ID);
            return false;
        }
    }
}

 

콜백메서드 내부를 보면 사용자의 접속을 string으로 파싱하여 로그를 만들어

 

MainWindow에 넘겨줘서 메인화면에 ListView로 표현해주고있습니다.

 

delegate와 event를 사용하였으니 아래게시글을 참고하세요.

 

frozenpond.tistory.com/3?category=1125073

 

c# delegate, event 사용법 및 사용예제

delegate는 메서드 대리자입니다. delegate로 메서드 대리자를 선언해주고 원하는 메서드를 참조시킬수 있습니다. 쉽게말하면 함수를 보관하는 통을 만들고(대리자선언) 그 통안에 함수를 넣고 나중

frozenpond.tistory.com

 

 

5. ClientData.cs

namespace ChattingServiceServer
{
    class ClientData
    {
        public static bool isdebug = true;
        public TcpClient tcpClient { get; set; }
        public Byte[] readBuffer { get; set; }
        public StringBuilder currentMsg { get; set; }
        public string clientName { get; set; }
        public int clientNumber { get; set; }

        public ClientData(TcpClient tcpClient)
        {
            currentMsg = new StringBuilder();
            readBuffer = new byte[1024];

            this.tcpClient = tcpClient;

            char[] splitDivision = new char[2];
            splitDivision[0] = '.';
            splitDivision[1] = ':';

            string[] temp = null;
            if (isdebug)
            {
                temp = tcpClient.Client.LocalEndPoint.ToString().Split(splitDivision);
            }
            else
            {
                temp = tcpClient.Client.RemoteEndPoint.ToString().Split(splitDivision);
            }

            this.clientNumber = int.Parse(temp[3]);
        }
    }
}

이전게시글과 동일한 내용입니다.

 

6. StaticDefine.cs

namespace ChattingServiceServer
{
    class StaticDefine
    {
        public const int ADD_CHATTING_LIST = 0;
        public const int ADD_ACCESS_LIST = 1;
        public const int ADD_USER_LIST = 2;
        public const int REMOVE_USER_LIST = 3;
    }
}

스위치문의 가독성을 위해 추가된 클래스입니다.

 

7. 실행화면(클라이언트는 콘솔 클라이언트를 사용하였습니다)

 

좌측부터 현재접속자(하데스는 메시지를 보내고 접속종료) 접속로그, 메인로그입니다.

 

제우스의 콘솔 클라이언트 화면

 

채팅프로그램 서버편이 완성되었습니다.

 

다음게시글에서는 채팅프로그램의 마지막인 클라이언트를 구현하겠습니다.

반응형

댓글