채팅프로그램 만들기는 총 12개의 게시글로 구성되어있습니다.
두번째 게시글 : 1:1지속성통신(동기서버 완성본 동기 클라이언트)
네번째 게시글: 1:N통신(여기서부터는 여러명을 받아야하므로 당연히 비동기서버입니다.)
일곱번째 게시글 : wpf를 통해 View를 구현한 서버
여덟번째 게시글 : wpf를 통해 View를 구현한 클라이언트(메인화면 만들기)
아홉번째 게시글 : wpf를 통해 View를 구현한 클라이언트(로그인화면 만들기)
열번째 게시글 : wpf를 통해 View를 구현한 클라이언트(채팅상대 선택화면 만들기)
열한번째 게시글 : wpf를 통해 View를 구현한 클라이언트(채팅화면 만들기)
열두번째 게시글 : wpf를 통해 View를 구현한 클라이언트(로직 구현)
이번 게시글에서는 WPF를 통해 뷰가 구현된 서버를 만들어보겠습니다.,
wpf에 익숙하지 않은 분들도 쉽게 접할 수 있도록
제 코드에서 사용되는 대부분의 내용을 블로그에 예제화 하여 포스팅하였습니다.
frozenpond.tistory.com/49?category=1126752
frozenpond.tistory.com/44?category=1126752
frozenpond.tistory.com/52?category=1126752
frozenpond.tistory.com/4?category=1126752frozenpond.tistory.com/4
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
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. 실행화면(클라이언트는 콘솔 클라이언트를 사용하였습니다)
채팅프로그램 서버편이 완성되었습니다.
다음게시글에서는 채팅프로그램의 마지막인 클라이언트를 구현하겠습니다.
댓글