KGW2027 2022. 12. 8. 23:02
728x90
반응형

네트워크에는 노드와 호스트가 있다.

노드(Node)는 네트워크 상에 존재하는 장치로 컴퓨터, 라우터, 스위치 등을 의미한다.

호스트(Host)는 이 노드 중 컴퓨터를 의미한다.

 

IP Address는 이런 노드와 호스트를 가리키는 주소다.

전통적인 IP Address는 IPv4 형태로, 4바이트로 구성된 주소다.

우리에게 익숙한 255.255.255.255 형태의 IP가 IPv4다.

하지만 계속 노드와 호스트의 수가 늘어나면서 IPv4의 256^4개 로는 부족해졌다.

 

여기서 새롭게 도입된것이 IPv6 형태로, 16바이트로 구성된 주소다.

IPv6는 2바이트씩 ':(콜론)'으로 나눠서 표기한다.

0000:0000:0000:0000:0000:0000:0000:0000 과 같은 형태가 IPv6이다.

일반적으로 IPv6의 마지막 4바이트는 IPv4 주소를 작성하고,

16바이트라는 긴 길이를 가지기 때문에 0000으로된 바이트그룹들은 생략할 수 있다.

예를 들어, 1234:5678:9000:0000:0000:0000:1234:5678 이라는 IPv6주소는

1234:5678:9000::1234:5678 로 생략할 수 있다.

 

우리가 들어가는 모든 사이트들은 이런 IP주소들로 표현할 수 있다.

하지만, 인간에게 이런 방식의 표현은 기억하기가 너무 어렵기 때문에

인간이 이해하기 쉬운 방식으로 변환하는데 이를 DNS라고 한다. ( Domain Name Server )

예를 들어, naver.com, google.com 과 같은 것들이 DNS다.

네이버의 IP주소는 223.130.195.200 이고, 이것을 주소창에 입력하면 네이버에 들어가진다.

그러나, 우리가 보기에 223.130.195.200 naver.com 중에 naver.com이 더 이해하기 쉬울것이다.

이러한 점을 위해 DNS를 사용한다.

 


기본적인 용어 설명은 마치고, Java에서 네트워크를 다루기 위한 기초에 대해 알아보자.

앞으로 살펴볼 클래스들은 다음과 같다.

- InetAddress : IP Address를 얻기 위한 클래스

- URI : Uniform Resource Identifier, 자원을 식별하기 위한 클래스

- URL : Uniform Resource Locator, 자원을 식별 + 검색하기 위한 클래스

- Socket : TCP 통신에서 Client역할을 수행하는 클래스

- ServerSocket : TCP 통신에서 Server역할을 수행하는 클래스

- DatagramSocket : UDP 통신에서 송수신을 수행하는 클래스

- DatagramPacket : UDP 패킷을 나타내는 클래스

- NetworkInterface : NIC를 얻기 위해 사용하는 클래스

- MulticastSocket : Multicast를 위해 사용하는 클래스

 

1. InetAddress

 네트워크상에서 통신을 수행하려면 일단 상대방의 IP를 알아야한다.

TCP통신을 하던, UDP통신을 하던 IP가 없으면 1비트조차 보낼 수 없기 때문에...

여기서 IP를 얻어오기 위해 사용하는 클래스가 InetAddress다.

 

 InetAddress 클래스를 이용하면 위에서 설명했던 DNS를 이용해 IP를 가져올 수도 있고, 그냥 IP를 생으로 박아넣을 수도 있다.

여기서 InetAddress의 특이한점이 있는데, 생성자를 사용하지 않고 static method를 사용한다는 점이다.

예를 들어,

new InetAddress("www.naver.com"); // 이거 아님

와 같이 사용하는게 아니라.

InetAddress naver = InetAddress.getByName("www.naver.com"); // 실제 사용

와 같은 방식으로 사용한다는 것이다.

그 이유로는 DNS를 통해 IP를 찾는 과정인 'DNS Lookup'이 무거운 작업이기 때문에,

이전 검색의 결과를 저장해두기 위해 하나의 instance로 작동한다.. 고 보면 될 것 같다.

예를 들어, 처음 'getByName("www.naver.com")'을 하면 DNS Lookup을 해서 naver의 주소를 가져오지만,

