feat. Netty BootStrap, ServerBootStrap
부트 스트랩은 Netty에서 제공하는 편리한 팩토리 클래스이며 Netty 클라이언트 측 또는 서버 측 Netty 초기화를 완료하는 데 사용할 수 있다. 두 클래스다 AbstractBootstrap 클래스를 상속하고 있어서 코드를 분석 할 때 부모클래스와 자식클래스를 잘 넘나들며 봐야 한다.
Netty에서 Channel은 Socket의 추상화로, 사용자에게 Socket 상태(연결 여부에 관계없이)와 Socket에 대한 읽기 및 쓰기와 같은 작업을 제공한다. Netty가 연결을 설정할 때 해당 채널 인스턴스를 사용한다.
채널 유형은 NioSocketChannel, NioServerSocketChannel, NioDatagramChannel, NioSctpChannel, NioSctpServerChannel, OioSocketChannel, OioServerSocketChannel,
OioDatagramChannel, OioSctpChannel, OioSctpServerChannel 등이 있다.
Netty의 힘과 유연성의 중요한 부분을 차지하는 것이 Pipeline 기반이기 때문이다. ChannelHandler 기반으로 다양한 핸들러를 자유롭게 결합하여 플러그인 추가와 같은 비즈니스 로직을 완성 할 수 있다.
예를 들어 HTTP 데이터를 처리해야 하는 경우 파이프라인 앞에 하나를 추가 할 수 있다. Http 인코딩 및 디코딩 처리기, 그런 다음 자체 비즈니스 처리를 추가하여 네트워크의 데이터 흐름이 다른 처리 및 인코딩 및 디코딩 처리 파이프라인을 통해 흐르고 마지막에 사용자의 비즈니스 로직 처리에 도달 가능하게 할 수 있다.
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new EchoClientHandler());
}
});
// Start the client.
ChannelFuture f = b.connect(HOST, PORT).sync();
// Wait until the connection is closed.
f.channel().closeFuture().sync(); } finally {
// Shut down the event loop to terminate all threads. group.shutdownGracefully();
}
EventLoopGroup : 서버 든 클라이언트 든 EventLoopGroup을 지정해야하며, 이 예제에서는 NioEventLoopGroup을 사용한다.
ChannelType : Channel의 종류를 지정하며 Client이기 때문에 NioSocketChannel을 사용한다.
핸들러 : 데이터 프로세서를 설정한다.
BootStrap은 AbstractBootstrap의 자식클래스이므로 AbstractBootstrap 구성부터 보자
AbstractBootstrap구성을 위해서는 주요 6개의 필드가 필요하다.
public abstract class AbstractBootstrap, C extends Channel> implements Cloneable {
private volatile EventLoopGroup group;
private volatile ChannelFactory channelFactory;
private volatile SocketAddress localAddress;
private final Map, Object> options = new LinkedHashMap, Object>();
private final Map, Object> attrs = new LinkedHashMap, Object>();
private volatile ChannelHandler handler;
// ...
}
AbstractBootstrap은 ChannelFactory를 통해 Channel 인스턴스를 생성한다. channel 메서드는 Channel을 설정하는 것처럼 보이지만 실제로는 기본 ChannelFactory 구현만 설정한다.
public B channel(Class channelClass) {
if (channelClass == null) {
throw new NullPointerException("channelClass");
}
return channelFactory(new BootstrapChannelFactory(channelClass));
}
기본 ChannelFactory 구현은 리플렉션을 사용하여 채널 인스턴스를 만든다.
private static final class BootstrapChannelFactory implements ChannelFactory {
private final Class clazz;
BootstrapChannelFactory(Class clazz) {
this.clazz = clazz;
}
@Override
public T newChannel() {
try {
return clazz.newInstance();
} catch (Throwable t) {
throw new ChannelException("Unable to create Channel from class " + clazz, t);
}
}
}
Bootstrap.connect() 메서드는 validate() 메서드를 호출하여 필요한 인스턴들이 준비되었는지 확인한 다음 doConnect() 메서드를 호출한다.
doConnect()메소드는 먼저 initAndRegister()메소드를 호출한 다음 doConnect0()메소드를 호출하게 된다.
initAndRegister() 메서드는 ChannelFactory를 사용하여 Channel의 인스턴스를 만든 다음 init() 메서드를 호출하여 Channel을 초기화하고 마지막으로 Channel을 EventLoopGroup에 등록한다.
지금까지의 호출 순서를 요약하면 아래와 같다.
BootStraap - connect() => BootStraap - doConnect() => AbstractBootStraap - initAndRegister() => Bootstrap-init(channel)
AbstractBootstrap.initAndRegister에서는 새로운 NioSocketChannel 인스턴스를 얻기 위해 channelFactory().newChannel() 이 호출 되며 newChannel에서 NioSocketChannel의 기본 생성자를 호출한다. 그리고 새로운 Channel 인스턴스는 클래스 객체의 newInstance를 통해 얻어진다.
public NioSocketChannel() {
this(newSocket(DEFAULT_SELECTOR_PROVIDER));
}
newSocket 이 호출 되어 새 Java NIO SocketChannel을 연다.
private static SocketChannel newSocket(SelectorProvider provider) {
...
return provider.openSocketChannel();
}
그런 다음 부모 클래스인 AbstractNioByteChannel의 생성자가 호출된다.
AbstractNioByteChannel(Channel parent, SelectableChannel ch)
입력 매개 변수 parent는 null이고 ch는 방금 new Socket을 사용하여 생성한 Java NIO SocketChannel이된다.
protected AbstractNioByteChannel(Channel parent, SelectableChannel ch) {
super(parent, ch, SelectionKey.OP_READ);
}
그런 다음 부모 클래스 AbstractNioChannel의 생성자를 호출하여서 readInterestOp = SelectionKey.OP_READ 매개 변수를 전달한다.
protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
super(parent);
this.ch = ch;
this.readInterestOp = readInterestOp;
ch.configureBlocking(false);
}
그런 다음 부모 클래스 AbstractChannel의 생성자를 호출하게 되고 채널은 인스턴스화 될 때 파이프라인과 자동으로 연결된다.
protected AbstractChannel(Channel parent) {
this.parent = parent;
unsafe = newUnsafe();
pipeline = new DefaultChannelPipeline(this);
}
BootStrap.init() 메소드는 파이프라인 끝에 핸드러를 추가한다. 이 시점에서 채널이 준비된다.
Bootstrap.doConnect(), initAndRegister() 메서드가 종료된 후 doConnect() 메서드는 즉시 doConnect0() 메서드를 호출하고 doConnect0() 메서드는 Channel.connect() 메서드를 호출하여 채널이 서버에 연결되고 메시지를 보내고 받을 수 있다.
이 시점에서 완전한 NioSocketChannel이 초기화된다.
NioSocketChannel을 구성하는 데 필요한 작업을 요약 할 수 있다.
1. NioSocketChannel.newSocket (DEFAULT_SELECTOR_PROVIDER)를 호출하여 새 Java NIO SocketChannel을 연다.
2. AbstractChannel (Channel parent)에서 AbstractChannel의 속성을 초기화한다. 상위 속성이 null로 설정됨 unsafe는 newUnsafe()를 통해 unsafe객체를 인스턴스화한다. 그 유형은 AbstractNioByteChannel.NioByteUnsafe 내부 클래스이다. 파이프라인은 새 DefaultChannelPipeline (this)의 새로 생성된 인스턴스이다.
3. AbstractNioChannel의 속성으로서 SelectableChannel ch는 NioSocketChannel - newSocket에서 반환한 Java NIO SocketChannel 인 Java SocketChannel로 설정된다. readInterestOp는 SelectionKey.OP_READ로 설정되고 SelectableChannel ch는 비 차단 ch.configureBlocking(false) 로 구성된다.
4. NioSocketChannel의 속성으로서 SocketChannelConfig 구성 = new NioSocketChannelConfig (this, socket.socket()) 이다.
NioSocketChannel을 인스턴스화하는 과정에서 부모 클래스 AbstractChannel의 생성자에서 newUnsafe()가 호출되어 인스턴스를 얻는데 중요한 것은 Java의 하단 Socket의 동작을 캡슐화하므로 실제로는 Netty의 상위 계층과 Java의 하단 계층을 연결하는 중요한 다리 역할을 한다.
interface Unsafe {
SocketAddress localAddress();
SocketAddress remoteAddress();
void register(EventLoop eventLoop, ChannelPromise promise);
void bind(SocketAddress localAddress, ChannelPromise promise);
void connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise);
void disconnect(ChannelPromise promise);
void close(ChannelPromise promise);
void closeForcibly();
void deregister(ChannelPromise promise);
void beginRead();
void write(Object msg, ChannelPromise promise);
void flush();
ChannelPromise voidPromise();
ChannelOutboundBuffer outboundBuffer();
}
언뜻 보기에 이러한 메소드는 실제로 기본 Java 소켓의 작업에 해당한다.
AbstractChannel의 구성 메소드로 돌아가서 여기에서 newUnsafe()가 호출되어 새로운 안전하지 않은 객체를 얻고 newUnsafe 메소드는 NioSocketChannel에서 재정의 되었다.
@Override
protected AbstractNioUnsafe newUnsafe() {
return new NioSocketChannelUnsafe();
}
NioSocketChannel.newUnsafe 메서드는 NioSocketChannelUnsafe의 인스턴스를 반환한다. 여기에서 인스턴스화 된 NioSocketChannel의 안전하지 않은 필드가 실제로 NioSocketChannelUnsafe의 인스턴스인지 확인할 수 있다.
위에서 우리는 채널의 일반적인 초기화 프로세스 (이 예에서는 NioSocketChannel)를 분석했지만 ChannelPipeline의 초기화 부분은 보지 않았었다.
Each channel has its own pipeline and it is created automatically when a new channel is created 에 따르면 채널을 인스턴스화 할 때 인스턴스가 동반되어야한다는 것을 볼수 있으며 ChannelPipeline을 바꾼다. AbstractChannel 생성자에서 파이프라인 필드가 DefaultChannelPipeline의 인스턴스로 초기화되었음을 확인하였다.
public DefaultChannelPipeline(AbstractChannel channel) {
if (channel == null) {
throw new NullPointerException("channel");
}
this.channel = channel;
tail = new TailContext(this);
head = new HeadContext(this);
head.next = tail;
tail.prev = head;
}
DefaultChannelPipeline의 생성자를 호출하고 채널을 전달한다. 이 채널은 실제로 인스턴스화 한 NioSocketChannel이다. DefaultChannelPipeline은이 NioSocketChannel 객체를 채널 필드에 저장하고 DefaultChannelPipeline에는 두 개의 필드 head, tail이 있다. 실제로 DefaultChannelPipeline에서는 노드로 AbstractChannelHandlerContext가있는 DoubleLinkedList가 유지되며. 이것이 Netty의 파이프라인 메커니즘의 핵심이다.
헤드가 ChannelOutboundHandler 이고 꼬리가 ChannelInboundHandler 임을 알 수 있다.
HeadContext(DefaultChannelPipeline pipeline) {
super(pipeline, null, HEAD_NAME, false, true);
unsafe = pipeline.channel().unsafe();
}
NioEventLoop에는 오버로드 된 생성자가 여러 개 있지만, 내용에는 큰 차이가 없다. 결국에는 모두 수퍼 클래스 MultithreadEventLoopGroup 이다.
protected MultithreadEventLoopGroup(int nThreads, ThreadFactory threadFactory, Object... args) {
super(nThreads == 0? DEFAULT_EVENT_LOOP_THREADS : nThreads, threadFactory, args);
}
흥미로운 점 중 하나는 nThreads에서 전달하는 스레드 수가 0이면 Netty가 기본 스레드 수를 DEFAULT_EVENT_LOOP_THREADS로 설정한다. 기본 스레드 수는 어떻게 결정되는 건가?
실제로 코드는 매우 간단하다.
static {
DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt( "io.netty.eventLoopThreads", Runtime.getRuntime().availableProcessors() * 2));
}
Netty는 먼저 시스템 속성에서 "io.netty.eventLoopThreads"값을 가져오며, 설정하지 않으면 기본값인 프로세서 코어 수 * 2로 돌아간다.
다시돌아가서 MultithreadEventLoopGroup 생성자는 상위 클래스 MultithreadEventExecutorGroup의 생성자를 계속 호출한다.
protected MultithreadEventExecutorGroup(int nThreads, ThreadFactory threadFactory, Object... args) {
children = new SingleThreadEventExecutor[nThreads];
if (isPowerOfTwo(children.length)) {
chooser = new PowerOfTwoEventExecutorChooser();
} else {
chooser = new GenericEventExecutorChooser();
}
for (int i = 0; i < nThreads; i ++) {
children[i] = newChild(threadFactory, args);
}
}
코드에 따르면 MultithreadEventExecutorGroup의 처리순서는 아래와 같다.
1. nThreads 크기의 SingleThreadEventExecutor 배열 만들기
2. nThreads의 크기에 따라 다른 Chooser를 만든다. 즉, nThreads가 2의 거듭제곱이면 PowerOfTwoEventExecutorChooser를 사용하고, 그렇지 않으면 GenericEventExecutorChooser를 사용. 사용되는 Chooser에 관계없이 해당 기능은 동일. 즉, 자식 배열에서 적절한 EventExecutor를 선택
3. newChhild 메서드를 호출하여 자식 배열을 초기화
위 코드에 따르면 MultithreadEventExecutorGroup은 내부적으로 EventExecutor 배열을 유지하고 있다.
Netty의 EventLoopGroup의 구현 메커니즘은 실제로 MultithreadEventExecutorGroup을 기반으로한다. Netty가 EventLoop이 필요할 때마다 next() 메서드를 호출하여 사용 가능한 EventLoop을 얻는다.
위 코드의 마지막 부분은 newChild 메소드인데 추상메소드이고 하는 일은 EventLoop 객체를 인스턴스화하는 것이다. 코드를 추적 하면 이 메서드가 NioEventLoopGroup 클래스에서 구현되고 내용이 매우 간단하다는 것을 알 수 있다. 실제로 NioEventLoop 개체를 인스턴스화하고 반환한다.
@Override protected EventExecutor newChild( ThreadFactory threadFactory, Object... args) throws Exception {
return new NioEventLoop(this, threadFactory, (SelectorProvider) args[0]);
}
전체 EventLoopGroup의 초기화 프로세스를 요약
1. EventLoopGroup (실제로 MultithreadEventExecutorGroup)은 스레드 풀을 구성하는 크기가 nThreads 인 EventExecutor 자식 유형의 배열을 내부적으로 유지
2. NioEventLoopGroup을 인스턴스화하는 경우 스레드 풀 크기가 지정되면 nThreads가 지정된 값이고 그렇지 않으면 프로세서 코어 수 * 2
3. MultithreadEventExecutorGroup은 newChild 추상 메서드를 호출하여 자식 배열을 초기화
4. 추상 메서드 newChild는 NioEventLoop 인스턴스를 반환하는 NioEventLoopGroup에서 구현
Bootstrap.initAndRegister에서 채널이 초기화된다고 언급했지만 이 초기화된 채널을 EventGroup에 등록하기도한다.
final ChannelFuture initAndRegister() {
final Channel channel = channelFactory().newChannel();
init(channel);
ChannelFuture regFuture = group().register(channel);
}
채널이 초기화되면 group() register() 메서드가 즉시 호출되어 채널을 등록한다.
순서는 아래와 같다.
AbstractBootstrap.initAndRegister-> MultithreadEventLoopGroup.register-> SingleThreadEventLoop.register-> AbstractUnsafe
Register는 콜 체인을 추적하고 마지막으로 unsafe register 메서드가 호출된 것이 확인 된다.
@Override public final void register(EventLoop eventLoop, final ChannelPromise promise) {
AbstractChannel.this.eventLoop = eventLoop;
register0(promise);
}
register 메서드 는 register0 메서드를 호출
private void register0(ChannelPromise promise) {
boolean firstRegistration = neverRegistered;
doRegister();
neverRegistered = false;
registered = true;
safeSetSuccess(promise);
pipeline.fireChannelRegistered();
if (firstRegistration && isActive()) {
pipeline.fireChannelActive();
}
}
register0은 AbstractNioChannel.doRegister를 다시 호출
@Override protected void doRegister() throws Exception {
selectionKey = javaChannel().register(eventLoop().selector, 0, this);
}
이전에 이미 알고 있는 javaChannel() 메서드는 Java NIO SocketChannel을 반환한다. 여기서는 이 SocketChannel을 eventLoop과 관련된 Selector에 등록한다.
채널 등록 과정을 요약 해보면
1. 먼저 AbstractBootstrap.initAndRegister에서 group().register(channel)을 통해 MultithreadEventLoopGroup.register 메서드를 호출
2. MultithreadEventLoopGroup.register에서 next()를 통해 사용 가능한 SingleThreadEventLoop을 가져온 다음 해당 레지스터를 호출한다.
3. SingleThreadEventLoop.register에서 channel.unsafe().register (this, promise)를 사용하여 채널의 unsafe() 기본 작업 객체를 가져온 다음 해당 레지스터를 호출.
4. AbstractUnsafe.register 메서드에서 register0 메서드를 호출하여 Channel을 등록.
5. AbstractUnsafe.register0에서 AbstractNioChannel.doRegister 메서드를 호출.
6. AbstractNioChannel.doRegister 메소드는 javaChannel().register(eventLoop().selector, 0, this)를 통해 Channel에 해당하는 Java NIO SockerChannel을 eventLoop의 Selector에 등록한다.
일반적으로 채널 등록 프로세스에서 수행하는 작업은 채널을 해당 EventLoop과 연결하는 것이므로 Netty에서 각 채널이 특정 EventLoop과 연결되고이 채널의 모든 IO 작업이 이 EventLoop에서 실행된다.
Channel과 EventLoop이 연결되면 기본 Java NIO SocketChannel의 register 메서드를 계속 호출하여 기본 Java NIO SocketChannel을 지정된 Selector에 등록한다.
Netty의 강력하고 유연한 기능은 Pipeline 기반의 커스텀 핸들러 메커니즘이다. 이를 기반으로 플러그인 추가와 같은 다양한 핸들러를 자유롭게 결합하여 비즈니스 로직을 완성 할 수 있다.
위의 사용 예제에서
... .handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception { ChannelPipeline p = ch.pipeline();
if (sslCtx != null) {
p.addLast(sslCtx.newHandler(ch.alloc(), HOST, PORT));
}
//p.addLast(new LoggingHandler(LogLevel.INFO));
p.addLast(new EchoClientHandler());
}
});
Bootstrap.handler 메소드가 ChannelHandler를 받고 전달하는 것은 ChannelHandler 인터페이스를 구현하는 ChannelInitializer에서 파생된 익명 클래스이다.
@Sharable public abstract class ChannelInitializer<C extends Channel> extends ChannelInboundHandlerAdapter {
private static final InternalLogger logger = InternalLoggerFactory.getInstance(ChannelInitializer.class);
protected abstract void initChannel(C ch) throws Exception;
@Override
@SuppressWarnings("unchecked")
public final void channelRegistered(ChannelHandlerContext ctx) throws Exception {
initChannel((C) ctx.channel());
ctx.pipeline().remove(this);
ctx.fireChannelRegistered();
}
...
}
ChannelInitializer는 추상 클래스이고 추상메소드인 initChannel이 있다. 이 메소드를 구현하고 이 메소드에 사용자 정의 핸들러를 추가했다. initChannel메소드는 ChannelInitializer.channelRegistered 메소드에서 호출된다. channelRegistered 메서드에서 initChannel 메서드가 호출되고 사용자 정의 핸들러가 ChannelPipeline에 추가된 다음 ctx.pipeline() remove로 ChannelPipeline에서 자신을 삭제한다.
public final class EchoServer {
static final boolean SSL = System.getProperty("ssl") != null;
static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));
public static void main(String[] args) throws Exception {
// Configure SSL.
final SslContext sslCtx;
if (SSL) {
SelfSignedCertificate ssc = new SelfSignedCertificate();
sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build(); } else {
sslCtx = null;
}
// Configure the server.
EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception
{
ChannelPipeline p = ch.pipeline();
if (sslCtx != null) {
p.addLast(sslCtx.newHandler(ch.alloc()));
}
//p.addLast(new LoggingHandler(LogLevel.INFO));
p.addLast(new EchoServerHandler());
}
});
// Start the server.
ChannelFuture f = b.bind(PORT).sync();
// Wait until the server socket is closed.
f.channel().closeFuture().sync();
} finally {
// Shut down all event loops to terminate all threads.
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
위의 BootStrap 코드에 비해 큰 차이는 없으며 기본적으로 다음과 같은 부분이 초기화된다.
1. EventLoopGroup : 서버이든 클라이언트이든 EventLoopGroup을 지정해야한다. 이 예제에서는 NIO EventLoopGroup을 나타내는 NioEventLoopGroup이 지정되어 있지만, 서버는 두 개의 EventLoopGroup을 지정해야한다. 하나는 클라이언트의 연결 요청을 처리하는 데 사용되는 bossGroup이다. 다른 하나는 workerGroup으로 각 클라이언트에 연결된 IO 작업을 처리하는 데 사용
2. ChannelType : Channel의 종류를 지정하며 서버 측이므로 NioServerSocketChannel을 사용
3. 핸들러 : 데이터 프로세서를 설정한다.
클라이언트의 채널 초기화 프로세스를 분석할 때 이미 채널이 Java의 기본 소켓 연결을 추상화한 것으로 언급했으며 클라이언트 채널의 특정 유형이 NioSocketChannel이므로 당연히 서버 측 채널 유형이 NioServerSocketChannel이라는 것을 알고 연결한다.
동일한 분석 루틴으로 클라이언트에서 Channel의 유형은 초기화될 때 실제로 Bootstrap.channel() 메서드에 의해 설정된다.
NioServerSocketChannel의 인스턴스화가 BootstrapChannelFactory 팩토리 클래스를 통해 수행되고 BootstrapChannelFactory의 clazz가 수행되는지 확인할 수 있다. 이 필드는 NioServerSocketChannel.class로 설정되어 있으므로 BootstrapChannelFactory.newChannel()이 호출될 때 NioServerSocketChannel의 인스턴스를 얻는다.
@Override public T newChannel() {
return clazz.newInstance();
}
요약 해보자
1. ServerBootstrap의 ChannelFactory 구현은 BootstrapChannelFactory이다.
2. 생성된 Channel의 특정 유형은 NioServerSocketChannel이다. Channel
의 인스턴스화 프로세스는 실제로 호출되는 ChannelFactory.newChannel 메소드이며 인스턴스화 된 Channel의 특정 유형은 ServerBootstrap이 초기화될 때 전달되는 channel() 메소드의 매개 변수이다. 따라서이 예에서 서버 측의 ServerBootstrap의 경우 생성된 Channel 인스턴스는 NioServerSocketChannel이다.
NioSocketChannel과 유사하게 생성자는 newSocket을 호출하여 Java NIO 소켓을 열지만, Bootstrap의 newSocket은 openSocketChannel을 호출하고 ServerBootStrap의 newSocket은 openServerSocketChannel을 호출한다.
private static ServerSocketChannel newSocket(SelectorProvider provider) {
return provider.openServerSocketChannel();
}
public NioServerSocketChannel() { this(newSocket(DEFAULT_SELECTOR_PROVIDER));
}
public NioServerSocketChannel(ServerSocketChannel channel) {
super(null, channel, SelectionKey.OP_ACCEPT);
config = new NioServerSocketChannelConfig(this, javaChannel().socket());
}
이 구조에서 상위 클래스 생성자를 호출할 때 전달된 매개 변수는 SelectionKey.OP_ACCEPT이다. 비교를 위해 Bootstrap의 채널이 초기화될 때 전달된 매개 변수는 SelectionKey.OP_READ이다. 우리는 Selector를 사용하여 I/O 멀티플렉싱을 수행한다. 처음에는 서버가 클라이언트의 연결 요청을 수신해야 하므로 여기서 SelectionKey를 설정한다. OP_ACCEPT 즉, 클라이언트의 연결 요청에 관심이 있음을 Selector에게 알린다.
그런 다음 부모 클래스 NioServerSocketChannel, AbstractNioMessageChannel, AbstractNioChannel, AbstractChannel의 생성자를 단계별로 호출한다.
마찬가지로 unsafe 파이프라인은 AbstractChannel에서 인스턴스화 된다.
protected AbstractChannel(Channel parent) {
this.parent = parent;
unsafe = newUnsafe();
pipeline = new DefaultChannelPipeline(this);
}
@Override
protected AbstractNioUnsafe newUnsafe() {
return new NioMessageUnsafe();
}
ServerBootstrap에서 unsafe 필드는 실제로 AbstractNioMessageChannel - AbstractNioUnsafe의 인스턴스이다. NioServerSocketChannsl의 인스턴스화 중에 수행해야 하는 작업을 요약해보자.
1. NioServerSocketChannel.newSocket (DEFAULT_SELECTOR_PROVIDER)를 호출하여 새 Java NIO ServerSocketChannel을 연다.
2. AbstractChannel (Channel parent)에서 AbstractChannel의 속성을 초기화한다. 상위 속성이 null로 설정되고 unsafe는 newUnsafe()를 통해 unsafe 객체를 인스턴스화한다. 유형은 AbstractNioMessageChannel - AbstractNioUnsafe 내부 클래스이다. 파이프라인은 새 DefaultChannelPipeline (this)의 새로 생성된 인스턴스이다.
3. AbstractNioChannel의 속성
- SelectableChannel ch는 NioServerSocketChannel - newSocket에서 반환한 Java NIO ServerSocketChannel 인 Java ServerSocketChannel로 설정된다.
- readInterestOp는 SelectionKey.OP_ACCEPT로 설정된다.
- SelectableChannel ch는 논블로킹 ch.configureBlocking(false) 로 구성된다.
4. NioServerSocketChannel의 속성
- ServerSocketChannelConfig 구성 = new NioServerSocketChannelConfig (this, javaChannel().socket())
서버와 클라이언트의 ChannelPipeline 초기화는 동일
서버와 클라이언트의 채널 등록 프로세스는 동일
클라이언트 측에서는 하나의 EventLoopGroup 객체만 제공하는 반면, 서버 측 초기화에서는 두 개의 EventLoopGroup을 설정한다. 하나는 bossGroup이고 다른 하나는 workerGroup이다.
따라서 이 두 EventLoopGroup은 무엇일까? bossGroup은 서버 측 수락, 즉 클라이언트 연결 요청을 처리하는 데 사용된다. workerGroup은 실제로 작업을 수행한다. 그들은 클라이언트 연결 채널의 IO 작업을 담당한다.
먼저 서버 측 bossGroup은 클라이언트 연결이 있는지 지속해서 모니터링하고 새로운 클라이언트 연결이 발견되면 bossGroup은이 연결에 대한 다양한 리소스를 초기화한 다음 workerGroup에서 EventLoop을 선택하여이 클라이언트에 바인딩한다.
먼저 ServerBootstrap이 초기화되면 b.group (bossGroup, workerGroup)이 호출되어 두 개의 EventLoopGroups 를 설정한다.
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
super.group(parentGroup);
...
this.childGroup = childGroup;
return this;
}
우리는 프로그램을 시작하고 b.bind 메소드를 호출 포트 바인드 메소드는 다음 호출 체인을 트리거한다.
bstractBootstrap.bind => AbstractBootstrap.doBind => AbstractBootstrap.initAndRegister
AbstractBootstrap.initAndRegister는 Bootstrap 분석시에도 보았다.
final ChannelFuture initAndRegister() {
final Channel channel = channelFactory().newChannel();
...
init(channel);
ChannelFuture regFuture = group().register(channel);
return regFuture;
}
여기서 group() 메서드는 위에서 언급한 bossGroup을 반환하고 여기에서 채널을 분석했다. 이것은 NioServerSocketChannel의 인스턴스이므로 group().register(channel)이 bossGroup과 NioServerSocketChannel이 연결되어 있다.
그렇다면 workerGroup은 NioSocketChannel과 연결되어 있을까?
@Override
void init(Channel channel) throws Exception {
...
ChannelPipeline p = channel.pipeline();
final EventLoopGroup currentChildGroup = childGroup;
final ChannelHandler currentChildHandler = childHandler;
final Entry<ChannelOption<?>, Object>[] currentChildOptions;
final Entry<AttributeKey<?>, Object>[] currentChildAttrs;
p.addLast(new ChannelInitializer<Channel>() {
@Override
public void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
ChannelHandler handler = handler();
if (handler != null) {
pipeline.addLast(handler);
}
pipeline.addLast(new ServerBootstrapAcceptor( currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
}
});
}
init 메소드는 ServerBootstrap에서 재정의되어있다. 파이프라인에 ChannelInitializer를 추가하고이 ChannelInitializer는 키 ServerBootstrapAcceptor 핸들러를 추가하는 것을 볼 수 있다. ServerBootstrapAcceptor 클래스에 집중해보자. channelRead 메서드는 ServerBootstrapAcceptor에서 재정의되며 기본 코드는 다음과 같다.
@Override
@SuppressWarnings("unchecked")
public void channelRead(ChannelHandlerContext ctx, Object msg) {
final Channel child = (Channel) msg;
child.pipeline().addLast(childHandler);
...
childGroup.register(child).addListener(...);
}
ServerBootstrapAcceptor의 childGroup은이 객체가 들어오는 currentChildGroup이고 workerGroup이며 Channel은 NioSocketChannel의 인스턴스이므로 여기서 childGroup.register는 workerGroup의 EventLoop를 NioSocketChannel과 연결하는 것이다.
이제 문제는 ServerBootstrapAcceptor.channelRead 메서드가 어떻게 호출되느냐인데 실제로 클라이언트가 서버에 연결할 때 Java 하단의 NIO ServerSocketChannel에 SelectionKey.OP_ACCEPT가 준비된 다음 NioServerSocketChannel.doReadMessages를 호출한다.
@Override protected int doReadMessages(List<Object> buf) throws Exception { SocketChannel ch = javaChannel().accept();
...
buf.add(new NioSocketChannel(this, ch));
return 1;
}
doReadMessages에서 새로 연결된 클라이언트의 SocketChannel은 javaChannel().accept()를 통해 얻은 다음 NioSocketChannel 이 인스턴스화 되고 NioServerSocketChannel 객체 (즉 this)가 전달된다. 이로부터 우리가 만든 NioSocketChannel의 부모 채널을 볼 수 있다. 다음으로 Netty의 ChannelPipeline 메커니즘을 통해 읽기 이벤트가 단계별로 각 핸들러로 전송되고 앞서 언급한 ServerBootstrapAcceptor.channelRead 메소드가 트리거 된다.
서버 측 핸들러를 추가하는 과정은 클라이언트 측과 약간 다르다. EventLoopGroup와 마찬가지로 서버 측에도 두 개의 핸들러가 있다. 하나는 handler() 메소드를 통해 핸들러 필드를 설정하는 것이고 다른 하나는 childHandler()를 통해 childHandler 필드를 설정하는 것이다.
이전 bossGroup 및 workerGroup의 분석으로 우리는 추측 할 수 있는 부분이 있다. 핸들러 필드는 Acceptor 프로세스와 관련이 있다는 것이다. 즉, 이 핸들러는 클라이언트의 연결 요청을 처리하고 childHandler는 클라이언트와의 연결에 대한 IO 상호 작용을 담당한다.
@Override
void init(Channel channel) throws Exception {
...
ChannelPipeline p = channel.pipeline();
final EventLoopGroup currentChildGroup = childGroup;
final ChannelHandler currentChildHandler = childHandler;
final Entry<ChannelOption<?>, Object>[] currentChildOptions;
final Entry<AttributeKey<?>, Object>[] currentChildAttrs;
p.addLast(new ChannelInitializer<Channel>() {
@Override
public void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
ChannelHandler handler = handler();
if (handler != null) {
pipeline.addLast(handler);
}
pipeline.addLast(new ServerBootstrapAcceptor(
currentChildGroup, currentChildHandler, currentChildOptions,
currentChildAttrs));
}
});
}
위 코드의 initChannel 메서드에서 먼저 handler() 메서드를 통해 핸들러를 획득한다. 획득한 핸들러가 비어 있지 않으면 파이프라인에 추가한 다음 ServerBootstrapAcceptor의 인스턴스를 추가한다. 그런 다음 여기에서 handler() 메서드에 의해 반환된다. 실제로 핸들러 필드를 반환하며 이 필드는 서버 측 시작 코드에 설정된다.
b.group(bossGroup, workerGroup)
...
.handler(new LoggingHandler(LogLevel.INFO))
이때 파이프라인의 핸들러는 다음과 같다.
원래 클라이언트 분석 경험에 따르면 채널이 eventLoop에 바인딩 될 때 (여기서 NioServerSocketChannel은 bossGroup에 바인딩 됨) fireChannelRegistered 이벤트가 파이프라인에서 fire된 다음 ChannelInitializer.initChannel 메서드가 트리거되었다.
따라서 바인딩이 완료된 후 이 시점의 파이프라인은 다음과 같다.
이전에 bossGroup 및 workerGroup을 분석했을 때 ServerBootstrapAcceptor.channelRead에서 새로 생성된 채널에 대한 핸들러가 설정되고 eventLoop에 등록된다는 것을 이미 있다.
@Override
@SuppressWarnings("unchecked")
public void channelRead(ChannelHandlerContext ctx, Object msg) {
final Channel child = (Channel) msg;
child.pipeline().addLast(childHandler);
...
childGroup.register(child).addListener(...);
}
여기에서 childHandler는 서버 측 시작 코드에서 설정한 핸들러이다.
클라이언트가 채널 등록 연결되면 ChannelInitializer.initChannel 메서드가 호출되고 클라이언트 연결의 ChannelPipeline 상태는 다음과 같다.
마지막으로 서버 측 핸들러와 childHandler 간의 차이점과 연결을 요약 해보겠다.
1. 핸들러와 ServerBootstrapAcceptor가 서버 NioServerSocketChannel의 파이프라인에 추가된다.
2. 새로운 클라이언트 연결 요청이있을 때 ServerBootstrapAcceptor.channelRead는이 연결에 대한 NioSocketChannel을 생성하고 NioSocketChannel에 해당하는 파이프라인에 childHandler를 추가하고이 채널을 workerGroup의 eventLoop에 바인딩한다.
3. 핸들러는 수락 단계에서 작동하며 클라이언트의 연결 요청을 처리한다.
4. ChildHandler는 클라이언트 연결이 설정된 후에 작동하며 클라이언트 연결의 IO 상호 작용을 담당한다.
아래 그림을 사용하여 서버 측 핸들러 추가 프로세스를 요약한다.
Netty 초기화를 위한 BootStrap, ServerBootStrap에 대해 알아보았다. 글보다는 실제로 코드를 따라가면서 함께 보면 이해가 좀 더 쉽게 된다. 이번 글의 주요점은 실행순서를 보는 것과 Bootstrap과 ServerBootStrap의 차이를 보는 것이다.
BootStrap은 EventLoopGroup을 하나만 전달하고 ServerBootStrap은 두 개를 전달한다는 것이 차이점이다. ServerBootStrap 초기화에서 하나는 bossGroup으로 다른 하나는 workerGroup으로 설정된다. ServerBootStrap 측 bossGroup은 클라이언트 연결 여부를 지속해서 모니터링하고, 새로운 클라이언트 연결이 발견되면 bossGroup은이 연결에 대한 다양한 리소스를 초기화한 후 workerGroup에서 EventLoop을 선택하여이 클라이언트에 바인딩한다. 클라이언트가 연결되면 서버와 클라이언트 간의 후속 상호 작용이 모두 여기에 할당된 EventLoop에서 처리한다.
- https://segmentfault.com/a/1190000007282789
- https://www.slideshare.net/kslisenko/networking-in-java-with-nio-and-netty-76583794
- https://www.slideshare.net/JangHoon1/netty-92835335?from_action=save
- https://blog.csdn.net/zxhoo/article/details/17419229
- https://slowdev.tistory.com/16
- https://sina-bro.tistory.com/15
- https://github.com/YonghoChoi/develop-note/blob/master/md/Netty/3장_부트스트랩.md
- https://blog.csdn.net/zxhoo/article/details/17532857
- https://clairdelunes.tistory.com/26
- https://runningup.tistory.com/entry/부트스트랩-1
- https://juyoung-1008.tistory.com/23