清润邯郸网站,百度快速收录办法,东莞知名企业排名,wordpress新建页面文章列表博客介绍#xff1a;运用之前学过的各种知识 自己独立做出一个HTTP服务器 自主设计WEB服务器 背景目标描述技术特点项目定位开发环境WWW介绍 网络协议栈介绍网络协议栈整体网络协议栈细节与http相关的重要协议 HTTP背景知识补充特点uri url urn网址url HTTP请求和…博客介绍运用之前学过的各种知识 自己独立做出一个HTTP服务器 自主设计WEB服务器 背景目标描述技术特点项目定位开发环境WWW介绍 网络协议栈介绍网络协议栈整体网络协议栈细节与http相关的重要协议 HTTP背景知识补充特点uri url urn网址url HTTP请求和响应HTTP请求协议格式HTTP响应格式 项目编写TcpSevermain.ccmakefileHttpServer.hppProrocol 接收报头读取请求日志请求读取与解析解析请求行解析请求报头处理请求正文 处理请求并且构建响应HTTP状态码获取参数Web根目录HTTP CGI机制简单介绍构建响应添加状态行添加响应正文发送响应添加响应报头 设计CGI程序进程间通信建立管道进程替换 交互数据POST方法处理GET方法处理传递传参方法 CGI程序接收数据CGI程序处理数据父进程读取处理完的数据 CGI程序总结 状态码介绍处理读取出错处理写入错误多线程转化线程池设计任务类设计线程池 补充内容表单测试cgi程序支持多种语言项目代码 背景
http协议被广泛使用 从移动端 pc端浏览器 http协议无疑是打开互联网应用窗口的重要协议
http在网络应用层中的地位不可撼动 是能准确区分前后台的重要协议
虽然说现在最常用的是https协议 但是我们学会了http协议之后对于https的学习也能更加轻松
目标
对http协议的理论学习 从零开始完成web服务器开发 坐拥下三层协议 从技术到应用 让网络难点无处遁形
描述
采用C/S模型 编写支持中小型应用的http 并结合mysql 理解常见互联网应用行为 做完该项目 我们可以从技术上完全理解从你上网开始 到关闭浏览器的所有操作中的技术细节
技术特点
关于此次WEB服务器项目 我们主要用到的技术有
网络编程 TCP/IP协议, socket流式套接字http协议多线程技术cgi技术shell脚本线程池
项目定位
开发环境
此次项目使用的开发环境是 LinuxCentos7
使用的工具有 gcc/g/gdb C/C
WWW介绍
WWW是环球信息网的缩写 亦作“Web”、“WWW”、“‘W3’”英文全称为“World Wide Web” 中文名字为“万维网”环球网等常简称为Web。
它分为web服务器和web客户端
WWW可以让Web客户端常用浏览器访问浏览Web服务器上的页面 是一个由许多互相链接的超文本组成的系统 通过互联网访问
在这个大系统中 我们将每一个有用的事务都称为一个资源 并且由一个全局 统一资源标识符 (URI)标识 这些资源通过超文本传输协议 HTTP协议 传送给用户 而后者通过点击连接来获取资源
网络协议栈介绍
网络协议栈整体
从之前网络部分的学习我们知道 TCP/IP协议下的网络模型可以分为四层
应用层传输层网络层数据链路层
具体见下图
网络协议栈细节 从作用上分类 我们可以将网络四层模型分为两部分
应用层 主要负责对于数据的应用数据传输 主要负责对于数据的传输
从细节上看
发送端自上而下会经过 应用层 传输层 网络层 数据链路层 其中经过每一层都会进行添加报头的操作来保证数据正确的送达对面
接收端自下而上的会对于这些数据进行解包 所以说接收端和发送端 我们可以认为他们同层之间看到的数据是相同的即同层之间可以看作是能够直接通信
与http相关的重要协议
tcpipdns
tcp和ip协议我们在网络部分已经深入了解学习过了 那么什么是dns协议呢 我们在ping www.baidu.com的时候 可以发现下方自动给我们转化为了一个ip地址
事实上这个ip地址就是百度服务器的公网ip 而dns的作用就是将域名转化为ip地址 为什么要有dns协议的存在呢 因为我们人类更擅长记住一些有意义的字符串而不是数字 域名和dns本质是为了优化用户的使用体验的
HTTP背景知识补充
目前主流的服务器使用的是http/1.1版本 而我们此次项目使用http/1.0来进行讲解 同时我们还会对比1.0和1.1版本的各种区别
此外我们此次项目只会写服务器 客户端使用浏览器代替
特点
HTTP协议有个特点就是C/S模式 客户端服务器模式 客户端通过一些方法get post等向服务器发送请求 之后服务器接收到请求之后发送响应
http协议有以下四个特点
简单快速 http服务器的规模很小比如说我们今天要写的服务器代码只有1000行左右 所以说通信速度很快灵活 http协议可以传输任意类型的数据 正在传输的类型由Content-Type 我们后面会详细讲解加以标记无连接 每次连接只处理一个请求 服务器处理完客户的请求 收到客户的应答后 即断开连接 采用这种方式可以节省传输时间无状态 http协议的无连接体现在哪里 http协议的无连接是对比tcp协议的连接而言的
http协议它本身对于连接没有概念 它只知道将自己要发送的数据交给下层协议 然后下层协议就会将数据发送到对端 http协议的无状态体现在哪里 http协议的无状态体现在它并不会记得自己发送或者接受过任何的数据
但是同学们读到这里可能会产生一个疑问 那么为什么我在浏览器上登录一个网站之后这个网站就记得我了呢
这实际上是由浏览器的cookie和session机制实现的 具体的原理可以参考这篇博客
cookie和session
uri url urn
URI 是uniform resource identifier 统一资源标识符 用来唯一的标识一个资源URL 是uniform resource locator 统一资源定位符 它是一种具体的URI 即URL可以用来标识一个资源 而且还指明了如何定位这个资源URN uniform resource name 统一资源命名 是通过名字来标识资源 比如说 mailto:javanetjava.sun.com
URI是一种抽象的 更高层次的一种统一资源标识符 而URL和URN则是一种具体的标识符
URL和URN是URI的子集 不过我们使用URN并不多
简单来说 URI和URL的主要区别是
URI强调唯一标识一个资源URL强调唯一定位一个资源URL是URI的子集 因为如果能唯一定位一个资源就一定能唯一标识一个资源
网址url
URLUniform Resource Lacator叫做统一资源定位符也就是我们通常所说的网址 其中服务器地址就是域名对应着我们的IP地址 带层次的文件路径实际上就是我们Linux中的路径
接下来我们来较为全面的认识下上面URL 一、协议方案名 http://表示的是协议名称 表示请求时需要使用的协议 通常使用的是HTTP协议或安全协议HTTPS
HTTPS是以安全为目标的HTTP通道 在HTTP的基础上通过传输加密和身份认证保证了传输过程的安全性 二、登录信息 usr:pass表示的是登录认证信息 包括登录用户的用户名和密码
虽然登录认证信息可以在URL中体现出来 但绝大多数URL的这个字段都是被省略的 因为登录信息可以通过其他方案交付给服务器 三、服务器地址 www.example.jp表示的是服务器地址 也叫做域名
HTTP请求和响应
HTTP请求协议格式
HTTP的请求协议格式如下 我们可以看到HTTP请求由四部分组成
请求行[请求方法][url][http版本]请求报头请求的属性 这些属性都是以key: value的形式按行陈列的空行遇到空行表示请求报头结束请求正文请求正文允许为空字符串 如果请求正文存在 则在请求报头中会有一个Content-Length属性来标识请求正文的长度
其中前面三部分是由HTTP协议自带的 而请求正文则是用户的相关信息和数据 如果说用户没有信息要上传给服务器 此时正文则为空 如何将HTTP请求的报头与有效载荷进行分离 首先我们要明白哪里是报头哪里是有效载荷
请求报头请求行请求报头 有效载荷请求正文
细心的同学就可以发现了 事实上报头和有效载荷之间隔着一个空行
如果我们将整个http协议想象成一个线性的结构 每一行都是使用\n来进行分隔的 那么如果我们连续读取到两个\n的话就说明报头读取完毕了开始读取有效载荷 获取浏览器的HTTP请求 在网络协议栈中 应用层的下一层叫做传输层 而HTTP协议底层通常使用的传输层协议是TCP协议
因此我们可以用套接字编写一个TCP服务器 然后启动浏览器访问我们的这个服务器
由于我们的服务器是直接用TCP套接字读取浏览器发来的HTTP请求 此时在服务端没有应用层对这个HTTP请求进行过任何解析
因此我们可以直接将浏览器发来的HTTP请求进行打印输出 此时就能看到HTTP请求的基本构成
HTTP响应格式
HTTP响应协议格式如下 HTTP响应由以下四部分组成
状态行[http版本][状态码][状态码描述]响应报头响应的属性 这些属性都是以key: value的形式按行陈列的空行遇到空行表示响应报头结束响应正文响应正文允许为空字符串 如果响应正文存在 则响应报头中会有一个Content-Length属性来标识响应正文的长度 比如服务器返回了一个html页面 那么这个html页面的内容就是在响应正文当中的 如何将HTTP响应的报头与有效载荷进行分离 对于HTTP响应来讲 这里的状态行和响应报头就是HTTP的报头信息 而这里的响应正文实际就是HTTP的有效载荷
而报头信息和响应正文之间我们使用换行符来进行分隔
当客户端收到一个HTTP响应后 就可以按行进行读取 如果读取到空行则说明报头已经读取完毕
再介绍其他的格式细节之前我们先来写上一部分的代码
项目编写
TcpSever
我们首先再Linux服务器上创建一个项目目录 之后在项目目录里面创建一个hpp文件 这里有同学可能会有疑惑 为什么要写的是一个HTTP的服务器 这里确要先写一个TcpSever呢
这是因为Http是基于Tcp的 我们首先要保证信息的传输之后再设计http 为什么文件格式要是hpp hpp格式的C文件通常可以将类的声明和实现放在一起 比较适合存在于开源项目中
我们设计一个TcpSever类只需要提供一个端口就可以了 因为操作系统会给我们自动分配一个ip 需要注意的是因为我们这里使用的是云服务器 最好不要使用云服务器的公网ip和私有ip 否则有可能会出现对端主机连接不上的情况ip不是真正的公网ip
下面是TcpServer的代码 #pragma once #include iostream #include cstdlib #include cstring #include sys/types.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include pthread.h #define PORT 8081 #define BackLog 5 class TcpServer{ private: int _port; int _listen_sock; static TcpServer* svr; private: TcpServer() :_port(PORT), _listen_sock(-1) {}
W TcpServer(const TcpServer s) {} TcpServer(int port) :_port(port), _listen_sock(-1) {} public: static TcpServer* getinstance(int port) { static pthread_mutex_t lock PTHREAD_MUTEX_INITIALIZER; if (nullptr svr) { pthread_mutex_lock(lock); if(nullptr svr) { svr new TcpServer(port); svr-InitSever(); } pthread_mutex_unlock(lock); } return svr; } void InitSever() { Socket(); Bind(); Listen(); } void Socket() { _listen_sock socket(AF_INET,SOCK_STREAM,0); if(_listen_sock 0) { exit(1); } // 地址复用 防止突然断开连接 不能立刻复用端口 int opt 1; setsockopt(_listen_sock,SOL_SOCKET,SO_REUSEADDR,opt , sizeof(opt)); } void Bind() { struct sockaddr_in local; memset(local , 0 ,sizeof(local)); local.sin_family AF_INET; local.sin_port htons(_port); local.sin_addr.s_addr INADDR_ANY; // 不能直接绑定公网IP if(bind(_listen_sock , (struct sockaddr*)local , sizeof(local))0){ exit(2); } } void Listen() { if(listen(_listen_sock,BackLog) 0) { exit(3); } } int Sock(){return _listen_sock;} ~TcpServer() { } }; TcpServer* TcpServer::svr nullptr; 上面是我们使用单例模式设计出来的一个TcpSever 创建Listen套接字 绑定 监听都是网络部分很常见的套路 封装即可
这部分代码其实在网络部分就写过很多遍了 这里就不过多赘述
main.cc
#include string
#include iostream
#include memory
#include HttpSever.hpp static void Usage(std::string proc)
{ std::cout Usage:\n\t proc port std::endl;
} int main(int argc , char* argv[])
{ if (argc ! 2) { Usage(argv[0]); exit(4); } int port atoi(argv[1]); std::shared_ptrHttpSever http_server(new HttpSever(port)); http_server-InitSever(); http_server-Loop(); for(;;) { ; } return 0;
} makefile
binhttpsecer
ccg
LD_FLAGS-stdc11 -lpthread
srcmain.cc $(bin):$(src) $(cc) -o $ $^ $(LD_FLAGS) .PHONY:clean
clean: rm $(bin) HttpServer.hpp #pragma once #include istream #include pthread.h #include TcpServer.hpp #include Prorocol.hpp class HttpSever{ private: bool _stop; // 是否在运行 int _port; TcpServer* _tcp_server; public: HttpSever(int port 8081)
W :_port(port), _stop(false), _tcp_server(nullptr) {} void InitSever() { _tcp_server TcpServer::getinstance(_port); } void Loop() { int listen_sock _tcp_server-Sock(); while(!_stop) { struct sockaddr_in peer; socklen_t len sizeof(peer); int sock accept(listen_sock ,(struct sockaddr*)peer , len); if (sock 0) { continue; // 获取失败了不暂停 } int* msock new int(sock); pthread_t tid; pthread_create(tid , nullptr , Entrance::HandlerRequest , msock); pthread_detach(tid); }}~HttpSever(){} };
Prorocol
#pragma once #include unistd.h
#include iostream class Entrance
{ public: static void* HandlerRequest(void* _sock) { int sock *(int*)_sock; delete (int*)_sock; std::cout get a new link ... sock std::endl; close(sock); return nullptr; } };我们写完上面的代码之后编译运行 之后使用浏览器来访问我们服务器的8081端口
我们可以发现这样的现象 服务器一直在打开4和5套接字 这是为什么呢
因为浏览器不知道自己的请求有没有被服务器接收到 所以说浏览器一直重新发送请求 这也就导致了服务器一直打开新的套接字 为什么文件描述符是从4开始的呢 因为C文件默认会打开 0 1 2 三个文件描述符 此外监听套接字也会占用一个文件描述符
接收报头
此时我们的服务器就已经可以跑起来了 接下来我们就重点完成Prorocol里面的内容 接收到请求之后应该怎么做
我们首先让协议打印出我们接收的请求 该段代码如下
#ifndef DEBUG
#define DEBUG char buffer[4096]; recv(sock , buffer , sizeof(buffer) , 0); std::cout --------------begin------------- std::endl; std::cout buffer std::endl; std::cout -------------- end ------------- std::endl;
#endif 之后我们重新编译运行 此时我们就可以发现 我们的服务器可以打印浏览器发送过来的一些请求报头
接下来我们逐段分析下这些请求报头 我们可以看到在请求报头的第一行GET方法后面有个 \ 标志 这个标志和我们的Linux服务器根目录的标志十分相似 那么它到底是不是Linux服务器的根目录呢 我们的回答是 不一定
这个路径通常不是我们Linux服务器的根目录 而是由http服务器设置的一个WEB根目录
这个WEB根目录就是Linux下一个特定的路径
读取请求
其实我们HTTP处理请求的过程无非就是三步
读取请求分析请求构建响应并返回
我们目前仍然出于处理请求的阶段 有同学可能会有疑问 我们这里不是将数据全部都读取完毕了嘛
其实我们这里读取的数据是不准确的 char buffer[4096]; recv(sock , buffer , sizeof(buffer) , 0); std::cout --------------begin------------- std::endl; std::cout buffer std::endl; std::cout -------------- end ------------- std::endl; 我们读取的代码是这样子 它不管客户端每次发送过来多少的请求 我们直接将它放在一个4096字节的数组里面
但是有没有一种可能 客户端一次性会发来多个请求呢 那么这样子我们还是一次性读取4096个字节 是不是就会发生一个粘包的问题啊 那么这个时候我们就要想办法解决这个问题
我们从它的请求格式来分析 我们观察上图可以发现 实际上HTTP协议的报头是按照行来分隔的 并且在报头和正文中间有一个空行作为间隔 所以说我们就能很简单的将报头和正文分隔开
但是这里就会出现一个问题 那就是每个平台或是浏览器他们分隔一行的方式可能不同 具体有下面三种
xxxxxxx /r/nxxxxxxx /rxxxxxxx /n
所以说我们还要自己主动实现一个类来实现分隔行的问题
代码如下
#pragma once #include sys/types.h
#include sys/socket.h
#include iostream
#include string
// ¹¤¾ßÀà class Util
{ public: static int ReadLine(int sock ,std::string out) { char ch X; while(ch ! \n) { ssize_t s recv(sock , ch , 1 ,0); if (s 0) { if (ch \r) { // ת»¯Îª /n recv(sock , ch , 1 , MSG_PEEK); if (ch \n) { recv(sock , ch , 1 , 0); } else { ch \n; } } // 1. ÆÕͨ×Ö·û // 2 \n out.push_back(ch); } else if (s 0) { return 0; } else { return -1; } } return out.size(); }
}; 简单解释下上面的代码 它的作用是将每一行的结束标识符全部统一为\n
因为我们行结束的标志只有三种 所以我们可以做以下区分
如果读到了\n 则说明一行读取完毕如果读到了\r 我们不确定后面一个是不是\n 所以说我们需要窥探下一个字符是什么我们使用recv(sock , ch , 1 , MSG_PEEK)来窥探下一个字符 其中MSG_PEEK选项的含义就是下一个字符如果下一个字符是\n 则读取下一个字符将当前的\r覆盖掉如果不是\n则直接将当前字符修改为\n
在能够读取单行之后我们可以开始获取一个完整的Http请求了
我们将实现获取完整Http请求这个步骤放在Prorocol这个文件里面方便管理
具体的实现思想是
我们分别设置一个请求类和响应类来保存请求和响应之后我们再设计一个终端类 在这个终端类当中获取到完整的Http请求并且构建起响应
具体代码如下
#pragma once #include unistd.h
#include iostream
#include sys/types.h
#include sys/socket.h
#include string
#include vector
#include Util.hpp class HttpRequest{ public: std::string _request_line; std::vectorstd::string _request_header; std::string _blank; std::string _request_body;
}; class HttpResponse{ public: std::string _response_line; std::vectorstd::string _response_header; std::string _blank; std::string _response_body;
}; class EndPoint{ private: int _sock; HttpRequest http_request; HttpResponse http_response; private: void RecvRequestLine() { Util::ReadLine(_sock ,http_request._request_line); } public: EndPoint(int sock) :_sock(sock) {} void RcvRequest() {} void ParseRequest() {} void SendResponse() {} void BuildResponse() {} ~EndPoint() { close(_sock); }
}; class Entrance
{ public: static void* HandlerRequest(void* _sock) { int sock *(int*)_sock; delete (int*)_sock; std::cout get a new link ... sock std::endl; #ifdef DEBUG char buffer[4096]; recv(sock , buffer , sizeof(buffer) , 0); std::cout --------------begin------------- std::endl; std::cout buffer std::endl; std::cout -------------- end ------------- std::endl;
#else EndPoint* ep new EndPoint(sock); ep-RcvRequest(); ep-ParseRequest(); ep-BuildResponse(); ep-SendResponse(); delete ep;
#endif return nullptr; }
};
日志 为什么要写日志 这个问题也可以是为什么要有日志
日志可以帮助我们了解错误发生的原因和程序运行的状态 从而可以帮助我们更好的去排除错误或者是优化我们的程序 我们要写的日志格式是什么样的 格式如下 分别介绍下上面各个信息 日志级别 日志也是分级别的 这些级别让我们更好的辨别要如何处理这条日志
级别一般分为下面几种
INFO 正常的打印信息WARNING 警告 可能会有错误ERROR 有错误 但是不影响程序运行FATAL 致命的错误 程序运行不了了 时间戳 我们可以直接使用c语言函数来获取时间戳
c语言中的time函数能够返回给我们一个时间戳
函数原型如下
timenullptr使用时我们只需要传入参数nullptr即可 如果不是C11以后的版本传入NULL 日志信息 告诉我们错误和风险提示等信息 错误文件名称和行数 在c语言中预先定义了__FILE__ 以及 __LINE__ 我们直接使用它们来获取错误文件名称和行数即可
代码如下
#pragma once #include iostream
#include string
#include ctime #define INFO 1
#define WARNING 2
#define ERROR 3
#define FATAL 4 #define LOG(level , message) Log(#level , message , __FILE__ , __LINE__) void Log(std::string level, std::string message , std::string file_name , int line)
{ std::cout level time(nullptr) message file_name line std::endl;
} 之后我们只需要将LOG宏使用起来即可
请求读取与解析
一个完整的请求报文如下 我们读取请求行只需要读取第一行即可 void RecvRequestLine() { Util::ReadLine(_sock ,http_request._request_line); }那么 我们应该如何读取行数不确定的请求报头呢
我们现在知道的有两点
请求报头是一行一行发送的请求报头读取完毕之后下一个一定是空行
有了上面这两点之后我们就能很轻松的读取完所有的请求报头
只需要按行读取并且读取到\n结束即可 void RecvRequestLine() { Util::ReadLine(_sock ,http_request._request_line); } void RecvRequestHeader() { std::string line; while(true) { line.clear(); Util::ReadLine(_sock , line); if (line \n) { break; } line.resize(line.size()-1); // remove \n http_request._request_header.push_back(line); } if (line \n) { http_request._blank line; } } 解析请求行
读取到请求行和请求报头之后我们就开始解析它们
首先是请求行 格式如下 即 std::string method;std::string uri;std::string version; 所以说我们只需要按照空格作为分隔符 将请求行的代码分隔为三部分即可
而我们这里推荐使用stringstream 这个类中重载了流插入运算符默认会按照空格来进行分隔给字符串赋值 用法代码示例如下 void ParseHttpRequestLine(){auto line http_request._request_line;std::stringstream ss(line);ss http_request.method http_request.uri http_request.version; }解释下上面这段代码 我们首先使用line拷贝构造一个stringstream对象 之后让这个对象分别以空格为分隔符将它的内容赋值给http_request.method http_request.uri http_request.version
解析请求报头 经过观察我们不难发现 这里其实就是一个键值对结构 所以说我们使用哈希表来存储即可
有关于哈希表部分知识不理解的同学可以参考我的这篇博客
unordered_map
但是首先我们需要写一个方法让一个字符串按照冒号分隔为两部分
具体实现方法如下 static bool CutString(const std::string target , std::string key_out , std::string value_out , std::string sep){ size_t pos target.find(sep);if (pos ! std::string::npos) { key_out target.substr(0 ,pos);value_out target.substr(pos sep.size());return true; } return false;
}之后我们只需要遍历整个请求报头 一个个分割key和value之后插入哈希表中就可以
代码表示如下 void ParseHttpRequestHeader() { std::string key; std::string value; for (auto iter : http_request._request_header) { Util::CutString(iter , key , value , SEP); http_request.header_kv.insert({key,value}); } } 处理请求正文
处理完上面的两个部分之后我们再回过来看请求报文的图 此时我们就面临两个问题了
是否存在正文如果存在正文 那么正文有多少个字节
关于第一个问题
一般来说我们的请求方法如果是GET 则一般没有正文
如果我们的请求方法是POST 则一般有正文
关于第二个问题
我们可以由请求报头中的 Content–legth字段来知晓
判断是否存在正文的代码如下 bool IsRecvBody() { auto method http_request.method; if (method POST) { auto header_kv http_request.header_kv; auto iter header_kv.find(Content-Length); if (iter ! header_kv.end()) { http_request.content_length atoi(iter-second.c_str()); return true; } } return false; } 读取正文代码如下 void RecvRequestBody() { if (IsRecvBody()){int content_length http_request.content_length;auto body http_request._request_body; char ch 0;while(content_length){ssize_t s recv(_sock , ch , 1, 0);if (s 0){body.push_back(ch);content_length--;}else {break;}}}}至此 我们的请求和解析报文全部完成
处理请求并且构建响应
在处理这个请求之前 我们首先要考虑这个请求是否是合法的请求
如果是非法的请求 我们应该怎么操作
假设我们现在的Http服务器只接收GET和POST方法的请求 其他的请求只给予一个错误的响应即可 (我们这里最开始默认为404 NOT FOUND)
代码如下 void BuildResponse() { if (http_request.method ! GET http_request.method ! POST) { } } 对于响应报文来说 最重要的一部分就是我们的状态码了 HTTP状态码
HTTP的状态码如下
编号类别意义1XXInformational信息性状态码接收的请求正在处理2XXSuccess成功状态码请求正常处理完毕3XXRedirection重定向状态码需要进行附加操作以完成请求4XXClient Error客户端错误状态码服务器无法处理请求5XXServer Error服务器错误状态码服务器处理请求出错
其中我们要记住的有下面这几个 101 信息请求中 101表示客户端发送的请求正在处理中 但是因为网速变快 这种状态已经不怎么常见了 200 OK 这是最常见的一个状态码 也就是我们访问网页成功的时候网页返回的响应行 301 永久重定向 比如果一个老的网站废弃不用了使用一个新的网站 那么此时这个网站就可以使用永久重定向 如果有人还在访问这个网站就会跳转到新网站上
此外如果收藏夹中收藏了老的网站 新的网站会覆盖收藏夹中老的网站 302 307 临时重定向 从名字看就更好理解了 和301永久重定向相比一个是永久的一个是临时的 它并不会覆盖掉收藏夹中的老网站 403 权限不足 这个常见于我们去实习的时候 自己的权限特别低 如果leader丢给你一个文档而你没有观看的权限就会出现这个状态码 404 NOT FOUND 常见于资源消失不见被删除或过期 又或者说资源根本不存在
比如说你访问一个网站的时候带上一个不存在的资源路径你就会看到这个状态码 504 Bad Gateway 常见于服务器出现问题 和客户端无关 Redirection重定向状态码 除了上面那些要记住的状态码之外 我们还需要更深入的理解重定向状态码
重定向又分为永久重定向和临时重定向 其中301表示永久重定向 302 307表示临时重定向
临时重定向和永久重定向本质是影响客户端的标签 决定客户端是否需要更新目标地址
如果某个网站是永久重定向 那么第一次访问该网站时由浏览器帮你进行重定向 但后续再访问该网站时就不需要浏览器再进行重定向了 此时你访问的直接就是重定向后的网站
而如果某个网站是临时重定向 那么每次访问该网站时如果需要进行重定向 都需要浏览器来帮我们完成重定向跳转到目标网站
获取参数
其实目前从宏观的角度上来说 我们的上网行为可以分为两种
浏览器向服务器上传数据浏览器向服务器请求数据 其中向浏览器上传数据的时候我们有两种方法
使用GET方法 通过url来进行传参使用POST方法 通过正文来进行传参 客户端为什么要将数据上传到服务器呢 当然是为了让服务器对客户端传输上来的数据进行处理
这里根据GET和POST传参方式的不同 我们处理请求的方式也需要发生一点变化
由于POST方法传参是通过正文传递参数的 而正文我们已经获取到了 所以说不用关心
但是GET方法传参是通过url传参 而url在请求行中 需要我们特殊处理一下
处理的代码如下 其实就是复用了我们前面剪切字符串的功能函数 auto code http_response.response_code; if (http_request.method ! GET http_request.method ! POST) { // waring request code NOT_FOUND; goto END; } if (http_request.method GET) { size_t pos http_request.uri.find(?); if (pos ! std::string::npos) { Util::CutString(http_request.uri,http_request.path, http_request.query_string , ?); } else { http_request.path http_request.uri; } } std::cout debug url: http_request.uri std::endl; std::cout debug path: http_request.path std::endl; std::cout debug query_string: http_request.query_string std::endl; 下面是运行结果
Web根目录
我们得到rul之后可以看到这样的一串标识符
这里的路径表明了是请求Linux服务器上的某种资源 那么这种资源是从哪里开始的呢是根目录嘛这个路径对应的资源是如何判断存在的 问题一资源是从哪里开始的 这个资源不一定是从根目录开始的
一般来说我们会自己指定一个web根目录 所有的资源都在这个web根目录当中
一般来说web根目录的名称是wwwroot如下 如果说访问我们服务器的客户端没有指明想要获取什么资源的话我们肯定不可能将web根目录下的所有资源全部发出去
所以说这个时候我们就要指定一个默认的资源 一般来说这个资源就是index.html 所以说此时我们的path就不能简单的是/a/b/c了 我们要在前面加上web根目录在Linux服务器中的定位 比如说像这样 wwwroot(web根目录的路径) 加上 /a/b/c定位代码如下
#define WEB_ROOT wwwroot/std::string _path http_request.path;
http_request.path WEB_ROOT;
http_request.path _path;
std::cout debug: http_request.path std::endl; 演示效果如下 我们前面也说过了 如果客户端请求的是Web根目录的话我们不可能将整个Web根目录的所有资源全部给他 所以说针对于请求web根目录的情况我们要做一些特殊处理
如果请求的是我们的web根目录我们就返回index.html页面给它
代码表示如下 #define HOME_PAGE index.htmlhttp_request.path WEB_ROOT; http_request.path _path; if (http_request.path[http_request.path.size()-1] /) { http_request.path HOME_PAGE; } std::cout debug: http_request.path std::endl; 演示效果如下 问题二 如何确认这个资源是存在的 我们首先使用百度来试验下 如果资源不存在会怎么样 可以发现 百度服务器直接给我们返回了一个404告知我们该资源不存在
所以说我们在返回给客户端请求之前需要确认一个资源是否存在
这里使用确认资源是否存在的函数是stat函数 stat函数 函数原型如下
int stat(const char* path , struct stat *buf)参数说明
const char* path 是我们要寻找的路径是一个字符串struct stat *buf 这是一个结构体 我们通过该结构体来查看文件的信息
返回值说明
如果找到该文件返回0 如果没找到返回-1 struct stat st;if (stat(http_request.path.c_str() , st) 0){ // exist } else { // not exist code NOT_FOUND;goto END;}所有的目录中都有一个index.html嘛 是的 因为一个目录中可能有着大量的网页资源 如果我们不设置一个默认的index.html则系统就不知道应该返回哪个资源给客户端了
在回答了问题二之后便会衍生出一个问题三
存在的资源就是可以读取的资源嘛
存在的资源可能是一个目录存在的资源可能是一个可执行程序 请求的资源是目录 我们可以使用下面的方法来判断是否请求的资源是一个目录
S_ISDIR(st.st_mode)S_ISDIR是一个宏 而st_mode则是stat结构体中的一个成员变量
使用这个宏我们就能够判断当前请求的资源是否是一个目录
如果是一个目录 我们前面介绍过 每个目录都会有一个index.html 所以我们直接在请求的路径上加上即可 请求的资源是可执行程序 一般来说 客户端请求可执行程序是被允许的
但是我们这里要对于这种情况做一下特殊处理
我们通过下面的方法来确定文件是否是一个可执行文件 if ((st.st_modeS_IXUSR) || (st.st_modeS_IXGRP) || (st.st_modeS_IXOTH)) { // special treatment } 关于如何特殊处理 本文后面会详细讲解
HTTP CGI机制简单介绍
CGI(Common Gateway Interface) 是WWW技术中最重要的技术之一 有着不可替代的重要地位
CGI是外部应用程序CGI程序与WEB服务器之间的接口标准 是在CGI程序和Web服务器之间传递信息的过程
其实 要真正理解CGI并不简单 首先我们从现象入手
浏览器除了从服务器下获得资源网页 图片 文字等 有时候还有能上传一些东西提交表单 注册用户之类的 看看我们目前的http只能进行获得资源 并不能够进行上传资源所以目前http并不具有交互式 为了让我们的网站能够实现交互式 我们需要使用CGI完成 时刻记着 我们目前是要写一个http 所以 CGI的所有交互细节 都需要我们来完成
理论上 可以使用任何语言来编写CGI程序
需要注意的是 http提供CGI机制 和CGI程序是两码事 就好比学校http提供教学CGI机制平台 学生CGI程序来学习 反应到具体的web服务器中 什么是CGI机制呢
我们首先创建一个handerdate.exe作为一个可执行文件 之后的过程如下
浏览器传输数据给服务器中的HTTPSERVERHTTPSERVER接收到数据之后不做处理 将输出传递给HANDERDATEHANDERDATE处理完数据之后再将处理完的数据传递给SERVERHTTPSERVER接收到数据之后将处理过的数据传递给浏览器 调用目标程序 传递目标数据 拿到目标结果 这中间用到的就是CGI技术
那么我们什么时候需要用到CGI技术呢
答案是只要用户上传上来数据此时我们就要用到CGI技术 此时我们只需要将cgi标志位设置为开启即可
最后我们通过判断cgi标志位是否开启来判断使用什么方法 代码表示如下 if (http_request.cgi) { ProcessCgi(); } else { ProcessNonCgi(); // return html } 关于大小写转化的问题
因为我们对于GET和POST方法并没有做出严格的大小写规定 而我们在项目中却使用了大写作为判定条件 这就有可能会导致一些错误的发生 所以说我们保证我们接收的method要转化为大写
C提供了一个函数来实现这个功能 OutputIterator transform (InputIterator first1, InputIterator last1,OutputIterator result, UnaryOperator op)参数说明
第一个参数是要转化起始位置的迭代器第二个参数是要转化末尾位置的迭代器第三个参数是最后要存放结果的位置的迭代器第四个参数是要转化的方式大写或者小写
代码表示如下 auto method http_request.method; std::transform(method.begin() , method.end() , method.begin() , ::toupper ); 构建响应
构建响应之前我们首先来回顾下响应的报文是什么样子的 它要有状态行 响应报头 空行 响应正文
所以说我们不单单要只返回一个静态网页正文 还需要加上前面的请求行 报头等信息
添加状态行
状态行由http版本 状态码 状态码描述符组成
其中我们默认http版本就是1.0版本
默认的状态码是200OK
状态码描述需要和状态码相匹配
此时我们只要设置一个函数 传入code输出一个状态码的string对象就可以 代码如下
static std::string Code2Desc(int code)
{ std::string desc; switch(code) { case 200: desc OK; break; case 404: descNot Found; break; default: break; } return desc; }之后一步步写好状态行就好了 http_response._response_line HTTP_VERSION; http_response._response_line ; http_response._response_line std::to_string(http_response.response_code); http_response._response_line ; http_request._request_line Code2Desc(http_response.response_code); 我们设置默认的响应行分隔符为 \r\n
#define LINE_END \r\n 之后响应报头的内容我们这里暂时跳过
添加响应正文
在前面我们已经获取了请求读取的路径
一般来说现在我们只需要根据那个路径打开对应的资源 之后将资源写到报文的正文中即可
但是在实际填写正文的过程中我们会遇到这样子的问题
我们写的body是用户层的缓冲区我们需要的网页html是磁盘中的文件磁盘中的文件要到用户层必须要经历内核层所以说如果我们使用read write等函数则IO效率较低 这里给大家介绍一个函数 sendfile
它的作用是不用经历用户层 直接在内核缓冲区拷贝数据 从而提高效率
它的函数原型如下
ssize_t sendfile(int out_fd, int in_fd , off_t* set , size_t count)参数说明
out_fd 是我们要往这里写数据的文件描述符in_fd 是我们要从这里读数据的文件描述符set 我们不用管 设置为空即可count 表示我们要拷贝的数据大小 以字节为单位
返回值说明
如果成功拷贝则返回成功拷贝的字节数 失败返回-1
发送响应
我们一步步将状态行 响应报头 空行 正文发送即可 void SendResponse() { write(_sock ,http_response._response_line.c_str() , http_response._response_line.size()); for(auto it : http_response._response_header) { write(_sock, it.c_str() , it.size()); } write(_sock,http_response._blank.c_str() , http_response._blank.size()); sendfile(_sock , http_response.fd , nullptr , http_response.size); close(http_response.fd); } 最后我们将index.html中写上hello world
编译运行后使用浏览器尝试接收响应 运行结果如下 由于博主并没有系统的学习前端知识 所以说这里就不写网页的前端了
如果有同学感兴趣可以自己写一些前端的代码放到web根目录下
添加响应报头
响应报头有很多字段可以填充 我们这里只填充两个比较重要的字段
一个是Content-length 即正文的大小
一个是Content-type 即正文的类型
正文的大小其实我们之前已经有过了 这里我们只需要插入到报头中即可 代码如下 std::string content_length_string Content-Length: ;content_length_string std::to_string(size); 而正文的类型则是我们比较难判断的一点
一般来说我们会根据文件名的后缀来判断这个文件是什么类型 当构建响应的时候我们也需要告知浏览器我们返回的是什么类型的资源
所以说我们的第一步操作就是后缀提取 found http_request.path.rfind(.); if (found std::string::npos) { http_request.suffix .html; } else { http_request.suffix http_request.path.substr(found); } Content-Type在文件后缀和自身之间有一张对照表 所以说在我们截取了文件的后缀之后还需要在表中找到对应的内容
我们这里为了方便起见使用静态函数的方式来帮助我们找到后缀对应的内容
同学们也可以尝试使用类来封装
static std::string Suffix2Desc(const std::string suffix)
{ static std::unordered_mapstd::string , std::string suffix2desc { {.html , text/html}, {.css , text/css}, {.jpg , text/html} }; auto iter suffix2desc.find(suffix); if (iter ! suffix2desc.end()) { return iter-second; } return text/html;
} 设计CGI程序
当浏览器请求的资源是一个可执行文件的时候 此时我们的服务器就会触发CGI模式 现在我们就可以开始编写CGI程序了 根据上面的原理图我们可以知道
httpsever是一个进程CGI程序也是一个进程那么我们应该如何用一个进程去执行另外一个进程呢
其实早在进程控制章节我们就学过了方法 那就是进程替换
当然我们在讲解程序替换的时候也说过 我们不能使用主进程进行进程替换使用主进程该进程就变成一次性的了 处理不了下一个任务 而应该使用子进程
整体的代码如下 int ProcessCgi() { pid_t pid fork(); if (pid 0) { // child } else if (pid 0) { // error LOG(ERROR , fork error); return 404; } else { // father waitpid(pid , nullptr , 0); } return OK; } 我们创建子进程的目的当然是为了让他去执行目标程序
那么目标程序是什么呢 它实际上就是浏览器传输给我们的path
进程间通信
建立管道
httpsever需要将数据传输给cgi程序 cgi程序处理完数据之后也需要将数据回传给httpsever
所以说我们这里就要用到进程间通信
因为httpsever进程和我们设计的cgi进程之间本质上是一个父子进程的关系 所以说我们选用进程间通信中的匿名管道
而由于此时我们需要数据进行双向传递 所以说我们可以设计一个双向的管道
为了不混淆这个双向管道的读取 我们约定 所有操作都站在父进程的视角上命名
代码表示如下 int input[2];int output[2]; if (pipe(input) 0){ return 404;}if (pipe(output) 0){ return 404; } pid_t pid fork();if (pid 0){// childclose(input[0]);close(output[1]); } else if (pid 0){ // error LOG(ERROR , fork error); return 404;}else {// fatherclose(input[1]);close(output[0]);waitpid(pid , nullptr , 0);}return OK;}进程替换
我们选择使用execl函数来进行进程替换
函数原型如下
int execl(const char *path, const char *arg, ...);我们先看这个函数的名字 相比我们的exec多了一个l
这个l其实就是列表的意思 意味着它的参数要使用列表的形式传入
它的第一个参数是 const char *path 它代表着要执行程序的路径
它的第二个参数是 const char *arg, ... 它代表着可变参数列表 是使用NULL结尾的
例如我们要执行ls程序的话 就可以写出下面的代码 execl(/usr/bin/ls , ls , -a , -i , NULL);当然如果我们直接使用路径作为可执行程序也是可以的 比如 execl(/usr/bin/ls , /usr/bin/ls , -a , -i , NULL);所以说我们程序替换的代码是 execl(bin.c_str() , bin.c_str() , nullptr);我们在进行程序替换之后把原先子进程的程序和代码全部替换了
那么替换后的子进程如何得知原先的管道信息呢
代码和数据全部没有了 但是我们要知道的是也仅仅是代码和数据没有了
它并不替换内核进程相关的数据结构 实际上原先子进程的文件描述符表依旧存在
但是数据已经被我们全部删除了 我们要怎么找到呢这两个文件描述符呢
此时我们可以做出以下的约定
让读取管道等价于读取标准输入让写入管道等价于写入标准输出
而由于标准输入和标准输出的文件描述符是固定的
所以说我们直接进行重定向即可 dup2(input[1] , 1);dup2(output[0], 0); 交互数据
在交互数据之前我们首先要知道父进程的数据在哪里
对于GET方法来说父进程的数据一定是在uri当中
对于POST方法来说 父进程的数据一定是在正文当中
POST方法处理
代码表示如下 if (method POST){const char* start body_text.c_str();int total 0;int size 0;while(1){size write(output[1] , starttotal , body_text.size()-total);if (size 0){totalsize;} else {break;}}}上面的代码我们做了一个小处理 让write一直写 直到写入成功的数据为0为止 这主要是为了防止数据太多 一次write写不完的情况出现
GET方法处理
首先我们要知道一点 进程替换是不会替换环境变量的 而子进程会继承父进程的环境变量 所以说我们可以直接使用父进程或者没有替换过的子进程的环境变量给替换后的子进程传递数据
代码标识如下 if (method GET) { query_string_env QUERY_STRING; query_string_env query_string; putenv(query_string.c_str()); } 传递传参方法
在子进程接收数据之前我们还要让子进程确认一点
浏览器传递参数使用的到底是什么方法
此时我们还是可以通过环境变量让子进程知道是用什么方法传递的参数 method_env METHOD; method_env method; putenv((char *)method_env.c_str()); CGI程序接收数据
我们首先写出一个CGI程序编译并且将这个程序放到wwwroot目录中
CGI程序代码如下
#include iostream
#include cstdlib using namespace std; int main()
{ cerr Debug Test : getenv(METHOD) endl; return 0;
} 这里需要注意的是我们使用的是cerr而不是cout 这是因为我们的cout使用的是标准输出 而标准输出已经被我们重定向了 如果使用cout将不会在显示器上输出任何结果 此时我们发现确实cgi程序确实能够得到浏览器传参的方法
接下来就是根据传参方法的不同使用不同的方式去获得数据了 具体为
GET方法 使用环境变量获得数据POST方法 使用管道获得数据
代码表示如下 if (method GET) { query_string getenv(QUERY_STRING); cerr Debug QUERY_STRING: query_string endl; } else if (method POST) { int cl atoi(getenv(CONTENT_LENGTH)); char c 0; while(cl) { read(0 , c , 1); query_string.push_back(c); cl--; } } else { ; } CGI程序处理数据
我们假设接收的数据是 x100y200
那么首先我们先要得到各个参数的名称和值 很简单的一个字符串分隔即可
void CutString(string in, const string sep, string out1 , string out2)
{auto pos in.find(sep);if (string::npos pos){ return; } out1 in.substr(0 , pos); out2 in.substr(possep.size());
} 值得注意的是 第二个参数最好加上const修饰 原因有二
分隔符一般是不做修改的如果我们不加const修饰 则分隔符必须要用string对象 而不能进行隐式类型转换 比如说填写“” 这样子就是不可以的 does not name a type 错误
博主在使用auto推导pos类型的时候遇到了这个错误
实际上在这里这个错误的产生的原因是在makefile文件中没有使用c11来编译该文件的 加上-stdc11之后错误即可解决 之后我们调用函数处理这批数据即可 string str1;string str2;CutString(query_string , , str1 , str2);string name1; string value1; CutString(str1 , , name1 , value1); string name2; string value2; CutString(str2 , , name2 , value2); 父进程读取处理完的数据
在cgi程序中 我们让程序向管道中写入了自己处理完的数据
到了父进程当中 我们只需要让父进程从管道中读取数据即可 代码如下 char ch 0; while(read(input[0] , ch , 1)) { response_body.push_back(ch); } CGI程序总结
我们的CGI程序总结可以浓缩为下面的一张图 简单介绍下
首先浏览器通过GET或者POST方法将想要请求的资源和参数如果有的话上传服务器服务器进行判断方法是POST还是GET 是否带参如果不带参则直接构建响应返回如果携带参数则根据方法获得参数父进程创建管道之后使用fork函数创建子进程cgi程序之后通过管道传递数据给子进程处理子进程处理完数据之后将处理完的数据传递给父进程之后父进程构建响应将响应传递给浏览器
我们如果将中间的步骤全部省略 就能得到这样一个图 cgi程序从浏览器获取数据cgi程序加工完数据之后再传递给浏览器 这样设计有什么好处呢 实际上我们使用cgi模式将通信和服务高度解耦了
这使得我们的cgi程序不必关注通信细节 只需要专心设计好服务即可
状态码介绍
HTTP的状态码如下
编号类别意义1XXInformational信息性状态码接收的请求正在处理2XXSuccess成功状态码请求正常处理完毕3XXRedirection重定向状态码需要进行附加操作以完成请求4XXClient Error客户端错误状态码服务器无法处理请求5XXServer Error服务器错误状态码服务器处理请求出错
其中我们要记住的有下面这几个 101 信息请求中 101表示客户端发送的请求正在处理中 但是因为网速变快 这种状态已经不怎么常见了 200 OK 这是最常见的一个状态码 也就是我们访问网页成功的时候网页返回的响应行 301 永久重定向 比如果一个老的网站废弃不用了使用一个新的网站 那么此时这个网站就可以使用永久重定向 如果有人还在访问这个网站就会跳转到新网站上
此外如果收藏夹中收藏了老的网站 新的网站会覆盖收藏夹中老的网站 302 307 临时重定向 从名字看就更好理解了 和301永久重定向相比一个是永久的一个是临时的 它并不会覆盖掉收藏夹中的老网站 403 权限不足 这个常见于我们去实习的时候 自己的权限特别低 如果leader丢给你一个文档而你没有观看的权限就会出现这个状态码 404 NOT FOUND 常见于资源消失不见被删除或过期 又或者说资源根本不存在
比如说你访问一个网站的时候带上一个不存在的资源路径你就会看到这个状态码 504 Bad Gateway 常见于服务器出现问题 和客户端无关 Redirection重定向状态码 除了上面那些要记住的状态码之外 我们还需要更深入的理解重定向状态码
重定向又分为永久重定向和临时重定向 其中301表示永久重定向 302 307表示临时重定向
临时重定向和永久重定向本质是影响客户端的标签 决定客户端是否需要更新目标地址
如果某个网站是永久重定向 那么第一次访问该网站时由浏览器帮你进行重定向 但后续再访问该网站时就不需要浏览器再进行重定向了 此时你访问的直接就是重定向后的网站
而如果某个网站是临时重定向 那么每次访问该网站时如果需要进行重定向 都需要浏览器来帮我们完成重定向跳转到目标网站 本项目中只使用了状态码404来减少工作量
在同学们围绕这个项目做拓展的时候可以参考上面的状态码写出更多 代码大同小异 难度也不大 主要是html页面的编写
处理读取出错
一般来说我们的http项目会出现两种类型的错误
逻辑错误 读取完毕 我们要给予对方回应 告诉对方为什么出错读取错误 读取不一定完毕 此时不给对方回应 退出即可
此处我们针对读取错误做出一些处理
首先我们在EndPoint类中设置一个成员变量bool stop 并且在构造函数中将它默认设定为false
之后我们将EndPoint里面的一些读取函数中加上读取错误判定
一旦我们判定此处读取错误则设置stop true
此处列举几处需要添加读取判断的地方 具体内容可以参考gitee上的源码在文章的最后 bool RecvRequestLine() { if(Util::ReadLine(_sock ,http_request._request_line) 0 ) { std::cout http_request._request_line ; } else { stop true; } return stop; } bool RecvRequestHeader(){std::string line;while(true){line.clear();if (Util:: ReadLine(_sock , line) 0){stop true;}if (line \n){ break;}line.resize(line.size()-1); // remove \nhttp_request._request_header.push_back(line);}if (line \n){http_request._blank line;}return stop;}
如果说stop为true 则我们在最后就不构建和发送数据了 代码表示如下 if (!ep-Stop()) { ep-BuildResponse(); ep-SendResponse(); } 处理写入错误
在进程间通信这一章中我们讲过两个进程通信的四种特殊情况
这其中有一种情况就是 读取端关闭 写入端就会强制关闭
实际上我们在深入理解之后也知道了 这是因为操作系统发送了13号信号的缘故
而在我们这次的项目当中 服务器是要尽量保持24h开机的 不可能因为读取端退出就关闭
所以说我们要忽略13号信号的作用 代码表示如下 signal(SIGPIPE , SIG_IGN); 多线程转化线程池
目前我们所使用的执行任务的方式是多线程 这种方式有以下的缺点
每次都是任务来之后再创建线程 浪费时间每次都需要创建和销毁线程 浪费时间如果线程数量并发过多会影响系统性能 从而导致卡顿
而以上的问题我们都可以通过线程池来解决一部分
所以说为了增强代码的健壮性我们将原本的多线程模式转化为线程池模式 设计思路如下
在服务器中设计一个任务队列每次浏览器上传任务就上传到服务器的任务队列中在服务器中设计一个线程池线程池中的线程从task_queue中拿任务来执行
有细心的同学可能发现了 这实际上就是一个生产者消费者模型
那么我们首先来设计一个任务类
设计任务类
任务类只需要有两个成员变量
套接字处理方法
套接字是为了让线程知道要处理什么
处理方法是为了让线程只要要用什么方法处理
代码表示如下 #pragma once #include iostream class Task { private: int sock; CallBack handler; public: Task() {} Task(int _sock) :sock(_sock){} void ProcessOn() { handler(sock); } ~Task() {} }; 设计回调函数 调用函数处理这个工作之前已经有一个Entrance 类做到过了 我们要做的只是给他改个名字
class CallBack
{public: CallBack() {} void operator()(int sock) { HandlerRequest(sock); } 之后再给这个类加上一个仿函数方便我们后续调用
设计线程池
线程池的设计思路如下
首先要有一个任务队列来存放任务要有一个num来标志任务队列的最大值有个锁和条件变量来保证同步和互斥
代码表示如下 class ThreadPool{private:std::queueTask task_queue;int num;bool stop;pthread_mutex_t lock;pthread_cond_t cond;之后就是一些常见的函数编写
比如说 加锁 解锁 睡眠 唤醒 添加任务 删除任务等等 这些代码在生产者消费者模型那一章节已经写过 这里就不再赘述 需要看全部代码的同学可以在文末连接处查看
补充内容
表单测试
此处会涉及到一些前端知识 有兴趣的同学可以去深入了解下 一般来说我们可以通过表单从前端页面向后端提交数据
表单的格式如下
form.
form elements.
/form我们可以写一个表单网页来测试我们的程序
它使用GET方法传参 分别传递两个数据x和y
!DOCTYPE html
html
bodyform action/testcgi methodGET
First name:br
input typetext namedate_x value0
br
Last name:br
input typetext namedate_y value1
brbr
input type submit valuesubmit
/form/body
/html我们前面学过了 GET方法是通过url传参的 那么当我们点击submit提交的时候在url中也应该出现参数 我们发现结果确实符合预期
cgi程序支持多种语言
除了c之外 我们的cgi程序还可以使用其他多种语言编写
比如说c语言 python java php
由于博主目前为止只学过c/c 没办法给大家演示 同学们如果有什么有意思的程序也可以在cgi上做一些扩展
项目代码
gitee