그 다음 똑같은 getByName()을 하면, HashMap.get("www.naver.com") 같은 방식으로 바로 가져올 수 있다는 것이다.

(실제로 HashMap을 사용하는지는 안뜯어봐서 잘 모르겠다. 암튼 이런 방식이라는 뜻이다.)

 

InetAddress 객체에서 자주 사용할 메소드는 다음과 같다. (전체 메소드는 Javadocs 참조)

 - InetAddress.getLocalHost() : 로컬의 InetAddress를 반환한다. 만약 인터넷 연결이 없다면 localhost가 반환된다.

 - getHostName() : InetAddress 객체의 리모트(Host)의 이름을 반환한다. [ ex: naver.com ]

 - getHostAddress() : 리모트(Host)의 IP값을 반환한다. [ ex: 223.130.195.200 ]

 - isReachable() : 리모트로 패킷을 전송할 수 있는 지의 여부를 반환한다.

 - toString() : 'getHostName()/getHostAddress()' 형태의 문자열을 반환한다.

 

InetAddress에 대해서는 간단히 알아봤으니, 이제 웹에 직접 연결해보자.

 


2. URI, URL

 이번에 다룰 것은 URI, URL이다. 위에 적혀있긴 하지만, 약어풀이부터 해보자면

URIUniform Resource Identifier, 자원(Resource) 식별(Identifier)을 위한 문자고

URLUniform Resource Locator, 자원(Resource) 검색(Locator)을 위한 문자다.

다만, URL은 URI의 subtree이므로 식별도 가능하다.

 

URI의 구조는 다음과 같다. ( [] 로 감싸진 곳은 생략 가능이라는 의미이다. )

scheme://[id:password]@authority[:port]/[path]

 - scheme : http, ftp, file, mailto, urn, telnet, 자바에서는 rmi 같은 것을 작성하는 곳이다.

 - id, password : 접속할 때 로그인이 필요하면 여기에 입력한다.

 - authority : 기관명으로 naver.com, google.com 같은 부분을 의미한다.

 - port : 연결할 포트이다. HTTP 통신의 기본은 80, FTP 통신의 기본은 20(데이터), 21(제어)이다.

 - path : authority로 접속한 곳에서 부터 접근할 경로이다.

 

 URL은 이러한 URI 구조에서 http, ftp 통신을 위한 확장... 같은 것이다.

URL의 구조는 다음과 같고, 일반적으로 GET Request에 사용되는 주소라고 생각하면 편하다.

[http,ftp]://[id:password]@authority[:port]/[path][?query1&query2...][#fragment]

 - query : GET Request의 parameters

 - fragment : html의 h[0-9] 태그의 id로 스크롤이 자동 set되는 것으로, 위키에서 버튼눌렀을 때 이동하는 방식이다.

 fragment에 대해 잘모르겠다면, 나무위키나 위키피디아 들어가서 문서 목차의 버튼을 눌러보자.

 

path에 대해 추가 설명을 해보자면, 우리가 사이트에 들어갔을 때, ~.com/ 뒤에 있는 모든 부분이 path다.

예를 들어, 이 블로그에서의 내 글을 보면 'kgwdiary.tistory.com/143'과 같은 식으로 되있는데, 이 143이 path다.

나에게 할당된 서버에서 143이라는 파일에 그 글의 내용이 담겨있다... 라는 내용인데,

이에 대해 좀 더 직관적으로 알아보려면 file scheme를 이용해 자신의 컴퓨터에 접근해보자.

 

브라우저 주소창에 file://localhost/C:/ 라고 입력해보자.

그러면 본인 컴퓨터 C드라이브의 폴더구조가 브라우저에 표시될 것이다.

여기서 authority는 localhost/ 이고 path는 C:/가 되는것이다.

대충 아무 경로나 들어가보면서 아무 텍스트 파일이나 열어보면 그 텍스트 파일 내용이 브라우저에 표시가 될텐데,

지금 이 블로그와 같은 웹사이트들도 똑같은 방식이다.

다만 내 컴퓨터가 아닌 tistory.com에 있는 파일을 탐색해서 연다는 것만 다를 뿐이다.

 

이 URI, URL을 Java에서 작성해보면 다음과 같다.

new URI("https://www.naver.com/");
new URL("https://www.naver.com/");

사실 아무 차이가 없다.

