自主web服务器
前言
http背景
http协议被广泛使用,从移动端,pc端浏览器,http是打开互联网应用窗口的重要协议,http在网络应用层中的地位不可撼动,是能准确区分出前后台的重要协议
目标
对http协议的理论学习,从零开始完成web服务器开发,实现核心部分,从理论到实践。通过实践来加深对理论部分的理解,看到理论的现象。
描述
采用C/S模型,编写支持中小型应用的http,并结合mysql,理解常见互联网应用行为,从技术上理解从上网开始到关闭浏览器的所有操作中的技术细节。
技术特点
- 网络编程(TCP/IP协议,socket流式套接字,http协议)
- 多线程技术
- cgi技术
- 线程池
项目背景
开发环境
开发环境centos 7+vim/gcc/gdb+、C/C++
www
www是环球信息网的缩写(也称作“Web”,“WWW”,“W3”,"环球网“等,常简称为Web)。Web分为Web客户端和Web服务器程序。
WWW可以让Web客户端(常用浏览器)访问浏览Web服务器上的页面,WWW是一个由许多互相链接的超文本组成的系统。在这个系统中,每个有用的事物称为一样”资源“,并且用一个全局”统一资源标识符“(URI)表示。这些资源通过超文本传输协议(HTTP)传送给用户,而后者通过点击链接来获得资源。
总结来说:WWW是访问互联网资源的一套生态,将所有的内容称为资源,规定了通过url的方式访问资源,并通过设计http协议将资源从远端拉取到本地传送。
这样的一套标准是W3C理事会创建的。
客户端浏览器发展史
因为浏览器离用户最近,互联网公司开发自家浏览器可以内置自家相关生态,便于互联网公司引流,因此发展史波澜起伏。
- 1990年11月,世界上第一台Web服务器和Web浏览器诞生
- 1993年1月,NCSA研发html内联显示图片的浏览器,不久windows和mac版的相关浏览器出现
- NASA httpd 1.0差不多出现
- 1994年网景公司浏览器出现
- 1995年微软发布IE1.0和2.0
- 2000年网景衰弱
- 2004年,Mozilla基金发布firebox,第二次浏览器大战开始
- 随后IE发布6,7,8,9,10版本,同步Chrome,Opera,Safari浏览器开始抢占市场
- 今天浏览器格局形成
服务器端http发展史
- 1990年,HTTP/0.9诞生
- 1996年5月,HTTP/1.0诞生(我们项目实现的标准)
- 1997月HTTP/1.1诞生,目前最主流版本
- HTTP/2.0正在定制
Tcp/IP分层概述
TCP/IP通信传输流
DNS域名解析
各协议之间的协作
http特点
- 简单快速,HTTP服务器的程序规模小,因而通信速度很快。
- 灵活,HTTP允许传输任意类型的数据对象,正在传输的类型由Content-Type加以标记。
- 无连接,每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。(http/1.0具有的功能,http/1.1兼容)
- 无状态。(http协议每当有新的请求产生,就会有对应的新响应产生。协议本身并不会保留你之前的一切请求或者响应,这是为了更快的处理大量的事务,确保协议的可伸缩性)
至于HTTP1.1中引入了Cookie,在网络基础部分中讲解。
URI、URL、URN
- URI,identifier,统一资源标识符,用来唯一地标识一个资源。(宏观)
- URL,locator,统一资源定位符,它是一种具体的URI,即URL不光光可以用来标识一个资源,而且还指明了如何locate这个资源,能够找到它。(具体)
- URN,name,统一资源命名,是通过名字来标识资源。
例如:isbn:0-395-36341-1是URI,一个国际标准书号,可以唯一确定哪本书,但是没法直接找到这本书。而两个图书馆的中url都有这个书,通过两个图书馆的url都可以找到这本书。
如果用户的URL没有指明要访问的某种资源(路径),虽然浏览器默认会添加/,但是依旧没有告知服务器要访问什么资源,此时默认返回对应服务的首页。
浏览器URL基本格式
- HTTP(超文本传输协议)是基于TCP的连接方式进行网络连接
- HTTP/1.1版本中给出一种持续连接的机制(长链接)
- 绝大多数的Web开发,都是构建在HTTP协议之上的Web应用
HTTP URL (URL是一种特殊类型的URI,包含了如何获取指定资源)的格式如下:
http://host:post/abs_path
- http表示要通过HTTP协议来定位网络资源
- host表示合法的Internet主机域名或者IP地址,本主机IP:127.0.0.1
- port指定一个端口号,为空则使用缺省端口80
- abs_path指定请求资源的URI
- 如果URL中没有给出abs_path,那么当它作为请求URI时,必须以“/”的形式给出,通常这个工作浏览器自动帮我们完成。
/是不是linux服务器的根目录开始呢?
不一定,通常不会设置成根目录,我们通常由http服务器设置为自己的WEB根目录(就是linux下一个特定的路径)
http请求与响应
示意图
http报文细节
http请求方法
序号 | 方法 | 描述 |
---|
1 | GET | 获取资源,获取被URI标识的资源。 | 2 | POST | 传输实体主体,和get一样很常见,向服务器提交资源让服务器处理,比如提交表单、上传文件等,可能导致建立新的资源或者对原有资源的修改。提交的资源放在请求体中。 | 3 | HEAD | 获取报文首部,和GET类似,但是不返回报文主体部分。用于确认URI的有效性以及资源的日期等。 | 4 | PUT | 传输文件,将指定文件放的URI所标示的路径,类似ftp,但是有安全问题,大部分web都不用 | 5 | DELETE | 与PUT相反,删除URI指定的资源,不安全,一般也不会被使用。 | 6 | CONNECT | 使用隧道协议链接代理。HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。就是把服务器作为跳板,去访问其他网页然后把数据返回回来,连接成功后,就可以正常的get、post了。 | 7 | OPTIONS | 询问支持方法 | 8 | TRACE | 追踪路径 |
http状态码
| 类别 | 原因短语 |
---|
1xx | Informational(信息性状态码) | 接受的请求正在处理 | 2xx | Success(成功状态码) | 请求正常处理完毕 | 3xx | Redirection(重定向) | 需要进行附加操作以完成请求 | 4xx | Client error(客户端错误) | 客户端请求出错,服务器无法处理请求 | 5xx | Server Error(服务器错误) | 服务器处理请求出错 |
常用的14个状态:
2XX系列 | 含义:正确相关 |
---|
200-OK | 表示从客户端发来的请求在服务器端被正常处理了 | 204-No Content | 表明请求结果被正确处理了,但是响应信息中没有响应正文 | 206-Partial Content | 该状态码表示客户端对服务器进行了范围请求,而且服务器成功的执行了这部分GET请求,响应报文中包含由Content-Range指定的实体内容范围 |
3XX系列 | 含义:重定向相关 |
---|
301-Moved Permanently永久性重定向 | 该状态码表示请求的资源已经被分配了新的URI,以后应使用新的URI,也就是说,如果之前将老的URI保存为书签了,后面应该按照响应的Location首部字段重新保存书签 | 302-Found临时性重定向 | 目标资源被分配了新的URI,希望用户本次使用新的URI进行访问 | 307-Temporary Redirect临时重定向 | 有空看看三者区别:https://www.cnblogs.com/cswuyg/p/3871976.html |
301和302的区别就是之前笔记中餐厅搬家和因为修路临时搬家的例子。
400系列 | 含义:表明客户端发生错误的原因 |
---|
400-Bad Request客户端错误 | 该状态码表明请求报文中存在语法错误,需修改请求内容重新发送, | 403-Forbidden | 该状态码表明浏览器所请求的资源被服务器拒绝了。服务器没有必要给出详细理由,如果想要说明,可以在响应实体内部进行说明。 | 404-Not Found | 服务器上没有对应请求的资源 |
500系列 | 含义:服务器错误 |
---|
500-Internal Server Error | 表明服务器端在执行的时候发生了错误,可能是Web本身存在的bug或者临时故障。 | 503-Server Unavailable | 状态码表明服务器目前处于超负载或正在进行停机维护状态,目前无法请求处理。这种情况下,最好写入Retry After首部字段在返回给客户端 |
项目代码
.h和.hpp
一般来说,.h里面只有声明,没有实现;而.hpp里声明实现都有,后者可以减少.cpp的数量。
所以,该类的调用者只需要include这个hpp文件即可,无需再将cpp文件加到project中进行编译。而实现代码将直接编译到调用者的obj文件中,不再生成单独的obj文件,采用hpp文件将大幅度减少调用project中的cpp文件数与编译次数,也不用再发布烦人的lib与dll文件,所以非常适合用来编写公开的开源库。
传输层及网络层封装
TcpServer.hpp封装
- 将TcpServer实现成饿汉的单例模式。
- 关于锁和多线程,可以使用C++的库,但是为了达到复习pthread库接口和回顾底层的效果,这里采用pthread原生库。
- 关于锁的初始化问题,锁初始化可以使用pthread_mutex_init,如果是全局锁/静态锁可以使用
PTHREAD_MUTEX_INITIALIZER 初始化并且不用destroy。
#pragma once
#include"Log.hpp"
#include<iostream>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/ip.h>
#include<pthread.h>
#include<unistd.h>
#include<cstring>
#include<string.h>
#define BACKLOG 5
class TcpServer {
private:
int _listenSock;
int _port;
static TcpServer* _tcpServerPtr;
TcpServer(int port) : _listenSock(-1), _port(port) {};
TcpServer(const TcpServer& t) = delete;
TcpServer& operator=(const TcpServer& t) = delete;
public:
static TcpServer* Getinstance(int port) {
if (nullptr == _tcpServerPtr) {
static pthread_mutex_t _mtx = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&_mtx);
if (nullptr == _tcpServerPtr) {
_tcpServerPtr = new TcpServer(port);
LOG(INFO, "new TcpServer success");
_tcpServerPtr->InitTcpServer();
}
pthread_mutex_unlock(&_mtx);
}
return _tcpServerPtr;
}
private:
void InitTcpServer() {
Sock();
Bind();
Listen();
LOG(INFO, "InitTcpServer success");
}
void Sock() {
_listenSock = socket(AF_INET, SOCK_STREAM, 0);
if (_listenSock < 0) {
LOG(FATAL, "listenSock create error");
exit(1);
}
int opt = 1;
setsockopt(_listenSock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
LOG(INFO, "listenSock success && setsockopt");
}
void Bind() {
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;
socklen_t len = sizeof(local);
if (bind(_listenSock, (struct sockaddr*)&local, len ) < 0) {
LOG(FATAL, "bind error");
exit(2);
}
LOG(INFO, "bind success");
}
void Listen() {
int res = listen(_listenSock, BACKLOG);
if (res == -1) {
LOG(FATAL, "listen error");
exit(3);
}
LOG(INFO, "listen success");
}
public:
int GetListenSock() {
return _listenSock;
}
~TcpServer() {
if (_listenSock >= 0) close(_listenSock);
}
};
TcpServer* TcpServer::_tcpServerPtr = nullptr;
HttpServer.hpp封装
HttpServer 的成员变量结构
#pragma once
#include"TcpServer.hpp"
#include"Protocol.hpp"
#include"PthreadPool.h"
#include"Task.hpp"
#include<signal.h>
class HttpServer {
private:
int _port;
bool _stop;
public:
HttpServer(int port) :_port(port), _stop(false) {
}
void InitHttpServer() {
signal(SIGPIPE, SIG_IGN);
LOG(INFO, "InitHttpServer success");
}
void Loop() {
sockaddr_in endPoint;
InitHttpServer();
LOG(INFO, "Loop begin...");
while (!_stop) {
socklen_t len= sizeof(endPoint);
int sock = accept( TcpServer::Getinstance(_port)-> GetListenSock(), (struct sockaddr*)&endPoint, &len);
if (sock < 0) {
LOG(ERROR, "accept failure");
continue;
}
LOG(INFO, "accept new link success...");
Task t(sock);
PthreadPool::GetInstance() -> Push(t);
}
}
~HttpServer() {
}
};
Log.hpp
用于将错误信息输出到日志中方便定位。重点以简单为主。
直接函数调用传参麻烦,配合宏来使用函数。注意这里可以cerr进行打印,因为后面cgi机制进行了重定向cout会输入到管道或者可以将输出调试信息放在重定向之前。
#pragma once
#include<iostream>
#include<string>
#include<ctime>
#define INFO 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
#define LOG(LEVEL, INFOMATION) (log(#LEVEL, INFOMATION, __FILE__, __LINE__))
void log(std::string level, std::string Information, std::string filename, int lines) {
std::cerr<<"["<<level<<"]["<<Information<<"]["<<time(nullptr)<<"]["<<filename<<"]["<<lines<<"]"<<std::endl;
}
处理HTTP协议——Protocol.hpp
Util.hpp工具类Util
Util.hpp:处理整个编码中需要大小写转换,字符判断、字符串转换等方法
实现读取请求行
面临粘包问题及解决方法
根据网络编程学习的知识,tcp是面向字节流的,并不能区分分离出整个报文,应用层在读取接收缓冲区的时候一次读出的字节数也是不固定的,需要使用应用层的协议对每个报文进行分离,解决粘包问题。
读取的基本单位,按照行来读取,读到空行先读取完请求报头,在请求包头中获得请求正文的长度。
Tips:不能用C、C++接口来读取,因为不同接口判断行的方式不同,可能是\r\n或者\n或者\r。也就是说作为设计者要兼容各种行分隔符,设计读取行的接口。
如何处理读行的工作?
统一转化成’\n’结尾。
并且使用recv的缓冲区数据窥探功能。
#pragma once
class Util{
public:
static int ReadLine(int sock, std::string& out) {
char op = 'x';
while (op != '\n') {
size_t s = recv(sock, (void*)&op, 1, 0);
if (s > 0) {
if (op == '\r') {
recv(sock, (void*)&op, 1 ,MSG_PEEK);
if (op == '\n') {
recv(sock, (void*)&op, 1, 0);
}
else {
op = '\n';
}
}
out.push_back(op);
}
else if (s == 0) {
close(sock);
return 0;
}
else {
return -1;
}
}
return static_cast<int>(out.size());
}
};
将请求报文切分成kv模型
class Util{
public:
static inline bool CutStrToPart(const std::string& target, std::string& out1, std::string& out2, const std::string& seg) {
auto pos = target.find(seg);
out1 = target.substr(0, pos);
out2 = target.substr(pos + seg.size());
return true;
}
};
将Methond进行大写转化
为了防止发来的请求不遵守协议,统一对Get和Post进行大写转化。使用Cpp的tranform 接口。
class Util{
public:
static inline void Tranform(std::string& str) {
std::transform(str.begin(), str.end(), str.begin(), toupper);
}
};
状态码和状态描述——Code2Desc方法
static std::string Code2Desc(int code) {
static inline std::string Code2Desc(const int& code) {
std::string Desc;
switch (code) {
case 404:
Desc = "NOT_FOUND";
break;
case 400:
Desc = "BAD_REQUEST";
break;
case 500:
Desc = "Internal_Server_Error";
break;
default:
Desc = "OK";
break;
}
return Desc;
}
}
Http请求——HttpRequest类
class HttpResponse {
public:
std::string _status_line;
std::vector<std::string> _response_header;
std::string _blank;
std::string _response_body;
int _status_code;
int _size;
int _fd;
public:
HttpResponse() :_blank(LINEEND), _status_code(OK) {};
~HttpResponse() {}
};
Http响应——HttpResponse类
class HttpResponse {
public:
std::string _status_line;
std::vector<std::string> _response_header;
std::string _blank;
std::string _response_body;
int _status_code;
int _fd;
int _size
public:
HttpResponse(): _blank(LINE_END), _status_code(OK), _fd(-1), _size(0){}
~HttpResponse(){}
};
读取及分离请求,构建响应,IO通信的业务逻辑——EndPoint类
读取及分离请求
- 使用Util工具类读取请求行
- 循环读取请求报头行直到空行
按行读取请求之后对于每一行要进行分离出来,但是分析出来的数据并不是立刻使用,因此需要将分析出来的数据保存在HttpRequest 中。
- 分析请求行,对于将一个字符串划分成三个串可以采用find+substr的方式,但是在这里小型的不划算,我们这里采用CppIO流中的stringstream字符串序列化来处理。
- 分析请求报头,可以发现请求报文中都是
k-v 模型,所以可以每一行都打散存入map中,常数考虑这里使用哈希表header_kv。切分的过程直接在工具类中编写CutString的方法(find和substr)切割成key和value。 - 判断是否读取请求正文(认为GET没有正文部分,POST有正文部分,有则可以通过k-v哈希表查找Content-Length来读取)
bool RecvHttpRequestLine() {
std::string& out = _http_request._request_lines;
if (Util::ReadLine(_sock, out) <= 0) {
return false;
}
return true;
}
void ParseHttpRequestLine() {
std::stringstream ss(_http_request._request_lines);
ss >> _http_request._method >> _http_request._url >> _http_request._version;
Util::Tranform(_http_request._method);
LOG(INFO, _http_request._method);
LOG(INFO, _http_request._url);
LOG(INFO, _http_request._version);
}
int RecvHttpRequestHeader() {
std::string out;
while (true) {
out.clear();
size_t s = Util::ReadLine(_sock, out);
if (s == 1) {
break;
}
if (s <= 0) {
return false;
}
out.resize(static_cast<int>(out.size()) - 1);
_http_request._request_header.push_back(out);
LOG(INFO ,out);
}
return true;
}
void ParseHttpRequestHeader() {
for(auto& str: _http_request._request_header) {
std::string key, value;
Util::CutStrToPart(str, key, value, SEG);
_http_request._kvMap.insert({key,value});
LOG(INFO, key);
LOG(INFO, value);
}
}
bool IsHasHttpRequestBody() {
auto& str = _http_request._method;
if (str == "POST") {
return true;
}
return false;
}
void RecvHttpRequestBody() {
if (IsHasHttpRequestBody()) {
auto iter = _http_request._kvMap.find("Content-Length");
if (iter == _http_request._kvMap.end()) {
LOG(ERROR, "POST NOT FOUND Content-Length");
}
else {
_http_request._Content_Length = atoi((iter -> second.c_str()));
LOG(INFO, "Content-Length get success");
int contentLength = _http_request._Content_Length;
char ch = 0;
while (contentLength) {
size_t s = recv(_sock, &ch, 1, 0);
if (s == 0) {
LOG(ERROR, "contentLength");
break;
}
_http_request._request_body.push_back(ch);
contentLength--;
}
}
}
}
分析及构建响应
在构建响应之前要先分析请求报文的正确性。
- 判断请求方法正确性
- 判断GET方法是否通过uri传参,若传参则直接下载,否则是上传。
- 分割uri成path和query_string
- 这里的路径表明了服务器上的某种资源,是从根目录开始的吗?不一定,需要指明web根目录。通过string的加减就可以变换。特别的,如果访问\,直接返回web目录的index.html。
- 这个路径对应的资源如何判断是否存在的?用系统调用
stat 获得文件信息,如果获取成功文件存在反之不存在,存在则可以获得文件的size和权限等信息。如果不存在就返回404页面,返回码标记成404。 - 存在就一定可以读取吗?
- 有可能是目录。如果访问的是目录路径,则默认返回该目录下的index.html文件,因此每个目录都要index.html。
S_ISREG(stat.st_mode) 判断普通文件,S_ISDIR(stat.st_mdoe) 判断是目录。 - 有可能请求的是一个可执行程序,需要特殊处理。检查是否有
S_IXUSR 或S_IXGRP 或S_IXOTH 权限,有就说明是可执行文件。
分析响应部分 :
void BuildHttpResponse() {
std::string& path = _http_request._path;
std::string& argv = _http_request._suffix;
int& code = _http_response._status_code;
if (_http_request._method != "POST" && _http_request._method != "GET") {
code = BAD_REQUEST;
goto END;
}
if (_http_request._method == "GET" ) {
if (_http_request._url.find(IFQUERY) != std::string::npos) {
Util::CutStrToPart(_http_request._url, path, argv, IFQUERY);
path = WEBROOT + path;
LOG(INFO, path);
LOG(INFO, argv);
_http_request._cgi = true;
}
else {
if ( _http_request._url == "/") {
path = WEBROOT + std::string("/") + std::string(PAGE);
}
else path = WEBROOT + _http_request._url;
LOG(INFO, path);
struct stat st;
int cnt = stat(path.c_str(), &st);
LOG(INFO, path);
if (cnt < 0) {
code = NOT_FOUND;
LOG(ERROR, "FILE NOT FOUND");
goto END;
}
if (S_ISDIR(st.st_mode)) {
path += "/";
path += PAGE;
cnt = stat(path.c_str(), &st);
LOG(INFO, path);
if (cnt < 0) {
code = NOT_FOUND;
LOG(ERROR, "Dir not found index.html");
goto END;
}
LOG(INFO, std::string("Dir -> html :") + path);
}
if ((S_IXUSR&st.st_mode) || (S_IXGRP&st.st_mode) || (S_IXOTH&st.st_mode)) {
_http_request._cgi = true;
}
_http_response._size = st.st_size;
}
}
else if (_http_request._method == "POST") {
_http_request._cgi = true;
path = WEBROOT + _http_request._url;
}
if ( _http_request._cgi == false) {
LOG(INFO, "Call ProcessNonCgi");
code = ProcessNonCgi();
}
else {
LOG(INFO, "Call ProcessCgi");
code = ProcessCgi();
}
END:
Code2Response();
return;
}
构建响应部分 :分别对非cgi和cgi部分进行构建不同的响应。这里先对非cgi部分(直接返回静态网页部分)进行处理。
- 到了这一段逻辑的时候保证目标网页是存在的
- 返回并不是单单返回网页,而是要构建HTTP响应
- 传统做法是直接打开文件,然后利用文件描述符进行读写(可以联系文件系统)。但是这样的模式拷贝多次,这里学习
sendfile (只在内核层面进行拷贝,少了用户拷贝的两次)。
?
private:
int ProcessNonCgi(size) {
_http_response._fd = open(_http_request.path.c_str(), O_RDONLY);
if(_http_response._fd >=0) {
_http_response._status_line = HTTP_VERSION;
_http_response._status_line += " ";
_http_response._status_line += std::to_string(_http_response.status_code);
_http_response._status_line += " ";
_http_response._status_line += Code2Desc(__http_response.status_code);
_http_response._status_line += LINE_END;
_http_response._size = size;
return OK;
}
return 404;
}
这里是后期整合后的代码 :
int ProcessNonCgi() {
_http_response._fd = open(_http_request._path.c_str(), O_RDONLY);
if (_http_response._fd < 0) {
LOG(ERROR, "ProcessNonCgi open error");
return Internal_Server_Error;
}
return OK;
}
报头至少要包含的两个属性:
- Content-Length告知正文长度
- Content-Type告知资源类型(请求资源是什么类型,取决于资源的后缀)
void HandleResponseStatusLine() {
_http_response._status_line += "HTTP/1.0";
_http_response._status_line += " ";
_http_response._status_line += std::to_string(_http_response._status_code);
_http_response._status_line += " ";
_http_response._status_line += Util::Code2Desc(_http_response._status_code);
_http_response._status_line += LINEEND;
}
void HandleOK() {
std::string ContentLength = "Content-Length: ";
if (_http_request._cgi == false) {
ContentLength += std::to_string(_http_response._size);
}
else {
ContentLength += std::to_string(_http_response._response_body.size());
}
ContentLength += LINEEND;
_http_response._response_header.push_back(ContentLength);
std::string ContentType = "Content-Type: ";
if (_http_request._cgi == false) {
size_t pos = _http_request._path.rfind(".");
if (pos == std::string::npos) {
LOG(ERROR, "dot is not found");
}
std::string suffix = _http_request._path.substr(pos);
ContentType += Suffix2Desc(suffix);
ContentType += LINEEND;
}
else {
std::string suffix = ".html";
ContentType += Suffix2Desc(suffix);
ContentType += LINEEND;
}
_http_response._response_header.push_back(ContentType);
}
void HandleError(std::string page) {
_http_request._cgi = false;
std::string ContentLength = "Content-Length: ";
struct stat st;
int result = stat(page.c_str(), &st);
if (result < 0) {
LOG(ERROR, "Error page not exists");
}
ContentLength += std::to_string(st.st_size);
ContentLength += LINEEND;
_http_response._response_header.push_back(ContentLength);
std::string ContentType = "Content-Type: text/html";
ContentType += LINEEND;
_http_response._response_header.push_back(ContentType);
LOG(INFO, std::to_string(_http_request._cgi));
_http_response._fd = open(page.c_str(), O_RDONLY);
_http_response._size = st.st_size;
}
IO通信
根据网络基础所学,这里的send实际上是将数据从用户层拷贝到内核级的发送缓冲区,具体什么时候发及发多少由tcp决定。
对于返回静态页面的请求,我们可以先保存文件描述符fd,然后直接使用sendfile进行传送,这样就不需要进行用户层到内核层的拷入和拷出。
对于cgi程序的结果返回,我们将结果通过管道传输到response的响应正文中最后传送过来。
- sendfile接口说明:[int out_fd(往这里写),int in_fd(从这里读),off_t *offset(0),size_t count(想传输的字节)]。而由于此时没有保存在用户缓冲区中,因此我们要保存起来最后发送的时候先发送状态行,再发送响应报头空行,最后发送响应正文。
void SendHttpResponse() {
LOG(INFO, "SendHttpResponse");
send(_sock, _http_response._status_line.c_str(), _http_response._status_line.size(), 0);
for (auto& iter: _http_response._response_header) {
send(_sock, iter.c_str(), iter.size(), 0);
LOG(INFO, iter.c_str());
}
send(_sock, _http_response._blank.c_str(), _http_response._blank.size(), 0);
ssize_t s = 0;
if (_http_request._cgi == false) {
s = sendfile(_sock, _http_response._fd, nullptr, _http_response._size);
LOG(INFO, "sendfile: " + std::to_string(_http_response._size));
}
else if (_http_request._cgi == true) {
auto& response_body = _http_response._response_body;
LOG(INFO, response_body);
size_t total = 0;
char* start = (char*)response_body.c_str();
while (total < response_body.size() && (s = send(_sock, start + total, response_body.size() - total, 0)) > 0) {
total += s;
LOG(INFO, std::to_string(total));
}
LOG(INFO, "send-s : " + std::to_string(total));
}
std::cout<<"errno = " <<errno <<std::endl;
close(_http_response._fd);
}
EndPoint类整体代码实现
class EndPoint {
private:
int _sock;
HttpRequest _http_request;
HttpResponse _http_response;
private:
bool RecvHttpRequestLine() {
std::string& out = _http_request._request_lines;
if (Util::ReadLine(_sock, out) <= 0) {
return false;
}
return true;
}
void ParseHttpRequestLine() {
std::stringstream ss(_http_request._request_lines);
ss >> _http_request._method >> _http_request._url >> _http_request._version;
Util::Tranform(_http_request._method);
LOG(INFO, _http_request._method);
LOG(INFO, _http_request._url);
LOG(INFO, _http_request._version);
}
int RecvHttpRequestHeader() {
std::string out;
while (true) {
out.clear();
size_t s = Util::ReadLine(_sock, out);
if (s == 1) {
break;
}
if (s <= 0) {
return false;
}
out.resize(static_cast<int>(out.size()) - 1);
_http_request._request_header.push_back(out);
LOG(INFO ,out);
}
return true;
}
void ParseHttpRequestHeader() {
for(auto& str: _http_request._request_header) {
std::string key, value;
Util::CutStrToPart(str, key, value, SEG);
_http_request._kvMap.insert({key,value});
LOG(INFO, key);
LOG(INFO, value);
}
}
bool IsHasHttpRequestBody() {
auto& str = _http_request._method;
if (str == "POST") {
return true;
}
return false;
}
void RecvHttpRequestBody() {
if (IsHasHttpRequestBody()) {
auto iter = _http_request._kvMap.find("Content-Length");
if (iter == _http_request._kvMap.end()) {
LOG(ERROR, "POST NOT FOUND Content-Length");
}
else {
_http_request._Content_Length = atoi((iter -> second.c_str()));
LOG(INFO, "Content-Length get success");
int contentLength = _http_request._Content_Length;
char ch = 0;
while (contentLength) {
size_t s = recv(_sock, &ch, 1, 0);
if (s == 0) {
LOG(ERROR, "contentLength");
break;
}
_http_request._request_body.push_back(ch);
contentLength--;
}
}
}
}
int ProcessNonCgi() {
_http_response._fd = open(_http_request._path.c_str(), O_RDONLY);
if (_http_response._fd < 0) {
LOG(ERROR, "ProcessNonCgi open error");
return Internal_Server_Error;
}
return OK;
}
int ProcessCgi() {
int code = OK;
std::string& bin = _http_request._path;
LOG(INFO , bin);
std::string PATH = "METHOD=";
PATH += _http_request._method;
putenv((char*)PATH.c_str());
std::string DATA = "INPUT=";
if (_http_request._method == "GET") {
DATA += _http_request._suffix;
LOG(INFO, DATA);
putenv((char*)DATA.c_str());
}
std::string LENGTH = "CONTENT_LENGTH=";
if (_http_request._method == "POST") {
LENGTH += std::to_string(_http_request._request_body.size());
putenv((char*)LENGTH.c_str());
}
else {
LENGTH += std::to_string(_http_request._suffix.size());
}
LOG(INFO, LENGTH);
int input[2], output[2];
if (pipe(input) < 0) {
LOG(FATAL, "INPUT PIPE CREATE FAILED");
code = Internal_Server_Error;
}
if (pipe(output) < 0) {
LOG(FATAL, "OUTPUT PIEPE CREATE FAILED");
code = Internal_Server_Error;
}
pid_t pid = fork();
if (pid == 0) {
close(input[0]);
close(output[1]);
LOG(INFO , bin);
dup2(input[1], 1);
dup2(output[0], 0);
execl(bin.c_str(), bin.c_str(), nullptr);
LOG(ERROR, "execl error");
exit(1);
}
else if (pid > 0) {
close(input[1]);
close(output[0]);
if (_http_request._method == "POST") {
int cnt = 0;
char* str = (char*)_http_request._request_body.c_str();
size_t size = _http_request._request_body.size();
size_t s;
while ((s = write(output[1], str + cnt, size - cnt)) > 0 ) {
cnt += s;
}
}
int status = 0;
pid_t res = waitpid(pid, &status, 0);
if (res == pid) {
if (WIFEXITED(status)) {
if (WEXITSTATUS(status)) {
LOG(ERROR, "cgi exitcode is > 0");
code = BAD_REQUEST;
}
else {
code = OK;
}
}
else if (WIFSIGNALED(status)) {
LOG(ERROR, "cig is killed");
code = Internal_Server_Error;
}
}
size_t size;
char ch = '0';
while ((size = read(input[0], &ch, 1)) > 0) {
_http_response._response_body.push_back(ch);
}
LOG(INFO, _http_response._response_body);
LOG(INFO, std::to_string(_http_response._response_body.size()));
close(input[0]);
close(output[1]);
}
else {
LOG(FATAL, "fork error..");
return Internal_Server_Error;
}
LOG(INFO, "ProcessCgi() done...");
return code;
}
void HandleResponseStatusLine() {
_http_response._status_line += "HTTP/1.0";
_http_response._status_line += " ";
_http_response._status_line += std::to_string(_http_response._status_code);
_http_response._status_line += " ";
_http_response._status_line += Util::Code2Desc(_http_response._status_code);
_http_response._status_line += LINEEND;
}
void HandleOK() {
std::string ContentLength = "Content-Length: ";
if (_http_request._cgi == false) {
ContentLength += std::to_string(_http_response._size);
}
else {
ContentLength += std::to_string(_http_response._response_body.size());
}
ContentLength += LINEEND;
_http_response._response_header.push_back(ContentLength);
std::string ContentType = "Content-Type: ";
if (_http_request._cgi == false) {
size_t pos = _http_request._path.rfind(".");
if (pos == std::string::npos) {
LOG(ERROR, "dot is not found");
}
std::string suffix = _http_request._path.substr(pos);
ContentType += Suffix2Desc(suffix);
ContentType += LINEEND;
}
else {
std::string suffix = ".html";
ContentType += Suffix2Desc(suffix);
ContentType += LINEEND;
}
_http_response._response_header.push_back(ContentType);
}
void HandleError(std::string page) {
_http_request._cgi = false;
std::string ContentLength = "Content-Length: ";
struct stat st;
int result = stat(page.c_str(), &st);
if (result < 0) {
LOG(ERROR, "Error page not exists");
}
ContentLength += std::to_string(st.st_size);
ContentLength += LINEEND;
_http_response._response_header.push_back(ContentLength);
std::string ContentType = "Content-Type: text/html";
ContentType += LINEEND;
_http_response._response_header.push_back(ContentType);
LOG(INFO, std::to_string(_http_request._cgi));
_http_response._fd = open(page.c_str(), O_RDONLY);
_http_response._size = st.st_size;
}
void Code2Response() {
int code = _http_response._status_code;
LOG(INFO ,std::to_string(code));
HandleResponseStatusLine();
switch (code) {
case OK :
LOG(INFO, "HandleOK");
HandleOK();
break;
case NOT_FOUND:
LOG(INFO, "HandleError NOT_FOUND");
HandleError(NOT_FOUNT_PAGE);
break;
case Internal_Server_Error:
LOG(INFO, "HandleError ServerERROR");
HandleError(Internal_Server_Error_PAGE);
break;
default:
break;
}
}
public:
EndPoint(int sock) :_sock(sock) {
}
int RecvHttpRequest() {
if (RecvHttpRequestLine() && RecvHttpRequestHeader()) {
ParseHttpRequestLine();
ParseHttpRequestHeader();
RecvHttpRequestBody();
return true;
}
else {
return false;
}
}
void BuildHttpResponse() {
std::string& path = _http_request._path;
std::string& argv = _http_request._suffix;
int& code = _http_response._status_code;
if (_http_request._method != "POST" && _http_request._method != "GET") {
code = BAD_REQUEST;
goto END;
}
if (_http_request._method == "GET" ) {
if (_http_request._url.find(IFQUERY) != std::string::npos) {
Util::CutStrToPart(_http_request._url, path, argv, IFQUERY);
path = WEBROOT + path;
LOG(INFO, path);
LOG(INFO, argv);
_http_request._cgi = true;
}
else {
if ( _http_request._url == "/") {
path = WEBROOT + std::string("/") + std::string(PAGE);
}
else path = WEBROOT + _http_request._url;
LOG(INFO, path);
struct stat st;
int cnt = stat(path.c_str(), &st);
LOG(INFO, path);
if (cnt < 0) {
code = NOT_FOUND;
LOG(ERROR, "FILE NOT FOUND");
goto END;
}
if (S_ISDIR(st.st_mode)) {
path += "/";
path += PAGE;
cnt = stat(path.c_str(), &st);
LOG(INFO, path);
if (cnt < 0) {
code = NOT_FOUND;
LOG(ERROR, "Dir not found index.html");
goto END;
}
LOG(INFO, std::string("Dir -> html :") + path);
}
if ((S_IXUSR&st.st_mode) || (S_IXGRP&st.st_mode) || (S_IXOTH&st.st_mode)) {
_http_request._cgi = true;
}
_http_response._size = st.st_size;
}
}
else if (_http_request._method == "POST") {
_http_request._cgi = true;
path = WEBROOT + _http_request._url;
}
if ( _http_request._cgi == false) {
LOG(INFO, "Call ProcessNonCgi");
code = ProcessNonCgi();
}
else {
LOG(INFO, "Call ProcessCgi");
code = ProcessCgi();
}
END:
Code2Response();
return;
}
void SendHttpResponse() {
LOG(INFO, "SendHttpResponse");
send(_sock, _http_response._status_line.c_str(), _http_response._status_line.size(), 0);
for (auto& iter: _http_response._response_header) {
send(_sock, iter.c_str(), iter.size(), 0);
LOG(INFO, iter.c_str());
}
send(_sock, _http_response._blank.c_str(), _http_response._blank.size(), 0);
ssize_t s = 0;
if (_http_request._cgi == false) {
s = sendfile(_sock, _http_response._fd, nullptr, _http_response._size);
LOG(INFO, "sendfile: " + std::to_string(_http_response._size));
}
else if (_http_request._cgi == true) {
auto& response_body = _http_response._response_body;
LOG(INFO, response_body);
size_t total = 0;
char* start = (char*)response_body.c_str();
while (total < response_body.size() && (s = send(_sock, start + total, response_body.size() - total, 0)) > 0) {
total += s;
LOG(INFO, std::to_string(total));
}
LOG(INFO, "send-s : " + std::to_string(total));
}
std::cout<<"errno = " <<errno <<std::endl;
close(_http_response._fd);
}
~EndPoint() {
close(_sock);
}
};
HandlerRequest接口——CallBack类
主要作用是通过仿函数来调用回调函数HandlerRequest,从而进行了处理报文部分和线程池部分的解耦。
class CallBack{
public:
CallBack() = default;
void operator()(int _sock) {
HandlerRequest(_sock);
}
static void* HandlerRequest(int sock) {
#ifdef DEBUG
char buffer[4096];
size_t s = 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);
if (ep -> RecvHttpRequest()) {
ep -> BuildHttpResponse();
ep -> SendHttpResponse();
}
else {
LOG(INFO, "RecvHttpRequest ERROR");
}
delete ep;
#endif
LOG(INFO, "Hander Request End");
return nullptr;
}
};
HTTP CGI机制
CGI机制的原理
前面提到客户端花费时间上传数据是为了服务端处理数据,如何处理呢?HTTP需要通过某种方式处理数据,这种方式叫CGI模式。
CGI(Common Gateway Interface)是WWW技术中最重要的技术之一,有着不可替代的重要地位(网络服务器和后端业务程序耦合重要机制,理解PhP/Python/Java如何把网络里的数据拿到自身的程序中)。CGI是外部应用程序(CGI程序)与WEB服务器之间的接口标准,是在CGI程序和Web服务器之间传递信息的过程。
怎么调用目标程序,怎么传递目标数据,怎么拿到结果这就是CGI机制的通信细节。这里就需要我们来实现cgi机制。
需求:httpserver是一个可执行程序,加载到内存中,变成了进程;cgi程序也是一个可执行程序,加载到内存中,也是一个进程。如何用一个进程去执行另一个进程呢?程序替换:exec*
但是目前为止不能直接进行exec,因为当前是一个进程两个线程。因此需要创建子进程,让子进程替换。同时要将client传上的数据交给cgi进程。这就涉及到进程间通信了。
使用CGI进行数据处理的时机
什么时候需要使用CGI来进行数据处理呢?只要用户有数据上传上来,这个数据不是httpServer应该处理的,而应该是CGI处理的。就服务器来说,当面临:
- POST方法时需要标记使用cgi
- GET方法传参时需要标记cgi
- 请求的资源是可执行文件的时候也需要标记cgi
当不存在cgi标记的时候说明就是传静态资源。当存在cgi标记的时候说明就是需要进行数据处理。
调用目标程序
- PATH中保存着可执行程序,使用exec调用的目标程序。
使用第一个相对来说更方便。
传递目标数据
父进程拿到的数据在哪里?
POST:body;GET:query_string
父进程如何将数据传递给子进程?
一种方式是管道,另一种方式是环境变量。环境变量是具有全局属性的(可以被子进程继承下去),不受exec*的影响。(bash创建子进程后exec后执行自己程序环境变量并没有被替换掉;同时进程地址空间mm_struct中包含命令行参数和环境变量,是进程地址空间的一部分,内核创建的数据结构不变)
CGI如何得知需要从标准输入读多少字节?
数据长度也要通过环境变量传参。
通过管道传输
对于POST方法其数据通常在请求正文中,会比较长,所以通过管道传输同时要保证传输完毕(类似字节流)。
这里约定:管道的通信站在父进程角度。
因此我们约定,让目标被替换之后的进程,在**exec***函数系列执行之前进行重定向,读取管道等价于读取标准输入,写出管道等价于写到标准输出。
dup2替换的规则是拷贝,将input的文件指针拷贝到1中;将output的文件指针拷贝到0中。
int ProcessCgi() {
int code = OK;
std::string& bin = _http_request._path;
LOG(INFO , bin);
LOG(INFO, LENGTH);
int input[2], output[2];
if (pipe(input) < 0) {
LOG(FATAL, "INPUT PIPE CREATE FAILED");
code = Internal_Server_Error;
}
if (pipe(output) < 0) {
LOG(FATAL, "OUTPUT PIEPE CREATE FAILED");
code = Internal_Server_Error;
}
pid_t pid = fork();
if (pid == 0) {
close(input[0]);
close(output[1]);
LOG(INFO , bin);
dup2(input[1], 1);
dup2(output[0], 0);
execl(bin.c_str(), bin.c_str(), nullptr);
LOG(ERROR, "execl error");
exit(1);
}
else {
LOG(FATAL, "fork error..");
return Internal_Server_Error;
}
LOG(INFO, "ProcessCgi() done...");
return code;
}
通过环境变量传输
因为GET方法的数据在url中,因此通常比较小,可以使用更加快速的环境变量来传递。
同时将告知程序为GET/POST的信息也通过环境变量来传参。
int ProcessCgi() {
std::string PATH = "METHOD=";
PATH += _http_request._method;
putenv((char*)PATH.c_str());
std::string DATA = "INPUT=";
if (_http_request._method == "GET") {
DATA += _http_request._suffix;
LOG(INFO, DATA);
putenv((char*)DATA.c_str());
}
std::string LENGTH = "CONTENT_LENGTH=";
if (_http_request._method == "POST") {
LENGTH += std::to_string(_http_request._request_body.size());
putenv((char*)LENGTH.c_str());
}
else {
LENGTH += std::to_string(_http_request._suffix.size());
}
}
传回结果数据
父进程通过管道读取子进程传回的数据。
else if (pid > 0) {
close(input[1]);
close(output[0]);
if (_http_request._method == "POST") {
int cnt = 0;
char* str = (char*)_http_request._request_body.c_str();
size_t size = _http_request._request_body.size();
size_t s;
while ((s = write(output[1], str + cnt, size - cnt)) > 0 ) {
cnt += s;
}
}
int status = 0;
pid_t res = waitpid(pid, &status, 0);
if (res == pid) {
if (WIFEXITED(status)) {
if (WEXITSTATUS(status)) {
LOG(ERROR, "cgi exitcode is > 0");
code = BAD_REQUEST;
}
else {
code = OK;
}
}
else if (WIFSIGNALED(status)) {
LOG(ERROR, "cig is killed");
code = Internal_Server_Error;
}
}
size_t size;
char ch = '0';
while ((size = read(input[0], &ch, 1)) > 0) {
_http_response._response_body.push_back(ch);
}
LOG(INFO, _http_response._response_body);
LOG(INFO, std::to_string(_http_response._response_body.size()));
close(input[0]);
close(output[1]);
}
总结CGI
浏览器和server进行数据交互的本质,就是进程间通信,这也是socket通信的本质。
浏览器和server无非两种形式的通信:
- 浏览器拿下来数据(通常拿下来的是静态网页)
- 浏览器上传数据(POST方法或者GET方法带参)
如何看待CGI程序?
子CGI程序的标准输入是浏览器,子CGI程序的标准输出是浏览器。
因此web开发的程序是CGI程序,直接scanf和printf。不用再关心通信细节(由http完成)。
目前主流的web开发语言底层一定有cgi机制,java、php、python直接拿到浏览器输入的请求和输出到浏览器上。
构建差错处理
处理过程中发现构建Response报头和返回静态页面的部分一致,因此可以整理进行复用。
处理http的时候,有两种类型的错误
- 读取错误(读取的时候)-读取不一定完毕-不给对方回应-退出即可
- 逻辑错误(分析的时候)-读取完毕-要给对方回应对应的错误页面
- 写入出错(响应的时候)-写端不一定完毕,读端关闭(系统会给进程发送SIGPIPE信号)-自定义捕捉进行忽略处理,服务器端继续发送
对每个差错进行返回对应的错误码从而返回不同的页面。
需要注意的细节
- sockaddr和sockaddr_in
- 线程池中accpet sock部分采用new
- getenv函数的坑
- 对waitpid返回值进行判断
引入线程池
解决问题:
- 大量链接过来导致服务器内部进程或者线程暴增,进而导致服务器效率严重降低或者挂掉
- 节省链接请求到来时,创建线程的时间成本
- 让服务器的效率在一个恒定的稳定区间内(线程个数不增多,CPU调度成本不变)
如何处理海量请求?
(缓解方案)线程池、EPOLL,HTTP改成1.1支持长连接
解决方案是在软件和硬件之间平衡,硬件不够加硬件,软件不行改软件。
回调方法
将原来的EndPoint 类进行设计仿函数,仿函数内部进行回调。而任务中包含了该类对象。此时可以发现,TcpServer和HttpServer解耦(连接是TcpServer的),HttpServer和Protocal解耦(只需要将sock构造成任务并放入线程池),线程池和Protocal只有入口函数的关系,也完成了解耦(通过仿函数调用回调函数进入Protocal入口)
Task.hpp
#pragma once
#include<iostream>
#include"Protocol.hpp"
#include"Log.hpp"
class Task {
private:
int _sock;
CallBack _callBack;
public:
Task() = default;
Task(int sock) :_sock(sock) {
}
void TaskHander() {
LOG(INFO, "CallBack begin");
_callBack(_sock);
LOG(INFO, "CallBack end");
}
~Task() {}
};
#include<iostream>
#include<pthread.h>
#include<queue>
#include<string>
#include<unistd.h>
#include<sys/wait.h>
#include"Task.hpp"
#include"Log.hpp"
#define NUM 5
class PthreadPool {
private:
std::queue<Task> _blockQueue;
pthread_cond_t _cond;
pthread_mutex_t _lock;
static PthreadPool* _pthreadPool;
private:
PthreadPool() = default;
PthreadPool(const PthreadPool& t) = delete;
PthreadPool& operator=(const PthreadPool& t) = delete;
void Wait() {
pthread_cond_wait(&_cond, &_lock);
}
void WakeUp() {
pthread_cond_signal(&_cond);
}
void Lock() {
pthread_mutex_lock(&_lock);
}
void UnLock() {
pthread_mutex_unlock(&_lock);
}
bool IsEmpty() {
return _blockQueue.size() == 0;
}
public:
static PthreadPool* GetInstance(){
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
if (_pthreadPool == nullptr) {
pthread_mutex_lock(&mtx);
if (_pthreadPool == nullptr) {
_pthreadPool = new PthreadPool();
_pthreadPool -> PthreadPoolInit();
LOG(INFO, "PthreadPoolInit done");
}
pthread_mutex_unlock(&mtx);
}
return _pthreadPool;
}
static void* ThreadHandler(void* arg) {
PthreadPool* _this = (PthreadPool*)(arg);
while(true) {
_this->Lock();
while (_this -> IsEmpty()) {
_this->Wait();
LOG(INFO , "pthread_t id :" + std::to_string(pthread_self()));
}
LOG(INFO, "I am awake ");
Task t;
_this->Pop(t);
LOG(INFO, "I am get task");
_this->UnLock();
LOG(INFO, "TaskHander begin..");
t.TaskHander();
LOG(INFO, "TaskHander done..");
}
return nullptr;
}
void PthreadPoolInit() {
pthread_mutex_init(&_lock, nullptr);
pthread_cond_init(&_cond, nullptr);
for (int i = 0; i < NUM; i++) {
pthread_t pid;
pthread_create(&pid, nullptr, ThreadHandler, (void*)this);
pthread_detach(pid);
}
}
void Push(Task& task) {
Lock();
_blockQueue.push(task);
LOG(INFO, "Push one task...");
UnLock();
WakeUp();
}
void Pop(Task& task) {
task = _blockQueue.front();
_blockQueue.pop();
}
~PthreadPool() {
pthread_mutex_destroy(&_lock);
pthread_cond_destroy(&_cond);
delete _pthreadPool;
_pthreadPool = nullptr;
}
};
PthreadPool* PthreadPool:: _pthreadPool = nullptr;
Makefile编写
bin=httpserver
src=main.cc
cc=g++
cgi=cgi_bin,shell_cgi.sh,python_cgi.py
FLAGS=-std=c++11 -lpthread #-DDEBUG1
pwd=$(shell pwd)
.PHONY:all
all:CGI $(bin)
$(bin):$(src)
$(cc) -o $@ $^ $(FLAGS)
CGI:
cd $(pwd)/Cgi;\
make;\
cd -;
.PHONY:Output
output:
mkdir -p Output
mkdir -p Output/Web
cp -r $(pwd)/Cgi/{$(cgi)} $(pwd)/Web
cp -r $(pwd)/Web $(pwd)/Output
cp -r $(bin) $(pwd)/Output
.PHONY:clean
clean:
rm -rf *.o $(bin) Output;\
cd $(pwd)/Cgi;\
make clean;\
cd -;
表单测试(前端页面发送整合http
请求)
一般像表单采用post方法提交,若在method中没有指明则是采用get方法提交。
因此比如百度页面的搜索部分其实就是表单<form> ,默认以get方法传参,而/s就是要交付的cgi程序,而后面的就是参数,&是参数连接符号,=是参数的value。此时就可以访问将数据传递给对应的进程从而返回html构建的数据结果显示到浏览器上。
https://www.baidu.com/s?wd=nba&rsv_spt=1&rsv_iqid=0x87c278bf0000c600&issp=1&f=8&rsv_bp=1&rsv_idx=2&ie=utf-8&tn=baiduhome_pg&rsv_dl=tb&rsv_enter=1&rsv_sug3=3&rsv_sug1=1&rsv_sug7=100&rsv_sug2=0&rsv_btype=i&prefixsug=nba&rsp=5&inputT=1588&rsv_sug4=1588
- GET通过uri传参,form提交的时候,会自动拼接我们的url
- POST通过正文传参(通过管道传参)
- GET通过url传参,大小一般有限制(通过环境变量传递给子进程),提交的数据基本比较公开
补充内容
做完这个项目,最主要的收获是两点,分别是原理层面和应用层面。额外的还有一些过程中的收获。
Postman测试
测试工具:telnet,Postman。
相较于telnet,前者更容易看到每一步的过程,后者更为直观更方便调试。另外Wfetch软件可以看到\r和\n
同时在这里我们发现之前直接用C/C++写的cgi程序,返回时构建html页面返回是很麻烦的,因为其对字符串的处理不方便(python/php更方便web后端开发)。因此实际上cgi程序上面对获得的数据进行处理的部分更适合用C/C++去处理,而下面的以html方式返回数据结果更适合用web开发的语言比如python和php。但倘若并不需要返回网页(web服务器),如果是游戏服务器,展示给用户的就是游戏数据为了效率采用C/C++。
使用其他语言编写cgi
#!/bin/bash
echo "hello world"
ls -a -l ./..
echo "hello world"
print "hello python"
访问数据库
先在官网下载linux的mysql安装包。
这部分由于数据库并不熟悉,等这学期数据库实训的时候学了数据库到时再回来处理这一部分。
导入数据库
这里涉及到之前学习的动态库的使用,-I-L-l,export LD_LIBRARY_PATH=路径。为了方便起见使用Lib中的静态库进行静态链接(交叉编译的时候要-lpthread,-ldl指明各种第三方库)。
测试代码 :
#include<iostream>
#include "./include/mysql.h"
int main() {
std::cout << "version: " << mysql_get_client_info() <<std::endl;
return 0;
}
创建数据库、用户、表
…
使用mysql接口
#include<iostream>
#include<string>
#include"mysql.h>
int main() {
MYSQL* conn = mysql_init(nullptr);
if( nullptr == mysql_real_connect(conn, "127.0.0.1", "http_test" , "12345678","http_test",3306, nullptr ,0) ){
std::cerr << "connect error!" <<std::endl;
return 1;
}
std::string sql = ;
int ret = mysql_query(conn,sql.c_str());
std::cerr << "connect success" <<std::endl;
mysql_close(conn);
}
|