一、HTTP协议简单介绍
1.1 什么是HTTP协议
HTTP(超文本传输协议)是一种用于在Web浏览器和Web服务器之间传输数据的应用层协议。它是一种无状态协议,即服务器不会保留与客户端的任何连接状态信息,每个请求都被视为一个独立的事务。
假设你使用Web浏览器(例如Chrome)访问一个网页。当你在浏览器中输入网址并按下"Enter"键时,浏览器会向服务器发送一个HTTP请求。你也可以理解为HTTP协议是在客户端(浏览器)和服务器之间传输数据的基础(约定)。
1.2 再次理解协议
协议是指在通信过程中,参与方之间所达成的一种约定或规范。在网络通信中,协议是用来定义数据传输规则和通信方式的一组规范。
具体来说,基于HTTP协议,它定义了客户端(例如Web浏览器)和服务器之间进行通信时所需遵循的规范。
HTTP协议主要包含以下几个方面的规定:
-
请求方式:HTTP协议定义了一系列的请求方法,如GET、POST、PUT、DELETE等,用于告知服务器进行何种操作。
-
请求和响应格式:HTTP协议规定了请求消息和响应消息的格式。请求消息由请求行、请求头部和请求正文组成,而响应消息由状态行、响应头部和响应正文组成。
-
状态码:HTTP协议定义了一系列的状态码,用于表示服务器对请求的处理结果。例如,200表示成功、404表示资源未找到、500表示服务器内部错误等。
-
头部信息:HTTP协议通过头部字段来携带各种元数据,例如Content-Type用于指示请求或响应的数据类型,Content-Length表示消息正文的长度等。
-
连接管理:HTTP协议还定义了一些机制用于管理连接,如持久连接(keep-alive)允许多个请求和响应复用同一个TCP连接,以减少连接建立的开销。
二、HTTP请求
2.1 HTTP的工作过程
我们不妨先来了解一下HTTP的工作过程。当你在浏览器中输入一个网址并按下"Enter"键时,浏览器就会向服务器发送一个HTTP请求。请求时,浏览器会给服务器发送请求报文。当服务器收到请求后,它会根据请求报文进行相应的处理,并生成一个HTTP响应(响应报文)返回给浏览器。一个请求再加一个回应,就完成了客户端与服务器的数据传输与交互。
上述讲述的都是概念。下面我们结合一段代码来理解。在看代码之前,强调一下HTTP 是一种应用层协议,是基于 TCP/IP 通信协议来传递数据的。具体也可看下图:
demo代码
首先我们需要基于套接字实现一个服务端HttpServer.hpp:
#include <iostream> #include <signal.h> #include "Sock.hpp" class HttpServer { public: using func_t = std::function<void(int)>; private: Sock _serverSock; int _sock; std::string _ip; uint16_t _port; func_t _func; public: HttpServer(uint16_t port, func_t func, std::string ip = "0.0.0.0") :_port(port) ,_func(func) ,_ip(ip) { _sock = _serverSock.Socket(); _serverSock.Bind(_sock, _port, _ip); _serverSock.Listen(_sock); } void start() { signal(SIGCHLD, SIG_IGN); while(true) { std::string clientIP; uint16_t clientPort = 0; int sockfd = _serverSock.Accept(_sock, &clientIP, &clientPort); if(sockfd < 0) continue; if(fork() == 0) { close(_sock); _func(sockfd); close(sockfd); exit(0); } close(sockfd); } } ~HttpServer() { if(_sock >= 0) close(_sock); } };
下面是对套接字操作的封装代码Sock.hpp:
#pragma once #include <iostream> #include <string> #include <cstring> #include <cerrno> #include <cassert> #include <unistd.h> #include <memory> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <ctype.h> #include "LogTest.hpp" class Sock { private: const static int gbacklog = 20; public: Sock() {} int Socket() { int listensock = socket(AF_INET, SOCK_STREAM, 0); if (listensock < 0) { LogMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno)); exit(2); } LogMessage(NORMAL, "create socket success, listensock: %d", listensock); return listensock; } void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0") { struct sockaddr_in local; memset(&local, 0, sizeof local); local.sin_family = AF_INET; local.sin_port = htons(port); inet_pton(AF_INET, ip.c_str(), &local.sin_addr); if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0) { LogMessage(FATAL, "bind error, %d:%s", errno, strerror(errno)); exit(3); } } void Listen(int sock) { if (listen(sock, gbacklog) < 0) { LogMessage(FATAL, "listen error, %d:%s", errno, strerror(errno)); exit(4); } LogMessage(NORMAL, "init server success"); } // 一般经验 // const std::string &: 输入型参数 // std::string *: 输出型参数 // std::string &: 输入输出型参数 int Accept(int listensock, std::string *ip, uint16_t *port) { struct sockaddr_in src; socklen_t len = sizeof(src); int servicesock = accept(listensock, (struct sockaddr *)&src, &len); if (servicesock < 0) { LogMessage(ERROR, "accept error, %d:%s", errno, strerror(errno)); return -1; } if(port) *port = ntohs(src.sin_port); if(ip) *ip = inet_ntoa(src.sin_addr); return servicesock; } bool Connect(int sock, const std::string &server_ip, const uint16_t &server_port) { struct sockaddr_in server; memset(&server, 0, sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(server_port); server.sin_addr.s_addr = inet_addr(server_ip.c_str()); if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0) return true; else return false; } ~Sock() {} };
下面我们要做的就是启动服务器,然后用Web浏览器访问我们所启动的服务器,这时候是浏览器向我们所写的服务器发送请求。根据上述HTTP协议的工作过程,这时候会像服务器发送一个请求报文。我们启动服务器HTTPServer.cc:
#include <iostream> #include <memory> #include <vector> #include <fstream> #include "Util.hpp" #include "HttpServer.hpp" void Usage(std::string name) { std::cout << "\nUsage :" << name << " Port\n" << std::endl; } void HandlerHttpRequest(int sockfd) { // 1. 读取请求 for test char buffer[10240]; ssize_t s = recv(sockfd, buffer, sizeof(buffer) - 1, 0); if (s > 0) { buffer[s] = 0; std::cout << buffer << "--------------------\n" << std::endl; } } int main(int argc, char* argv[]) { if(argc != 2) { Usage(argv[0]); exit(0); } std::unique_ptr<HttpServer> httpServer(new HttpServer(atoi(argv[1]), HandlerHttpRequest)); httpServer->start(); return 0; }
运行结果:
我们看到确实我们所写的服务器发送了一些信息。该信息就是请求报文。但发现无法打开此页面,是因为我们并没有向浏览器发送任何响应数据。接下来我们详细了解一下HTTP的请求。
2.2 URL介绍
URL(Uniform Resource Locator)是用于标识和定位互联网上资源的字符串。URL由多个组件构成,包括协议、域名(或IP地址)、端口号、路径和查询参数等。
下面是一个示例URL:http://www.example.com:8080/path/to/resource?param1=value1¶m2=value2
解释:
- 协议:URL的第一部分是协议,这里是"http"。协议指定了浏览器与服务器之间的通信规则,常见的有HTTP和HTTPS。
- 域名(或IP地址):在示例中,域名是"www.example.com"。域名是用于标识互联网上特定站点的字符串,也可以使用IP地址来代替。
- 端口号:示例中的端口号是"8080"。默认情况下,HTTP使用80端口,HTTPS使用443端口,但可以使用不同的端口号来访问特定的服务。
- 路径:路径指定了在服务器上资源的位置,示例中是"/path/to/resource"。路径可以是文件、目录或其他资源的位置。
- 查询参数:在示例中,查询参数是"?param1=value1¶m2=value2"。查询参数用于向服务器传递额外的信息,以便执行特定的操作或获取特定的结果。
平时我们俗称的 "网址" ,其实就是说的 URL。 具体也可看下图:
域名就是服务器地址。浏览器会对域名进行解析,解析后就会转换为对应的地址。一个服务器地址,再加上端口号,这就标示了该服务器的唯一进程。端口号后面用 ‘ / ’ 分隔的就是我们所请求资源在该服务器上的路径。
2.3 HTTP 请求格式
服务器收到一个HTTP请求后,请求格式如下:
-
请求行:浏览器发送的第一部分是请求行,它包含了请求的方法(例如GET)、要访问的资源路径(例如/index.html)以及使用的HTTP版本(例如HTTP/1.1)。
-
请求头部:接下来,浏览器发送请求头部,其中包含一些额外的信息,例如浏览器类型、所支持的编码方式、语言首选项等。
-
空行:请求头部之后是一个空行,用于分隔请求头部和请求正文。
-
请求正文(可选):有些请求可能包含请求正文,例如表单数据或上传的文件。
其实我们对照我们刚刚举例的运行结果,也可总结出请求报文的格式,具体如下图:
当服务器拿到请求报文后,会对请求报文进行分析。例如,其中就包含了请求的方法(例如GET)、请求的资源路径和协议版本,结合请求报头就会对此进行分析,找到资源并形成响应报文进行返回。其中有许多细节并未解释,后文会详细解释。下面我们先来看一下响应报文的格式。
三、HTTP响应
上述我们例子中并未看到有任何界面。原因就是在于Web浏览器并未收到任何响应。根本在于我们所写的服务器就没有对此进行响应。我们不妨先看一下响应的实例。
3.1 响应demo
HttpServer.cc:
// 一般http都要有自己的web根目录 #define ROOT "./wwwroot" // ./wwwroot/index.html // 如果客户端只请求了一个/,我们返回默认首页 #define HOMEPAGE "index.html" void Usage(std::string name) { std::cout << "\nUsage :" << name << " Port\n" << std::endl; } void HandlerHttpRequest(int sockfd) { // 1. 读取请求 for test char buffer[10240]; ssize_t s = recv(sockfd, buffer, sizeof(buffer) - 1, 0); if (s > 0) { buffer[s] = 0; // std::cout << buffer << "--------------------\n" << std::endl; } std::vector<std::string> vline; Util::cutString(buffer, "\n", &vline); std::vector<std::string> vblock; Util::cutString(vline[0], " ", &vblock); std::string file = vblock[1]; std::string target = ROOT; if (file == "/") file = "/index.html"; target += file; std::cout << target << std::endl; std::string content; std::ifstream in(target); if (in.is_open()) { std::string line; while (std::getline(in, line)) { content += line; } in.close(); } std::string HttpResponse; if (content.empty()) HttpResponse = "HTTP/1.1 404 NotFound\r\n"; else HttpResponse = "HTTP/1.1 200 OK\r\n"; HttpResponse += "\r\n"; HttpResponse += content; // 2. 试着构建一个http的响应 send(sockfd, HttpResponse.c_str(), HttpResponse.size(), 0); } int main(int argc, char *argv[]) { if (argc != 2) { Usage(argv[0]); exit(0); } std::unique_ptr<HttpServer> httpServer(new HttpServer(atoi(argv[1]), HandlerHttpRequest)); httpServer->start(); return 0; }
这里有一个细节:当我们输入URL没有请求资源路径时,浏览器会自动加上一个 ’ / ‘,代表着根目录。这里的根目录与Liunx 的根目录是不同的。一般服务器都会设置默认的Web根目录。这时候就是访问的默认界面。
index.html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>HTTP响应</title> </head> <body> <h3>现在你能够看到我了</h3> <p>我是一个Linux的学习者,我正在进行http的测试工作!!</p> <p>我是一个Linux的学习者,我正在进行http的测试工作!!</p> <p>我是一个Linux的学习者,我正在进行http的测试工作!!</p> <p>我是一个Linux的学习者,我正在进行http的测试工作!!</p> <p>我是一个Linux的学习者,我正在进行http的测试工作!!</p> <p>我是一个Linux的学习者,我正在进行http的测试工作!!</p> <p>我是一个Linux的学习者,我正在进行http的测试工作!!</p> </body> </html>
运行结果:
确实有了界面,也正是我们所设计的界面。通过HTTP,客户端可以获取到Web服务器上的各种资源,例如HTML文档、图像、视频、样式表等。
3.2 HTTP响应格式
当服务器收到请求后,它会进行相应的处理,并生成一个HTTP响应返回给浏览器。
响应状态行:响应的第一部分是状态行,它包含了响应的HTTP版本(例如HTTP/1.1)、响应状态码(例如200表示成功)以及对应的状态消息(例如"OK")。
响应头部:接下来,服务器发送响应头部,其中包含一些额外的信息,例如服务器类型、响应时间、返回的数据类型等。
空行:响应头部之后是一个空行,用于分隔响应头部和响应正文。
响应正文:响应正文包含了服务器返回的实际数据,例如HTML页面、图像、CSS样式表等。
具体也可结合下图理解: