HTTP协议全称为超文本传输协议(英语:HyperText Transfer Protocol,缩写:HTTP)是一种用于分布式、协作式和超媒体信息系统的应用层协议,是因特网上应用最为广泛的一种网络传输协议,所有的 WWW 文件都必须遵守这个标准。HTTP 是为 Web 浏览器与 Web 服务器之间的通信而设计的。
HTTP 是一个基于 TCP/IP 通信协议来传递数据的(HTML 文件、图片文件、查询结果等),是一种应用层协议。其中HTTP1.0、HTTP1.1、HTTP2.0均为TCP协议实现,HTTP3.0是基于UDP实现的,现在主要使用的是HTTP1.0和HTTP3.0 。
另外,现在我们访问一些网站时我们可以发现,很多都是使用的HTTPS协议进行通信的,其实 HTTPS 是在 HTTP 的基础之上,利用SSL/TLS来加密数据包,一定程度上保证了数据传输的安全性。HTTPS 开发的主要目的,是提供对网站服务器的身份认证,保护交换资料的隐私与完整性。关于HTTPS的详细内容可见博主的后续文章。
HTTP 协议工作与客户端—服务端架构上。浏览器作为 HTTP 客户端通过 URL 向 HTTP 服务端即web服务器发送请求,web服务器根据接收到的请求向客户端发送回响信息,从而实现客户端与服务端之间的通信。
HTTP的注意事项:
下图展示了HTTP协议通信的基本流程:
客户端发送一个HTTP请求到服务器的请求消息包括以下格式:请求行(request line)、请求头部(header)、空行和请求数据四个部分组成,下图给出了请求报文的一般格式。
请求实例:
HTTP响应也由四个部分组成,分别是:状态行、响应头部、空行和响应正文。
响应实例:
URL 的定义:
URL 其实就是我们所说的”网址“,URL(Uniform Resource Locator),翻译为统一资源定位符。互连网上的每个文件都有一个唯一的 URL,它包含的信息指出文件的位置以及浏览器应该怎么处理它。
URL 基本格式:
协议类型:[服务器地址[:端口号]][资源层级 UNIX 文件路径]文件名[?查询字符串][#片段标识符]
协议类型:[[访问资源需要的凭证信息@]服务器地址[:端口号]][资源层级 UNIX 文件路径]文件名[?查询字符串][#片段标识符]
URL encode 和 URL decode:
像 /
、?
:等这样的字符,已经被 URL 当做特殊意义理解了,因此这些字符不能随意出现。如果某个参数中需要带有这些特殊字符,就必须先对特殊字符进行转义,即 URL encode。
一个中文字符由 UTF-8 或者 GBK 这样的编码方式构成,虽然在 URL 中没有特殊含义,但是仍然需要进行转义,否则浏览器可能把 UTF-8/GBK 编码中的某个字节当做 URL 中的特殊符号。
转义的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式。
例如:
我们使用百度搜索C++
,会得到以下的URL:
https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&tn=baidu&wd=C%2B%2B&fenlei=256&oq=%2526lt%253B%252B%252B&rsv_pq=e54ba5da000bec62&rsv_t=356cAxbNy2N%2FqCyjiqiUM%2FegsZGQCgI7yoN0BsJk0FDJ2jKMW8rVEj11dIg&rqlang=cn&rsv_dl=tb&rsv_enter=0&rsv_btype=t&rsv_sug=1
可以发现 query string 的值是 C%2B%2B
、,通过使用 URL encode工具 对其进行解码,就知道 C%2B%2B
就是表示 C++。
根据 HTTP 标准,HTTP 请求可以使用多种请求方法。其中HTTP1.0 定义了三种请求方法: GET, POST 和 HEAD 方法。HTTP1.1 新增了六种请求方法:OPTIONS、PUT、PATCH、DELETE、TRACE 和 CONNECT 方法。
方法 | 描述 |
---|---|
GET | 请求指定的页面信息,并返回实体主体。 |
HEAD | 类似于 GET 请求,只不过返回的响应中没有具体的内容,用于获取报头 |
POST | 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST 请求可能会导致新的资源的建立和/或已有资源的修改。 |
PUT | 从客户端向服务器传送的数据取代指定的文档的内容。 |
DELETE | 请求服务器删除指定的页面。 |
CONNECT | HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器。 |
OPTIONS | 允许客户端查看服务器的性能。 |
TRACE | 回显服务器收到的请求,主要用于测试或诊断。 |
PATCH | 是对 PUT 方法的补充,用来对已知资源进行局部更新 。 |
其中最常用的就是GET方法和POST方法。
HTTP请求头提供了关于请求,响应或者其他的发送实体的信息。
应答头 | 说明 |
---|---|
Allow | 服务器支持哪些请求方法(如GET、POST等)。 |
Content-Encoding | 文档的编码(Encode)方法。 |
Content-Length | 表示内容长度。只有当浏览器使用持久HTTP连接时才需要这个数据。 |
Content-Type | 表示后面的文档属于什么MIME类型。Servlet默认为text/plain,但通常需要显式地指定为text/html。由于经常要设置Content-Type,因此HttpServletResponse提供了一个专用的方法setContentType。 |
Date | 当前的GMT时间。可以用setDateHeader来设置这个头以避免转换时间格式的麻烦。 |
Expires | 应该在什么时候认为文档已经过期,从而不再缓存它? |
Host | 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上。 |
Last-Modified | 文档的最后改动时间。客户可以通过If-Modified-Since请求头提供一个日期,该请求将被视为一个条件GET,只有改动时间迟于指定时间的文档才会返回,否则返回一个304(Not Modified)状态。Last-Modified也可用setDateHeader方法来设置。 |
Location | 表示客户应当到哪里去提取文档。Location通常不是直接设置的,而是通过HttpServletResponse的sendRedirect方法,该方法同时设置状态代码为302。 |
Refresh | 表示浏览器应该在多少时间之后刷新文档,以秒计。 |
Server | 服务器名字。 |
Set-Cookie | 设置和页面关联的Cookie。 |
WWW-Authenticate | 客户应该在Authorization头中提供什么类型的授权信息?在包含401(Unauthorized)状态行的应答中这个头是必需的。 |
当浏览者访问一个网页时,浏览者的浏览器会向网页所在服务器发出请求。当浏览器接收并显示网页前,此网页所在的服务器会返回一个包含 HTTP 状态码的信息头(server header)用以响应浏览器的请求。HTTP 状态码的英文为 HTTP Status Code。
常见的 HTTP 状态码:
HTTP 状态码由三个十进制数字组成,第一个十进制数字定义了状态码的类型。响应分为五类:信息响应(100 ~ 199) ,成功响应(200 ~ 299) ,重定向(300 ~ 399) ,客户端错误(400~499) 和服务器错误 (500 ~ 599) :
分类 | 描述 |
---|---|
1** | 信息,服务器收到请求,需要请求者继续执行操作 |
2** | 成功,操作被成功接收并处理 |
3** | 重定向,需要进一步的操作以完成请求 |
4** | 客户端错误,请求包含语法错误或无法完成请求 |
5** | 服务器错误,服务器在处理请求的过程中发生了错误 |
利用TCP传输协议实现一个只在网页上显示“This is my httpServer!”,的简单HTTP服务器。
封装Server.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define CRLF "\r\n"
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define HOME_PAGE "index.html"
#define ROOT_PATH "wwwroot"
std::string ReadFile(const std::string &path)
{
std::ifstream in(path, std::ifstream::binary);
if (!in.is_open())
{
return "404 NOT FOUND";
}
std::string content;
std::string line;
while (std::getline(in, line))
{
content += line;
}
in.close();
return content;
}
std::string GetPath(std::string request)
{
std::size_t pos = request.find(CRLF);
if(pos == std::string::npos)
{
return nullptr;
}
// 获取第一行:GET PATH HTTP/1.1
std::string line = request.substr(0, pos);
std::size_t first_space = line.find(SPACE);
if(first_space == std::string::npos)
{
return nullptr;
}
std::size_t second_space = line.rfind(SPACE);
if(second_space == std::string::npos)
{
return nullptr;
}
std::string path = line.substr(first_space + SPACE_LEN, second_space - (SPACE_LEN + first_space));
if(path[0] == '/' && path.size() == 1)
path += HOME_PAGE;
return path;
}
void _handlerHttpRequest_(int sock)
{
char buf[1024 * 10] = {0};
ssize_t read_size = read(sock, buf, sizeof buf);
if (read_size > 0)
{
std::cout << buf;
}
// std::string response = "HTTP/1.1 302 Temporarily Moved\r\n"; // 临时重定向
std::string response = "HTTP/1.1 301 Permanently Moved\r\n"; // 永久重定向
response += "Location: https://www.baidu.com/\r\n";
response += "\r\n";
// write(sock, response.c_str(), response.size());
send(sock, response.c_str(), response.size(), 0);
}
void handlerHttpRequest(int sock)
{
char buf[1024 * 10] = {0};
ssize_t read_size = read(sock, buf, sizeof buf);
if (read_size > 0)
{
std::cout << buf;
}
// 获取请求中的路径
std::string path = GetPath(buf);
// 请求的文件路径在请求行中,第二个字段就是要访问的文件
// GET PATH HTTP/1.1
std::string resource = ROOT_PATH;
resource += path;
//debug
std::cout << resource << std::endl;
// 获取文件后缀
std::size_t pos = resource.find(".");
std::string suffix = resource.substr(pos);
// 读取文件内容
std::string html = ReadFile(resource);
// 回响
std::string response;
response = "HTTP/1.0 200 OK\r\n";
if (suffix == ".jpg")
response += "Content-Type: image/jpeg\r\n";
else
response += "Content-Type: text/html\r\n";
response += ("Content-Length: " + std::to_string(html.size()) + "\r\n");
// 添加cookie
response += "Set-Cookie: this is my cookie content;\r\n";
response += "\r\n";
response += html;
send(sock, response.c_str(), response.size(), 0);
}
class Server
{
public:
Server(uint16_t port, const std::string &ip = "")
: _listenSock(-1), _port(port), _ip(ip), _quit(false)
{
signal(SIGCHLD, SIG_IGN);
}
~Server()
{
if (_listenSock >= 0)
{
close(_listenSock);
}
}
public:
void init()
{
// 创建socket
_listenSock = socket(AF_INET, SOCK_STREAM, 0);
if (_listenSock < 0)
{
std::cerr << "socket error" << std::endl;
exit(1);
}
// 填充信息
sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = PF_INET;
addr.sin_port = htons(_port);
addr.sin_addr.s_addr = _ip.empty() ? htonl(INADDR_ANY) : inet_addr(_ip.c_str());
// bind
if (bind(_listenSock, (const sockaddr *)&addr, sizeof(addr)) < 0)
{
std::cerr << "bind error" << std::endl;
exit(2);
}
// 监听
if (listen(_listenSock, 5) < 0)
{
std::cerr << "listen error" << std::endl;
exit(3);
}
}
void loop()
{
while (!_quit)
{
sockaddr_in peer;
socklen_t len = sizeof(peer);
int peer_sock = accept(_listenSock, (sockaddr *)&peer, &len);
if (peer_sock < 0)
{
std::cerr << "accept error" << std::endl;
exit(4);
}
// 多进程版
pid_t id = fork();
if (id > 0)
{
// 父进程
close(peer_sock);
}
else if (id == 0)
{
// 子进程
close(_listenSock);
handlerHttpRequest(peer_sock);
close(peer_sock);
exit(0);
}
else
{
std::cerr << "fork error" << std::endl;
exit(5);
}
}
}
bool quitServer()
{
_quit = true;
return true;
}
private:
int _listenSock; // sock
uint16_t _port; // 服务器端口
std::string _ip; // IP地址
bool _quit; // 安全退出的标志
};
实现httpServer
#include
#include "Server.hpp"
static void Usage(std::string proc)
{
std::cerr << "Usage:\n\t" << proc << " port" << std::endl;
std::cerr << "example:\n\t" << proc << " 8080\n"
<< std::endl;
}
int main(int argc, char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(-1);
}
uint16_t port = atoi(argv[1]);
Server server(port);
server.init();
server.loop();
return 0;
}
编写 index.html
DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>我的测试title>
head>
<body>
<p> This is my httpServer!p>
body>
html>