RPC Talk: problemas de conexión

RPC Talk: problemas de conexión

que es conexion

No existe tal cosa como una conexión en el mundo físico. Después de que los datos se convierten en una señal óptica/eléctrica, se envían de una máquina a otra. El dispositivo intermedio analiza la información de destino a través de la señal para determinar cómo reenviar la paquete. La llamada "conexión" en nuestra vida diaria es un concepto abstracto puramente artificial. El propósito es clasificar los datos sin estado transmitidos en diferentes sesiones con estado a través de un campo fijo como identificador, para facilitar la implementación de algunos estados dependientes. tareas en la capa de transporte.

Tomando TCP como ejemplo, el protocolo de enlace inicial de tres vías se usa para confirmar un número de secuencia inicial (Números de secuencia iniciales, ISN) en ambos lados. Este ISN marca una sesión TCP, y esta sesión tiene una tupla quíntuple exclusiva (IP de origen). IP, puerto de origen, dirección IP de destino, puerto de destino, protocolo de capa de transporte). En un sentido físico, una sesión TCP es equivalente a una ruta relativamente fija a un determinado servidor (es decir, un conjunto fijo de dispositivos físicos intermedios). Debido a esto, realizamos un control de congestión con estado y otras operaciones para que cada sesión TCP sea significativa. .

sobrecarga de conexión

A menudo escuchamos que la operación y el mantenimiento dirán que cierta máquina tiene demasiadas conexiones, por lo que hay fluctuaciones en el servicio. La mayoría de las veces, aceptaremos esta afirmación e intentaremos reducir la cantidad de conexiones. Sin embargo, rara vez pensamos en un problema. Cuando hay demasiadas conexiones a un servicio, la CPU, la memoria y la tarjeta de red en la máquina a menudo tienen muchos recursos libres. ¿Por qué todavía tiemblan? ¿Cuáles son los costos específicos de mantener una conexión?

Sobrecarga de memoria:

La pila de protocolos TCP generalmente la implementa el sistema operativo. Debido a que la conexión es un par con estado, el sistema operativo necesita guardar esta información de sesión en la memoria. La sobrecarga de memoria para cada conexión es inferior a 4 kb.

Ocupación del descriptor de archivo:

Desde la perspectiva de Linux, cada conexión es un archivo y ocupa un descriptor de archivo. La memoria ocupada por el descriptor de archivo se ha calculado en la sobrecarga de memoria anterior, pero para proteger su propia estabilidad y seguridad, el sistema operativo limitará la cantidad máxima de descriptores de archivo que se pueden abrir simultáneamente en todo el sistema y en cada proceso:

Configuración de la máquina: Linux 1 core 1 GB

$ cat /proc/sys/fs/file-max
97292

La configuración anterior de $ ulimit -n
1024
significa que todo el sistema operativo puede abrir hasta 97292 archivos al mismo tiempo, y cada proceso puede abrir hasta 1024 archivos al mismo tiempo.

Estrictamente hablando, un descriptor de archivo no es un recurso en absoluto, el recurso real es la memoria. Si tiene una necesidad clara, puede permitir que todas las aplicaciones pasen por alto esta limitación estableciendo un valor máximo.

Sobrecarga de hilo:

Algunas implementaciones de servidor más antiguas aún brindan servicios para cada conexión exclusivamente (recién creada u obtenida del grupo de conexiones) con un hilo. Para este tipo de servicio, además de la conexión en sí, también hay hilos fijos. Sobrecarga de memoria:

Configuración de la máquina: Linux 1 core 1 GB

número máximo de subprocesos del sistema operativo

$ cat /proc/sys/kernel/threads-max
7619

El número máximo de subprocesos en un solo proceso del sistema operativo, undef significa ilimitado

$ cat /usr/include/bits/local_lim.h
/* No tenemos un límite predefinido en el número de subprocesos. */
#undef PTHREAD_THREADS_MAX

El tamaño predeterminado de una pila de subprocesos individuales, en KB

$ ulimit -s
8192
En la máquina anterior, el número de subprocesos que se pueden crear está limitado por el valor de configuración del propio sistema operativo, por un lado, y por otro lado, por el tamaño de la memoria. Dado que 1024 MB / 8 MB = 128 > 7619, el número máximo de subprocesos que se pueden crear en esta máquina es 128. Si el servidor usa un subproceso y una conexión, entonces el servidor solo puede proporcionar servicios para hasta 128 conexiones al mismo tiempo.

