UDP_java를 통한 패킷전송

koeyhoyh·2021년 12월 1일
2
post-thumbnail

<Network Programming 강의 요약>

TCP의 특성

TCP : Connection-Oriented Mode

데이터를 전달하기 전에 미리 연결 설정
한 번 연결되면 반복적으로 데이터 전송

Reliable service

처리율이 100%

예제

  • 상대방 번호를 눌러 전화를 건다.
  • 상대가 전화를 받으면 연결된다.
  • 데이터를 전달을 반복한다.
  • 전화 연결을 종료한다.

Tradeoff

  • 100% 신뢰할 수 있다. (High Reliability)
  • 속도가 느리다 (Low Speed)
    - 연결 과정에 시간 소요
    • 확인(Acknowledgement) 및 재전송(Retransmission)에 시간 소요

UDP (User Datagram Protocol)

IP 계층 위에 놓인 비연결형 전달 계층 프로토콜

UDP는 Unreliable (믿을 수 있지만, 100%를 보장하지는 않는다.)

  • 데이터 전달의 정확성을 보장하지 않는다.
  • 데이터의 전달 순서를 보장하지 않는다(전송 순서대로 도착하지 않을 수 있다.)
  • 전송 계층인 UDP의 Unreliability에 대해서는 응용프로그램에서 대처해야 하며 필요할 경우 손실되거나 손상된 데이터의 복구 문제를 해결해야 한다.

UDP의 활용

  • 데이터 전달의 정확성보다 제시간 정송(Timeliness)이 더욱 중시 되는 애플리케이션 또는 근거리 네트워크에서 이용
  • 사례 : Audio / Video 등 멀티미디어 데이터 (실시간 동영상, 화상회의)

UDP의 특성

  • Connectionless mode
    - Connectionless 서비스에서는 각각의 데이터그램 전달에 필요한 목적지 주소가 데이터그램에 포함되어야 한다.
  • Unreliable Service
    - 100%는 신뢰할 수 없는 서비스
  • 비유 : 편지, 메일

DatagramPacket과 DatagramSocket

  • UDP의 핵심어는 Connectionless (연결과정 X)
    - 연결 과정이 없고 데이터를 전송할 때마다 상대방 주소와 포트번호를 제시해야 한다.
    • UDP 소켓은 상대와 무관하게 자신 혼자서 연다.

  • Java에서 UDP 프로토콜 서비스를 활용하기 위한 클래스
    - DatagramPacket
    -데이터 송신을 위해 Datagram이라는 UDP 패킷으로 포장하거나 수신한 Datagram으로부터 데이터를 추출하는 클래스

    • DatagramSocket
      • UDP Datagram을 보내거나 받는 클래스
        - 서버 소켓, 클라이언트 소켓의 구별이 없다.
        - 서버와 클라이언트 모두 송수신에 동일한 소켓 사용

UDP Client의 동작

  • 포트 0번에서 소켓을 연다
    - DatagramSocket socket = new DatagramSocket(0);
    - 0으로 임의의 여유분 로컬 포트를 지정

    • UDP 소켓은 원격 호스트에 대해서는 아는 것이 없다.
    • DatagramSocket은 AutoCloseable 인터페이스를 구현하고 있으므로 try-with-resource 구문 사용이 가능하다.
      • try(DatagramSocket socket = new DatagramSocket(0)) {
        // 서버와 메시지 교환
        }catch(IOException e){
        	System.err.println("Could not bind to port 0");
            }
  • 소켓 옵션 설정
    - socket.setSoTimeout(10000);

    • 선택 사항이지만 사용하는 것이 바람직하다.
      • TCP일 경우 IOException이 발생할 많은 문제에도 UCP에서는 사용자에게 알리지 않고 조용히 처리됨
    • 한없이 기다리는 경우를 방지한다.
  • 송신용 패킷을 준비한다.
    - InetAddress remoteHost = InetAddress.getByName("time.nist.gov");

    • DatagramPacket request = new DatagramPacket(new byte[1], 1, remoteHost, 13);
      • 보낼 패킷에서 중요한 것은 원격 주소와 포트
  • 수신용 패킷을 준비한다.
    - byte[] data = new byte[1024];

    • DatagramPacket response = new DatagramPacket(data, data.length);
  • 서버와 패킷을 보내고 받는다
    - socket.send(request)

    • socket.receive(response);
  • 서버가 보낸 응답 패킷으로부터 데이터를 추출해서 문자열로 변환해 사용한다.
    - String result = new String(response.getData(), 0, response.getLength(), "US-ASCII");

    • System.out.println(result);

예제 : Daytime Protocol client

  • 서버에게 시간을 요청한다.
    - 서버에게 DatagramPacket을 전송한다.
    • 서버가 클라이언트의 주소와 포트를 알 수 있게 한다.
  • 실행 : java DaytimeUDPClient
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class UDPDaytimeClient1 {
	private static final int PORT = 13;
	private static final String HOSTNAME = "time.nist.gov";
	public static void main(String[] args) {
		try (DatagramSocket socket = new DatagramSocket(0)) {
			socket.setSoTimeout(10000);
			InetAddress host = InetAddress.getByName(HOSTNAME);
			DatagramPacket request = new DatagramPacket(new byte[1], 1, host, PORT);
			DatagramPacket response = new DatagramPacket(new byte[1024], 1024);
			socket.send(request);
			socket.receive(response);
			String result = new String(response.getData(), 0, response.getLength(), "US-ASCII");
			System.out.println(result);
		} catch (IOException ex) {
			ex.printStackTrace();
		}
	}
}

UDP Server

UDP Server의 동작

  • 소켓을 연다.
    - Daytime의 경우 13번 포트
    • DatagramSocket socket = new DatagramSocket(13);
  • 수신용 패킷을 준비한다.
    - DatagramPacket request = new DatagramPacket(new byte[1024], 1024);
  • 패킷을 수신한다.
    - socket.receive(request);
  • 송신용 패킷을 준비한다.
    - String daytime = new Date().toString();
    • byte[] data = daytime.getBytes("US-ASCII");
    • DatagramPacket response = new DatagramPacket(data, data.length, request.getAddress(), request.getPort());
  • 패킷을 송신한다.
    - socket.send(response);

예제 Daytime Protocol Server

  • 실행 : java DaytimeUDPServer

코드 1

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.util.Date;
public class UDPDaytimeServer1 {
	public final static int DEFAULT_PORT = 13;
	public static void main(String[] args) {
		try {
			DatagramSocket socket = new DatagramSocket(DEFAULT_PORT);
			DatagramPacket packet = new DatagramPacket(new byte[1], 1);
			while (true) {
				socket.receive(packet);
				System.out.println("Request from " + packet.getAddress() + " : " + packet.getPort());
				byte[] outBuffer = (new Date().toString() + " from JNU UDP Daytime server").getBytes();
				packet.setData(outBuffer);
				packet.setLength(outBuffer.length);
				socket.send(packet);
			}
		} catch (IOException ex) {
			System.err.println(ex);
		}
	}
}

코드 2

import java.net.*;
import java.util.Date;
import java.util.logging.*;
import java.io.*;
public class UDPDaytimeServer2 {
	private final static int PORT = 13;
	public static void main(String[] args) {
		try (DatagramSocket socket = new DatagramSocket(PORT)) {
			while (true) {
				try {
					DatagramPacket request = new DatagramPacket(new byte[1024], 0, 1024);
					socket.receive(request);
					String daytime = new Date().toString();
					byte[] data = daytime.getBytes("MS949");
					DatagramPacket response = new DatagramPacket(data, data.length, request.getAddress(),
							request.getPort());
					socket.send(response);
					System.out.println(daytime + " " + request.getAddress());
				} catch (IOException | RuntimeException e) {
					System.err.println(e.getMessage());
					e.printStackTrace();
				}
			}
		} catch (IOException e) {
			System.err.println(e.getMessage());
			e.printStackTrace();
		}
	}
}

