Principio de comunicación concurrente del servicio Dubbo y análisis de código fuente

Dubbo usa netty para la comunicación TCP por defecto. TCP es un protocolo de capa de transporte. En la capa de aplicación, los protocolos personalizados a menudo se expanden. En primer lugar, puede manejar el pegado y desempaquetado del propio TCP y, en segundo lugar, puede acordar otros detalles del proceso de comunicación.
Así que dubbo adopta el protocolo dubbo personalizado por defecto. Descripción del documento:
el protocolo predeterminado de Dubbo utiliza una única conexión larga y comunicación asíncrona NIO, que es adecuada para un volumen de datos pequeño y llamadas de servicio simultáneas grandes, y la situación en la que la cantidad de máquinas consumidoras de servicios es mucho mayor que la cantidad de máquinas proveedoras de servicios.

El protocolo predeterminado se basa en la interacción netty3.2.5+hessian3.2.1.
Número de conexiones: conexión única
Método de conexión: conexión larga
Protocolo de transmisión: TCP
Método de transmisión: transmisión asíncrona NIO
Serialización: serialización binaria Hessian
Ámbito de aplicación: los paquetes de datos de parámetros entrantes y salientes son pequeños (se recomienda menos de 100K), los consumidores son más que proporcionados La cantidad de proveedores es grande y un solo consumidor no puede llenar el proveedor. Intente no usar el protocolo dubbo para transferir archivos grandes o cadenas muy grandes.
Escenario aplicable: llamada de método de servicio remoto regular

El procesamiento del flujo de bytes en el protocolo dubbo se encuentra en la clase com.alibaba.dubbo.remoting.exchange.codec.ExchangeCodec, que incluye la codificación y decodificación de la solicitud y la codificación y decodificación de la respuesta.
El protocolo dubbo utiliza un encabezado de mensaje de longitud fija (16 bytes) y un cuerpo de mensaje de longitud variable para la transmisión de datos.El encabezado del mensaje define alguna información que el marco de comunicación necesita cuando procesa hilos IO. El formato de mensaje específico del protocolo dubbo es el siguiente:
Explicación detallada del encabezado del mensaje del protocolo dubbo:
mágico: similar al número mágico en el archivo de código de bytes java, utilizado para determinar si es un paquete de datos del protocolo dubbo. El número mágico es una
bandera 0xdabb constante: bit de bandera, un total de 8 bits de dirección. Los cuatro bits inferiores se utilizan para indicar el tipo de herramienta de serialización utilizada para los datos del cuerpo del mensaje (arpillera predeterminada). Entre los cuatro bits superiores, el primer bit es 1, lo que significa que es una solicitud de solicitud, y el segundo bit es 1. , lo que significa transmisión bidireccional (es decir, hay una respuesta de retorno), y el tercer bit es 1, lo que indica que es un evento de ping de latido.
estado: bit de estado, establece el estado de respuesta de la solicitud, dubbo define algunos tipos de respuesta. Para tipos específicos, consulte com.alibaba.dubbo.remoting.exchange.Response
id de invocación: id de mensaje, tipo largo. El ID de identificación único de cada solicitud (debido al uso de comunicación asíncrona, se utiliza para hacer coincidir la solicitud de solicitud con la respuesta devuelta)
longitud del cuerpo: la longitud del cuerpo del mensaje, tipo int, es decir, cuántos bytes contiene el cuerpo El contenido tiene.

Debido a que se usa una única conexión larga, si el consumidor solicita desde varios subprocesos y el servidor regresa después de procesar el mensaje, se generará confusión en el mensaje. La idea de resolver este problema es similar a resolver el problema del paquete pegajoso en el zócalo.
Solución al problema del paquete pegajoso de socket: el más utilizado es en realidad definir un encabezado de paquete de datos de longitud fija, que contiene la longitud del paquete de datos completo, para completar el trabajo de desempaquetado del lado del servidor.
De manera similar, la forma en que Dubbo resuelve los problemas anteriores es agregar una identificación de identificador único global al encabezado, y el servidor también debe llevar esta identificación al responder a la solicitud, a fin de proporcionar pistas para que el cliente reciba los datos de respuesta correspondientes en múltiples hilos.

A continuación, analicemos directamente el código fuente:
en la clase HeaderExchangeChannel, echemos un vistazo al método de solicitud

public ResponseFuture request(Object request, int timeout) throws RemotingException {
   if (closed) {
       throw new RemotingException(this.getLocalAddress(), null, "Failed to send request " + request + ", cause: The channel " + this + " is closed!");
   }
   // create request.
   Request req = new Request();
   req.setVersion("2.0.0");
   req.setTwoWay(true);
   req.setData(request);

//这个future就是前面我们提到的:客户端并发请求线程阻塞的对象
   DefaultFuture future = new DefaultFuture(channel, req, timeout);
   try{
       channel.send(req);  //非阻塞调用
   }catch (RemotingException e) {
       future.cancel();
       throw e;
   }
   return future;
}

Preste atención al objeto ResponseFuture devuelto por este método. El subproceso que actualmente procesa la solicitud del cliente obtendrá el objeto ResponseFuture después de una serie de llamadas, y finalmente el subproceso se bloqueará en la llamada al método get() de este objeto, de la siguiente manera:

