对web服务器的简单尝试

一.简介

        以下的内容以及代码,是基于c++实现的一个简单的部署在Linux的web服务器,以及一个简单的前端加上一个负责保存留言的guestbook.txt,这个web服务器是一个简单的单线程程序,并不涉及线程池,进程池,互斥锁等意在实现Linux高性能服务器的编程。则相当这是我自己的一个学习记录。想看进阶一点的可以尝试这位朋友的博客。简单web服务器的实现(C++)icon-default.png?t=N7T8https://blog.youkuaiyun.com/qq_36573828/article/details/82784425

        本人也很少参与与他人合作的代码项目,而关于网络编程也大概只是停留在大学的计算机网络这门课程上,因此代码可读性不强,臃肿甚至错误多多也许希望大家尽情谅解,本人也是小白一只。

二.项目实现

        一旦涉及网络通信,就不能避开TCP/IP协议以及三次握手和四次挥手。而在c++中也提供了socket.h等头文件来直接调用实现网络通信。

这是一个简单的c++web服务器

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

const int port = 8888;

int main(int argc,char *argv[])
{
    // 检查命令行参数数量
    if(argc<0)
    {
        printf("need two canshu\n");
        return 1;
    }
    
    int sock;
    int connfd;
    struct sockaddr_in sever_address;

    // 清空并初始化服务器地址结构
    bzero(&sever_address,sizeof(sever_address));
    sever_address.sin_family = PF_INET;
    sever_address.sin_addr.s_addr = htons(INADDR_ANY);
    sever_address.sin_port = htons(8888);
 
    // 创建TCP套接字
    sock = socket(AF_INET,SOCK_STREAM,0);
    assert(sock>=0);
 
    // 将套接字绑定到服务器地址上
    int ret = bind(sock, (struct sockaddr*)&sever_address,sizeof(sever_address));
    assert(ret != -1);
 
    // 监听套接字,等待连接
    ret = listen(sock,1);
    assert(ret != -1);

    while(1)
    {
        struct sockaddr_in client;
        socklen_t client_addrlength = sizeof(client);

        // 接受客户端连接
        connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
        if(connfd<0)
        {
            printf("errno\n");
        }
        else
        {
            // 读取客户端请求
            char request[1024];
            recv(connfd,request,1024,0);
            request[strlen(request)+1]='\0';
            printf("%s\n",request);
            printf("successeful!\n");

            // 构造HTTP响应头部
            char buf[520]="HTTP/1.1 200 ok\r\nconnection: close\r\n\r\n";

            // 发送HTTP响应头部
            int s = send(connfd,buf,strlen(buf),0);

            // 打开并发送HTML文件内容
            int fd = open("hello.html",O_RDONLY);
            sendfile(connfd,fd,NULL,2500); // 使用零拷贝发送文件内容
            close(fd);

            // 关闭连接套接字
            close(connfd);
        }
    }
    return 0;
}

  以及一个简单的html(hello.html)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hello, World!</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 0;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            background-color: #f0f0f0;
        }
        .container {
            text-align: center;
        }
        h1 {
            font-size: 3em;
            color: #333;
        }
        p {
            font-size: 1.2em;
            color: #666;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>Hello, World!</h1>
        <p>Welcome to my beautiful page.</p>
    </div>
</body>
</html>

在这段代码中,并没有直接处理TCP的三次握手和四次挥手过程,因为这些过程是由操作系统的网络栈自动管理的。应用程序只需要通过标准的套接字API(如socket, bind, listen, accept, send, recv, close)来请求建立、使用和关闭TCP连接。

在上述代码中,三次握手的实现是隐含的:

  • 当服务器调用listen函数进入监听状态后,它会等待客户端的连接请求。
  • 当客户端尝试连接服务器时,操作系统会为服务器自动处理第一个SYN和随后的SYN-ACK的接收和发送。
  • 当服务器调用accept函数时,它实际上是在等待一个已经完成了三次握手的客户端连接。accept函数返回一个全新的套接字(称为已接受套接字),用于与客户端通信。

四次挥手的实现也是隐含的:

  • 当客户端或服务器决定关闭连接时,它会调用close函数。对于服务器端,这是在处理完每个客户端请求后发生的。
  • close函数会导致套接字发送一个FIN段,开始关闭连接的过程。
  • 对方接收到FIN后,会发送一个ACK,并可能在适当的时候发送自己的FIN。
  • 最后,当FIN被确认后,连接被完全关闭。

在浏览器上面成功访问到本地web服务器后,我们可以在服务器得到以下的响应

        这是一个 HTTP 请求报文,用于向服务器发起请求。

GET / HTTP/1.1                   // 请求行:请求方法为GET,请求路径为根路径'/',使用HTTP/1.1协议版本
Host: localhost:8888             // 主机头部:指定了请求的目标主机和端口号
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/116.0  // 用户代理头部:发起请求的用户代理(浏览器)的信息
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8  // 接受头部:指定客户端可以接受的媒体类型及其优先级
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2  // 接受语言头部:指定客户端接受的自然语言及其优先级
Accept-Encoding: gzip, deflate, br  // 接受编码头部:指定客户端接受的内容编码方式
Connection: keep-alive           // 连接头部:指定客户端请求完成后是否保持与服务器的连接
Upgrade-Insecure-Requests: 1     // 升级头部:表示客户端是否愿意升级到HTTPS请求以提高安全性
Sec-Fetch-Dest: document         // 安全头部:帮助服务器判断请求的目的地
Sec-Fetch-Mode: navigate         // 安全头部:帮助服务器判断请求的模式
Sec-Fetch-Site: none             // 安全头部:帮助服务器判断请求的来源
Sec-Fetch-User: ?1               // 安全头部:帮助服务器判断请求的用户
                                  // 空行,用于分隔请求头部和请求体
successeful!                      // 表示服务器成功处理请求的标志,由客户端发出

虽然http1.1协议问世已经有一段年头了,但是它仍然是绝大多数浏览器可执行的协议,对于涉及web服务器,我们也避不开对http的方法调用与状态码分析,这就要及时翻阅相关的技术手册。大家也可以看这篇博客。这里,http的icon-default.png?t=N7T8https://blog.youkuaiyun.com/weixin_60358891/article/details/130231494

这里也补一个http响应格式,

        其实关于http响应格式的,感兴趣的也可以做抓包实验,学校一般做的是wireshark的抓包实验,做过一次也会对这个东西很清楚的。我也放了一个实验指导(由于目前互联网上的许多网站已经更新协议至https,导致使用wireshark通过http协议来抓取明文密码的方法已不再可行,现在的网站一般都不可能直接抓包获取你提交的账号密码了)。

https://pan.baidu.com/s/1Q3ms10SU-Sk9tOuHXVCjgw?pwd=4566
提取码:4566

        以下就是单单为原本的web服务器加上从前端读取留言,并将时间戳和留言以及响应记录在guestbook.txt,而如果前端向服务器发出请求获取留言,则将guestbook.txt打印出来。为了实现好这一目的,我们需要在前端网页提前设计向web服务器发送和刷新留言的功能,虽然下述代码是通过使用了JavaScript和AJAX技术实现的,但是AJAX在其中的使用是偏向简单的,或者说无论是我的前端和web服务器都没有实现完整的错误处理逻辑。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>留言板</title>
    <script>
        // refreshMessages 函数用于通过 AJAX 请求刷新留言记录
        function refreshMessages() {
            var xhr = new XMLHttpRequest(); // 创建 AJAX 请求对象
            xhr.onreadystatechange = function() { // 设置请求状态变化的回调函数
                if (xhr.readyState === XMLHttpRequest.DONE) { // 检查请求是否完成
                    if (xhr.status === 200) { // 检查请求是否成功
                        // 将响应内容设置为留言记录表的内联HTML
                        document.getElementById("messagesTable").innerHTML = xhr.responseText;
                    } else {
                        alert("无法获取留言记录"); // 请求失败时提示用户
                    }
                }
            };
            xhr.open("GET", "guestbook.txt", true); // 初始化 GET 请求
            xhr.send(); // 发送请求
        }

        // sendMessage 函数用于发送用户留言到服务器
        function sendMessage() {
            var message = document.getElementById("messageInput").value; // 获取用户输入的留言
            if (message.trim() === "") { // 检查留言是否为空
                alert("留言不能为空!"); // 留言为空时提示用户
                return;
            }

            var xhr = new XMLHttpRequest(); // 创建 AJAX 请求对象
            xhr.onreadystatechange = function() { // 设置请求状态变化的回调函数
                if (xhr.readyState === XMLHttpRequest.DONE) { // 检查请求是否完成
                    if (xhr.status === 200) {
                        alert("留言成功!");
                        // 留言成功后清空留言框,并刷新留言记录
                        document.getElementById("messageInput").value = "";
                        refreshMessages();
                    } else {
                        alert("留言失败!");
                    }
                }
            };
            xhr.open("POST", "", true); // 初始化 POST 请求
            xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); // 设置请求头
            // 发送留言内容
            xhr.send("message=" + message);
        }
    </script>