주의

  • UDP 클라이언트가
    - DatagramPacket sendP = new DatagramPacket(데이터, 길이, 상대주소, 상대포트);의 패킷을 보내면
  • UDP 서버는
    - DatagramPacket packet = new DatagramPacket(데이터, 길이); 형태의 패킷을 받는다.
    • 패킷을 보내온 클라이언트의 주소와 포트는 packet.getAddress(), packet.getPort()로 얻는다.

다르고, 주소와 포트는 메소드로 얻는다. 왜??


DatagramPacket

DatagramPacket 클래스

  • UDP는 IP 서비스를 거의 그대로 이용한다.
    - 따라서 UDP Datagram은 IP Datagram과 별 차이가 없다.
    • IP Datagram = IP Header (20 ~ 60 bytes) + UDP Datagram
    • UDP Datagram = UDP Header (8 bytes) + Application Data
    • UDP Header 8 bytes의 내용
      • Source port (2 bytes) : 0 ~ 65535의 범위 (호스트마다 65535개의 UDP 포트 가능)
        • Destination Port (2 bytes)
        • UDP Datagram Length (2 bytes) : Data + UDP Header의 길이
        • Checksum (2 bytes)

UDP Datagram Length

  • 2-byte unsigned byte로 표현되며 최대 값은 65535, 단위는 바이트

  • UDP Datagram은 IP Datagram에 캡슐화된다
    - UDP Datagram에 IP Header가 붙어 IP Datagram을 구성한다.

  • 그렇지만 IP Datagram 헤더의 Length 필드 역시 2바이트로 최대 값은 65535

  • UDP Datagram의 최대 길이는 IP 헤더용 20바이트와 UDP 헤더용 8바이트를 제외한 65535 - (20 + 8) = 65507 바이트지만, IP 헤더에는 옵션을 40바이트까지 허용하므로 길이는 65535 - (20 + 8 + 40) = 65467이다.

  • 이론적으로 Datagram의 최대 길이는 65507 이지만 현실적으로는 이보다 훨씬 작게 대개는 8KB로 제한되며 보통의 응용프로그램은 512바이트로 처리한다.

  • UDP Datagram의 최소 길이는 8 바이트
    - 극단적으로 데이터를 포함하지 않고 헤더만 전송하는 것도 허용한다.

  • UDP Datagram의 길이는 IP Datagram의 길이에 의존적이므로 사실 이 필드는 무의미하다고 할 수 있다.

Checksum

  • Checksum은 자바 응용프로그램에서는 접근 불가능
  • 데이터에 대한 Checksum이 틀릴 경우 native network software가 데이터그램을 폐기한다.
  • 이런 상황에 관해 UDP는 sende나 receive에게 통보하지 않는다.
    - 따라서 unreliable이라 불림

UDP Port

  • 주의 : UDP 포트는 TCP 포트와 다르다.
    - 즉 TCP 서버와 UDP 서버는 동일한 숫자의 포트번호를 사용할 수 있다.

DatagramPacket 생성자

  • 자바의 UDP 데이터그램은 DatagramPacket 객체로 표현한다.
    - public final class DatagramPacket extends Object
  • 생성자는 6가지
    - 2가지의 수신용
    - 4가지의 송신용

수신용 DatagramPacket 생성자

  • 네트워크로부터 데이터를 수신하기 위한 DatagramPacket 생성자
  • public DatagramPacket(byte[] buf, int length)
  • public DatagramPacket(byte[] buf, int offset, int length)
    - buf - 수신 데이터그램의 데이터부분을 저장할 버퍼
    • offset - 시작 바이트
    • length - 저장할 바이트 수, buf 배열의 길이보다 작아야 한다.
    • 데이터그램을 수신한 소켓은 데이터그램의 데이터 부분을 buf의 0 또는 offset 위치로부터 length개의 바이트를 저장한다. (또는 수신 데이터가 완전히 옮겨질 때까지 저장)
    • length <= (buf배열의 길이 - offset)가 성립하지 않으면 IllegalArgumentException 발생
      • 이 예외는 RuntimeException이므로 Unchecked Exception이다.

예 : 8192 바이트 길이의 데이터그램을 수신하기 위한 DatagramPacket을 생성하는 코드

import java.net.*;
public class UDPReceiver {
    public static void main (String args[ ]) {
        byte[] buffer = new byte[8192];
        DatagramPacket dp = new DatagramPacket(buffer, buffer.length);
    }
}

송신용 DatagramPacket

  • 다른 호스트로 전송할 DatagramPacket을 생성하며 수신용과의 차이점은 Remote 주소와 포트를 포함하는 점
    - public DatagramPacket(byte[] data, int length, InetAddress destination, int port)
    • public DatagramPacket(byte[] data, int offset, int length, InetAddress destination, int port)
    • public DatagramPacket(byte[] data, int length, SocketAddress destination, int port)
    • public DatagramPacket(byte[] data, int length, SocketAddress destination, int port)
      - data : UDP가 취급하는 데이터는 바이트 배열 형태
      • offset : packet data offset
      • length : 보낼 데이터의 바이트 수 (data.length - offset보다 큰 length를 가진 DatagramPacket을 생성하려고 하면 IllegalArgumentException이 발생한다.)
      • destination : 패킷 목적지의 주소
      • port : 목적지의 포트 번호
      • 데이터그램을 송신할 소켓은 data 배열의 0 또는 offset 위치로부터 length개의 바이트로 데이터그램의 데이터를 구성한다.

바이트 배열은 어떻게 생성하는가

  • UDP 송신용 DatagramPacket 생성을 위한 바이트 배열 생성
    - 보통 String 의 getBytes()를 사용
    • 또는 java.io.ByteArrayOutputStream을 사용

String s = "This is a Test";
byte[] data = s.getBytes("UTF-8");
try {
	InetAddress addr = InetAddress.getByName("www.ibiblio.org");
	int port = 7;
	DatagramPacket dp = new DatagramPacket(data, data.length, addr, port);
	// 이 위치에서 데이터그램을 전송한다
} catch (IOException e) { }

DatagramPacket의 get 메소드

  • DatagramPacket 클래스의 6개 메소드를 이용하여 데이터그램에 관한 정보를 확인할 수 있다.
    - InetAddress getAddress()
    • int getPort()
    • SocketAddress getSocketAddress()
    • byte[] getData()
    • int getOffset()
    • int getLength()

InetAddress getAddress()

  • 2종류의 원격 호스트 주소
    - DatagramPacket을 수신했을 때는 패킷을 보낸 remote 호스트의 주소
    - UDP 데이터그램을 보낸 호스트의 주소를 알아 내어 응답을 보내기 위한 용도로 사용하며 이 때 패킷은 DatagramPacket(byte[] buf, int length) 타입이다.
    - 송신을 위해 DatagramPacket을 생성했을 경우에는 목적지 주소

int getPort()

  • 2종류의 원격 호스트 포트 번호
    - DatagramPacket을 수신했을 때는 패킷을 보낸 호스트의 remote 포트
    • 송신을 위해 DatagramPacket을 생성했을 경우에는 목적지 포트

      (수신용은 DatagramPacket input = new DatagramPacket(new byte[512], 512); 형식의 패킷임을 참고)

