[C#] TCP 소켓 통신

maldaliza·2023년 1월 29일
1

C# 개발

목록 보기
1/2
post-thumbnail

이번 시간에는 C#으로 TCP 소켓 통신을 구현해보려고 한다. IPPort만으로 일명 채팅 프로그램을 만들어 보려고 한다. .NET Framework에서 제공하는 TcpListenerTcpClient로 구현을 해볼 예정이다. System.Net.Sockets 네임스페이스에서 보다 많은 옵션을 제공하는 Socket 클래스가 있지만, 구현하는 것이 복잡하고 TcpListenerTcpClient만으로도 충분히 TCP 소켓 통신을 구현할 수 있기 때문에 포스팅에서 제외하도록 하겠다.

1. TCP란 무엇인가?

TCP 소켓 통신을 구현해보기에 앞서, TCP가 무엇인지부터 이해하고 넘어가는 것이 좋다고 생각한다. 우선 TCP란 Transmission Control Protocol의 줄임말으로, 주된 특징으로는 다음과 같다.

  • 서버와 클라이언트 간에 데이터를 신뢰성 있게 전달하기 위해 만들어진 프로토콜이다.
  • 데이터를 전송하기 전에 데이터 전송을 위한 연결을 만드는 연결지향 프로토콜이다.
  • 데이터가 전달되는 과정에서 손실되거나 순서가 뒤바뀔 수 있으나, TCP는 손실을 검색하여 이를 교정할 수 있다.

특징에 대한 간략한 소개는 여기서 마치고, 참조할 만한 사이트 링크를 첨부하겠다.
[참조] https://kotlinworld.com/94

2. TCP 소켓 통신 구현

지금부터 TCP를 활용한 소켓 통신을 구현해보려고 한다. IPPort를 입력받아 통신할 수 있는 간단한 양방향 채팅 프로그램이다. 서버와 클라이언트 프로그램을 분리하여 설계할 것이다.

가. 서버 구현

UI는 위처럼 간단하게 IPPort를 입력받을 수 있도록 구성하였고, 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)는 사용자로부터 입력받은 IPPort다. 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라는 클래스가 나오는데, 이는 말 그대로 네트워크의 종단지점을 IPPort로 나타내는 것이다. 그 후에 전역에 선언된 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 통신에 대해서 포스팅할 수 있도록 하겠다.

[참조] https://unininu.tistory.com/475

0개의 댓글