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
- 채널에 의한 멀티캐스팅
- 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) { // 예외처리 }
- Server
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)