(참고) Local 주소와 포트를 알려면

  • DatagramSocket의 getLocalAddress(), getLocalport() 이용

SocketAddress getSocketAddress()

  • Remote 호스트의 IP주소와 포트 번호가 들어 있는 SocketAddress 객체를 반환
  • 대개는 UDP 데이터그램을 받았을 때 이것을 보낸 송신자의 주소를 알아 내어 응답을 보내기 위한 용도로 사용한다.
  • 송신을 위해 데이터그램 패킷을 생성했을 경우에는 목적지 주소이다.

byte[] getData()

  • 데이터그램의 데이터를 가진 바이트 배열을 리턴한다.
  • 데이터그램을 수신했을 때, 이를 사용하기 위해서는 대개 이 것을 다른 타입으로 바꾸는 변환 과정이 필요하다.

데이터그램을 String으로 변환하는 방법

  • String 생성자
    - public String(byte[] buffer, String encoding)
    • public String(byte[] bytes, int offset, int length, String encoding)
    • public String(byte[] bytes, int offset, int length, Charset charset)
  • 예 : dp가 전달받은 DatagramPacket일 경우
    - String s = new String(dp.getData(), "UTF-8");

데이터그램을 ByteArrayInputStream으로 변환하는 방법

  • getData()로 넘겨받은 데이터로부터 ByteArrayInputStream 객체를 생성한다.
  • 생성자 public ByteArrayInputStream (byte[] data, int offset, int length)을 이용
    - 바이트 배열은 데이터그램의 getData()를 통해 얻으며
    • offset은 0이거나 데이터그램의 getOffset() 메소드를 통해 얻는다.
    • length는 데이터그램의 getLength() 메소드를 통해 얻는다.

  • - InputStream is = new ByteArrayInputStream(packet.getData(), packet.getOffset(), packet.getLength());
    • DataInputStream dis = new DataInputStream(is);
    • // readInt(), readLong(), readChar() 등의 메ㅐ소드를 이용하여 데이터를 읽는다.

int getLength()

  • 데이터그램 내의 데이터의 바이트 수 return
  • 이것은 실제 데이터의 개수이므로 데이터그래미의 바이트 배열의 길이, 즉 getData()를 통해 반환되는 바이트 배열의 길이보다 작을 수 있다.

int getOffset()

  • getData()로부터 리턴되는 바이트 배열에서 데이터가 시작되는 지점 리턴

예제 : 송신을 위한 DatagramPacket 생성

  • 이 예제에서는 아직 UDP 데이터 전송 방법을 학습하지 않은 상황에서, 임의로 UDP 패킷을 만들어 DatagramPacket에 포함된 각종 메소드를 시험 호출해 본다.
import java.io.*;
import java.net.*;

public class UDPDatagramExample {

  public static void main(String[] args) {
  
    String s = "This is a test.";

    try {
      byte[] data = s.getBytes("UTF-8");
      InetAddress ia = InetAddress.getByName("www.ibiblio.org");
      int port = 7;
      DatagramPacket dp = new DatagramPacket(data, data.length, ia, port);
      System.out.println("이 패킷을 보낼 곳의 주소는 " + dp.getAddress() + ", 포트는 " + dp.getPort());
      System.out.println("패킷의 길이는 " + dp.getLength()  + " 바이트임");
      System.out.println("보낼 데이터는 " + new String(dp.getData(), dp.getOffset(), dp.getLength(), "UTF-8"));
    } catch (UnknownHostException | UnsupportedEncodingException ex) {
      System.err.println(ex);
    }
  }
}

결과 :

set 메소드

  • 데이터그램 생성 후에 원격지 주소, 포트 변경 가능
  • DatagramPacket 객체를 재사용하면 객체 데이터를 더 빠르게 전송할 수 있다.
    - 데이터를 보낼 때마다 새로운 DatagramPacket을 매번 생성하여 사용하는 것은 쓰레기 처리 때문에 성능에 부정적 영향을 미치므로 기존의 객체를 재사용하는 것이 유리

setData()

  • public void setData(byte[] data)
  • public void setData(byte[] data, int offset, int length)
    - UDP 데이터그램 내의 데이터(Payload)를 교체한다.
    • 보낼 데이터가 너무 커서 분할하여 전송할 때 요긴하며 데이터를 전송할 때 동일한 DatagramPacket 객체의 데이터 부분만을 바꾸어 전송한다.

전송 예 : 모든 데이터를 하나의 대형 배열(bigArray)에 넣고 한 번에 한 조각(512bytes)씩 쪼개서 보낸다.

// 이 위치에서 bigArray에 데이터를 넣는다.
int offset = 0
DatagramPacket dp = new DatagramPacket(bigArray, offset, 512);
int bytesSent = 0;
while(bytesSent < bigArray.length) {
	
    socket.send(dp); // 하나의 데이터그램을 전송
    bytesSent += dp.getLength(); // 보낸 바이트 수 누적
    int bytesToSend = bigArray.length - bytesSent; // 잔여 바이트 수
    int size = (byteToSend > 512) ? 512 : bytesToSend;
    dp.setData(bigArray, bytesSent, size)
    }

setAddress()

  • public void setAddress(InetAddress remote)
    - UDP 데이터그램 내의 목적지 주소를 바꾼다.
    • 동일한 데이터를 여러 수신자에게 전송할 때 요긴

사용 예 : 로컬 네트워크에 연결된 모든 호스트로 전송

String s = "Really Important Message";
byte[] data = s.gerBytes("UTF-8");
DatagramPacket dp = new DatagramPacket(data, data.length);
dp.setPort(2000);
String network = "128.238.5.";

for (int host = 1; host < 255; host++) {
	try{
    	InetAddress remote = InetAddress.getByName(network + host);
        dp.setAddress(remote);
        socket.send(dp);
        }catch(Exception e) {}
    }

setPort(int)

  • public void setPort(int port)
    - 포트 변경
    • 용도 매우 좁음

setAddress(SocketAddress)

  • public void setAddress(SocketAddress remote)
    - 주소와 포트를 변경한다.

사용 예 : 수신한 데이터그램으로부터 목적지 SocketAddress을 추출하여 송신용 데이터그램에 반영하여 보내는 예

DatagramPacket input = new DatagramPacket(new byte[8192], 8192);
socket.receive(input);

DatagramPacket output = new DatagramPacket("Hello there".getBytes("UTF-8"), 11);
SocketAddress address = input.getSocketAddress();
output.setAddress(address);
socket.send(output);

setLength()

  • public void setLength(int length)
    - 데이터그램의 데이터 부분으로 채워져 있는 내부 버퍼의 바이트 수 변경

UDP Daytime 서버의 패킷 보내기(Revisit)

  • 수신
    - DatagramPacket packet = new DatagramPacket(new byte[1024], 1024);
    • socket.receive(packet);
  • 송신
    - String daytime = new Date().toString();
    • byte[] data = daytime.getBytes("US-ASCII");
    • packet.setData(data);
    • packet.setLength(data.length);
    • socket.send(packet);
  • 참고 : 송신용으로 수신 패킷을 그대로 사용하면 주소, 포트 처리가 필요없다. # 나중에 숙달 된다면...
  • (packet.getAddress(), packet.getPort() 를 이용해 주소와 포트를 패킷에 반영할 필요가 없다.)

DatagramSocket