막 프로토콜이랑 주소랑 분리하고 이런식으로 하면 좀 달라질 수도 있는데,

어차피 간단한 통신에 그정도까지 복잡하게 할 필요가 없으므로...

 

그리고 URI나 URL주소를 보면 : / . 과 같은 특별한 역할을 하는 특수문자들이 있다.

만약 이런 특수문자들이 path나 query에 오게 된다면 이게 path인지 query인지 아니면 다른 역할을 하는지 구분하기 어렵다.

그렇기 때문에, URI나 URL로서 동작하는 동안에는 이런 문자로서의 특수문자들은 encode를 해놓고,

동작이 끝나고 다시 원래 문자로 되돌리기 위해 decode를 하게 되는데,

이러한 역할을 하는 클래스가 URLEncoder, URLDecoder이다.

 

사용 예시를 들어보자면

URLEncoder.encode("The Encode T@st")The+Encode+T%40st 를 반환한다.

반대로,

URLDecoder.decode("The+Encode+T%40st")The Encode T@st 를 반환한다.

Encode로 변환되는 특수문자들은 대표적으로 아래와 같다.

/ & ? @ # ; $ + = % .

하지만 상황에 따라 모두 인코드를 하기도 하는데, 이러한 경우 변환하는 규칙에 대해서는 링크를 참고하시라..

 

URL.openStream()이나 URL.openConnect()를 이용해 http통신을 할수있지만..

TCP 통신을 위해 소켓에 대해 알아보자.

 


3. TCP ( Socket, ServerSocket )

 

 드디어 통신을 하기 위한 Socket과 ServerSocket이다. 각자의 역할을 간단하게 소개해보자면,

Socket은 클라이언트의 역할을 수행하는 클래스로, 서버의 IP와 port를 필요로 한다.

ServerSocket은 서버 역할을 수행하는 클래스로, 서버를 열기 위한 port를 필요로 한다.

 

먼저 Socket에 대해 알아보자. Socket 객체 생성법은 다음과 같다.

new Socket(); // 비-연결
new Socket("IP", port); // 연결(직접입력)
new Socket(InetAddress, port); // 연결 (InetAddress 사용)

비-연결 소켓의 경우, socket.connect(SocketAddress[, timeout])를 이용해 수동으로 연결할 수 있다.

 ※ SocketAddress는 socket.getLocalSocketAddress()나 socket.getRemoteSocketAddress()를 통해 기존 소켓에서 얻거나, InetAddress를 넣으면 된다.

 ※ timeout은 socketAddress로 TCP Handshake를 보냈을 때, 응답을 기다릴 시간이다. 단위는 milisec

 

socket.getInputStream()socket.getOutputStream() 함수를 이용해 Stream을 사용한 데이터 송수신이 가능하다.

통신이 끝나면 socket.close()로 소켓을 종료하거나, socket.shutdownInput(), socket.shutdownOutput()을 이용해 입력, 출력을 따로따로 닫을 수 있는데, 입출력을 모두 닫더라도 소켓은 자동으로 닫히지 않고, 소켓이 닫히지 않으면 포트 점유가 풀리지 않기 때문에, 마지막에 socket.close()는 반드시 넣어야 한다.

(당연하지만 프로그램이 종료되면 소켓도 자동으로 닫힌다.)

 

위에서 비 연결 소켓을 만든 후, socket.connect를 이용해 연결과정에서의 타임아웃을 구하는 방법을 봤다면,

통신중에 입력을 기다리는 시간에 대한 타임아웃을 정하는 함수도 존재하는데, 그것이 socket.setSoTimeout(milisec)이다.

이를 통해 소켓에 milisec동안 입력이 안주어지면 타임아웃이 일어나도록 할 수 있다.

 

Socket에 대해 간단히 알아봤으니, ServerSocket에 대해서도 알아보자.

ServerSocket은 특정 포트에서 TCP 연결을 대기하다가, TCP 연결이 오면 그 연결에 매칭하는 Socket 객체를 생성해서 통신하게 된다. 그렇기 때문에, ServerSocket 생성에는 IP 없이 포트만 있으면 된다.

 

서버 소켓이 연결을 대기하기 위해서는 serverSocket.accept()를 사용하는데, 함수가 입력됬을 때 부터 TCP 연결이 도착할 때 까지 연결을 대기하고, 연결 요청이 오면 Socket을 생성하고 함수가 종료된다.