Se puede ver que este modo de subproceso único de conexión única hará que la cantidad de conexiones se vea muy restringida por la cantidad de subprocesos, por lo que la mayoría de las implementaciones de servidor modernas abandonan este modo y permiten que un solo subproceso maneje las conexiones exclusivamente.

Problema C10K
A través de la discusión anterior, podemos ver que lo que realmente restringe el número de conexiones son esencialmente los recursos de memoria. Otras variables pueden omitirse modificando los parámetros predeterminados u optimizarse cambiando el diseño del software. Pero si es realmente tan simple, ¿por qué existe el famoso problema C10K?

En realidad, esto es puramente una cuestión de ingeniería de software, no de hardware. Cuando se diseñó el sistema operativo inicial, no consideró el problema de las conexiones de 10K o más en el futuro, por lo que la interfaz no estaba optimizada para tales escenarios, y el software de infraestructura (como Apache) encima, naturalmente. No hay consideración de tratar con tales escenarios.

Para los desarrolladores de software de aplicaciones, el sistema operativo es como la ley. Lo que podemos hacer no se basa únicamente en lo que el mundo físico puede hacer, sino también en lo que el sistema operativo nos permite hacer.

Cabe señalar que el problema C10K se refiere a la cantidad de conexiones, no a la cantidad de solicitudes. Si también se puede realizar 10K QPS (consulta por segundo) en una conexión, es por eso que generalmente no hay problema de C10K para las llamadas RPC en la intranet de la empresa. El problema de C10K a menudo ocurre en escenarios como servicios push y servicios de mensajería instantánea que necesitan establecer conexiones persistentes con una gran cantidad de clientes.

En el contexto de Linux, las conexiones se abstraen como archivos, por lo que la clave del problema C10K es si el diseño de la interfaz de E/S proporcionado en Linux puede manejar escenarios de conexión a gran escala y cómo usamos estas interfaces para implementar software que pueda soportar una alta concurrencia. arquitectura de conexiones.

La evolución histórica de Linux IO
Si queremos procesar múltiples conexiones (es decir, múltiples descriptores de archivos) a la vez, entonces el sistema operativo debe ser capaz de brindarnos una función de monitoreo por lotes, que nos permita monitorear múltiples descriptores de archivos al mismo tiempo. al mismo tiempo, ya través de El valor de retorno nos dice qué archivos se pueden leer/escribir, y luego podemos operar estos descriptores de archivos listos.

El trabajo fundamental de Linux IO no es más que lo anterior, no parece ser un diseño complicado, pero los diferentes métodos de implementación de esta parte del trabajo en la historia han afectado profundamente el desarrollo del software de aplicación posterior, y también es el diferencia central entre muchos programas básicos. .

select, 1993
select 的函数签名:

#define __FD_SETSIZE 1024

typedef struct
  {
    
    
    __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
  } fd_set;

int select (int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
          struct timeval *timeout)
使用方式:

// 初始化文件描述符数组
fd_set readfds;
FD_ZERO(&readfds);

// socket1, socket2 连接注册进 readfds
FD_SET(conn_sockfd_1, &readfds);
FD_SET(conn_sockfd_2, &readfds);

// 循环监听 readfds
while(1)
{
    
    
    // 返回就绪描述符的数目
    available_fd_count = select(maxfd, &readfds, NULL, NULL, &timeout);

    // 遍历当前所有文件描述符
    for(fd = 0; fd < maxfd; fd++)
    {
    
    
        // 检查是否可读
        if(FD_ISSET(fd, &readfds))
        {
    
    
            // 从 fd 读取
            read(fd, &buf, 1);
        }
    }
}

Los defectos de la función de selección en la conexión a gran escala se encuentran principalmente en los siguientes dos aspectos:

Atraviesa linealmente todos los descriptores de archivos, complejidad O(N):

La función de selección en sí no devuelve qué descriptores de archivo específicos están listos, y el usuario debe recorrer todos los descriptores de archivo por sí mismo y juzgar a través de FD_ISSET. El impacto no es grande cuando hay pocas conexiones, pero cuando el número de conexiones llega a 10K, el desperdicio causado por esta complejidad O(N) será muy exagerado.

límite de tamaño fd_set:

Detrás de la estructura fd_set hay un mapa de bits, cada bit representa un descriptor de archivo pero el estado está listo, 1 significa listo. El FD_SETSIZE predeterminado de Linux es 1024, es decir, el tamaño real es 1024/8bits = 128 bytes. Y esta parte de la memoria finalmente se copiará al estado del kernel, lo que también provocará una sobrecarga de copia.

Este diseño parece tosco, pero la ventaja es que puede manejar bien el problema de las colas. Si una conexión está particularmente ocupada, no afectará el rendimiento de la llamada al sistema en sí, porque solo le importa si está lista y no le importa cuántos datos están esperando para ser procesados. Ver más discusión sobre esto en el correo de Linus de 2000.

sondeo, 1998
La firma de la función de sondeo:

struct pollfd
  {
    
    
    int fd;             /* File descriptor to poll.  */
    short int events;   /* Types of events poller cares about.  */
    short int revents;  /* Types of events that actually occurred.  */
  };

int poll (struct pollfd *fds, nfds_t nfds, int timeout)
使用方式:

// 初始化 pollfd 数组
int nfds = 2
struct pollfd fds[fds_size];
fds[0].fd = STDIN_FILENO;
fds[0].events = POLLIN;
fds[1].fd = STDOUT_FILENO;
fds[1].events = POLLOUT;

// 监听 pollfd 数组内的文件描述符
poll(fds, nfds, TIMEOUT * 1000);

// 遍历 fds
for(fd = 0; fd < nfds; fd++)
{
    
    
    // 是否是读取事件
    if (fds[fd].revents & POLLIN)
    {
    
    
        // 从 fd 读取
        read(fds[fd].fd, &buf, 1);
    }
}

Hay dos diferencias principales entre sondear y seleccionar:

Unifique los tres tipos de eventos de readfds, writefds y exceptfds a través de pollfd.
Los descriptores de archivo que se supervisarán se pasan a través de la matriz pollfd[] y el número de descriptores de archivo ya no está limitado. (El núcleo convierte la matriz en una lista enlazada).
Pero en 1998, cuando se inventó la encuesta, la infraestructura de red a gran escala todavía no era un requisito común, por lo que este aumento de API no resolvió el problema antes mencionado de atravesar todos los descriptores de archivos en conexiones a gran escala. Pero algunas cosas sucedieron en 1999, justo después de que se publicara la encuesta:

El problema C10K se planteó oficialmente
y se lanzó HTTP 1.1. En esta versión, se introdujo el concepto de conexión persistente viva .
Se lanzó QQ
y alrededor de 2000, Internet 2C marcó el comienzo de la era de la gran explosión y la gran burbuja.

En la época en que se lanzó QQ, este problema no tenía solución, por lo que una aplicación orientada a sesiones como QQ abandonó el protocolo TCP orientado a sesiones y utilizó UDP.

epoll, 2003
Firma de la función epoll:

typedef union epoll_data {
    
    
    void    *ptr;
    int      fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

struct epoll_event {
    
    
    uint32_t     events;    /* Epoll events */
    epoll_data_t data;      /* User data variable */
};

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events,
                 int maxevents, int timeout);

Cómo utilizar:

#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd;

// 创建 epollfd 对象,后续 epoll 操作都围绕该对象
epollfd = epoll_create(10);

// 对 ev 绑定关心对 EPOLLIN 事件,并注册进 epollfd 中
ev.events = EPOLLIN;
ev.data.fd = listen_sock;
if(epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
    
    
   perror("epoll_ctl: listen_sock");
   exit(EXIT_FAILURE);
}

for(;;) {
    
    
    // 传入 events 空数组,阻塞等待直到一有就绪事件便返回,返回值为有效事件数
    nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
    if (nfds == -1) {
    
    
        perror("epoll_pwait");
        exit(EXIT_FAILURE);
    }

    // 只需要遍历有效事件即可
    for (n = 0; n < nfds; ++n) {
    
    
        if (events[n].data.fd != listen_sock) {
    
    
            //处理文件描述符,read/write
            do_use_fd(events[n].data.fd);
        } else {
    
    
            //主监听socket有新连接
            conn_sock = accept(listen_sock,
                            (struct sockaddr *) &local, &addrlen);
            if (conn_sock == -1) {
    
    
                perror("accept");
                exit(EXIT_FAILURE);
            }
            setnonblocking(conn_sock);
            
            //将新连接注册到 epollfd 中,并以边缘触发方式监听读事件
            ev.events = EPOLLIN | EPOLLET;
            ev.data.fd = conn_sock;
            if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,
                        &ev) == -1) {
    
    
                perror("epoll_ctl: conn_sock");
                exit(EXIT_FAILURE);
            }
        }
    }
}

