目录
HTTP(超文本传输协议)工作在应用层,是一个简单的请求-响应协议,它通常运行在TCP之上。它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。
认识URL
- http:// —>协议名
- URL—>服务器ip地址+
:
+端口号;标志着我们服务器进程的位置(可和相关域名绑定让用户更容易记忆) - /路径—>网页根目录(一般设置为wwwroot)下的文件,默认为index.html
HTTP协议格式
HTTP请求格式
首行: [请求方式] + [url] + [协议版本]
Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束
Body: 空行分隔符后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个
Content-Length属性来标识Body的长度;
HTTP响应格式
首行: [协议版本] + [状态码] + [状态码描述]
Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束
Body: 空行分隔符后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个
Content-Length属性来标识Body的长度; 如果服务器返回了一个html页面, 那么html页面内容就是在
body中.
实现一个简单的HTTP服务器
这里不用实现客户端了,我们可以用手机/电脑/平板的浏览器直接通过http协议访问我们的server;
http_server.hpp
#pragmaonce#include<iostream>#include<sys/socket.h>#include<unistd.h>#include<sys/wait.h>#include<cstdio>#include<netinet/in.h>#include<arpa/inet.h>#include<strings.h>#include<pthread.h>#include<string>usingnamespace std;//方便制订响应报文使用#defineHTTP_VERSION"HTTP/1.0"#defineHOME_PAGE"wwwroot/index.html"#definePAGE_ERROR"wwwroot/404.html"classHttpServer{int listen_sock;int _port;public:TcpServer(int port):_port(port){}voidInit(){
listen_sock =socket(AF_INET, SOCK_STREAM,0);
sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port =htons(_port);
server.sin_addr.s_addr = INADDR_ANY;if(bind(listen_sock,(sockaddr *)&server,sizeof(server))){
cerr <<"bind error"<< endl;exit(1);}//服务器初始化完成,进入监听状态;listen(listen_sock,5);}staticvoid*fun(void*p)//类里搞多线程的执行函数毕需static,不然会多一个参数导致匹配不上;{int sock =*(int*)p;delete(int*)p;//接收请求部分char buff1[4096];
buff1[0]=0;//清空;
cout <<"###################################"<< endl;int c =recv(sock, buff1,sizeof(buff1),0);//接收数据,类似于read, 少了一个反序列化(将所收数据转成对应的类型录入)if(c >0){//打印http请求信息;
cout << buff1;//请求信息里格式很完善,不用多打印\n了;}
cout <<"###################################"<< endl;//发回响应部分---需按照http协议要求首行+报头header+\n+正文body(html文件)发回给浏览器;
string response;//响应报文;
string body;//存html的全部信息;
FILE *f =fopen(HOME_PAGE,"r+");if(!f){
cerr <<"open error"<< endl;}else{//打开成功//存储对应html的正文body部分char buff2[4096];
buff2[0]=0;//清空;while(fread(buff2,1,4096,f)){
body+=buff2;}//首行
response = HTTP_VERSION;
response +=" 200 OK\n";//报头
response +="Content-Type: text/html\n";
response +=("Content-Length: "+to_string(body.size())+"\n");//空行
response +="\n";//空行,用来区分报头和有效载荷//正文
response += body;}//发送响应send(sock, response.c_str(), response.size(),0);fclose(f);close(sock);returnnullptr;}voidLoop(){while(true){
sockaddr_in peer;
peer.sin_family = AF_INET;
peer.sin_port =htons(_port);
peer.sin_addr.s_addr = INADDR_ANY;
socklen_t len =sizeof(peer);int sock =accept(listen_sock,(sockaddr *)&peer,&len);//连接到client的socket;int*p =newint(sock);//防止局部sock销毁导致找不到sock,搞堆上;
pthread_t tid;pthread_create(&tid,0, fun, p);pthread_detach(tid);//线程分离,直接accept下一个client的连接,实现并发}}~TcpServer(){if(listen_sock >=0)close(listen_sock);}};
http_server.cc
#include"http_server.hpp"intmain(int argc,char*argv[]){
HttpServer svr(atoi(argv[1]));
svr.Init();
svr.Loop();return0;}
通过pc端Chrome浏览器访问结果:
请求报文
可以看到http的请求会被分为三部分;
- 红框部分为第一部分请求行: [方法] + [url] + [版本]
- 黄框部分为第二部分请求报头:请求的属性, 一组组冒号分割的键值对 ;每组属性之间使用\n分隔;遇到空行表示Header部分结束;
- 紫框为空行,其下面的部分为第三部分body:请求正文部分,Body允许为空字符串。如果Body存在,那么在Header中会有Content-Length的属性来标识Body的长度。上述请求没有body存在;
其中,空行的存在是作为一个特殊符号,来表明HTTP请求报头的结束。
网络基础讲过,每一层协议都需要能够将自己的**报头**和**有效载荷**分离(即解包),那么这里的空行就是分离的特殊符号依据,header是报头,body是有效载荷;
HTTP协议规定的请求报文格式如下,方便server获取需求:
**
**
响应报文
浏览器获得响应,打开网页:
HTTP协议规定的响应报文格式如下,方便浏览器处理并显示网页;
可以看到,响应报文与请求报文同样分为三部分;
- 首行: [版本号] + [状态码] + [状态码解释]
- 响应报头:响应的属性, 一组组冒号分割的键值对 ;每组属性之间使用\n分隔;遇到空行表示Header部分结束;\
- Body:
空行下面部分
,如果服务器返回了一个html页面, 那么html页面内容就是在 body中,浏览器能识别body部份直接打开网页;
因此,所谓的HTTP协议本质就是对发过来的HTTP请求字符串进行文本解析。
HTTP常见的报头属性
- Content-Type: 数据类型(text/html等)
- Content-Length: Body的长度
- Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
- User-Agent: 声明用户的操作系统和浏览器版本信息; eg:Chrome,ios,linux,安卓等
- referer: 当前页面是从哪个页面跳转过来的;
- location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
- Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能;
HTTP请求的方法
请求方法会在请求报文的首行第一个单词显示出来,其中最常用的就是GET方法和POST方法;
模拟实现GET方法
网页中添加表单,设置POST相关属性;
<body><h1>Hello World</h1><imgsrc="https://img-blog.csdnimg.cn/09789a1db9454ce39ae573b65865488e.png"/><formaction="/dir/get"method="GET">
搜索:<inputtype="text"name="content">
//提交按钮
<inputtype="submit"value="submit"></form></body>
响应报文:
可见,GET方法输入内容提交以后,浏览器上方和服务器收到的请求报文中都能看到刚才提交的内容;
模拟实现POST方法
<body><h1>Hello World</h1><imgsrc="https://img-blog.csdnimg.cn/09789a1db9454ce39ae573b65865488e.png"/><formaction="/dir/post"method="GET">
account:<inputtype="text"name="用户名"><br>
password:<inputtype="passworld"name="密码"><br><inputtype="submit"value="submit"></form></body>
响应报文:
可见,浏览器上方和响应报头里的referer都不会出现输入内容了,而是出现在了报文的body里;
那么,不管是GET还是POST方法,对于服务器来说 数据都是可见的,则两种方法都是不安全的(被黑客截取捕获等等),要保证安全性就得加密;
POST相较于GET的优点在于:
- POST可以保证用户提交时数据的私密性,在提交时用户数据不会回显。
- POST提交的数据最终在服务器端的正文部分,因此提交的数据可以很大。
POST和GET两种方法相比:
- 作为HTTP请求方法,GET和POST两种方法都不安全,因为二者默认情况下都没有加密。
- GET通常用于网页等资源的获取,但也可以提交数据,比如搜索引擎。
- POST通常用于提交数据,并且能保证数据更加私密(输入密码后客户端不会直接回显暴露出来)。且POST请求不会被缓存,并且对数据长度没有限制。
HTTP响应的状态码
状态码会在响应报头首行的第二个单词显示出来,紧接着第三个单词是相应描述;对应状态码和对应的描述都是http协议制订的规定;
我们已经测试了200 OK正常的响应状态,接下来模拟一下404 Not Found的服务器段找不到网页的错误状态;
404 Not Found
客户端错误状态码,服务器无法处理请求;
//模拟实现404 Not Found
string response;
string body;
FILE *f =fopen(HOME_PAGE,"r+");char buff2[4096];
buff2[0]=0;//清空;int flag =1;if(!f)//打开失败,打开404 NOT FOUND{
f =fopen(PAGE_ERROR,"r+");//设计好的404页面;
flag =0;}else{//打开成功}while(fread(buff2,1,4096,f)){
body+=buff2;}
response = HTTP_VERSION;//状态码 状态码解释if(flag)response +=" 200 OK\n";else response +=" 404 Not Found\n";
response +="Content-Type: text/html\n";
response +=("Content-Length: "+to_string(body.size())+"\n");
response +="\n";//空行,用来区分报头和有效载荷
response += body;send(sock, response.c_str(), response.size(),0);
上述代码功能:打开默认文件wwwroot/index.html失败的话,则默认打开404.html;然后我们把index.html暂时删掉;
浏览器获得响应报文:
浏览器显示:
307 Temporary Redirect
临时重定向
假设我们的原域名(ip:端口)维护,想让接下来访问的用户跳转到另一个域名,那么就在报头添加location:xxx属性;
//模拟实现307(Temporary Redirect)临时重定向
string status_line = HTTP_VERSION;//状态行
status_line +=" 307 Temporary Redirect\n";//状态码 状态码解释
string response_header ="Content-Type: text/html\n";
response_header +="location: https://www.baidu.com/\n";//307 Temporary Redirect
response_header +="\n";//空行send(sock, status_line.c_str(), status_line.size(),0);//tcp流式传输,可以分多次;send(sock, response_header.c_str(), response_header.size(),0);
浏览器获得响应报文:
此时,响应报文就不用body了,因为已经重定向了;
浏览器结果:
输入的是我们的服务器地址,浏览器发现location:baidu.com后,直接跳转到百度首页;
去掉重定向报头属性后,恢复正常:
HTTP的特点
针对第1点:
http是应用层协议,tcp是传输层协议,在底层建立好tcp连接以后,http才会直接的发起http request,不需要再连接;
针对第2点:
http本身是无状态的,但是与日常网页使用相悖,比如视频网站登陆一次之后会自动登录,不需要再输入账户信息,相关知识接下来的cookie&&session;
针对第3点:
我们上面实现的简易http服务器就是基于短链接的,即服务器端的线程处理完一个来自客户端的请求后,该线程就被线程池回收,请求便被关闭了;
基于长连接的http协议减少了请求和连接的时间,但是每次交互都是由客户端发起的,客户端发送消息,服务端才能返回客户端消息。因为客户端也不知道服务端什么时候会把结果准备好,所以客户端的很多请求是多余的,因此长链接其实弥补短链接多可能造成并发多的一种补足方式。
cookie&&session
上述HTTP是无状态的,那么理论上意味着用户每次打开一个网站看vip私密信息等都需要登录一下账户,这样无疑是非常麻烦的;
http协议为了提升用户体验,引入了cookie与session通过浏览器的协助来达到保持状态的效果,提升用户体验;
cookie
cookie是保存在本地的数据,浏览器一般会指定路径集中储存它们,那些就是cookie文件。
查看方法:
进入网页,点击地址栏开头的锁;
一般cookie都有到期时间,会自动清除,届时需要再次手动登入账户;
一般存在两种cookie文件:
- 文件级:eg视频网站,登陆一次即便是关机重启,一段时间内再次登录cookie还在,自动登入服务器;(居多)
- 内存级:eg某些小网站,登陆一次只要不关掉网页,继续打开主站下其他关联网页会自动登入;但如果关掉网页或者重启,下次又得键入账户信息手动登入;
cookie工作原理
验证cookie
在上文创建的建议http_server添加报头:
response_header+="Set-Cookie:password=123456abc";//报头添加cookie属性
那么这个cookie可以通过其他属性eg:Path等,进行指定路径等一系列操作传入server端进行验证,验证通过则登录该账户,就相当于自动登录了;
cookie安全性问题
这是软件捕获的请求报文:
很明显,直接将信息打包成cookie处理的话会有很高的信息泄露风险,大多数文件形式的cookie存储于本地终端,如果不慎电脑中病毒木马用户的账户名和密码将会直接被读取,则cookie配合session来进行会话保持会安全一点;
session
session是配合cookie通过server服务器提高账户安全性的一种做法;
session工作原理
在我们登录一次网页以后,server端验证成功后会记录登录信息,创建保存一个sessionID来标识该用户,并且返回给我们的client浏览器的cookie(value)中保存;
这个sessionID是唯一的,可以当作一种随机密文来看待,下次我们打开该网页,浏览器会自动把sessionID发往server进行验证,进而自动登录;\
response_header+="Set-Cookie:session=id123456";//报头添加cookie属性
使用这种方式的好处是,即便数据泄露,也泄漏的是该sessionID,他是server端创造的标识ID,而用户的账户以及密码等个人信息并不会被泄露;(但也无法避免黑河用这个session进入相关网页获取私密资源)
除了cookie:session这种配合方式外,还有许多物理方式比如快速异地ip登陆需要重新输入账号密码验证等方式保护个人账户;
HTTPS协议
上述即便引用cookie与session配合的技术,那也只是保证用户的私密信息不容易被泄露了,但是还是不能保证用户的帐户安全,黑客仍可以截取你的cookie:session,虽然读不出有效信息,但是可以冒充你的身份与服务器交互,从事违法活动;eg:qq号被盗;
就如同HTTP协议得POST请求一样,虽然和GET相比,敏感信息不会显示在脸上了,但是还是会在报文中出现,被别人截获,相当于交互的数据在互联网中裸奔;
因此HTTP协议还是不安全的,下面引入https协议!;
HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
其中涉及一些密码学相关知识,我们肤浅理解;
准备知识
上图红色箭头代表基于HTTP协议的交互,粉色箭头代表基于HTTPS协议的交互,可以看到,HTTPS协议相对于HTTP协议引入了SSL/TSL加密解密机制;
对称加密:加密解密用同一密钥,优点效率高,缺点不安全;
非对称加密:两把密钥,加密用一把,解密用另一把,优点安全,缺点效率低,一班会将公钥公开;(a(ab) = b相当于用a解密得结果b,异或运算其实类似于简易的非对称加密算法)
HTTPS工作原理
因此我们知道,虽然对称加密效率高,但是第一次建立通信无法使用对称加密;因为使用对称加密需要双方都知道该加密算法的公钥;
但是非对称加密效率又不高,多次交互大大影响效率;
因此https协议采用:首次交互非对称加密,之后交互采用对称加密;
- server首次使用将自己的公钥直接发给client;
- client收到server公钥,并产生双方之后使用的对称密钥,用收到的server公钥加密刚产生的对称密钥之后,然后发回给server端;
- server端用自己的私钥解密client加密的对称密钥信息,拿到对称密钥,之后通信就使用对称加密(会话密钥/对称密钥)的方式;
这样在建立真正通信前的一系列操作,相当于逻辑上给server和client建立了基于对称加密的一条安全传输管道,只需要第一次交互采用非对称加密,既保证了安全性,又保证了效率;(有攻就有防,我们说的安全都是相对的,世界上没有绝对的安全,真正的网络安全技术要复杂和重要得多)
上面是大致工作流程,但是还是存在安全问题,如下图,如果中间加入了一个黑客,截获了server的公钥,并把自己的公钥发给client,之后client用黑客的公钥加密的对称密钥(会话密钥)就会被窃取,同时黑客用截server公钥再次加密发回server,神不知鬼不觉地存在了中间人,双方走对称加密的内容就会被完全掌握;
引入第三方CA证书认证过程
该过程主要目的是使client相信收到的公钥就是server的公钥,而没有被篡改;
(操作系统和浏览器出厂时会默认内置各种权威机构的各种证书公钥!)
CA证书认证机构会走一系列流程为server颁发一个数字证书,该整数中含有两部分重要信息:
- 证书信息(server域名,身份,IP,你的公钥,颁发时间等);
- 通过证书的私钥,对证书内容的摘要,进行加密,生成防篡改证书指纹;
接下来server向client发公钥的时候,直接把数字证书发来(证书里包含server公钥);
client端信任server公钥没有被篡改的过程
- client收到server的数字证书,首先用CA机构的公钥对证书指纹解密,如果成功解密代表改指纹没有被动过,因为这个密文是当初CA机构用自己的私钥加密来的,只能用CA自己的公钥解密;(假设有人替换了的证书签名(中server的公钥),那么CA的公钥解不出来东西;这样client会及时发现,代表不安全就中断交互了)
- 将刚才证书指纹解密的内容(server机构加密过的原始证书信息)与server发来证书上的证书信息做对比,如果两部分信息吻合一致,则代表证书中的证书信息(中server的公钥)在传输途中没有被更改;(假设被更改,则更改后的信息与证书原始签名解出来的信息会不一样!这样client会及时发现,代表不安全就中断交互了)
client最终信任了收到的公钥就是server的公钥以后,才会把自己产生的对称密钥通过改公钥加密然后发回给server;
这样就可以保证世界上第一个知道该对称密钥的人是我自己client(我产生的),第二个人是server(我用他的公钥加密了,只有他的私钥才能解密拿到该对称密钥)!
之后就可以进行愉快的对称加密(会话密钥)交互了;
顺便提一句,黑客把自己的证书掉包发给client不也可以拿到对称密钥吗?
答案是:client会发现,别忘了 证书内容里有我是谁 ip 域名等,不光只是我的公钥;
关于上述加密解密等密码学的技术我们先不研究!;
版权归原作者 谜一样的男人1 所有, 如有侵权,请联系我们删除。