目录
1、2个简单的预备知识
首先我们来看一个域名:http://www.baidu.com/,很明显这是百度的域名,但是结合我们之前的知识其实这个域名做过包装,实际上可以解析成ip地址的形式。所以这里我们用ip地址220.181.38.150:80(http绑定端口号80,https绑定端口号443)也可以访问百度。
了解了这些后我们来看看域名的整体结构:
服务器地址后面的一串文件索引就是url(统一资源定位符),所有网络上的资源,都可以用唯一的一个“字符串标识”,并且可以获取到。
我们要知道。网络的行为就是两种:把别人的东西拿下来或者把自己的东西传上去。在少数情况下,提交或者获取的数据本身可能包含和url中特殊的字符冲突的字符,所以就要求CS双方要进行编码(encode)和解码(decode),这些我们之前已经实现过一遍,但http这里编码解码的方式有一些小区别,通过接下来要将的http请求和响应的格式就会观察到。
2、HTTP请求和响应的格式
Http请求
首行 : [ 方法 ] + [url] + [ 版本 ];Header: 请求的属性 , 冒号分割的键值对 ; 每组属性之间使用\r \n 分隔 ; 遇到空行表示 Header 部分结束;Body: 空行后面的内容都是 Body. Body 允许为空字符串 . 如果 Body 存在 , 则在 Header 中会有一个 Content-Length属性来标识 Body 的长度 ;
Http响应
首行 : [ 版本号 ] + [ 状态码 ] + [ 状态码解释 ];Header: 请求的属性 , 冒号分割的键值对 ; 每组属性之间使用\r \n 分隔 ; 遇到空行表示 Header 部分结束;Body: 空行后面的内容都是 Body. Body 允许为空字符串 . 如果 Body 存在 , 则在 Header 中会有一个 Content-Length属性来标识 Body 的长度 ; 如果服务器返回了一个 html 页面 , 那么 html 页面内容就是在body中。
HTTP请求和响应的结构和过程如图所示:
3、实现一个最简单的httpserver
HttpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include <fstream>
#include <vector>
#include <sstream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unordered_map>
#include "Socket.hpp"
#include "Log.hpp"
//设置根目录和主页以及分隔符
const std::string wwwroot = "./wwwroot"; //web 根目录
const std::string sep = "\r\n";
const std::string homepage = "index.html";
static const int defaultport = 8080;
class HttpServer;
//设置多线程
class ThreadData
{
public:
ThreadData(int fd, HttpServer *s):sockfd(fd), svr(s)
{}
public:
int sockfd;
HttpServer *svr;
};
//服务器请求,实现解码
class HttpRequest
{
public:
void Deserialize(std::string req)
{
while(true)
{
std::size_t pos = req.find(sep);
if(pos == std::string::npos) break;
std::string temp = req.substr(0, pos);
if(temp.empty()) break;
req_header.push_back(temp);
req.erase(0, pos+sep.size());
}
text = req;
}
void Parse()
{
std::stringstream ss(req_header[0]);
ss >> method >> url >> http_version;
file_path = wwwroot; // ./wwwroot
if(url == "/" || url == "/index.html")
{
file_path += '/';
file_path += homepage; // ./wwwroot/index.html
}
else file_path += url; // /a/b/c/d.html->./wwwroot/a/b/c/d.html
auto pos = file_path.rfind(".");
if(pos == std::string::npos) suffix = ".html";
else suffix = file_path.substr(pos);
}
void DebugPrint()
{
std::cout << "------------------------------" << std::endl;
for(auto &line : req_header)
{
std::cout << "------------------------------" << std::endl;
std::cout << line << "\n\n";
}
std::cout << "method: " << method << std::endl;
std::cout << "url: " << url << std::endl;
std::cout << "http_version: " << http_version << std::endl;
std::cout << "file_path: " << file_path << std::endl;
std::cout << text << std::endl;
}
public:
std::vector<std::string>req_header;
std::string text;
// 解析之后的结果
std::string method;
std::string url;
std::string http_version;
std::string file_path;
std::string suffix;
};
//服务器接口
class HttpServer
{
public:
HttpServer(uint16_t port = defaultport):port_(port)
{
content_type.insert({".html", "text/html"});
content_type.insert({".jpg", "image/jpeg"});
}
//启动服务器
bool Start()
{
listensock_.Socket();
listensock_.Bind(port_);
listensock_.Listen();
for(;;)
{
std::string clientip;
uint16_t clientport;
int sockfd = listensock_.Accept(&clientip, &clientport);
if (sockfd < 0) continue;
lg(Info, "get a new connet, sockfd: %d", sockfd);
pthread_t tid;
ThreadData *td = new ThreadData(sockfd, this);
td->sockfd = sockfd;
pthread_create(&tid, nullptr, ThreadRun, td);
}
}
//读取指定路径的文件
static std::string ReadHtmlContent(const std::string &htmlpath)
{
// 坑 传图片,要读二进制数据
std::ifstream in(htmlpath, std::ios::binary);
if(!in.is_open()) return "";
in.seekg(0, std::ios_base::end);
auto len = in.tellg();
in.seekg(0, std::ios_base::beg);
std::string content;
content.resize(len);
in.read((char*)content.c_str(), content.size());
// std::string content;
// std::string line;
// while(std::getline(in, line))
// {
// content += line;
// }
in.close();
return content;
}
//确定文件后缀
std::string SuffixToDesc(const std::string &suffix)
{
auto iter = content_type.find(suffix);
if(iter == content_type.end()) return content_type[".html"];
else return content_type[suffix];
}
//处理请求,对响应进行编码并发回
void HandlerHttp(int sockfd)
{
char buffer[10240];
ssize_t n = recv(sockfd , buffer, sizeof(buffer) - 1, 0);
if(n > 0)
{
buffer[n] = 0;
std::cout << buffer;
HttpRequest req;
req.Deserialize(buffer);
req.Parse();
//req.DebugPrint();
// 返回响应的过程
std::string text;
bool ok = true;
text = ReadHtmlContent(req.file_path); // 失败?
if(text.empty())
{
ok = false;
std::string err_html = wwwroot;
err_html += "/";
err_html += "err.html";
text = ReadHtmlContent("err_html");
}
std::string response_line;
if(ok)
response_line = "HTTP/1.0 200 OK\r\n";
else
response_line = "HTTP/1.0 404 Not Found\r\n";
// response_line = "HTTP/1.0 302 Found\r\n";
std::string response_header = "Content-Length: ";
response_header += std::to_string(text.size());
response_header += "\r\n";
response_header += "Content-Type: ";
response_header += SuffixToDesc(req.suffix);
response_header += "\r\n";
response_header += "Set-Cookie: name=haha&&passwd=12345";
response_header += "\r\n";
// response_header += "Location: https://www.qq.com\r\n";
std::string blank_line = "\r\n";
std::string response = response_line;
response += response_header;
response += blank_line;
response += text;
send(sockfd, response.c_str(), response.size(), 0);
}
close(sockfd);
}
static void *ThreadRun(void *args)
{
pthread_detach(pthread_self());
ThreadData *td = static_cast<ThreadData*>(args);
td->svr->HandlerHttp(td->sockfd);
delete td;
return nullptr;
}
~HttpServer()
{}
private:
Sock listensock_;
uint16_t port_;
std::unordered_map<std::string, std::string> content_type;
};
HttpServer.cc
#include "HttpServer.hpp"
#include <iostream>
#include <memory>
using namespace std;
int main(int argc, char *argv[])
{
if(argc != 2)
{
exit(1);
}
uint16_t port = std::stoi(argv[1]);
// unique<HttpServer> svr(new HttpServer());
HttpServer *svr = new HttpServer(port);
svr->Start();
return 0;
}
用到的Socket接口和Log接口之前博客中已给出,这里不再给出。
实现的网页服务器主页界面如上(网页的美化等工作属于前端的内容,我们研究的是后端服务器的具体细节,所以不对前端做过多描述。)
4、HTTP的细节字段
4.1、GET和POST
HTTP中最常用的方法就是GET方法和POST方法,那么这两种方法有什么区别呢?接下来我们来研究一下。
首先来看get方法:
我们将主页文件里的表单提交改成get方法,输入用户名密码进行提交;
这了可以看到我们输入的用户名和密码提交给服务器时,是通过url提交的,那么我们改成post方法试一下:
这个时候我们发现,post方法的url里并没有用户名密码,那么它们在哪里呢?就在请求的正文部分,还记得我们说的请求和响应的结构吗?正文和报头之间是用一个空行分隔,而报头里都是键值对。
总结一下,如果我们要提交的参数给我们的服务器,我们使用get方法的时候,我们提交的参数是通过url提交的!而post方法也支持参数提交,采用请求的正文提交参数!
4.2、HTTP的状态码
类别 | 原因短语 | |
---|---|---|
1XX | Informational(信息性状态码) | 接收的请求正在处理 |
2XX | Success(成功状态码) | 请求正常处理完毕 |
3XX | Redirection(重定向状态码) | 需要进行附加操作以完成请求 |
4XX | Client Error(客户端错误状态码) | 服务器无法处理请求 |
5XX | Server Error(服务器错误状态码) | 服务器处理请求出错 |
代码中的这一段就是在判断服务器状态码并返回相应的状态页面。
还有一个重定向的状态码我们单独拿出来说一下,重定向就是让服务器指导浏览器,让浏览器访问新的地址;
如果我们将这段注释的代码放开,那么浏览器就会自动的跳转访问qq的主页面。
4.3、HTTP常见Header
Content-Type: 数据类型 (text/html 等 );Content-Length: Body 的长度;Host: 客户端告知服务器 , 所请求的资源是在哪个主机的哪个端口上 ;User-Agent: 声明用户的操作系统和浏览器版本信息 ;referer: 当前页面是从哪个页面跳转过来的 ;location: 搭配 3xx 状态码使用 , 告诉客户端接下来要去哪里访问 ;Cookie: 用于在客户端存储少量信息 . 通常用于实现会话 (session) 的功能 ;