2011년 6월 9일 목요일

non-blocking 소켓

non-blocking 통신 구현에 JSSE와 NIO 사용하기

Kenneth Ballard
컴퓨터공학, Peru State College
2003년 10월 22일
SSL blocking 작동이 non-blocking 작동 보다 I/O 에러 공지에 더 낫지만 non-blocking 작동은 호출 쓰레드를 지속시킨다. 이 글에서 클라이언트와 서버 양 측면을 모두 다룰 것이며 Java Secure Socket Extensions (JSSE)과 Java NIO (새 I/O) 라이브러리를 사용하여 non-blocking 보안 연결을 만드는 방법을 설명한다. non-blocking 소켓을 만드는 전통적인 접근방식을 설명한다.
blocking 할 것인가? 말 것인가? 그것이 문제다. 셰익스피어는 아니더라도 이 글은 인터넷 클라이언트를 작성하는 프로그래머라면 생각해야 할 중요한 포인트를 끄집어낸다. blocking인가? non-blocking인가?
많은 프로그래머들은 자바로 인터넷 클라이언트를 작성할 때 이 질문을 고려하지 않는다. 주된 이유는 blocking 통신이라는 유일한 옵션만 있기 때문이다. 하지만 지금은 자바 프로그래머들에게 선택이 주어졌다. 따라서 작성하는 모든 클라이언트 마다 옵션을 고려해야 한다.
non-blocking 통신은 Java 2 SDK 1.4 버전에 도입되었다. 이 버전으로 프로그래밍을 했다면 새로운 I/O (NIO) 라이브러리를 봤을 것이다. 이것이 도입되기 전에 non-blocking 통신은 제 삼자 라이브러리를 구현할 때에만 사용될 수 있었다.
NIO 라이브러리에는 파일, 파이프, 클라이언트 소켓, 서버 소켓용 non-blocking 기능이 포함된다. 라이브러리가 없앤 기능은 보안 non-blocking 소켓 연결이다. NIO 또는 JSSE 라이브러리에 구현된 보안 채널 클래스가 없다. 하지만 이것이 보안 non-blocking 통신을 가질 수 없다는 것을 의미하는 것은 아니다. 약간의 오버헤드가 개입될 뿐이다.
이 글을 완벽히 이해하기 위해서는 다음 사항에 익숙해져야 한다:
  • 자바 소켓 통신 개념.
  • SSL.
  • NIO 라이브러리.
  • Java 2 SDK 1.4 또는 이후 버전.

blocking 통신과 non-blocking 통신
blocking 통신은 소켓에 접근하거나 데이터를 읽거나 쓰는 동안, 통신 방식이 소켓으로의 blocking 접근이라는 것을 의미한다. JDK 1.4에 앞서 이러한 blocking 한계를 극복하는 방법은 쓰레드를 자유롭게 사용하는 것이었지만 상당한 쓰레드 오버헤드를 만들어냈다. 이는 시스템의 퍼포먼스와 확장성에 영향을 끼쳤다. java.nio 패키지는 서버가 I/O 스트림을 효과적으로 사용할 수 있도록 하고 적당한 시간 동안 여러 클라이언트 요청을 핸들링하여 새 지평을 열었다.
non-blocking 통신은 기본적으로 프로세스가 할 수 있는 것을 보내거나 읽는다. 읽을 수 있는 것이 없다면 읽기를 중단하고 프로세스는 데이터가 가능해 질 때까지 다른 것을 할 수 있다. 데이터를 보낼 때 프로세스는 모든 데이터를 보내는 것을 시도하지만 실제로 보내지는 것을 리턴할 것이다. 모든 데이터가 되거나 아니면 어떤 데이터도 아니다.
blocking은 non-blocking에 비해 몇 가지 유리한 점이 있다. 특히 에러 제어 부분에 있어서 그렇다. blocking 소켓 통신에서 어떤 에러 결과든지 메소드는 자동으로 에러를 규명하는 코드를 같이 리턴한다. 에러는 네트워크 종료, 닫힌 소켓, 아이오 에러 등이 될 수 있다. non-blocking 소켓 통신에서 이 메소드에 의해 프로세스되지 않는 유일한 에러는 네트워크 종료이다. non-blocking 통신을 사용하여 종료를 탐지하려면 더 많은 코드가 작성되어 데이터를 마지막으로 받은 후 얼마나 많은 시간이 흘렀는지를 결정해야한다.
어떤 애플리케이션이 더 나은지는 애플리케이션에 달려있다. 동기식 통신을 사용한다면 blocking 통신이 더 낫다. 비동기식 통신에서는 소켓이 묶이는 것을 피하기 위해 non-blocking 통신이 필요하다.
전통적인 non-blocking 클라이언트 소켓 구현
자바 NIO 라이브러리는 스트림 대신 채널을 사용한다. 이 채널은 blocking 통신과 non-blocking 통신 모두를 사용할 수 있다. 하지만 만들어질 때는 non-blocking 버전이 디폴트이다. 하지만 모든 non-blocking 통신이 이 이름의 Channel을 사용하여 클래스로 간다. 소켓 통신의 경우 SocketChannel 클래스가 사용된다. (Listing 1):

