UDP와 멀티캐스팅

  • UDP(User Datagram Protocol)은 TCP에 비교해 패킷이 의도한 목적지에 도착한다는 보장, 순서의 보장을 할 수 없기 때문에 신뢰할 수 없다.
  • 비연결 프로토콜로 패킷 전송을 용이하게 하는 두개의 노드간의 메시지 교환이 없다.
  • DNS, NTP, VOIP, P2P 네트워크에서 네트워크 통신의 조정, 비디오 스트리밍 등에 사용된다
UDP에 대한 자바 지원
  • DatagramSocket: 노드간에 소켓 연결 생성
  • DatagramPacket: 데이터 패킷을 나타내며, send, receive 메소드를 통해 네트워크를 통해 패킷 전송
  • 노드 식별하는 데 IP, Port 번호 사용
    • 0~1023: 잘 알려진 포트
    • 1024~49151: 등록된 포트
    • 49152~65535: 동적/사설 포트
  • TCP와 UDP
    • 차이점 1. 신뢰성: TCP는 UDP 보다 더 신뢰할 수 있다
    • 차이점 2. 순서: TCP는 패킷 전송의 순서가 유지되는 것을 보장
    • 차이점 3. 헤더 크기: UDP 헤더는 TCP 헤더보다 작다
    • 차이점 4. 속도: UDP는 TCP 보다 빠르다
UDP Server/Client
  • 서버 측에서는 생성된 UDP 서버 소켓은 클라이언트 요청을 기다리며, 클라이언트는 응답하는 UDP 소켓을 생성하고 서버에게 메세지를 전송하는 데 사용. 서버는 요청을 처리하고 응답을 재전송 할 수 있다
  • receive 메소드는 응답할때까지 블록되며 패킷은 이후에 채워진다
  • UDP Server ```java System.out.println(“UDP Server Started”); /** DatagramSocket 생성하는 다른 방법
    • DatagramSocket socket = new DatagramSocket(null);
    • socket.bind(new InetSocketAddress(9003); */ try(DatagramSocket socket = new DatagramSocket(9003)) { while(true) { byte[] receiveMessage = new byte[64]; // 바이트 배열 생성 DatagramPacket packet = new DatagramPacket(receiveMessage, receiveMessage.length); // DatagramPacket 인스턴스 생성 socket.receive(packet); // 도착 메세지의 대기를 위한 DatagramSocket 인스턴스 사용 String message = new String(packet.getData()); System.out.println(“Received Data: “ + message);

       // 응답 전송 - 클라이언트 주소, Port 사용
       InetAddress address = packet.getAddress();
       int port = packet.getPort();
       byte[] sendMessage = message.getBytes();
       DatagramPacket sendPacket = new DatagramPacket(sendMessage, sendMessage.length, address, port);
       socket.send(sendPacket);  } } catch (IOException e) {  // 예외 처리 } System.out.println("UDP Server Terminating"); ```
      
  • UDP Client
    System.out.println("UDP Client Started");
    Scanner scanner = new Scanner(System.in);
    
    try(DatagramSocket socket = new DatagramSocket()) {
        InetAddress address = InetAddress.getByName("127.0.0.1");
        int port = 9003;
        byte[] sendMessage;
    
        while(true) {
            System.out.print("Enter a message: ");
            String message = scanner.nextLine();
            if("quit".equals(message)) {
                break;
            }
    
            // 서버로 데이터 전달
            sendMessage = message.getBytes();
            DatagramPacket packet = new DatagramPacket(sendMessage, sendMessage.length, address, port);
            socket.send(packet);
    
            // 서버에게 데이터 받음
            byte[] receiveMessage = new byte[1024];
            DatagramPacket receivePacket = new DatagramPacket(receiveMessage, receiveMessage.length);
            socket.receive(receivePacket);
            message = new String(receivePacket.getData());
            System.out.println("Received Data: " + message);
        }
        socket.close();
    } catch (IOException e) {
        // 예외처리
    }
    System.out.println("UDP Client Terminated");
    
