首页 > 解决方案 > Web 服务器未接收到所有请求

问题描述

我有一个用 C 语言编写的简单 Web 服务器,当向浏览器提供带有一些图像的小型 HTML 文件时,它可以正常工作。当我尝试为一个包含更多具有不同内容类型的对象(如 css 和 js 文件)的更复杂的网站提供服务时,我发现我没有收到对正确加载 index.html 所需的许多对象的请求 - 浏览器一直在等待无限期地为主机。如果我刷新页面几次,最终一切都会正确加载并且我可以访问超链接。我注意到的另一件事是,通常不会将相同的文件发送回浏览器。

#include <sys/socket.h>
#include <sys/sendfile.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <time.h>
#include <stdbool.h>
#include <pthread.h>

bool writeDataToClient(int sckt, const void *data, int datalen)
{
    const char *pdata = (const char*) data;

    while (datalen > 0){
        int numSent = send(sckt, pdata, datalen, 0);
        if (numSent <= 0){
            if (numSent == 0){
                printf("The client was not written to: disconnected\n");
            } else {
                perror("The client was not written to");
            }
            return false;
        }
        pdata += numSent;
        datalen -= numSent;
    }

    return true;
}

bool writeStrToClient(int sckt, const char *str)
{
    return writeDataToClient(sckt, str, strlen(str));
}

int get_filename_and_method(char *str, char **buf1, char **buf2)
{   
    char *request = str;
    char *status_line;
    char *url;
    char *token = strtok(request, "\r\n");
    status_line = token;

    *buf1 = strtok(status_line, " ");
    if (strcasecmp(*buf1, "GET") != 0) return -1;

    url = strtok(NULL, " ");
    if (strncmp(url, "/", strlen("/")) != 0) return -1;

    if (strlen(url) == 1) strcat(url, "index.html");
    if (url[strlen(url) - 1] == '/') strcat(url, "index.html");

    char *tmp = strdup(url);
    strcpy(url, "web");
    strcat(url, tmp);
    *buf2 = url;

    free(tmp);

    return 0;
}

int get_connection_type(char *str, char **buf)
{   
    char *req = str;
    char *token = strtok(req, "\r\n");
    char *connection;

    while (token != NULL)
    {   

        if (strncmp(token, "Connection:", 11) == 0)
        {   
            connection = token;
            strtok(connection, " ");
            if (strcasecmp(strtok(NULL, " "), "Keep-Alive") == 0)
            {   
                *buf = "Connection: keep-alive\r\n\r\n";
                return 0;
            }
        }

        token = strtok(NULL, "\r\n");
    }

    *buf = "Connection: close\r\n\r\n";
    return 0;
}

void *connection_handler (void *sockfd)
{
    // Connection handler
    int sock = *(int*)sockfd;
    char *buffer, *method, *filename, *connection_type, *content_type;
    int bufsize = 2048;

    const char *HTTP_404_CONTENT = "<html><head><title>404 Not "
    "Found</title></head><body><h1>404 Not Found</h1>The requested "
    "resource could not be found but may be available again in the "
    "future."</body></html>";

    const char *HTTP_501_CONTENT = "<html><head><title>501 Not "
    "Implemented</title></head><body><h1>501 Not Implemented</h1>The "
    "server either does not recognise the request method, or it lacks "
    "the ability to fulfill the request.</body></html>";

    buffer = (char*) malloc(bufsize);    
    if (!buffer){
        printf("The receive buffer was not allocated\n");
        exit(1);    
    }

    while (1)
    {
        int numRead = recv(sock, buffer, bufsize, 0);
        if (numRead < 1){
            if (numRead == 0){
                printf("The client was not read from: disconnected\n");
                break;
            } else {
                perror("The client was not read from");
                break;
            }
            close(sock);
            continue;
        }
        printf("%.*s\n", numRead, buffer);

        // Extract info from request header
        get_connection_type(buffer, &connection_type);
        if (get_filename_and_method(buffer, &method, &filename) == -1)
        {
            char clen[40];
            writeStrToClient(sock, "HTTP/1.1 501 Not Implemented\r\n");
            sprintf(clen, "Content-length: %zu\r\n", strlen(HTTP_501_CONTENT));
            writeStrToClient(sock, clen);
            writeStrToClient(sock, "Content-Type: text/html\r\n");
            writeStrToClient(sock, connection_type);
            writeStrToClient(sock, HTTP_501_CONTENT);
        }
        else
        {

            // Open and read file
            long fsize;
            FILE *fp = fopen(filename, "rb");
            if (!fp){
                perror("The file was not opened");
                char clen[40];
                writeStrToClient(sock, "HTTP/1.1 404 Not Found\r\n");
                sprintf(clen, "Content-length: %zu\r\n", strlen(HTTP_404_CONTENT));
                writeStrToClient(sock, clen);
                writeStrToClient(sock, "Content-Type: text/html\r\n");
                writeStrToClient(sock, connection_type);
                writeStrToClient(sock, HTTP_404_CONTENT);

                if (strcmp(connection_type, "Connection: close\r\n\r\n") == 0)
                    break;

                continue;    
            }

            printf("The file was opened\n");

            if (fseek(fp, 0, SEEK_END) == -1){
                perror("The file was not seeked");
                exit(1);
            }

            fsize = ftell(fp);
            if (fsize == -1) {
                perror("The file size was not retrieved");
                exit(1);
            }
            rewind(fp);

            char *msg = (char*) malloc(fsize);
            if (!msg){
                perror("The file buffer was not allocated\n");
                exit(1);
            }

            if (fread(msg, fsize, 1, fp) != 1){
                perror("The file was not read\n");
                exit(1);
            }
            fclose(fp);

            // Get extension of filename
            char *ext = strrchr(filename, '.');
            if (ext != NULL)
                ext++;
            if (strcmp(ext, "html") == 0 || strcmp(ext, "htm") == 0)
                content_type = "Content-Type: text/html\r\n";
            else if (strcmp(ext, "css") == 0)
                content_type = "Content-Type: text/css\r\n";
            else if (strcmp(ext, "jpg") == 0)
                content_type = "Content-Type: image/jpeg\r\n";
            else if (strcmp(ext, "png") == 0)
                content_type = "Content-Type: image/png\r\n";
            else if (strcmp(ext, "gif") == 0)
                content_type = "Content-Type: image/gif\r\n";
            else
                content_type = "Content-Type: text/plain\r\n";


            if (!writeStrToClient(sock, "HTTP/1.1 200 OK\r\n")){
                close(sock);
                continue;
            }
            char clen[40];

            sprintf(clen, "Content-length: %ld\r\n", fsize);

            if (!writeStrToClient(sock, clen)){
                printf("Cannot write content length\n");
                close(sock);
                continue;
            }


            if (!writeStrToClient(sock, content_type)){
                close(sock);
                continue;
            }



            if (!writeStrToClient(sock, connection_type) == -1){
                close(sock);
                continue;
            }



            if (!writeDataToClient(sock, msg, fsize)){
                close(sock);
                continue;
            }

            printf("The file was sent successfully\n");
        }

        if (strcmp(connection_type, "Connection: close\r\n\r\n") == 0)
            break;
    }

    close(sock);
    pthread_exit(0);
}

