前言

有关reactor的简单基础实现的前置代码在上一个博客~

这里讲具体服务器的实现

代码实现

以下是前置代码

reactor.c的逻辑如下:

其中reactor.c的代码如下,具体细节可以见上一个博客

#include <errno.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <poll.h>
#include <sys/epoll.h>
#include <errno.h>
#include <sys/time.h>

#include "server.h"
//头文件的引入 记得引入server.h

#define CONNECTION_SIZE  1048576  //1024 * 1024   最多准备管理 1048576 个连接对象。 后续要创建一个很大的连接数组,最多能放 1048576 个 conn。
#define MAX_PORTS 20     //最多监听 20 个端口。
#define TIME_SUB_MS(tv1, tv2)  ((tv1.tv_sec - tv2.tv_sec) * 1000 + (tv1.tv_usec - tv2.tv_usec) / 1000) //计算两个时间点相差多少毫秒。

int accept_cb(int fd);   //监听 socket 有新连接时调用
int recv_cb(int fd);    //客户端 socket 有数据可读时调用
int send_cb(int fd);    //客户端 socket 可以写数据时调用
//三个回调函数声明。 提前声明是因为后面的 event_register() 里面会用到

int epfd = 0;   //这个是整个服务器的 epoll 实例 fd。后面会有:epfd = epoll_create(1); 创建成功后,epfd 就代表一个 epoll 对象。
struct timeval begin;   //记录时间

struct conn conn_list[CONNECTION_SIZE] = {0};  //conn_list[fd]保存了某个 fd 对应的信息。

int set_event(int fd,int event,int flag){  //set_event 是对 epoll_ctl 的简单封装,作用是"告诉 epoll:请帮我盯着这个 fd 的某种事件"。
/*参数: fd:表示你要操作哪个 socket 可能是监听socket也可能是客户端socket  参数 event:表示你关心什么事件。 可读/可写  参数 flag:第一次注册用 ADD
后续切换事件用MOD  flag == 1就是第一次添加 flag == 0就是后续切换*/
/*EPOLL_CTL_ADD    添加:第一次让 epoll 盯着这个 fd
EPOLL_CTL_MOD    修改:已经盯着了,但我想换一种盯法(比如从盯读改成盯写)
EPOLL_CTL_DEL    删除:不要再盯了(fd 关闭时常用)*/


    if(flag){  //flag == 1第一次添加  
        struct epoll_event ev;  //创建 epoll_event ev是告诉epoll:我要监听哪个 fd 的什么事件。
        ev.events = event;   //设置监听事件 比如如果是EPOLLIN 就代表监听可读事件
        ev.data.fd = fd;   //绑定 fd
        epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev);  //把 fd 添加到 epfd 这个 epoll 实例中。
//四个参数分别是:epfd:哪个epoll实例 EPOLL_CTL_ADD:添加操作 fd:要添加哪个 fd &ev:这个 fd 要监听什么事件
    }else{  //flag == 0修改已经有的
        struct epoll_event ev;
        ev.events = event;
        ev.data.fd = fd;
        epoll_ctl(epfd,EPOLL_CTL_MOD,fd,&ev);  //其他和上面一样 只有这里变成MOD 表示这个fd已经在epoll里面了,现在只是修改它监听的事件。
    }
}
/*使用时机:时机 1:服务器启动时,把监听 socket 交给 epoll 盯着 位置:main 函数里 set_event(sockfd, EPOLLIN, 1);
时机 2:新客户端连接进来,把这个新连接也交给 epoll 盯着 位置:event_register 函数里(accept_cb 调用它) set_event(fd, event, 1);
时机 3:收到客户端数据后,告诉 epoll "我现在要写数据了,请盯写事件" 位置:recv_cb 函数末尾 set_event(fd, EPOLLOUT, 0);
时机 4:数据发完后,告诉 epoll "改回去,继续盯读事件" 位置:send_cb 函数末尾 set_event(fd, EPOLLIN, 0);
每当你需要"让 epoll 开始盯一个 fd"或"修改正在盯的方式",就调用 set_event。*/

int event_register(int fd,int event){  //函数的作用:初始化一个客户端连接,并把它注册到 epoll 中。
    if(fd < 0) return -1;    //判断fd合法

    conn_list[fd].fd = fd;  //保存fd
    conn_list[fd].r_action.recv_callback = recv_cb;  //绑定读回调recv_cb
    conn_list[fd].send_callback = send_cb;  //绑定写回调 send_cb

    memset(conn_list[fd].rbuffer,0,BUFFER_LENGTH);
    conn_list[fd].rlength = 0;     //初始化这个连接的读缓冲区。
    memset(conn_list[fd].wbuffer,0,BUFFER_LENGTH);
    conn_list[fd].wlength = 0;   //初始化这个连接的写缓冲区。

    set_event(fd,event,1);   //把这个 fd 加入 epoll。
}

/*在注册阶段,也就是事件发生之前,先把fd准备好。event_register负责初始化新客户端连接,并绑定 recv_cb / send_cb。*/



int accept_cb(int fd){   //监听 fd 发生 EPOLLIN 事件时,调用 accept_cb。这里的fd是监听fd,不是客户端fd

    struct sockaddr_in clientaddr;  //声明 sockaddr_in 结构体 下面调用 accept 时,操作系统会把客户端的 IP 和端口写到这个变量里——这样我们就能知道"是谁连进来了"。
    socklen_t len = sizeof(clientaddr);  //声明长度变量 下面accept要用

    int clientfd = accept(fd,(struct sockaddr*)&clientaddr,&len);  //调用函数 accept,从监听 fd 的"等待队列"里取出一个客户端连接。
//3个参数:sockfd:监听 socket的fd addr:指向一块内存,accept会把客户端地址写进去(IP+端口) addrlen:指向socklen_t的指针
//这里强转为通用接口struct sockaddr*
    if(clientfd < 0){
        printf("accept errno: %d ---> %s\n",errno,strerror(errno));  //clientfd非法 strerror(errno)是把errno解耦成人类能看懂的语言
        return -1;
    }
    event_register(clientfd,EPOLLIN);   // 注册新客户端 EPOLLIN | EPOLLET 是边缘触发
//LT(水平触发):只要 fd 上有数据没读完,每次 epoll_wait 都会通知你。容错强,但效率略低。
//ET(边缘触发):只在状态变化的瞬间通知一次,没读完是你的事。高效,但写错容易丢数据。
//新连接的整个建立流程到这里就完成了。 下面是性能统计部分
    if(clientfd % 1000 == 0){   //每当 clientfd 是 1000 的倍数时,打印一次统计信息。
        struct timeval current;    //声明一个 timeval 变量,然后调用 gettimeofday 把"当前时间"写进去。 获取到当前时间
        gettimeofday(&current,NULL);   

        int time_used = TIME_SUB_MS(current,begin);  //计算从上次记录的时间点 begin 到当前 current 经过了多少毫秒。
        memcpy(&begin,&current,sizeof(struct timeval));   //把 current 的内容复制到 begin,作为下一次计算的"起点"。

        printf("accept finished: %d,time_used: %d\n",clientfd,time_used); //打印性能日志
    }
    return 0;

}