</head>
<body>
    <h1>留言板</h1>
    <!-- 留言记录表,留言记录将通过 AJAX 请求动态加载 -->
    <table id="messagesTable" border="1">
        <!-- 表内内容将被 AJAX 请求的响应文本替换 -->
    </table>
    <!-- 留言输入框和发送按钮 -->
    <!-- 表单的onsubmit事件被设置为阻止默认提交行为,并调用 sendMessage 函数 -->
    <form id="messageForm" onsubmit="event.preventDefault(); sendMessage();">
        <input type="text" id="messageInput" placeholder="输入您的留言">
        <input type="submit" value="发送留言">
    </form>
    <!-- 刷新留言记录按钮,点击时调用 refreshMessages 函数 -->
    <button onclick="refreshMessages()">刷新留言记录</button>
</body>
</html>
#include <cstdio>
#include <cstdlib>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <unistd.h>
#include <cstring>
#include <errno.h>
#include <dirent.h>
#include <map>
#include <ctime>

// 定义服务器监听的端口号和文件路径
const int port = 8888;
const char* guestbook_path = "guestbook.txt"; // 留言板文件的路径
const char* html_file_path = "h.html"; // HTML表单文件的路径

// 函数:发送文件内容给客户端
void send_file(int client_socket, const char* file_path) {
    // 打开文件用于读取
    FILE *file = fopen(file_path, "rb");
    if (file) {
        // 获取文件大小
        fseek(file, 0, SEEK_END);
        size_t file_size = ftell(file);
        fseek(file, 0, SEEK_SET);

        // 分配内存用于存储文件内容
        char *file_content = (char *)malloc(file_size);
        // 读取文件内容到内存
        if (fread(file_content, 1, file_size, file) != file_size) {
            perror("fread failed"); // 读取失败,打印错误
            free(file_content); // 释放内存
            fclose(file); // 关闭文件
            return;
        }

        // 发送HTTP响应头
        const char *response = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n";
        if (send(client_socket, response, strlen(response), 0) < 0) {
            perror("send failed"); // 发送失败,打印错误
        }

        // 发送文件内容
        if (send(client_socket, file_content, file_size, 0) < 0) {
            perror("send failed"); // 发送失败,打印错误
        }

        // 释放内存和关闭文件
        free(file_content);
        fclose(file);
    } else {
        perror("fopen failed"); // 打开文件失败,打印错误
        // 发送404 Not Found响应
        const char *response = "HTTP/1.1 404 Not Found\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n";
        if (send(client_socket, response, strlen(response), 0) < 0) {
            perror("send failed"); // 发送失败,打印错误
        }
    }
}

