Capítulo 10 Análisis del código fuente de Netty Core ②

Pipeline Handler HandlerContext para crear análisis de código fuente
ChannelPipeline Scheduling Handler análisis de código fuente

El propósito del análisis del código fuente
Netty ChannelPipeline, ChannelHandler y ChannelHandlerContext son componentes muy básicos. A partir del código fuente, analizamos cómo Netty diseñó estos tres componentes principales y analizamos cómo se crean y coordinan.

ChannelPipeline| ChannelHandler| ChannelHandlerContextIntroducción

1.1 La relación entre los tres

  1. Cada vez que ServerSocket crea una nueva conexión, se crea un Socket, correspondiente al cliente de destino.

  2. A cada Socket recién creado se le asignará una nueva ChannelPipeline (en adelante denominada pipeline)

  3. Cada ChannelPipeline contiene múltiples ChannelHandlerContext (en lo sucesivo denominado Context)

  4. Juntos forman una lista doblemente vinculada. Estos contextos se utilizan para ajustar el ChannelHandler (en adelante denominado manejador) que agregamos cuando llamamos al método addLast
    Inserte la descripción de la imagen aquí

  • En la figura anterior: ChannelSocket y ChannelPipeline son asociaciones uno a uno, y varios contextos dentro de la tubería forman una lista vinculada, y Context es solo una encapsulación del controlador.
  • Cuando llega una solicitud, ingresará a la tubería correspondiente al Socket y pasará a todos los controladores de la tubería. Sí, es el modo de filtro en el modo de diseño.

1.2 Función y diseño de ChannelPipeline

1)
Inserte la descripción de la imagen aquí
Parte del código fuente del diseño de la interfaz de la tubería
Inserte la descripción de la imagen aquí

Se puede ver que la interfaz hereda las interfaces inBound, outBound, Iterable, lo que significa que puede llamar al método de salida de datos y al método de entrada , y también puede atravesar la lista vinculada interna . Mire varios de sus métodos representativos. Todos son para las operaciones de inserción, adición, eliminación y reemplazo de la lista enlazada del controlador, similar a una lista enlazada. Mientras tanto, puede volver Channel (es decir Socket)
Inserte la descripción de la imagen aquí
. 1) en el documento de interfaz de tubería, se proporciona un mapa de
los datos de flujo de la tubería se inserta en la pila, la pila está tubería fluye
Inserte la descripción de la imagen aquí

Explicación de la imagen de arriba:

  • Esta es una lista de controladores. El controlador se utiliza para procesar o interceptar eventos entrantes y salientes. La canalización implementa una forma avanzada de filtros para que los usuarios puedan controlar cómo se manejan los eventos y cómo interactúan los controladores en la canalización.

  • La figura anterior describe la forma típica de un manejador de manejar eventos de E / S en la tubería. Los eventos de E / S son manejados por inboundHandler o outBoundHandler y se ChannelHandlerContext.fireChannelReadenvían a su manejador más cercano llamando al método.
    Inserte la descripción de la imagen aquí
    Inserte la descripción de la imagen aquí
    Inserte la descripción de la imagen aquí

  • El manejador entrante maneja los eventos entrantes en una dirección ascendente, como se muestra en la figura. El controlador entrante generalmente procesa los datos entrantes generados por el hilo de E / S en la parte inferior de la figura. Los datos entrantes generalmente se obtienen de, por ejemplo, SocketChannel.read (ByteBuffer).

  • Por lo general, una canalización tiene múltiples manejadores. Por ejemplo, un servidor típico tendrá los siguientes manejadores en la canalización de cada canal.
    Protocolo decodificador-convertir datos binarios en objetos Java.
    Protocolo codificar-convertir objetos Java en datos binarios.
    Controladores de lógica de negocios: ejecutan la lógica de negocios real (por ejemplo, acceso a la base de datos)

  • Su programa de negocios no puede bloquear el hilo, afectará la velocidad de E / S y luego afectará el rendimiento de todo el programa Netty. Si su programa de negocios es rápido, puede ponerlo en el hilo de E / S, de lo contrario, debe ejecutarlo de forma asincrónica. O agregue un grupo de subprocesos al agregar el controlador,
    por ejemplo:
    // Cuando se ejecuta la siguiente tarea, no bloqueará el subproceso IO. El subproceso ejecutado proviene del grupo de subprocesos del grupo
    pipeline.addLast (group, "handler", new MyBusinessLogicHandler () );
    O poner en taskQueue o scheduleTaskQueue

