四、HTTP协议格式
应用层常见的协议有HTTP和HTTPS,传输层常见的协议有TCP、UDP,网络层常见的协议是IP,数据链路层对应的就是MAC帧。其中下三层是由操作系统或者驱动完成的,主要负责的是通信细节。若应用层不考虑下三层,在应用层的角度看来,就可以认为自己是在和对方的应用层在直接进行数据交互
下三层负责的是通信细节,而应用层负责的是如何使用传输过来的数据,两台主机在进行通信的时候,应用层的数据能够成功交给对端应用层,因为网络协议栈的下三层已经负责完成了这样的通信细节,而如何使用传输过来的数据就需要定制协议,这里最典型的就是HTTP协议
HTTP是基于请求和响应的应用层服务,作为客户端,可以向服务器发起request,服务器收到这个request后,会对这个request做数据分析,然后服务器再构建response,完成这一次HTTP的请求。这种基于request&response这样的工作方式,被称为cs或bs模式,其中c表示client,s表示server,b表示browser
HTTP是基于请求和响应的应用层访问,因此HTTP对应的请求格式和响应格式是学习HTTP的重点
4.1 HTTP请求协议格式
请求行:[请求方法]+[url]+[http版本]
请求报头:请求的属性,这些属性都是以key: value的形式按行陈列的
空行:遇到空行表示请求报头结束
请求正文:请求正文允许为空字符串,若请求正文存在,则在请求报头中会有一个Content-Length属性来标识请求正文的长度
前面三部分是一般是HTTP协议自带的,由HTTP协议自行设置的,而请求正文一般是用户的相关信息或数据,若用户在请求时没有信息要上传给服务器,此时请求正文就为空字符串
如何将HTTP请求的报头与有效载荷进行分离?
当应用层收到一个HTTP请求时,必须将HTTP的报头与有效载荷进行分离。对于HTTP请求而言,请求行和请求报头就是HTTP的报头信息,而请求正文就是HTTP的有效载荷
可以根据HTTP请求中的空行进行分离,当服务器收到一个HTTP请求后,先按行进行读取,若读取到空行则说明已将报头读取完毕,HTTP请求中的空行就是用来分离报头和有效载荷的
若将HTTP请求想象成一个大的线性结构(字符串),此时每行的内容都是用\n隔开的,因此在读取过程中,若连续读取到了两个\n,就说明已将报头读取完毕,后面剩下的就是有效载荷
获取浏览器的HTTP请求
在网络协议栈中,应用层的下一层叫做传输层,而HTTP协议底层通常使用的传输层协议是TCP协议,因此可以用套接字编写一个TCP服务器,然后启动浏览器访问该服务器
由于该服务器是直接用TCP套接字读取浏览器发来的HTTP请求,此时在服务端没有应用层对这个HTTP请求进行过任何解析。直接将浏览器发来的HTTP请求进行打印输出,此时就能看到HTTP请求的基本构成
#include <iostream> #include <fstream> #include <string> #include <cstring> #include <unistd.h> #include <sys/wait.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <arpa/inet.h> using namespace std; int main() { //创建套接字 int listen_sock = socket(AF_INET, SOCK_STREAM, 0); if (listen_sock < 0){ cerr << "socket error!" << endl; return 1; } //绑定端口 struct sockaddr_in local; memset(&local, 0, sizeof(local)); local.sin_family = AF_INET; local.sin_port = htons(9090); local.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){ cerr << "bind error!" << endl; return 2; } //监听 if (listen(listen_sock, 5) < 0){ cerr << "listen error!" << endl; return 3; } //启动服务器 struct sockaddr peer; memset(&peer, 0, sizeof(peer)); socklen_t len = sizeof(peer); for (;;){ 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(0); } //孙子进程 char buffer[1024]; recv(sock, buffer, sizeof(buffer), 0); //读取HTTP请求 cout << "--------------------------http request begin--------------------------" << endl; cout << buffer << endl; cout << "---------------------------http request end---------------------------" << endl; close(sock); exit(0); } //爷爷进程 close(sock); waitpid(-1, nullptr, 0); //等待爸爸进程 } return 0; }
运行服务器程序后,用浏览器进行访问,此时服务器就会收到浏览器发来的HTTP请求,并将收到的HTTP请求进行打印输出
浏览器向服务器发起HTTP请求后,服务器没有对进行响应,此时浏览器会认为服务器没有收到,然后再不断发起新的HTTP请求,因此只用浏览器访问了一次,但会收到多次HTTP请求
由于浏览器发起请求时默认用的就是HTTP协议,因此在浏览器的url框当中输入网址时可以不用指明使用的是HTTP协议
url当中的 / 并不一定云服务器上根目录,这个 / 表示的是web根目录,这个web根目录可以是机器上的任何一个目录,这个可由程序员指定
请求行中的url表示要访问这个服务器上的哪一路径下的资源。若浏览器在访问服务器时指明要访问的资源路径,那么此时浏览器发起的HTTP请求中的url也会变成该路径
请求报头中全部都是以 key:value 形式按行陈列的各种请求属性,请求属性陈列完后紧接着的就是一个空行,空行后的就是HTTP请求的请求正文,此时请求正文为空字符串,因此这里有两个空行
4.2 HTTP响应协议格式
状态行:[http版本]+[状态码]+[状态码描述]
响应报头:响应的属性,都以key: value的形式按行陈列
空行:遇到空行表示响应报头结束
响应正文:响应正文允许为空字符串,若响应正文存在,则响应报头中会有一个Content-Length属性来标识响应正文的长度。比如服务器返回了一个html页面,那么这个html页面的内容就在响应正文中
如何将HTTP响应的报头与有效载荷进行分离?
与HTTP请求基本一致。对于HTTP响应来讲,状态行和响应报头就是HTTP的报头信息,响应正文实际就是HTTP的有效载荷。与HTTP请求相同,当应用层收到一个HTTP响应时,也根据HTTP响应中的空行来分离报头和有效载荷的
构建HTTP响应给浏览器
服务器读取到客户端发来的HTTP请求后,需要对这个HTTP请求进行各种数据分析,然后构建成对应的HTTP响应发回给客户端。而服务器连接到客户端后,实际就只读取了客户端发来的HTTP请求就将连接断开了
接下来可以构建一个HTTP请求给浏览器,下面给浏览器返回一个固定的HTTP响应。当前服务程序所在的路径作为web根目录,可以在该目录下创建一个html文件,然后编写一个简单的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>Document</title> </head> <body> <h1>hello world!!!</h1> </body> </html>
当浏览器向服务器发起HTTP请求时,不管浏览器发来的是什么请求,都将这个网页响应给浏览器,此时这个html文件的内容就应该放在响应正文中,只需读取该文件当中的内容,然后将其作为响应正文即可
#include <iostream> #include <fstream> #include <string> #include <cstring> #include <unistd.h> #include <sys/wait.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <arpa/inet.h> using namespace std; int main() { //创建套接字 int listen_sock = socket(AF_INET, SOCK_STREAM, 0); if (listen_sock < 0){ cerr << "socket error!" << endl; return 1; } //绑定 struct sockaddr_in local; memset(&local, 0, sizeof(local)); local.sin_family = AF_INET; local.sin_port = htons(9090); local.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){ cerr << "bind error!" << endl; return 2; } //监听 if (listen(listen_sock, 5) < 0){ cerr << "listen error!" << endl; return 3; } //启动服务器 struct sockaddr peer; memset(&peer, 0, sizeof(peer)); socklen_t len = sizeof(peer); for (;;){ 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(0); } //孙子进程 char buffer[1024]; recv(sock, buffer, sizeof(buffer), 0); //读取HTTP请求 cout << "--------------------------http request begin--------------------------" << endl; cout << buffer << endl; cout << "---------------------------http request end---------------------------" << endl; #define PAGE "index.html" //网站首页 //读取index.html文件 ifstream in(PAGE); if (in.is_open()){ in.seekg(0, in.end); int len = in.tellg(); in.seekg(0, in.beg); char* file = new char[len]; in.read(file, len); in.close(); //构建HTTP响应 string status_line = "http/1.1 200 OK\n"; //状态行 string response_header = "Content-Length: " + to_string(len) + "\n"; //响应报头 string blank = "\n"; //空行 string response_text = file; //响应正文 string response = status_line + response_header + blank + response_text; //响应报文 //响应HTTP请求 send(sock, response.c_str(), response.size(), 0); delete[] file; } close(sock); exit(0); } //爷爷进程 close(sock); waitpid(-1, nullptr, 0); //等待爸爸进程 } return 0; }
实际在进行网络请求的时候,若不指明请求资源的路径,此时默认想访问的就是目标网站的首页,即web根目录下的index.html文件
由于只是作为示例,在构建HTTP响应时,在响应报头中只添加了一个属性信息Content-Length,表示响应正文的长度,实际HTTP响应报头中的属性信息还有很多
HTTP为什么要交互版本?
HTTP请求中的请求行和HTTP响应中的状态行,当中都包含了http的版本信息。其中HTTP请求是由客户端发的,因此HTTP请求中表明的是客户端的http版本,而HTTP响应是由服务器发的,因此HTTP响应中表明的是服务器的http版本
客户端和服务器双方在进行通信时会交互双方HTTP版本,主要还是为了兼容性的问题。因为服务器和客户端使用的可能是不同的HTTP版本,为了让不同版本的客户端都能享受到对应的服务,此时就要求通信双方需要进行版本协商
客户端在发起HTTP请求时告诉服务器自己所使用的HTTP版本,此时服务器就可以根据客户端使用的HTTP版本,为客户端提供对应的服务,而不至于因为双方使用的HTTP版本不同而导致无法正常通信。因此为了保证良好的兼容性,通信双方需要交互一下各自的版本信息
五、HTTP方法
GET方法和POST方法
GET方法一般用于获取某种资源信息,而POST方法一般用于将数据上传给服务器。但实际我们上传数据时也有可能使用GET方法,比如百度提交数据时实际使用的就是GET方法
GET方法和POST方法都可以带参
GET方法是通过url传参
POST方法是通过正文传参
从GET方法和POST方法的传参形式可以看出,POST方法能传递更多的参数,因为url的长度是有限制的,POST方法通过正文传参就可以携带更多的数据
当使用GET方法时,提交的参数会回显到url当中,因此GET方法一般是处理数据不敏感的。
若要传递的数据比较私密的话一定要用POST方法,并不是因为POST方法更安全,实际上GET和POST方法传参时都是明文传送,所以都不安全,但是POST方法更私密,因为POST是通过正文传参的,不会将参数立马回显到浏览器的url框当中的,所以相对更私密
六、HTTP状态码
常见的状态码,如200(OK),404(Not Found),403(Forbidden请求权限不够),302(Redirect),504(Bad Gateway)
Redirection(重定向状态码)
重定向就是通过各种方法将各种网络请求重新定个方向转到其它位置,此时这个服务器相当于提供了一个引路的服务
重定向又可分为临时重定向和永久重定向,其中状态码301表示的就是永久重定向,而状态码302和307表示的是临时重定向
临时重定向和永久重定向本质是影响客户端的标签,决定客户端是否需要更新目标地址。若某个网站是永久重定向,那么第一次访问该网站时由浏览器进行重定向,但后续再访问该网站时就不需要浏览器再进行重定向了,此后访问的直接就是重定向后的网站。而若某个网站是临时重定向,那么每次访问该网站时若需要进行重定向,都需要浏览器来完成重定向跳转到目标网站
//构建HTTP响应 string status_line = "http/1.1 307 Temporary Redirect\n"; //状态行 string response_header = "Location: https://www.csdn.net/\n"; //响应报头 string blank = "\n"; //空行 string response = status_line + response_header + blank; //响应报文
七、HTTP常见Header
Content-Type:数据类型(text/html等)
Content-Length:正文的长度
Host:客户端告知服务器,所请求的资源是在哪个主机的哪个端口上
User-Agent:声明用户的操作系统和浏览器的版本信息
Referer:当前页面是哪个页面跳转过来的
Location:搭配3XX状态码使用,告诉客户端接下来要去哪里访问
Cookie:用于在客户端存储少量信息,通常用于实现会话(session)的功能
Host
Host字段表明了客户端要访问的服务的IP和端口,当浏览器访问服务器时,浏览器发来的HTTP请求当中的Host字段填的就是该服务的IP和某个端口。但客户端不就是要访问服务器吗?直接访问即可,为什么客户端还要告诉服务器它要访问的服务对应的IP和端口?
因为有些服务器提供的是代理服务,即代替客户端向其他服务器发起请求,然后将请求得到的结果再返回给客户端。在这种情况下客户端就必须告诉代理服务器要访问的服务对应的IP和端口
User-Agent
User-Agent代表的是客户端对应的操作系统和浏览器的版本信息
譬如用电脑下载某些软件时,网页会自动展示与操作系统相匹配的版本,这实际就是因为在向目标网站发起请求的时候,User-Agent字段中包含了主机信息,此时该网站就会推送相匹配的软件版本
Referer
Referer代表的是当前是从哪一个页面跳转过来的。Referer记录上一个页面的作用一方面是方便回退,另一方面可以知道当前页面与上一个页面之间的相关性
Keep-Alive(长连接)
HTTP/1.0是通过request&response的方式来进行请求和响应的,HTTP/1.0常见的工作方式就是客户端和服务器先建立链接,然后客户端发起请求,服务器再对该请求进行响应,然后断开连接
但若一个连接建立后客户端和服务器只进行一次交互,就将连接关闭,就太浪费资源了,因此现在主流的HTTP/1.1是支持长连接的。所谓的长连接就是建立连接后,客户端可以不断的向服务器一次写入多个HTTP请求,而服务器在上层依次读取这些请求就行了,此时一条连接就可以传送大量的请求和响应,这就是长连接
若HTTP请求或响应报头中的Connect字段对应的值是Keep-Alive,就代表支持长连接
八、Cookie和Session
HTTP实际上是一种无状态协议,HTTP的每次请求/响应之间是没有任何关系的,但在使用浏览器的时候发现并不是这样的
比如当登录一次CSDN后,就算把CSDN网站关了甚至是重启电脑,当再次打开CSDN网站时,CSDN并没有要求再次输入账号和密码,这实际上是通过cookie技术实现的,点击浏览器中锁的标志就可以看到对应网站的各种cookie数据
这些cookie数据都是对应的服务器方提供的,若将对应的某些cookie删除,那么此时就需要重新进行登录认证了
cookie是什么?
因为HTTP是一种无状态协议,若没有cookie的存在,那么每当进行页面请求时都需要重新输入账号和密码进行认证
而HTTP不支持记录用户状态,那么就需要有一种独立技术来支持,这种技术目前现在已经内置到HTTP协议中了,叫做cookie
当第一次登录某个网站时,需要输入账号和密码进行身份认证,此时若服务器经过数据比对后判定你是合法用户,那么为了让你后续在进行某些网页请求时不用重新输入账号和密码,此时服务器就会进行Set-Cookie的设置(Set-Cookie也是HTTP报头当中的一种属性信息)
当认证通过并在服务端进行Set-Cookie设置后,服务器在对浏览器进行HTTP响应时就会将这个Set-Cookie响应给浏览器。而浏览器收到响应后会自动提取出Set-Cookie的值,将其保存在浏览器的cookie文件中,此时就相当于账号和密码信息保存在本地浏览器的cookie文件中
从第一次登录认证之后,浏览器再向该网站发起的HTTP请求当中就会自动包含一个cookie字段,其中携带的就是我第一次的认证信息,此后对端服务器需要对你进行认证时就会直接提取出HTTP请求当中的cookie字段,而不会重新输入账号和密码了
即在第一次认证登录后,后续所有的认证都变成了自动认证,这就叫做cookie技术
内存级别&文件级别
cookie就是浏览器中的一个小文件,文件里记录的就是用户的私有信息。cookie文件可以分为两种,内存级别的cookie文件,文件级别的cookie文件
将浏览器关掉后再打开,访问之前登录过的网站,若需要重新输入账号和密码,说明你之前登录时浏览器当中保存的cookie信息是内存级别的
将浏览器关掉甚至将电脑重启再打开,访问之前登录过的网站,若不需要重新输入账户和密码,说明之前登录时浏览器中保存的cookie信息是文件级别的
cookie被盗
若浏览器中保存的cookie信息被非法用户盗取了,那么此时这个非法用户就可以用cookie信息,以你的身份去访问你曾经访问过的网站,这种现象就是cookie被盗取了
比如不小心点了某个链接,这个链接可能是一个下载程序,点击后它就会通过某种方式把程序下载到本地,并且自动执行该程序,该程序会扫描浏览器中的cookie目录,把所有的cookie信息通过网络的方式传送给恶意方,当恶意方拿到cookie信息后就可以拷贝到它的浏览器对应的cookie目录中,然后以你的身份访问你曾经访问过的网站
SessionID
单纯的使用cookie是非常不安全的,因为此时cookie文件当中就保存的是个人的私密信息,一旦cookie文件泄漏隐私信息也就泄漏
所以当前主流的服务器还引入了SessionID这样的概念,当第一次登录某个网站输入账号和密码后,服务器认证成功后在服务端生成一个对应的SessionID,这个SessionID与用户信息是不相关的。系统会将所有登录用户的SessionID值统一维护起来
此时当认证通过后服务端在对浏览器进行HTTP响应时,就会将这个生成的SessionID值响应给浏览器。浏览器收到响应后会自动提取出SessionID的值,将其保存在浏览器的cookie文件中。后续访问该服务器时,对应的HTTP请求中就会自动携带上这个SessionID
而服务器识别到HTTP请求中包含了SessionID,就会提取出这个SessionID,然后再到对应的集合中进行对比,对比成功就说明这个用户是曾经登录过的,此时也就自动就认证成功了,然后就会正常处理发来的请求,这是当前主流的工作方式
安全是相对的
引入SessionID之后,浏览器当中的cookie文件保存的是SessionID,此时这个cookie文件同样可能被盗取。此时用户的账号和密码虽然不会泄漏了,但用户对应的SessionID是会泄漏的,相当于还是存在刚才的问题
之前的工作方式就相当于把账号和密码信息在浏览器中保存一份,每次请求时都自动将账号和密码的信息携带上,但是账号和密码一直在网中发送太不安全了
现在的工作方式是,服务器只有在第一次认证的时候需要在网络中传输账号和密码,此后在网络上发送的都是SessionID
虽然没有真正解决安全问题,但这种方法是相对安全的。互联网上是不存在绝对安全这样的概念的,任何安全都是相对的,就算你将发送到网络中的信息进行加密,也有可能被别人破解
若破解某个信息的成本已经远远大于破解之后获得的收益,那么就可以说这个信息是安全的
IP是有归类的,可以通过IP地址来判断登录用户所在的地址范围。若一个账号短时间内登录地址发生巨大变化,此时服务器就会识别到账号发生异常,进而在服务器中清除对应的SessionID。这时非法用户想要访问服务器时,就需要重新输入账号和密码进行身份认证
当操作者想要进行某些高权限的操作时,会要求操作者再次输入账号和密码信息,再次确认身份。就算SessionID被非法用户盗取了,但非法用户在修改密码时需要输入旧密码,这是非法用户在短时间内无法做到的
SessionID有过期策略,如SessionID是一个小时内是有效的。所以即便你的SessionID被非法用户盗取了,也仅仅是在一个小时内有效,而且在功能上受约束,所以不会造成太大的影响
string status_line = "http/1.1 200 OK\n"; string response_header = "Content-Length: " + to_string(len) + "\n"; response_header += "Set-Cookie: bjy19\n"; string blank = "\n"; string response_text = file; string response = status_line + response_header + blank + response_text;