C语言手写HTTPD网站服务器

news/2024/5/19 4:52:05/文章来源:https://blog.csdn.net/weixin_55751709/article/details/126977378

网站服务器(HTTPD)已经有很多版本,但是大部分对初学者都非常不友好。适合初学者学习的httpd服务器,最负盛名的当数tinyhttpd, 但是这个版本,是基于Linux系统的,而且配套的CGI也是使用perl语言写的,直接劝退了大部分想学后端开发的初学者。基于此,特意写了这个小项目,让只有C语言基础的初学者,就可以直接手写后端服务器,快速提升C语言和网络开发技能。

这个项目是基于tinyhttpd改写的,解决了以下问题:

1. 解决了tinyhttpd服务器只支持html纯文本的问题,添加了支持图片文件和JS脚本的问题,可以直接支持各种复杂的网页。

2. 使用C语言实现了CGI功能。tinyhttpd服务器的CGI是perl脚本实现的,对于C/C++初学者不友好,用C语言实现CGI功能,可以更加深刻的理解动态网站的实现原理和实现方法。

3. 解决和tineyhttpd服务器中文显示的问题,完美支持GET和POST的中文字符。

4. 本项目直接使用Window系统实现,C/C++初学者可以零障碍掌握学习。tinyhttpd服务器是基于Linux系统的,而大部分初学者对Linux系统并不熟悉。

5. 本项目在最后使用内网穿透,把自己的网站零成本的分享给自己的同学朋友。
项目效果:点这里看本教程配套视频​​​​​​​

项目准备

  • Windows系统
  • vs2019或者任意其它版本的vs
  • 创建项目

创建项目 

使用任意版本的VS或者VC++,创建一个空项目。

创建服务器端的套接字

基于网络的通信,需要先创建“套接字”。“套接字”这个专业术语,非常古怪,被很多人吐槽。我们不要深究它为什么叫这个名字,我们只需要了解“套接字”的作用即可。

套接字,就相当于一个“网络插座”,通过网络进行通信,就是通过这个“插座”收发信息的,相当于一个电话机的电话线插槽。

 

在创建服务器时,还必须要指定一个端口号。当一台服务器,同时对外提供多种服务时,比如WEB服务,远程登录服务等等,就需要使用“端口号”,对不同的服务进行区别。每个服务,都有自己唯一的端口号。

但是,服务器端在网站访问服务之前,需要创建“套接字”。

#include <stdio.h>// 初始化网络并创建服务端的套接字
int startup(unsigned short* port)
{return 0;
}int main(void)
{//httpd默认的端口是80,这里指定了8000端口,也可以使用其它端口unsigned short port = 8000;  // 初始化网络,并使用指定端口来创建服务端的套接字int server_sock = startup(&port);printf("httpd running on port %d\n", port);return(0);
}

 

以上代码,只是写了函数接口,还没有真正创建套接字。马上做详细的实现。

执行WEB服务前的准备工作

在接受浏览器前端的网页请求之前,服务器端需要做一些准备工作,流程如下:(详细详解可以参考本教程配套的分享视频)

代码实现如下:

#include <stdio.h>
#include <winsock2.h>
#pragma comment (lib, "WS2_32.lib")void error_die(const char* sc)
{perror(sc);exit(1);
}// 初始化网络并创建服务端的套接字
int startup(unsigned short* port)
{// 网络协议初始化WSADATA wsaData; // 网络通信相关的版本等信息// 在windows系统使用网络通信,必须先进行网络协议初始化(Linux系统不需要)int ret = WSAStartup( // WSAStartup 网络通信初始化,MAKEWORD(1, 1),   // 指定使用Windows Sockets规范的1.1版本&wsaData);        // 存储初始化后的版本等信息if (ret != 0) {return false;}int server_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);if (server_socket == -1) {error_die("socket");}struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(*port);server_addr.sin_addr.s_addr = htonl(INADDR_ANY);// 端口复用int opt = 1;ret = setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, (const char*)&opt, sizeof(opt));if (ret == -1) {error_die("setsockopt");}if (bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {error_die("[bind]");}// 动态分配端口if (*port == 0) {int namelen = sizeof(server_addr);if (getsockname(server_socket, (struct sockaddr*)&server_addr, &namelen) == -1) {error_die("getsockname");}*port = ntohs(server_addr.sin_port);}if (listen(server_socket, 5) < 0) {error_die("listen");}return(server_socket);
}

 

接收浏览器的WEB请求

我们的httpd web服务器准备好以后,就可以接受来自前端浏览器的网页访问请求了。
我们先来看一下浏览器访问网站的完整流程:

处理浏览器请求的框架

