• C语言手写HTTPD网站服务器


    网站服务器(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服务,远程登录服务等等,就需要使用“端口号”,对不同的服务进行区别。每个服务,都有自己唯一的端口号。

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

    1. #include
    2. // 初始化网络并创建服务端的套接字
    3. int startup(unsigned short* port)
    4. {
    5. return 0;
    6. }
    7. int main(void)
    8. {
    9. //httpd默认的端口是80,这里指定了8000端口,也可以使用其它端口
    10. unsigned short port = 8000;
    11. // 初始化网络,并使用指定端口来创建服务端的套接字
    12. int server_sock = startup(&port);
    13. printf("httpd running on port %d\n", port);
    14. return(0);
    15. }

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

    执行WEB服务前的准备工作

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

    代码实现如下:

    1. #include <stdio.h>
    2. #include <winsock2.h>
    3. #pragma comment (lib, "WS2_32.lib")
    4. void error_die(const char* sc)
    5. {
    6. perror(sc);
    7. exit(1);
    8. }
    9. // 初始化网络并创建服务端的套接字
    10. int startup(unsigned short* port)
    11. {
    12. // 网络协议初始化
    13. WSADATA wsaData; // 网络通信相关的版本等信息
    14. // 在windows系统使用网络通信,必须先进行网络协议初始化(Linux系统不需要)
    15. int ret = WSAStartup( // WSAStartup 网络通信初始化,
    16. MAKEWORD(1, 1), // 指定使用Windows Sockets规范的1.1版本
    17. &wsaData); // 存储初始化后的版本等信息
    18. if (ret != 0) {
    19. return false;
    20. }
    21. int server_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
    22. if (server_socket == -1) {
    23. error_die("socket");
    24. }
    25. struct sockaddr_in server_addr;
    26. memset(&server_addr, 0, sizeof(server_addr));
    27. server_addr.sin_family = AF_INET;
    28. server_addr.sin_port = htons(*port);
    29. server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    30. // 端口复用
    31. int opt = 1;
    32. ret = setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, (const char*)&opt, sizeof(opt));
    33. if (ret == -1) {
    34. error_die("setsockopt");
    35. }
    36. if (bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
    37. error_die("[bind]");
    38. }
    39. // 动态分配端口
    40. if (*port == 0) {
    41. int namelen = sizeof(server_addr);
    42. if (getsockname(server_socket, (struct sockaddr*)&server_addr, &namelen) == -1) {
    43. error_die("getsockname");
    44. }
    45. *port = ntohs(server_addr.sin_port);
    46. }
    47. if (listen(server_socket, 5) < 0) {
    48. error_die("listen");
    49. }
    50. return(server_socket);
    51. }

    接收浏览器的WEB请求

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

    处理浏览器请求的框架

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

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

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

    具体代码实现如下:
     

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

    处理浏览器的请求

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

    GET请求报文的格式

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

    1. GET / HTTP/1.1\n
    2. Host: 127.0.0.1:8000\n
    3. Connection: keep-alive\n
    4. Cache-Control: max-age=0\n
    5. Upgrade-Insecure-Requests: 1\n
    6. 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
    7. 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
    8. Sec-Fetch-Site: none\n
    9. Sec-Fetch-Mode: navigate\n
    10. Sec-Fetch-User: ?1\n
    11. Sec-Fetch-Dest: document\n
    12. Accept-Encoding: gzip, deflate, br\n
    13. Accept-Language: zh-CN,zh;q=0.9\n
    14. \n

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

    第一行报文详细说明 

    响应报文的格式

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

    常用的关键字有:

    POST请求报文的格式 

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

    1. POST /color.cgi HTTP/1.1\n
    2. Host: 127.0.0.1:8000\n
    3. Connection: keep-alive\n
    4. Content-Length: 9\n
    5. Cache-Control: max-age=0\n
    6. Upgrade-Insecure-Requests: 1\n
    7. Origin: http://127.0.0.1:8000\n
    8. Content-Type: application/x-www-form-urlencoded\n
    9. 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
    10. 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
    11. Sec-Fetch-Site: same-origin\n
    12. Sec-Fetch-Mode: navigate\n
    13. Sec-Fetch-User: ?1\n
    14. Sec-Fetch-Dest: document\n
    15. Referer: http://127.0.0.1:8000/\n
    16. Accept-Encoding: gzip, deflate, br\n
    17. Accept-Language: zh-CN,zh;q=0.9\n
    18. \n
    19. color=red

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

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

    1. #include <sys/stat.h> //访问文件的属性
    2. #define ISspace(x) isspace((int)(x))
    3. #define PRINTF(str) printf("[%s - %d] "#str" = %s\r\n",__func__,__LINE__,str);
    4. DWORD WINAPI accept_request(LPVOID arg) {
    5. char buf[1024];
    6. int numchars;
    7. char method[255];
    8. char url[255];
    9. char path[512];
    10. size_t i, j;
    11. struct stat st;
    12. int cgi = 0; /* becomes true if server decides this is a CGI
    13. * program */
    14. int client = (SOCKET)arg;
    15. char* query_string = NULL;
    16. numchars = get_line(client, buf, sizeof(buf));
    17. i = 0; j = 0;
    18. while (!ISspace(buf[j]) && (i < sizeof(method) - 1))
    19. {
    20. method[i] = buf[j];
    21. i++; j++;
    22. }
    23. method[i] = '\0'; //解析后, method的值:"GET"
    24. PRINTF(method);
    25. // method是指http请求的具体类型,例如:
    26. // <FORM ACTION="color.cgi" METHOD="POST">
    27. // HTTP的请求方法,一共有8种:GET,POST,HEAD,PUT,DELETE,TRACE,OPTIONS,CONNECT
    28. // 主要使用GET和POST, 本服务器只实现GET和POST方法
    29. if (stricmp(method, "GET") && stricmp(method, "POST"))
    30. {
    31. unimplemented(client);
    32. return 0;
    33. }
    34. if (stricmp(method, "POST") == 0)
    35. cgi = 1;
    36. i = 0;
    37. while (ISspace(buf[j]) && (j < sizeof(buf))) //跳过buff中的空格
    38. j++;
    39. while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < sizeof(buf))) //获得资源url 比如 / 或者 /images/head.png
    40. {
    41. url[i] = buf[j];
    42. i++; j++;
    43. }
    44. url[i] = '\0';
    45. PRINTF(url);
    46. // 解析查询字符串
    47. // 如果浏览器的访问地址是: http://127.0.0.1:8000?name=rock
    48. // 那么服务器端第一次收到的报文头就是: buf = GET /?name=rock HTTP/1.1
    49. // 通过如果解析,query_string的值就是 "name=rock"
    50. if (stricmp(method, "GET") == 0)
    51. {
    52. query_string = url;
    53. while ((*query_string != '?') && (*query_string != '\0'))
    54. query_string++;
    55. if (*query_string == '?')
    56. {
    57. cgi = 1;
    58. *query_string = '\0';
    59. query_string++;
    60. }
    61. }
    62. sprintf(path, "htdocs%s", url);
    63. // 如果浏览器的地址输入:http://127.0.0.1:8000/movies/
    64. // 那么url就是 /movies/
    65. // url的最后一个字符是路径分隔符/
    66. // 表示默认访问的是:/movies/index.html
    67. if (path[strlen(path) - 1] == '/')
    68. strcat(path, "index.html");
    69. PRINTF(path);
    70. // 检查访问的资源是否存在
    71. if (stat(path, &st) == -1) { //stat获取指定文件的属性信息
    72. // 如果不能访问它的属性信息,那么这个文件就不存在
    73. // 此时,就需要把这个请求报文,读完!虽然已经没有用了,但是也要把这个报文读完
    74. while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */
    75. numchars = get_line(client, buf, sizeof(buf));
    76. not_found(client);
    77. }
    78. else
    79. {
    80. // 如果浏览器的地址输入:http://127.0.0.1:8000/movies
    81. // 如果movies是目录,就默认访问这个目录下的index.html
    82. if ((st.st_mode & S_IFMT) == S_IFDIR)
    83. strcat(path, "/index.html");
    84. if (!cgi)
    85. // 发送一个普通文件(path)给浏览器客户端
    86. serve_file(client, path);
    87. else
    88. // 使用CGI来处理“动态请求”,例如在网页中,用户填写信息后点击提交按钮后,服务器端使用CGI来处理这个请求
    89. execute_cgi(client, path, method, query_string);
    90. }
    91. closesocket(client); //关闭套接字
    92. return 0;
    93. }
    94. // 向浏览器发送一个错误提示信息,表示请求的方法还没有实现(现在只实现了GET和POST)
    95. void unimplemented(int client) {
    96. }
    97. void not_found(int client) {
    98. }
    99. void serve_file(int client, const char* filename) {
    100. }
    101. void execute_cgi(int client, const char* path, const char* method, const char* query_string) {
    102. }
    103. // 从浏览器客户端对应的套接字中,读取一行字符串
    104. // 返回值:成功读取的字符个数
    105. int get_line(int sock, char* buf, int size) {
    106. return 0;
    107. }

    发送错误请求的响应包

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

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

    代码如下:
     

    1. // 向浏览器发送一个错误提示信息,表示请求的方法还没有实现(现在只实现了GET和POST)
    2. void unimplemented(int client) {
    3. char buf[1024];
    4. sprintf(buf, "HTTP/1.0 501 Method Not Implemented\r\n");
    5. send(client, buf, strlen(buf), 0);
    6. sprintf(buf, SERVER_STRING);
    7. send(client, buf, strlen(buf), 0);
    8. sprintf(buf, "Content-Type: text/html\r\n");
    9. send(client, buf, strlen(buf), 0);
    10. sprintf(buf, "\r\n");
    11. send(client, buf, strlen(buf), 0);
    12. sprintf(buf, "Method Not Implemented\r\n");</div></div></li><li><div class="hljs-ln-numbers"><div class="hljs-ln-line hljs-ln-n" data-line-number="15"></div></div><div class="hljs-ln-code"><div class="hljs-ln-line"> <span class="hljs-built_in">send</span>(client, buf, strlen(buf), <span class="hljs-number">0</span>);</div></div></li><li><div class="hljs-ln-numbers"><div class="hljs-ln-line hljs-ln-n" data-line-number="16"></div></div><div class="hljs-ln-code"><div class="hljs-ln-line"> <span class="hljs-built_in">sprintf</span>(buf, "\r\n");
    13. send(client, buf, strlen(buf), 0);
    14. sprintf(buf, "

      HTTP request method not supported.\r\n");

    15. send(client, buf, strlen(buf), 0);
    16. sprintf(buf, "\r\n");
    17. send(client, buf, strlen(buf), 0);
    18. }

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

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

    代码如下:

    1. void not_found(int client) {
    2. char buf[1024];
    3. sprintf(buf, "HTTP/1.0 404 NOT FOUND\r\n");
    4. send(client, buf, strlen(buf), 0);
    5. sprintf(buf, SERVER_STRING);
    6. send(client, buf, strlen(buf), 0);
    7. sprintf(buf, "Content-Type: text/html\r\n");
    8. send(client, buf, strlen(buf), 0);
    9. sprintf(buf, "\r\n");
    10. send(client, buf, strlen(buf), 0);
    11. sprintf(buf, "Not Found\r\n");
    12. send(client, buf, strlen(buf), 0);
    13. sprintf(buf, "

      The server could not fulfill\r\n");

    14. send(client, buf, strlen(buf), 0);
    15. sprintf(buf, "your request because the resource specified\r\n");
    16. send(client, buf, strlen(buf), 0);
    17. sprintf(buf, "is unavailable or nonexistent.\r\n");
    18. send(client, buf, strlen(buf), 0);
    19. sprintf(buf, "\r\n");
    20. send(client, buf, strlen(buf), 0);
    21. }

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


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

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

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

  • 相关阅读:
    【vscode】本地配置和根据不同项目不同的vscode配置
    vscode使用flake8设置单行最长字符限制设置失败的问题
    Mac如何远程连接Ubuntu主机(一)ssh连接|Mac通过ssh远程连接Ubuntu主机
    C++ day1
    Discuz IIS上传附件大于28M失败报错Upload Failed.修改maxAllowedContentLength(图文教程)
    clion远程编译
    redis序列化协议RESP
    springboot毕设项目超市仓库管理系统15g4i(java+VUE+Mybatis+Maven+Mysql)
    Windows10/11 缩放与布局自定义
    tg群组内容怎么更新到网页
  • 原文地址:https://blog.csdn.net/weixin_55751709/article/details/126977378