3 분 소요

Java Socket 이 어떻게 구현 되는지 약간 살펴봤습니다.


Java 에서 Socket 사용

웹을 살짝만 뒤져봐도 금방 찾을 수 있는 내용인데요, Java 에서 Socket 을 이런식으로 사용하고 있습니다.

1
2
3
4
5
6
ServerSocket server = new ServerSocket(8080);
Socket client = server.accept();
	:
	:
client.close();
server.close();

Client 라면 연결할 서버의 주소정도만 추가로 제공해주면 됩니다. 이런 코드는 충분히 high-level 이라, 기반이 되는 하드웨어나 Socket 대해 전혀 몰라도 사용할 수 있습니다.

Socket implementation

소켓의 구현부를 찾아 constructor 를 훑어봤습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private Socket(SocketAddress address, SocketAddress localAddr, boolean stream)  
    throws IOException  
{  
    Objects.requireNonNull(address);  
  
    // create the SocketImpl and the underlying socket  
    SocketImpl impl = createImpl();  
    impl.create(stream);  
  
    this.impl = impl;  
    this.state = SOCKET_CREATED;  
  
    try {  
        if (localAddr != null)  
            bind(localAddr);  
        connect(address);  
    } catch (IOException | IllegalArgumentException | SecurityException e) {  
        try {  
            close();  
        } catch (IOException ce) {  
            e.addSuppressed(ce);  
        }  
        throw e;  
    }  
}

7번 줄 부터 잘 보면 Socket 클래스 자체는 소켓의 엄밀한 구현부는 아니고, 사용할 프로그래머 입장에서 편리하게 사용하는 부분에 대해 구현한 클래스라고 느껴집니다. 실제 lower level 구현부는 SocketImpl 이고, createImpl() 메서드로 만들어주고 있습니다.

건너건너 찾아보면 NioSocketImpl 이라는 실제 구현체를 찾을 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Override  
protected void create(boolean stream) throws IOException {  
    synchronized (stateLock) {  
        if (state != ST_NEW)  
            throw new IOException("Already created");  
        if (!stream)  
            ResourceManager.beforeUdpCreate();  
        FileDescriptor fd;  
        try {  
            if (server) {  
                assert stream;  
                fd = Net.serverSocket(true);  
            } else {  
                fd = Net.socket(stream);  
            }  
        } catch (IOException ioe) {  
            if (!stream)  
                ResourceManager.afterUdpClose();  
            throw ioe;  
        }  
        Runnable closer = closerFor(fd, stream);  
        this.fd = fd;  
        this.stream = stream;  
        this.cleaner = CleanerFactory.cleaner().register(this, closer);  
        this.state = ST_UNCONNECTED;  
    }  
}

여기서 FileDescriptor 객체를 가져오고 있습니다. 보통 생각하는 File 이 HDD 라는 하드웨어를 편리하게 사용할 수 있게 만들어진 인터페이스라면, Socket 도 NIC 라는 하드웨어를 편리하게 사용할 수 있는 추상화된 인터페이스라서, C/C++ 에선 file descriptor 로 소켓을 취급합니다. FileDescriptor 를 가져온 Net.socket() 을 살펴보면

1
2
3
4
5
6
7
8
9
static FileDescriptor serverSocket(ProtocolFamily family, boolean stream) {  
    boolean preferIPv6 = isIPv6Available() &&  
        (family != StandardProtocolFamily.INET);  
    return IOUtil.newFD(socket0(preferIPv6, stream, true, FAST_LOOPBACK));  
}  
  
// Due to oddities SO_REUSEADDR on Windows reuse is ignored  
private static native int socket0(boolean preferIPv6, boolean stream, boolean reuse,  
                                  boolean fastLoopback);

8번 줄에 보이듯 네이티브 메서드를 사용하고 있습니다. 리턴값인 int 는 file descriptor 를 가르키는 것으로 IOUtil.newFD 에 의해 Java 의 FileDescriptor 로 감싸집니다.

UNIX 의 경우 man socket, 윈도우즈의 경우 win32 apps 에서, 두 명령어 모두 file descriptor 를 반환하는 걸 알 수 있습니다.

SocketChannel implementation

파일을 읽고 쓰듯 소켓에 읽고 쓰는 것 역시 IO 작업이기 때문에 (파일과 다르게 클라이언트로부터 데이터를 받는 걸 기다려야 하기도 하고) blocking 이 일어날 수 있습니다. Java 에서 non-blocking socket 을 쓰기 위해서 Selector, SocketChannel 등을 사용해 구현합니다. 대략 이런 식으로…

1
2
3
4
5
6
7
8
9
10
11
12
  Selector selector = Selector.open();
  ServerSocketChannel server = ServerSocketChannel.open();
  server.bind(new InetSocketAddress("localhost", 8080));
  server.configureBlocking(false);
  SelectionKey key = server.register(selector, SelectionKey.OP_ACCEPT);
  key.attach( new CustomAcceptor(selector, server));

  while(true){
    selector.select();
    Set<SelectionKey> keys = selector.selectedKeys();
    keys.Stream().forEach( k -> k.attachment().handle() );
  }

CustomAcceptorhandle() 메서드를 요청이 온 경우만 실행시킬 수 있게 됩니다. 앞서 Socket 의 경우와 비슷하게, 구현의 마지막 단은 결국엔 네이티브 메서드인데요. Selector, SocketChannel 에서 거슬러 올라가면 WindowsSelectorImpl 이라는 구현부를 찾을 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
  private int poll(int index) throws IOException {
      // poll for helper threads
      return  poll0(pollWrapper.pollArrayAddress +
                (pollArrayIndex * PollArrayWrapper.SIZE_POLLFD),
                Math.min(MAX_SELECTABLE_FDS,
                        totalChannels - (index + 1) * MAX_SELECTABLE_FDS),
                readFds, writeFds, exceptFds, timeout, fdsBuffer);
  }

  private native int poll0(long pollAddress, int numfds,
        int[] readFds, int[] writeFds, int[] exceptFds, long timeout, long fdsBuffer);

여기서도 네이티브 메서드롤 사용하고 있습니다. man poll 을 보면 file descriptor 에서 이벤트가 일어나기까지 기다리는 명령어입니다.

느낀점

Python 을 먼저 시작해서 그런건지도 모르겠지만, Java 코드를 읽다 보면 그래서 어디서 구현이 되는건지 찾기가 힘들 때가 많습니다. 다행히 IDE 가 implementation 들을 찾아주긴 하지만, Native method 같은 경우는 안돼죠… 이 포스트는 예전에 질문 받았던 “더 low level” 에선 어떻게 되느냐를 알기 위해서 작성했는데, 어쩐지 수박 겉핥기가 된 것 같습니다. 다음엔 NIC 나 네트워크에 대해서 좀 더 공부해 봐야겠습니다.

태그:

카테고리:

업데이트: