MiniHttp服务器的设计与实现

MiniHttp服务器的设计与实现

1. 目标及要求

img
img

2. 功能设计及思想

2.0 前置知识-C/S请求响应的过程

C/S架构,也称为客户端/服务器架构。客户端通过http/https协议向服务器发送请求,服务器接收到客户端的请求之后,就会将服务器上存有的数据返回给客户端。

在上述过程中,客户端向服务器发送的http请求,称为http请求包。服务器向客户端发送的http请求,称为http应答包(响应包)。

我们的目的就是要编写一个可以与客户端进行交互的http/https服务器,并可以向客户端发送http/https应答包。

2.0 前置知识-http请求的格式

img

以上图片来自:https://www.bilibili.com/video/BV1gg411P7hL/?spm_id_from=333.999.0.0&vd_source=a642bb3ddc5b706845426dc41d73fbda

2.0 前置知识-http响应的格式

img

以上图片来自:
https://www.bilibili.com/video/BV1gg411P7hL/?spm_id_from=333.999.0.0&vd_source=a642bb3ddc5b706845426dc41d73fbda

2.0 前置知识-使用openssl导入证书

我们可以使用如下的步骤来进行ssl上下文的初始化和证书的导入工作:

  1. 初始化 OpenSSL
  2. 创建 SSL 上下文
  3. 加载服务器证书和私钥

初始化OpenSSL

// 1. 初始化 OpenSSL
SSL_library_init();
OpenSSL_add_all_algorithms();
SSL_load_error_strings();

创建SSL上下文

// 2. 创建SSL上下文
ctx = SSL_CTX_new(SSLv23_server_method());
if(!ctx){
    fprintf(stderr,"Error creating SSL context\n");
    return NULL;
}

加载服务器证书与私钥

// 3. 加载服务器证书和私钥
if(SSL_CTX_use_certificate_file(ctx,SERVER_CERT,SSL_FILETYPE_PEM) <= 0 ||SSL_CTX_use_PrivateKey_file(ctx,SERVER_KEY,SSL_FILETYPE_PEM) <= 0){            
    fprintf(stderr,"Error loading server certificate or private key\n");            
    return NULL;
}

2.0 前置知识-使用多线程来监听80和443端口

我们可以使用多线程来处理客户端的http和https请求,在函数中我们用端口号来进行区分,以便于进行不同的处理。

主线程需要使用join函数,直到子线程全部执行完成后,主线程才可以继续往下执行。否则就会出现子线程没有执行完,主线程执行完成导致子线程被迫停止的现象。

2.0 前置知识-使用socket来处理http请求,解析,响应

关于socket的具体用法,这里不进行阐述,可以参考我之前的博客-简单Echo服务器的实现

服务器从socket接收到客户端的请求后,通过读取一行(逐个字符读取),来进行http请求的解析工作。

对于不同的http请求,我们通过将响应的内容按照http协议的格式进行书写,最后写到字符数组中,将其通过socket给到客户端即可。

2.1 https 200响应

当客户端向服务器发送请求(443端口)时,这里设定请求为GET,请求的文件是index.html。服务器通过读取请求行中的GET和index.html之后,就会判断服务器的网站目录下是否存在index.html文件,如果存在响应200 OK,之后就会将index.html的内容通过响应体返回给客户端,客户端通过读取服务器的响应头和响应体,将内容渲染到屏幕上。

具体的响应头为:

HTTP/1.0 200 OK
Server: Gao79135 Server
Content-Type: text/html
Connection: Close
Content-Length: 53464

2.2 http 301重定向

当客户端向服务器发送GET请求(80端口)时,服务器直接向客户端返回301 Moved Permanently,后跟Location字段表达要重定向的url。客户端通过读取该响应头,就会跳转到该url。

具体的响应头为:

HTTP/1.1 301 Moved Permanently
Location: https://192.168.11.137/index.html

2.3 https 404响应

当客户端向服务器发送GET请求时,当服务器读取请求行的文件,发现不存在时,就会向客户端返回404 Not Found,响应体存储所要向用户提示的信息。

具体的响应头为:

HTTP/1.0 404 NOT FOUND
Server: Gao79135 Server
Content-Type: text/html
Connection: Close

2.4 https 206响应

当客户端向服务器发送GET请求,且请求的是一个大图片/视频等文件时,服务器可以采取断点续传,每次响应都将一部分的内容(响应体)给到客户端。客户端依次解析服务器响应的部分内容,解析之后进行对应的处理。

上述的过程是一次请求,一次响应的过程。并不是一次请求,多次响应的过程。

具体的响应头为:

HTTP/1.1 206 Partial Content
Content-Range: bytes 26214400-31457279/38309344
Content-Length: 5242880
Content-Type: video/mp4

2.5 https 500响应

当客户端正在访问服务器的资源时,服务器在解析的过程中,由于自身原因出现了一些致命错误,就会向客户端返回500 Internal Error。

