最简单版web server

第一部分:概念与流程解释

1. 什么是"单线程,阻塞式 I/O"?
  • 单线程 :就像餐厅里只有一个服务员。他一次只能接待一桌客人。如果这桌客人点菜慢,或者一直在吃饭,服务员就得干等着,不能去招呼下一桌。
  • 阻塞式 (Blocking):意味着"死等"。
  • 当程序运行到 accept(等待连接)时,如果没有人来连,程序就在那里不动,直到有人来。
  • 当程序运行到 recv(接收数据)时,如果对方没发数据,程序也在那里不动。
2. Socket API 流程详解 (打电话的比喻)

Socket 网络编程的流程,几乎完美对应生活中座机电话的流程:

  1. socket() ------ 买个电话机
  • 含义:向操作系统申请一个"套接字"资源。
  • 代码意图:创建一个文件描述符(fd),告诉系统我要开始网络通信了。
  1. bind() ------ 插上电话线(绑定号码)
  • 含义 :把电话机和你的电话号码(IP地址 + 端口号)绑定在一起。
  • 代码意图 :告诉系统,如果有人打 8080 这个端口,就转给我这个程序。
  1. listen() ------ 接通电源,准备接听
  • 含义:把电话机状态设置为"可接听"。
  • 代码意图:告诉系统,我准备好了,如果有连接进来,先帮我放在"等待队列"里排队(backlog)。
  1. accept() ------ 电话响了,拿起听筒
  • 含义:从等待队列里取出一个连接,正式建立通话。
  • 关键点 :这是一个阻塞 操作。如果没有人打进来,代码就停在这行不动。一旦有人打进来,它会返回一个新的 socket(专门用于和这个人通话),原来的 socket 继续监听别人的电话。
  1. read() / recv() ------ 听对方说话
  • 接收浏览器发过来的 HTTP 请求(一堆字符串)。
  1. write() / send() ------ 回复对方
  • 发送 HTTP 响应(HTML 页面)。
  1. close() ------ 挂断电话
  • 结束这次通话,释放资源。

第二部分:极简乞丐版 WebServer 代码

这是一个在 Linux/Mac 环境下可运行的 C++ 代码(Windows 下 socket API 略有不同,建议在 Linux 虚拟机或 WSL 下运行)。

文件名建议保存为:simple_server.cpp

cpp 复制代码
#include <iostream>
#include <cstring>      // for memset
#include <sys/socket.h> // 核心 socket API
#include <netinet/in.h> // 包含 AF_INET, sockaddr_in 等
#include <unistd.h>     // 包含 close, read, write

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    // 1. 创建 Socket (买电话机)
    // AF_INET: 使用 IPv4
    // SOCK_STREAM: 使用 TCP 协议
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == 0) {
        perror("Socket creation failed");
        return -1;
    }

    // 2. 绑定 IP 和 端口 (插电话线)
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    
    // 初始化结构体
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY; // 监听本机所有 IP
    address.sin_port = htons(PORT);       // 端口号转为网络字节序

    // 执行 bind
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("Bind failed");
        return -1;
    }

    // 3. 监听 (打开铃声)
    // 3 代表等待队列的最大长度
    if (listen(server_fd, 3) < 0) {
        perror("Listen failed");
        return -1;
    }

    std::cout << "Server is listening on port " << PORT << "..." << std::endl;

    while (true) {
        // 4. 接受连接 (拿起听筒) - 阻塞点
        // accept 会返回一个新的 socket (new_socket) 用于专门和这个客户端通信
        int new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);
        if (new_socket < 0) {
            perror("Accept failed");
            continue;
        }

        // 5. 读取数据 (听对方说啥)
        char buffer[BUFFER_SIZE] = {0};
        read(new_socket, buffer, BUFFER_SIZE);
        
        // 打印请求内容(调试用)
        // std::cout << "Received Request:\n" << buffer << std::endl;

        // 简单的字符串查找判断 GET
        if (strstr(buffer, "GET") != NULL) {
            
            // 6. 构造响应 (硬编码 HTML)
            // HTTP 响应格式必须严格遵守:状态行 + 响应头 + 空行 + 响应体
            const char *html_content = "<html><body><h1>Hello, World!</h1><p>This is a C++ WebServer.</p></body></html>";
            
            std::string response = "HTTP/1.1 200 OK\r\n";
            response += "Content-Type: text/html\r\n";
            response += "Content-Length: " + std::to_string(strlen(html_content)) + "\r\n";
            response += "\r\n"; // 必须有的空行,分隔头和体
            response += html_content;

            // 7. 发送响应 (说话)
            send(new_socket, response.c_str(), response.length(), 0);
            std::cout << "Response sent to client." << std::endl;
        }

        // 8. 关闭连接 (挂断)
        // 在 HTTP/1.0 中,发完就关。
        close(new_socket);
    }

    // 实际运行中这里不可达,但逻辑上需要关闭监听 socket
    close(server_fd);
    return 0;
}