int main(int argc, char *argv[]){
    int create_socket, new_socket;    
    struct sockaddr_in address;    
    socklen_t addrlen;    
    char *ptr;

    if (argc != 2)
    {
        printf("Usage: %s <port number>\n", argv[0]);
        exit(0);
    }

    create_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (create_socket == -1){    
        perror("The socket was not created");    
        exit(1);    
    }

    printf("The socket was created\n");

    const unsigned short port = (unsigned short) strtol(argv[1], &ptr, 10);

    memset(&address, 0, sizeof(address));    
    address.sin_family = AF_INET;    
    address.sin_addr.s_addr = INADDR_ANY;    
    address.sin_port = htons(port);    

    if (bind(create_socket, (struct sockaddr *) &address, sizeof(address)) == -1){    
        printf("The socket was not bound because that port is not available\n");    
        exit(1);    
    }

    printf("The socket is bound\n");    

    if (listen(create_socket, 10) == -1){
        perror("The socket was not opened for listening");    
        exit(1);    
    }    

    printf("The socket is listening\n");

    while (1) {    

        addrlen = sizeof(address);
        pthread_t tid;
        new_socket = accept(create_socket, (struct sockaddr *) &address, &addrlen);

        if (new_socket == -1) {    
            perror("A client was not accepted");    
            exit(1);    
        }    

        printf("A client is connected from %s:%hu...\n", inet_ntoa(address.sin_addr), ntohs(address.sin_port));

        if (pthread_create(&tid, NULL, connection_handler, (void *)&new_socket) < 0)
        {
            perror("Could not create thread");
            return 1;
        }

        pthread_join(tid, NULL);
   }

   if (new_socket < 0)
   {
    perror("accept failed");
    return 1;
   }    

   close(create_socket);
   printf("Socket was closed\n");
   return 0;    
}

此外,关闭浏览器(与服务器断开连接)会导致接受另一个连接,该连接发送浏览器请求但未收到的第一个文件,然后服务器程序结束而没有任何错误消息。

更新:删除 pthread_join 允许页面正确加载。正如用户提到的,浏览器并行执行多个连接,所以我认为正在发生的事情是所有请求都通过多个连接发送(查看我的程序输出,似乎有 5 个连接到服务器)。当 pthread_join 等待线程(连接)完成时,一次只处理一个连接,这就是我没有收到所有请求的原因。

标签: csocketsnetwork-programmingwebserver

解决方案


HTTP可能比您想象的要复杂。您是否完整阅读过它的规范( RFC 7230 for HTTP 1.1),或者一些关于 HTTP 的书?您是否考虑过使用一些 HTTP 服务器库,例如libonionlibhttp(或libmicrohttpd或其他)?这些库的大小说明了 HTTP 的复杂性!而且这些库是免费软件,因此您可以研究它们的源代码并从中汲取灵感。(如果您诚实地告诉他您学习了 eg 的源代码libonion并阅读了RFC 7230 ,您的老师应该会很高兴)。

顺便说一句,现代浏览器(最近的 Firefox 或 Chrome 等)倾向于使用多个并行连接来显示一个页面。现代浏览器能够向您展示实际的 HTTP 流量和网络协议。

我的建议是使用一些现有的库。我很高兴使用libonion,即使它有一些限制。

最后,阅读如何调试小程序。启用所有警告和调试信息(因此使用gcc -Wall -Wextra -g最新的GCC 进行编译,例如2018 年底的GCC 8)。了解如何使用 GDB 进行调试(也可以使用最近的一个, 2018 年底的GDB 8.2)。也可以使用valgrind和可能的 clang-analyzer

我有一个用 C 编写的简单 Web 服务器

这在术语上是矛盾的。Web 服务器要么不能简单,要么不能实现所有 HTTP。

您的使用sprintf是危险的(缓冲区溢出的风险)。我强烈建议改用snprintf。而且您的 404 处理看起来非常糟糕。


一个不同的问题(我不知道它的答案)是,如果在您的特定情况下,对于某些特定的浏览器客户端,您可能会为您的特定情况实现一小部分 HTTP(足够用于您的浏览器和客户端)。


推荐阅读