SOCKETCHANNELS을 이용해서 작업하기

|
출처 : Sun Korea Developer Network



Web Services API나 혹은 높은 수준의 API까지 다뤄야 하는 네트워킹 태스크를 해야 할 일이 생긴다면, 소켓을 이용해서 태스크를 적은 비용으로 좀 더 간단하게 완수할 수 있는 방법을 찾아보자. 이번 Tech Tip에서는 네트워크화 된 간단한 애플리케이션을 생성하기 위해 java.nio packageSocketChannelServerSocketChannel 클래스를 사용하게 될 것이다.

사용자가 특정 웹 페이지를 보기 위해 브라우저를 띄울 때, 페이지의 세부정보는 감춰지게 된다. 브라우저를 열고 다음을 입력하자.

http://developer.java.sun.com/developer/JDCTechTips/

결과적으로, 이 엔트리는 사용자를 웹 사이트의 80포트에 연결시켜주며, 보고자 하는 페이지의 이름을 포함하는 특정(particular) HTTP 요구(request)도 보내게 된다

자 이제 SocketChannel을 이용해서 다음의 액션을 명시적으로 실행해보자. TechTipReader프로그램은 페이지를 검색하기 위해서 SocketChannel를 사용한다. 이 프로그램의 getTips() 메소드를 주목해서 보면, 먼저 SocketChannel을 실행하고 난 후, 그것을 developer.java.sun.com의 80포트에 연결하기 위해서 인스턴스를 사용하는 것을 알 수 있다. 그리고 나서 JDCTechTips 페이지를 검색하기 위해 표준(standard) HTTP "GET"요구(request)를 보낸다.

서버로부터의 응답이 standard out에 출력된다.

   import java.nio.channels.SocketChannel;
   import java.nio.charset.Charset;
   import java.nio.ByteBuffer;
   import java.net.InetSocketAddress;
   import java.io.IOException;

   public class TechTipReader {

      private Charset charset = 
        Charset.forName("UTF-8");
      private SocketChannel channel;

      public void getTips() {
        try {
          connect();
          sendRequest();
          readResponse();
        } catch (IOException e) {
          e.printStackTrace();
        } finally {
          if (channel != null) {
            try {
              channel.close();
            } catch (IOException e) {}
          }
       }
      }

      private void connect() throws IOException {
         InetSocketAddress socketAddress =
           new InetSocketAddress(
             "developer.java.sun.com", 80);
         channel = SocketChannel.open(socketAddress);
       }

      private void sendRequest() throws IOException {
          channel.write(charset.encode("GET "
                            + "/developer/JDCTechTips/"
                            + "\r\n\r\n"));
        }

      private void readResponse() throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        while ((channel.read(buffer)) != -1) {
          buffer.flip();
          System.out.println(charset.decode(buffer));
          buffer.clear();
        }
      }

      public static void main(String[] args) {
        new TechTipReader().getTips();
      }
   }

TechTipReader 프로그램을 자세히 들여다보면, connect() 메소드가 InetSocketAddress를 생성할 때, 도메인과 표준(standard) HTTP 80포트를 나타내는 스트링, 이렇게 두 개의 매개변수(parameters)를 받고 있는 것을 발견하게 될 것이다. 그리고 스태틱 메소드인 SocketChannel.open()을 호출하면 InetSocketAddress를 매개변수(parameter)로 갖는데, 이 호출은 다음 2개의 과정에 상당한다.

   channel = SocketChannel.open();
   channel.connect(socketAddress);

sendRequest() 메소드는 첫번째 라인에 "GET /developer/JDCTechTips/", 두번째와 세번째 라인은 비어있는3줄짜리 요구(request)를 보낸다. channel.write() 호출은 ByteBuffer를 인수(argument)로 갖기 때문에 사용자는 스트링을 변환해야 한다. 사실 SocketChannels을 이용해서 작업할 때, ByteBuffers를 변환하는 데에 대부분의 시간을 할애하게 된다. 변환하는 방법에는 여러 가지가 있을 수 있지만 이 예제에서는 java.nio.charset.Charset클래스의 encode()메소드를 사용한다.

readResponse()메소드는 SocketChannel로 리턴되는 응답을 처리하는 역할을 하며, SocketChannel을 통해서 ByteBuffer를 읽어들인다. 전송되는 파일의 끝에 도달하지 않는 한, readResponse()메소드는 버퍼를 읽기상태에서 쓰기준비 상태로 뒤집는다. ByteBuffer의 내용은 charset.decode()메소드에 의해 스트링으로 변환되고, 이 스트링은 standard out으로 보내진다.

TechTipReader 프로그램을 실행할 때, 디스플레이 된 페이지의 HTML을 봐야 한다.