1.3 Función y diseño de ChannelHandler

  1. Código fuente
public interface ChannelHandler {

 //当把 ChannelHandler 添加到 pipeline 时被调用
 void handlerAdded(ChannelHandlerContext ctx) throws Exception;
 
//当从 pipeline 中移除时调用
 void handlerRemoved(ChannelHandlerContext ctx) throws Exception;
 
// 当处理过程中在 pipeline 发生异常时调用
@Deprecated
void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception;
}

El rol de ChannelHandler es manejar eventos IO o interceptar eventos IO, y reenviarlo al siguiente manejador ChannelHandler.
Cuando el controlador procesa eventos, se divide en entrante y saliente. Las operaciones en ambas direcciones son diferentes. Por lo tanto, Netty define dos subinterfaces para heredar ChannelHandler

2) ChannelInboundHandlerinterfaz de eventos entrantes
Inserte la descripción de la imagen aquí

  • channelActive se usa cuando Channel está activo;

  • Se llama a channelRead cuando se leen datos del canal.

  • Los programadores necesitan reescribir algunos métodos. Cuando ocurre un evento de interés, necesitamos implementar nuestra lógica de negocios en el método, porque cuando ocurra el evento, Netty volverá a llamar al método correspondiente.

3) ChannelOutboundHandlerInterfaz de evento saliente
Inserte la descripción de la imagen aquí

  • método de enlace, llamado cuando se solicita que el canal esté vinculado a una dirección local
  • método de cierre, llamado cuando el canal está cerrado
  • Las operaciones de salida son métodos similares de conexión y escritura de datos.

4) ChannelDuplexHandlerManejar eventos salientes y entrantes
Inserte la descripción de la imagen aquí

  • ChannelDuplexHandler implementa indirectamente la interfaz de entrada e implementa directamente la interfaz de salida.
  • Es una clase general que puede manejar eventos entrantes y salientes.

1.4 El papel y el diseño de ChannelHandlerContext

  1. Diagrama UML de ChannelHandlerContext
    Inserte la descripción de la imagen aquí
    ChannelHandlerContext hereda la interfaz de invocación del método de salida y la interfaz de invocación del método de entrada

1) ChannelOutboundInvokery una ChannelInboundInvokerparte de la fuente
Inserte la descripción de la imagen aquí
Inserte la descripción de la imagen aquí

  • Estos dos invocadores son para el método entrante o saliente, que consiste en envolver una capa en la capa externa del controlador entrante o saliente para lograr el propósito de interceptar y realizar algunas operaciones específicas antes y después del método

2) ChannelHandlerContextParte del código fuente
Inserte la descripción de la imagen aquí

  • ChannelHandlerContext no solo hereda sus dos métodos, sino que también define algunos de sus propios métodos
  • Estos métodos pueden obtener si el canal correspondiente, el ejecutor, el controlador, la canalización, el asignador de memoria y el controlador asociado en el contexto contextual se eliminan.
  • El contexto es envolver todo lo relacionado con el controlador, para que el contexto pueda operar fácilmente el controlador en la tubería

ChannelPipeline| ChannelHandler| ChannelHandlerContextProceso de creación

Hay 3 pasos para ver el proceso de creación:

  • Cuando se crea cualquier ChannelSocket, se creará una canalización al mismo tiempo.

  • Cuando el usuario o el sistema llama internamente al método add *** de la canalización para agregar un controlador, se crea un Contexto que envuelve este controlador.

  • Estos contextos forman una lista doblemente vinculada en la tubería.

2.1 Socket se crea cuando se crea Socket; en el constructor de AbstractChannel, la clase padre abstracta de SocketChannel

 protected AbstractChannel(Channel parent) {
        this.parent = parent; //断点测试
        id = newId();
        unsafe = newUnsafe();
        pipeline = newChannelPipeline(); 
    }