// 函数:将留言追加到留言板文件中
void append_message(const char* message) {
    // 打开留言板文件用于追加
    FILE *guestbook = fopen(guestbook_path, "a");
    if (guestbook) {
        // 获取当前时间
        time_t now = time(NULL);
        // 将留言和时间追加到文件中
        fprintf(guestbook, "%s - %s\n", ctime(&now), message);
        fclose(guestbook); // 关闭文件
    } else {
        perror("Error opening guestbook file"); // 打开文件失败,打印错误
    }
}

// 函数:处理GET请求,显示留言板内容
void handle_get_guestbook(int client_socket) {
    send_file(client_socket, guestbook_path); // 发送留言板文件内容
}

int main(int argc, char *argv[]) {
    // 检查命令行参数数量
    if (argc != 1) {
        fprintf(stderr, "Usage: %s\n", argv[0]);
        return 1;
    }

    // 声明套接字和地址变量
    int server_socket, client_socket;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
    char buffer[1024];
    ssize_t bytes_received;

    // 创建服务器套接字
    server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket < 0) {
        perror("socket creation failed"); // 创建失败,打印错误
        return 1;
    }

    // 初始化服务器地址结构
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY; // 监听任意IP地址
    server_addr.sin_port = htons(port); // 端口号

    // 将服务器套接字绑定到地址
    if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind failed"); // 绑定失败,打印错误
        return 1;
    }

    // 开始监听服务器套接字
    if (listen(server_socket, 1) < 0) {
        perror("listen failed"); // 监听失败,打印错误
        return 1;
    }

    // 打印服务器监听信息
    printf("Server is listening on port %d...\n", port);
    while (1) {
        // 接受客户端连接
        client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &client_len);
        if (client_socket < 0) {
            perror("accept failed"); // 接受失败,打印错误
            continue;
        }

        // 打印客户端连接信息
        printf("Connected to client at %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

        // 接收客户端请求
        bytes_received = recv(client_socket, buffer, sizeof(buffer) - 1, 0);
        if (bytes_received <= 0) {
            perror("recv failed"); // 接收失败,打印错误
            close(client_socket); // 关闭客户端套接字
            continue;
        }
        buffer[bytes_received] = '\0'; // 确保以空字符结尾

        // 打印接收到的请求
        printf("Received request: %s\n", buffer);

        // 检查请求类型
        if (strncmp(buffer, "GET /", 5) == 0) {
            // 处理GET请求
            const char *get_target = buffer + 5;
            if (strncmp(get_target, "guestbook", 8) == 0) {
                handle_get_guestbook(client_socket); // 显示留言板内容
            } else {
                send_file(client_socket, html_file_path); // 发送HTML表单页面
            }
        } else if (strncmp(buffer, "POST /", 6) == 0) {
            // 处理POST请求
            const char *message_start = buffer + 6; // 跳过"POST /"
            append_message(message_start); // 追加留言到留言板
            // 发送200 OK响应
            const char *response = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n";
            if (send(client_socket, response, strlen(response), 0) < 0) {
                perror("send failed"); // 发送失败,打印错误
            }
        } else {
            // 处理其他请求类型
            // 发送400 Bad Request响应
            const char *response = "HTTP/1.1 400 Bad Request\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n";
            if (send(client_socket, response, strlen(response), 0) < 0) {
                perror("send failed"); // 发送失败,打印错误
            }
        }

        // 关闭客户端套接字
        close(client_socket);
    }

    // 关闭服务器套接字
    close(server_socket);
    return 0;
}

        当然前端的代码实现也有改进的地方,可以通过JavaScript过滤发来的无效信息以及重新排版达到美观的效果,到此,上述代码以及一个 guestbook.txt文本文件,就可以实现通过本地接口8888发送留言保存在web服务器,并且通过这个接口获取留言,以上代码仅仅是简单的单线程服务器,并不涉及构建高性能服务器的知识。

三.后记

        这几天,从我舍友捞了一本书回来,我大略看了一下,如果大家想做Linux的高性能的服务器的,这本书也不错,是机械工业出版社的《Linux高性能服务器编程》,刚坑蒙拐骗拿过来,还没细看,但是讲的挺深的,不过我也基本是把这个项目打完了,而且这本书也确实有点难啃,抽屉里还有一本3年也没看下去的数据科学的,从线性回归到深度学习的书,大概率这本书也是这样,不过能看一点是一点。说实话,这种技术类的书籍,比课本难也不如博客来的简便,但它的专业性够强,还是希望未来的学弟学妹别像我一样,东看看西跑跑的,静不下来,这类型的书还是很有帮助的,上述的web服务器代码也可能在我看这本书到某个程度的时候改进一下,说不定也搞一个高性能的服务器出来。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值