Cómo implementar un servidor http de alta concurrencia de 0 a 1

1. Introducción al servidor http

        Sabemos que el navegador es un cliente de http (s), el propósito es conectarse al servidor http remoto y luego el servidor devuelve los datos del navegador. El navegador recibe los datos y los muestra después de analizarlos.

        El rendimiento externo que vemos es que el navegador accede a una URL y luego obtiene la página web correspondiente. Durante este período, el navegador y el servidor http transmiten datos a través del protocolo http. La capa de transporte es el protocolo tcp porque es un protocolo conectado y confiable.

2. Herramientas necesarias

Si quieres hacer bien tu trabajo, ¡primero debes afilar tus herramientas! ! !

1. Una máquina virtual Linux (o servidor en la nube)

Aquí utilizo el servidor de aplicaciones ligero Alibaba Cloud. Si es posible, también puedes comprar un nombre de dominio.

2. Un editor de lenguaje C que se puede utilizar con soltura.

2.1 Estudio Visual 2022

Después de descargar e instalar, abra el instalador de Visual Studio para instalar el complemento para Linux.

Después de la instalación, cree el proyecto. 

Seleccione "Herramientas"---->Seleccione "Opciones"-------->Seleccione "Multiplataforma"----->Seleccione "Agregar" y luego ingrese:

  • Nombre de host: la dirección IP pública del servidor
  • Nombre de usuario: raíz
  • Contraseña: la contraseña que usted mismo estableció

 Haga clic derecho en el proyecto, seleccione "Propiedades" -----> seleccione "C/C++" y configure el compilador c o c++.

Utilice gcc para c y g++ para c++. Si el proceso de compilación descubre que no se puede encontrar, puede utilizar un directorio en su lugar. Por ejemplo, mi compilador de c usa directorios.

 Haga clic en Aceptar y podremos copiar el código local a una carpeta de proyectos en el servidor Linux (en el directorio raíz)

 2.2 Clión

La configuración de Clion es más complicada (es necesario instalar CMake, gdb, etc. en el servidor), pero creo que es relativamente fácil de usar.

El de la derecha es el archivo interno del servidor, el de abajo es el terminal remoto del servidor y el de arriba es el archivo abierto directamente desde el servidor.

 3. Implementar el servidor http

1. protocolo http

        Antes de escribir el código, debemos tener una comprensión general del proceso de trabajo del servidor http .

1.1 El cliente se conecta al servidor web 

        El navegador establece una conexión de socket TCP con el puerto HTTP del servidor web (el valor predeterminado es 80). Por ejemplo, http://www.raying.top

1.2Enviar  solicitud HTTP

        A través del socket TCP, el cliente envía un mensaje de solicitud de texto al servidor web.

1.3 El servidor acepta la solicitud y devuelve una respuesta HTTP

        El servidor web analiza la solicitud y localiza el recurso solicitado. El servidor escribe una copia del recurso en el socket TCP, que el cliente lee.

1.4 Liberar la conexión TCP

        Si el modo de conexión es cerrado, el servidor cierra activamente la conexión TCP y el cliente cierra pasivamente la conexión y libera la conexión TCP; si el modo de conexión es keepalive, la conexión se mantendrá durante un período de tiempo, tiempo durante el cual puede seguir recibiendo solicitudes.

1.5 El navegador del cliente analiza el contenido HTML

        El navegador del cliente primero analiza la línea de estado en busca de un código de estado que indique si la solicitud se realizó correctamente. Luego se analiza cada encabezado de respuesta y el encabezado de respuesta le indica al siguiente documento HTML varios bytes y el conjunto de caracteres del documento. El navegador del cliente lee los datos de respuesta HTML, los formatea según la sintaxis de HTML y los muestra en la ventana del navegador.

2. Solicitudes y respuestas

        Abra el navegador y analice la captura de paquetes para obtener una comprensión general de los encabezados de solicitud y de respuesta.

2.1 Encabezado de solicitud

2.2 Encabezado de respuesta 

 3. Implementar el código

3.1 Recibir solicitud http

3.1.1 Aquí puede leer el encabezado de la solicitud línea por línea y leer los datos del cliente carácter por carácter:

  • Parámetros: tamaño del búfer buf del socket del calcetín sizeof(buf)
  • Valor de retorno: =-1 Error de lectura =0 Leer una línea en blanco >0 Leer una línea correctamente

3.1.2 La función read()  aquí  está en el archivo de encabezado <unistd.h>, que es equivalente a la función recv() en <windows.h>.

