C#을 이용하여 멀티 스레드 소캣통신 기반의 채팅 프로그램을 작성해 보겠습니다.
C# .Net
닷넷 프레임워크(.NetFrameWork)는 2002년 마이크로소프트에서 발표한 응용프로그램 개발 환경으로써 프로세스 가상 머신에 속합니다. C#은 이러한 .Net 환경에서 구동되는 언어들 중 대표적인 하나의 언어라고 할 수 있습니다. 따라서 C#은 닷넷을 위해 태어났고, 닷넷과 함께 발전해 나가는 관계하고 할 수 있습니다. 아마 JAVA에 익숙하신 분들이라면 .Net = JVM, C# = JAVA로 생각하시면 됩니다. C#은 윈도우 프로그램을 작성하기 좋은 플랫폼이기 때문에 C#을 이용하여 소켓통신을 구현해 볼까 합니다.
<소켓통신>
설계
구조 설계
다수의 클라이언트가 하나의 서버에 접속하여 채팅할 수 있는 환경을 구축하기 위해 멀티 스레드 기반의 프로그램으로 설계합니다.
*프로세스와 스레드
프로세스 : 운영체제로부터 자원을 할당받은 독립적인 작업(연산)의 단위
스레드 : 프로세스가 할당받은 자원을 이용하는 연산이나 코드실행흐름의 단위
클라이언트들은 TcpClient( IP, PortNumber ) 함수를 통해 서버에 있는 TcpListener()에 접속합니다. 클라이언트의 접속이 감지되어 클라이언트의 요청을 Accept 할 때 새로운 Socket을 만들어 각각의 Socket들이 각각의 클라이언트를 상대할 수 있습니다.
UI 설계
Server UI
- 클라이언트의 접속 및 퇴장 내역과 채팅 내역을 볼 수 있는 텍스트 필드입니다.
- 서버의 현재 상태를 표시하는 라벨입니다.
- 서버를 시작하고 종료할 수 있는 버튼입니다.
Client UI
- 채팅에서 이용할 사용자 명을 기입하는 텍스트 필드입니다.
- 채팅 서버에 입장 및 종료 버튼입니다.
- 채팅 내역이 올라오는 텍스트 필드입니다.
- 채팅 내용을 기입하는 텍스트 필드입니다.
주요 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
잘 작동되는 것을 볼 수 있습니다!
오늘은 소켓통신을 이용한 채팅 프로그램을 작성해 봤는데요. 통신을 하려면 기본적으로 알아야 할 개념인 것 같습니다.
** 틀린 내용이 있을 시 지적해 주시면 감사하겠습니다.