http实现与websocket
预备知识
1.水平触发和边沿触发
水平触发是只要被监听的事件是可读状态,就会触发相应回调函数;
边沿触发是只有新的状态改变,才会触发相应回调函数。
换个方式理解:
对于水平触发,epoll监听的状态,如果一直是可读状态,就会一直调用回调函数;如果是边沿触发状态,只有状态改变,才会调用回调函数;
举例:
每次建立一个tcp连接,会有相应的clientfd文件描述符生成,假设此时客户端发送100个字符的数据,服务端的内核会把这100字符存放在buffer缓冲区中。当epoll监听到该clientfd可读时,就会作为事件驱动去调用回调函数,假设回调函数每次只能读取10个字符,那么本次回调函数只能获取前10个字符,结束本次回调。此时内核缓冲区中还剩下90个字符。
进入下一轮循环后,epoll如果是水平触发的状态,那么操作系统内核看到缓冲区还有字符,就会继续作为事件驱动调用回调函数再次读取接下来10个字符。直到循环10次,才能把第一次客户发送的数据读取完毕。
如果是边缘触发状态,即使进入了下一轮,内核不会因为缓冲区有数据而作为事件驱动,所以不会调用回调函数读取缓冲区的数据,只有当再一次客户端发送数据时,此时操作系统内核才会把该事件作为驱动,去调用回调函数,此时虽然客户端发送了两次数据,但是服务器才仅仅把第一次的第11个字符到第20个字符读取出来。
当然,如果使用边缘触发,可以在读取的时候采用while循环,这样就等同于水平触发了。
2.sprintf函数
把格式化的字符串写入到相应的字符串中。
cpp
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:82\r\n"
"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");
把后面的字符串写入到wbuffer里面去,代表http响应的数据,相当于客户端是读取这部部分相应数据的。
相应部分的数据包含两部分:数据头和数据体
cpp
状态行: HTTP/1.1 200 OK
表示使用 HTTP/1.1 协议,响应状态为 "200 OK",即请求成功。
头部字段:
Content-Type: text/html 表示返回内容类型为 HTML 文档。
Accept-Ranges: bytes 表示支持字节范围请求。客户端可以请求部分文件。
Content-Length: 82 指明响应体的字节长度为82。这个值应该与实际返回内容的长度相对应。
Date: Tue, 30 Apr 2024 13:16:46 GMT 当前时间,表明该响应被生成时的日期和时间。
主体内容:
html
<html><head><title>0voice.king</title></head><body><h1>King</h1></body></html>
包含 HTML 内容,用于在网页上显示"King"标题。
3.stat结构体与stat()函数
stat
结构体用于获取文件的状态信息,例如文件大小、权限、创建时间等。它通常用于系统调用 stat()
、fstat()
和 lstat()
等,以便于在程序中获取关于文件或目录的详细信息。它们的主要功能是填充一个 stat
结构体,该结构体包含有关文件或目录的详细信息,如大小、权限、时间戳等。以下是对这三个函数的详细解释:
cpp
#include<stat.h>
struct stat stat_buff;
//调用fstat函数,把stat_buff结构体的内容填充完成,第一个参数是文件的fd
fstat(filefd, &stat_buff);
//调用stat函数,把stat_buff结构体的内容填充完成,第一个参数是文件的路径
stat("path", &stat_buff);
4.文件操作
open() close() read() write()
fopen() fclose() fread() fwrite()
第一组更倾向于底层的读取,更灵活;第二组更倾向于高层的读写,较为方便。
http
在之前的课程中完成了事件驱动,现在需要在原有逻辑上,实现一个http的事件驱动:如果有http请求,就返回相应内容。
相当于把之前的在回调函数里实现应用层的内容
在之前的课程中,实现了事件的驱动,即当有fd可读或可写状态时,直接调用相关的回调函数。在此基础上,通过应用层,以http为基础,实现一个webserver。当在浏览器输入地址敲回车后,服务器检测到有可读事件,调用
request()函数。当服务器内核检测到有可写事件时,调用response()函数。实现了简单的webserver,如下图:
代码如下:
cpp
int send_cb(int fd) {
http_response(&conn_list[fd]);
//http头和体一起发送
int count = send(fd, conn_list[fd].wbuffer, conn_list[fd].wlength, 0);
//发送完直接修改下一轮的监听状态
set_event(fd, EPOLLIN, 0);
return count;
}
新版本代码的逻辑在于,服务器epoll检测到可读后调用request,随后把监听事件event设为可写事件,当下一轮循环时监听到她可写,就执行response函数。在执行response时,对于http而言,需要发送两个内容,一个是HTTP头,一个是http体。但是在实际开发过程中,往往需要将这两个写入的事件分开,即服务器需要连着进行两次写操作,第一次写完成后,在下一轮监听之前,还是监听其epollout状态,所以上一版代码需要改进。
于是引入了状态机的概念,状态机就是在event_rigister结构体中,添加一个整型变量,用不同的值代表不同的状态,决定下一轮开始监听之前,相应的事件是哪种事件:
修改后的代码如下:
cpp
int sent_cb(int fd){
http_response(&conn_list[fd]);
int count = 0;
if(conn_list[fd].status == 1){
count = send(fd, conn_list[fd].wbuffer, conn_list[fd].wlength, 0);
set_event(fd, EPOLLOUT, 0);
}
else if(conn_list[fd].status == 2){
set_event(fd, EPOLLOUT, 0); //保持下一轮继续监听EPOLLOUT
}
else if(conn_list[fd].status == 0){
if(conn_list[fd].wlength != 0){
count = send(fd, conn_list[fd].wbuffer, conn_list[fd].wlength, 0);
}
set_event(fd, EPOLLIN, 0); //当HTTP头和体都发送完后,再将监听状态改为EPOLLIN
}
return count;
}
web_socket
以上所用的代码实现了一个webserver,基于此,可以设计出一个websocket。
什么是websocket?
WebSocket 是一种网络通信协议,旨在为客户端和服务器之间提供全双工、实时的通信通道。它是在 HTML5 规范中引入的,可以让浏览器与服务器进行持久化连接,以便实现低延迟的数据交换。
WebSocket 的特点:
- 全双工通信:客户端和服务器可以同时发送和接收消息,而不必等待对方完成操作。
- 轻量级:相较于传统的 HTTP 协议,WebSocket 头部信息更小,这减少了网络开销。
- 持久连接:一旦建立连接,双方可以一直保持这个连接,直到主动关闭。这样避免了频繁建立和关闭连接带来的性能损耗。
- 实时性:适合需要即时数据更新的应用,如在线聊天、游戏、股票行情等
如何实现?
新建一个文件,里面写两个函数用来替代之前webserver中的requeset 和 reponse即可。
课程地址:0voice · GitHub
一些心得
从webserver和websocket两个小项目出发,体会到,通过reactor的方式,可以很好的将网络管理和业务开发区分开。使用reactor框架,然后单独实现一个业务逻辑,只需要在reactor框架下把相关需要实现的回调函数进行修改就可以了。