//返回值: -1 表示读取出错, 等于0表示读到一个空行, 大于0 表示成功读取一行
int get_line(int sock, char *buf, int size) {
    int count = 0; // 已经读到的字符数
    char ch = '\0'; // 读到的字符
    int len = 0; // 已经读到的的长度
    while ((count < size - 1) && ch != '\n') {
        len = read(sock, &ch, 1); // 读取客户端发送的数据,1个字符一个字符读
        if (len == 1) { // 成功读到一个字符
            if (ch == '\r') { // 回车符号
                continue;
            } else if (ch == '\n') { // 换行符
                break;
            }
            buf[count] = ch; // 处理正常的字符,非回车换行符
            count++;
        } else if (len == -1) {    //读取出错
            perror("read failed");
            count = -1;     // 返回-1表示读取出错
            break;
        } else { // read 返回0,客户端关闭sock 连接.
            fprintf(stderr, "client close.\n");
            count = -1;
            break;
        }
    }
    if (count >= 0) buf[count] = '\0';
    return count;
}

3.2 Analizar la solicitud

Después de recibir la solicitud http, podemos analizarla de acuerdo con el protocolo http. La siguiente figura es el diagrama de ideas al analizar la solicitud. Puede utilizar esta idea para diseñar el código.

void *do_http_request(void *pclient_sock) {
    int len = 0;
    char buf[256];
    char method[64];
    char url[256];
    char path[256];
    int client_sock = *(int *) pclient_sock;

    struct stat st;

    /*读取客户端发送的http 请求*/
    //1.读取请求行
    len = get_line(client_sock, buf, sizeof(buf));

    if (len > 0) {//读到了请求行
        int i = 0, j = 0;
        while (!isspace(buf[j]) && (i < sizeof(method) - 1)) {
            method[i] = buf[j];
            i++;
            j++;
        }
        method[i] = '\0';
        if (debug) printf("request method: %s\n", method);

        if (strncasecmp(method, "GET", i) == 0) { //只处理get请求
            if (debug) printf("method = GET\n");

            //获取url
            while (isspace(buf[j++]));//跳过白空格
            i = 0;

            while (!isspace(buf[j]) && (i < sizeof(url) - 1)) {
                url[i] = buf[j];
                i++;
                j++;
            }
            url[i] = '\0';
            if (debug) printf("url: %s\n", url);

            //继续读取http 头部
            do {
                len = get_line(client_sock, buf, sizeof(buf));
                if (debug) printf("read: %s\n", buf);

            } while (len > 0);

            //***定位服务器本地的html文件***
            //处理url 中的?
            {
                char *pos = strchr(url, '?');    // 查找字符串中有无?
                if (pos) {
                    *pos = '\0';
                    printf("real url: %s\n", url);
                }
            }
//            sprintf(path, "./html_docs/%s", url);
            sprintf(path, "./resource/%s", url);

            if (debug) printf("path: %s\n", path);

            //执行http 响应
            //判断文件是否存在,如果存在就响应200 OK,同时发送相应的html 文件,如果不存在,就响应 404 NOT FOUND.
            if (stat(path, &st) == -1) {//文件不存在或是出错
                fprintf(stderr, "stat %s failed. reason: %s\n", path, strerror(errno));
                not_found(client_sock);
            } else {//文件存在
                if (S_ISDIR(st.st_mode)) {    // 判断路径是不是目录
                    strcat(path, "/index.html");     // 追加字符串index.html到结尾
                }
                do_http_response(client_sock, path);
            }
        } else {//非get请求, 读取http 头部,并响应客户端 501 	Method Not Implemented
            fprintf(stderr, "warning! other request [%s]\n", method);
            do {
                len = get_line(client_sock, buf, sizeof(buf));
                if (debug) printf("read: %s\n", buf);

            } while (len > 0);
            unimplemented(client_sock);   //请求未实现
        }
    } else {//请求格式有问题,出错处理
        bad_request(client_sock);   //在响应时再实现
    }
    close(client_sock);
    if (pclient_sock) free(pclient_sock);//释放动态分配的内存
    return NULL;
}

 Vale la pena señalar que aquí se agrega el uso de subprocesos múltiples , que se explicará en detalle más adelante.

3.3 Responder a solicitudes http

Después de permitir que el servidor reciba la solicitud http analizada, puede responder al navegador.

La siguiente figura es una idea para responder a solicitudes http:

void do_http_response(int client_sock, const char *path) {
    int ret = 0;
    FILE *resource = NULL;
    resource = fopen(path, "r");

    if (resource == NULL) {
        not_found(client_sock);
        return;
    }

    //1.发送http 头部
    ret = headers(client_sock, resource);

    //2.发送http body .
    if (!ret) {
        cat(client_sock, resource);
    }

    fclose(resource);
}

3.4 Enviar encabezado http

3.4.1 Al enviar el encabezado http, se deben pasar dos parámetros:

  • socket de cliente client_sock
  • archivo de recursos de recursos (obtenga el código de error de la transferencia del archivo)

3.4.2 Aquí se utiliza una función stat () para devolver la información de estado del archivo. Es necesario llamar a tres archivos de encabezado.   

        #incluye <sys/types.h> #incluye <sys/stat.h> #incluye <unistd.h>

        int stat(const char *ruta, struct stat *buf);

        parámetro:

        camino:

                    ruta de archivo

        buf:

                    El puntero pasado para guardar el estado del archivo se utiliza para guardar el estado del archivo.

        valor de retorno:

                    Devuelve 0 en caso de éxito, -1 en caso de error y establece errno

 3.4.3 Agregar información del servidor al buf mediante la función strcat()

3.4.4 Pasar información buf al socket del cliente

/****************************
 *返回关于响应文件信息的http 头部
 *输入:
 *     client_sock - 客服端socket 句柄
 *     resource    - 文件的句柄
 *返回值: 成功返回0 ,失败返回-1
******************************/
int headers(int client_sock, FILE *resource) {
    struct stat st;
    int fileid = 0; //文件传输错误代码
    char tmp[64];
    char buf[1024] = {0};

    strcpy(buf, "HTTP/1.0 200 OK\r\n");  //将src指针指向的字符串复制(替换)到buf指向的数组中
    strcat(buf, "Server: Ray Server\r\n");  //将src指针指向的字符串添加到dst指针指向的字符串后面
    strcat(buf, "Content-Type: text/html\r\n");
    strcat(buf, "Connection: Close\r\n");

    fileid = fileno(resource);

    if (fstat(fileid, &st) == -1) {     // 服务器内部出错了
        inner_error(client_sock);
        return -1;
    }

    snprintf(tmp, 64, "Content-Length: %ld\r\n\r\n", st.st_size);
    strcat(buf, tmp);

    if (debug) fprintf(stdout, "header: %s\n", buf);

    // 将文件内容发送给客户端socket,0是一个flag
    if (send(client_sock, buf, strlen(buf), 0) < 0) {
        fprintf(stderr, "send failed. data: %s, reason: %s\n", buf, strerror(errno));
        return -1;
    }
    return 0;
}

3.5 Enviar el archivo html especificado

 Además de pasar y enviar los parámetros pasados ​​en el encabezado http, también es necesario utilizar las siguientes tres funciones:

  • fgets(): lee los caracteres html en buf.
  • feof(): comprueba si se ha leído el final del archivo.
  • escribir(): El uso es similar a leer(). Envíe el archivo leído al cliente.
/****************************
 *说明:实现将html文件的内容按行
        读取并送给客户端
 ****************************/
void cat(int client_sock, FILE *resource) {
    char buf[1024];

    // 先读取一行并保存
    // 从 resource 流中读取 size 个字符存储到字符指针变量 buf 所指向的内存空间
    fgets(buf, sizeof(buf), resource);

    // feof()是检测流上的文件结束符的函数,如果文件结束,则返回非0值,否则返回0
    while (!feof(resource)) {
        int len = write(client_sock, buf, strlen(buf));

        if (len < 0) {//发送body 的过程中出现问题,怎么办?1.重试? 2.break
            fprintf(stderr, "send body error. reason: %s\n", strerror(errno));
            break;
        }
        if (debug) fprintf(stdout, "%s", buf);
        fgets(buf, sizeof(buf), resource);
    }
}

3.6 Manejo de errores

Para mejorar la solidez del código, debemos manejar algunos errores que se generan fácilmente.

Para que sea más fácil ver el efecto, no se escribe ninguna página de error especial. Envía el código html directamente al cliente.

 3.6.1 500 (Error interno del servidor) El servidor encontró un error y no pudo completar la solicitud.

void unimplemented(int client_sock) {
    const char *reply = "HTTP/1.0 501 Method Not Implemented\r\n\
Content-Type: text/html\r\n\
\r\n\
<HTML>\r\n\
<HEAD>\r\n\
<TITLE>Method Not Implemented</TITLE>\r\n\
</HEAD>\r\n\
<BODY>\r\n\
    <P>HTTP request method not supported.\r\n\
</BODY>\r\n\
</HTML>";

    int len = write(client_sock, reply, strlen(reply));
    if (debug) fprintf(stdout, reply);

    if (len <= 0) {
        fprintf(stderr, "send reply failed. reason: %s\n", strerror(errno));
    }
}

