MiniHttp服务器的设计与实现
MiniHttp服务器的设计与实现
1. 目标及要求
2. 功能设计及思想
2.0 前置知识-C/S请求响应的过程
C/S架构,也称为客户端/服务器架构。客户端通过http/https协议向服务器发送请求,服务器接收到客户端的请求之后,就会将服务器上存有的数据返回给客户端。
在上述过程中,客户端向服务器发送的http请求,称为http请求包。服务器向客户端发送的http请求,称为http应答包(响应包)。
我们的目的就是要编写一个可以与客户端进行交互的http/https服务器,并可以向客户端发送http/https应答包。
2.0 前置知识-http请求的格式
2.0 前置知识-http响应的格式
2.0 前置知识-使用openssl导入证书
我们可以使用如下的步骤来进行ssl上下文的初始化和证书的导入工作:
- 初始化 OpenSSL
- 创建 SSL 上下文
- 加载服务器证书和私钥
初始化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 在浏览器上测试
4.2 在vlc上测试
4.3 在mininet上测试
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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现