//accept_cb 不负责和客户端通信,它只负责 accept 新连接,然后把新的 clientfd 注册到 epoll 和 conn_list 里,让这个 clientfd 以后由 recv_cb / send_cb 处理。


int recv_cb(int fd){  //当某个客户端 fd 上有数据可读时,调用 recv 把数据收进来,交给协议层处理,然后切换关注写事件。
    //这里的fd是客户端fd,不是监听fd
    memset(conn_list[fd].rbuffer,0,BUFFER_LENGTH);  //清空读缓冲区 防止本次接收残留上次接收的数据
    int count = recv(fd,conn_list[fd].rbuffer,BUFFER_LENGTH,0);  //真正从客户端读数据。
//第1个参数 fd:从哪个客户端socket读  第2个参数 conn_list[fd].rbuffer:读到的数据放哪里
//第3个参数 BUFFER_LENGTH:最多读多少字节,这里是1024  第 4 个参数 0:默认读取方式 返回值 count 表示:实际读到了多少字节
    if(count == 0){  //返回0:对端关闭了连接(client 调用了 close) 应该关闭我方 fd,从 epoll 移除
        printf("client disconnect : %d\n",fd);  //打印日志

        epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);  //把 fd 从 epoll 监听集合里移除。
        close(fd);    //关闭fd

        return 0;
    }else if(count < 0){  //count < 0 读取出错
        printf("count: %d,errno: %d,%s\n",count,errno,strerror(errno));  //打印日志
        epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL); //把fd从epoll家你听集合中删除
        close(fd);   //关闭fd
        return 0;
    }

    conn_list[fd].rlength = count;  //记录数据长度

    //三选一的协议处理
#if 0 // echo回显  最简单的回显服务器:把刚收到的数据原样复制到写缓冲区,等会发给客户端就是。
    conn_list[fd].wlength = conn_list[fd].rlength;
    memcpy(conn_list[fd].wbuffer,conn_list[fd].rbuffer,conn_list[fd].wlength);

    printf("[%d]RECV : %s\n",conn_list[fd].rlength,conn_list[fd].rbuffer);
#elif 0  //HTTP 协议 后续在server.c中补充
    http_request(&conn_list[fd]); 
#else   //WebSocket 协议(当前激活) 调用 ws_request 也在server.c中补充
    ws_request(&conn_list[fd]);
#endif

    set_event(fd,EPOLLOUT,0);//切换关注事件 把 fd 在 epoll 中的关注事件从 EPOLLIN 改成 EPOLLOUT。
//发送缓冲区绝大多数时候都是空的(因为内核很快把数据发出去了)。如果一直关注 EPOLLOUT,epoll 会一直通知你"可以写了",但没东西要写所以读完切写
    return count;   //返回收到的字节数
}

int send_cb(int fd){   //当某个 fd 的发送缓冲区有空间时,把 wbuffer 里准备好的响应发出去,然后切回关注 EPOLLIN。
// 客户端连接 fd 触发 EPOLLOUT 事件时调用
#if 0
    http_response(&conn_list[fd]);    //http协议 后会实现
#else
    ws_response(&conn_list[fd]);   //也是未实现部分 WebSocket服务器
#endif
    int count = 0;
#if 0    //"协议状态机"的设计 支持分多次发送
    if(conn_list[fd].status == 1){    //状态 1:发数据,但发完之后还要继续发(保持 EPOLLOUT)
        count = send(fd,conn_list[fd].wbuffer,conn_list[fd].wlength,0);
        set_event(fd,EPOLLOUT,0);
    }else if(conn_list[fd].status == 2){    // 状态 2:现在不发,但保持关注 EPOLLOUT
        set_event(fd,EPOLLOUT,0);
    }else if(conn_list[fd].status == 0){    // 状态 0:发完了,切回 EPOLLIN
        if(conn_list[fd].wlength != 0){
            count = send(fd,conn_list[fd].wbuffer,conn_list[fd].wlength,0);
        }
        set_event(fd,EPOLLIN,0);
    }
#else

        if(conn_list[fd].wlength != 0){     //如果写缓冲区里有数据,就发送。
            count = send(fd,conn_list[fd].wbuffer,conn_list[fd].wlength,0);
        }

        set_event(fd,EPOLLIN,0);   //发送完切回 EPOLLIN  避免一直告诉你可以写的空转 

#endif

        return count;

}

int init_server(unsigned short port){   //创建一个监听某个端口的 TCP socket,并返回这个 socket 的 fd。
//只在程序启动时被调用,被 main 函数循环调用 20 次
    int sockfd = socket(AF_INET,SOCK_STREAM,0);  //调用 socket() 系统调用,创建一个新的 socket,得到一个 fd。
//三个参数:协议族  socket 类型(流式还是数据报)  具体协议(一般传 0 让系统选)
    struct sockaddr_in servaddr;   //声明地址结构体
    servaddr.sin_family = AF_INET;   //把 sin_family 字段填成 AF_INET,告诉系统"这是 IPv4 地址"。
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);   //填 IP 地址 0.0.0.0 绑定到本机的所有网卡 IP 地址 注意要转化成网络字节序
     
    servaddr.sin_port = htons(port);   //把端口号转成网络字节序后填进去。 0-1023需要特权

    if(-1 == bind(sockfd,(struct sockaddr*)&servaddr,sizeof(struct sockaddr_in))){  //调用 bind() 系统调用,把 socket 和地址绑定
        printf("bind failed: %s\n",strerror(errno));
        close(sockfd);
        return -1;
    }
    listen(sockfd,10);   //调用 listen() 系统调用,把 socket 变成监听状态。

    return sockfd;   //// 返回监听 fd,main 会拿去注册到 epoll
}