3.6.2 400 (Solicitud incorrecta) El servidor no comprende la sintaxis de la solicitud.

void bad_request(client_sock) {
    const char *reply = "HTTP/1.0 400 BAD REQUEST\r\n\
Content-Type: text/html\r\n\
\r\n\
<HTML>\r\n\
<HEAD>\r\n\
<TITLE>BAD REQUEST</TITLE>\r\n\
</HEAD>\r\n\
<BODY>\r\n\
    <P>Your browser sent a bad request!\r\n\
</BODY>\r\n\
</HTML>";

    int len = write(client_sock, reply, strlen(reply));
    if (len <= 0) {
        fprintf(stderr, "send reply failed. reason: %s\n", strerror(errno));
    }
}

3.6.3 Error interno del servidor

void inner_error(int client_sock) {
    const char *reply = "HTTP/1.0 500 Internal Sever Error\r\n\
Content-Type: text/html\r\n\
\r\n\
<HTML lang=\"zh-CN\">\r\n\
<meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\">\r\n\
<HEAD>\r\n\
<TITLE>Inner Error</TITLE>\r\n\
</HEAD>\r\n\
<BODY>\r\n\
    <P>服务器内部出错.\r\n\
</BODY>\r\n\
</HTML>";

    int len = write(client_sock, reply, strlen(reply));
    if (debug) fprintf(stdout, reply);

    if (len <= 0) {
        fprintf(stderr, "send reply failed. reason: %s\n", strerror(errno));
    }
}

3.6.4 404 (No encontrado) El servidor no puede encontrar la página web solicitada.

void not_found(int client_sock) {
    const char *reply = "HTTP/1.0 404 NOT FOUND\r\n\
Content-Type: text/html\r\n\
\r\n\
<HTML lang=\"zh-CN\">\r\n\
<meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\">\r\n\
<HEAD>\r\n\
<TITLE>NOT FOUND</TITLE>\r\n\
</HEAD>\r\n\
<BODY>\r\n\
	<P>文件不存在!\r\n\
    <P>The server could not fulfill your request because the resource specified is unavailable or nonexistent.\r\n\
</BODY>\r\n\
</HTML>";

    int len = write(client_sock, reply, strlen(reply));
    if (debug) fprintf(stdout, reply);

    if (len <= 0) {
        fprintf(stderr, "send reply failed. reason: %s\n", strerror(errno));
    }
}

 4. Prueba

1. Escribe la función principal.

int main(void) {
    int sock;
    struct sockaddr_in server_addr;
    sock = socket(AF_INET, SOCK_STREAM, 0);
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;    //选择协议IPV4
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//监听本地所有IP地址
    server_addr.sin_port = htons(SERVER_PORT);//绑定端口号
    bind(sock, (struct sockaddr *) &server_addr, sizeof(server_addr));
    listen(sock, 128);
    printf("等待客户端的连接\n");

    int done = 1;
    while (done) {
        struct sockaddr_in client;
        int client_sock, len, i;
        char client_ip[64];
        char buf[256];
        pthread_t id;
        int *pclient_sock = NULL;
        socklen_t client_addr_len;
        client_addr_len = sizeof(client);
        client_sock = accept(sock, (struct sockaddr *) &client, &client_addr_len);
        //打印客户端IP地址和端口号
        printf("client ip: %s\t port : %d\n",
               inet_ntop(AF_INET, &client.sin_addr.s_addr, client_ip, sizeof(client_ip)),
               ntohs(client.sin_port));

        /*处理http 请求,读取客户端发送的数据*/

        //启动线程处理http 请求
        pclient_sock = (int *) malloc(sizeof(int));
        *pclient_sock = client_sock;

        // 多线程
        pthread_create(&id, NULL, do_http_request, (void *) pclient_sock);
    }
    close(sock);
    return 0;
}

2. Inicie el servidor

3. Ingrese el nombre de dominio (o IP pública) y agregue el archivo html para probar

 Se puede encontrar que el servidor también imprime mucha información relacionada con el cliente.

 5. Código fuente del proyecto

OracleRay/MiniHttpServer (github.com) https://github.com/OracleRay/MiniHttpServer

Supongo que te gusta

Origin blog.csdn.net/weixin_51418964/article/details/124282294
Recomendado
Clasificación