이번 시간에는 C#으로 UDP
소켓 통신을 구현해보려 한다. 이전 시간에 알아보았던 TCP
방식은 3 Way HandShake
라는 방식으로 서버와 클라이언트 간의 접속 상태를 확인하는 부분이 있었고, 접속 상태가 활성화된 상태에만 통신을 할 수 있었다. 그러나 UDP
방식에서는 접속 상태를 확인하지 않고, 일방적으로 패킷을 전달하는 방식이다. TCP
소켓 통신과 마찬가지로 Socket
클래스를 사용하지 않고, UdpClient
클래스 만으로 간단하게 구현해보겠다.
UDP 소켓 통신을 구현해보기에 앞서, UDP가 무엇인지부터 이해하고 넘어가도록 하자. 우선 UDP는 User Datagram Protocol
의 약자로 "데이터를 데이터그램 단위로 처리하는 프로토콜"이다. 주된 특징은 다음과 같다.
1. TCP와 달리 UDP는 비연결형 프로토콜이다.
한마디로, 연결을 위해 할당되는 논리적인 경로가 없다는 뜻. 그렇기 때문에 각각의 패킷은 다른 경로로 전달되고, 각각의 패킷이 독립적인 관계를 갖게 된다.
2. TCP 보다 속도가 빠르다.
UDP는 비연결형 프로토콜이기 때문에, 연결을 설정하고 해제하는 과정이 필요가 없다. 고로 서로 다른 독립적인 경로로 처리함으로 패킷의 순서를 조립하거나 하는 등의 기능을 처리하지 않으므로 TCP 보다 속도가 빠르며, 네트워크의 부하가 적다.
3. 신뢰성이 낮다.
UDP는 말그대로 데이터를 뿌려주는 것으로 내부 구조가 간단한 것에 비해, 가끔 패킷이 유실되거나 패킷의 순서가 바뀌는 등의 에러가 발생한다.
위의 특징 이외에도 UDP의 특징을 소개하는 자료가 많다. 간략한 소개는 여기서 마치고, 참조할 만한 사이트를 첨부하겠다.
[참조]https://mangkyu.tistory.com/15
UI는 위처럼 간단하게 IP
와 Port
만 입력할 수 있도록 설계하였고, TCP
방식과는 달리, Connect 버튼이 없다. 이는 UDP
방식은 비연결형 프로토콜이라서 접속이 따로 필요없기 때문이다.
서버 클라이언트 간의 접속이 필요 없기 때문에, 바로 메시지 송신 부분을 알아보자.
private void send_btn_Click(object sender, EventArgs e)
{
ip = tb_ip.Text;
port = int.Parse(tb_port.Text);
string message = tb_message.Text;
byte[] bytes = Encoding.UTF8.GetBytes(message);
try
{
using (UdpClient client = new UdpClient())
{
IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse(ip), port);
client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
client.Send(bytes, bytes.Length, endPoint);
WriteLog(message);
tb_message.Text = string.Empty;
client.Close();
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
}
}
메시지 Send 버튼을 클릭하면 발생하는 코드이다. 서버 프로그램의 기능이 메시지 Send 밖에 없고, 클라이언트의 Receive 여부에 대해 상관하지 않으므로 따로 쓰레드를 분리할 필요가 없었다.
코드를 살펴보면, 우선 IP
와 Port
를 입력받는다. IP
의 Default값을 255.255.255.255
라고 설정했는데, 이유는 "글로벌 브로드캐스트 주소" 로, 패킷을 모든 네트워크 인터페이스로 보낸다 라는 뜻이다. 그리고 Port
를 설정해준다.
그 후, 메시지를 UTF8
방식으로 인코딩하여 바이트 단위 배열로 받는다. UdpClient
클래스를 이용하여 UDP
네트워크에 대한 정보를 client
객체로 받는다. IPEndPoint
클래스를 이용하여 패킷을 전달받을 주소의 종단점을 입력받고, client.Client.SetSocketOption()
메소드를 통해 Socket 옵션을 설정한다.
마지막으로, Send()
를 통해 파라미터로 바이트 배열
, 배열 크기
, 종단점
을 넘겨준다.
위의 코드를 통해 원하는 종단점으로 메시지를 송신하는 것을 알아보았다.
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.Tasks;
using System.Windows.Forms;
namespace SocketProgramming.UDP
{
public partial class Udp_Server : Form
{
private static string ip = string.Empty;
private static int port = 0;
public Udp_Server()
{
InitializeComponent();
}
private void send_btn_Click(object sender, EventArgs e)
{
ip = tb_ip.Text;
port = int.Parse(tb_port.Text);
string message = tb_message.Text;
byte[] bytes = Encoding.UTF8.GetBytes(message);
try
{
using (UdpClient client = new UdpClient())
{
IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse(ip), port);
client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
client.Send(bytes, bytes.Length, endPoint);
WriteLog(message);
tb_message.Text = string.Empty;
client.Close();
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
}
}
private void WriteLog(string message)
{
string date = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
Invoke(new MethodInvoker(delegate ()
{
tb_log.AppendText("[" + date + "] " + message + "\r\n");
}));
}
}
}
클라이언트 프로그램도 서버와 마찬가지로 UdpClient
클래스를 이용하여 설계한다. 메시지를 수신만 하는 기능만 설계할 것이다. 그러나 메시지 수신 기능만 있다고 메인 쓰레드에 설계를 하게 된다면, 서버가 메시지를 송신하지 않는 동안 아무것도 할 수가 없다. 그래서 메시지를 수신할 수 있는 쓰레드를 분리하여 설계해야 한다.
private void Udp_Client_Load(object sender, EventArgs e)
{
port = int.Parse(tb_port.Text);
Thread socketThread = new Thread(new ThreadStart(Start));
socketThread.IsBackground = true;
socketThread.Start();
}
클라이언트 프로그램이 로드될 때 실행되는 코드다. TextBox
로부터 Port
만 넘겨받는다. 이유는 메시지를 송신하는 위치의 IP
를 모를 수 있기 때문이다.
Thread
클래스를 이용하여 메인 쓰레드와 분리를 시켜준다.
private void Start()
{
if (client != null) { Debug.WriteLine("이미 UDP 소켓이 생성되어있음.."); }
client = new UdpClient();
endPoint = new IPEndPoint(IPAddress.Any, port);
client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
client.ExclusiveAddressUse = false;
client.Client.Bind(endPoint);
StartListening();
}
분리된 쓰레드에서 실행한 Start()
메소드이다. 전역에 선언한 UdpClient
의 객체인 client
가 다시 생성되면 안되므로 조건을 걸어준다.
그 후에 UdpClient
객체와, IP
와 Port
로 네트워크 종단점을 선언하는 IPEndPoint
클래스의 객체를 선언한다. 모든 네트워크 인터페이스의 클라이언트 동작을 수신 대기해야 하므로 IPAddress.Any
를 파라미터로 IPEndPoint
객체를 생성한다.
SetSocketOption()
메소드는 SocketOptionLevel
, SocketOptionName
, optionValue
값이 파라미터로 전달된다.
첫번째 필수 파라미터인 SocketOptionLevel
은 소켓의 옵션 수준을 정의한다. 열거형을 사용하여 옵션이 적용될 OSI 게층 모델을 지정한다. IP
, Socket
, Tcp
, Udp
의 옵션이 존재한다.
두번째 필수 파라미터인 SocketOptionName
은 값이 설정될 파라미터의 이름을 정의한다. ReuseAddress
는 해당 포트에서 이미 수신 대기 중인 다른 애플리케이션이 있는 경우, 소켓 연결을 실패할지 여부를 제어한다. 즉, 다른 응용 프로그램에서 이미 소켓이 열려 있는 경우에도 이 포트에서 수신하게 된다는 것이다.
세번째 파라미터인 optionValue
는 소켓 옵션의 현재 동작을 결정한다. (활성화 할 것인지, 말 것인지..)
ExclusiveAddressUse
속성은 Boolean
값으로 하나의 포트에 하나의 프로세스만 바인딩할 것을 허용하는지 여부를 지정하는 값이다. Socket
이 특정 포트에 하나의 소켓만 바인딩하는 것을 허용하면 true
, 그렇지 않으면 false
이다.
그 후, 통신의 종단점인 endPoint
객체를 바인딩하고, StartListening()
를 실행한다.
private void StartListening()
{
asyncResult = client.BeginReceive(Receive, new object());
}
StartListening()
메소드는 클래스 전역에 선언된 비동기 작업 상태를 표현하는 IAsyncResult
클래스의 asyncResult
객체를 초기화하는 메소드이다. 데이터그램을 원격 호스트에서 비동기적인 수신을 시작하는 부분으로 첫번째 파라미터의 Receive
메소드가 실행된다.
private void Receive(IAsyncResult ar)
{
if (client == null) { return; }
try
{
byte[] bytes = client.EndReceive(ar, ref endPoint);
string message = Encoding.UTF8.GetString(bytes, 0, bytes.Length);
if (tb_log.InvokeRequired)
{
tb_log.Invoke(new MethodInvoker(delegate ()
{
WriteLog(message);
}));
}
StartListening();
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
}
}
이제 진짜 메시지를 수신 받는 Receive
메소드다. BeginReceive
메소드에서 호출할 때, IAsyncResult
클래스의 객체를 파라미터로 전달해주기 때문에 그 값으로 endPoint
에서 전달하는 메시지를 수신한다. 메시지를 수신 받고 다시 StartListening()
을 실행하는 구조이다.
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.UDP
{
public partial class Udp_Client : Form
{
private static int port = 0;
private static UdpClient client;
private static IPEndPoint endPoint;
IAsyncResult asyncResult;
public Udp_Client()
{
InitializeComponent();
}
private void Udp_Client_Load(object sender, EventArgs e)
{
port = int.Parse(tb_port.Text);
Thread socketThread = new Thread(new ThreadStart(Start));
socketThread.IsBackground = true;
socketThread.Start();
}
private void Start()
{
if (client != null) { Debug.WriteLine("이미 UDP 소켓이 생성되어있음.."); }
client = new UdpClient();
endPoint = new IPEndPoint(IPAddress.Any, port);
client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
client.ExclusiveAddressUse = false;
client.Client.Bind(endPoint);
StartListening();
}
private void StartListening()
{
asyncResult = client.BeginReceive(Receive, new object());
}
private void Receive(IAsyncResult ar)
{
if (client == null) { return; }
try
{
byte[] bytes = client.EndReceive(ar, ref endPoint);
string message = Encoding.UTF8.GetString(bytes, 0, bytes.Length);
if (tb_log.InvokeRequired)
{
tb_log.Invoke(new MethodInvoker(delegate ()
{
WriteLog(message);
}));
}
StartListening();
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
}
}
private void WriteLog(string message)
{
string date = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
Invoke(new MethodInvoker(delegate ()
{
tb_log.AppendText("[" + date + "] " + message + "\r\n");
}));
}
}
}
지금까지 UDP
를 이용한 메시지 송수신에 대해 알아보았다. 부족한 점이 많은 포스팅이었지만, 가능한 여유가 있을 때 부족했던 설명을 보완할 예정이다.
[참고]
https://social.msdn.microsoft.com/Forums/en-US/5d755d4a-796c-4906-87a3-d7b40c420c4b/how-to-set-the-reuseaddress-socket-option?forum=netfxnetcom
https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=wkqldpfm&logNo=40155005822