epoll tiene las siguientes caracteristicas:

Cada descriptor de archivo solo se copiará una vez por epoll_ctl cuando se cree.
epoll_wait tiene solo parámetros limitados, lo que evita la copia frecuente del modo usuario al modo kernel.
epoll_wait solo devuelve descriptores de archivos listos, evitando atravesar todos los descriptores de archivos.
Por lo tanto, cuando el número de conexiones aumenta linealmente, el rendimiento de la propia llamada epoll no aumentará linealmente. La mayoría de los servidores modernos han recurrido al uso de epoll bajo la plataforma Linux.

Diseño de servicios de alta concurrencia
Podemos descomponer el problema C10K en los siguientes tres subproblemas:

  1. Cómo establecer de forma eficiente un gran número de conexiones (aceptar)
  2. Si lee y escribe una gran cantidad de conexiones de manera eficiente (lectura/escritura)
  3. Cómo manejar eficientemente una gran cantidad de solicitudes

La diferencia entre estos tres subproblemas es que el establecimiento de una conexión solo ocupará la CPU al principio, y solo los recursos de memoria se ocuparán después de que se establezca la conexión, y los recursos consumidos cada vez que se establezca una conexión son fijos. Sin embargo, las operaciones de lectura y escritura en cada conexión y el procesamiento de los datos solicitados con frecuencia consumirán recursos de memoria y CPU impredecibles, y las diferencias entre conexiones y solicitudes serán muy grandes.

Cómo establecer conexiones de manera eficiente

Dado que los recursos consumidos al establecer una conexión son fijos, asumiendo que se requiere x ms, si usamos un solo hilo, solo es responsable de escuchar el descriptor del archivo del puerto de escucha, creando una nueva conexión pero no responsable de leerlo y escribirlo. Entonces, la cantidad de conexiones que el subproceso puede crear por segundo debe ser 1000/x.

En términos generales, el consumo de crear una conexión en sí es muy pequeño, y un solo subproceso es suficiente para manejar 10K o incluso una concurrencia mayor.

Cómo leer y escribir conexiones de manera eficiente

Nos hemos asegurado de que el servicio pueda establecer nuevas conexiones de manera eficiente a través del paso anterior, pero no podemos estimar con precisión la carga de trabajo de las tareas de lectura y escritura para estas nuevas conexiones, por lo que necesitamos un grupo de subprocesos para usar epoll_wait para monitorear los eventos de conexión en lotes y realizar Operaciones reales de lectura y escritura. Pero las operaciones de lectura y escritura aquí involucran dos modos de notificación de epoll—disparador horizontal y disparador de borde.

Para seleccionar/sondear, se obtiene la lista de descriptores de archivos listos. Cada llamada solo verificará si se puede leer/escribir. Después de obtener los descriptores disponibles, lea y escriba, y luego continúe con la próxima vez que seleccione/sondee. Si no ha terminado de leer, la próxima vez que llame a select/poll, continuará volviendo al estado legible, mientras continúe leyendo, no hay problema. Si no está terminado, cuando vuelva al estado de escritura la próxima vez, puede continuar escribiendo. epoll también tiene este tipo de modo, y lo llamamos disparo de nivel de procesamiento.

El disparador de borde corresponde al disparador horizontal, y su nombre proviene del concepto de nivel:

Nivel activado:

| |
| | _

Activado por flanco:
____
| |
.| | _

// "." significa notificación de activación
La activación de Edge solo se notifica una vez cuando no hay datos y hay datos, y las llamadas posteriores devuelven False. Entonces, cuando reciba la notificación, debe leer todo el contenido del archivo a la vez. Y si una conexión está extremadamente ocupada, se producirá una inanición en este momento. Pero este tipo de fenómeno de inanición tiene poco que ver con epoll o edge triggering, sino que necesitamos considerar el equilibrio de lectura y escritura cuando implementamos el código. Si siempre intenta leer todo el contenido a la vez en el modo activado por nivel, todavía habrá hambre.