int main(){
    unsigned short port = 2000;

    epfd = epoll_create(1);

    int i = 0;
    for(i = 0;i < MAX_PORTS;++i){     //启动 20 个监听端口 循环 20 次,每次:创建一个监听 socket(端口 = 2000 + i)
//给监听 fd 在 conn_list 里建档  注册到 epoll,关注 EPOLLIN
        int sockfd = init_server(port + i);
        conn_list[sockfd].fd = sockfd;
        conn_list[sockfd].r_action.recv_callback = accept_cb;

        set_event(sockfd,EPOLLIN,1);   //注册到 epoll  关注 EPOLLIN(读事件)。
    }
    gettimeofday(&begin,NULL);   //获取当前时间,写到全局变量 begin
    while(1){
        struct epoll_event events[1024] = {0};
        int nready = epoll_wait(epfd,events,1024,-1);   //调用 epoll_wait() 系统调用,等待至少一个事件就绪,然后返回就绪事件的数量。
        int i = 0;
        for(i = 0;i < nready;++i){  //处理就绪事件
            int connfd = events[i].data.fd;
#if 0      //只处理一个事件。
            if(events[i].events & EPOLLIN){
                conn_list[connfd].r_action.recv_callback(connfd);
            }else if(events[i].events & EPOLLOUT){
                conn_list[connfd].send_callback(connfd);
            }
#else   //用两个独立 if,同一 fd 的多个事件都处理。
            if(events[i].events & EPOLLIN){
                conn_list[connfd].r_action.recv_callback(connfd);
            }
            if(events[i].events & EPOLLOUT){
                conn_list[connfd].send_callback(connfd);
            }
#endif
        }

    }


}
/*main 函数是 Reactor 的事件循环中心。
启动时,它创建 epoll 和监听 socket;
然后把监听 socket 绑定 accept_cb 并注册到 epoll;
运行时,它通过 epoll_wait 等待所有 fd 的事件;
事件来了以后,它不直接 accept/recv/send,
而是根据 conn_list[fd] 里提前绑定好的 callback 来调用对应函数。*/


server.h代码

#ifndef __SERVER_H__   //头文件保护宏
#define __SERVER_H__

#define BUFFER_LENGTH  1024   //每个连接的读缓冲区、写缓冲区都是 1024 字节。

typedef int (*RCALLBACK)(int fd);  //RCALLBACK 是一种函数指针类型。 这种函数接收一个 int fd,返回 int。
//int accept_cb(int fd); int recv_cb(int fd); int send_cb(int fd); 这些函数都是符合要求的函数

struct conn{   //结构体:一个连接的信息。
    int fd;    //主文件里面写:struct conn conn_list[CONNECTION_SIZE] = {0};  这里的下标就是fd  epoll_wait 返回 fd 后,可以 O(1) 找到这个 fd 对应的连接信息。

    char rbuffer[BUFFER_LENGTH]; //读缓冲区:从客户端收到的数据放这
    int rlength;   //读缓冲区里实际有多少字节有效数据

    char wbuffer[BUFFER_LENGTH];  //写缓冲区:要发给客户端的数据放这
    int wlength;   //写缓冲区里待发送的字节数

    RCALLBACK send_callback;   //// 写就绪时调用的回调(指向 send_cb)

    union{
        RCALLBACK recv_callback;
        RCALLBACK accept_callback;
    }r_action;
// 联合体:监听 fd 用 accept_callback,普通连接 fd 用 recv_callback
// 它们永远不会同时存在,所以可以共用内存

//可以用这种方式进行赋值:conn_list[fd].r_action.recv_callback = recv_cb; conn_list[fd].send_callback = send_cb;


    int status;  // 状态机字段(HTTP/WS 协议解析用)
    


#if 1 // websocket
    char *payload;   //WebSocket 真正的数据内容
    char mask[4];   //WebSocket 客户端数据会带 4 字节掩码
#endif

};

int http_request(struct conn *c);  // HTTP 请求解析
int http_response(struct conn *c);   // HTTP 响应构造

int ws_request(struct conn *c);    // WebSocket 请求解析
int ws_response(struct conn *c);   // WebSocket 响应构造
//具体实现在server.c中

#endif

以下是新置内容:

webserver.c代码

-----在recv_cb 收到客户端请求字节后调用

代码逻辑:

recv_cb()收到客户端请求字节->ws_request(&conn)->http_request(&conn)->切换 EPOLLOUT->
send_cb() 准备发响应->http_response(&conn)->send 把字节发出去

#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <sys/sendfile.h>
#include <errno.h>


#include "server.h"
//引入 struct conn、BUFFER_LENGTH、http_request/http_response 的声明。


#define WEBSERVER_ROOTDIR	"./"
//定义"网站根目录"为当前目录。
//http_request:解析客户端请求
//http_response:构造服务端响应

以上是项目头文件和宏定义

http_request 函数

int http_request(struct conn *c){        //处理 HTTP 请求函数 清空 wbuffer,重置 wlength,重置 status,为 http_response() 生成响应做准备。
//解析 c->rbuffer 里的 HTTP 请求字节流,提取出关键信息(方法、URL、头部、body),存到struct conn的字段里,供后续处理使用。
//每次客户端发来数据,把字节读进 rbuffer 后,立刻调用 http_request 解析。
//传入指针 可以直接查找
    printf("request: %s\n", c->rbuffer);
    memset(c->wbuffer,0,BUFFER_LENGTH);  //清空wbuffer 避免上次响应的残留。
    c->wlength = 0;   //重置写缓冲区长度
    c->status = 0;    //把 status 字段设为 0。 状态机重置回初始状态,准备处理新一轮的请求-响应。
}

/*完整的 HTTP 响应报文:
状态行:HTTP/1.1 200 OK\r\n —— 协议版本、状态码、状态描述
响应头:每一行一个键值对,用 \r\n 结尾
空行 + 响应体:响应头结束后必须有一个空行(\r\n\r\n),然后才是响应体内容
*/

这里并没有真正实现解析 只是实现了清空缓冲区以及一些初始化 保证下面request可以正常进行

http_response 函数

