본문 바로가기

Java

[NIO] Socket Channel

채널은 파일, 소켓, 데이터그램 등과 같은 다양한 I/O 소스로부터 데이터 블록을 버퍼로 쓰거나 읽어온다.

네트워크 프로그래밍을 위해서는 SocketChannel, ServerSocketChannel, DatagramChannel 세개가 가장 중요하다. 그리고 TCP 연결을 위해서는 이 중에서도 앞의 두 개의 채널만 필요하다.

1. SocketChannel class

SocketChannel 클래스는 TCP 소켓에 대해 읽고 쓴다. 읽고 쓸 데이터는 ByteBuffer 객체로 먼저 인코딩 되어야 한다. 각각의 SocketChannel 은 Socket 객체와 연결되어 있다.

연결하기

SocketChannel 클래스는 public 으로 선언된 생성자를 제공하지 않는다. 대신에 다음 두 정적 open() 메소드를 사용하여 새로운 SocketChannel 객체를 만들 수 있다.

// 인자가 없는 생성자
// 인자가 없는 버전은 즉시 연결하지 않고, 최초에 연결되지 않은 소켓을 반환한다.
public static SocketChannel open() throws IOException {
        return SelectorProvider.provider().openSocketChannel();
}

// 인자가 있는 생성자
// 이 메소드를 호출시 connection 이 만들어지거나 exception 이 발생할 때까지 블록된다.
public static SocketChannel open(SocketAddress remote) throws IOException
{
    SocketChannel sc = open();
    try {
        sc.connect(remote);
    } catch (Throwable x) {
        try {
            sc.close();
        } catch (Throwable suppressed) {
            x.addSuppressed(suppressed);
        }
        throw x;
    }
    assert sc.isConnected();
    return sc;
}

 

인자가 없는 버전은 나중에 connect() 메소드를 호출하여 연결할 수 있다.

네트워크에 연결하기 이전에 채널이나 소켓에 옵션을 설정하고자 한다면 이 방법이 유용하다.

(특히, 논블록 채널을 열고 싶을때 유용하다.)

 

SocketChannel channel = SocketChannel.open();
SocketAddress address = new InetSocketAddress("www.cafeaulate.org", 80);
channel.connect(address);

 

논블록 채널의 connect() 메소드는 연결이 되기 전에 즉시 반환한다. 프로그램은 운영체제가 연결을 완료하는 동안 기다리지 않고 다른 일을 수행할 수 있다. 그러나 실제로 연결을 사용하기 전에 프로그램은 finsihConnect() 메소드를 호출해야 한다.

public abstract boolean finishConnect() throws IOException

 

연결이 맺어지고 사용할 준비가 된 경우 finishConnect() 메소드는 true 를 반환한다. 연결이 아직 맺어지지 않은 경우 finishConnect() 메소드는 false 를 반환한다.

마지막으로, 연결을 맺을 수 없는경우, 예를 들어 네트워크가 다운된 경우는 이 메소드는 예외를 발생시킨다.

읽기

SocketChannel 을 읽기 위해서는 먼저 채널이 데이터를 저장할 ByteBuffer 를 만든다. 그리고 생성된 버퍼를 read() 메소드에 전달한다.

public abstract int read(ByteBuffer dst) throws IOException

 

채널은 채울 수 있을 만큼의 데이터로 버퍼를 채운다. 그리고 나서 저장한 바이트 수를 반환한다.

메소드가 스트림의 끝에 도달할 경우, 채널은 남아 있는 바이트로 버퍼를 채우고 다음 호출시에 -1 을 반환한다.

예외 발생시에는, 블록 모드인 경우에는 -1을 반환하고, 논 블록 모드인 경우에는 0 을 반환한다.

 

데이터는 버퍼의 현재 위치에 저장되며, 이 위치는 저장될 때마다 자동으로 업데이트 되기 때문에, 버퍼가 가득 찰 떄까지 동일한 버퍼를 read() 메소드의 인자로 전달하여 호출할 수 있다. 예를 들어, 다음 루프는 버퍼가 가득 차거나 스트림이 끝에 도달할 때까지 읽는다.

 

while (buffer.hasRemaining() && channel.read(buffer) != -1);

한 소스로부터 여러 버퍼를 채우는 방식을 스캐터(scatter) 라고 하는데, 다음 메소드는 스캐터를 할 때 유용하게 사용할 수 있다. 다음 두 메소드는 인자로 ByteBuffer 객체의 배열을 전달받고 배열에 데이터를 차례대로 채운다.

 

/**
     * @throws  NotYetConnectedException
     *          If this channel is not yet connected
     */
    public abstract long read(ByteBuffer[] dsts, int offset, int length)
        throws IOException;

    /**
     * @throws  NotYetConnectedException
     *          If this channel is not yet connected
     */
    public final long read(ByteBuffer[] dsts) throws IOException {
        return read(dsts, 0, dsts.length);
    }

버퍼 배열에 데이터를 완전히 채우기 위해서는, 배열의 마지막 버퍼에 공간이 남아 있는 동안 루프를 돌기만 하면 된다. 예를 들어:

ByteBuffer [] buffers = new ByteBuffer[2];
buffers[0] = ByteBuffer.allocate(1000);
buffers[1] = ByteBuffer.allocate(1000);

while (buffer[1].hasRemaining() && channel.read(buffer) != -1);

쓰기

쓰기 위해서는 단지 ByteBuffer 를 채우고 flip() 을 호출한 다음, 쓰기 메소드 중 하나에 전달하기만 하면 된다.

그리고 쓰기 메소드는 버퍼의 데이터를 출력으로 내보낸다.

 

public abstract int write(ByteBuffer src) throws IOException;

 

위의 메소드는 채널이 논블록인 경우 버퍼의 내용을 완전히 출력한다고 보장하지 않는다. 그러나 버퍼는 커서 기반이기 때문에 버퍼 내용을 모두 내보낼 때까지 반복해서 호출해주면 된다.

while (buffer.hasRemainig() && channel.write(buffer) != -1);

여러 버퍼로부터 한 소켓에 데이터를 쓰는 방법을 gather 라고 하며, 다음 메소드는 gather 를 하는데 유용하게 사용된다.

/**
     * @throws  NotYetConnectedException
     *          If this channel is not yet connected
     */
    public abstract int write(ByteBuffer src) throws IOException;

    /**
     * @throws  NotYetConnectedException
     *          If this channel is not yet connected
     */
    public abstract long write(ByteBuffer[] srcs, int offset, int length)
        throws IOException;

첫 번째 메소드는 모든 버퍼를 내보낸다. 두 번째 메소드는 첫 배열의 offset 위치에서 시작하여 length 길이만큼 내보낸다.

 

종료하기

일반적인 소켓과 마찬가지로 채널 사용이 끝난 다음에 사용한 포트와 다른 리소스들을 해제하기 위해 채널을 닫아줘야 한다.

public void close() throws IOException

이미 닫힌 채널을 다시 닫을 경우 아무런 일도 일어나지 않는다. 닫힌 소켓에 읽기나 쓰기를 시도할 경우 채널은 예외를 발생시키기 때문에, 채널이 닫혔는지 확실하지 않은 경우, isOpen() 을 호출하여 확인해야 한다.

public boolean isOpen()

 

2. ServerSocketChannel class

ServerSocketChannel class 는 들어오는 연결을 수용하기 위한 한 가지 목적으로 사용된다.

이 클래스에 대해 읽거나 쓸 수 없으며 연결할 수도 없다.

이 클래스가 제공하는 유일한 동작은 새로운 연결을 수용하는 것 뿐이다.

클래스 자체는 단지 네 개의 메소드만 선언하고 있으며, 그 중 accept() 가 가장 중요한 메소드이다.

 

2-1. 서버 소켓 채널 만들기

정적 팩토리 ServerSocketChannel.open() 메소드는 새로운 ServerSocketChannel 객체를 만든다.

그러나 이 메소드의 이름은 약간 오해의 소지가 있다. 이 메소드는 실제 새로운 서버 소켓을 열지 않는다. 단지 객체를 만들 뿐이다. 반환된 객체를 사용하기 전에 해당 채널에 연결된 ServerSocket 을 구하기 위해 socket() 메소드를 호출해야 한다.

이 시점에서, ServerSocket 메소드가 제공하는 다양한 set 메소드를 호출하여 버퍼 사이즈나 소켓 타임아웃과 같은 서버 옵션들을 설정할 수 있다.

ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(port));

2-2. 연결 수용하기

ServerSocketChannel 객체를 열고 바인드하고 나면 accept() 를 호출하여 들어오는 연결을 대기할 수 있다.

public abstract SocketChannel accept() throws IOException

accpet() 메소드는 블록/논블록 모든 모드에서 동작할 수 있다. 블록 모드에서 accept() 메소드는 연결이 들어올때까지 기다린다. 연결이 수용되면 accpet() 메소드는 원격 클라이언트에 연결된 SocketChannel 객체를 반환한다.

ServerSocketChannel 은 또한 non-block 으로 동작한다. 이 경우에, accept() 메소드는 들어오는 연결이 없는 경우 null 을 반환한다.

non-block 모드는 각 연결에 대해 많은 작업이 필요하며, 다수의 요청을 병렬로 처리해야 하는 서버에 적절하다.

논블록 모드는 보통 셀렉터와 함께 사용된다.

 

3. Channels class

Channels 는 전통적인 I/O 기반 스트림, reader, 그리고 writer 등을 채널로 감싸기 위한 간단한 유틸리티 클래스이다.

이 클래스는 프로그램의 성능을 위해, 프로그램의 일부에 새로운 I/O 모델을 사용하고자 할 때 유용하게 사용할 수 있다.

 

'Java' 카테고리의 다른 글

[NIO] FileChannel 과 FileInput/Output Stream 과의 차이  (0) 2024.04.21
[NIO] Selection  (0) 2021.05.08