1.HTTP协议
下面进入HTTP协议部分,先来谈谈简单的预备知识:
浏览器中输入的东西我们一般把它称为域名。根据我们目前学到的知识,客户端想访问服务端在技术上只需要知道IP和端口号就可以访问服务。实际上日常生活中我们并不使用IP地址,而是直接使用一个叫域名的东西,但本质访问时还是需要IP地址,只不过会把域名通过域名解析服务转为IP地址。怎么证明:
可以ping一下,ping是用来检测网络连通性的,可以看到域名有对应的IP地址。直接用IP也是可以在搜索框访问到的,但好像没看到使用端口号。我们把输的IP贴过来:
看到默认带了HTTP协议,有时是HTTPS。一般像HTTP或HTTPS这种知名的服务端口号在服务端一般要固定下来,HTTP所绑定端口默认是80,HTTPS绑定的端口是443。我们之前写网络套接字时说过,服务器必须得绑定一个端口号,客户端也要绑定,但客户端不需要由程序员自己指明端口,由系统随机绑定。所以客户端端口是多少不重要,能在我这个系统里区分客户端的唯一性就行。服务器都要把客户端口号指明出来,这个端口号一旦有了一般不能轻易修改,一旦改了所有客户端就找不到了。一般服务器上的端口号是众所周知的端口号,像这种常见协议浏览器自己就会知道,如同119每个人知道是火警,这样HTTP浏览器知道是80。因此只输入IP浏览器会默认添加端口,也可以IP:端口试一下照样可访问。我们平时可能网上看了个文章特别好,然后会复制文章链接分享给同学:
一般把分享的这个字符叫url,统一资源定位符,也就是说互联网中拿着这个字符串在浏览器中访问就可以找到分享的文章。我们用域名确定互联网中唯一一台主机,协议默认端口号可表示该主机上唯一的一个服务,后面的资源路径表示我可以找到相应的资源。所以在公网服务器上,每个资源都有唯一的字符串路径,这个资源路径是唯一的。因此所有网落上的资源,都可以用唯一的一个字符串标识,并且可以获取到。图片上面是一个完整URL的样子,登录信息一般已经很少见了,端口号因协议已指明所以可省略,第一个/一般是web根目录。平时我们网络行为一般是两种:1.把别人的东西拿下来。2.把自己的东西传上去。如果我们要把自己的东西传上去,URL中就可以把自己的信息携带上,存在如?后面有个uid=1。uid=1是个KV,代表这个请求的一些动态数据。最后总结一下:1.我们网络中访问某些资源的时候通过URL来定位某一个资源。2.网络中所有通过浏览器访问到的东西都称为资源。我们通信时URL里有很多特殊字符,如/@等,这些都是作为功能性的符号来用的。那我要提交一个特殊符号是否会混淆有问题呢:
发现aaaaa和bbbb之间剩下的符号变了一个样子,所以少量的情况,提交或者获取的数据本身可能包含和URL中特殊字符冲突的字符,要求BS(浏和服)双方进行编码(encode)和解码(decode),这样就没影响了(转移有相应的工具)。
我在浏览器上输域名回车拿到了网页,这个工作相当于向目标服务器发了一次HTTP请求。下面把HTTP请求和响应格式化出来,再用两个工具见一见http请求/响应的样子。下面谈一下HTTP的 request格式:
它是以行为方式来陈列请求或响应格式。HTTP请求是由多行内容构成,第一行叫请求行;第二部分叫请求报头,实际上由多行构成;第三部分叫请求正文,它不一定以行为单位,这个是用户要上传的内容,可以没有。这上面一行分割符都是行分割符,可以是\n或\r\n,每一行内容都是以\r\n结尾的。说个题外话,\r\n也是字符,画图时一行行画的,实际上TCP的接收/发送缓冲区里是一个大的字符串,只不过中间有特殊字符,这就是存的时候还是按字节一个个存的,打的时候是多行。下面说说请求行,请求行用行分割符和请求报头做区分,所以请求行里面不能再出现回车换行符。请求行所有字段一共分三部分:第一部分是请求方法,请求方法很多,但HTTP场景里95%请求方法只有两种:1.GET方法 2.POST方法。一个是获取方法,一个是上传方法,以空格做分割符。第二部分是URL,以空格做分割符。第三部分是请求的协议版本,最常见1.0、1.1、2.0:
第二大部分请求报头是由多行内容构成的,每一行都叫做HTTP请求的请求属性,大部分是key:Val键值对。key表示是什么属性,Value表示属性的值是什么:
如果包含了请求正文会存在一个重大的问题,怎么去区分整体大报头部分(包括请求行和请求报头)和正文部分?所以http协议做一个规定,在报头和正文之间加个空行,这是HTTP请求的第四部分:
我们一般进行HTTP请求或报头读取时,我们都一直按行读取,读到空行后前半部分是HTTP整体报头,后半部分是内容,第一行是请求行。曾经我们写代码在读取网络请求的时候,怎么保证自己读到的是一个完整的网络请求呢?我们通过calback,decode不断分析,我们自定义协议可以区分清楚。那HTTP协议怎么保证自己读到的是一个完整的报文呢?保证不了,但可以保证读到一个完整的HTTP的报头,因为读到空行就行。那怎么保证把正文读完呢?报头里有个属性是Cont-Length:xxx,表明正文部分的长度,这样根据属性可读完正文,这样上述一配合就可以保证读到一个完整的HTTP请求。
下面来谈谈HTTP的response,它和request的格式几乎一模一样。第一行称为状态行,结尾字符\r\n;再下一部分内容也是大量KV式内容;第三部分是响应正文,一般里面是html/CSS/JS/图片等各种资源:
同样的,当我作为一个客户端收到了response,怎么保证把整个response全部读完呢?保证不了,但响应这也包含空行:
这样空行前都可以读完,正文依照读取来读。可以保证读取下一步就是序列化,反序列化:
然后一来一回完成一次HTTP请求和响应。
下面先看看响应,先来介绍一个小工具抓一个response,telnet:
我们用80登陆,说明连接成功,连的ip也显示了。ctrl+]出现talnet提示符,要抓到百度首页需要构建一个最简单的请求,请求报头和正文都可以没有,空行和请求行必须得有。我们弄个简单的:用GET方法,/根目录一般是获取首页资源,HTTP/1.1把版本也要发过去。第一个回车没反应,这只是给请求行加了\r\n,没报头就在空行,此时相当于拿到了请求,然后我们得到了响应。这是response报头:
第一行是响应的状态行,空行前剩下的内容是HTTP的响应报头,都是KV的。我们看到的状态行也是分为三部分,用空格做为分隔符:第一个字段是HTTP服务端的版本,第二个字段是状态码,第三个字段是状态码描述:
服务端的http version常见的也是1.0,1.1,2.0,那客户端的和服务端要通告一下版本是什么意思呢?这样双方能够协商功能。状态码常见404,200,类似于当时我们定义网络版本计算机时定义个code,表明计算状态。码描述是对状态码字符中版本的描述,方便让用户知道什么原因失败。所以HTTP里面,哪怕请求的资源是失败的也要有响应,所以请求必有响应。
下面看看请求,我们要基于http做一次抓包,这里介绍个工具叫fiddler,它是一个可帮我们抓包的软件,下载启动以后可通过它发起请求抓到对应的报文。比如访问百度:
上面和下面对应请求和响应。Raw可看到最原始的(空格下加密了,HTTPS协议我们不管):
以前是我电脑上的浏览器直接去访问服务器,现在启动了一个fiddler,浏览器上发的所有请求是先交给fiddler,然后fiddler把请求包装一下发给服务器。服务器把响应返回给fiddler再返回给我们, fiddler相当于一种代理,所以可以抓到报文。还有个工具是模拟浏览器行为构建请求的,postman,比如发个请求,send后可抓到响应:
2.编写
下面写一个最简单的httpserver,用浏览器直接测试(前提要有网络功能):
创建好后启动。再然后:
至此得到了链接,接下来想办法处理。我们用多线程处理man 2 recv:
它可用来tcp读取,前三个和read一样,最后一个参数是读取方式,我们设为0,作用和read一样:
相当于我们收到它后把它读一次,打到显示器上关了就行,至此完成了简单的服务器。下面测试一下:
改一下:
可用浏览器IP:端口访问一下:
可看到收到的请求。再改一下:
读成功后返回一个响应(目前不能保证读到完整请求),构建一个简单的响应:
然后把手写的响应返回去,但不一定全能发过去,这忽略网络细节。这用man send:
返回值及前三个参数含义和write一样,选项设为0:
现在就把消息返回去了(可以用手机访问尝试)。测试一下:
telnet请求的时候服务器给的响应是这样子,浏览器上显示的是正文部分,上面的内容由浏览器做解释了。HTTP响应正文里通常是图片、网页等,我们这写了个字符串,我们其实可在正文部分写个简单的网页。网页不会怎么办?搜索W3Cschool html,它里面有html一些相关的教程,简单说说:
一般一个网页包含<!DOCTYPE html>,表明网页是一个新的标准h5。网页最开始有自己的标签<html>及最后的</html>,说明这是个完整的DOCTYPE文档。<head>和</head>表明是网页的头部信息,<body>和</body>携带网页的主体。还有一些具有特性的标签,如<h2></h2>作为大标题、一级、二级标签,<p></p>表明内容是一些文档。下面试一下:
现在发了这样的信息:
服务器在收到请求时,浏览器访问时想找到特定路径下的一些文件,所以url可以是图中样子的。那这里的\我们说过是web根目录,不一定是 Linux 根目录,这个该怎么理解?还有网页难道每次要静态编码写到服务器里吗?应该是独立的文件,需要访问哪个文件通过HTTP访问就行了。网页文件可不止一个,各种各样文件存在的话一定会有一个目录结构,这个目录结构可以整体被外部去访问。
综上下面开始做第一个事情:一个http协议一定要有自己根目录,可以是Linux根目录,也可以由自己去指定:
定一个目录,这个目录就是当前工作目录。在当前目录下新建个文件夹叫wwwroot:
将来把所有资源都放在这个目录里面,从这个目录开始让别人去访问,我们把这个目录叫做web根目录。当前有了一个网页:
下面把网页动态让服务器响应给浏览器:
服务启动后,又往网页加一些东西:
此时一刷新网页就更新了,不用关服务器。现在随便输个路径,返回的是同一个网页文件,但将来我们可能要不同的文件。现在要客户端说明去哪里,所以我们要分析一下请求:先把整个请求的第一行读到,按空格分隔符把URL提出来,拿到对应的目录,拼上web根目录去读取,若不存在返回404:
假设我们读到一个完整的HTTP请求,接下来把对应字符串打散按行放到req_header,把正文放test里,这样更容易获取里面的字段。接下来进行完善:
这样反序列化工作完成。为了方便再做一下Debug(为了方便暂时注释响应):
下面测试一下:
看到了提取内容。我们要的是url,HttpRequest里再填一些变量:
下面介绍个分割字符串的另一个方法,stringstream:
这样就分开了。读到后反序列化,然后提取:
下面继续测试:
看到成功打散。我们只拿IP和端口访问时,默认URL是根目录。如果只请求根目录,总不能把根目录下的所有内容返回,一般只返回整个网站的首页,这个首页就叫index.html。因此改一下代码:
解析完判断一下:
接下来把对应网页文件打开,然后把要访问资源读取,然后构建响应后send回来:
下面测试一下:
现在就可以访问很多资源了:
通过指定路径可访问wwwroot的任意合法资源了。可我们访问网页的并没有一个个输入路径来访问,都是上一页,下一页,从一个连接跳到另一个,并没有改过url。 有个a标签:
buffer后可以跟上要连的url:
这样可以跳转了。
3.研究http细节
下面开始研究HTTP的细节字段,先看HTTP的方法:
目前我们想获取资源请求大多是GET方法。GET方法作用是获取资源,就是把远端的资源获取下来,基于HTTP协议把别人的网页获取下来。因为浏览器支持HTTP协议,所以用的大部分方法都是GET方法。还有种方法是POST,也可用来获取网页,但更多是要上传我们的东西(剩下方法了解就行,见得少)。如果服务器启动之后,通过浏览器向服务器提交数据就使用POST方法,那我们平时是如何向服务器提交数据的呢?(比如打开面板有个搜索框)数据都是通过表单提交的,开发工具前端网页里有个叫from的东西是from表单,input表示输入框,button对应按钮,还有输密码的输入框。我们想向我们服务器提交参数也需要from表单:
(网页注释<!--注释-->)下面完成一下:
from表单里有个action,后面跟个路径,表示想把表单提交给服务器中哪个资源当中。 method表示这个表单想以什么方法提交。input是输入框;type表示输入框所对应的类型,这是文本类型;name="..."转化为这样:
value是缺省值可省掉:
下面测一下:
我们用的提交方法是get方法:
所以如果我们要提交参数给我们的服务器,我们使用get方法的时候,我们提交的参数是通过url提交的,看到提交的内容拼到了url上。作为服务器来使用?作为分隔符,左边是访问的资源,右边是参数,参数交给资源。
下面把提交方法由get改为post:
启动服务访问后看到表单提交方式是post:
输入后提交:
看到了我们提交的HTTP,它的请求方法会在提交表单时由浏览器自动帮我们构建HTTP请求,请求方法是post,提交路径是/a/b/hello.html这个资源路径,参数放在空行之后。看一下抓的包(raw):
所以post方法也支持参数提交,采用请求的正文提交参数,更私密一些(url上不显示)。get方法通过url进行提参,参数数量受限,不私密(url上可以看到)。其实都谈不上安全,想安全得加密。
下面说说HTTP的状态码:
一般HTTP在请求的时候会得到响应,响应会包含状态码,在状态行的第2列,是个数字,常见的 1~5数字开头的状态码。下面把代码改一下,模拟返回 404,一定是经过序列化,再经过parse,提上来资源路径后本身并不存在:
下面再次测试:
这也是一个响应。再说一下3开头的,有2种重定向方式,分别是永久重定向和临时重定向。我们先看一下重定向的效果,我们使用一下302,但还要了解一下HTTP报头属性中的Location属性:
看图感受一下:
现在向目标服务器发起了请求,服务器可能是老版本服务器不想再给浏览器提供服务了,此时返回了个响应。这个响应报头状态码写的3xx, 响应报头中location: 后跟新的地址。相当于浏览器向别人发起请求,它因某种原因不能给我们提供服务,它告诉我应该去哪里访问资源。此时浏览器发起2次请求,重新构建HTTP请求去访问新的地址。所以重定向功能是让服务器指导浏览器,让浏览器访问新的地址。下面改一下代码体会:
访问时就跳到qq了。我们正常向别人发起请求,响应时Location指明了新的地址:
4.谈谈http请求的属性
下面谈http请求的属性:
Content-Length表明正文部分长度,一般在请求和响应里都会携带。Host代表请求的目标主机是谁。User-Agent用来识别客户端的软件信息。refer表示上一次从哪个页面来的,这样可配合前进一个页面。Connection:keep-alive,下面解释一下:我们代码是处理完请求后就把链接直接关了,意味着HTTP请求是请求了一次。我们平时见的网页有无数的图片构成,一个巨大的页面是会包含非常多的元素的,每一个元素就是一个资源。比如网页上包含了100张图片,我们要把整个网页让用户看到可能要发起101次请求。第一次是把网页本身请求到,剩下的是浏览器基于每张图片都要发起HTTP请求。 HTTP协议底层是基于TCP的,TCP是面向链接的,每次发起HTTP请求交互 request和response前提条件是浏览器发起套接字基于TCP的connect连接建立请求。按照我们写的想要获取完整要建立101次链接,每次建立好链接资源返回去链接就断开了,这样方案非常低效。基于一次请求,响应一个资源,关闭链接,我们称为短链接。后来HTTP协议的设计者就想TCP就是双向建立好的通信信道,缓冲区在操作系统内,以前是建好通信信道后基于连接请求一次再响应一次,再把链接关了,下次再访问重复这个动作:
现在变成在发送端同时构建好多HTTP请求,然后把这么多请求按串行化全部都发给服务器,然后一个个处理,不断给客户端给响应:
这里只建立一个TCP链接,可发送和返回多个HTTP的request和response。本轮请求全部完毕,该链接才断开,这种我们称为长链接。 HTTP/1.0支持的是短链接,HTTP/1.1支持的是长链接。通信双方在建立HTTP请求时,双方要协商HTTP版本看双方是否支持长链接技术,都支持也可不用。Connect选项表明的是双方通信时是否选择长连接,客户端和服务器Connection都填上keep-alive才会采用长链接的方案来通信,否则短链接。
下面让我们网页把图片也显示出来,一旦请求首页我们应该看到向服务器发起了很多次请求,现在让我们首页也包含很多图片:
现在有两个图片文件,所有图片资源必须全在web根目录及Linux服务器下。现在服务器上要把图片文件拿上来:
想在首页支持图片,首页里要包含h5标签把图片包进来。图片也是文件,想看图片先获取网页,根据网页标签语言继续发起请求拿图片。下面改代码(img表示图片,若挂了显示alt文字,根据src向我们的服务器,浏览器自动发起二次请求):
注释重定向到qq:
下面测试一下:
有时发现请求完了,但会发现什么都没显示。因为HTTP在这要发起多次请求,刚开始请求的是首页,后来在标签语言驱使下由浏览器发起二次请求。请求image/1.jpg时要携带对应的正文,但图片是二进制的,得告诉浏览器图片是什么格式的,什么类型的图片,这样浏览器才好进行显示。所以我们这里需要有一个报头叫Content-Type:
这是Content-Type对照表。我们请求的资源非常多,怎么知道请求什么类型的文件?我们请求的资源在file-path中:
可根据文件的后缀来。所以我们这样:
现在读取请求时已知道资源后缀是什么,还维护了一个简单的映射表。继续完善:
下面测试一下:
成功拿到了Content-Type。但是浏览器访问依旧没有显示图片,因为按字符串方式读二进制可能把图片读漏了,所以再改一下:
预先获取一大部分的空间,强转一下把内容放到空间中,拼接到响应后返回,现在访问就可以了。还可设一下大小:
再来试一下,请求的是首页,它包含的资源也会自动请求:
还有个Cookie,我们比如访问B站时请求第一个网页要登录,请求第二张网页时怎么知道我们曾经登录过呢? HTTP协议默认是无状态的。下面说说HTTP对登录用户的会话保持功能:
第一次访问要登录,输入账号和密码,认证完后进行响应。响应中报头里会有Set-Cookie选项,里面有用户名和密码。浏览器收到响应后会在内部把cookie内容保存起来,把用来保存的文件一般称为cookie文件。从此浏览器继续访问同一个网站时,每一次请求部分携带cookie文件中的内容,用户名和密码。所以每请求任何一个b站网页都可做协商,身份认证,因此不用再输入了。浏览器有两种保存cookie文件的方法:1.文件级。2.内存级。若是内存级,进程内是可new空间的,此时不关浏览器可一直访问,关了再打开又要登录。若是文件级,这样即便关机后再访问也不用登录。下面我们做个cookie:
下面测试一下:
看到cookie里有了内容,再次刷新cookie也是一样内容。
下面继续实验证明Set-Cookie这个选项允许服务器向浏览器中进行数据写入的(&&这是请求中的分隔符):
测试一下:
看到cookie确实写到了浏览器中,往后请求都会带上cookie选项交给服务器。想写多个cookie向浏览器就用多个cookie选项:
这样多个cookie就被写入了。了解上述说个题外话,比如电脑不小心被黑客种了病毒,在电脑中把 cookie文件偷走了,这样两方比如都使用HTTP协议访问同一个服务就都可登录,这样下来我就是你,因为你访问的资源我也可访问。因此涉及两个问题:1.cookie被盗取的问题。2.个人私有信息泄露。先谈谈第二的解决方案:
首次访问输入账号和密码,b站进行认证,认证成功后在服务器端为我们创建一个session文件,session里保存的是用户合法信息与登录相关内容。服务器器形成session文件的同时还要给它生成一个全服务器内唯一的一个序号sessionID, 然后进行响应时调用set-Cookie把当前用户的session ID写回到浏览器的cookie文件。浏览器再次发起请求携带cookie字段里是session ID, 只不过session ID在整个站内唯一的,认证ID来比对,不存在就跳到登录,这样私人信息就不泄露了。再说第一个问题:Session ID是服务器统一管理分配的,检测到不对会删了session信息重新登录。