UDP를 위한 채널 지원
  • DatagramChannel 은 UDP에 대한 추가 지원 제공하며 nonblocking interchanges 지원
    • 쉽게 멀테스레드 애플리케이션을 생성하는 SelectableChannel 클래스에서 파생
  • DatagramSocket 클래스는 채널을 포트에 바인딩하여 사용하며 DatagramSocket은 직접적으로 사용되지 않는다
    • DatagramPacket 또한 직접 사용하지 않으며 Buffer를 사용
  • UDP Echo Server
    System.out.println("UDP Echo Server Started");
    
    try(DatagramChannel channel = DatagramChannel.open();
        DatagramSocket socket = channel.socket()) {
    
        SocketAddress address = new InetSocketAddress(9000);
        socket.bind(address);
    
        ByteBuffer buffer = ByteBuffer.allocateDirect(65507); // allocateDirect는 버퍼 할당에서 네이티브 OS 지원을 사용하려고 시도한다
        while(true) {
            // 메시지 얻기
            SocketAddress client = channel.receive(buffer); // 클라이언트 메시지를 얻기 위한 채널에 적용, 수신될 때까지 블록된다
            buffer.flip(); // 처리를 위한 버퍼를 사용 가능하게 한다. limit을 현재 position으로 설정하고 이후 position 0으로 설정
    
            // 메시지 출력
            buffer.mark(); // mark 현재 position으로 마킹한다
            StringBuilder message = new StringBuilder();
            while(buffer.hasRemaining()) {
                message.append((char)buffer.get());
            }
            System.out.println("Received: " + message.toString());
            buffer.reset(); // 마크된 position으로 복원
    
            // 메시지 반환
            channel.send(buffer, client);
            System.out.println("Send: " + message);
            buffer.clear(); // position을 0으로 설정하고, limit을 capacity로 설정, 마크 삭제
        }
    } catch (IOException e) {
        // 예외 처리
    }
    System.out.println("UDP Echo Server Terminated");
    
  • UDP Echo Client
    Scanner scanner = new Scanner(System.in);
    System.out.println("UDP Echo Client Started");
    try {
        SocketAddress address = new InetSocketAddress("127.0.0.1", 9000);
        DatagramChannel channel = DatagramChannel.open();
        channel.connect(address);
    
        while(true) {
            String message = scanner.nextLine();
            if("quit".equals(message)) {
                break;
            }
            ByteBuffer buffer = ByteBuffer.allocate(message.length());
            buffer.put(message.getBytes());
    
            buffer.flip(); // limit을 현재의 position으로 설정하고 position을 0으로 설정
            channel.write(buffer);
            System.out.println("Send: " + message);
    
            buffer.clear();
            channel.read(buffer);
            buffer.flip();
            StringBuilder receivedMessage = new StringBuilder();
            while(buffer.hasRemaining()) {
                receivedMessage.append((char)buffer.get());
            }
            System.out.println("Received: " + receivedMessage.toString());
        }
    } catch (IOException e) {
        // 예외 처리
    }
    System.out.println("UDP Echo Client Terminated");
    
