brunch

You can make anything
by writing

C.S.Lewis

by myner Nov 08. 2020

네티 ChannelPipeline

feat. 네티 Channel

Channel & ChannelPipeline 구성관계

ChannelHandlerContext로 구성된 양방향 연결 목록을 유지하고 있음을 알 수 있다. 연결 목록의 헤드는 HeadContext, 연결 목록의 끝은 TailContext, 각 ChannelHandlerContext는 연결된 목록이다. ChannelHandler.



protected AbstractChannel(Channel parent) {     

    this.parent = parent;     

    unsafe = newUnsafe();     

    pipeline = new DefaultChannelPipeline(this); 

}


AbstractChannel 생성자 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 생성자에서 먼저 연관된 Channel을 필드 채널에 저장 한 다음 두 개의 ChannelHandlerContext를 인스턴스화한다. 하나는 HeadContext 인스턴스 헤드이고 다른 하나는 TailContext 인스턴스 테일이다. 그런 다음 헤드와 테일은 서로를 가리켜 양방향을 형성한다. Linked list : HeadContext와 TailContext는 AbstractChannelHandlerContext로부터 상속 받고 ChannelHandler 인터페이스도 구현하기 때문에 Context와 Handler의 이중 속성을 갖는다.


채널 수명주기

Channel 인터페이스는 ChannelInboundHandler API와 밀접한 관련이있는 단순하지만 강력한 상태 모델을 정의한다.

채널의 4 가지 상태는 다음과 같다.


ChannelUnregistered

: 채널이 생성되었지만 EventLoop에 등록되지 않음

ChannelRegistered

. : 채널이 EventLoop에 등록되었음

ChannelActive

. 채널이 활성화되었음 (원격 노드에 연결됨).

이제 데이터를 송수신 할 수 있음

ChannelInactive

. 채널이 원격 노드에 연결되어 있지 않음


ChannelHandler의 수명주기

In 

ChannelHandler이 추가 

ChannelPipeline되거나 

ChannelPipeline이러한 작업 제거가 호출 된 시점 부터 이러한 각 메서드는 

ChannelHandlerContext매개 변수를 받아들인다.


ChannelHandler의 수명주기 방법 분석 :

handlerAdded: 때 ChannelHandler 받는 추가 ChannelPipeline 호출 될 때 매체


handlerRemoved 때부터 : ChannelPipeline 제거가 ChannelHandler 호출


exceptionCaught 될 때 프로세스 : ChannelPipeline 장해 발생이라고


Netty는 다음 두 가지 중요한 ChannelHandler 하위 인터페이스를 정의한다.

ChannelInboundHandler

—— 인바운드 데이터 및 다양한 상태 변경

ChannelInboundHandler인터페이스 수명주기의 메서드 , 데이터를 받거나 해당 상태가 변경되면 메서드가 호출된다.



ChannelOutboundHandler

처리 —— 아웃 바운드 데이터를 처리하고 모든 작업을 가로 챌 수 있다.

동작에 의해 생성 된 아웃 바운드 데이터  ChannelOutboundHandler 방법이다 처리 Channel , ChannelPipeline 및 ChannelHandlerContext 호출. ChannelOutboundHandler

강력한 기능 중 하나는 요청에 따라 작업 또는 이벤트를 연기 할 수있어 복잡한 방법을 통해 요청을 처리 할 수 있다는 것다.


예를 들어 원격 노드에 대한 쓰기가 일시 중단 된 경우 플러시를 연기하고 나중에 계속할 수 있다.


ChannelPromise 및 ChannelFuture :

ChannelOutboundHandler대부분의 메서드에는  ChannelPromise작업이 완료 될 때 알림을 받기 위해 매개 변수가 필요하다 . 서브 클래스의  ChannelPromise예  ChannelFuture와 같은 몇몇 쓰기 방법, 정의  setSuccess()하고  setFailure(), 그래서 ChannelFuture는 불변이다.


ChannelPipeline 인스턴스화 프로세스

채널을 인스턴스화 할 때 ChannelPipeline의 인스턴스화가 수반되며이 채널은이 ChannelPipeline과 연결한다. 이는 NioSocketChannel의 상위 클래스 AbstractChannel의 생성자에 의해 입증 될 수 있다.


채널을 인스턴스화 할 때 (EchoClient를 예로 들면 Channel은 NioSocketChannel), 파이프 라인 필드는 새로 생성 된 DefaultChannelPipeline 객체이다. 그러면 DefaultChannelPipeline의 생성 방법을 살펴 보겠다.