Depuración, puede ver que el código se ejecutará aquí y luego continuar rastreando

 protected DefaultChannelPipeline(Channel channel) {
        this.channel = ObjectUtil.checkNotNull(channel, "channel");
        succeededFuture = new SucceededChannelFuture(channel, null);
        voidPromise =  new VoidChannelPromise(channel, true);

        tail = new TailContext(this);
        head = new HeadContext(this);

        head.next = tail;
        tail.prev = head;
    }

Explicación:
1) Asigne el canal al campo del canal, utilizado para el canal de operación de tubería.
2) Cree un futuro y prometa una devolución de llamada asincrónica.
3) Crear una entrada es tailContextpara crear un tipo de entrada y de salida es de tipo headContext. Inserte la descripción de la imagen aquí
Inserte la descripción de la imagen aquí
4) Por último, dos Contexto conectados entre sí para formar una lista doblemente enlazada.
5) tailContext y HeadContext son muy importantes, todos los eventos en la tubería fluirán a través de ellos,

2.2 Crear contexto al agregar el procesador del controlador en add ** Mire el contexto creado por el método addLast de DefaultChannelPipeline, el código es el siguiente

@Override
    public final ChannelPipeline addLast(EventExecutorGroup executor, ChannelHandler... handlers) {
        if (handlers == null) { //断点
            throw new NullPointerException("handlers");
        }

        for (ChannelHandler h: handlers) {
            if (h == null) {
                break;
            }
            addLast(executor, null, h);
        }

        return this;
    }

Continuar depuración

public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
        final AbstractChannelHandlerContext newCtx;
        synchronized (this) {
            checkMultiplicity(handler);

            newCtx = newContext(group, filterName(name, handler), handler);//

            addLast0(newCtx);
            
            if (!registered) {
                newCtx.setAddPending();
                callHandlerCallbackLater(newCtx, true);
                return this;
            }

            EventExecutor executor = newCtx.executor();
            if (!executor.inEventLoop()) {
                newCtx.setAddPending();
                executor.execute(new Runnable() {
                    @Override
                    public void run() {
                        callHandlerAdded0(newCtx);
                    }
                });
                return this;
            }
        }
        callHandlerAdded0(newCtx);
        return this;
    }

Explicación

  1. Agregue un controlador a la canalización, el parámetro es el grupo de subprocesos, el nombre es nulo y el controlador es el controlador que pasamos nosotros o el sistema. Netty sincronizó este código para evitar que varios hilos causen problemas de seguridad. Los pasos son los siguientes:
  2. Compruebe si la instancia del controlador está compartida, si no es así, y si ya está siendo utilizada por otra canalización, luego arroje una excepción.
  3. Llamar al newContext(group, filterName(name, handler), handler)método, crear un contexto . Se puede ver a partir de esto que cada vez que agrega un controlador, se crea un contexto asociado.
  4. Llame al método addLast para agregar el contexto a la lista vinculada.
  5. Si el canal no se ha registrado con selecor, agregue este contexto a las tareas pendientes de esta canalización. Cuando se completa el registro, se llamará al método callHandlerAdded0 (el valor predeterminado es no hacer nada, el usuario puede implementar este método).
  6. En este punto, para el proceso de creación de tres objetos, usted sabe casi lo mismo. Como dije inicialmente, cada vez que se crea un ChannelSocket, se crea una tubería enlazada, una relación uno a uno y un nodo de cola y El nodo principal forma la lista vinculada inicial. tail es un controlador de tipo entrante y entrante, y head es un controlador de tipo entrante y saliente. Cuando se llama al método addLast de la canalización, se crea un Contexto de acuerdo con el controlador dado, y luego el Contexto se inserta al final de la lista vinculada (antes de la cola).

Inserte la descripción de la imagen aquí

ChannelPipeline El análisis del código fuente de cómo programar el controlador

Inserte la descripción de la imagen aquí
Inserte la descripción de la imagen aquí
Cómo DefaultChannelPipeline implementa estos métodos de disparo

1.1 DefaultChannelPipelinecódigo fuente

public class DefaultChannelPipeline implements ChannelPipeline {
@Override
    public final ChannelPipeline fireChannelActive() {
        AbstractChannelHandlerContext.invokeChannelActive(head);
        return this;
    }