그렇기 때문에, 계속해서 연결을 받기 위해서 while(true) 블록 안에 serverSocket.accept()를 넣는다.

ServerSocket serverSocket = new ServerSocket(port);
while(true) {
    Socket client = serverSocket.accept();
    // client와의 통신
}

통신 요청이 오면 한 번 짧게 통신과정을 끝내고, 바로 다음 통신 요청을 대기할 수 있다면 이런 방식으로 충분하겠지만, 만약 실제 웹사이트 환경처럼 동시에 여러 요청을 수행하려면 어떻게 해야할까?
보통 Client와의 통신을 하기 위한 클래스를 하나 만들어서 그 클래스에 client Socket을 입력하고, 통신을 위한 스레드를 하나 배정해주는 방식을 사용한다.

 

스레드를 배정하는 방식도 두 개가 있는데,

 하나는 Native하게 Thread를 상속하게 해서 Thread.start()를 사용하는 방식

 두번째는 Runnable Interface를 상속하게 해서 ExecutorService를 사용하는 방식이 있다.

Thread를 생으로 사용하면 스레드의 생명주기를 직접 관리해야하는 불편함이 있지만,

ExecutorService.newFixedPool인가? 뭐 그런 함수를 사용하면 ExecutorService가 대신 스레드들을 관리해주므로 편리하다.

// 1. Thread 이용
class Client extends Thread {
    Socket clientSocket;
    public Client(Socket sock) {
        this.clientSocket = sock;
    }
    
    @Override
    public void run() {
        // 통신
    }
}

...
Socket clientSocket = serverSocket.accept();
new Client(clientSocket).start();


// 2. ExecutorService 이용
class Client implements Runnable {
// 대충 생성자를 통해 clientSocket을 전달받는 코드
    @Override
    public void run() {
        // 통신
    }
}

...
ExecutorService executor = Executors.newFixedThreadPool(10);
...
Socket clientSocket = serverSocket.accept();
executor.submit(new Client(clientSocket));

 

소켓 옵션이라는 것도 있다. 위에서 설명한 setSoTimeout같은 것들이 소켓 옵션인데 이에 대해 알아보자.

TCP_NODELAY : 버퍼를 사용하지 않고, 데이터를 바로바로 전송한다.
 - socket.setTcpNoDelay(boolean)

SO_LINGER : Socket에 데이터가 저장되어있을 최대 시간을 정한다. 이를 넘어가면 바로 전송한다. 
 만약 최대시간이 0이라면, socket.close()됬을 때, 전송되지 않은 데이터는 버려진다.
 - socket.setSoLinger(boolean, time)
 
SO_TIMEOUT : 위에서 설명한바와 같이, read()를 통해 읽을 때 최대로 기다릴 시간이다.
 - socket.setSoTimeout(time)
 
SO_RCVBUF : TCP 스택 버퍼 사이즈를 조절하는 옵션이다.
 - socket.setReceiveBufferSize(size)
 
SO_KEEPALIVE : 서버의 정상동작을 확인하기 위해 주기적으로 데이터를 전송하고, 
 720초간 응답이 없으면 close한다.
 - socket.setSoKeepAlive(boolean)
 
OOBINLINE : OOB : Out of band, 긴급 데이터 수신을 위한 옵션이다.
 - socket.sendUrgentData(int data) // 하위 1바이트의 데이터를 긴급하게 전송한다.
 - socket.setOOBINLINE(boolean)
 
SO_REUSEADDR : 소켓이 close될 때, 다른 소켓이 바로 그 포트에 bind하지 못하게 한다.
 - socket.setReuseAddress(boolean)
 
IP_TOS : 전송하는 데이터 타임에 대한 트래픽 정책을 정한다.
 - socket.setTrafficClass(int trafficClass)
    IPTOS_LOWCOST (0x02)
    IPTOS_RELIABILITY (0x04)
    IPTOS_THROUGHPUT (0x08)
    IPTOS_LOWDELAY (0x10)

 


4. UDP ( DatagramSocket, DatagramPacket )

 

 UDP 통신은 잘 알다시피 연결이 없고 Reliable하지 않은 통신 방식이다.

소켓은 클라이언트, 서버와 관계 없이 new DatagramSocket(port)를 사용해 소켓을 생성하고,