DefaultChannelPipeline의 생성 방법에서 들어오는 채널은 this.channel 필드에 할당되고 두 개의 특수 필드 인 tail 과 head 가 인스턴스화 된다.이 두 필드는 이중 연결 목록의 머리와 꼬리이다. 실제로 DefaultChannelPipeline에서는 노드로 AbstractChannelHandlerContext를 사용하는 이중 연결 목록이 유지된다.이 연결 목록은 Netty의 파이프 라인 메커니즘 구현의 핵심


헤드는 ChannelInboundHandler 를 구현하고 테일은 ChannelOutboundHandler 인터페이스를 구현하며 모두 ChannelHandlerContext 인터페이스를 구현하고 있으므로 헤드와 테일은 ChannelHandler와 ChannelHandlerContext 라고 할 수 있다 .


HeadContext(DefaultChannelPipeline pipeline) {     

    super(pipeline, null, HEAD_NAME, false, true);     

    unsafe = pipeline.channel().unsafe(); 

}


상위 클래스 AbstractChannelHandlerContext의 생성자를 호출하고 매개 변수 inbound = false, outbound = true를 전달한다. TailContext의 생성자는 HeadContext의 반대이다. 상위 클래스 AbstractChannelHandlerContext의 생성자를 호출하고 매개 변수 inbound = true, outbound를 전달한다. = false 즉, 헤더는 outboundHandler이고 tail은 inboundHandler이다.이 점에 관해서는 모든 사람이 각별한주의를 기울여야한다. 후속 분석에서는 인바운드와 아웃 바운드의 두 가지 속성을 반복적으로 사용하기 때문이다.


ChannelInitializer 추가


위 섹션에서 우리는 Channel의 구성을 분석했다. 처음에 ChannelPipeline에는 두 개의 ChannelHandlerContext (또한 ChannelHandler)가 포함되어 있지만이 파이프 라인은 특별한 기능을 수행 할 수 없다. 사용자 지정 ChannelHandler를 추가한다.


일반적으로 부트 스트랩을 초기화 할 때 사용자 지정 ChannelHandler를 추가한다. 익숙한 EchoClient를 예로 들어 보겠다.


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());      

        }  

});


핸들러가 호출되면 ChannelInitializer 객체가 전달되어 ChannelHandler를 초기화 할 수있는 initChannel 메서드를 제공한다. 초기화 과정은 무엇일까? 알아 보자


