본문 바로가기

JAVA/네트워크 프로그래밍

[JAVA] 소켓 옵션 설정하기(Client)

소켓 옵션은 자바 Socket 클래스 내부의 네이티브 소켓이 데이터를 보내거나 받는 방법을 지정한다.


자바 클라이언트 소켓에 대한 9가지 옵션


▶ TCP_NODELAY

▶ SO_BINDADDR

▶ SO_TIMEOUT

▶ SO_LINGER

▶ SO_SNDBUF

▶ SO_RCVBUF

▶ SO_KEEPALIVE

▶ OOBINLINE

▶ IP_TOS



TCP_NODELAY


public void setTcpNoDelay(boolean on) throws SocketException

public boolean getTcpNoDelay() throws SocketException


TCP_NODELAY의 설정을 true로 하면 패킷의 크기에 상관없이 가능한 한 빨리 패킷을 전송한다. 


일반적으로 작은 패킷은 전송하기 전에 큰 패킷 하나로 합쳐지고, 다른 패킷을 보내기 전에 로컬 호스트는 원격 호스트로부터 ACK 신호를 기다린다. 이것이 바로 네이글 알고리즘 (Nagle's algorithm)이다. 


네이글 알고리즘의 문제는 원격에서 ACK 신호를 빨리 보내오지 않을 경우에 작은 다위의 데이터를 지속적으로 보내야 하는 애플리케이션의 경우 느려진다는 점에 있다.


특히 서버에서 실시간으로 클라이언트 측의 마우스 움직임을 추적해야 하는 게임이나 네트워크 애플리케이션과 같은 GUI프로그램에서 더욱 문제가 된다.


실제로 느린 네트워크에서는 지속적인 버퍼링으로 인해 심지어 단순한 타이핑마저 느려질 수 있다. 이 때 TCP_NODELAY옵션을 true로 설정하면 이러한 버퍼링 구조를 사용하지 않으며 모든 패킷이 준비되는 즉시 전송된다.


setTcpNoDelay(true) 는 소켓의 버퍼링 옵션을 끈다. setTcpNoDelay(false)은 버퍼링 옵션을 다시 켠다.

getTcpNoDelay() 는 버퍼링 옵션이 켜져 있는 경우 false를 반환하고, 꺼져 있으면 true를 반환한다.


if(!s.getTcpNoDelay()) s.setTcpNoDelay(true);


위의 두 메소드는 내부 소켓 구현에서 TCP_NODELAY 옵션을 지원하지 않을 경우 SocketException 예외를 발생시키지 않도록 선언되어 있다.



SO_LINGER


public void setSoLinger(boolean on, int seconds) throws SocketException

public int getSoLinger() throws SocketException


이 옵션은 소켓이 닫힐 때, 전송되지 않은 데이터그램을 어떻게 처리할지 결정한다.


기본적으로 close() 메소드는 호출 즉시 반환된다. 그러나 시스템은 내부적으로 아직 전송되지 않은 데이터를 계속해서 전송한다. 링거(linger) 시간이 0으로 설정될 경우, 소켓이 닫힐 때 아직 전송되지 않은 패킷은 버려진다.


SO_LINGER 옵션이 켜져 있고 링거 타임이 정수값인 경우, close() 메소드는 지정된 시간 동안 데이터를 보내고 응답을 받기 위해 블록(block)된다. 그리고 지정된 시간이 초과될 경우, 소켓은 닫히고 아직 남아 있는 데이터는 보내지 않으며 응답을 기다리지 않는다.


이 두 메소드는 내부의 소켓 구현이 SO_LINGER 옵션을 지원하지 않을 경우 Socket Exception 예외를 발생시킨다.


setSoLinger() 메소드는 시간을 음수로 지정할 경우 IllegalArgumentException을 발생시킨다.


getSoLinger() 메소드는 옵션이 설정되지 않은 경우 -1을 반환하거나, 남아 있는 데이터를 전송하는 데 필요한 시간을 반환한다.


지연시간이 설정되지 않은 경우 소켓 s에 대해 링거 타임아웃을 4분으로 설정하는 방법


if(s.getTcpSoLinger() == -1) s.setSoLinger(true, 240);


최대 링거타임은 65535초이다.



SO_TIMEOUT


public void setSoTimeout(int milliseconds) throws SocketException

public int getSoTimeout() throws SocketException


일반적으로 소켓에서 데이터를 읽으려고 할 때 read() 호출은 충분한 바이트를 읽을 때 까지 블록된다.


SO_TIMEOUT을 설정하면 read() 메소드의 호출이 지정된 밀리초 이상 블록되지 않는다.


타임아웃이 발생되면 InterruptedIOException이 발생하므로, 이를 처리할 수 있도록 해야한다.


하지만 예외가 발생하더라도 연결은 유지되며 read() 메소드는 호출이 실패해도 다시 호출할 수 있으며, 이전 실패와 상관없이 호출이 성공할 수 있다.


타임아웃은 밀리초로 설정되며 default 값은 0이며, 무한 대기를 의미한다.


소켓 s의 타임아웃 값이 설정되지 않은 경우 10만 밀리초로 설정한다


if (s.getSoTimeout() == 0) s.setSoTimeout(100000);


이 두 메소드는 내부 구현 소켓이 SO_TIMEOUT 옵션을 지원하지 않는 경우 SocketException이 발생하며, 타임아웃 설정 시간이 음수일 경우  IllegalArgumentException을 발생시킨다.



SO_RCVBUF 그리고 SO_SNDBUF


TCP는 네트워크 성능을 향상시키기 위해 버퍼를 사용한다. 


속도가 빠른 경우 큰 버퍼, 느린 경우에는 작은 버퍼가 성능에 더 유리하다.


FTP나 HTTP처럼 큰 데이터를 지속적으로 보낼 경우 큰 버퍼가 유리하며


텔넷이나 게임에서는 지속적 상호작용이 일어나기 때문에 작은 버퍼가 유리하다.


BSD 4.2는 크기가 작고 네트워크가 느리던 시대에 설계된 운영체제로 2킬로바이트의 버퍼를 사용한다.


윈도우 XP는 17,520바이트의 버퍼를 사용하며, 요즘 운영체제에서는 128킬로바이트가 일반적인 기본값이다.



달성 가능한 최대 대역폭은 버퍼크기를 대기 시간(latency)으로 나눈 값과 같다.


예를 들어, 윈도우 XP에서 두 호스트 사이의 지연 시간이 500ms라고 가정해 보자. 


대역폭은,



가 되며,


이 값이 곧 네트워크 속도에 상관없이 어떤 소켓의 최대 속도가 된다.


이 정도 값이라면 모뎀에서는 빠른속도,  ISDN을 사용할 경우 나쁘지 않은 속도이다.

그러나, DSL회선을 사용하는 경우에는 많이 부족하다.



대기시간을 줄일 경우 속도를 향상 시킬 수 있으나, 대기 시간은 우리가 제어할 수 있는 요소가 아니다.


반면에 버퍼 크기는 제어할 수 있다. 예를 들어 버퍼크기를 17,520에서 128킬로바이트로 늘리면, 최대 대역폭은 초당 2메가비트로 증가한다.


다시 두배인 256킬로바이트로 늘리면 최대 대역폭도 4메가비트로 증가한다.


이 때, 버퍼를 너무 높게 설정할 경우 프로그램이 네트워크가 처리할 수 있는 것 보다 더 빠르게 데이터를 전송하려고 하게 되며, 이런 경우 네트워크가 혼잡해져 패킷이 손실되며 결국 성능 저하로 이어진다.


따라서 최대 대역폭이 필요한 경우 버퍼크기를 연결의 대기 시간과 맞출 필요가 있으며, 결국 이 값은 네트워크의 최대 대역폭보다 조금 작은 값이 된다.


(지연시간을 측정하는 방법으로는 ping을 사용하는 방법, 그리고 프로그램내에서 InetAddress.isReachable()를 호출하여 시간을 측정하는 방법이 있다.)



SO_RCVBUF 옵션은 네트워크 입력에 사용된 수신 버퍼의 크기를 제안한다.

SO_SNDBUF 옵션은 네트워크의 출력에 사용된 송신 버퍼의 크기를 제안한다.


public void setReceiveBufferSize(int size) throws SocketException, IllegalArgumentException

public int getReceiveBufferSize() throws SocketException

public void setSendBufferSize(int size) throws SocketException, IllegalArgumentException

public int getSendBufferSize() throws SocketException


메소드 선언에는 송신 버퍼와 수신 버퍼를 각각 설정할 수 있는 것 처럼 보이나, 일반적으로 버퍼는 이 두 값보다 작게 설정된다.


예를 들어, 송신 버퍼의 크기를 64K로 설정하고 수신 버퍼의 크기를 128K로 설정하면, 송신과 수신 모두의 64K의 버퍼 크기를 가지게 될 것이다. 그리고 자바는 설정된 수신 버퍼의 크기를 128K라고 알려주지만, 내부 TCP스택은 실제로 64K를 사용할 것이다.



setReceiveBufferSize() / setSendBufferSize 메소드는 이 소켓에 대한 출력을 버퍼링하기 위해 사용할 바이트 수를 제안한다. 그러나 내부 구현은 자유롭게 이 제안을 무시하거나 조정할 수 있다. 특히 유닉스와 리눅스 시스템은 종종 최대 버퍼 크기를 명시하고 있는데, 일반적으로 64K또는 256K이고, 더 큰 값의 소켓은 허용되지 않는다. 더 큰 값을 설정하려고 할 경우 자바는 버퍼크기를 가능한 한 최대값으로 설정한다. 리눅스의 내부 구현에서 드물게 요청된 크기의 두 배를 설정하는 경우도 있다. 예를 들어 64K 버퍼를 요청한다면, 대신 128K버퍼를 얻을수도 있다.


이 두 메소드는 전달된 매개변수가 0이하일 경우 IllegalArgumentException을 발생시킨다.



프로그램이 이용 가능한 대역폭을 충분히 활용하지 못하는 경우에는 버퍼의 크기를 늘려야한다.


반면에 네트워크가 혼잡하거나 패킷 손실이 발생한다면, 버퍼 크기를 줄여야한다.


그러나 네트워크의 한 쪽 방향으로 부하가 쏠리는 경우를 제외한 대부분의 상황에서는 기본값을 사용하는 것이 좋다.



SO_KEEPALIVE


이 옵션이 설정된 경우, 클라이언트는 종종 유휴 연결(idle connection)을 통해 데이터 패킷을 보내 서버와의 연결을 유지한다. 서버가 응답이 없을 경우 클라이언트는 응답 받을 때 까지 계속해서 시도하며, 12분이 지나도 응답이 없을 경우 소켓을 닫는다.


이 옵션을 설정하지 않을 경우 네트워크 통신이 활발하지 않은 클라이언트는 서버가 장애로 종료된 상황에도 아무런 알림을 받지 못하며 계속 실행되게 된다.


public void setKeepAlive(boolean on) throws SocketException

public boolean getKeepAlive() throws SocketException


SO_KEEPALIVE의 default 상태는 false이다. 켜져있을 경우 끄기 위한 코드는 다음과 같다.


if(s.getKeepAlive()) s.setKeepAlive(false);



OOBINLINE


TCP는 단일 바이트를 긴급하게 전송하는 기능을 제공한다. 이 데이터는 전송 즉시 보내진다.


수신자는 긴급 데이터를 수신 했을 때 알림을 받으며, 이미 도착한 다른 데이터를 처리하기 전에 긴급 데이터를 먼저 처리해야한다. 


자바는 이러한 긴급 데이터를 보내고 받는 기능을 지원하며, 메소드의 이름은 sendUrgentData() 이다.


public void sendUrgentData(int data) throws IOException


이 메소드는 매개변수로 전달된 값의 하위 8비트를 거의 즉시 전송한다. 필요하다면 현재 캐시에 저장된 데이터를 먼저 플러시한다.


긴급데이터를 보통의 데이터와 함께 수신하고자 할 경우에는 다음 메소드를 사용하여 옵션을 설정하면 된다.


public void setOOBInline(boolean on) throws SocketException

public boolean getOOBInline() throws SocketException


OOBINLINE의 기본 값은 false이다. 아래 코드는 OOBINLINE 설정이 꺼져 있는지 확인하고 켠다.


if(!s.getOOBInline()) s.setOOBInline(true);



SO_REUSEADDR


소켓이 종료될 때, 로컬 포트를 즉시 해제하지 않는 경우가 있다. 특히 소켓을 닫을 때 열린 연결이 있는 경우 포트가 즉시 해제되지 않을 수 있다. 소켓은 종료될 때 아직 네트워크를 통해 해당 포트로 전송 중인 나머지 패킷이 있는 경우에는 때로 일정 시간동안 기다린다. 시스템은 늦게 도착한 패킷을 위해 특별한 뭔가를 하지는 않는다. 다만 시스템은 같은 포크에 바운드(bound)된 새로운 프로세스에게 늦게 도착한 패킷이 전달되지 않도록 한다.


소켓이 well-known port를 사용할 경우에 문제가 되며, 그 이유는 일정 시간동안 다른 소켓이 해당 포트를 사용할 수 없도록 막기 때문이다.


SO_REUSEADDR 옵션이 켜진 경우(default값은 꺼짐) 이전 소켓으로 전송된 데이터가 남아 있는 경우에도 또 다른 소켓이 해당 포트를 바인드(bind)할 수 있다.


자바에서 이 옵션을 제어하기 위한 메소드는


public void setReuseAddress(boolean on) throws SocketException

public boolean getReuseAddress() throws SocketException


이 기능의 동작을 위해서는 새로운 소켓이 포트에 바인드 되기 전에  setReuseAddress() 메소드가 호출되어야 한다. 이 말은 곧 먼저 매개변수가 없는 소켓 생성자를 사용하여 연결되지 않은 소켓을 만들어야 한다는 의미이다. 


그러고 나서 setReuseAddress(true)를 호출한 다음 connect() 메소드를 호출하여 연결한다. 이전에 연결된 소켓과 이전의 주소를 재사용하는 새 소켓은 SO_REUSEADDR을 true로 설졍해야 효과를 볼 수 있다.