Listing 1. SocketChannel 객체 구현 및 연결
SocketChannel sc = SocketChannel.open(); sc.connect("www.ibm.com",80); sc.finishConnect();


SocketChannel 유형의 포인터가 선언되어야하지만 new 오퍼레이터를 사용하여 객체를 만들 수 없다. 대신 SocketChannel 클래스의 정적 메소드가 호출되어 채널을 열어야한다. 채널이 열린 후에 connect() 메소드를 호출하여 연결 될 수 있다. 하지만 메소드가 리턴될 때 소켓은 반드시 연결되는 것은 아니다. 확인하기 위해 finishConnect()에 대한 후속 호출이 이루어져야 한다.
소켓이 연결되면 non-blocking 통신은 SocketChannel 클래스의 read()와 write()메소드를 사용하여 시작된다. 그렇지 않으면 객체를 개별 ReadableByteChannel과 WritableByteChannel 객체에 던질 수 있다. 이 두 경우 모두 데이터용 Buffer 객체를 사용하게 될 것이다
소켓이 더이상 필요하지 않으면 close() 메소드를 사용하여 닫힌다:
sc.close();


이는 소켓 연결과 기반의 통신 채널 모두를 닫는다.
대안 non-blocking 클라이언트 소켓 구현하기
이전 방식은 소켓 커넥션을 만드는 전통적인 루트 보다는 약간 복잡하다. 하지만 전통적인 루트는 non-blocking 소켓을 만드는데 사용될 수 있다. non-blocking 통신이 가능하도록 몇 가지 단계만 추가하면 된다.
SocketChannel 객체의 기반 통신에는 두 개의 Channel 클래스가 있다: ReadableByteChannel과 WritableByteChannel. 이 두 클래스들은 기존 blocking 스트림인 InputStream과 OutputStream에서 만들어 질 수 있다. (Listing 2):

Listing 2. 스트림에서 채널 검색하기
ReadableByteChannel rbc = Channels.newChannel(s.getInputStream()); WriteableByteChannel wbc = Channels.newChannel(s.getOutputStream());


Channels 클래스는 채널들을 스트림이나 리더&라이터로 변환하는데 사용된다. 이는 blocking 모드로 통신을 변환하는 것처럼 보이나 그런 경우는 아니다. 채널에서 검색된 스트림에서 읽기를 시도한다면 읽기 메소드는 IllegalBlockingModeException을 던진다.
같은 것이 반대로 적용된다. Channels 클래스를 사용하여 스트림을 채널로 변경할 수 없고 non-blocking 통신을 기대할 수도 없다. 스트림에서 검색된 채널에서 읽기를 시도한다면 blocking 리드만 여전히 남아있다. 하지만 모든 경우가 그렇듯 예외 없는 규칙은 없다.
예외는 SelectableChannel 추상 클래스를 구현하는 클래스에 적용된다. SelectableChannel과 이것의 파생물은 blocking 또는 non-blocking 모드를 선택할 수 있다. SocketChannel이 그 파생물이다.
하지만 이 둘 사이를 앞뒤로 변환하려면 인터페이스가 SelectableChannel로서 구현되어야 한다. 소켓을 사용하여 SocketChannel은 이 기능을 가능하게 하는 소켓 대신 사용된다.

