一、HTTP简介
HTTP协议(超文本传输协议HyperText Transfer Protocol),它是基于TCP协议的应用层传输协议,简单来说就是客户端和服务端进行数据传输的一种规则。
1、认识URL
URL(Uniform Resource Lacator)叫做统一资源定位符,也就是我们通常所说的网址,URL是对从互联网上得到的资源的位置和访问方法的一种简洁的表示,是互联网上标准资源的地址,互联网上的每个文件都有一个唯一的URL,它包含的信息指出文件的位置以及浏览器应该怎处理它。
一个URL大致由如下几部分构成(其中有些部分是可以省略的):
解释 :
http://
表示请求时需要使用的协议,通常使用的是HTTP协议或安全协议HTTPS,成熟的协议要和端口号一 一匹配,例如使用HTTP协议的服务器要采用80号端口提供服务,使用HTTPS协议的服务器要采用443号端口提供服务usr:pass
表示的是登录认证信息,包括登录用户的用户名和密码。虽然登录认证信息可以在URL中体现出来,但绝大多数URL的这个字段都是被省略的,因为登录信息可以通过其他方案交付给服务器。www.example.jp
表示的是服务器地址,也叫做域名,通过域名解析系统能够得到域名对应的IP地址。
例如使用ping
命令获得www.baidu.com
域名解析后的IP地址为39.156.66.14
我们可以在浏览器的地址栏使用这个IP直接访问百度的网站。80
表示的是服务器端口号,这个字段一般是被省略的,因为协议和端口号是一一对应的。/dir/index.htm
表示的是要访问的资源所在的路径。访问服务器的目的是获取服务器上的某种资源,通过前面的域名和协议已经能够找到对应的服务器进程了,此时要做的就是指明该资源所在的路径。
例如我们现在要访问百度的更多资源,我们就要指明路径(当然对于我们普通人,我们只需要点击就行,由百度的后端帮我们输入链接进行跳转)
uid=1
表示的是请求时提供的额外的参数,这些参数是以键值对的形式,通过&符号分隔开的。
比如我们在百度上面搜索你好世界
,此时可以看到URL中有很多参数,而在这众多的参数当中有一个参数wd(word),表示的就是我们搜索时的搜索关键字wd=你好世界。ch1
表示的是片段标识符,是对资源的部分补充。
2、URL encode和URL decode
如果在搜索关键字当中出现了像/?:
这样的字符,由于这些字符已经被URL当作特殊意义理解了,因此URL在呈现时会对这些特殊字符进行转义。
转义的规则如下:
- 将需要转码的字符转为十六进制,然后从右到左,取4位(不足4位直接处理),每两位做一位,前面加上%,编码成%XY格式。
比如当我们搜索C++时,由于+
加号在URL当中也是特殊符号,经过转义以后变成了%2B
,因此一个+
就会被编码成一个%2B
。
说明: URL当中会对这些特殊符号做编码,中文也属于特殊字符。
我们浏览器看到的是中文,但是复制其url
时,就会发现其被转义了。
- URL编码(URL encode):是将URL中的特殊字符转换为%符号后跟着两个十六进制数的形式。
- URL解码(URL decode):是将这种经过URL编码的字符串重新转换回原始的字符串。
3、HTTP协议格式
Ⅰ、请求格式
HTTP请求由以下四部分组成:
- 请求行:[请求方法] + [url] + [http版本]
- 请求报头:请求的属性,这些属性都是以
key: value
的形式按行陈列的。 - 空行:遇到空行表示请求报头结束,HTTP就是根据空行来进行请求报头与有效载荷进行分离的!
- 请求正文:请求正文允许为空字符串,如果请求正文存在,则在请求报头中会有一个
Content-Length
属性来标识请求正文的长度。
前面三部分是一般是由HTTP协议自行设置的,而请求正文一般是用户提交的相关信息或数据,如果用户在请求时没有信息要上传给服务器,此时请求正文就为空字符串。
为了证明上面的结论我们可以通过启动下面的代码将浏览器的发送的请求,打印出来。
// Sock.hpp // 这是一个对socket相关接口的一个封装,大概了解其接口的作用就行 #pragma once #include <iostream> #include <cstring> #include <cerrno> #include <cstdlib> #include <sys/socket.h> #include <sys/types.h> #include <arpa/inet.h> #include <netinet/in.h> #include <unistd.h> class Sock { public: Sock() :_sock(-1) {} void Socket() { _sock = socket(AF_INET, SOCK_STREAM, 0); } void Bind(uint16_t port) { struct sockaddr_in local; socklen_t len = sizeof(local); memset(&local, 0, len); local.sin_family = AF_INET; local.sin_addr.s_addr = INADDR_ANY; local.sin_port = htons(port); bind(_sock, (struct sockaddr*)&local, len); } void Listen(int backlog = 32) { listen(_sock, backlog); } int Accept(std::string* client_ip, uint16_t* client_port) { struct sockaddr_in client; socklen_t len = sizeof(client); memset(&client, 0, len); int sockfd = accept(_sock, (struct sockaddr*)&client, &len); return sockfd; } int Connect(const std::string& server_ip, uint16_t server_port) { struct sockaddr_in server; socklen_t len = sizeof(server); memset(&server, 0, len); server.sin_family = AF_INET; inet_pton(AF_INET, server_ip.c_str(), &server.sin_addr); server.sin_port = htons(server_port); return connect(_sock, (struct sockaddr*)&server, len); } int getFd() { return _sock; } ~Sock() { if (_sock >= 0) { close(_sock); } } private: int _sock; // 对于服务端来说是监听套接字 };
// httpserver.hpp // 这是一个对服务器相关接口的一个封装,大概了解其接口的作用就行。 // 注意这里使用了原生线程库pthread #pragma once #include <iostream> #include <string> #include <functional> #include <pthread.h> #include <sys/types.h> #include <sys/socket.h> #include "Sock.hpp" class HttpServer { using func_t = std::function<std::string(const std::string&)>; public: HttpServer(func_t func, uint16_t server_port) :_func(func), _server_port(server_port) {} void Init() { _listen_fd.Socket(); _listen_fd.Bind(_server_port); _listen_fd.Listen(); } void Start() { std::string client_ip; uint16_t client_port; while (true) { // 接收连接 int sockfd = _listen_fd.Accept(&client_ip, &client_port); if (sockfd < 0) { continue; } // 多线程并发处理任务 pthread_t tid; ThreadData* ptd = new ThreadData(sockfd, this, client_port, client_ip); pthread_create(&tid, nullptr, ThreadRoutine, ptd); } } private: struct ThreadData { ThreadData(int sockfd, HttpServer* is, uint16_t client_port, std::string client_ip) :_sockfd(sockfd), _is(is), _client_port(client_port), _client_ip(client_ip) {} ~ThreadData() { if (_sockfd >= 0) { // logMessage(Info, "通讯完成,文件描述符关闭"); close(_sockfd); } } public: int _sockfd; // 通信套接字 HttpServer* _is; // 传递过来的this指针 uint16_t _client_port; // 客户端端口号 std::string _client_ip; // 客户端ip }; static void* ThreadRoutine(void* args) { pthread_detach(pthread_self()); ThreadData* ptd = static_cast<ThreadData*>(args); ptd->_is->HandleHttpRequest(ptd->_sockfd, ptd->_client_ip, ptd->_client_port); delete ptd; return nullptr; } // 这个必须是一个可重入函数 void HandleHttpRequest(int sockfd, const std::string& client_ip, uint16_t client_port) { char buf[4096]; // 1.假设一次能够读取完毕,并且只请求一次 ssize_t n = recv(sockfd, buf, sizeof(buf) - 1, 0); buf[n] = '\0'; std::string request = buf; // 2.业务处理 std::string response = _func(request); // 3.结果返回 send(sockfd, response.c_str(), response.size(), 0); } private: func_t _func; // 回调函数 uint16_t _server_port; // 服务器端口号 Sock _listen_fd; // 监听套接字 };
// httpserver.cpp #include <iostream> #include <memory> #include <cstring> #include <cstdlib> #include <functional> #include "httpserver.hpp" // 使用手册 static void Usage(std::string proc) { std::cout << "usage\n\t" << proc << " 端口" << std::endl; } // 处理http请求的函数,这里我们对于请求不作任何处理,只打印请求的内容 std::string Handler(const std::string& request) { // 打印请求的内容 std::cout << "------------------------------------" << std::endl; std::cout << request << std::endl; return ""; } // 必须以 程序名 端口号 的方式启动 int main(int argc, char* argv[]) { if (argc != 2) { Usage(argv[0]); exit(USAGE_ERR); } uint16_t server_port = atoi(argv[1]); std::unique_ptr<HttpServer> up(new HttpServer(Handler, server_port)); up->Init(); up->Start(); return 0; }
编译运行我们的程序,这里我们的直接在浏览器的地址栏里面,输入我们的公网IP和端口号,格式:IP: 端口号
。
此时我们发现我们的服务器打印了下面的信息:
说明:
- 浏览器向我们的服务器发起HTTP请求后,因为我们的服务器程序没有对进行响应,此时浏览器就会认为服务器没有收到,然后再不断发起新的HTTP请求,因此虽然我们只用浏览器访问了一次,但会受到多次HTTP请求。
- 由于浏览器发起请求时默认用的就是HTTP协议,因此我们在浏览器的url框当中输入网址时可以不用指明HTTP协议。
- url当中的
/
表示的是web根目录,这个web根目录可以是你的机器上的任何一个目录,由你自己指定的。