DatagramPacket에 전송할 데이터의 byteArray와 목적지 IP, Port를 입력한 후, 소켓을 통해 전송한다.

이 때, 데이터를 전송할 때는 datagramSocket.send(), 수신할 때는 datagramSocket.receive()를 사용한다.

일반적으로 패킷이 전송과정에 오류가 발생했는지를 알려면 checksum 비트를 이용해야하는데,

Java에서는 이 checksum을 기본적으로 제공하지 않으므로, 직접 구현해야한다.

 

[데이터를 전송하는 경우]

String stringData = "전송을 위한 문자열 데이터";
DatagramSocket udp_socket = new DatagramSocket(port);

// 패킷 생성 - 데이터 입력
byte[] byteData = stringData.getBytes();
DatagramPacket udp_packet = new DatagramPacket(byteData, byteData.length);
// 패킷 생성 - 목적지 설정
udp_packet.setAddress(InetAddress);
udp_packet.setPort(port);

// UDP 전송
udp_socket.send(udp_packet);

 

[데이터를 수신하는 경우]

// UDP 소켓 생성
DatagramSocket udp_receiver = new DatagramSocket(port);

// 데이터를 전달받을 패킷 생성
byte[] receive_data = new byte[1024];
DatagramPacket udp_packet = new DatagramPacket(receive_data, receive_data.length);

// 데이터의 수신을 기다릴 최대 시간 설정 ( 10초 )
udp_receiver.setSoTimeout(10 * 1000);

// 데이터 수신 대기 ( ServerSocket.accept()와 비슷함 )
udp_receiver.receive(udp_packet);

// 만약 데이터가 수신되면 udp_packet에 정보가 담긴다.

 

TCP의 Socket과 달리 DatagramSocket은 Stream이 아닌 byteArray 형태로 데이터를 통신하므로,

ByteArrayInputStream과 ByteArrayOutputStream을 이용하면 좋다.

 


5. Multicast ( MulticastSocket )

 

 멀티캐스트는 1:N 통신에서 사용하는 기법이다.

먼저 1:1 통신의 경우는 Unicast라고 한다.

만약 N명의 사람들이 동시에 채팅을 하는 어플리케이션이 있다고 할 때, 이 N명의 사람들이 모두 Unicast로 연결되어 있다면, 한 사람이 채팅을 보낼 때 마다 N-1명에게 패킷을 직접 보내야 할 것이다.

이것은 꽤나 큰 부담이 될 수 있으므로, 이 역할을 라우터에게 맡기는 방식이 Multicast이다.

나는 채팅을 라우터의 Multicast로 보내고, Multicast는 같은 서브넷에 있는 N-1명에게 모두 채팅을 전달하는 방식이다.

 

이 때, Multicast의 주소는 IPv4 형태에서 0.0.0.0/3 이다.

이 IP 대역에서 Multicast 채널을 만들고, 사람들이 그 채널에 들어오면 Multicast 데이터를 송수신할 수 있는 것이다.

이 때, Multicast 채널에 속하지 않고도 그 채널에 데이터를 송신하는 것이 가능하다. (수신만 불가능)

 

또한, 멀티캐스트는 서브넷단위로 데이터가 전송되므로, 전송되는 범위를 지정하기 위해 TTL이라는 값을 사용한다.

Subnet과 Subnet 사이를 연결하는 Router를 몇번 지나갈 것인가.. 라는 값이 TTL인데,

TTL의 값과 그 범위에 대해서는 아래 그림을 참조 하면 아마 좀 더 이해하기 쉬울 것이라 생각한다.

http://what-when-how.com/voip/controling-scope-in-multicast-applications-voip/

 

멀티캐스트 데이터를 전송하는 방법은 다음과 같다.

InetSocketAddress multicast_group = new InetSocketAddress(ip, port);
NetworkInterface nic = NetworkInterface.getByName("lo"); // loopback nic

// 멀티캐스트 소켓을 만든다.
MulticastSocket multicast_socket = new MulticastSocket();

// 그룹에 가입한다.
multicast_socket.join(multicast_group, nic);

// 데이터 패킷을 만든다.
DatagramPacket udp_packet = new DatagramPacket(data, data.length, multicast_group);

// 데이터를 전송한다.
multicast_socket.send(udp_packet);

 

728x90
반응형