int http_response(struct conn *c){   //生成http响应的函数

#if 0

//HTTP 头 + HTML 正文,都写死在 sprintf 里 
    c -> wlength = sprintf(c -> wbuffer,       //HTTP 协议规定,一行结束用:\r\n
    "HTTP/1.1 200 OK\r\n"                 // 响应行   HTTP版本 状态码 状态描述
    "Content-Type: text/html\r\n"          // 响应头   响应体是 HTML 文本,告诉浏览器怎么渲染
    "Accept-Ranges: bytes\r\n"             // 响应头    告诉客户端服务器支持按字节范围请求(断点续传相关)
    "Content-Length: 82\r\n"               // 响应头    响应体长度,这里硬编码写死 82
    "Date: Tue, 30 Apr 2024 13:16:46 GMT\r\n\r\n" // 响应头结束 + 空行  响应生成时间
    "<html><head><title>0voice.king</title></head><body><h1>King</h1></body></html>\r\n\r\n");             // 响应体       



#elif 0

//HTTP 头还是 sprintf 生成 HTML 正文从 index.html 文件读取
        int filefd = open("index.html",O_RDONLY);   //以只读方式打开当前目录下的 index.html 返回文件描述符fd
        struct stat stat_buf;   //定义一个结构体变量 用来存文件信息
        fstat(filefd,&stat_buf);  //根据文件描述符 filefd 获取文件状态信息,并填充到 stat_buf 里。
        //stat_buf填充了许多信息 下面用st_size 代表index.html的文件大小,单位是字节

        c->wlength = sprintf(c->wbuffer, 
		"HTTP/1.1 200 OK\r\n"
		"Content-Type: text/html\r\n"
		"Accept-Ranges: bytes\r\n"
		"Content-Length: %ld\r\n"
		"Date: Tue, 30 Apr 2024 13:16:46 GMT\r\n\r\n",   //这里只生成了响应头 没有body
		stat_buf.st_size);     //这个size刚刚存在stat_buf里面了

	int count = read(filefd, c->wbuffer + c->wlength, BUFFER_LENGTH - c->wlength);
    //read() : ssize_t read(int fd, void *buf, size_t count) 从 fd 读最多 count 字节到 buf,返回实际读到的字节数
    //wbuffer 前面已经放了 HTTP 响应头。所以从c->wbuffer + c->wlength 开始读 BUFFER_LENGTH - c->wlength是剩余可用空间
    //也就是 跳过已经写完的响应头 文件内容接着往下写
	c->wlength += count;   //累加实际读到的字节数 

	close(filefd);   // 关闭文件

#elif 0
//状态机版本
        int filefd = open("index.html",O_RDONLY);  ////以只读方式打开当前目录下的 index.html 返回文件描述符fd
        //HTTP 响应头:放进 wbuffer ,index.html 文件正文:用 sendfile 直接发给客户端 之前的是read()函数,将文件内容读入wbuffer 也就是wbuffer里只装响应头
//具体状态:status == 0:准备响应头 status == 1:发送文件正文 status == 2:清理状态
        struct stat stat_buf;   //定义一个结构体变量 用来存文件信息
        fstat(filefd,&stat_buf);   //根据文件描述符 filefd 获取文件状态信息,并填充到 stat_buf 里。

        if(c->status == 0){
            c->wlength = sprintf(c->wbuffer, 
		"HTTP/1.1 200 OK\r\n"
		"Content-Type: text/html\r\n"
		"Accept-Ranges: bytes\r\n"
		"Content-Length: %ld\r\n"
		"Date: Tue, 30 Apr 2024 13:16:46 GMT\r\n\r\n",   //这里只生成了响应头 没有body
		stat_buf.st_size);     //这个size刚刚存在stat_buf里面了
            
            c->status = 1;     //将状态置为1
        }else if(c->status == 1){  //响应头已经准备好了,下一次该发送文件正文了  负责发送 index.html 文件正文。
	        int ret = sendfile(c->fd, filefd, NULL, stat_buf.st_size);  //sendfile 函数来发送文件正文
            //sendfile四个参数:客户端fd(输出目标),文件(输入来源),使用文件当前偏移量,最多发送的整个文件大小
            //这里不用read,是因为大文件时候,wbuffer里面可能装不下,因此直接sendfile了
            if (ret == -1) {
		        printf("errno: %d\n", errno);    //检查sendfile是否成功
	        }

            //c->wlength = 0;
	        //memset(c->wbuffer, 0, BUFFER_LENGTH);   注释掉了,清理动作可以放在下一个状态

	        c->status = 2;   // 文件正文发送完毕,下一阶段进入清理状态

        }else if(c -> status == 2){
		    // 当前没有需要通过 wbuffer 发送的数据了
		    c->wlength = 0;

		    // 清空写缓冲区,防止响应头残留
		    memset(c->wbuffer, 0, BUFFER_LENGTH);

		    // 状态恢复到 0,方便下一次请求重新开始
		    c->status = 0;
        }


#else 1    // 返回一张 PNG 图片给浏览器
        int filefd = open("c1000k.png", O_RDONLY);   // 打开当前目录下的 c1000k.png 图片文件
        struct stat stat_buf;    // 定义 stat 结构体变量,用来保存文件信息
        fstat(filefd, &stat_buf);   // 根据文件描述符 filefd 获取图片文件信息
        if (c->status == 0) {

		// 把 HTTP 响应头写入 c->wbuffer
		c->wlength = sprintf(c->wbuffer, 
			"HTTP/1.1 200 OK\r\n"                 // 状态行:HTTP 1.1,200 表示成功
			"Content-Type: image/png\r\n"          // 响应体类型:PNG 图片 image/png 表示响应体是一张 PNG 图片
			"Accept-Ranges: bytes\r\n"             // 表示支持字节范围请求
			"Content-Length: %ld\r\n"              // 响应体长度,也就是图片大小
			"Date: Tue, 30 Apr 2024 13:16:46 GMT\r\n\r\n", // \r\n\r\n 表示响应头结束
			stat_buf.st_size);

		c->status = 1;   //status改为1

	} else if (c->status == 1) {

		// 发送图片正文
		// c->fd:客户端 socket,表示发送目标 filefd:c1000k.png 文件描述符,表示发送来源 NULL:使用文件当前偏移量 stat_buf.st_size:最多发送整个图片文件大小
		int ret = sendfile(c->fd, filefd, NULL, stat_buf.st_size);


		if (ret == -1) {
			printf("errno: %d\n", errno);   //显示报错
		}

		// c->wlength = 0;
		// memset(c->wbuffer, 0, BUFFER_LENGTH);  被注释了,再status为2时清理

		// 图片正文发送完成,下一阶段进入清理状态
		c->status = 2;

	} else if (c->status == 2) {

		c->wlength = 0;

		// 清空写缓冲区,防止旧响应头残留
		memset(c->wbuffer, 0, BUFFER_LENGTH);

		// 状态恢复为 0,下一次请求可以重新开始
		c->status = 0;
	}


	// 关闭图片文件描述符
	close(filefd);


    
#endif 

    return c->wlength;
   
}

以上是所有的webserver.c的代码

大体思路:

客户端请求

server.c recv 读到 c->rbuffer

http_request(c)

清空 c->wbuffer
c->wlength = 0
c->status = 0

http_response(c)

根据分支生成响应:
1. 固定 HTML
2. read index.html
3. sendfile index.html
4. sendfile png

server.c send 发送 c->wbuffer

如果是 sendfile 分支:
再调用 http_response()
sendfile 发送文件正文

下面讲websocket.c:

先讲websocket到底在干嘛:
HTTP 是"一问一答":浏览器发个请求,服务器回个响应,连接就断了。想再要数据?重新建连接。
WebSocket 解决的就是这个:在一条 TCP 连接上,建立一个全双工(双向同时)的长连接通道,建好之后双方随时都能发消息,不用反复握手。

工作分两个阶段:
1.握手阶段(Handshake):借用一次 HTTP 请求来"升级"协议。客户端发一个特殊的 HTTP 请求,服务器算一个值回去,双方达成共识:"咱俩以后不说 HTTP 了,改说 WebSocket"。→ 对应代码里的 handshark() 函数。

2.数据传输阶段(Transmission):握手成功后,数据不再是 HTTP 文本,而是按 WebSocket 自己的二进制帧格式(Frame)来收发。→ 对应 decode_packet() / encode_packet() 函数。

websocket.c代码

头文件和宏定义:


#include <stdio.h>

#include "server.h"

#include <openssl/sha.h>
#include <openssl/pem.h>
#include <openssl/bio.h>
#include <openssl/evp.h>
#include <openssl/buffer.h>
#include <string.h>
//编译:gcc reactor.c websocket.c -o server -lssl -lcrypto

#define GUID  "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"    //WebSocket 协议规定死的固定字符串

类定义:

/*
socket流程::
key : NiLo7P3pIXkZXnb1Na1pQQ==
key + GUID : NiLo7P3pIXkZXnb1Na1pQQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11
对拼好的字符串做 SHA-1 哈希
SHA-1 输出固定 20 字节
再做 base64 编码
最后得到 Sec-WebSocket-Accept。浏览器收到这个结果后,会验证: 服务器是不是按 WebSocket 协议正确计算的?
返回给客户端,完成握手 handshark
握手后进入数据传输 transmission
收数据时 decode  服务器收到客户端 WebSocket 帧后,需要解码
发数据时 encode  服务器发送数据给客户端前,需要编码成 WebSocket 帧
*/

struct _nty_ophdr{   //WebSocket 数据帧的基础头部
	unsigned char opcode:4,  // 操作码,4 位   opcode 表示这一帧的数据类型。
			rsv3:1,          // 保留位,1 位 一般情况下为0
			rsv2:1,
			rsv1:1,
			fin:1;           // 是否是消息的最后一帧,1 位
	unsigned char payload_length:7,    // 数据长度,7 位
		mask:1;               // 是否加掩码,1 位  客户端发给服务器的数据必须 mask,服务器发给客户端的数据一般不 mask
		//所以后面有demask(),服务器收到浏览器数据时,要根据 mask_key 把真实内容还原出来。
}__attribute__((packed));  //让结构体按紧凑方式存储,不要自动填充对齐字节 防止解析错误


/*WebSocket 的 payload 长度不一定都能放进基础帧头的 7 bit 里。payload_length:7 代表payload_length 只占 7 bit。最多可以表示0 ~ 127
机制:0 - 125 :当前 payload_length 就是真实数据长度
126 :后面额外 2 字节表示真实数据长度,126并不是真实长度,这里可以理解成中等长度的数据
127 :后面额外 8 字节表示真实数据长度,127并不是真实长度,这里可以理解成长度很长的数据
也就是说,payload_length 为126 127的时候都是特殊标记,并不是真实的长度,真正的长度要去后面找
*/

/*
短数据整体格式:
基础帧头 2 字节
mask_key 4 字节
payload 数据
start = 6
*/

/*
中等长度数据,126情况:
基础头 2 字节
扩展长度 2 字节
mask_key 4 字节
payload 数据
start = 8
*/

/*
长数据,127情况:
基础头 2 字节
扩展长度 8 字节
mask_key 4 字节
payload 数据
start = 14
*/



struct _nty_websocket_head_126 {
	unsigned short payload_length;   // 2 字节 存扩展长度(真实长度)
	char mask_key[4];                // 4 字节 存掩码key websocket协议规定:浏览器/客户端 发给服务器的数据必须带 mask
	unsigned char data[8];           // 8 字节  占位字段,代表数据区起点
} __attribute__ ((packed)); 

struct _nty_websocket_head_127 {
	unsigned long long payload_length;  // 8 字节 存扩展长度(真实长度)
	char mask_key[4];                    // 4 字节 存掩码key
	unsigned char data[8];               // 8 字节 占位字段,代表数据区起点
} __attribute__ ((packed));


typedef struct _nty_websocket_head_127 nty_websocket_head_127;
typedef struct _nty_websocket_head_126 nty_websocket_head_126;
typedef struct _nty_ophdr nty_ophdr;      //这三行都是给结构体起别名

函数部分:

/*做完 SHA-1 哈希后,得到的是 20 个字节的二进制数据。二进制数据里啥都有——可能有 0(字符串结束符)、有换行符、有各种不可打印的控制字符。
这种东西没法直接塞进 HTTP 响应头里,因为 HTTP 头是纯文本协议,塞个二进制乱码进去,整个 HTTP 报文就废了。
base64充当翻译器,把任意二进制数据,翻译成只用64种可打印字符(A-Z、a-z、0-9、+、/,外加 = 补位)组成的纯文本。这样二进制就能安全地放进 HTTP 头里传输了。
base64_encode() 的输出结果最终会被放到: Sec-WebSocket-Accept: xxx */
int base64_encode(char *in_str, int in_len, char *out_str) {  //函数用来做Base64编码,三个参数:SHA1算出来的结果的地址 输入数据长度 输出缓冲区
	BIO *b64, *bio;     // 两个 BIO 指针,b64 是 base64 过滤层,bio 是内存缓冲层
	BUF_MEM *bptr = NULL;    // 指向内存缓冲区的结构体指针,最后用它取出结果 后面通过:BIO_get_mem_ptr(bio, &bptr);可以拿到内存 BIO 里的数据。
	size_t size = 0;    // 记录结果长度


	if (in_str == NULL || out_str == NULL)        
		return -1;    

	b64 = BIO_new(BIO_f_base64());      //创建一个 Base64 编码过滤器
	bio = BIO_new(BIO_s_mem());       //创建一个内存缓冲区
	bio = BIO_push(b64, bio);//把两个 BIO 串起来,组成 b64 → bio 的链条。意思是:数据先经过 b64(被 base64 编码),编码后的结果再流进 bio(存到内存里)。
	
	BIO_write(bio, in_str, in_len);     //把 in_str 开始的 in_len 个字节写入 BIO 链 
	BIO_flush(bio);    //刷新bio:把 BIO 里缓存的数据全部处理完并输出  不 flush 的话,最后几个字节可能丢失,结果不完整。 
	BIO_get_mem_ptr(bio, &bptr);     //把 BIO 内部的内存缓冲区地址取出来,放到 bptr 里 执行完 bptr->data 就是编码结果数据,bptr->length 就是结果长度。
	memcpy(out_str, bptr->data, bptr->length);  //内存拷贝 这句把 BIO 里保存的编码结果复制到调用者提供的 out_str 中。
	//拷贝:因为下一步要释放掉整个 BIO 管道,管道一释放,bptr->data 那块内存就没了,所以得先把数据搬到自己的地盘。  
	out_str[bptr->length-1] = '\0';     //去掉末尾换行
	size = bptr->length;    //记录结果长度

	BIO_free_all(bio);     //释放 BIO
	return size;
}
/*1. base64_encode() 是握手阶段用的,不是普通消息传输阶段用的
2. 它把 SHA1 后的二进制摘要转成 Base64 字符串
3. WebSocket 握手里的 Sec-WebSocket-Accept 需要这个结果
4. BIO 可以理解成 OpenSSL 的数据处理管道
5. BIO_write() 写入原始数据,BIO_flush() 刷新,BIO_get_mem_ptr() 取结果*/
/*整体作用:从 allbuf 这个大缓冲区里 从 level 这个位置开始读 一直读到 \r\n 为止
把这一行内容拷贝到linebuf 最后返回下一行的起始位置 也就是从 HTTP 请求头中读取一行。*/
int readline(char* allbuf,int level,char* linebuf){    //三个参数:完整的大缓冲区  从哪个下标开始读   输出缓冲区  返回值:下一行开始的位置
	int len = strlen(allbuf);    //计算 allbuf 这个字符串的长度

	for (;level < len; ++level)    {        
		if(allbuf[level]=='\r' && allbuf[level+1]=='\n')            
			return level+2;          //+2是为了跳过\r\n
		else            
			*(linebuf++) = allbuf[level];    //如果当前字符不是一行结束标记 就把当前字符复制到 linebuf 然后 linebuf 指针向后移动一格
	}    

	return -1;  //没找到结尾 就返回-1,表示读取失败
}
void demask(char *data,int len,char *mask){     //把客户端发来的 WebSocket payload 还原成真实数据
/*浏览器通过 WebSocket 发消息时,不是直接把明文发给服务器。 WebSocket 客户端发送给服务器的数据格式大概是: WebSocket 帧头 + mask_key + masked payload
服务器要先拿到 mask_key,然后对 payload 做一次异或运算,才能得到真实内容。这个函数就是帮助做masked payload → 原始 payload
*/

//三个参数 : 1.要被还原的数据地址。原地修改:解密结果直接写回 data 本身,不另开缓冲区。2.payload 的真实长度。 3.4 字节 mask key
	int i;    
	for (i = 0;i < len;i ++)          //循环处理每个字节 进行异或 把payload的每个字节都还原。
		*(data+i) ^= *(mask+(i%4));
}
//数据传输阶段用,把客户端发来的加掩码数据异或还原成明文。
/*1. 浏览器发给服务器的 WebSocket payload 是 masked,不是原文
2. mask_key 固定是 4 字节
3. demask() 用异或 ^ 还原原始数据
4. mask 使用规则是 mask[i % 4]
5. demask() 是原地修改 data
6. 如果没有 demask,服务器看到的是乱码*/
//解析客户端发来的 WebSocket 数据帧,找到真实 payload 的位置、长度、mask_key,然后调用 demask() 把数据还原出来。
/*这个函数要做三件事:
看懂帧头:这帧数据多长?(要处理前面写的 126/127 变长长度机制)
找出掩码:那 4 个字节的掩码 key 在哪?抠出来。
定位 + 解密数据:真正的负载数据从第几个字节开始?把它异或还原成明文,返回出去。
流程:
收到的一整段 WebSocket 数据 stream
        ↓
解析基础帧头 _nty_ophdr
        ↓
判断 payload 长度类型
        ↓
取出 mask_key
        ↓
找到 payload 真正开始的位置
        ↓
调用 demask() 还原 payload
        ↓
返回真实 payload 的地址*/
char* decode_packet(unsigned char *stream, char *mask, int length, int *ret) {
//四个参数:1.收到的完整 WebSocket 帧数据。一般是 c->rbuffer 2.保存解析出来的 4 字节 mask_key 一般传 c->mask 3. 收到的数据总长度 4.输出参数,用来把 payload 的真实长度传出去。
	nty_ophdr *hdr =  (nty_ophdr*)stream;  //把 stream 开头强转成基础帧头 转换后赋值给 hdr 
	unsigned char *data = stream + sizeof(nty_ophdr);  //让 data 指向跳过那 2 字节基础帧头之后的位置,也就是"帧头后面紧跟着的内容"的起点。
	int size = 0;   //定义size 保存真实 payload 长度。
	int start = 0;   //定义start 表示 payload 数据相对于 stream 开头的偏移量
	//char mask[4] = {0};
	int i = 0;  //循环计数器

	if (hdr->payload_length == 126){   //判断是否为126模式

		nty_websocket_head_126 *hdr126 = (nty_websocket_head_126*)data;  //把 data 解析成 126 扩展头
		size = hdr126->payload_length;  
		
		for (i = 0;i < 4;i ++) {
			mask[i] = hdr126->mask_key[i];   //把帧里面的 mask_key 保存到传入的 mask 数组中。
		}
		
		start = 8;  //2 字节基础头 + 2 字节扩展长度 + 4 字节 mask_key = 8  所以起始偏移量为8
		
	} else if (hdr->payload_length == 127) {  //判断是否为127模式

		nty_websocket_head_127 *hdr127 = (nty_websocket_head_127*)data; //把 data 解析成 127 扩展头
		size = hdr127->payload_length;
		
		for (i = 0;i < 4;i ++) {
			mask[i] = hdr127->mask_key[i];  //把帧里面的 mask_key 保存到传入的 mask 数组中。
		}
		
		start = 14;  // 2 字节基础头 + 8 字节扩展长度 + 4 字节 mask_key = 14 所以起始偏移量为14

	} else {
		size = hdr->payload_length;

		memcpy(mask, data, 4);
		start = 6;   //2基础头 + 4mask_key掩码 = 6
	}

	*ret = size;   //设置返回长度
	demask(stream+start, size, mask);  //调用 demask() 还原数据 把负载数据原地异或还原成明文

	return stream + start; //返回指向明文数据起点的指针。此刻stream+start那块内存已经被 demask 就地改成明文了,返回这个指针,调用者就能直接读到明文。
	 // demask 后,这块内存就变成真实消息内容
}
/*1. decode_packet() 是 WebSocket 拆包函数
2. stream 指向完整 WebSocket 帧
3. hdr 指向基础 2 字节帧头
4. size 表示真实 payload 长度
5. start 表示 payload 起始偏移
6. 短数据 start = 6
7. 126 扩展 start = 8
8. 127 扩展 start = 14
9. mask_key 必须取出来给 demask()
10. demask() 执行后,payload 才变成真实数据*/
//把服务器要发给客户端的普通字符串 stream,封装成 WebSocket 数据帧,放进 buffer 里。 浏览器收到:WebSocket帧头 + 真正的数据内容
/*1.造一个帧头(设置 fin=1、opcode=1 表示"这是一条完整的文本消息")。
2.根据数据长度,决定用哪种长度格式(还是那个 <126 / <65535 / 更大 的三分法,和 decode 对称)。
3.把帧头和数据拼到一块输出缓冲区里,返回总长度。*/
int encode_packet(char *buffer,char *mask, char *stream, int length) {
//四个参数: 1.输出缓冲区,最后封装好的 WebSocket 包,要写到这里面。2.输入,4 字节掩码 key 3.真正想要发送的明文数据 4.真正的数据长度
//返回 int:返回打包后整帧的总字节数(调用者据此知道要往网络上发多少字节)。
	nty_ophdr head = {0};  //定义一个 WebSocket 的前两个字节结构体。
	head.fin = 1;  //设置fin位为1,表示这是消息的最后一帧,因为这里每一条消息都是一帧发完,所以总是1
	head.opcode = 1; //设置opcode是1,表示发送的是文本消息
	int size = 0;

	if (length < 126) {   //数据长度小于 126
		head.payload_length = length;  //数据长度小于 126,直接塞进基础帧头那 7 位长度字段里。
		memcpy(buffer, &head, sizeof(nty_ophdr));  //把这个 2 字节的帧头拷进输出 buffer 的开头。
		size = 2;
	} else if (length < 0xffff) {
		nty_websocket_head_126 hdr = {0};   //造一个 head_126 扩展头,清零。
		head.payload_length = 126;  	// 告诉 WebSocket:后面使用 2 字节扩展长度
		hdr.payload_length = length;   //把真实长度写进那个 2 字节字段
		memcpy(hdr.mask_key, mask, 4);  //把掩码拷进扩展头的 mask_key

		memcpy(buffer, &head, sizeof(nty_ophdr));  //先把 2 字节基础帧头拷到 buffer 开头
		memcpy(buffer+sizeof(nty_ophdr), &hdr, sizeof(nty_websocket_head_126));  //再把扩展头拷到 buffer 第 2 字节往后的位置(buffer+2)
		size = sizeof(nty_websocket_head_126);  //记录帧头总大小
		
	} else {
		
		nty_websocket_head_127 hdr = {0};
		head.payload_length = 127; 
		hdr.payload_length = length;
		memcpy(hdr.mask_key, mask, 4);
		
		memcpy(buffer, &head, sizeof(nty_ophdr));
		memcpy(buffer+sizeof(nty_ophdr), &hdr, sizeof(nty_websocket_head_127));

		size = sizeof(nty_websocket_head_127);
		
	}

	memcpy(buffer+size, stream, length);

	return length + size;
}
#define WEBSOCK_KEY_LENGTH	19    //宏定义 代表字符串 "Sec-WebSocket-Key: " 的长度
//等会儿要从 Sec-WebSocket-Key: QWz1vB... 这一整行里,跳过前面这 19 个字符的"标签部分",只取出后面真正的 key 值