UDP Multicasting
  • 멀티캐스팅은 동일한 시간에 다수의 클라이언트에 메시지를 전송하는 프로세스이며 각 클라이언트에 같은 메시지를 수신한다
    • 클라이언트는 멀티캐스트 그룹에 참여해야 한다
  • 멀티캐스트는 기존 IPv4의 클래스 D 공간과 224.0.0.0 ~ 239.255.255.255dml wnthfmf tkdyd

  • MulticastSocket, DatagramPacket 을 활용한 멀티캐스팅
    • Server
      System.out.println("UDP Multicast Server Started");
      try {
          MulticastSocket socket = new MulticastSocket();
          InetAddress address = InetAddress.getByName("228.5.6.7"); // 228.5.6.7 은 멀티캐스트 그룹을 나타내며 joinGroup은 멀티캐스트 그룹 참여에 사용
          socket.joinGroup(address);
      
          byte[] data;
          DatagramPacket packet;
      
          while(true) {
              Thread.sleep(1000);
              String message = LocalDateTime.now().toString();
              System.out.println("Sending: " + message);
              data = message.getBytes();
              packet = new DatagramPacket(data, message.length(), address, 9877); // 9877 포트는 서버에 할당
              socket.send(packet); // send 메소드를 통해 클라이언트에게 패킷 전송
          }
      } catch (IOException | InterruptedException e) {
          // 예외 처리
      }
      System.out.println("UDP Multicast Server Terminated");
      
    • Client
      System.out.println("UDP Multicast Client Started");
      try {
          MulticastSocket socket = new MulticastSocket(9877); // 9877 포트 할당
          InetAddress address = InetAddress.getByName("228.5.6.7"); // 멀티캐스트 주소를 사용하며 joinGroup 메소드를 통해 멀티캐스트 그룹 참여
          socket.joinGroup(address);
      
          byte[] data = new byte[256];
          DatagramPacket packet = new DatagramPacket(data, data.length); // 수신하는 메세지를 
          while(true) {
              socket.receive(packet);
              String message = new String(packet.getData(), 0, packet.getLength());
              System.out.println("Message from: " + message);
          }
      } catch (IOException e) {
          // 예외처리
      }
      System.out.println("UDP Multicast Client Terminated");
      
  • 채널에 의한 멀티캐스팅
    • Server
      try {
          System.setProperty("java.net.preferIPv6Stack", "true"); // IPv6 를 사용하게 지정
          DatagramChannel channel = DatagramChannel.open();
          NetworkInterface networkInterface = NetworkInterface.getByName("eth0"); // eth0 네트워크 인터페이스 생성
          channel.setOption(StandardSocketOptions.IP_MULTICAST_IF, networkInterface); // 채널과 그룹을 식별하는 데 사용된 네트워크 인터페이스를 연결
          InetSocketAddress group = new InetSocketAddress("FF01:0:0:0:0:0:0:FC", 9003);
      
          String message = "Message";
          ByteBuffer buffer = ByteBuffer.allocate(message.length()); // 바이트 버퍼를 문자열 크기만큼 생성
          buffer.put(message.getBytes());
      
          while(true) {
              // 버퍼는 그룹 멤버에게 전송
              channel.send(buffer, group);
              System.out.println("Sent the multicast message: " + message);
              buffer.clear();
      
              buffer.mark(); // 현재 position으로 marking 처리
              StringBuilder receiveMessage = new StringBuilder();
              while(buffer.hasRemaining()) {
                  receiveMessage.append((char)buffer.get());
              }
              System.out.print("Sent: " + receiveMessage.toString());
              buffer.reset(); // mark된 상태로 position 이동
          }
      } catch (IOException e) {
          // 예외처리
      }
      
    • Client
      try {
          System.setProperty("java.net.preferIPv6Stack", "true");
          NetworkInterface networkInterface = NetworkInterface.getByName("eth0");
          DatagramChannel channel = DatagramChannel.open()
                  .bind(new InetSocketAddress(9003))
                  .setOption(StandardSocketOptions.IP_MULTICAST_IF, networkInterface);
      
          // IPv6 주소 기반으로 생성되고, 채널의 join 메소드를 사용하여 MembershipKey 인스턴스 생성
          InetAddress group = InetAddress.getByName("FF01:0:0:0:0:0:0:FC");
          MembershipKey key = channel.join(group, networkInterface);
          // 예제에서는 1024 만큼 버퍼를 할당하며 메시지를 받기 전까지 블록된다
          ByteBuffer buffer = ByteBuffer.allocate(1024);
          channel.receive(buffer);
          // 버퍼 내용을 처리하기 위해 flip 처리해야 한다
          buffer.flip();
          StringBuilder message = new StringBuilder();
          while(buffer.hasRemaining()) {
              message.append((char)buffer.get());
          }
          System.out.println("Received: " + message.toString());
          // 키를 드롭하면 그룹 메시지를 수신하는 것에 관심 없음을 나타낸다
          key.drop();
      } catch (IOException e) {
          // 예외처리
      }
      