因为可能有多个用户同时发起请求,为了更快的处理网页请求,这里使用多线程技术。流程如下:

服务端收到浏览器的请求后,accept函数会返回一个“客户端套接字”,这个套接字对应于这个浏览器客户端。以后服务器就通过这个“客户端套接字”和对应的浏览器通信。此时,服务器端有两种套接字:

    服务器端套接字:用来等待新的浏览器客户端的发起请求,收到请求后,返回一个客户端套接字。
    客户端套接字:用来和对应的浏览器客户端通信。每个浏览器客户端连接到服务器后,都有一个对应的客户端套接字。

具体代码实现如下:
 

DWORD WINAPI accept_request(LPVOID arg) {return 0;
}int main(void)
{//httpd默认的端口是80,这里指定了8000端口,也可以使用其它端口unsigned short port = 8000;  // 初始化网络,并使用指定端口来创建服务端的套接字int server_sock = startup(&port);printf("httpd running on port %d\n", port);while (1){struct sockaddr_in client_addr;int client_addr_len = sizeof(client_addr);int client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &client_addr_len);if (client_sock == -1) {error_die("accept"); //打印错误信息并结束}DWORD dwThreadID = 0;HANDLE handleFirst = CreateThread(NULL, 0, accept_request, (void*)client_sock, 0, &dwThreadID);}return(0);
}

 

处理浏览器的请求

在新线程中,单独处理对应浏览器客户端的请求。

GET请求报文的格式

浏览器发起新的访问时,将向服务器端发送一个请求报文。例如,在浏览器地址输入 127.0.0.1:8000 回车后,服务器端收到的完整报文如下:

GET / HTTP/1.1\n
Host: 127.0.0.1:8000\n
Connection: keep-alive\n
Cache-Control: max-age=0\n
Upgrade-Insecure-Requests: 1\n
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\n
Sec-Fetch-Site: none\n
Sec-Fetch-Mode: navigate\n
Sec-Fetch-User: ?1\n
Sec-Fetch-Dest: document\n
Accept-Encoding: gzip, deflate, br\n
Accept-Language: zh-CN,zh;q=0.9\n
\n

 请求报文由4四个部分组成:请求行、请求头部行、空行、请求数据。具体格式如下:

第一行报文详细说明 

 

响应报文的格式

 服务器发送数据给浏览器时,发送的响应报文,由4个部分组成:
 状态行、消息头部、空行和响应正文。格式如下:

常用的关键字有:

 

POST请求报文的格式 

浏览器发送的POST报文的格式,和GET报文格式其实是一致的,只是多了最后一部分内容“请求数据”,实例如下:

POST /color.cgi HTTP/1.1\n
Host: 127.0.0.1:8000\n
Connection: keep-alive\n
Content-Length: 9\n
Cache-Control: max-age=0\n
Upgrade-Insecure-Requests: 1\n
Origin: http://127.0.0.1:8000\n
Content-Type: application/x-www-form-urlencoded\n
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\n
Sec-Fetch-Site: same-origin\n 
Sec-Fetch-Mode: navigate\n
Sec-Fetch-User: ?1\n
Sec-Fetch-Dest: document\n
Referer: http://127.0.0.1:8000/\n
Accept-Encoding: gzip, deflate, br\n
Accept-Language: zh-CN,zh;q=0.9\n
\n
color=red

 

最后一行“color=red”就是网页提交的数据。 

报文的解析(每行代码的详细说明,可参考本教程的分享视频)