/*处理浏览器发来的 WebSocket 握手请求,然后生成服务器的握手响应。
思路:WebSocket 一开始不是直接发 WebSocket 数据帧,而是先通过 HTTP 发一个升级请求。
服务器要从里面找到: Sec-WebSocket-Key: QWz1vB/77j8JcT/qtiLQ==  然后按照 WebSocket 协议计算一个: Sec-WebSocket-Accept
然后返回 才能证明 HTTP 已经升级成 WebSocket 连接了。*/
int handshake(struct conn *c) {
//参数 : conn指针 表示一条客户端链接,里面存着这个链接的所有状态
	//ev->buffer , ev->length

	char linebuf[1024] = {0};   //临时保存当前读到的这一行 HTTP 头。
	int idx = 0;   //表示当前读到 c->rbuffer 的哪个位置了 (HTTP请求头解析进度)
	char sec_data[128] = {0};  //存 SHA-1 算出来的 20 字节二进制结果。
	char sec_accept[32] = {0};  //存 base64 编码后的最终答案(Sec-WebSocket-Accept 的值)。

	do {   //do-while 循环 : 至少先读一行

		memset(linebuf, 0, 1024);   //每轮先清空 linebuf
		idx = readline(c->rbuffer, idx, linebuf);  //读取一行 HTTP 头 每一次的idx指向下一行 有助于循环判断是否包括key

		if (strstr(linebuf, "Sec-WebSocket-Key")) {   //判断 linebuf 里面是否包含 "Sec-WebSocket-Key"

			//linebuf: Sec-WebSocket-Key: QWz1vB/77j8JcT/qtiLQ==
			strcat(linebuf, GUID);   //把固定 GUID 拼到当前这一行后面。

			//linebuf: 
			//Sec-WebSocket-Key: QWz1vB/77j8JcT/qtiLQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11

			
			SHA1(linebuf + WEBSOCK_KEY_LENGTH, strlen(linebuf + WEBSOCK_KEY_LENGTH), sec_data); // openssl  计算SHA1
//三个参数: 1.表示要计算 SHA1 的字符串起始地址。2.要计算多少字节。也就是这个字符串的长度。3.把 SHA1 计算结果放到哪里。

			base64_encode(sec_data, strlen(sec_data), sec_accept); //把 SHA1 出来的 20 字节二进制摘要转换成 Base64 字符串。sec_accept 最后保存的就是:Sec-WebSocket-Accept

			memset(c->wbuffer, 0, BUFFER_LENGTH);  //清空发送缓冲区 避免残留旧数据
			//下面组织 HTTP 101 响应
			c->wlength = sprintf(c->wbuffer, "HTTP/1.1 101 Switching Protocols\r\n"  //状态码 101,意思是"协议切换" 表示"同意从 HTTP 切换到 WebSocket"
					"Upgrade: websocket\r\n"  //告诉客户端"升级到 websocket 协议"
					"Connection: Upgrade\r\n"  //告诉客户端"这个连接要做升级处理"。
					"Sec-WebSocket-Accept: %s\r\n\r\n", sec_accept); //我们计算的结果 浏览器就靠这个判断服务器是不是正确处理了握手。

			printf("ws response : %s\n", c->wbuffer);

			break;
			
		}

	} while((c->rbuffer[idx] != '\r' || c->rbuffer[idx+1] != '\n') && idx != -1 );  //只要还没读到 HTTP 头部结束的空行,就继续读。

	return 0;
}
int ws_request(struct conn *c) {//在服务器每次从某条连接收到数据时被调用。
	//它看一眼这条连接当前的 status,决定是去握手还是去解析数据帧。它是"读事件"的处理总入口。

	printf("request: %s\n", c->rbuffer);

	if (c->status == 0) {    //当前连接刚建立,还没有完成 WebSocket 握手。
		
		handshake(c);   //执行握手
		c->status = 1;  //修改状态 这个连接以后进入 WebSocket 数据传输阶段。
		
	} else if (c->status == 1) {  //已经握手过了,现在收到的是 WebSocket 数据帧
		char mask[4] = {0};  //准备两个局部变量。mask 存掩码
		int ret = 0;  //ret 接收"解析出的数据长度"

		c->payload = decode_packet(c->rbuffer, c->mask, c->rlength, &ret);  //调用解析函数
//四个参数 :1.reactor从socket读到的数据。[WebSocket帧头] [mask key] [被mask后的数据] 2.用于保存从 WebSocket 帧里解析出来的 mask key。
//3.当前读到的数据总长度 4.把 ret 的地址传进去 这样 decode_packet() 里面才能修改外面的 ret
		printf("data : %s , length : %d\n", c->payload, ret);  //c->payload 指向解码后的业务数据。

		c->wlength = ret;  //把数据长度存进 c->wlength

		c->status = 2; //状态推进到 2,表示"数据解析好了,等着 ws_response 来打包回复"
	}

	return 0;
}
int ws_response(struct conn *c) {
//当连接可以发送数据时,把要发送的 payload 封装成 WebSocket 帧。准备发数据时调用
	if (c->status == 2) { //只有当"数据已解析完,等着回复"时才动作。其他状态(0 握手中、1 等收数据)下,这个函数什么都不做。
		
		c->wlength = encode_packet(c->wbuffer, c->mask, c->payload, c->wlength);  //把普通 payload 封装成 WebSocket 帧。
//四个参数:1.封装好的 WebSocket 响应帧会写进去 2.解析出来的 mask 3.要发送的真正数据 4.待发送 payload 的长度
		c->status = 1;  //修改状态
	}

	return 0;
}