Para la gran mayoría de los mensajes de pequeño volumen, sin importar qué método de activación pueda leer rápidamente el mensaje, hay poca diferencia. Pero para mensajes de gran volumen, como video, el método de disparo horizontal hará que epoll_wait se despierte con frecuencia y habrá muchas más llamadas al sistema en comparación con el disparo de borde, por lo que el rendimiento será peor.

Cómo manejar las solicitudes de manera eficiente

Para la mayoría de las empresas, el procesamiento de la lógica empresarial es la operación que realmente consume recursos, por lo que no podemos colocar esta parte de la operación en el subproceso de IO, de lo contrario, afectará a otras conexiones monitoreadas en este subproceso. Por lo tanto, es necesario abrir un grupo de subprocesos de trabajo separado para procesar la lógica comercial en sí.

Al final del día, para tareas que son económicas y constantes, puede usar un solo hilo. Para tareas con consumo indeterminado, debe usar un grupo de subprocesos.

Arquitectura final
A través de la serie anterior de división de tareas, podemos obtener un modelo de servicio que admite alta concurrencia llamado Reactor maestro-esclavo en la industria:

 单线程                         线程池                         线程池

[Reactor principal] == nueva conexión ==> [Sub Reactor] <-- Datos --> [Hilo de trabajo]
Establecer una nueva conexión Procesamiento de lógica empresarial de conexión de lectura y escritura
Este modelo también es la implementación del marco Netty de Java y gnet de Go Base del marco.

El negocio de Internet tiene dividendos, y la transformación del software de infraestructura también tiene dividendos. Epoll es uno de los mayores dividendos de los últimos 10 años.

Agrupación de conexiones frente a multiplexación

Hay dos ideas básicas para la gestión de conexiones: agrupación de conexiones y multiplexación.

La conexión es el portador de transmisión de la solicitud, y una solicitud incluye un ida y vuelta, es decir, un tiempo RTT. El grupo de conexiones generalmente significa que cada conexión solo atiende una solicitud al mismo tiempo, es decir, solo habrá una solicitud en un RTT. En este momento, si hay una gran cantidad de solicitudes concurrentes, se debe usar un grupo de conexiones para gestionar el ciclo de vida. Pero la conexión en sí es full-duplex. Es posible enviar Solicitudes y Respuestas todo el tiempo. Este también es el significado de multiplexación, pero esto requiere que el protocolo de la capa de aplicación sea compatible para marcar una ID para cada paquete para dividir diferentes solicitudes y respuestas. .

Debido a que el protocolo HTTP 1.0 no marca los ID para cada solicitud, es imposible admitir la multiplexación en la implementación. En el protocolo HTTP 1.1 se añaden los conceptos de conexión Keep-Alive y Pipeline.Las solicitudes se pueden enviar de forma continua, pero el orden en que regresan las Respuestas debe ser el orden en que se enviaron, para reutilizar las conexiones tanto como sea posible. . Sin embargo, los requisitos de Pipeline para el orden de respuesta harán que si una determinada solicitud tarda mucho tiempo en procesarse, las devoluciones posteriores se seguirán acumulando. 1.1 Dado que es una revisión de 1.0, es poco probable que agregue demasiados cambios incompatibles. Pero en HTTP 2.0, la ID de transmisión se agrega para realizar la capacidad de multiplexación.

El protocolo Thrift también marcará su propia ID de número de serie al principio (8 ~ 12 bytes), por lo que también puede soportar bien la multiplexación.

Sin embargo, la mayoría de los principales protocolos de bases de datos, como Mysql, no admiten la multiplexación de conexiones, por lo que a menudo necesitamos configurar grupos de conexiones de bases de datos. La mayor parte del tiempo de la base de datos se consume en cálculos de CPU y E/S de disco. El uso del grupo de conexiones puede garantizar una cantidad limitada de solicitudes simultáneas, y las tareas se completan una por una. Si hay una situación ocupada, las solicitudes se bloquean principalmente del lado del cliente. Si se trata de un método de multiplexación, el cliente no tiene forma de estimar con precisión la capacidad de carga del servidor, y una gran cantidad de solicitudes seguirán siendo enviadas y bloqueadas en el lado de la base de datos, que a menudo es lo que no queremos. ver. Además, la forma de agrupación de conexiones suele ser más sencilla para la implementación de Client.