Listing 3. 소켓 구현 대안
Socket s = new Socket("www.ibm.com", 80); ReadableByteChannel rbc = Channels.newChannel(s.getInputStream()); WriteableByteChannel wbc = Channels.newChannel(s.getOutputStream());


에뮬레이션 레이어에서 데이터 읽기
에뮬레이션 레이어는 읽기 작동을 시도하기에 앞서 데이터 가용성을 점검한다. 데이터가 가능하면 읽힌다. 데이터를 사용할 수 없다면(소켓이 닫혔을 경우) 이를 신호하는 코드를 리턴한다. InputStream이 이 액션을 완전히 수행할 수 있음에도 ReadableByteChannel이 읽기에 사용되는 것에 주목하라.(Listing 4). 이유는? NIO가 에뮬레이션 레이어 대신 통신을 수행한다는 착각을 보여주기 위해서이다. 게다가 에뮬레이션 레이어와 다른 채널들이 데이터를 파일 채널에 작성토록 하는 것은 더욱 쉽다.

Listing 4. non-blocking 읽기 작동 에뮬레이션
/* The checkConnection method returns the character read when determining if a connection is open. */ y = checkConnection(); if(y <= 0) return y; buffer.putChar((char ) y); return rbc.read(buffer);


에뮬레이션 레이어에 데이터 작성하기
non-blocking 통신을 사용하면 쓰기 작동은 쓰여질 수 있는 것만 작성한다. 버퍼 사이즈는 Socket 객체의 getSendBufferSize() 메소드를 호출하여 결정 될 수 있다. 사이즈는 non-blocking 쓰기 작동을 시도할 때 고려되어야 한다. 블록 사이즈 보다 큰 데이터를 작성한다면 다중의 쓰기 작동으로 쪼개야한다.

Listing 5. non-blocking 쓰기 작동 에뮬레이션
int x, y = s.getSendBufferSize(), z = 0; int expectedWrite; byte [] p = buffer.array(); ByteBuffer buf = ByteBuffer.allocateDirect(y); /* If there isn't any data to write, return, otherwise flush the stream */ if(buffer.remaining() == 0) return 0; os.flush() for(x = 0; x < p.length; x += y) { if(p.length - x < y) { buf.put(p, x, p.length - x); expectedWrite = p.length - x; } else { buf.put(p, x, y); expectedWrite = y; } /* Check the status of the socket to make sure it's still open */ if(!s.isConnected()) break; /* Write the data to the stream, flushing immediately afterward */ buf.flip(); z = wbc.write(buf); os.flush(); if(z < expectedWrite) break; buf.clear(); } if(x > p.length) return p.length; else if(x == 0) return -1; else return x + z;


에뮬레이션 레이어 클래스 템플릿
전체 에뮬레이션 레이어는 클래스 내부에 위치하여 애플리케이션에 쉽게 통합될 수 있다. 이것이 정확히 수행된다면 ByteChannel 에서 클래스를 검색할 것을 권한다. 클래스는 ByteChannel에서 개별 ReadableByteChannel과 WritableByteChannel 객체로 던져질 수 있다.
Listing 6은 에뮬레이션 레이어의 예제 클래스이다.

Listing 6. 에뮬레이션 레이어의 클래스 템플릿
public class nbChannel implements ByteChannel { Socket s; InputStream is; OutputStream os; ReadableByteChannel rbc; WritableByteChannel wbc; public nbChannel(Socket socket); public int read(ByteBuffer dest); public int write(ByteBuffer src); public void close(); protected int checkConnection(); }


에뮬레이션 레이어를 사용하여 소켓 만들기
새로운 에뮬레이션 레이어를 사용하여 소켓을 만드는 것은 매우 간단하다. Socket 객체를 만든 다음 nbChannel 객체를 만들면된다. (Listing 7):