public Object get(int timeout) throws RemotingException {
    if (timeout <= 0) {
        timeout = Constants.DEFAULT_TIMEOUT;
    }
    if (! isDone()) {
        long start = System.currentTimeMillis();
        lock.lock();
        try {
            while (! isDone()) {    //无限连
                done.await(timeout, TimeUnit.MILLISECONDS);
                if (isDone() || System.currentTimeMillis() - start > timeout) {
                    break;
                }
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }
        if (! isDone()) {
            throw new TimeoutException(sent > 0, channel, getTimeoutMessage(false));
        }
    }
    return returnFromResponse();
}

He visto arriba que el hilo de solicitud ha sido bloqueado, entonces, ¿cómo se despierta? Ahora echemos un vistazo más de cerca a la definición de la clase HeaderExchangeHandler, primero observe el método recibido que define, el siguiente es un fragmento de código:

public void received(Channel channel, Object message) throws RemotingException {
    channel.setAttribute(KEY_READ_TIMESTAMP, System.currentTimeMillis());
    ExchangeChannel exchangeChannel = HeaderExchangeChannel.getOrAddChannel(channel);
    try {
        if (message instanceof Request) {
          .....
        } else if (message instanceof Response) {   
            //这里就是作为消费者的dubbo客户端在接收到响应后,触发通知对应等待线程的起点
            handleResponse(channel, (Response) message);
        } else if (message instanceof String) {
           .....
        } else {
            handler.received(exchangeChannel, message);
        }
    } finally {
        HeaderExchangeChannel.removeChannelIfDisconnected(channel);
    }
}

Principalmente nos fijamos en la rama condicional en el medio, que se utiliza para procesar el mensaje de respuesta, es decir, cuando el cliente dubbo recibe la respuesta del servidor, ejecutará esta rama y simplemente llamará al método handleResponse:

static void handleResponse(Channel channel, Response response) throws RemotingException {
    if (response != null && !response.isHeartbeat()) {  //排除心跳类型的响应
        DefaultFuture.received(channel, response);
    }
}

DefaultFuture aquí implementa el tipo de interfaz ResponseFuture que mencionamos anteriormente.
Continúe mirando los detalles de implementación del método DefaultFuture.received:

public static void received(Channel channel, Response response) {
    try {
        DefaultFuture future = FUTURES.remove(response.getId());
        if (future != null) {
            future.doReceived(response);
        } else {
            logger.warn("The timeout response finally returned at " 
                        + (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date())) 
                        + ", response " + response 
                        + (channel == null ? "" : ", channel: " + channel.getLocalAddress() 
                            + " -> " + channel.getRemoteAddress()));
        }
    } finally {
        CHANNELS.remove(response.getId());
    }
}

A través de la identificación de la bandera mencionada anteriormente, DefaultFuture.FUTURES puede obtener el objeto DefaultFuture específico, que es el objeto que mencionamos anteriormente que bloquea el hilo de solicitud. Después de encontrar el objetivo, llame a su método doReceived para activar el subproceso correspondiente para ejecutar:

private void doReceived(Response res) {
    lock.lock();
    try {
        response = res;
        if (done != null) {
            done.signal();
        }
    } finally {
        lock.unlock();
    }
    if (callback != null) {
        invokeCallback(callback);
    }
}

El código fuente anterior se resume en los siguientes pasos:
1. Un hilo del consumidor llama a la interfaz remota para generar una identificación única (como una cadena aleatoria, UUID, etc.) Dubbo usa AtomicLong para acumular números desde 0.
2. Encapsule la información de la llamada al método (como el nombre de la interfaz llamada, el nombre del método, la lista de valores de parámetros, etc.) y la devolución de llamada del objeto de devolución de llamada (es decir, el objeto ResponseFuture) del resultado del procesamiento para formar un objeto. Coloque (ID, objeto) en el ConcurrentHashMap global que almacena información de llamadas especialmente.
3. Encapsule el ID y la información de la llamada al método empaquetado en un objeto connRequest y envíelo de forma asincrónica a través de Netty.
4. El subproceso actual usa el método get() de devolución de llamada para tratar de obtener el resultado devuelto por el control remoto. Dentro de get(), use lock.lock() para obtener el bloqueo de la devolución de llamada del objeto de devolución de llamada, y luego verifique primero si el resultado se ha obtenido, si no, y luego llame al método wait() de la devolución de llamada para liberar el bloqueo en la devolución de llamada, de modo que el subproceso actual esté en un estado de espera.
5. Después de que el servidor recibe y procesa la solicitud, envía el resultado (el resultado contiene el ID anterior, es decir, el retorno) al cliente. El subproceso en la conexión de socket del cliente que escucha el mensaje recibe el mensaje, analiza el resultado, y toma Get the ID, y luego get(ID) del ConcurrentHashMap anterior para encontrar la devolución de llamada, y establece el resultado de la llamada al método en el objeto de devolución de llamada.
6. El subproceso de escucha luego usa lock.lock() para adquirir el bloqueo de la devolución de llamada del objeto de devolución de llamada, y luego llama al método signal() (similar a notificar a Todos()) para activar el subproceso que esperaba anteriormente para continuar con la ejecución. Hasta ahora, todo el proceso ha terminado.

 

El artículo es de mi blog:  http://www.iotjike.com/article/68

 

 

 

 

 

 

 

Supongo que te gusta

Origin blog.csdn.net/dreamer23/article/details/109512670
Recomendado
Clasificación