#include <sys/stat.h> //访问文件的属性#define ISspace(x) isspace((int)(x))
#define PRINTF(str) printf("[%s - %d] "#str" = %s\r\n",__func__,__LINE__,str);DWORD WINAPI accept_request(LPVOID arg) {char buf[1024];int numchars;char method[255];char url[255];char path[512];size_t i, j;struct stat st;int cgi = 0;      /* becomes true if server decides this is a CGI* program */int client = (SOCKET)arg;char* query_string = NULL;    numchars = get_line(client, buf, sizeof(buf));i = 0; j = 0;while (!ISspace(buf[j]) && (i < sizeof(method) - 1)){method[i] = buf[j];i++; j++;}method[i] = '\0';  //解析后, method的值:"GET"PRINTF(method);//  method是指http请求的具体类型,例如:// <FORM ACTION="color.cgi" METHOD="POST">// HTTP的请求方法,一共有8种:GET,POST,HEAD,PUT,DELETE,TRACE,OPTIONS,CONNECT// 主要使用GET和POST, 本服务器只实现GET和POST方法if (stricmp(method, "GET") && stricmp(method, "POST")){unimplemented(client);return 0;}if (stricmp(method, "POST") == 0)cgi = 1;i = 0;while (ISspace(buf[j]) && (j < sizeof(buf))) //跳过buff中的空格j++;while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < sizeof(buf))) //获得资源url 比如 / 或者 /images/head.png{url[i] = buf[j];i++; j++;}url[i] = '\0';PRINTF(url);// 解析查询字符串// 如果浏览器的访问地址是: http://127.0.0.1:8000?name=rock // 那么服务器端第一次收到的报文头就是:  buf = GET /?name=rock HTTP/1.1// 通过如果解析,query_string的值就是 "name=rock"if (stricmp(method, "GET") == 0){query_string = url;while ((*query_string != '?') && (*query_string != '\0'))query_string++;if (*query_string == '?'){cgi = 1;*query_string = '\0';query_string++;}}sprintf(path, "htdocs%s", url);// 如果浏览器的地址输入:http://127.0.0.1:8000/movies/// 那么url就是 /movies/ // url的最后一个字符是路径分隔符/// 表示默认访问的是:/movies/index.htmlif (path[strlen(path) - 1] == '/')strcat(path, "index.html");PRINTF(path);// 检查访问的资源是否存在if (stat(path, &st) == -1) {  //stat获取指定文件的属性信息// 如果不能访问它的属性信息,那么这个文件就不存在// 此时,就需要把这个请求报文,读完!虽然已经没有用了,但是也要把这个报文读完while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */numchars = get_line(client, buf, sizeof(buf));not_found(client);}else{// 如果浏览器的地址输入:http://127.0.0.1:8000/movies // 如果movies是目录,就默认访问这个目录下的index.htmlif ((st.st_mode & S_IFMT) == S_IFDIR)strcat(path, "/index.html");if (!cgi)// 发送一个普通文件(path)给浏览器客户端serve_file(client, path);else// 使用CGI来处理“动态请求”,例如在网页中,用户填写信息后点击提交按钮后,服务器端使用CGI来处理这个请求 execute_cgi(client, path, method, query_string);}closesocket(client); //关闭套接字return 0;
}// 向浏览器发送一个错误提示信息,表示请求的方法还没有实现(现在只实现了GET和POST)
void unimplemented(int client) {}void not_found(int client) {}void serve_file(int client, const char* filename) {}void execute_cgi(int client, const char* path, const char* method, const char* query_string) {}// 从浏览器客户端对应的套接字中,读取一行字符串
// 返回值:成功读取的字符个数
int get_line(int sock, char* buf, int size) {return 0;
}

 

发送错误请求的响应包

发送501未实现服务的响应包

HTTP 状态码分为 5 类,如下所示:
1xx 信息
2xx 成功
3xx 重定向
4xx 客户端错误
      404 表示服务器找不到浏览器请求的资源(例如某个图片或某个文件)。
5xx 服务器错误
      501 表示服务器现在还不能满足客户端请求的某个功能。

代码如下:
 

// 向浏览器发送一个错误提示信息,表示请求的方法还没有实现(现在只实现了GET和POST)
void unimplemented(int client) {char buf[1024];sprintf(buf, "HTTP/1.0 501 Method Not Implemented\r\n");send(client, buf, strlen(buf), 0);sprintf(buf, SERVER_STRING);send(client, buf, strlen(buf), 0);sprintf(buf, "Content-Type: text/html\r\n");send(client, buf, strlen(buf), 0);sprintf(buf, "\r\n");send(client, buf, strlen(buf), 0);sprintf(buf, "<HTML><HEAD><TITLE>Method Not Implemented\r\n");send(client, buf, strlen(buf), 0);sprintf(buf, "</TITLE></HEAD>\r\n");send(client, buf, strlen(buf), 0);sprintf(buf, "<BODY><P>HTTP request method not supported.\r\n");send(client, buf, strlen(buf), 0);sprintf(buf, "</BODY></HTML>\r\n");send(client, buf, strlen(buf), 0);
}

 

注意代码中的Server关键字,表示服务器端的软件名称和它的版本号。

发送404资源不存在的响应包

代码如下:

void not_found(int client) {char buf[1024];sprintf(buf, "HTTP/1.0 404 NOT FOUND\r\n");send(client, buf, strlen(buf), 0);sprintf(buf, SERVER_STRING);send(client, buf, strlen(buf), 0);sprintf(buf, "Content-Type: text/html\r\n");send(client, buf, strlen(buf), 0);sprintf(buf, "\r\n");send(client, buf, strlen(buf), 0);sprintf(buf, "<HTML><TITLE>Not Found</TITLE>\r\n");send(client, buf, strlen(buf), 0);sprintf(buf, "<BODY><P>The server could not fulfill\r\n");send(client, buf, strlen(buf), 0);sprintf(buf, "your request because the resource specified\r\n");send(client, buf, strlen(buf), 0);sprintf(buf, "is unavailable or nonexistent.\r\n");send(client, buf, strlen(buf), 0);sprintf(buf, "</BODY></HTML>\r\n");send(client, buf, strlen(buf), 0);
}

