⭐️ 本篇博客开始给大家介绍应用层的一个协议——HTTP协议,我会带大家先认识协议,再谈HTTP协议的格式、报头、状态码和方法等相关细节,我还会简单介绍一下在他基础之上扩增一层软件层的协议——HTTPS,它相比HTTP来说,要更安全一些。话不多说,进入今天主要内容~
目录
- 认识协议
- HTTP协议
- 介绍
- URL、URI和URN
- urlencode和urldecode
- HTTP协议格式
- 请求协议格式
- 响应协议格式
- HTTP的方法
- HTTP状态码
- HTTP常见的报头属性
- HTTPS
- 总结
认识协议
协议是一种“约定”,这种约定是双方都知道的。有了一致的约定,双方才能够正常地进行通信。协议在网络的第一篇博客中也提到过,协议是双方进行通信的基础,在网络通信中存在着各种协议,有了这些协议,网络的通信才能够正常运转。
今天我要用一个例子带大家认识协议——网络计算器。我结合上一篇博客的线程池版本的TCP服务器进行编写这个网络计算器。
大致过程如下:
- 客户端向服务端发送一个请求数据包
- 服务端将请求数据包进行解析,并且进行业务处理,然后返回一个响应数据包给客户端
- 客户端将响应数据包进行解析,得到计算结果
注意: 客户端将请求封装成一个数据包,该过程叫做序列化,服务端将请求数据包进行解析的过程叫做反序列化。目前市面上有json、xml等格式,都可以供程序员进行该操作。
协议定制:
- 请求数据包用一个结构体进行封装,里面有两个操作数和一个操作符
- 响应数据包也用一个结构体进行封装,里面有计算结果和状态码
协议的头文件如下:
/**Protocol.hpp**/#pragma oncestruct request{int _num1;int _num2;char _op;request(int num1, int num2, char op):_num1(num1) ,_num2(num2) ,_op(op){}};struct response{int _code;// 0 正常 1 除以0错误2 操作符选择错误int _result;// 结果response(int code, int result):_code(code) ,_result(result){}};
客户端填充请求数据包: 客户端需要让用户输入两个操作数和一个操作符,然后填充好请求数据包(序列化),并且调用send
将请求数据包发送过去,然后reccv
接受服务端发送过来的响应数据包,并且进行解析(反序列化),分析出状态码和结果即可,代码如下(客户端发起请求部分的代码,服务器创建和上篇博客代码一样):
void Request(){std::string msg;while (1){request rq(0, 0, 0);std::cout << "Please Enter first num# ";std::cin >> rq._num1;std::cout << "Please Enter second num# ";std::cin >> rq._num2;std::cout << "Please Enter(format:num1(+-*/)num2)# ";std::cin >> rq._op;send(_sock, &rq, sizeof(rq), 0);response rp(0, 0);ssize_t size = recv(_sock, &rp, sizeof(rp), 0);if (size <= 0){std::cerr << "read error" << std::endl;exit(-1);}if (rp._code == 1){std::cout << "code: " << rp._code << std::endl;//std::cout << "除零错误" << std::endl;}else if (rp._code == 2){std::cout << "code: " << rp._code << std::endl;//std::cout << "非法操作符" << std::endl;}else{std::cout << "code: " << rp._code << std::endl;std::cout << "result: " << rp._result << std::endl;}}}
服务端响应: 这里使用了线程池为每个服务端提供服务,这里只需要修改Task中的Run方法即可,也就是修改业务处理的部分,代码如下:
static void Service(std::string ip, int port, int sock){while (1){request rt(0, 0, 0); ssize_t size = recv(sock, &rt, sizeof(rt), 0);// 阻塞方式读取 if (size > 0){// 正常读取size字节的数据std::cout << "[" << ip << "]:[" << port<< "]# "<< rt._num1 << rt._op << rt._num2 << "=?"<< std::endl;response rp(0, 0);switch(rt._op){case '+':rp._result = rt._num1 + rt._num2; break;case '-':rp._result = rt._num1 - rt._num2; break;case '*':rp._result = rt._num1 * rt._num2; break;case '/':if (rt._num2 == 0){rp._code = 1;}else{rp._result = rt._num1 / rt._num2; }break;default:rp._code = 2;break;}send(sock, &rp, sizeof(rp), 0);}else if (size == 0){// 对端关闭std::cout << "[" << ip << "]:[" << port<< "]# close" << std::endl;break;}else{// 出错std::cerr << sock << "read error" << std::endl; break;}}close(sock);std::cout << "service done" << std::endl;}struct Task{int _port;std::string _ip;int _sock;Task(int port, std::string ip, int sock):_port(port),_ip(ip) ,_sock(sock){}void Run(){Service(_ip, _port, _sock);}};
代码运行效果如下:
正常运算:
错误处理: 状态码为1,表示发生除0错误
HTTP协议
介绍
HTTP协议是Hyper Text Transfer Protocol(超文本传输协议
)的缩写,本质是基于TCP协议来进行文本设置完成协议通信。HTTP协议支持客户端——服务端模式,也就是请求与响应模式,且客户端需要以浏览器的方式访问服务端。
上网的大部分行为就是进行进程间通信,获取信息和发送信息:
- 把服务器资源拿到本地(下载资源、刷视频等)
- 把本地资源推送到服务器(搜索信息)
URL、URI和URN
- URI: 统一资源标识符(Uniform Resource Identifier),用来标识资源的唯一性
- URN: 统一资源名称(Uniform Resource Name),用名字标识资源
- URL: 统一资源定位符(Uniform Resource Locator),给互联网上的每一个文件资源都贴上这样一个唯一标签,并且包含了资源位置信息和访问方式浏览器可以通过URL中的文件位置信息找到对应的资源文件。
结构:
- 协议方案名: 发起请求用到的协议
- 登录信息: 登录认证是用的的信息,通常被忽略
- 服务器地址: 访问资源所在的服务器的地址,也就是域名(字符串风格的)
- 端口号: 服务器绑定的端口号
- 文件路径: 访问资源在目标服务器上的位置信息
- 查询字符串: 查询信息
- 片段标识符: 对某些资源信息的描述与补充
三者关系与区别:
- URL是URI的一种,URL是URI的一种具体表现,包含了资源的位置信息和获取资源的方式
- URN是URI的一种,用特定命名字标识资源,但不包含访问方式。
urlencode和urldecode
urlcode是一种编码方式,urldecode是一种解码方式。在客户端向服务器发起http请求时,为了方便http服务器识别,会将请求进行urlcode编码,同样地httpserver也会对这些字符进行urldecode解码。
编码规则:
- 数字,字母和连字符不作处理
- 中文字符和特殊字符会进行编码
- 要转码的字符会被转为16进制,从右至左取4位,每两位为以为,前面加上%,编码成%XY格式
代码实现如下:
string UrlEncode(const string& szToEncode){string src = szToEncode;char hex[] = "0123456789ABCDEF";string dst; for (size_t i = 0; i < src.size(); ++i){unsigned char cc = src[i];if (isascii(cc)){if (cc == ' '){dst += "%20";}elsedst += cc;}else{unsigned char c = static_cast<unsigned char>(src[i]);dst += '%';dst += hex[c / 16];dst += hex[c % 16];}}return dst;}string UrlDecode(const string& szToDecode){string result;int hex = 0;for (size_t i = 0; i < szToDecode.length(); ++i){switch (szToDecode[i]){case '+':result += ' ';break;case '%':if (isxdigit(szToDecode[i + 1]) && isxdigit(szToDecode[i + 2])){string hexStr = szToDecode.substr(i + 1, 2);hex = strtol(hexStr.c_str(), 0, 16);//字母和数字[0-9a-zA-Z]、一些特殊符号[$-_.+!*'(),] 、以及某些保留字[$&+,/:;=" />//可以不经过编码直接用于URLif (!((hex >= 48 && hex <= 57) || //0-9(hex >=97 && hex <= 122) || //a-z(hex >=65 && hex <= 90) ||//A-Z//一些特殊符号及保留字[$-_.+!*'(),][$&+,/:;=?@]hex == 0x21 || hex == 0x24 || hex == 0x26 || hex == 0x27 || hex == 0x28 || hex == 0x29|| hex == 0x2a || hex == 0x2b|| hex == 0x2c || hex == 0x2d || hex == 0x2e || hex == 0x2f|| hex == 0x3A || hex == 0x3B|| hex == 0x3D || hex == 0x3f || hex == 0x40 || hex == 0x5f)){result += char(hex);i += 2;}else result += '%';}else {result += '%';}break;default:result += szToDecode[i];break;}}return result;}
在线工具: https://tool.chinaz.com/tools/urlencode.aspx
HTTP协议格式
请求协议格式
我们可以编写一个简单的基于TCP协议的服务器,并且通过以浏览器作为客户端,对服务器发起请求,并把请求部分打印下来,代码如下:
#include #include #include #include #include #include #include #include #include #include using namespace std;int main(){// 创建套接字int listen_sock = socket(AF_INET, SOCK_STREAM, 0);if (listen_sock < 0){cerr << "socket creat fail" << endl;exit(1);}cout << "socket creat succes, socket: " << listen_sock << endl;// 绑定struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(8081);local.sin_addr.s_addr = INADDR_ANY;if (bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){cerr << "bind fail" << endl;exit(2);}cout << "bind success" << endl;// 将套接字设置为监听状态if (listen(listen_sock, 5) < 0){cerr << "listen fail" << endl;exit(3);}struct sockaddr_in peer;socklen_t len = sizeof(peer);while (1){// 获取连接int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);if (sock < 0){cerr << "accept error" << endl;continue;}// 创建子进程if (fork() == 0){close(listen_sock);if (fork() > 0){exit(-1);}else{// 孙子进程while (1){char buf[1024];ssize_t size = recv(sock, buf, sizeof(buf), 0);if (size >0){buf[size] = 0;cout << "#################################### http begin ############################################" << endl;cout << buf << endl;cout << "#################################### http end ############################################" << endl;}}else if (size == 0){cout << "close" << endl;break;}else{cerr << "recv error" << endl;}}close(sock);exit(0);}}close(sock);waitpid(-1, nullptr, 0);}return 0;}
下面是通过浏览器对我们的服务器发起请求:
请求内容:
上面是一次请求的内容,大体分为四个部分:请求行、请求报头、空行和请求正文
- 请求行: 请求方法+请求url+http协议版本
- 请求报头: 请求相关属性信息,以
key:value
的形式显示,且用空行分隔每一个属性信息 - 空行: 分隔报头和报文
- 请求正文: 允许为空,且如果请求方法为post,请求报头中会有
Content-Length
属性字段来标识请求正文的长度
回答几个小问题:
1、HTTP协议如何保证报头和有效载荷分离?
通过空行来分离报头和有效载荷,空行之后都是有效载荷的内容,且一般还会有
Content-Length
属性字段来标识正文的长度
2、服务端如何保证自己读取报头完毕?
循环读取,直到读到空行,就说明报头信息读取完毕
响应协议格式
响应的格式如下:
- 响应行: 版本号+状态码+状态码解释
- 响应报头: 响应相关属性信息,以
key:value
的形式显示,且用空行分隔每一个属性信息 - 空行: 分隔报头和报文
- 响应正文: html、JSON、XML等格式的文本
为了更好地让大家看到响应的效果,这里再上面的代码的基础上增加一点响应的部分,其中包含两个字段:Content-Type(正文格式,数据类型)和Content-Length(正文长度) 两个字段
这里我们正文返回一个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>注册页面</title></head><body><form method = "POST" name="注册表" action="mysql_cgi">用户名:<input type="text" name = "username"> <br/>密码:<input type="password" name = "password"> <br/><input type=submit value="注册"> </form></body></html>
这里使用sendfile
这个接口来发送该文本,该接口可以直接将一个文件的内容拷贝给另一个文件,且在内核中完成该操作,效率极高,介绍如下:
sendfile
功能: 把一个文件描述符的内容拷贝给另一个文件描述符,在内核层完成该操作,不经过用户层,比read和write的效率高
#include ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
参数:
- out_fd: 要被写入的文件描述符
- in_fd: 要被读取的文件描述符
- offset: 偏移量,可以记录读取文件的位置
- count: 要拷贝内容的大小
返回值: 成功返回已经写入的字节数,失败返回-1
在sendfile
这个接口中,需要填充需要发送多少字节,这里我们发送整个文件,所以这里为了能够获取一个文件大小,这里再介绍一个接口stat
,具体如下:
stat
功能: 获取文件的属性
#include #include #include int stat(const char *pathname, struct stat *buf);
参数:
- pathname: 文件所在路径
- buf: 存储文件属性的
struct stat
类型的结构体返回值: 成功返回0,失败返回-1
我们可以通过stat
获取文件大小,填充Content-Length
字段,然后通过sendfile
接口发送html格式的文件到套接字中,具体如下:
// 响应int fd = open("index1.html", O_RDONLY);struct stat st;stat("./index1.html", &st);//string status_line = "HTTP/1.1 307 Temporary Redirect";string status_line = "HTTP/1.1 200 OK\n";// 2.响应报头string response_header = "Content-Length: " + to_string(st.st_size) + "\n";response_header += "Content-Type: text/html\n";//response_header += "Set-Cookie: ;\n";//response_header += "location: https://www.qq.com\n";// 3.空行string blank = "\n";// 4.正文send(sock, status_line.c_str(), status_line.size(), 0);send(sock, response_header.c_str(), response_header.size(), 0);send(sock, blank.c_str(), blank.size(), 0); sendfile(sock, fd, nullptr, st.st_size);close(fd);
这里分别用telnet和Postman做测试:
telnet
Postman
回答几个问题:
- 如何将报头和有效载荷进行分离?
空行分隔有效载荷和报头
- 如何将报文读取完毕?
在读取报头时,将
Contetn-Length
属性字段描述的内容记录下来,正文读取Content-Length
大小的内容,就代表将正文读取完毕
HTTP的方法
列举常见的几种:
方法 | 说明 | 支持的HTTP协议版本 |
---|---|---|
GET | 获取资源 | 1.0/1.1 |
POST | 传输实体主体 | 1.0/1.1 |
PUT | 传输文件 | 1.0/1.1 |
HEAD | 获得报文首部 | 1.0/1.1 |
DELETE | 删除文件 | 1.0/1.1 |
OPTIONS | 询问支持的方法 | 1.1 |
TRACE | 追踪路径 | 1.1 |
说明: HTTP/1.1在HTTP/1.0基础上增加了长连接和缓存处理等内容,HTTP/1.0是基于请求与响应的,每次发送请求都需要重新建立连接,获得响应后关闭连接,而HTTP/1.1是支持长连接,客户端可以通过一个连接向服务器发起多个HTTP请求,上层依次读取请求后还会保持连接。
这里主要介绍两个方法:GET和POST
GET 可以直接获取资源,还可以上传数据,通过url进行参数的传递(例如:” />
下一次访问该网站时,浏览器会自动将cookie文件中的内容提取并填充请求报头中的cookie字段,这样服务器端也就自动完成了认证,也就实现了免账号密码登录了。
cookie文件分为内存级别和文件级别,前者是浏览器关闭,cookie文件就消失,后者是保存在本地磁盘,可以永久保存。
cookie带来的问题:
cookie文件容易被恶意软件盗取,或者一些非法钓鱼网站获取,这样非法用户就可以拿着我们的cookie文件登录访问我们登陆过的网站,这样就造成了信息泄漏。
cookie和session共同解决:
为了解决cookie文件泄漏的问题,又引进了一种机制——session,cookie与session结合使用,减少了信息泄漏的可能性。具体如下:
在上面的基础上增加了一个session的机制,浏览器第一次想服务端发起请求,服务器会创建一个Session
将用户的信息进行保存,用来确定用户身份,同时会生成一个Session ID
,服务器会将这个Session ID
进行返回,给浏览器,浏览器会将Session ID
保存在Cookie
文件中,以后的每一次认证,都是通过Session ID
来进行的
这样,Session通过在服务器端记录信息确定用户身份,Cookie通过在客户端记录信息确定用户身份。当然Session ID也是有时间限制的,Cookie的文件大小也限制在了4K。这样就能够比单纯使用Cookie更安全。HTTPS
HTTPS是HTTP的安全版本,也叫超文本安全传输,HTTPS会对要传输的数据进行加密,由SSL软件层来提供加密基础,这样HTTPS就比HTTP更安全。可以理解为HTTP+SSL/TSL=HTTPS,SSL和TSL选择一个,SSL(Secure Sockets Layer)协议既安全套接字层协议,TLS(Transport Layer Security)协议即安全传输层协议。
)
HTTP和HTTPS区别:- HTTP使用明文传输数据,HTTPS是密文传输,中间有一层SSL或TSL进行加密,更安全
- HTTP使用的端口号是80,HTTPS使用的端口号是443
- HTTPS需要申请证书
总结
HTTP协议的全部内容就介绍到这里了,喜欢的话,欢迎点赞、收藏和关注支持~