具体的响应头为:

HTTP/1.0 500 Internal Server Error
Server: Gao79135 Server
Content-Type: text/html
Connection: Close

2.6 https 400响应

当客户端访问服务器的资源,但是自身的http请求格式不合法时,服务器解析请求,就会向客户端返回400 Bad Request。

具体的响应头为:

HTTP/1.0 400 BAD REQUEST
Server: Gao79135 Server
Content-Type: text/html
Connection: Close

2.7 https 501响应

当客户端向服务器发送请求时,请求方法在服务器端没有实现,那么服务器就会向客户端返回501 Not Implemented。

具体的响应头为:

HTTP/1.0 501 Method NOT IMPLEMENTED
Server: Gao79135 Server
Content-Type: text/html
Connection: Close

3. 各功能具体实现

3.1 https 200响应

int send_response_headers(SSL* ssl,FILE* resource){
    struct stat st;                 //文件元数据
    int file_id = 0;                //文件描述符
    char tmp[64];                   //Content-Length
    char buffer[1024] = {0};        //响应头
    strcpy(buffer,"HTTP/1.0 200 OK\r\n");
    strcat(buffer,"Server: Gao79135 Server\r\n");
    strcat(buffer,"Content-Type: text/html\r\n");
    strcat(buffer,"Connection: Close\r\n");
    file_id = fileno(resource);

    if(fstat(file_id,&st) == -1){
        //返回服务器内部错误:500
        inner_error(ssl);
        return -1;
    }

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

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

    //响应给服务器
    if(SSL_write(ssl,buffer,strlen(buffer)) < 0){
        fprintf(stderr,"send failed. data: %s, reason: %s\n",buffer,strerror(errno));
        return -1;
    }
    return 0;
}

3.2 http 301重定向