Listing 7. 에뮬레이션 레이어 사용하기
Socket s = new Socket("www.ibm.com", 80); nbChannel socketChannel = new nbChannel(s); ReadableByteChannel rbc = (ReadableByteChannel)socketChannel; WritableByteChannel wbc = (WritableByteChannel)socketChannel;


전통적인 non-blocking 서버 소켓 구현하기
서버 측에 non-blocking 소켓은 클라이언트 측의 그것과 다르지 않다. 인커밍 커넥션을 받아들이기 위해 소켓을 설정하는데 약간의 오버헤드가 더 있을 뿐이다. 소켓은 서버 소켓 채널에서 blocking 서버 소켓을 검색하여 blocking 모드로 묶여야한다.

Listing 8. non-blocking 서버 소켓 구현하기 (SocketChannel)
ServerSocketChannel ssc = ServerSocketChannel.open(); ServerSocket ss = ssc.socket(); ss.bind(new InetSocketAddress(port)); SocketChannel sc = ssc.accept();


클라이언트 소켓 채널 처럼 서버 소켓 채널은 new 오퍼레이터와 콘스트럭터를 사용하는 대신 열려야한다. 이것이 열린 후 서버 소켓 객체는 소켓 채널이 포트에 묶일 수 있도록 검색되어야 한다. 일단 소켓이 묶이면 서버 소켓 객체는 버려질 수 있다.
채널은 accept()메소드를 사용하여 인커밍 커넥션을 받아들이고 이들을 소켓 채널로 라우팅한다. 일단 인커밍 연결이 받아들여지고 소켓 채널 객체로 라우팅되면 통신은 read()와 write()메소드를 통해 시작될 수 있다.
대안 non-blocking 서버 소켓 구현하기
실제로 이는 대안이 아니다. 서버 소켓 채널은 서버 소켓 객체를 사용하여 묶여야하기 때문에 서버 소켓 채널을 피하고 서버 소켓 객체를 사용하는 것은 당연한 것 아니겠는가? 통신에 SocketChannel을 사용하는 대신 에뮬레이션 레이어인 nbChannel을 사용한다.

Listing 9. 서버 소켓 설정 대안
ServerSocket ss = new ServerSocket(port); Socket s = ss.accept(); nbChannel socketChannel = new nbChannel(s); ReadableByteChannel rbc = (ReadableByteChannel)socketChannel; WritableByteChannel wbc = (WritableByteChannel)socketChannel;


SSL 커넥션 만들기
클라이언트 측
SSL 커넥션을 만드는 전통적인 방식에는 소켓 팩토리와 다른 몇 가지 것들을 사용한다. (참고자료).
SSL 소켓을 만드는 기본 방식은 간단하며 다음과 같은 몇 단계만 거치면된다. (Listing 10):
  1. 소켓 팩토리 만들기.
  2. 연결된 소켓 만들기.
  3. 핸드쉐이크 시작하기.
  4. 스트림 찾기.
  5. 통신.


Listing 10. 보안 클라이언트 소켓 만들기
SSLSocketFactory sslFactory = (SSLSocketFactory)SSLSocketFactory.getDefault(); SSLSocket ssl = (SSLSocket)sslFactory.createSocket(host, port); ssl.startHandshake(); InputStream is = ssl.getInputStream(); OutputStream os = ssl.getOutputStream();


기본 방식에는 특정 연결에 필요한 클라이언트 인증, 커스텀 인증 등이 포함되지 않는다.
서버 측
SSL 서버 연결을 설정하는 전통적인 방식에는 어느정도의 오버헤드가 필요하고 많은 유형 캐스팅도 있다.
디폴트 SSL 서버 소켓 구현하기:
  1. 서버 소켓 팩토리 만들기.
  2. 서버 소켓 구현 및 바인딩.
  3. 인커밍 커넥션 수락.
  4. 핸드쉐이크 시작.
  5. 스트림 검색.
  6. 통신.


Listing 11. 보안 서버 소켓 만들기
SSLServerSocketFactory sslssf = (SSLServerSocketFactory)SSLServerSocketFactory.getDefault(); SSLServerSocket sslss = (SSLServerSocket)sslssf.createServerSocket(port); SSLSocket ssls = (SSLSocket)sslss.accept(); ssls.startHandshake(); InputStream is = ssls.getInputStream(); OutputStream os = ssls.getOutputStream();