ChannelInitializer는 ChannelHandler를 구현하는데, ChannelPipeline에 언제 추가 되었습니까? 검색 후 Bootstrap.init 메서드에서 ChannelPipeline에 추가 된 것을 확인했다.


 ChannelHandler는 파이프 라인에 대한 핸들러 ()에 의해 반환 등) 핸들러 (에 의해 반환 된 핸들러가 실제로 핸들러에 의해 ChannelInitializer 인스턴스 집합이다 추가 우리는 부트 스트랩, 그래서 여기에 파이프 라인의 마지막에 삽입 된 ChannelInitializer이다 초기화 할 때

혼란 스러울 수 있다. 확실히 ChannelInitializer 인스턴스를 삽입했습다. ChannelPipeline의 이중 연결 목록에있는 요소가 ChannelHandlerContext 인 이유는 무엇입니까?이 질문에 답하기 위해 코드에서 계속해서 답을 찾아 보겠다.


p.addLast () 메서드는 연결된 목록의 끝에 ChannelInitializer를 삽입하기 위해 Bootstrap.init에서 호출된다.

@Override public ChannelPipeline addLast(EventExecutorGroup group, final String name, ChannelHandler handler) {     

    synchronized (this) {         

        checkDuplicateName(name);         

        AbstractChannelHandlerContext newCtx = new DefaultChannelHandlerContext(this, group, name, handler);         

        addLast0(name, newCtx);     

    }     


    return this; 

}


addLast 메서드에서 먼저 ChannelHandler의 이름이 반복되는지 확인하고 반복되지 않는 경우이 핸들러에 해당하는 DefaultChannelHandlerContext를 생성한다. 인스턴스와 연관된다 (컨텍스트에 해당 Handler 인스턴스를 보유하는 핸들러 속성이 있음). 핸들러가 동일한 이름을 갖는지 여부를 판별하는 방법은 매우 간단하다. Netty에 name2ctx Map 필드가 있고 키는 핸들러의 이름이며 값은 다음과 같다. 핸들러 자체이므로 다음 코드를 사용하여 핸들러 이름이 같은지 판단 할 수 있다.


private void checkDuplicateName(String name) {     

    if (name2ctx.containsKey(name)) {         

        throw new IllegalArgumentException("Duplicate handler name: " + name);     

    } 

}


위 코드에서 newCtx 객체가 새로 인스턴스화되고 핸들러가 생성자에 매개 변수로 전달되는 것을 볼 수 있다. 그런 다음 살펴 보겠다. 인스턴스화 된 DefaultChannelHandlerContext의 수수께끼는 무엇입니까?


먼저 생성자를 살펴보십시오.

DefaultChannelHandlerContext(         DefaultChannelPipeline pipeline, EventExecutorGroup group, String name, ChannelHandler handler) {     

    super(pipeline, group, name, isInbound(handler), isOutbound(handler));     

    if (handler == null) {         

        throw new NullPointerException("handler");     

    }     

    this.handler = handler; 

}


DefaultChannelHandlerContext의 생성자에서 두 가지 매우 흥미로운 메소드가 호출된다 : isInbound 및 isOutbound 이 두 메소드는 무엇을합니까?


private static boolean isInbound(ChannelHandler handler) {     

    return handler instanceof ChannelInboundHandler; 


private static boolean isOutbound(ChannelHandler handler) {     

    return handler instanceof ChannelOutboundHandler; 

}


소스 코드에서 볼 수 있듯이 핸들러가 ChannelInboundHandler 인터페이스를 구현하면 isInbound가 true를 반환한다. 마찬가지로 핸들러가 ChannelOutboundHandler 인터페이스를 구현하면 isOutbound가 true를 반환한다.


이 두 부울 변수는 상위 클래스 AbstractChannelHandlerContext에 전달된다. , 그리고 부모 클래스의 두 필드 인 inbound 및 outbound를 초기화한다 .


그러면 여기서 ChannelInitializer에 해당하는 DefaultChannelHandlerContext의 인바운드 및 인바운드 필드는 무엇입니까? 그런 다음 ChannelInitializer가 구현하는 인터페이스를 살펴보십시오. 다음은 ChannelInitializer의 것이다.


ChannelInitializer는 ChannelInboundHandler 인터페이스 만 구현하므로 여기에서 인스턴스화 된 DefaultChannelHandlerContext의 inbound = true 및 outbound = false가 명확하게 표시된다.


인바운드 및 아웃 바운드 필드가 아닌가요? 왜 그렇게 많이 분석해야합니까? 이 두 필드는 파이프 라인 이벤트의 흐름 및 분류와 관련되어 있으므로 매우 중요하지만 여기서 먼저 키를 판매 한 다음이 두 필드의 역할을 자세히 분석 할 것이다. 여기서 독자는 기억하기 만하면된다. Live, ChannelInitializer에 해당하는 DefaultChannelHandlerContext의 inbound = true, outbound = false를 수행 할 수 있다.


컨텍스트가 생성되면이 컨텍스트를 파이프 라인의 이중 연결 목록에 삽입한다.


private void addLast0(final String name, AbstractChannelHandlerContext newCtx) {         checkMultiplicity(newCtx);     


    AbstractChannelHandlerContext prev = tail.prev;     

    newCtx.prev = prev;     

    newCtx.next = tail;     

    prev.next = newCtx;     

    tail.prev = newCtx;     


    name2ctx.put(name, newCtx);     


    callHandlerAdded(newCtx); 

}


이 코드는 일반적인 이중 연결 목록 삽입 작업이다. addLast 메서드가 호출되면 Netty는 이중 연결 목록에서 꼬리 요소 앞의 위치에이 핸들러를 추가한다.


커스텀 ChannelHandler 프로세스 추가

이전 섹션에서는 ChannelInitializer가 파이프 라인에 삽입되는 방법을 분석했다. 다음으로 ChannelInitializer가 호출되는 위치, ChannelInitializer의 역할 및 사용자 정의 ChannelHandler가 파이프 라인에 삽입되는 방법에 대해 설명하겠다.


이미 Bootstrap의 신비한 빨간 표지 (클라이언트) 장의 채널 등록 프로세스 섹션 에서 채널 등록 프로세스를 분석 했다 . 여기서 간단히 검토한다.  


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에 등록하고 현재 Channel을 첨부 파일로 사용한다.  



사용자 정의 ChannelHandler를 추가하는 프로세스는 AbstractUnsafe.register0에서 발생한다.이 메서드에서는 pipeline.fireChannelRegistered() 메서드가 호출되며 구현은 다음과 같다.


@Override 

public ChannelPipeline fireChannelRegistered() {     

    head.fireChannelRegistered();     

    return this; 

}


head.fireChannelRegistered() 메서드를 호출하기 만하면된다.


head는 AbstractChannelHandlerContext 인스턴스이며 fireChannelRegistered 메서드를 재정의하지 않으므로 head.fireChannelRegistered는 실제로 AbstractChannelHandlerContext.fireChannelRegistered


@Override public ChannelHandlerContext fireChannelRegistered() {     

    final AbstractChannelHandlerContext next = findContextInbound();     

    EventExecutor executor = next.executor();     


    if (executor.inEventLoop()) {         

        next.invokeChannelRegistered();     

    } else {         

        executor.execute(new OneTimeTask() {             

            @Override             

            public void run() {                 

                next.invokeChannelRegistered();             

            }         

        });     

    }     


    return this; 

}



이 메소드의 첫 번째 문장은 Context를 얻기 위해 findContextInbound를 호출하는 것이다. 그래서 그것이 반환하는 Context는 정확히 무엇입니까? 후속 코드를 살펴 보겠다.


private AbstractChannelHandlerContext findContextInbound() {     

    AbstractChannelHandlerContext ctx = this;     

    do {         

        ctx = ctx.next;     

    } while (!ctx.inbound);     

    return ctx; 

}


당연히이 코드는 헤드에서 파이프 라인의 이중 연결 목록을 탐색 한 다음 인바운드 속성이 참인 ChannelHandlerContext의 첫 번째 인스턴스를 찾는다. 기억하십니까? 이전에 ChannelInitializer를 분석했을 때 인바운드 및 잉크를 분석하는 데 많은 펜과 잉크를 사용했다. 아웃 바운드 속성은 지금 여기에서 사용된다. ChannelInitializer는 ChannelInboudHandler를 구현하므로 해당 ChannelHandlerContext의 인바운드 속성이 true이므로 여기서 반환되는 값은 ChannelInitializer 인스턴스에 해당하는 ChannelHandlerContext이다. 즉,

인바운드 컨텍스트를 확보하면 invokeChannelRegistered 메소드가 호출된다.


private void invokeChannelRegistered() {    

     try {         

        ((ChannelInboundHandler) handler()).channelRegistered(this);     

    } catch (Throwable t) {         

        notifyHandlerException(t);     

    } 

}


각 ChannelHandler는 ChannelHandlerContext와 연관되어 있으며 ChannelHandlerContext를 통해 해당 ChannelHandler를 가져올 수 있음을 이미 강조했다. 


따라서 여기서 반환하는 handler()는 실제로 처음에 인스턴스화 한 ChannelInitializer 객체이고, ChannelInitializer.channelRegistered 메서드가 호출된다. 


@Override @SuppressWarnings("unchecked") public final void channelRegistered(ChannelHandlerContext ctx) throws Exception {    

    initChannel((C) ctx.channel());     

    ctx.pipeline().remove(this);     

    ctx.fireChannelRegistered(); 

}


initChannel 메서드는 우리에게 매우 친숙하다. 이것은 Bootstrap을 초기화 할 때 핸들러 메서드를 호출하여 전달 된 익명 내부 클래스에 의해 구현 된 메서드이다.

따라서이 메서드가 호출되면 사용자 지정 ChannelHandler가 파이프 라인에 삽입되고 이때 파이프 라인은 다음 그림과 같다.

사용자 정의 ChannelHandler가 추가되면 ChannelInitializer ChannelHandler( "ctx.pipeline(). remove(this)")가 삭제되므로 최종 파이프 라인은 다음과 같다.


ChannelHandler의 이름

pipeline.addXXX에 오버로드 된 버전이있는 addLast와 같은 오버로드 된 메서드가 있음을 확인했다.


ChannelPipeline addLast(String name, ChannelHandler handler);


첫 번째 매개 변수는 추가 된 핸들러의 이름을 지정한다 (정확하게는 ChannelHandlerContext의 이름이지만 일반적으로 핸들러를 설명의 객체로 사용하므로 핸들러 이름을 더 쉽게 이해할 수 있다). 따라서 핸들러 이름의 용도는 무엇입니까? 이름을 설정하지 않으면 핸들러는 어떤 이름을 갖게 될까요?


@Override public ChannelPipeline addLast(EventExecutorGroup group, final String name, ChannelHandler handler) {     

    synchronized (this) {         

        checkDuplicateName(name);         

        AbstractChannelHandlerContext newCtx = new DefaultChannelHandlerContext(this, group, name, handler);         

        addLast0(name, newCtx);     

    }     


    return this; 

}


첫 번째 매개 변수는 null로 설정되어 있으므로 신경 쓰지 않는다. 두 번째 매개 변수는 핸들러의 이름이다. 코드에서 알 수 있듯이 핸들러를 추가하기 전에 checkDuplicateName 메소드를 호출 하여 핸들러 이름이 추가 된 핸들러의 이름과 동일한 지 확인해야한다. 이름이 반복된다. checkDuplicateName 메소드는 앞서 언급 한 바 있다. 


핸들러의 이름이 반복되는지 여부를 판단하는 Netty의 기준은 매우 간단합다 .DefaultChannelPipeline 에는 Map <String, AbstractChannelHandlerContext> 유형의 name2ctx 필드가 있고 해당 키는 핸들러의 이름이며 값은이 핸들러에 해당하는 ChannelHandlerContext이다. 새로운 핸들러가 추가되면 name2ctx에 들어가게되므로 name2ctx에 이름이 포함되어 있는지 확인하십시오.


같은 이름의 핸들러가없는 경우이 핸들러에 대한 관련 DefaultChannelHandlerContext 객체가 생성 된 후 name과 newCtx가 생성된다. 키-값 쌍으로 name2Ctx에 넣으십시오.


핸들러 이름 자동 생성

다음과 같이 addLast 메서드를 호출하면 그러면 Netty는 generateName을 호출하여 핸들러의 이름을 자동으로 생성한다.


generateName는 호출 generateName0을 실제로 핸들러의 이름을 생성한다 


private static String generateName0(Class<?> handlerType) {     

    return StringUtil.simpleClassName(handlerType) + "#0"; 

}


자동으로 생성 된 이름에 대한 규칙은 매우 간단하다. 즉, 핸들러의 간단한 클래스 이름에 "# 0"을 더한 것이므로 EchoClientHandler의 이름은 "EchoClientHandler # 0"이며 디버그 창을 통해 확인할 수도 있다.


Pipeline의 이벤트 전송 메커니즘 정보

이전 장에서 우리는 AbstractChannelHandlerContext에 두 개의 부울 변수 인 inbound와 outbound가 있다는 것을 알고 있다. 이는 Context에 해당하는 핸들러 유형을 식별하는 데 사용된다.  


- inbound가 true이면 해당 ChannelHandler가 ChannelInboundHandler 메서드를 구현 함을 의미한다.  

- outbound가 true이면 해당 ChannelHandler가 ChannelOutboundHandler 메서드를 구현 함을 의미한다.


ChannelPipeline에서 전송하는 이벤트 유형에서 시작된다.
Netty 이벤트는 Inbound 및 Outbound 이벤트로 나눌 수 있다.


위 그림에서 알 수 있듯이 인바운드 이벤트와 아웃 바운드 이벤트의 흐름이 다르다. 인바운드 이벤트의 인기도는 아래에서 위로, 아웃 바운드는 위에서 아래로 그 반대이다. 그리고 인바운드 전송 방법은 해당 ChannelHandlerContext 를 호출하는 것디다. .fireIN_EVT() 메서드와 Outbound 메서드는 ChannelHandlerContext.OUT_EVT() 메서드 를 호출하여 전달된다 . 예를 들어 ChannelHandlerContext.fireChannelRegistered() 호출은 ChannelRegistered 인바운드를 다음 ChannelHandlerContext로 보내고 ChannelHandlerContext.bind 호출은 바인드 를 보낸다. 아웃 바운드 이벤트는 다음 ChannelHandlerContext에 제공된다.


인바운드 이벤트 전파 방법은 다음과 같다.

ChannelHandlerContext.fireChannelRegistered() ChannelHandlerContext.fireChannelActive() ChannelHandlerContext.fireChannelRead(Object) ChannelHandlerContext.fireChannelReadComplete() ChannelHandlerContext.fireExceptionCaught(Throwable) ChannelHandlerContext.fireUserEventTriggered(Object) ChannelHandlerContext.fireChannelWritabilityChanged() ChannelHandlerContext.fireChannelInactive() ChannelHandlerContext.fireChannelUnregistered()


Oubound 이벤트 전송 방법은 다음과 같다.

ChannelHandlerContext.bind(SocketAddress, ChannelPromise) ChannelHandlerContext.connect(SocketAddress, SocketAddress, ChannelPromise) ChannelHandlerContext.write(Object, ChannelPromise) ChannelHandlerContext.flush() ChannelHandlerContext.read() ChannelHandlerContext.disconnect(ChannelPromise) ChannelHandlerContext.close(ChannelPromise)


public class MyInboundHandler extends ChannelInboundHandlerAdapter {     

    @Override     

    public void channelActive(ChannelHandlerContext ctx) {         

        System.out.println("Connected!");         

        ctx.fireChannelActive();     

    } 


public clas MyOutboundHandler extends ChannelOutboundHandlerAdapter {     

    @Override     

    public void close(ChannelHandlerContext ctx, ChannelPromise promise) {         

        System.out.println("Closing ..");         

        ctx.close(promise);     

    } 

}


위의 예에서 MyInboundHandler는 channelActive 이벤트를 수신했으며, 처리 된 후 이벤트를 계속 전파하려면 ctx.fireChannelActive()를 호출해야한다.


채널의 아웃 바운드 작업

어떤 일이 일어나도록 요청한 후 Outbound 이벤트를 통해 알리게되며 Outbound 이벤트
의 전파 방향은 tail-> customContext-> head이다.

연결 이벤트를 예로 들어 아웃 바운드 이벤트의 전파 메커니즘을 분석해 보겠다.
먼저 사용자가 Bootstrap.connect 메소드를 호출하면 Connect 요청 이벤트 가 트리거된다.이 호출은 다음 호출 체인을 트리거한다.


Bootstrap.connect -> Bootstrap.doConnect -> Bootstrap.doConnect0 -> AbstractChannel.connect


추적을 계속하면 AbstractChannel.connect가 실제로 DefaultChannelPipeline.connect 메서드를 호출한다는 것을 알 수 있다.


@Override public ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {     

    return pipeline.connect(remoteAddress, promise); 

}


@Override public ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {     

    return tail.connect(remoteAddress, promise); 

}


보시다시피 아웃 바운드 이벤트 (여기서는 연결 이벤트)가 파이프 라인에 전달되면 실제로 꼬리에서 확산되기 시작하고


tail.connect는 실제로 AbstractChannelHandlerContext.connect 메서드를 호출한다.

@Override public ChannelFuture connect(         final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {     

    ...     

    final AbstractChannelHandlerContext next = findContextOutbound();     

    EventExecutor executor = next.executor();     

    ...     

    next.invokeConnect(remoteAddress, localAddress, promise);    

     ...     

    return promise; 

}


findContextOutbound() 이름에서 알 수 있듯이 그 기능은 현재 Context로 시작하여 파이프 라인에서 Context의 이중 연결 목록의 프런트 엔드를 찾아 Context의 첫 번째 아웃 바운드 속성(즉, ChannelOutboundHandler와 연결된 Context)을 찾은 다음 반환하는 것이다.


구현은 다음과 같다. 


private AbstractChannelHandlerContext findContextOutbound() {     

    AbstractChannelHandlerContext ctx = this;     

    do {         

        ctx = ctx.prev;     

    } while (!ctx.outbound);     


    return ctx; 

}


아웃 바운드 컨텍스트를 찾으면 invokeConnect 메소드를 호출한다.이 메소드는 컨텍스트와 연관된 ChannelHandler의 연결 메소드를 호출한다.


private void invokeConnect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) {     

    try {         

        ((ChannelOutboundHandler) handler()).connect(this, remoteAddress, localAddress, promise);     

    } catch (Throwable t) {         

        notifyOutboundHandlerException(t, promise);     

    } 

}


사용자가 ChannelHandler의 연결 메서드를 재정의하지 않으면 ChannelOutboundHandlerAdapter에 의해 구현 된 메서드가 호출된다.


@Override 

public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress,         SocketAddress localAddress, ChannelPromise promise) throws Exception {     

    ctx.connect(remoteAddress, localAddress, promise); 

}


ChannelOutboundHandlerAdapter.connect는 ctx.connect 만 호출하고이 호출은 다음으로 돌아간다.


Context.connect -> Connect.findContextOutbound -> next.invokeConnect -> handler.connect -> Context.connect


이러한 루프에서 연결 이벤트가 DefaultChannelPipeline의 이중 링크 목록의 헤드 노드 인 head로 전달 될 때까지 헤드로 전달되는 이유는 무엇입니까? head는 ChannelOutboundHandler를 구현하므로 해당 아웃 바운드 속성은 true이다.

따라서 연결시 메시지가 헤드에 전달 된 후 메시지는 처리를 위해 해당 ChannelHandler로 전달되고 헤드의 handler()가 헤드 자체를 반환한다.


@Override public ChannelHandler handler() {     

    return this; 

}


따라서 최종 접속 이벤트는 헤드에서 처리되며 헤드의 접속 이벤트 처리 방법은 다음과 같다.


@Override public void connect(         ChannelHandlerContext ctx,         SocketAddress remoteAddress, SocketAddress localAddress,         ChannelPromise promise) throws Exception {     

    unsafe.connect(remoteAddress, localAddress, promise); 

}


이 시점에서 전체 Connect 요청 이벤트가 종료되었다.

Connect request 이벤트를 예로 들어 아웃 바운드 이벤트의 전파 프로세스를 분석하지만 실제로는 모든 아웃 바운드 이벤트 전파가 동일한 전파 법칙을 따른다. 


인바운드 이벤트

인바운드 이벤트 및 아웃 바운드 이벤트의 처리는 약간 미러링된다.

무언가가 발생한 후 인바운드 이벤트를 통해 알림을받는다. 인바운드는 일반적으로 채널 상태가 변경되거나 IO 이벤트가 준비 될 때 발생한다. 인바운드
의 특징은 전파 방향이 헤드라는 것이다. -> customContext-> 꼬리.


이제 위에서 Connect의 Outbound 이벤트를 분석 했으므로 Connect 이벤트 이후에 발생할 인바운드 이벤트를 분석하고 마지막으로 Outbound 이벤트와 Inbound 이벤트 간의 연결을 찾는다.


Connect Outbound가 안전하지 않은 것으로 전파되면 실제로 AbstractNioUnsafe.connect 메소드에서 처리된다.


@Override public final void connect(         final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {     

    ...     

    if (doConnect(remoteAddress, localAddress)) {         

        fulfillConnectPromise(promise, wasActive);     

    } else {        

         ...     

    }     

    ... 

}


AbstractNioUnsafe.connect에서는 실제 Socket 연결을 위해 doConnect 메서드가 먼저 호출되고, 연결되면 fulfillConnectPromise 메서드가 호출된다.


private void fulfillConnectPromise(ChannelPromise promise, boolean wasActive) {          ...    

    // Regardless if the connection attempt was cancelled, channelActive() event should be triggered,    

     // because what happened is what happened.     

    if (!wasActive && isActive()) {         

        pipeline().fireChannelActive();     

    }     

    ... 

}


fulfillConnectPromise에서 pipeline().fireChannelActive()를 호출하여 채널 활성화 메시지 (즉, 소켓 연결 성공)가 전송되는 것을 확인한다. 따라서 pipeline().fireChannelActive()가 호출되면 ChannelActive가 생성딘다. 인바운드 이벤트, 여기부터 시작하여이 인바운드 이벤트가 어떻게 확산되는지 살펴 보겠다.


@Override public ChannelPipeline fireChannelActive() {     

    head.fireChannelActive();     

    if (channel.config().isAutoRead()) {         

        channel.read();     

    }     


    return this; 

}


@Override public ChannelHandlerContext fireChannelActive() {     

    final AbstractChannelHandlerContext next = findContextInbound();     

    EventExecutor executor = next.executor();     

    ...     

    next.invokeChannelActive();     

    ...     

    return this; 

}


위의 코드는 익숙 할 것이다. Outbound 이벤트(예 : Connect 이벤트)를 전송하는 동안에도 유사한 작업이 수행된다.  


1. 먼저 findContextInbound를 호출하고 두 배로 연결된 Pipeline 목록에서 inbound가 true Context 인 첫 번째 속성을 찾은 다음  

2. 이 컨텍스트의 invokeChannelActive를 호출한다.


invokeChannelActive 메소드는 다음과 같다.


private void invokeChannelActive() {     

    try {         

    ((ChannelInboundHandler) handler()).channelActive(this);     

    } catch (Throwable t) {         

        notifyHandlerException(t);     

    } 

}


이 메서드는 Outbound의 해당 메서드 (예 : invokeConnect)와 동일하다. Outbound와 마찬가지로 사용자가 channelActive 메서드를 재정의하지 않으면 ChannelInboundHandlerAdapter의 channelActive 메서드가 호출된다.


@Override public void channelActive(ChannelHandlerContext ctx) throws Exception {     

    ctx.fireChannelActive(); 

}


마찬가지로 ChannelInboundHandlerAdapter.channelActive에서는 ctx.fireChannelActive 메서드 만 호출되므로 다음과 같은 루프가 있다.


Context.fireChannelActive -> Connect.findContextInbound -> nextContext.invokeChannelActive -> nextHandler.channelActive -> nextContext.fireChannelActive


같은 루프에서. 같은 방식으로 tail 자체는 ChannelInboundHandler 인터페이스와 ChannelHandlerContext 인터페이스를 모두 구현 하므로 channelActive 메시지가 tail로 전달되면 메시지가 처리를 위해 해당 ChannelHandler로 전달되고 tail의 handler()가 발생한다. 꼬리 자체를 반환한다.


@Override public ChannelHandler handler() {     

    return this; 

}


따라서 channelActive Inbound 이벤트는 마지막으로 꼬리에서 처리된다. 처리 방법을 살펴 보겠다.


@Override public void channelActive(ChannelHandlerContext ctx) throws Exception { }


TailContext.channelActive 메소드는 비어 있다. 독자가 TailContext의 Inbound 처리 메소드를 직접 확인하면 구현이 모두 비어 있음을 알게된다. Inbound 인 경우 사용자가 사용자 정의 프로세서를 구현하지 않을 때 표시되는 경우 기본값은 다음과 같다. 진행되지 않았다.

요약하자면

아웃 바운드 이벤트의 경우 :  

    1. 아웃 바운드 이벤트는 요청 이벤트이다 (요청은 Connect에 의해 시작되고 요청은 최종적으로 안전하지 않음에 의해 처리됨).  

2. 아웃 바운드 이벤트의 시작자는 채널이다.

3. 아웃 바운드 이벤트의 처리기가 안전하지 않는다.

4. 파이프 라인에서 아웃 바운드 이벤트의 전송 방향은 꼬리-> 머리이다.

5. ChannelHandler에서 이벤트를 처리 할 때이 Handler가 마지막 Hnalder가 아니면 ctx.xxx (예 : ctx.connect)를 호출하여이 이벤트의 전파를 계속해야한다. 이렇게하지 않으면이 이벤트의 전파가 조기에 종료된다.

6. 아웃 바운드 이벤트 흐름 : Context.OUT_EVT-> Connect.findContextOutbound-> nextContext.invokeOUT_EVT-> nextHandler.OUT_EVT-> nextContext.OUT_EVT


인바운드 이벤트의 경우 :  

    1. 인바운드 이벤트는 알림 이벤트로, 준비가되면 상위 계층에 알림이 전송된다.  

2. 인바운드 이벤트 개시자가 안전하지 않음

3. 인바운드 이벤트의 핸들러는 Channel이고, 사용자가 사용자 정의 처리 방법을 구현하지 않은 경우 인바운드 이벤트의 기본 핸들러는 TailContext이고 처리 방법은 빈 구현이다.

4. 파이프 라인에서 인바운드 이벤트의 전송 방향은 헤드-> 테일이다.

5. ChannelHandler에서 이벤트를 처리 할 때이 Handler가 마지막 Hnalder가 아닌 경우 ctx.fireIN_EVT (예 : ctx.fireChannelActive)를 호출하여이 이벤트의 전파를 계속해야한다. 그렇지 않으면이 이벤트의 전파가 조기에 종료된다.

6. 인 바운드 이벤트 흐름 : Context.fireIN_EVT-> Connect.findContextInbound-> nextContext.invokeIN_EVT-> nextHandler.IN_EVT-> nextContext.fireIN_EVT





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

https://zhuanlan.zhihu.com/p/70970558  

https://zhuanlan.zhihu.com/p/69554839 

https://blog.csdn.net/zxhoo/article/details/17920907 

https://blog.csdn.net/zxhoo/article/details/17964353 

https://blog.csdn.net/zxhoo/article/details/17264263 

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari