이번 시간에는 C#으로 TCP 소켓 통신을 구현해보려고 한다. IP
와 Port
만으로 일명 채팅 프로그램을 만들어 보려고 한다. .NET Framework에서 제공하는 TcpListener
와 TcpClient
로 구현을 해볼 예정이다. System.Net.Sockets
네임스페이스에서 보다 많은 옵션을 제공하는 Socket 클래스
가 있지만, 구현하는 것이 복잡하고 TcpListener
와 TcpClient
만으로도 충분히 TCP 소켓 통신을 구현할 수 있기 때문에 포스팅에서 제외하도록 하겠다.
TCP 소켓 통신을 구현해보기에 앞서, TCP가 무엇인지부터 이해하고 넘어가는 것이 좋다고 생각한다. 우선 TCP란 Transmission Control Protocol
의 줄임말으로, 주된 특징으로는 다음과 같다.
특징에 대한 간략한 소개는 여기서 마치고, 참조할 만한 사이트 링크를 첨부하겠다.
[참조] https://kotlinworld.com/94
지금부터 TCP를 활용한 소켓 통신을 구현해보려고 한다. IP
와 Port
를 입력받아 통신할 수 있는 간단한 양방향 채팅 프로그램이다. 서버와 클라이언트 프로그램을 분리하여 설계할 것이다.
UI는 위처럼 간단하게 IP
와 Port
를 입력받을 수 있도록 구성하였고, Connect 버튼
을 클릭하여 클라이언트의 접속을 기다릴 수 있도록 설계하였다. 하단에 클라이언트로 전달할 메시지를 입력할 수 있고 Send 버튼
을 통해 해당 메시지를 클라이언트에 전달할 수 있도록 설계하였다.
private void connect_btn_Click(object sender, EventArgs e)
{
... // IP, Port가 정상적으로 입력되었는지 확인하는 부분
Thread connectThread = new Thread(new ThreadStart(Connect));
connectThread.IsBackground = true;
connectThread.Start();
}
Connect 버튼
을 클릭하게 되면 위의 코드가 실행된다. 메인 쓰레드에서 connectThread
를 분리하여 Connect()
메소드를 실행시킨다. (분리시키는 이유는 메인 쓰레드에서 바로 Connect 시, 클라이언트가 접속하지 않은 시간동안은 메인 쓰레드가 아무것도 할 수 없기 때문이다..)
private void Connect()
{
try
{
TcpListener listener = new TcpListener(IPAddress.Parse(ip), int.Parse(port));
listener.Start();
WriteLog("Ready to connect ...");
client = listener.AcceptTcpClient();
WriteLog("Client connected!");
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
}
}
메인 쓰레드에서 분리된 connectThread
에서 실행된 Connect()
메소드이다. TcpLister
라는 클래스는 TCP 네트워크 클라이언트에서 연결을 수신한다. 파라미터로 전달되는 IPAddress.Parse(ip), int.Parse(port)
는 사용자로부터 입력받은 IP
와 Port
다. listener.Start();
를 통해 들어오는 연결 요청 수신 대기를 시작한다.
client = listener.AcceptTcpClient();
이 부분의 client
는 전역에서 선언된 private static TcpClient client = new TcpClient();
다. TcpClient
클래스는 TCP 네트워크 서비스에 대한 클라이언트 연결을 제공한다. listener.AcceptTcpClient()
이 부분에서 클라이언트의 접속을 기다리고, 접속이 된 경우에 client
객체로 클라이언트의 정보를 넘겨준다.
try-catch
문은 그냥 디버깅 용도로 설계해놓은 것이다..
private void send_btn_Click(object sender, EventArgs e)
{
string message = tb_message.Text;
if (message.Length <= 0) { return; }
try
{
NetworkStream stream = client.GetStream();
byte[] buffer = Encoding.UTF8.GetBytes(message);
stream.Write(buffer, 0, buffer.Length);
WriteLog("[Me] " + message);
tb_message.Text = string.Empty;
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
}
}
Send
버튼을 클릭하면 위의 코드가 실행된다. NetworkStream
클래스는 네트워크 엑세스를 위한 데이터의 기본 스트림을 제공한다. 클라이언트 접속 부분에서 넘겨받은 client
객체에서 GetStream()
메소드를 이용하여 NetworkStream
을 활성화 시킨다. 텍스트박스에서 입력받은 메시지를 위의 코드에선 UTF8
방식으로 인코딩하여 바이트 단위 배열로 받는다. 그리고 NetworkStream
클래스의 Write()
메소드를 통하여 메시지를 송신한다.
다른 블로그를 참고하다보면 메시지 수신 부분을 다양하게 설계하는 걸 알 수 있으나, 본인 블로그에선 다음과 같이 설계하였다.
private void Tcp_Server_Load(object sender, EventArgs e)
{
Thread receiveThread = new Thread(new ThreadStart(Receive));
receiveThread.IsBackground = true;
receiveThread.Start();
}
위처럼 Form
이 Load되는 시점에서 메인 쓰레드와 분리하여 receiveThread
를 이용하여 Receive()
메소드를 따로 실행한다.
private void Receive()
{
while (true)
{
if (client.Connected)
{
try
{
NetworkStream stream = client.GetStream();
byte[] buffer = new byte[1024];
int bytes = stream.Read(buffer, 0, buffer.Length);
if (bytes <= 0) { continue; }
string message = Encoding.UTF8.GetString(buffer, 0, bytes);
WriteLog("[Other] " + message);
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
}
}
}
}
Receive()
메소드 코드이다. while (true)
를 통하여 수신을 계속 반복하여 실행하도록 설계했다. while
문 안에 if (client.Connected)
조건을 걸어, 접속 부분에서 넘겨받은 client
객체가 연결되었을 시에만 실행되도록 설계했다. 이후는 메시지 송신 부분과 동일하게 설계했고, byte[] buffer = new byte[1024];
에서 미리 바이트 단위 배열을 설계하였고, stream.Read(buffer, 0, buffer.Length);
를 통해 수신된 메시지의 배열 크기를 확인하였다. 메시지는 송신과 마찬가지로 UTF8
로 구성하였다.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace SocketProgramming
{
public partial class Tcp_Server : Form
{
private static string ip = string.Empty;
private static string port = string.Empty;
private static TcpClient client = new TcpClient();
public Tcp_Server()
{
InitializeComponent();
}
private void Tcp_Server_Load(object sender, EventArgs e)
{
Thread receiveThread = new Thread(new ThreadStart(Receive));
receiveThread.IsBackground = true;
receiveThread.Start();
}
private void Receive()
{
while (true)
{
if (client.Connected)
{
try
{
NetworkStream stream = client.GetStream();
byte[] buffer = new byte[1024];
int bytes = stream.Read(buffer, 0, buffer.Length);
if (bytes <= 0)
{
continue;
}
string message = Encoding.UTF8.GetString(buffer, 0, bytes);
WriteLog("[Other] " + message);
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
}
}
}
}
private void connect_btn_Click(object sender, EventArgs e)
{
ip = tb_ip.Text;
port = tb_port.Text;
if (ip.Split('.').Length != 4 || port == "")
{
tb_ip.Text = string.Empty;
tb_port.Text = string.Empty;
return;
}
Thread connectThread = new Thread(new ThreadStart(Connect));
connectThread.IsBackground = true;
connectThread.Start();
}
private void Connect()
{
try
{
TcpListener listener = new TcpListener(IPAddress.Parse(ip), int.Parse(port));
listener.Start();
WriteLog("Ready to connect ...");
client = listener.AcceptTcpClient();
WriteLog("Client connected!");
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
}
}
private void send_btn_Click(object sender, EventArgs e)
{
string message = tb_message.Text;
if (message.Length <= 0) { return; }
try
{
NetworkStream stream = client.GetStream();
byte[] buffer = Encoding.UTF8.GetBytes(message);
stream.Write(buffer, 0, buffer.Length);
WriteLog("[Me] " + message);
tb_message.Text = string.Empty;
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
}
}
private void WriteLog(string text)
{
string date = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
Invoke(new MethodInvoker(delegate ()
{
tb_log.AppendText("[" + date + "] " + text + "\r\n");
}));
}
}
}
클라이언트 프로그램도 서버와 그렇게 다르지는 않다. 서버가 아니다보니, 접속하는 부분에서 TcpListener
클래스가 필요없다! 왜나하면 서버는 말그대로 미리 켜져있어 클라이언트의 접속을 기다리는 역할을 수행하지만, 클라이언트는 서버가 켜졌는지 기다릴 필요가 없기 때문이다. 그래서 바로 접속 부분을 알아보자.
private void Connect()
{
IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse(ip), int.Parse(port));
try
{
client.Connect(endPoint);
WriteLog("Connected...");
}
catch (SocketException se)
{
WriteLog(se.Message);
}
}
Connect()
메소드 실행 부분은 서버와 동일하게 메인 쓰레드에서 connectThread
를 따로 분리하여 실행시킨다. 마찬가지로 메인 쓰레드에서 접속을 할려고 하면, 접속하는 동안 아무것도 할 수가 없기 때문...
IPEndPoint
라는 클래스가 나오는데, 이는 말 그대로 네트워크의 종단지점을 IP
와 Port
로 나타내는 것이다. 그 후에 전역에 선언된 private static TcpClient client = new TcpClient();
에서 생성된 client
객체의 Connect()
메소드를 이용하여 서버에 접속한다.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace SocketProgramming
{
public partial class Tcp_Client : Form
{
private static string ip = string.Empty;
private static string port = string.Empty;
private static TcpClient client = new TcpClient();
public Tcp_Client()
{
InitializeComponent();
}
private void Tcp_Client_Load(object sender, EventArgs e)
{
Thread receiveThread = new Thread(new ThreadStart(Receive));
receiveThread.IsBackground = true;
receiveThread.Start();
}
private void Receive()
{
while (true)
{
if (client.Connected)
{
try
{
NetworkStream stream = client.GetStream();
byte[] buffer = new byte[1024];
int bytes = stream.Read(buffer, 0, buffer.Length);
if (bytes <= 0)
{
continue;
}
string message = Encoding.UTF8.GetString(buffer, 0, bytes);
WriteLog("[Other] " + message);
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
}
}
}
}
private void connect_btn_Click(object sender, EventArgs e)
{
ip = tb_ip.Text;
port = tb_port.Text;
if (ip.Split('.').Length != 4 || port == "")
{
tb_ip.Text = string.Empty;
tb_port.Text = string.Empty;
return;
}
Thread connectThread = new Thread(new ThreadStart(Connect));
connectThread.IsBackground = true;
connectThread.Start();
}
private void Connect()
{
IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse(ip), int.Parse(port));
try
{
client.Connect(endPoint);
WriteLog("Connected...");
}
catch (SocketException se)
{
WriteLog(se.Message);
}
}
private void send_btn_Click(object sender, EventArgs e)
{
string message = tb_message.Text;
if (message.Length <= 0) { return; }
try
{
NetworkStream stream = client.GetStream();
byte[] buffer = Encoding.UTF8.GetBytes(message);
stream.Write(buffer, 0, buffer.Length);
WriteLog("[Me] " + message);
tb_message.Text = string.Empty;
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
}
}
private void WriteLog(string text)
{
string date = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
Invoke(new MethodInvoker(delegate ()
{
tb_log.AppendText("[" + date + "] " + text + "\r\n");
}));
}
}
}
여기까지 TCP 소켓 통신에 대하여 간단한 구현 예제 프로그램을 통해 알아보았다. 다음 기회에는 UDP 통신에 대해서 포스팅할 수 있도록 하겠다.