자 이제 두 번째 예제를 보자. 클라이언트와 서버를 동반한 이 예제에서는 2개의 숫자가 더해진다. 클라이언트가 더해질 2개의 숫자를 서버에 보내면 서버는 덧셈을 수행하고 합계를 낸다. 이 예는 SocketChannel API을 기반으로 하지만, 웹서비스나 RMI 혹은 서블릿을 이용하는 다른 솔루션도 가능하다.

여기에서 ByteBuffer는 2개의 int값을 갖는데 이들은 사용자가 ByteBuffer을 8byte로 제한하고, ByteBuffer에 대한 뷰(view)의 역할을 하는 IntBuffer을 생성하도록 한다.

  private ByteBuffer buffer = ByteBuffer.allocate(8);
  private IntBuffer intBuffer = buffer.asIntBuffer();

SumClient라는 다음의 프로그램은 예제의 클라이언트부분인데 이를 실행하기 위해서는 이 글의 뒷부분에서 보게 될 예제의 서버부분(SumServer)을 먼저 시작해야 한다.

   import java.nio.channels.SocketChannel;
   import java.nio.ByteBuffer;
   import java.nio.IntBuffer;
   import java.io.IOException;
   import java.net.InetSocketAddress;

   public class SumClient {

      private SocketChannel channel;
      private ByteBuffer buffer = ByteBuffer.allocate(8);
      private IntBuffer intBuffer = buffer.asIntBuffer();

      public void getSum(int i, int j) {
        try {
          channel = connect();
          sendSumRequest(i, j);
          receiveResponse();
        } catch (IOException e) {
          // add exception handling code here
          e.printStackTrace();
        } finally {
          if (channel != null) {
            try {
              channel.close();
            } catch (IOException e) {
              // add exception handling code here
              e.printStackTrace();
            }
          }
        }
      }

      private SocketChannel connect() 
                                   throws IOException {
        InetSocketAddress socketAddress =
          new InetSocketAddress("localhost", 9099);
        return SocketChannel.open(socketAddress);
      }

      private void sendSumRequest(int i, int j)
                                   throws IOException {
        buffer.clear();
        intBuffer.put(0, i);
        intBuffer.put(1, j);
        channel.write(buffer);
        System.out.println("Sent request for sum of "
                           + i + " and " + j + "...");
      }

      private void receiveResponse() 
                                   throws IOException {
        buffer.clear();
        channel.read(buffer);
        System.out.println(
                       "Received response that sum is "
                       + intBuffer.get(0) + ".");
      }

      public static void main(String[] args) {
        new SumClient().getSum(14, 23);
      }
   }

SumClientgetSum()메소드는 SocketChannel을 미리 예정된 주소로 연결한다. connect()메소드는 TechTipReader 예제에서 보았던 것과 본질적으로 동일하다. connect() 메소드를 호출하면, getSum() 메소드는 2개의 int값(14, 23)을 차례대로 sendSumRequest() 메소드로 넘기는데, 이 때 sendSumRequest()ByteBuffer의 내용을 SocketChannel에 입력하는 역할을 한다. 그러면 receiveResponse() 메소드는 리턴된 메소드의 내용을 가지고 와서 그것을 standard out에 출력하게 된다.

사용자가 SumClient를 실행시키면, 다음 사항이 화면에 표시된다. (SumServer를 먼저 실행해야 하는 것을 잊지 말자.)

   Sent request for sum of 14 and 23...
   Received response that sum is 37.

SumClient를 자세히 보면, sendSumRequest()가 같은 ByteBuffer의 2개의 다른 뷰(view)를 사용하는 것을 알 수 있다. 먼저, buffer.clear()는 사실상 버퍼를 비우고 ByteBufferintBuffer 핸들의 사용으로 인해 2개의 int값을 갖는 버퍼로 간주된다. sendSumRequest()의 첫번째 파라미터로 넘겨진 int값은 intBuffer.put(0,i)을 이용하여 첫번째 슬롯에 놓여지고, 두번째 파라미터 또한 유사한 방법으로 두번째 슬롯에 놓여진다. SocketChannelwrite()메소드는 ByteBuffer를 갖기 때문에 버퍼의 첫번째 뷰(view)가 필요하다.

receiveResponse()메소드는 sendSumRequest()와 같다(parallel). 버퍼의 내용이 또다시 비워지고 SocketChannel의 내용은 버퍼의 ByteBuffer 뷰(view)로 해석된다. IntBuffer는 첫번째 위치에서 int값을 도출하기 위해 사용된다.