Server Push
Todos sabemos que la conexión en sí es full-duplex, y puede enviar cualquier mensaje entre el Cliente y el Servidor, sin mencionar que el mensaje de respuesta de una solicitud en sí es un Server Push literal. Entonces, ¿por qué HTTP 2.0 y algunos protocolos RPC anuncian que admiten Server Push?

Esta pregunta en sí misma es nuevamente una pregunta de ingeniería de software. Estamos acostumbrados a programar en el modo de entrada y salida, por lo que al diseñar el protocolo, rara vez consideramos la situación de que la entrada no tiene salida y la salida no tiene entrada. Server Push es una situación en la que no hay salida pero tampoco entrada.

En HTTP 1.1, un mensaje completo debería ser el siguiente:

=== Solicitud ===
GET /hello.txt HTTP/1.1
User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3
Host: www.example.com
Accept-Language: En mi

=== Respuesta ===
HTTP/1.1 200 OK
Fecha: lunes 27 de julio de 2009 12:28:53 GMT
Servidor: Apache
Última modificación: miércoles 22 de julio de 2009 19:15:56 GMT
ETag: “34aa387-d- 1568eb00”
Rangos de aceptación: bytes
Longitud del contenido: 51
Variación: Codificación de aceptación
Tipo de contenido: texto/sin formato

¡Hola, mundo! Mi carga útil incluye un CRLF final.
Cada respuesta debe corresponder a una solicitud, que es una llamada de función desde el lado del código. Solo imagine si el servidor está en la conexión en este momento y de alguna manera devuelve una respuesta que el cliente no solicitó, ¿qué se puede hacer en la implementación del código? El código no se llama en absoluto, por lo que, naturalmente, no hay lugar para escuchar y esperar esta respuesta y, naturalmente, no hay lugar para procesarla.

El llamado long polling implementado por generaciones posteriores sobre HTTP 1.1 no es más que utilizar las características de las conexiones largas para retrasar el envío de mensajes. No es tanto una nueva tecnología, no es un truco oportunista, pero de hecho es una solución útil en escenarios simples. Para solucionar completamente el problema se debe modificar el protocolo, por eso existen WebSocket y HTTP 2.0.

En HTTP 2.0, el marco especial PUSH_PROMISE está marcado para indicar que el mensaje no corresponde a la Solicitud, pero en la implementación real, se encontrará que incluso si el servidor puede enviar contenido al cliente, el cliente todavía necesita analizar el significado específico de diferentes mensajes. En el modo tradicional, el significado de una Respuesta está determinado por la Solicitud, pero ahora una Respuesta sin una Solicitud solo puede determinarse analizando su contenido. Esto ha llevado a que el proceso de realización de este análisis pueda ser definido por cada familia. Para los estándares de navegador, Server Push generalmente se usa para recursos estáticos, por lo que es necesario establecer un conjunto de estándares de almacenamiento en caché de recursos.

En grpc, aunque se usa HTTP 2.0 en la capa inferior, la función PUSH_PROMISE no se usa, porque para RPC, puedo tener múltiples retornos para una solicitud (el llamado modo de transmisión), pero no se puede decir que no hay devolución directa sin solicitud, de lo contrario, el procesamiento del usuario será más complicado e inconsistente. Ejemplo de grpc usando el modo de transmisión:

stream, err := streamClient.Ping(ctx)
err = stream.Send(request)
for {
    
    
    response, err := stream.Recv()
}

Se puede ver que Server Push nunca ha sido una tecnología nueva, porque esta función siempre ha estado disponible en TCP. Lo que nos falta es en realidad solo la especificación de operación en la capa de aplicación.

por fin

Desde el nacimiento de epoll hasta la solución del problema Server Push, no es difícil ver que la llamada nueva tecnología no es una tecnología en absoluto desde una perspectiva más macro, sino solo algunos cambios en el consenso acordado. Pero es este cambio de consenso el que puede llevar décadas.

La civilización se basa en el consenso, al igual que la tecnología. El progreso de la civilización depende de romper el viejo consenso de que las mujeres son incapaces de la virtud, al igual que el progreso de la tecnología.

Supongo que te gusta

Origin blog.csdn.net/kalvin_y_liu/article/details/130004037
Recomendado
Clasificación