第三部分:如何运行和测试

请确保你在 Linux 环境(Ubuntu/CentOS)或者 MacOS 下。

1. 编译代码

打开终端,进入代码所在目录,输入:

bash 复制代码
g++ simple_server.cpp -o server

如果没有报错,会生成一个名为 server 的可执行文件。

2. 运行服务器

输入:

bash 复制代码
./server

终端会显示:Server is listening on port 8080...

此时,程序阻塞 在了 accept 函数,正在等待连接。

3. 测试访问

你可以用两种方式测试:

  • 方式 A(浏览器):
    打开浏览器(Chrome/Edge),在地址栏输入:http://localhost:8080
    你将看到页面显示大号的 Hello, World!
  • 方式 B(命令行 curl):
    打开另一个终端窗口,输入:
bash 复制代码
curl -v http://localhost:8080

你会看到详细的 HTTP 握手过程和返回的 HTML。


第四部分:这个版本的"缺陷"在哪?(为了下一阶段学习)

你在运行这个代码时,如果手速够快,或者用脚本同时发 10 个请求,你会发现:

它一次只能服务一个人。

代码逻辑是:
accept -> read -> send -> close -> 回到 accept

如果我在 read 之后加一个 sleep(10)(模拟处理业务很慢),那么在这 10 秒钟内,其他任何人访问这个网页,浏览器都会在那转圈圈(等待连接),因为主线程还在睡觉,没空回到 accept 去接新的电话。

这就是"阻塞式 I/O + 单线程"的致命弱点。

为了解决这个问题,下一阶段(进阶版)通常会引入:

  1. IO 多路复用 (Epoll):不用傻等一个电话,而是雇一个接线员(内核),哪个电话响了通知我处理哪个。
  2. 线程池:来了任务扔给线程池去做,主线程立刻回去接下一个电话。
相关推荐
m0_502724952 小时前
Arco Design Vue 中的a-upload
前端·javascript·arco design vue
VT.馒头2 小时前
【力扣】2637. 有时间限制的 Promise 对象
前端·javascript·leetcode·typescript
zhengxianyi5152 小时前
Vue2 打包部署后通过修改配置文件修改全局变量——实时生效
前端·vue.js·前后端分离·数据大屏·ruoyi-vue-pro
灵犀坠2 小时前
Vue3 实现音乐播放器歌词功能:解析、匹配、滚动一站式教程
开发语言·前端·javascript·vue.js
north_eagle2 小时前
ReAct 框架详解
前端·react.js·前端框架
OEC小胖胖2 小时前
13|React Server Components(RSC)在仓库中的落点与边界
前端·react.js·前端框架·react·开源库
OEC小胖胖2 小时前
14|Hook 的实现视角:从 API 到 Fiber Update Queue 的连接点
前端·react.js·前端框架·react·开源库
军军君012 小时前
Three.js基础功能学习十:渲染器与辅助对象
开发语言·前端·javascript·学习·3d·前端框架·ecmascript
Marshmallowc2 小时前
React useState 数组 push/splice 后页面不刷新?深度解析状态被『蹭』出来的影子更新陷阱
前端·react.js·前端框架