发送正常的GET请求的响应包(待更新)


  今天的分享就到这里了,大家要好好学C语言/C++哟~
对于准备学习C/C++编程的小伙伴,如果你想更好的提升你的编程核心能力(内功)不妨从现在开始!

C语言C++编程学习交流圈子,企鹅群:【点击进入】
整理分享(多年学习的源码、项目实战视频、项目笔记,基础入门教程)

欢迎转行和学习编程的伙伴,利用更多的资料学习成长比自己琢磨更快哦!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_11770.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

宝塔面板修改secure_file_priv设置

1、secure_file_priv文件作用 mysql读取系统文件权限的设置参数 2、查询secure_file_priv设置 show variables like %secure%; 3、修改secure_file_priv设置 设置 secure_file_priv"/" 需要修改mysql配置文件my.cnf my.cnf文件有两个位置 /etc/my.cnf /www/serv…

线程安全简述

目录 1、线程是否安全 2、出现线程安全的原因如下&#xff1a; 3、原子性问题 4、synchronized关键字 1、锁对象 2、用法&#xff1a; 3、可重入锁 5、内存可见性 6、volatile关键字 7、JMM 1、线程是否安全 线程不安全就是一些代码在多线程的运行状态下&#xff0c…

一个基于.Net Core开发的适合外贸商城系统

今天给大家推荐一个适合外贸的商城系统。 项目简介 这是一个基于.Net Core开发的&#xff0c;兼容PC、平板、移动端的商城系统。被下载次数超过300w&#xff0c;拥有最活跃的成员&#xff0c;由专业团队开发与支持。支持PayPal、信用卡、发票支付。 技术架构 1、跨平台&…

Jmeter电商系统压测实战<二>

目录一、Jmeter优化tips二、Jmeter的使用建议-参数配置1. XX:MaxMataspaceSize&#xff08;jdk8的参数&#xff09;2. -Xmx2048m3. -Xms1g三、Jmeter插件1. 介绍及安装2. 常用插件四、Jmeter日志收集1. 概览2. elk&#xff0c;kibana和es的安装和配置3. Prometheus和Node Expor…

全系标配L2占比首次突破30%,「数据」赛道争夺战一触即发

智能驾驶的进阶战&#xff0c;无论是提升车型产品竞争力&#xff0c;还是为高阶功能和现有功能优化提供闭环数据迭代&#xff0c;全系标配已经成为主流趋势。 如果说智能化1.0阶段&#xff0c;车企拼的是技术的快速落地和高阶能力的标杆效应&#xff0c;那么2.0阶段就是拼规模…

python中validators库用法详解

首先安装validators库&#xff1a; pip install validators validators.between(value, minNone, maxNone) 验证一个数字value是否在最小值min和最大值max之间&#xff0c;value不仅仅可以是整数&#xff0c;也可以是其它数据类型&#xff0c;例如floats, decimals 和 dates。…

Three使用OimoPhysics实现物体相关物理特性实例

基础环境搭建&#xff1a; InstancedMesh()创建的立方体物品集合&#xff1a; boxes new THREE.InstancedMesh(new THREE.BoxGeometry(0.1, 0.1, 0.1),new THREE.MeshLambertMaterial(),100)const matrix new THREE.Matrix4()const color new THREE.Color()for (let i 0; i…

Win11 22H2 22621.521大版本更新!

注意&#xff01;注意&#xff01;Win11 22H2 22621.521大版本更新啦&#xff0c;此次更新带来了不小的优化和改进&#xff0c;包括带有标签的更新文件资源管理器、更丰富的开始菜单和任务栏体验、增强的搜索功能、对改进的安全性和无密码登录的支持等等。 让每个人都能更轻松、…

生成网络论文阅读styleGAN1(一):论文速览

研究什么内容 研究如何把生成图片当中的内容拆分开 研究方法 为了把各种风格分开先得把控制信息分开输入&#xff0c;于是作者就分开输入了&#xff0c;在PGGAN的基础上分开输入&#xff0c;取得了好的效果。 个人理解 1.这里能取得好效果的主要原因是PGGAN的逐渐提升像素…

多模块间通信存在完美的设计么?