보안 non-blocking 연결 구현하기
클라이언트 측
보안 non-blocking 연결을 클라이언트 측에 설정하기는 단순하다:
  1. Socket 객체 구현 및 연결.
  2. Socket 객체를 에뮬레이션 레이어에 어태치하기.
  3. 에뮬레이션 레이어를 통해 통신하기.


Listing 12. 보안 클라이언트 연결 구현
/* Create the factory, then the secure socket */ SSLSocketFactory sslFactory = (SSLSocketFactory)SSLSocketFactory.getDefault(); SSLSocket ssl = (SSLSocket)sslFactory.createSocket(host, port); /* Start the handshake. Should be done before deriving channels */ ssl.startHandshake(); /* Put it into the emulation layer and create separate channels */ nbChannel socketChannel = new nbChannel(ssl); ReadableByteChannel rbc = (ReadableByteChannel)socketChannel; WritableByteChannel wbc = (WritableByteChannel)socketChannel;


서버 측
서버측에 소켓을 설정할 때 기본 보안 때문에 약간의 오버헤드가 더 필요하다. 하지만 일단 소켓이 허용되어 라우팅되면 설정하는 것은 클라이언트 측과 같다. (Listing 13):

Listing 13. Creating a secure, non-blocking server socket
/* Create the factory, then the socket, and put it into listening mode */ SSLServerSocketFactory sslssf = (SSLServerSocketFactory)SSLServerSocketFactory.getDefault(); SSLServerSocket sslss = (SSLServerSocket)sslssf.createServerSocket(port); SSLSocket ssls = (SSLSocket)sslss.accept(); /* Start the handshake on the new socket */ ssls.startHandshake(); /* Put it into the emulation layer and create separate channels */ nbChannel socketChannel = new nbChannel(ssls); ReadableByteChannel rbc = (ReadableByteChannel)socketChannel; WritableByteChannel wbc = (WritableByteChannel)socketChannel;


보안&비보안 클라이언트 커넥션 통합하기
대부분의 인터넷 클라이언트 애플리케이션은 자바로 작성되었든지 아니면 다른 언어든지 간에 보안 및 비보안 커넥션을 제공해야 한다. Java Secure Socket Extensions 라이브러리로 이를 쉽게 할 수 있다. 이것은 HTTP 클라이언트 라이브러리를 작성할 때 최근에 도입된 방식이다.
SSLSocket 클래스는 Socket에서 찾을 수 있다. 필요한 것은 이 객체에 대한 하나의 Socket 포인터이다. 소켓 커넥션이 SSL을 사용하지 않으면 소켓은 정상적으로 구현될 수 있다. SSL을 사용하면 오버헤드가 생기지만 코드는 단순하다. (Listing 14):

Listing 14. 보안&비보안 클라이언트 커넥션 통합하기
Socket s; ReadableByteChannel rbc; WritableByteChannel wbc; nbChannel socketChannel; if(!useSSL) s = new Socket(host, port); else { SSLSocketFactory sslsf = SSLSocketFactory.getDefault(); SSLSocket ssls = (SSLSocket)SSLSocketFactory.createSocket(host, port); ssls.startHandshake(); s = ssls; } socketChannel = new nbChannel(s); rbc = (ReadableByteChannel)socketChannel; wbc = (WritableByteChannel)socketChannel; ... s.close();


채널 구현 다음에 소켓이 SSL을 사용하면 통신은 보안이 된다. 그렇지 않을 경우 비보안 통신이 된다. 소켓을 닫으면 SSL이 사용되고 있을 경우 핸드쉐이킹이 종료된다.

댓글 없음:

댓글 쓰기

ETL 솔루션 환경

ETL 솔루션 환경 하둡은 대용량 데이터를 값싸고 빠르게 분석할 수 있는 길을 만들어줬다. 통계분석 엔진인 “R”역시 하둡 못지 않게 관심을 받고 있다. 빅데이터 역시 데이터라는 점을 볼때 분산처리와 분석 그 이전에 데이터 품질 등 데이...