DatagramSocket 클래스

  • public class DatagramSocket extends Object
  • 데이터그램을 보내거나 받을 때 사용하는 유일한 UDP 소켓 클래스
    - UDP 에서는 클라이언트 소켓과 서버 소켓이 동일하다.
    • DatagramServerSocket과 같은 것은 존재하지 않는다.
    • 하나의 DatagramSocket으로 복수의 원격 호스트 및 포트와 데이터그램을 주고 받을 수 있다.
      • UDP 소켓에는 상대(짝) 이라는 개념이 없다. 그저 데이터그램의 송수신에 쓰인다.

DatagramSocket 생성자

  • public DatagramSocket() throws SocketException
  • public DatagramSocket(int localPort) throws SocketException
  • public DatagramSocket(int localPort, InetAddress localAddr) throws SocketException

public DatagramSocket() throws SocketException

  • 임의의 포트에 바인드되는 DatagramSocket을 생성

  • 클라이언트용 소켓 생성

  • DatagramSocket이 설정된 로컬 포트를 알려면 getLocelport() 이용

  • 사용 예

public static void main(String[] args) {
	try {
		DatagramSocket theClient = new DatagramSocket();
	} catch(SocketException e) {
	}
}

public DatagramSocket(int localPort) throws SocketExcepion

  • 지정된 포트에서 들어오는 데이터그램을 기다리는 DatagramSocket을 생성한다.
  • 주로 서버용 소켓을 생성할 때 사용한다
  • UDP Port와 TCP Port는 서로 무관하다.
    - 동일한 번호를 사용할 수 있다.

예제 : 로컬 UDP 포트 검색

  • DatagramSocket 생성을 시도하여 예외가 발생하면 포트가 이미 사용중이라고 결론 내린다.

  • UDP 포트 사용 예
    - 30,000 번대는 RPC(Remote Procedure Call)가 사용

    • NFS, TFTP, FSP가 UDP 사용
import java.net.*;
import java.net.*;
public class UDPPortScanner {
	public static void main(String[] args) {
		for (int port = 1024; port <= 65535; port++) {
			try {
				// 포트 i에서 실행되는 서버가 있을 경우
				// 다음 줄은 예외를 발생시키고 catch 블록으로 떨어진다
				DatagramSocket server = new DatagramSocket(port);
				server.close();
			} catch (SocketException ex) {
				System.out.println("There is a server on port " + port + ".");
			}
		}
	}
}

결과 :

public DatagramSocket(int port, InetAddress interface) throws SocketException

  • 로컬 포트와 주소를 지정한다
  • Multi-homed 호스트에서 사용
  • 지정된 포트와 지정된 네트워크 인터페이스에 소켓을 생성
    - port : 소켓이 데이터그램을 기다리는 로컬 포트
    • interface : 로컬 호스트의 네트워크 주소 중 하나에 대한 InetAddress 객체
  • 객체 생성에 실패하는 예
    - 지정된 포트가 이미 사용 중일때
    • 루트 권한이 없는 자가 1024번 이하의 포트를 사용하려고 할 때
    • 제시된 주소가 실제 존재하지 않는 네트워크 인터페이스 주소일 때

public DatagramSocket(SocketImpl impl) throws SocketException

  • 개발자 고유의 UDP 구현 사용 생성자
  • 이상의 4개의 생성자는 포트에 바인드되어 있는 소켓을 생성하지만 이 생성자에 의해 생성되는 소켓은 포트에 바인드되어 있지 않다.
  • 생성 후 사용하기 전에 다음 메소드를 사용하여 바인드해야 한다.
    - public void bind(SocketAddress addr) throws SocketException

UDP Datagram 보내고 받기

UDP 송수신

  • DatagramSocket 하나로
    - 데이터그램 송수신 가능
    • 여러 호스트에 동시에 데이터그램 송수신 가능

send

  • public void send(DatagramPacket dp) throws IOException
    - DatagramPacket이 생성되고, DatagramSocket이 열리면 소켓의 send() 메소드에 패킷을 넘김으로써 데이터를 전송

  • 예 : theSocket.send(theOutput);

  • UDP는 신뢰성이 없는 프로토콜이므로 전송된 패킷이 목적지에 도달하지 않더라도 예외가 발생하지 않는다.

  • 단, 다음과 같은 경우에는 IOException 발생
    - 호스트의 네트워크 소프트웨어가 지원하는 것보다 큰 데이터그램을 보내려고 하면 Exception 발생

    • (운영체제에 있는 UDP 소프트웨어와 자바 DatagramSocketImpl의 코드에 따라 달라짐)
  • Security Manager가 통신을 불허할 때 SecurityException 발생

receive

  • public void receive(DatagramPacket dp) throws SocketTimeoutException
    - 네트워크를 통해 수신된 UDP 데이터그램을 DatagramPacket 객체로 변환한다
    - 네트워크로부터 UDP 데이터그램을 수신하여 미리 생성되어 있는 DatagramPacket 객체에 저장
    • 이 메소드가 호출되면 데이터가 도착할 때까지 블록된다.
      • SocketSercer의 accept()와 유사
    • Timeout이 초과하면 SocketTimeoutException 예외가 발생됨

특별한 예외 (Revisited)

  • InterruptedException
    - java.lang.Throwable <- java.lang.Exception <- InterruptedException
    • 스레드가 waiting, sleeping, 또는 다른 이유로 장시간 멈추어 있을 때 다른 스레드가 Thread의 interrupt()를 호출하여 유휴 상태가 유지되는 것을 방해할 때 던져진다.
  • InterruptedIOException (타임아웃 표시 예외)
    - java.lang.Throwable <- java.lang.Exception <- InterruptedException <- java.io.InterruptedIOException <- java.net.SocketTimeoutException
    • I/O 동작이 방해를 받았다는 표시
    • accept() 메소드가 호출되면 카운트다운이 시작되고 타임아웃되면 accept()는 InterruptedIOException을 발생시킴 (1.4에서는 이 예외의 서브클래스인 SocketTimeoutException을 발생시킴)

패킷 받기

  • 특정 포트에 소켓을 연다
    - DatagramSocket socket = new DatagramSocket(13);
  • 수신용 패킷 생성
    - byte[] buffer = new byte[65507];
    • DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
  • 패킷이 도착하기를 기다린다.
    - socket.receive(packet);
  • 받은 패킷의 처리
    - getData(), getAddress(), getPort(), getLength() 활용
    • getData()로 얻는 바이트 배열은 실제로는 패킷에 제공했던 buffer와 동일
    • 예 :
      • System.out.println("Request from" + packet.getAddress() + ":" + packet.getPort());

패킷 보내기

  • 소켓을 연다
    - DatagramSocket socket = new DatagramSocket();
    - 로컬 포트는 신경 쓰지 않는다.
  • 송신용 패킷 생성
    - Remote 주소와 포트 반영
    • 예 :
      • byte[] buffer = new byte[65507];
        • DatagramPacket packet = new DatagramPacket(buffer, buffer.length, InetAddress.getByName("www.nsc.gov"), 1628);
  • 패킷을 보낸다.
    - socket.send(packet);
    • 보내는 측의 로컬 주소와 포트는 UDP 헤더의 일부로서 자동적으로 보내진다.