一、前言 在 App 的使用中&#xff0c;常常会有一些功能的依赖&#xff0c;比如评论需要用户登录、支付需要用户实名绑定银行卡等。从代码开发角度而言&#xff0c;如果我们的项目使用了多模块&#xff0c;那么也就会出现模块依赖的场景&#xff0c;比如评论模块依赖登录模块提…

企业复杂的数据治理需求,TempoDF让数据开发更简单!

伴随着企业的发展以及信息化建设的不断深入&#xff0c;业务之间不关联、数据之间彼此独立、流程之间相互封闭的现象越来越普遍&#xff0c;“数据孤岛”问题愈发严重&#xff0c;已成为制约企业发展的桎梏。 为了实现企业全局数据的系统化运作管理&#xff0c;不少企业开始着…

PDF转换成PPT后格式混乱,可能这个没做好

PDF转换成PPT后格式混乱怎么处理?这类问题其实对于经常使用PPT的朋友们来说并不陌生。我们有时候需要把一篇PPT演讲稿转换成PDF文档&#xff0c;但在操作过程中常常不仅过程复杂且效果不理想。有时甚至在转化之后出现格式混乱&#xff0c;影响了阅读体验不说&#xff0c;还会让…

WPF 图片头像自由剪切器实时截图细节放大器

本文参考博文&#xff1a;WPF 自定义图片剪切器 - 头像剪切&#xff08;扩展与完善、实时截图&#xff09; 在网上找了好久都找不到合适的截图框架&#xff0c;只能用WPF 自定义图片剪切器 - 头像剪切&#xff08;扩展与完善、实时截图&#xff09;_孤夜一点星的博客-CSDN博客…

《uni-app》表单组件-form表单

本文分享的Form组件为uni-app的内置组件Form&#xff0c;非扩展组件&#xff0c;两者在用法上其实大同小异&#xff0c;只是扩展组件的属性以及事件更多…没有本质上的区别&#xff5e; 《uni-app》表单组件-form表单一. 简介二. 基础用法三. submit事件四. reset事件五. repor…

虚拟机是什么意思?

&#x1f308; 个人主页&#xff1a;python老鸟的博客 &#x1f506; 所属专栏&#xff1a;Python基础教程 ❤️ 刷题 &#x1f449; Python练习题库&#xff0c;不断更新中~~ &#x1f64f; 如果觉得博主文章对你有所帮助的话&#xff0c;还望大家多多支持呀&#xff01;关注 …

安卓APT技术讲解(下)-实现安卓组件化的路由功能

前言&#xff1a; 组件化是安卓目前很流行的一门技术&#xff0c;其目的是避免复杂的业务逻辑交织到一起&#xff0c;相互影响。通过解耦&#xff0c;让每个子项目都是一个独立的工程&#xff0c;即使其余模块出现问题&#xff0c;也不会影响这个子模块的运行。 本篇系“利用A…

为k8s节点预留资源,防止pod占用资源过多导致雪崩

Kubernetes 的节点可以按照节点的资源容量进行调度&#xff0c;默认情况下 Pod 能够使用节点全部可用容量。这样就会造成一个问题&#xff0c;因为节点自己通常运行了不少驱动 OS 和 Kubernetes 的系统守护进程。除非为这些系统守护进程留出资源&#xff0c;否则它们将与 Pod 争…

70 QDateTime时间戳转换有误

1 前言 在开发工具中需要用时间戳转换成格式化时间来显示&#xff0c;但引用QT中自带的时间类QDateTime转换时&#xff0c;发现转换时间有误问题&#xff0c;转换的结果时分秒是正确的&#xff0c;但月份确实错误的。因此在未深入研究qt实现情况下&#xff0c;需要得到正确的格…

KeeWiDB:兼容Redis协议,领跑NoSQL

如果现在的我们离开了互联网&#xff0c;生活会是什么样子&#xff1f; 互联网&#xff0c;已经深刻渗透到人们的生活中。 不知道大家有没有想过&#xff1f;每一个互联网结合的背后都是海量的存储需求。你查看的每一个商品、组建的每一个战队、阅读的每一篇文章&#xff0c;…

去中心化与无平台成员:与 Nasheq.eth、Ivan Manchev和Rob Edwards开启 “智能钱包”系列对话!

Ambire 的 Twitter Space 第 9 集围绕着 Web3 社区和符号化模式展开。 &#x1f342;&#x1f341;&#x1f342;秋意渐浓&#xff0c;Ambire 准备收获我们今年夏天的工作成果&#xff1a;市场看跌&#xff0c;但建设者们很忙 &#x1f477;&#x1f477;♀️一些惊人的版本即将…