WPF C# - UDP통신 소켓

wsung·2026년 2월 6일

파일의 이름 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();
    }
}

요약

  • pc에서 UDP 포트 50001을 열고 (bind)
    백그라운드에서 ReceiveAsync 루프로 계속 받으며
    받은 데이터를 DatagramReceived 이벤트로 ViewModel에 전달하고
    SnedAsync()로 특정IP(보드)로 보낼 수 있게 해주는 클래스임.

필드(멤버 변수)들

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

  • UDP로 데이터가 들어오면(수신되면) 이 이벤트가 발생
    전달되는 값:
  • byte[] : 받은 데이터(프레임 전체)
  • IPEndPoint : 보낸 놈의 IP/포트 (보드 주소 확인 가능)

Log

  • Start/Stop/에러 같은 상태 메시지를 UI 로그에 띄우고 싶을 때 사용 (선택)

상태 프로퍼티

public bool IsRunning => _udp != null;
  • _udp가 생성되어 있으면 실행중이라고 판단

생성자

public IctUdpService(string pcIp, int port = 50001)
{
    _pcIp = IPAddress.Parse(pcIp);
    _port = port;
}

문자열 IP를 IPAddress로 변환해서 저장
port는 기본 50001

  • 내 설정
    - pcIp = 192.168.0.50
    • port = 50001

Start() — “UDP 수신 시작”

        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 출력

Stop() — “UDP 중지”

        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 가능

SendAsync() — “UDP 송신”

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(상대 주소)로 전송

profile
0부터 시작하는 백엔드

0개의 댓글