솔루션의 서버부분인 SumServer로 넘어가기 전에, SocketChannel 객체의 read() write() 메소드를 호출하는 장소에 추가적인 안전장치를 넣길 원할 수도 있다. 이 메소드들은 읽히거나 혹은 입력되는 바이트의 수와 함께 long값을 리턴하는데, 이번의 경우에는 8바이트라고 생각하면 된다. sendSumRequest()의 다음 라인을 찾아보고:

   channel.write(buffer);

입력되는 버퍼의 사이즈를 확인하는 다음의 내용으로 바꾸자.

   if (channel.write(buffer)!= 8){
       throw new IOException("Expected 8 bytes.");
}

receiveResponse()메소드의 channel.read() 호출주변에 비슷한 가드를 넣자.

이제 SumServer를 보자. 이 예제를 실행하기 위해서, 사용자는 동의한 위치에 착신요구(incomingrequest)를 처리하게 될 서버를 셋업해야 한다. 밑의 openChannel() 메소드에서는 9099포트가 사용되었다. ServerSocketChannel의 static method인 open()이 호출되면, 그 다음 라인은 명시된 포트에 소켓을 묶는다. 그리고 channel.isOpen() 메소드가 트루(true)를 리턴하는대로 확인메시지가 standard out으로 보내진다.

   import java.nio.ByteBuffer;
   import java.nio.IntBuffer;
   import java.nio.channels.ServerSocketChannel;
   import java.nio.channels.SocketChannel;
   import java.io.IOException;
   import java.net.InetSocketAddress;

   public class SumServer {

      ByteBuffer buffer = ByteBuffer.allocate(8);
      IntBuffer intBuffer = buffer.asIntBuffer();
      ServerSocketChannel channel = null;
      SocketChannel sc = null;

      public void start() {
        try {
          openChannel();
          waitForConnection();
        } catch (IOException e) {
          e.printStackTrace();
        }
      }

      private void openChannel() throws IOException {
        channel = ServerSocketChannel.open();
        channel.socket().bind(
                          new InetSocketAddress(9099));
        while (!channel.isOpen()) {
        }
        System.out.println("Channel is open...");
      }

      private void waitForConnection() 
      throws IOException {
        while (true) {
          sc = channel.accept();
          if (sc != null) {
            System.out.println(
                           "A connection is added...");
            processRequest();
            sc.close();
          }
        }
      }

      private void processRequest() throws IOException {
        buffer.clear();
        sc.read(buffer);
        int i = intBuffer.get(0);
        int j = intBuffer.get(1);
        buffer.flip();
        System.out.println("Received request to add "
                           + i + "  and " + j);
        buffer.clear();
        intBuffer.put(0, i + j);
        sc.write(buffer);
        System.out.println("Returned sum of " +
                           intBuffer.get(0) + ".");
      }

      public static void main(String[] args) {
        new SumServer().start();
      }

   }

사용자가 SumServer를 시작하면, 다음 메시지가 나타난다.

   Channel is open...

   Sent request for sum of 14 and 23...
   Received response that sum is 37.  

SumClient를 실행시키면, SumServer는 이하의 내용을 화면에 표시한다.

   
   A connection is added...
   Received request to add 14  and 23
   Returned sum of 37. 

waitForConnection() 메소드에서는 ServerSocketChannel에 접속하려는 요구가 도착할 때까지 애플리케이션이 주기를 갖고 반복한다. 이 요구가 충족되면, channel.accept()SocketChannel을 리턴하기 때문에 변수 sc는 더 이상 null이 아니다. 착신요구(incoming request)가 처리되면 SocketChannel는 종료되고, 서버는 다시 SumServer를 시작해서 다른 접속요청을 대기한다. 이 간단한 예제에서 요구(requests)들은 한번에 완전히 처리된다는 것을 상기하자. 동시다발적인 요구를 수행할 수 있는 좀 더 심화된 서버를 위해서는 다른 구조가 필요하지만, 이것은 이번 테크팁이 커버하는 범위를 벗어나기 때문에 다루지 않겠다.

processRequest() 메소드는 SumClient의 대응 메소드(corresponding methods)와 매우 흡사하다. 버퍼가 지워지면 채널이 버퍼로 읽혀지고 IntBuffer 뷰(view)는 버퍼에서 파생된 2개의 int값을 검색하는데 사용된다. 사용 후 버퍼가 제거되고 합계는 IntBuffer 뷰(view)의 사용 중에 로드되는데, 그리고 나서 버퍼는 SocketChannel로 다시 쓰여지게 된다. SocketChannel관한 자세한 정보는 테크니컬 아티클인 "New I/0 Functionality for Java 2 Standard Edition 1.4"을 참고한다.

Trackback 0 And Comment 0
prev | 1 ... | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 ... | 150 | next