예제 : UDP 버리기 클라이언트

  • Well-Known port 9
  • DatagramSocket을 연다.
  • 사용자가 .(점)을 입력할 때까지 다음을 반복
    - System.in에서 1줄의 사용자 입력을 받아 버리기 서버에 보낸다.
    - 보낼 DatagramPacket 객체를 생성한다.
    - 사용자의 입력을 readLine()으로 받아 -즉 String으로 받아- byte 배열로 변환하여 보낼 데이터를 만든다.(userInput.getBytes())
    - 상대편 주소와 포트를 패킷에 반영
      - DatagramPacket 객체를 send 한다.
  • 서버가 보내 올 응답에는 신경 쓰지 않는다.
import java.net.*;
import java.io.*;

public class UDPDiscardClient {

  public final static int PORT = 9;

  public static void main(String[] args) {

    String hostname = args.length > 0 ?  args[0] : "localhost";
    
    try (DatagramSocket theSocket = new DatagramSocket()) {
      InetAddress server = InetAddress.getByName(hostname);
      BufferedReader userInput 
          = new BufferedReader(new InputStreamReader(System.in));
      while (true) {
        String theLine = userInput.readLine();
        if (theLine.equals(".")) break;
        byte[] data = theLine.getBytes();
        DatagramPacket theOutput = new DatagramPacket(data, data.length, server, PORT);
        theSocket.send(theOutput);
      } // end while
    } catch (IOException ex) {
      System.err.println(ex);
    }
  }

예제 : UDP 버리기 서버

  • 서버는 클라이언트로부터 보내온 자료를 그냥 버린다.
  • 처리할 내용 :
    - 9번 포트로 소켓을 연다.
    • 수신용 packet을 마련한다.
    • 패킷을 수신한다.
    • 본래의 버리기 서버의 기능으로는 불필요하나, 확인을 위해 System.out에 출력하는 기능을 둔다.
      • 클라이언트의 주소
        • 클라이언트의 포트
        • 보내온 데이터 등
import java.net.*;
import java.io.*;

public class UDPDiscardServer {

  public final static int PORT = 9;
  public final static int MAX_PACKET_SIZE = 65507;

  public static void main(String[] args) {

    byte[] buffer = new byte[MAX_PACKET_SIZE];

    try (DatagramSocket server = new DatagramSocket(PORT)) {
      DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
      while (true) {
        try {
          server.receive(packet);
          String s = new String(packet.getData(), 0, packet.getLength(), “MS949");
          System.out.println(packet.getAddress() + " at port " + packet.getPort() + " says " + s);
          // 다음 패킷 수신을 위해 길이를 리셋한다 # (받고 나면 길이가 줄어들어 있다!!)
          packet.setLength(buffer.length);
        } catch (IOException ex) {
          System.err.println(ex);
        }      
       } // end while
    } catch (SocketException  ex) {
      System.err.println(ex);
    } 
  }
}

DatagramSocket의 다른 메소드들

  • public synchronized void close()
    - 소켓이 점유하고 있는 포트의 해제
  • public int getLocalPort()
    - 로컬 호스트에서 소켓이 귀를 기울이고 있는 포트의 번호를 int로 반환
    • 익명 포트를 사용해서 DatagramSocket을 생성한 경우, 사용 중인 포트 번호를 알고자 할 때 이 메소드를 사용
  • public InetAddress getLocalAddress()
  • public InetAddress getLocalSocketAddress()

연결 관리

  • TCP 소켓과 달리 UDP 소켓은 통신 상대가 누구인가는 중요하지 않다.

  • 그러나 종종 상대를 고정해야 하는 경우가 있다. 게임 같은 경우가 여기에 해당된다. 이럴 때는 게임에 참여한 사람으로부터의 데이터그램만 기다려야 한다.
    - 이 기능은 데이터를 주고 받을 상대를 특정인으로 고정하는 것이다.

  • public void connect(InetAddress remoteHost, int remotePort)
    - TCP와 같은 성질의 연결이 아니며 지정된 호스트하고만 패킷을 주고 받을 것임을 설정
    - 다른 호스트로 데이터그램을 전송하려 시도하면 IllegalArgumentException 발생
    - 다른 호스트나 포트에서 전달받은 데이터는 폐기

    • 메소드 호출 시 보안 체크하며 해당 포트와 데이터 전송에 문제가 발생하면 SecurityException 발생
  • public void disconnect()
    - 연결 종료

    • 이제 다시 connect 이전처럼 임의의 호스트와 패킷 교환 가능
  • public int getPort()
    - 연결되어 있는 소켓의 포트번호 return

    • 연결되어 있지 않으면 -1 return
  • public InetAddress getInetAddress()
    - 연결되어 있는 소켓의 원격지 주소 반환

    • 연결되어 있지 않다면 null return
  • public SocketAddress getRemoteSocketAddress()
    - 연결되어 있는 소켓의 원격지 주소 반환

    • 연결되어 있지 않으면 null return

소켓 옵션 SO_TIMEOUT

  • public syncrhonized void setSoTimeout(int timeout) throws SocketException
    - 데이터그램 소켓의 SO_TIMEOUT 필드에 값을 설정
    • receive() 메소드가 데이터그램이 들어오기를 기다려야 하는 시간을 milliseconds 단위로 설정한다. 설정된 시간이 지나면 SocketTimeoutException 발생
  • public synchronized int getSoTimeout() throws IOException
    - getSoTimeout() 메소드는 현재 DatagramSocket 객체의 SO_TIMEOUT 필드 값을 반환
    • int timeout = ds.getSoTimeout();

나머지 소켓 옵션들은 지나가도록 하겠습니다.


유용한 애플리케이션들

Simple UDP Clients

  • 몇몇 인터넷 UDP 서비스는 서비스를 요청하는 클라이언트의 주소와 포트번호만 알면 동작할 수 있다.
    - 서버는 클라이언트로부터 Datagram 형태로 보내 오는 데이터를 그냥 폐기하고 데이터와 무관하게 서비스 한다.
    • 이런 서버는 동일한 방식으로 동작하므로 서버에게 데이터그램을 보내고 보내 오는 응답을 읽는 UDPPoke라는 하나의 공용 클라이언트를 통해 동작을 시험할 수 있다.
  • 예 :
    - Daytime (port 13) 텍스트 시간
    • Quote of the day (port 17) 오늘의 명언
    • Time (port 37) 바이너리 시간
    • Chargen (port 19) 문자발생기 (연속된 문자)

예제 : 공용 클라이언트 UDPPoke

  • 이 클라이언트는 특정 주소와 포트에서 실행되는 UDP 서버에게 자신의 주소와 포트만 알려주면 된다.
    - 클라이언트는 서버의 주소와 포트가 들어 있고 데이터 부분이 비어 있는 UDP 패킷을 보낸다.

    • 서버는 수신된 패킷으로부터 getAddress() 등으로 클라이언트의 주소와 포트를 알아낸다.
    • 클라이언트는 서버로부터 보내 오는 응답 패킷을 읽는다.
  • 세 개의 private 필드
    - bufferSize : return 받을 패킷을 담을 버퍼의 크기

    • socket : DatagramSocket 객체
    • outgoing : DatagramPacket 객체
  • <강조> socket.setSoTimeout(int)
    - 설정된 시간을 초과하여도 응답이 없으면 catch(InterruptedIOException ex) {} 이 실행된다.

  • poke()
    - UDP 클라이언트가 돌려받는 데이터는 바이트 배열 형태이므로 반환타입을 byte[]로 설정하며 null로 초기화된다.

    • timeout 시간 안에 메시지를 받으면 바이트 배열 response에 저장된 값이 poke()의 return 값이 된다.
    • 만약 InterruptedIOException이 발생하면 초기값 null이 return됨
      • 추후 Client에서 null은 서버가 응답을 하지 않는다고 해석
  • 실행 형식
    - java UDPPoke host port