    @Override
    public final ChannelPipeline fireChannelInactive() {
        AbstractChannelHandlerContext.invokeChannelInactive(head);
        return this;
    }

    @Override
    public final ChannelPipeline fireExceptionCaught(Throwable cause) {
        AbstractChannelHandlerContext.invokeExceptionCaught(head, cause);
        return this;
    }

    @Override
    public final ChannelPipeline fireUserEventTriggered(Object event) {
        AbstractChannelHandlerContext.invokeUserEventTriggered(head, event);
        return this;
    }

    @Override
    public final ChannelPipeline fireChannelRead(Object msg) {
        AbstractChannelHandlerContext.invokeChannelRead(head, msg);
        return this;
    }

    @Override
    public final ChannelPipeline fireChannelReadComplete() {
        AbstractChannelHandlerContext.invokeChannelReadComplete(head);
        return this;
    }

    @Override
    public final ChannelPipeline fireChannelWritabilityChanged() {
        AbstractChannelHandlerContext.invokeChannelWritabilityChanged(head);
        return this;
    }
}

Explicación: Se
puede ver que estos métodos son todos métodos entrantes, es decir, eventos entrantes, y el método estático también se denomina controlador de cabecera de tipo entrante. Estos métodos estáticos llamarán al método de la interfaz ChannelInboundInvoker de la cabeza, y luego llamarán al método real del controlador
Inserte la descripción de la imagen aquí
Inserte la descripción de la imagen aquí
Inserte la descripción de la imagen aquí

1.2 Mire el código fuente de implementación del método de fuego saliente de piepline

public class DefaultChannelPipeline implements ChannelPipeline {
 @Override
    public final ChannelFuture bind(SocketAddress localAddress) {
        return tail.bind(localAddress);
    }

    @Override
    public final ChannelFuture connect(SocketAddress remoteAddress) {
        return tail.connect(remoteAddress);
    }

    @Override
    public final ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress) {
        return tail.connect(remoteAddress, localAddress);
    }

    @Override
    public final ChannelFuture disconnect() {
        return tail.disconnect();
    }

    @Override
    public final ChannelFuture close() {
        return tail.close();
    }

    @Override
    public final ChannelFuture deregister() {
        return tail.deregister();
    }

    @Override
    public final ChannelPipeline flush() {
        tail.flush();
        return this;
    }

    @Override
    public final ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) {
        return tail.bind(localAddress, promise);
    }

    @Override
    public final ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {
        return tail.connect(remoteAddress, promise);
    }

    @Override
    public final ChannelFuture connect(
            SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) {
        return tail.connect(remoteAddress, localAddress, promise);
    }

    @Override
    public final ChannelFuture disconnect(ChannelPromise promise) {
        return tail.disconnect(promise);
    }
}

Descripción:

  1. Estas son todas implementaciones salientes, pero el controlador de cola de tipo saliente se llama para el procesamiento porque son eventos salientes.
  2. La salida comienza con la cola y la entrada comienza con la cabeza. Debido a que la salida se escribe de adentro hacia afuera, comenzando desde la cola, el controlador anterior se puede procesar para evitar que el controlador se pierda, como la codificación. Por el contrario, la entrada es, por supuesto, la entrada desde la cabeza hacia el interior, de modo que los controladores posteriores pueden procesar los datos de estas entradas. Como la decodificación. Entonces, aunque head también implementa la interfaz de salida, no inicia las tareas de salida desde head
    Inserte la descripción de la imagen aquí

2. Acerca de cómo programar, use una imagen para representar:
Inserte la descripción de la imagen aquí

Descripción:

  1. La canalización primero llamará al método estático fireXXX de Context, y pasará el Context
  2. Luego, el método estático llama al método de invocador del Contexto, y el método de invocador llama internamente al método real XXX del Manejador contenido en el Contexto. Una vez que finaliza la llamada, si necesita continuar pasándola, llame al método fireXXX2 del Contexto, y viceversa. Modelo de cadena de responsabilidad

Inserte la descripción de la imagen aquí

Publicados 138 artículos originales · ganado elogios 3 · Vistas 7218

Supongo que te gusta

Origin blog.csdn.net/weixin_43719015/article/details/105398597
Recomendado
Clasificación