void moved_permanently(int client_socket){
    char buffer[1024];                                                      //响应头
    strcpy(buffer,"HTTP/1.1 301 Moved Permanently\r\n");
    strcat(buffer,"Location: https://192.168.11.137/index.html\r\n\r\n");

    int len = write(client_socket,buffer,strlen(buffer));
    if(debug) fprintf(stdout,buffer);

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

3.3 https 404响应

void not_found(SSL* ssl){
    const char * reply = "404 not found!!!";        //响应正文
    char buffer[1024];                              //响应头
    strcpy(buffer,"HTTP/1.0 404 NOT FOUND\r\n");
    strcat(buffer,"Server: Gao79135 Server\r\n");
    strcat(buffer,"Content-Type: text/html\r\n");
    strcat(buffer,"Connection: Close\r\n\r\n");
    strcat(buffer,reply);
    int len = SSL_write(ssl,buffer,strlen(buffer));
    if(debug) fprintf(stdout,buffer);

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

3.4 https 206响应

//send video to browser
//start_byte:当前传输的起始字节,end_byte:当前传输的结束字节,total_byte:整个文件的总字节数
void send_video_to_browser(SSL* ssl,FILE* resource){
    unsigned char content[UPLOAD_SIZE];                      //一次上传UPLOAD_SIZE个k
    long start_bytes = 0;
    long end_bytes = 0;
    size_t read_len;
    long video_total_bytes;                                  //代表视频文件的总大小
    char buffer[1024];                                       //响应头
    char content_range[128];                                 //content-range字段
    long content_length;                                     //代表当前响应体的长度

    fseek(resource, 0, SEEK_END);                            //将文件指针移动到末尾
    video_total_bytes = ftell(resource);                     //求得总大小
    fseek(resource, 0, SEEK_SET);                            //将文件指针移动回起始位置


    if(!flag){
        start_bytes = 0;
        end_bytes = UPLOAD_SIZE - 1;
        content_length = end_bytes - start_bytes + 1;
        //返回206状态码
        // 设置Content-Range头
        snprintf(content_range, sizeof(content_range), "bytes %ld-%ld/%ld", start_bytes, end_bytes, video_total_bytes);
        // 设置206 Partial Content响应头
        snprintf(buffer, sizeof(buffer),
                "HTTP/1.1 206 Partial Content\r\n"
                "Content-Range: %s\r\n"
                "Content-Length: %ld\r\n"
                "Content-Type: video/mp4\r\n"
                "\r\n", content_range, content_length);
        int len = SSL_write(ssl,buffer,strlen(buffer));

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

        if(debug) fprintf(stdout,buffer);

        flag = 1;
    }else{
        fseek(resource,prev_position,SEEK_SET);
        start_bytes = ftell(resource);
        read_len = fread(content,1,UPLOAD_SIZE,resource);
        if(read_len == 0){
            prev_position = 0;
            flag = 0;
            return;
        }
        end_bytes = ftell(resource) - 1;
        if(end_bytes > video_total_bytes){
            end_bytes = read_len;
        }

        prev_position = ftell(resource);
        content_length = end_bytes - start_bytes + 1;       //当前响应体的长度
        //返回206状态码
        // 设置Content-Range头
        snprintf(content_range, sizeof(content_range), "bytes %ld-%ld/%ld", start_bytes, end_bytes, video_total_bytes);
        // 设置206 Partial Content响应头
        snprintf(buffer, sizeof(buffer),
                "HTTP/1.1 206 Partial Content\r\n"
                "Content-Range: %s\r\n"
                "Content-Length: %ld\r\n"
                "Content-Type: video/mp4\r\n"
                "\r\n", content_range, content_length);

        if(debug) fprintf(stdout,buffer);

        int len = SSL_write(ssl,buffer,strlen(buffer));

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

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

    }
}
//send video to vlc
void send_video_to_vlc(SSL* ssl,FILE* resource){
    unsigned char content[UPLOAD_SIZE];                      //一次上传UPLOAD_SIZE个k
    long start_bytes = 0;
    long end_bytes = 0;
    size_t read_len;
    long video_total_bytes;                                  //代表视频文件的总大小
    char buffer[1024];                                       //响应头
    char content_range[128];                                 //content-range字段
    long content_length;                                     //代表当前响应体的长度

    fseek(resource, 0, SEEK_END);                            //将文件指针移动到末尾
    video_total_bytes = ftell(resource);                     //求得总大小
    fseek(resource, 0, SEEK_SET);                            //将文件指针移动回起始位置



    //返回200状态码
    snprintf(buffer, sizeof(buffer),
            "HTTP/1.1 200 OK\r\n"
            "Content-Type: video/mp4\r\n"
            "\r\n");

    int len = SSL_write(ssl,buffer,strlen(buffer));

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

    if(debug) fprintf(stdout,buffer);

    while(1){
        fseek(resource,prev_position,SEEK_SET);
        start_bytes = ftell(resource);
        read_len = fread(content,1,UPLOAD_SIZE,resource);
        if(read_len == 0){
            prev_position = 0;
            return;
        }
        end_bytes = ftell(resource) - 1;

        if(end_bytes > video_total_bytes){
            end_bytes = read_len;
        }

        content_length = end_bytes - start_bytes + 1;
        prev_position = ftell(resource);

        printf("start:%ld end:%ld total:%ld\n",start_bytes,end_bytes,video_total_bytes);

        len = SSL_write(ssl,content,read_len);

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

3.5 https 500响应

void inner_error(SSL* ssl){
    const char * reply = "500 internal server error!!!";//响应正文
    char buffer[1024];                                  //响应头
    strcpy(buffer,"HTTP/1.0 500 Internal Server Error\r\n");
    strcat(buffer,"Server: Gao79135 Server\r\n");
    strcat(buffer,"Content-Type: text/html\r\n");
    strcat(buffer,"Connection: Close\r\n\r\n");
    strcat(buffer,reply);
    int len = SSL_write(ssl,buffer,strlen(buffer));
    if(debug) fprintf(stdout,buffer);

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

3.6 https 400响应

void bad_request(SSL* ssl){
    const char * reply = "400 bad request!!!";        //响应正文
    char buffer[1024];                                //响应头
    strcpy(buffer,"HTTP/1.0 400 BAD REQUEST\r\n");
    strcat(buffer,"Server: Gao79135 Server\r\n");
    strcat(buffer,"Content-Type: text/html\r\n");
    strcat(buffer,"Connection: Close\r\n\r\n");
    strcat(buffer,reply);
    int len = SSL_write(ssl,buffer,strlen(buffer));
    if(debug) fprintf(stdout,buffer);

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

3.7 https 501响应

void unimplemented(SSL* ssl){
    const char * reply = "501 method not implemented!!!";   //响应正文
    char buffer[1024];                                      //响应头
    strcpy(buffer,"HTTP/1.0 501 Method NOT IMPLEMENTED\r\n");
    strcat(buffer,"Server: Gao79135 Server\r\n");
    strcat(buffer,"Content-Type: text/html\r\n");
    strcat(buffer,"Connection: Close\r\n\r\n");
    strcat(buffer,reply);
    int len = SSL_write(ssl,buffer,strlen(buffer));
    if(debug) fprintf(stdout,buffer);

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

4. 测试

4.1 在浏览器上测试

img
img
img
img
img
img

4.2 在vlc上测试

img

4.3 在mininet上测试

img

5. 地址

https://github.com/gao79135/minihttp

6. 致谢

    [1] 上图的课件来自于孙毅老师的计算机网络课程。
    [2] https://www.bilibili.com/video/BV14Y411s7yB/?spm_id_from=333.1007.top_right_bar_window_custom_collection.content.click&vd_source=a642bb3ddc5b706845426dc41d73fbda
    [3] https://www.bilibili.com/video/BV1gg411P7hL/?spm_id_from=333.999.0.0&vd_source=a642bb3ddc5b706845426dc41d73fbda
posted @   夏目^_^  阅读(82)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示