  • 예 :
    - C:>java UDPPoke rama.poly.edu 13
    Tue May 30 11:42:07 2021

    • C:>java UDPPoke rama.poly.edu 19
      !"#$%&'()*+,
      ./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcde
import java.io.*;
import java.net.*;
public class UDPPoke {
	private int bufferSize; // in bytes
	private int timeout; // in milliseconds
	private InetAddress host;
	private int port;
	public UDPPoke(InetAddress host, int port, int bufferSize, int timeout) {
		this.bufferSize = bufferSize;
		this.host = host;
		if (port < 1 || port > 65535) {
			throw new IllegalArgumentException("Port out of range");
		}
		this.port = port;
		this.timeout = timeout;
	}
	public UDPPoke(InetAddress host, int port, int bufferSize) {
		this(host, port, bufferSize, 30000);
	}
	public UDPPoke(InetAddress host, int port) {
		this(host, port, 8192, 30000);
	}
	public byte[] poke() {
		try (DatagramSocket socket = new DatagramSocket(0)) {
			DatagramPacket outgoing = new DatagramPacket(new byte[1], 1, host, port);
			socket.connect(host, port);
			socket.setSoTimeout(timeout);
			socket.send(outgoing);
			DatagramPacket incoming = new DatagramPacket(new byte[bufferSize], bufferSize);
			// 다음 라인은 응답을 받을 때까지 중단됨
			socket.receive(incoming);
			int numBytes = incoming.getLength();
			byte[] response = new byte[numBytes];
			System.arraycopy(incoming.getData(), 0, response, 0, numBytes);
			return response;
		} catch (IOException ex) {
			return null;
		}
	}
	public static void main(String[] args) {
		InetAddress host;
		int port = 0;
		try {
			host = InetAddress.getByName(args[0]);
			port = Integer.parseInt(args[1]);
		} catch (RuntimeException | UnknownHostException ex) {
			System.out.println("Usage: java UDPPoke host port");
			return;
		}
		try {
			UDPPoke poker = new UDPPoke(host, port);
			byte[] response = poker.poke();
			if (response == null) {
				System.out.println("No response within allotted time");
				return;
			}
			String result = new String(response, "US-ASCII");
			System.out.println("Response = " + result);
		} catch (UnsupportedEncodingException ex) {
			// Really shouldn't happen
			ex.printStackTrace();
		}
	}
}

예제 UDP Time Client

  • 클라이언트가 받은 byte[]을 그대로 사용하려면 그냥 UDPPoke를 사용하면 된다. (UDPPoke의 main() 이용)
  • 그러나 Time 처럼 결과를 완전하게 해석하려면 poke()를 실행시키고 결과 바이트 배열을 변환하여 해석하는 별도의 main() 코드가 필요하다.
  • 타입 서버는 여러 컴퓨터의 시간 동기화에 쓰인다.
    - 클라이언트가 자신 컴퓨터 시간을 서버의 표준 시간에 동기화시킴
  • UDPPoke의 main()
    - UDPPoke 객체의 생성
    - host : 서버의 호스트명으로 InetAddress 객체를 만든다.
    - port : 정수형으로 변환
    • UDPPoke 객체의 poke()를 호출하여 결과 바이트 배열을 얻는다.
    • 서버가 리턴하는 4개의 원시 바이트를 java.util.Date() 객체로 변환
    • 변환 결과를 출력
import java.net.*;
import java.util.*;
public class UDPTimeClient {
	public final static int PORT = 37;
	public final static String DEFAULT_HOST = "localhost";
	public static void main(String[] args) {
		InetAddress host;
		try {
			if (args.length > 0) {
				host = InetAddress.getByName(args[0]);
			} else {
				host = InetAddress.getByName(DEFAULT_HOST);
			}
		} catch (RuntimeException | UnknownHostException ex) {
			System.out.println("Usage: java UDPTimeClient [host]");
			return;
		}
		UDPPoke poker = new UDPPoke(host, PORT);
		byte[] response = poker.poke();
		if (response == null) {
			System.out.println("No response within allotted time");
			return;
		} else if (response.length != 4) {
			System.out.println("Unrecognized response format");
			return;
		}
		// Time 프로토콜은 1900년 기준
		// Java의 Date 클래스는 1970년 기준
		// 다음 값은 이들 사이의 시간 차
		long differenceBetweenEpochs = 2208988800L;
		long secondsSince1900 = 0;
		for (int i = 0; i < 4; i++) {
			secondsSince1900 = (secondsSince1900 << 8) | (response[i] & 0x000000FF);
		}
		long secondsSince1970 = secondsSince1900 - differenceBetweenEpochs;
		long msSince1970 = secondsSince1970 * 1000;
		Date time = new Date(msSince1970);
		System.out.println(time);
	}
}

참고 : UDPTimeServer

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.util.Date;
public class UDPTimeServer2 {			
	public final static int PORT = 37;
	public static void main(String[] args) {		
		DatagramSocket dsock = null;	
		long differenceBetweenEpochs = 2208988800L;
		try{			
			System.out.println("UDP Time Server 클라이언트의 접속을 기다림 " + new Date());		
			dsock = new DatagramSocket(PORT);		
			String line = null;		
			while(true){		
				// 받기	
				byte[] buffer = new byte[1024];	
				DatagramPacket receivePacket = new DatagramPacket(buffer, buffer.length);	
				dsock.receive(receivePacket);	
				
		 Date now = new Date();
		 long msSince1970 = now.getTime();
		 long secondsSince1970 = msSince1970/1000;
		 long secondsSince1900 = secondsSince1970 
		 + differenceBetweenEpochs;
		 byte[] time = new byte[4];
		 time[0] 
		 = (byte) ((secondsSince1900 & 0x00000000FF000000L) >> 24);
		 time[1] 
		 = (byte) ((secondsSince1900 & 0x0000000000FF0000L) >> 16);
		 time[2] 
		 = (byte) ((secondsSince1900 & 0x000000000000FF00L) >> 8);
		 time[3] = (byte) (secondsSince1900 & 0x00000000000000FFL);				
				
				System.out.println(receivePacket.getAddress() + "으로 현재시간을 전송합니다.");	
				DatagramPacket sendPacket = new DatagramPacket(time, time.length, receivePacket.getAddress(), receivePacket.getPort());	
				dsock.send(sendPacket);	
			} // while		
		}catch(Exception ex){			
			System.out.println(ex);		
		}finally{			
			if(dsock != null)		
				dsock.close();	
		}			
	} // main				
} // class					

예제 : UDPServer

  • 재사용이 가능한 클라이언트처럼 UDP 서버도 재사용이 가능하도록 유연하게 구현할 수 있다.
    - UDP 서버들은 지정된 포트에서 데이터그램을 기다리고 있다가 다른 데이터그램으로 응답하는데 응답하는 데이터그램의 내용만 다르다.
  • 설계 :
    - UDP 서버를 UDPServer 라는 이름의 추상 클래스로 만들고 특수한 프로토콜을 구현하는 서버는 이 클래스의 하위 클래스로 만들기로 한다.
    • 추상 클래스 UDPServer에는 추상 메소드 void respond()를 둔다.
      • 하위 클래스인 특수한 서버에서 적절히 대응할 수 있게 함
        • 서버는 자신들이 리턴하는 데이터그램의 내용만 달리하여 전송한다
    • UDPServer 는 Runnable 클래스로 구성한다.
      • 복수의 스레드 객체를 병행 실행시키기 위함
        • run() 에 무한 루프를 두고 이 루프 속에서 클라이언트의 데이터그램을 수신하여 respond()에 넘겨준다.
  • 필드 :
    - private int bufferSize
    • protected DatagramSocket socket : 서브 클래스에서 사용 가능하도록 protected로 선언
  • 생성자 :
    - UDP 데이터그램을 수신할 버퍼 크기와 포트를 지정하여 DatagramSocket 객체를 연다.
  • run()
    - 수신 패킷을 생성하여 소켓의 receive(DatagramPacket)를 호출한다.
    • respond()를 호출하여 수신 패킷을 넘긴다.
import java.io.*;
import java.net.*;
public abstract class UDPServer implements Runnable {
	private final int bufferSize; // in bytes
	private final int port;
	private volatile boolean isShutDown = false;
	public UDPServer(int port, int bufferSize) {
		this.bufferSize = bufferSize;
		this.port = port;
	}
	public UDPServer(int port) {
		this(port, 8192);
	}
	public void run() {
		byte[] buffer = new byte[bufferSize];
		try (DatagramSocket socket = new DatagramSocket(port)) {
			socket.setSoTimeout(10000); // check every 10 seconds for shutdown
			while (true) {
				if (isShutDown)
					return;
				DatagramPacket incoming = new DatagramPacket(buffer, buffer.length);
				try {
					socket.receive(incoming);
					this.respond(socket, incoming);
				} catch (SocketTimeoutException ex) {
					shutDown();
				} catch (IOException ex) {
					ex.printStackTrace();
				}
			} // while
		} catch (SocketException ex) {
			System.err.println("Could not bind to port: " + port);
			ex.printStackTrace();
		}
	}
	public abstract void respond(DatagramSocket socket, DatagramPacket request) throws IOException;
	public void shutDown() {
		this.isShutDown = true;
	}
}

예제 : 고성능 버리기 서버

  • 버리기 서버를 TCP 대신 UDP로 구현했으므로 속도가 빨라 고성능이라고 명명함
  • UDPFastDiscardServer.java
    - 스레드로 구현하여 병렬로 고속 실행
  • 바로 전 예제의 추상클래스 UDP 서버를 상속하여 구현한 고성능 버리기 서버
    - 서브 클래스에서 거의 할 일이 없다.
    • 이 서버는 반응할 것이 없으므로 추상 메소드 response() 의 구현은 빈 몸체 {} 을 추가하는 것으로 끝낸다.
    • main()
      • 포트를 설정
        • 스레드를 발진시킴
import java.net.*;

public class UDPFastDiscardServer extends UDPServer {

  public final static int DEFAULT_PORT = 9;
  
  public UDPFastDiscardServer() {
    super(DEFAULT_PORT);
  }
  
  public static void main(String[] args) {
    UDPServer server = new UDPFastDiscardServer();
    Thread t = new Thread(server);
    t.start();
  }

  public void respond(DatagramSocket socket, DatagramPacket request) {
  }
}

연습 : UDP Daytime Server

  • UDP 서버를 상속하여 구현
    - 포트를 설정
    • 추상 메소드 response()의 구현에 시간 보내는 루틴 추가
    • main()
      • 스레드를 발진시킴
import java.net.*;
import java.io.*;
import java.util.*;
/*
* 이 서버는 추상클래스 UDPServer를 상속하여 구성
*/
public class UDPDaytimeServer3 extends UDPServer {
	public final static int DEFAULT_PORT = 13;
	public UDPDaytimeServer3() throws SocketException {
		super(DEFAULT_PORT);
	}
	public void respond(DatagramSocket ds, DatagramPacket packet) {
		try {
			Date now = new Date();
			String response = now.toString() + "\r\n";
			byte[] data = response.getBytes("ASCII");
			DatagramPacket outgoing = new DatagramPacket(data, data.length, packet.getAddress(), packet.getPort());
			System.out.println("Response will be sent to " + packet.getAddress() + "-" + packet.getPort());
			ds.send(outgoing);
		} catch (IOException e) {
			System.err.println(e);
		}
	}
	public static void main(String[] args) {
		try {
			UDPDaytimeServer3 server = new UDPDaytimeServer3();
			new Thread(server).start();
		} catch (SocketException e) {
			System.err.println(e);
		}
	}
}

UDP Echo

  • Echo 프로토콜을 UDP로 구현하기 어려운 점
    - I/O stream 을 지원하지 않는다.
    • 연결이라는 개념이 존재하지 않는다.
    • UDP 에서는 데이터를 보내고 받는 프로세스가 비동기적으로 이루어 져야 한다.
      • Thread 이용
        • UDPEchoClient, echoInputThread, echoOutputThread 등 여러 개의 클래스로 구성 해야 한다.

예제 : UDPEchoServer Class

import java.io.*;
import java.net.*;

public class UDPEchoServer extends UDPServer {

  public final static int DEFAULT_PORT = 7;

  public UDPEchoServer() {
    super(DEFAULT_PORT); 
  }

  @Override
  public void respond(DatagramSocket socket, DatagramPacket packet) throws IOException {
    DatagramPacket outgoing = new DatagramPacket(packet.getData(), packet.getLength(), packet.getAddress(), packet.getPort());
    socket.send(outgoing);
  }

  public static void main(String[] args) {
    UDPServer server = new UDPEchoServer();
    Thread t = new Thread(server);
    t.start();
  }
}

예제 : UDPEchoClient Class

  • UDPPoke 를 이용하여 Echo 클라이언트를 구현하기도 어렵다.
    - Echo 클라이언트는 여러 개의 Datagram 이 필요하기 때문
  • UDPEchoClient.java
    - 명령어 입력 라인에서 호스트 이름을 읽어 들인다.
    • 소켓 생성 후
    • ReceiveThread 클래스, SendThread 클래스를 실행시킨다.
    • UDPEchoClient, ReceiveThread, SendThread 는 다음을 공유
      • UDP socket
        • remote host
        • remote port
//
// Example 12-12
//
import java.net.*;

public class UDPEchoClient {

  public final static int PORT = 7;

  public static void main(String[] args) {

    String hostname = "localhost";
    if (args.length > 0) {
      hostname = args[0];
    }

    try {
      InetAddress ia = InetAddress.getByName(hostname);
      DatagramSocket socket = new DatagramSocket();
      SenderThread sender = new SenderThread(socket, ia, PORT);
      sender.start();
      Thread receiver = new ReceiverThread(socket);
      receiver.start();
    } catch (UnknownHostException ex) {
      System.err.println(ex);
    } catch (SocketException ex) {
      System.err.println(ex);
    }
  }
}

UDPEchoClient >> SenderThread :

import java.io.*;
import java.net.*;

class SenderThread extends Thread {

  private InetAddress server;
  private DatagramSocket socket;
  private int port;
  private volatile boolean stopped = false;
  
  SenderThread(DatagramSocket socket, InetAddress address, int port) {
    this.server = address;
    this.port = port;
    this.socket = socket;
    this.socket.connect(server, port);
  }
  
  public void halt() {
    this.stopped = true; 
  }

  public void run() {
    try {
      BufferedReader userInput 
          = new BufferedReader(new InputStreamReader(System.in));
      while (true) {
        if (stopped) return;
        String theLine = userInput.readLine();
        if (theLine.equals(".")) break;
        byte[] data = theLine.getBytes("UTF-8");
        DatagramPacket output 
            = new DatagramPacket(data, data.length, server, port);
        socket.send(output);
        Thread.yield();
      }
    } catch (IOException ex) {
      System.err.println(ex);
    }
  }
}

UDPEchoClient >> ReceiverThread :

import java.io.*;
import java.net.*;

class ReceiverThread extends Thread {

