파일의 이름 IctUdpService.cs
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
namespace ICT_WPF.Services
{
/// <summary>
/// UDP 통신 서비스
/// - PC IP + 포트(50001)로 bind
/// - recvfrom 루프
/// - sendto 제공
///
/// ※ UDP는 연결 개념이 없음 (Connect 아님)
/// </summary>
public sealed class IctUdpService : IDisposable
{
private readonly IPAddress _pcIp;
private readonly int _port;
private UdpClient? _udp;
private CancellationTokenSource? _cts;
/// <summary>
/// UDP 데이터 수신 이벤트
/// </summary>
public event Action<byte[], IPEndPoint>? DatagramReceived;
/// <summary>
/// 로그 출력용 이벤트(선택)
/// </summary>
public event Action<string>? Log;
public bool IsRunning => _udp != null;
public IctUdpService(string pcIp, int port = 50001)
{
_pcIp = IPAddress.Parse(pcIp);
_port = port;
}
/// <summary>
/// UDP 수신 시작
/// </summary>
public void Start()
{
if (_udp != null) return;
// 중요: PC IP로 명시 바인딩 (멀티 NIC 환경에서 꼬임 방지)
var local = new IPEndPoint(_pcIp, _port);
_udp = new UdpClient(local);
_cts = new CancellationTokenSource();
_ = Task.Run(() => ReceiveLoop(_cts.Token));
Log?.Invoke($"[UDP] Started : {local}");
}
/// <summary>
/// UDP 중지
/// </summary>
public void Stop()
{
try { _cts?.Cancel(); } catch { }
_cts = null;
try { _udp?.Close(); } catch { }
try { _udp?.Dispose(); } catch { }
_udp = null;
Log?.Invoke("[UDP] Stopped");
}
/// <summary>
/// UDP 송신
/// </summary>
public async Task SendAsync(byte[] data, string remoteIp)
{
if (_udp == null)
throw new InvalidOperationException("UDP not started");
var ep = new IPEndPoint(IPAddress.Parse(remoteIp), _port);
await _udp.SendAsync(data, data.Length, ep);
}
/// <summary>
/// recvfrom 루프
/// </summary>
private async Task ReceiveLoop(CancellationToken ct)
{
if (_udp == null) return;
while (!ct.IsCancellationRequested)
{
try
{
var r = await _udp.ReceiveAsync(ct);
DatagramReceived?.Invoke(r.Buffer, r.RemoteEndPoint);
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
Log?.Invoke($"[UDP] RX Error : {ex.Message}");
}
}
}
public void Dispose() => Stop();
}
}
요약
private readonly IPAddress _pcIp;
private readonly int _port;
private UdpClient? _udp;
private CancellationTokenSource? _cts;
_pcIp : 내 PC의 IP (예: 192.168.0.50)
_port : 사용할 UDP 포트 (기본 50001)
_udp : 실제 UDP 소켓 객체 (UdpClient)
_cts : ReceiveLoop를 멈추기 위한 취소 토큰
public event Action<byte[], IPEndPoint>? DatagramReceived;
public event Action<string>? Log;
DatagramReceived
Log
public bool IsRunning => _udp != null;
public IctUdpService(string pcIp, int port = 50001)
{
_pcIp = IPAddress.Parse(pcIp);
_port = port;
}
문자열 IP를 IPAddress로 변환해서 저장
port는 기본 50001
public void Start()
{
if (_udp != null) return;
// 중요: PC IP로 명시 바인딩 (멀티 NIC 환경에서 꼬임 방지)
var local = new IPEndPoint(_pcIp, _port);
_udp = new UdpClient(local);
_cts = new CancellationTokenSource();
_ = Task.Run(() => ReceiveLoop(_cts.Token));
Log?.Invoke($"[UDP] Started : {local}");
}
_udp != null : 이미 start했으면 중복 실행 방지
local = new IPEndPoint(_pcIp, _port); : ip, port 포인트 값 설정
_udp(소켓) = new UdpClient(local); : 중요 bind, local의 저장된 포인트 값으로 프로그램이 받음
==> 여기까지 udp 소켓 생성완료
cts = new CancellationTokenSource(); : 나중에 stop할 때 ReceiveLoop 끊을 도구
= Task.Run(() => ReceiveLoop(cts.Token)); : ReceiveLoop는 무한루프니깐 UI를 멈추면 안됨 그래서 백그라운드 Task로 돌림 앞의 =는 “리턴 Task 안 받을 거야”라는 뜻 (fire-and-forget)
Log?.Invoke($"[UDP] Started : {local}"); : lop 출력
public void Stop()
{
try { _cts?.Cancel(); } catch { }
_cts = null;
try { _udp?.Close(); } catch { }
try { _udp?.Dispose(); } catch { }
_udp = null;
Log?.Invoke("[UDP] Stopped");
}
_cts.Cancel() : ReceiveLoop에 “멈춰!” 신호 전달
ReceiveAsync가 취소되면 OperationCanceledException이 날 수 있음 (아래 ReceiveLoop에서 처리)
_udp.Close() / _udp.Dispose() : 포트 점유 해제
프로그램 종료 없이도 소켓 정리
_udp = null : 다시 Start 가능
public async Task SendAsync(byte[] data, string remoteIp)
{
if (_udp == null)
throw new InvalidOperationException("UDP not started");
var ep = new IPEndPoint(IPAddress.Parse(remoteIp), _port);
await _udp.SendAsync(data, data.Length, ep);
}
Start 안 했는데 Send 하면 에러 던짐
ep에 remoteIp로 ip값, 포트값 포인트 설정
data 바이트 배열을 UDP 패킷으로 만들어서 ep(상대 주소)로 전송