总结:

reactor部分:

Reactor 是一种网络服务器的工作模式:用一个(或几个)线程,通过 epoll 这个机制同时监视成千上万条连接,哪条连接有"事件"发生(可读/可写),就把那个事件"分发"给对应的处理函数。

Reactor :用一个 epoll 同时盯着所有连接,自己不傻等,而是"睡觉",直到 epoll 告诉它"喂,这几条连接有数据了",才醒来处理这几条。 没事件就休眠,有事件才干活,一个线程就能扛住海量连接。这就是所谓的 I/O 多路复用

webserver部分:

webserver.c 是 HTTP 协议处理模块,主要负责处理浏览器发来的 HTTP 请求,并构造 HTTP 响应返回给客户端。它本身不负责 accept、recv、send 等底层网络事件,这些由 reactor.c 负责。

当浏览器访问服务器时,reactor.c 中的 recv_cb 会先通过 recv 将 HTTP 请求读入 conn 的 rbuffer,然后调用 http_request。当前 http_request 主要负责清空写缓冲区、初始化写长度,并设置连接状态,为后续响应做准备。

随后 reactor.c 将该 fd 的监听事件切换为 EPOLLOUT。当连接可写时,send_cb 被触发,并调用 http_response。http_response 根据当前状态组织响应内容。如果是简单 HTML 响应,它会直接使用 sprintf 将 HTTP 响应头和 HTML 正文写入 wbuffer,并记录 wlength,随后由 send_cb 调用 send 发送给浏览器。

如果返回的是图片或大文件,http_response 可以使用状态机分阶段发送。第一次 status 为 0 时,先构造 HTTP 响应头,并将 status 改为 1;下一次 EPOLLOUT 事件再次触发 send_cb 时,http_response 进入 status 为 1 的分支,通过 sendfile 发送真正的文件内容。发送完成后再将状态恢复,继续等待下一次请求。

因此,webserver.c 的核心逻辑可以概括为:收到 HTTP 请求后,准备响应;可写事件到来时,构造 HTTP 响应头和响应体;最后由 reactor.c 统一发送。它和 websocket.c 的区别在于,webserver.c 处理的是普通 HTTP 请求响应模型,而 websocket.c 先通过 HTTP 完成协议升级,之后再使用 WebSocket 帧进行长连接通信。

websocket部分:

本 WebSocket 服务器基于 Reactor + epoll 实现。reactor.c 负责网络事件分发,包括监听连接、接收数据和发送数据;websocket.c 负责 WebSocket 协议处理,包括握手、解码客户端数据帧和封装服务器响应帧。

程序启动后,main 函数创建 epoll 实例,并监听指定端口。当有客户端连接到来时,accept_cb 接收连接并将 clientfd 注册到 epoll。客户端发送数据后,recv_cb 将数据读入连接对象的 rbuffer 中,然后调用 ws_request 进入 WebSocket 协议处理流程。

ws_request 根据连接状态 status 判断当前阶段。若 status 为 0,说明连接尚未完成 WebSocket 握手,此时调用 handshake 解析 HTTP Upgrade 请求,从请求头中取出 Sec-WebSocket-Key,拼接 WebSocket 协议规定的 GUID,经过 SHA1 和 Base64 计算生成 Sec-WebSocket-Accept,并构造 HTTP 101 Switching Protocols 响应。握手完成后将 status 设置为 1。

当 status 为 1 时,说明连接已经进入 WebSocket 数据传输阶段。此时收到的数据不再是普通 HTTP 报文,而是 WebSocket 数据帧。ws_request 调用 decode_packet 解析帧头,判断 payload 长度类型,取出 mask_key,定位真实 payload 的起始位置,并调用 demask 对客户端数据进行解掩码,最终得到明文消息。

收到并解析完消息后,连接状态被设置为 2。随后 epoll 切换到可写事件,send_cb 被调用,并进一步调用 ws_response。ws_response 在 status 为 2 时,将要发送的明文 payload 通过 encode_packet 封装成 WebSocket 数据帧,写入 wbuffer,然后由 send_cb 调用 send 发送给客户端。发送完成后,连接重新回到等待下一次数据的状态。

零声社区资源链接:https://github.com/0voice

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