  private DatagramSocket socket;
  private volatile boolean stopped = false;

  ReceiverThread(DatagramSocket socket) {
    this.socket = socket;
  }

  public void halt() {
    this.stopped = true; 
  }

  @Override
  public void run() {
    byte[] buffer = new byte[65507];
    while (true) {
      if (stopped) return;
      DatagramPacket dp = new DatagramPacket(buffer, buffer.length);
      try {
        socket.receive(dp); // <<강조>> Timeout이 설정되지 않아 무작정 기다린다.
        String s = new String(dp.getData(), 0, dp.getLength(), "UTF-8");
        System.out.println(s);
        Thread.yield();
      } catch (IOException ex) {
        System.err.println(ex);
      } 
    }  
  }
}

참고 : 간단한 UDP Echo Client

  • DatagramSocket 객체 생성
    - DatagramSocket dsock = new DatagramSocket();

  • 사용자로부터 문자열을 입력 받기 위한 스트림
    - BufferedReader br = new BufferedReader(new InputStreamReader(System.in))

  • 전송 :
    - 서버에게 전달할 바이트 배열, 배열 길이, InetAddress 객체, 포트번호로 DatagramPacket 객체 생성

    • 소켓의 send() 로 전송
      • DatagramPacket sendPacket = new DatagramPacket(line.getBytes(), line.getBytes().length, inetAddr, port);
        • dsock.send(sendPacket);
  • 받기 :
    - 반송되는 DatagramPacket 객체를 읽어 들인다.
    - byte [] buffer = new byte[line.getBytes().length];
    - DatagramPacket receivePacket = new DatagramPacket(buffer, buffer.length);
    - dsock.receive(receivePacket);

  • 반송된 결과 출력 :
    - DatagramPacket 을 통해 전달된 바이트 배열을 문자열로 변환하여 화면에 출력
    - String msg = new String(receivePacket.getData(), 0, receivePacket.getData().length);
    - System.out.println("전송 받은 문자열 = " + msg);

// UDP Echo 클라이언트
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.UnknownHostException;


public class UDPSimpleEchoClient {

   public static void main(String[] args) {
      String ip = "localhost";
      int port = 7;
      InetAddress inetaddr = null;	
      try {	
         inetaddr = InetAddress.getByName(ip);
      } catch (UnknownHostException e) {	
         System.out.println("잘못된 도메인이나 ip입니다.");
         System.exit(1);
      }	
      DatagramSocket dsock = null;
      try{
         BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
         dsock = new DatagramSocket();
         String line = br.readLine() ;
         while( line != null){
            if(line.equals("quit")) break; // 사용자 입력의 끝은 quit로 표시
            // 보내기
            DatagramPacket sendPacket = new DatagramPacket(line.getBytes(), line.getBytes().length, inetaddr, port);
            dsock.send(sendPacket);
	 	// 받기
            byte[] buffer = new byte[line.getBytes().length];
            DatagramPacket receivePacket = new DatagramPacket(buffer, buffer.length);
            dsock.receive(receivePacket);

            // 서버로부터 되받은 결과 출력
            String msg = 
                      new String(receivePacket.getData(), 0, receivePacket.getData().length);
            System.out.println("Message from Echo server :" + msg);

            // 사용자의 다음 입력을 받아 들인다
            line = br.readLine();
         } // end while

         System.out.println("UDPEchoClient를 종료합니다.");
      } catch(Exception ex){
         System.out.println(ex);
      }finally {
         if(dsock != null) dsock.close();
       }
   } // end main()
} // end class

UDP 응용 프로그램 구성

UDP 응용 프로그램 설계

  • UDP는 Unreliable
  • UDP를 사용하는 응용 프로그램에서 보완책을 세워야 한다.

예제 : 패킷 손실에 대비하는 Client :

/*
* 패킷 상실로 인한 Timeout을 고려한 Echo 클라이언트
* echo 메시지를 보내고 받는다.
* 패킷이 정해진 시간 내 도착하지 않으면 5회 반복 시도한다.
*/
import java.net.*;
import java.io.*;
public class UDPEchoClientTimeout {
	private static final int MAXTRIES = 5; // 최대 재전송 시도 횟수
	public static void main(String[] args) throws IOException {
		String server = "localhost";
		InetAddress serverAddress = InetAddress.getByName(server);
		byte[] message = "This is message from client.".getBytes();
		int serverPort = 7;
		DatagramSocket socket = new DatagramSocket();
		socket.setSoTimeout(3000); // <<강조>> 최대 대기시간 (milliseconds)
		// Sending packet
		DatagramPacket sendPacket = new DatagramPacket(message, message.length, serverAddress, serverPort);
		// Receiving packet
		DatagramPacket receivePacket = new DatagramPacket(new byte[message.length], message.length);
		int tries = 0; // 패킷 상실 대비 시도 회수 저장
		boolean receiveOK = false;
		do {
			socket.send(sendPacket); // 에코 문자열을 보낸다
			try {
				socket.receive(receivePacket); // 에코 응답 수신 시도
				if (!receivePacket.getAddress().equals(serverAddress)) // 응답 출처 검사
					throw new IOException("Received packet from an unknown source");
				receiveOK = true; // <<강조>> 또는 SocketTimeoutException
			} catch (InterruptedIOException e) { // 시간 내 받지 못함
				tries += 1;
				System.out.println("Timed out, " + (MAXTRIES - tries) + " time try...");
			}
		} while ((!receiveOK) && (tries < MAXTRIES));
		if (receiveOK)
			System.out.println("Message received from server: " + new String(receivePacket.getData()));
		else
			System.out.println("No response -- giving up.");
		socket.close();
	}
}

예제 : 매우 에러율이 높은 UDP Echo Server

  • 일부러 에러율을 높였다.
//
// 10%의 응답을 의도적으로 폐기
//
import java.net.*;
import java.io.*;
public class UDPSimpleEchoServer2{
	protected DatagramSocket socket;
	public UDPSimpleEchoServer2 (int port) throws IOException {
		socket = new DatagramSocket (port);
	}
	public void execute () throws IOException {
		while (true) {
			DatagramPacket packet = receive();
			System.out.print("Received message: ");
			System.out.write(packet.getData());
			System.out.println();
			if (Math.random () < .9)
				sendEcho (packet.getAddress (), packet.getPort (), packet.getData (), packet.getLength ());
			else
				System.out.println ("discarded");
		} 
	}
	protected DatagramPacket receive() throws IOException {
		byte buffer[] = new byte[65535];
		DatagramPacket packet = new DatagramPacket (buffer, buffer.length);
		socket.receive (packet);
		return packet;
	}
	protected void sendEcho(InetAddress address, int port, byte data[], int length) throws IOException {
		DatagramPacket packet = new DatagramPacket (data, length, address, port);
		socket.send (packet);
		System.out.print("Sent message: ");
		System.out.write(data);
		System.out.println();
	}
	public static void main (String args[]) throws IOException {
		UDPSimpleEchoServer2 echo = new UDPSimpleEchoServer2 (7);
		System.out.println("Echo Server running on port 7 ......");
		echo.execute ();
	} 
}
profile
내가 만들어낸 것들로 세계에 많은 가치를 창출해내고 싶어요.

0개의 댓글