UDP Streaming
  • 오디오, 비디오 등의 스트리밍을 위해 UDP를 사용하는 것이 일반적이다
    • 효율적이며 패킷의 손실이나 잘못된 순서의 패킷 문제를 최소로 발생시킨다
  • 자바는 javax.sound.sampled 패키지의 클래스로 오디오 스트림을 처리한다
    • AudioFormat: 사용되는 오디오 포맷의 특성을 지정. 다양한 오디오 포맷을 사용할 수 있기 때문에 시스템은 사용되는 것을 인식해야 한다
    • AudioInputStream: 기록 및 재생되는 오디오를 나타냄
    • AudioSystem: 시스템의 오디오 디바이스 및 리소스에 대한 접속 제공
    • DataLine: 이 인터페이스는 스트림의 시작과 정지 같은 스트림에게 적용되는 동작 제어
    • SourceDataLine: 스피커 같은 사운드의 목적지를 나타낸다
    • TargetDataLine: 마이크로폰과 같은 사운드의 소스를 나타낸다
  • 오디오 전송 서버
    public class AudioUDPServer {
        private final byte[] audioBuffer = new byte[10000];
        private TargetDataLine targetDataLine;
        public AudioUDPServer() {
            setupAudio();
            broadcastAudio();
        }
        private void broadcastAudio() {
            try {
                // 루프백 IP, 8000 포트로 생성
                DatagramSocket socket = new DatagramSocket(8000);
                InetAddress address = InetAddress.getByName("127.0.0.1");
                while(true) {
                    // read 메소드를 통해 audioBuffer를 채우고 읽은 바이트의 수를 반환한다. 9786 포트에서 리스닝하여 클라이언트에게 송신
                    int count = targetDataLine.read(audioBuffer, 0, audioBuffer.length);
                    if(count > 0) {
                        DatagramPacket packet = new DatagramPacket(audioBuffer, audioBuffer.length, address, 9786);
                        socket.send(packet);
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        private void setupAudio() {
            try {
                AudioFormat format = getAudioFormat();
                DataLine.Info dataLineInfo = new DataLine.Info(TargetDataLine.class, format);
                targetDataLine = (TargetDataLine) AudioSystem.getLine(dataLineInfo);
                targetDataLine.open(format);
                targetDataLine.start();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        private AudioFormat getAudioFormat() {
            float sampleRate = 16000F;
            int sampleSizeIntBits = 16;
            int channels = 1;
            boolean signed = true;
    
            // 바이트 순서 의미 빅인디언: 최상위 바이트를 가장 작은 메모리 주소에 저장하고, 최하위 바이트를 가장 큰 메모리 주소에 저장/리틀인디언: 이 순서를 뒤집는다.
            boolean bigEndian = false;
            return new AudioFormat(sampleRate, sampleSizeIntBits, channels, signed, bigEndian);
        }
        public static void main(String[] args) {
            new AudioUDPServer();
        }
    }
    
  • 오디오 수신 클라이언트
    public class AudioUDPClient {
        AudioInputStream audioInputStream;
        SourceDataLine sourceDataLine;
        public AudioUDPClient() {
            try {
                // 9786 포트에 바인드되는 소켓 생성
                DatagramSocket socket = new DatagramSocket(9786);
                byte[] audioBuffer = new byte[10000];
    
                while(true) { // 서버에서 패킷을 수신, AudioInputStream 생성, 패킷이 수신될때까지 패킷은 생성되고 이후에 블록된다
                    DatagramPacket packet = new DatagramPacket(audioBuffer, audioBuffer.length);
                    socket.receive(packet);
    
                    try { // 오디오 스트림 생성, 바이트 배열에 패킷이 추출되며 실제 오디오 스트림을 생성하기 위해 오디오 포맷 정보와 함께 ByteArrayInputStream 사용 
                        byte[] audioData = packet.getData();
                        InputStream inputStream = new ByteArrayInputStream(audioData);
                        AudioFormat audioFormat = getAudioFormat();
                        audioInputStream = new AudioInputStream(inputStream, audioFormat, audioData.length / audioFormat.getFrameSize());
                        DataLine.Info dataLineInfo = new DataLine.Info(SourceDataLine.class, audioFormat);
                        sourceDataLine.open(audioFormat);
                        sourceDataLine.start();
                        playAudio();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        // AudioInputStream의 read 메소드는 소스 데이터 라인에 기록된 버퍼를 채운다
        private void playAudio() {
            byte[] buffer = new byte[10000];
            try {
                int count;
                while( (count = audioInputStream.read(buffer, 0, buffer.length)) != -1) {
                    if(count > 0) {
                        sourceDataLine.write(buffer, 0, count);
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        private AudioFormat getAudioFormat() {
            float sampleRate = 16000F;
            int sampleSizeIntBits = 16;
            int channels = 1;
            boolean signed = true;
    
            // 바이트 순서 의미 빅인디언: 최상위 바이트를 가장 작은 메모리 주소에 저장하고, 최하위 바이트를 가장 큰 메모리 주소에 저장/리틀인디언: 이 순서를 뒤집는다.
            boolean bigEndian = false;
            return new AudioFormat(sampleRate, sampleSizeIntBits, channels, signed, bigEndian);
        }
    }
    
    출처
  • 에이콘 출판사. 자바 네트워크 프로그래밍. 리차드 리스 지음, 유연재 옮김 (http://www.yes24.com/Product/Goods/34894821)