【Linux】应用层协议:HTTP

URL

在之前的文章中我们实现了一个网络版本的计算器,在那个计算器中揉合了协议定制以及序列化反序列化的内容,我们当时也自己定制了一套协议标准,比如请求和响应的格式应该是什么?如何读到一个完整的报文?支持的运算符有什么?等等我们都有自己的标准。

那么有没有其他大佬针对应用层的某些使用场景,已经提前给我们写好了协议软件呢?有,这个协议就是HTTP协议,我们当时的协议仅仅是针对计算场景所设计的,而HTTP协议主要是针对web场景所设计的。

虽然到现在我们还没真正的接触HTTP协议的具体内容,但我们现在已经可以知道,HTTP中一定有网络套接字编程,序列化反序列化,以及HTTP要进行的自己的业务逻辑,而这三个方面实际和我们当时的计算器相同,都是分别对应OSI上三层模型,分别是会话,表示,应用,HTTP的业务逻辑一般主要是电子邮件的发送,远程登陆,文件传输等......

URL听着比较高级,但他其实就是我们平常所说的网址,红色下划线是域名,这个域名等价于IP,用于标识一台主机在全网中的唯一性,域名实际还会做解析,解析之后就是服务器的IP地址,用域名不用IP主要是因为域名用起来方便,域名会和特定的IP地址做映射,域名后面的就是访问资源的路径,/是web根目录,这个根目录可以是Linux下的任意一个目录,/后面的weixin_65660590是向服务器请求的资源,?后面的是请求资源时所匹配的参数。HTTPS是在HTTP的基础上增加了一层加密层,这个协议后面会讲。

在URL里面我们并没有看到port,这是怎么回事呢?其实是因为浏览器自动忽略了服务器的端口号,但真正在发送url进行请求的时候,还要将端口号填充到URL里面,那浏览器怎么知道自己要访问的服务器的端口号是多少呢?浏览器其实是通过我们的协议来知晓服务器的端口号的,一般HTTP协议对应服务器的端口号是80,HTTPS对应的服务器的端口号是443.

服务器端口号大部分情况下都可以省略,因为协议和端口号是强绑定的!所有的网络服务都有对应的明确的端口号,这些是已经确定好的。

而我们所有的网络行为无外乎就两种,一种是通过HTTP协议从服务器拿下来对应的资源,一种是通过HTTP协议向服务器上传你本地的资源。

你在网络中看到的音频,视频,网页,图片等等,都是服务器上的文件资源,客户端看到的这些资源实际就是服务器对应返回的响应结果,因为文件资源的种类很多,文件后缀就很多,但这些文件的传输HTTP协议都能搞定,比如音乐,视频等文件都能传输,所以HTTP叫做超文本传输协议,不仅仅只能传文本,还能传其他很多种类的文件资源。

像我们平常刷抖音,听网易云实际就是抖音的服务器将他内部的视频传输到我们客户端,或是网易云的服务器将音乐传输到我们的客户端,而我们远程登录,在浏览器中搜索某些东西等行为就是将信息提交到远端服务器,将账号密码或搜索的关键字提交给服务器。

在url中像// ? : @ #等字符已经被当作特殊意义处理了,所以如果你要搜索的内容中出现了这些特殊意义的字符,则他们会被编码化也就是urlencode化,除这些已经被使用的特殊字符外,汉字也会被urlencode化。

一般来说网页URL只能使用英文,数字,还有一些特定的字符等可以不经过编码直接用于URL,其他的字符都必须先经过urlencode编码才能用于URL,否则传给服务器的request URL会包含乱码,服务器无法正确识别URL,自然就无法确定你要访问什么样的资源,这样就会出现错误。


HTTP协议格式

HTTP请求和响应的格式

HTTP请求分为请求行,请求报头,空行,请求报文这4个部分,其中请求行又以空格作为分隔符,由请求方法 URL 协议版本三部分组成,HTTP这里最常用的两个请求方法就是GET和POST,后者可以保证用户数据的私密性,这个我们后面会讲,常用的协议版本是1.1,除1.1外还有1.0版本。

request的Header报头中是以行为单位的HTTP请求的各种属性,每行都是由name 冒号 空格 value \r\n组成,接下来是空行,空行之后就是正文内容body,请求正文可以为空,如果你只单纯的想从服务器上拿资源到本地的话,请求正文可以为空,如果你想向服务器上提交一些内容,比如提交账号和密码进行登录,又或是提交一些搜索时需要的关键字进行相关网络内容的搜索,这些信息就可以放在请求正文body中。

HTTP响应分为状态行,响应报头,空行,响应报文这四个部分,其中状态行又以协议版本,状态码,状态码描述组成,常见的状态码有200 404 502等等,响应报头的格式和请求报头一样,内容包含响应内容的各种属性,接下来是空行,之后就是响应正文,相应正文就是客户端请求的资源内容,所以响应正文内容可以是html网页,图片,视频,音频等各种资源。

HTTP协议也会遇到我们当时定制网络版本计算器协议时所遇到的问题,比如你怎么保证读到一个完整的请求或响应报文呢?

其实很简单,因为首行和报头都是以行作为分隔符的,所以只要while循环一直按行读取,直到读取到空行为止,这样就读取完了首行和报头,而报头中会有一个字段Content-Length表示正文内容的长度,则读取空行结束后,我们只需要再读取Content-Length长度个字节的内容即可读取一个完整的报文。

HTTP的请求和响应怎么做到序列化和反序列化呢?这个问题也很简单,HTTP并没有使用json protobuf xml等方案来实现序列化和反序列化,因为根本没那个必要!如果要进行序列化,则从第一行开始,将首行 报头 空行 正文等内容,以\r\n作为分隔将每行字符串进行拼接,拼接成一个大字符串,然后直接发送即可。反序列化则按照以\r\n作为分隔符,读取每个小字符串的内容即可。

所以HTTP的序列化和反序列化实际就是通过特殊字符\r\n来将每个内容分隔开来的。

如果正文的内容是图片 音频 视频等文件,则他的序列化和反序列化工作一般不考虑,直接按照二进制发送就行。

打印出http请求报文(了解一下request的各个字段)

下面是HTTP服务器的代码,服务器的代码和最开始我们写TCP套接字编程的代码没太大区别,首先服务器初始化时,要传递一个回调函数func,这个函数是HTTP的业务逻辑处理函数,并且服务器运行后,子线程执行的函数也发生了改变,我们让主线程负责会话管理,监听来自客户端的连接请求并受理,而孙子进程负责给客户端提供对应的HTTP服务,执行HttpHandler方法。

在HttpHandler里面,首先要保证读取一个完整的请求报文,但今天我们不做这个工作了,这个工作做起来比较麻烦,需要套一堆的while循环进行判断,我们大概率是直接能读取到一个完整的HTTP请求的。

所以直接从sockfd里面读取HTTP请求,将内容放到buffer里面

上面就是我们所写的HTTP服务器,那HTTP的客户端呢?我们没有写浏览器的客户端啊。其实不用担心,我们有现成的HTTP客户端,就是浏览器,我们可以用本主机的浏览器来发送HTTP请求到云服务器上,而我们在云服务器上写的代码完成的HTTP请求处理工作其实很简单,就是单纯的把HTTP请求内容打印出来而已。

所以在输入Web地址的一栏中,我们可以用ip+端口号的方式来访问我们的云服务器,只要一访问,vscode的命令行就会打印出HTTP请求的内容。内容包括首行 报头 空行 以及正文,当然今天我们仅仅只是访问了一下服务器,没有向服务器提交什么信息,所以请求正文自然为空,什么都没有。

浏览器请求时,默认的请求方法是GET,如果请求时请求资源的路径没有指定,则服务器会默认返回web server的首页,也就是web根目录下的index.html首页,通常情况下一个网站只有一个index.html作为此网站的首页,一个服务器可以存在多个网站,每个网站会分配不同的端口号。

HTTP在请求时,还会交换bs(browser server)通信双方的协议版本,因为客户端的版本可能由于未及时更新导致和服务器的协议版本不兼容,为了保证服务器返回的响应报文的协议版本和客户端保持一致,所以服务器需要知晓browser的协议版本。

通过HTTP请求的User-Agent字段,还可以理解一个生活现象,我们用电脑浏览器搜索微信进行下载时,浏览器会主动给我们推送pc版本的微信,用手机浏览器搜索微信进行下载时,浏览器会主动推送Android版本的微信,这是为什么呢?因为浏览器的服务器会收到HTTP请求,而HTTP请求的User-Agent字段包含了客户端的操作系统版本信息,通过OS版本信息,浏览器就可以给我们推荐响应版本的微信app了

硬编码响应一个html网页

上面仅仅只是打印了http请求的内容,下面我们构建http响应,让服务器给浏览器返回一个静态的html网页,再谈论html网页之前先来介绍一个本地测试的工具telnet,该工具可以远程登陆我们的服务器,进行TCP连接,连接成功后,可以在命令行中手动构建http请求,测试服务器返回的http响应结果是否正确。

安装telnet的指令分别为sudo yum install -y telnet-server和sudo yum install -y telnet.*

安装成功后,输入ctrl+],按下回车就可以得到telnet的命令行,得到命令行之后,继续按下回车,然后开始构建HTTP请求的请求行并按下回车,然后服务器就会返回该请求行对应的响应内容。

首页中增加跳转链接(重新发起http请求)

如果想要将一个HTML文件返回给浏览器实际也很简单,我们只需要以读取文件的方式,将HTML文件内容读取到响应正文respbody里面即可,然后将respbody字符串拼接到resp中的outbuffer字段里面即可,实际就是多了个文件操作。

而在首页中增加跳转链接也很简单,设置href加路径即可,当我们在首页中点击链接时,其实就是重新又向服务器发起该链接对应资源的请求,如果你点击a,则服务器会重新给你返回a网页的资源,重新在浏览器端显示出来。

2.2.5 首页中增加图片(服务器读取图片文件内容时,必须以二进制读取到缓冲区中)

除将HTML文件返回给浏览器外,我们也可以将图片文件返回给浏览器,两者本质是相同的,因为在Linux下一切皆文件!别跟我说你是音频 视频 网页 还是什么乱七八糟的东西,我linux服务器不管这些,我只认文件,无论是什么到linux这里全都是文件,所以无论返回给浏览器什么,其实无非都是把文件内容按照二进制的方式先读取到缓冲区中,然后调用socket接口将缓冲区的内容发送回浏览器而已,本质都是相通的。

所以用户看到的一个网页,可能是经过多个资源组合而成的!例如我们现在所看到的网页是由多个HTML文件组成,包括体育新闻,金融新闻,首页HTML文件,以及图片文件.jpg,每次发送HTTP请求时,请求资源的类型都不一样,那我们怎么知道请求的资源类型是什么呢?

其实道理很简单,答案就藏在请求资源的后缀里,我们可以从path路径内容中倒着查找,以.作为分隔符将后缀字符串切割出来,放到请求结构体req的suffix字段里面,然后我们就可以把这个字段转成Content-Type: text/html\r\n等这样的类型,根据切分出来的suffix字段进行穷举类型,是什么类型我们就将该类型尾插到Content-Type: 中,然后将此字段尾插到响应结构体resp中的outbuffer里面。


HTTP中细节探究

HTTP的请求方法(GET和POST)

HTTP的请求方法有很多,但我们只要了解两个最常用的就够了,一个是GET获取资源,一个是POST上传资源,GET方法通常用于请求服务器发送一些信息,GET方法的主要限制是URL的长度。大多数浏览器和服务器都对URL的长度有限制。

一般向服务器提交数据的时候,前端都是通过form表单的方式来提交,浏览器会自动将form表单中的内容转换为GET或POST方法下的http请求中的某个字段,下面是表单提交的路径和方法,一旦输入内容点击登录后,页面会自动跳转到/a/b/c.py目录.

GET方法比较尴尬,用户提交的信息直接拼接到url的后面了,谁都能够看到,私密性不够好。
而POST方法会将用户提交的参数信息保存到请求正文里面,然后提交给服务器,用户是看不到的,私密性比GET更好一些。
但值得注意的是GET和POST只是在私密性上有所不同,两者都是不安全的,网上某些人说POST方法比GET方法更安全,这种说法是错误的,想要使网络通信安全,则必须加密(HTTPS)。
一般如果不要求私密性,想要用URL给服务器传参,参数必须是简洁的KV式的参数,这样的场景可以使用GET方法。如果传参内容过长,则可以使用POST方法来传,因为请求正文可以很大,例如上传简历,文件什么的,都可以使用POST,也比GET方法更私密一些。

HTTP状态码

在中构建响应的时候,状态行中的状态码直接写的200,状态码描述是OK。那么状态码到底有哪些呢?它们代表的意义是什么?

重定向状态码(3XX):

这些状态码没啥好说的,重点说一下重定向。

重定向就是将网络请求重新定个方向转到其它位置(跳转网站),此时这个服务器相当于提供了一个引路的服务。

相信都有过这样的经历,打开一个网址以后,自动就弹出一些广告网页,这就是一种重定向。

浏览器发送请求给服务端,服务端返回一个新的url,并且状态码是3XX,浏览器会自动用这个新的url向新地址的服务端发起请求。

所以说,重定向是由客户端完成的,当客户端浏览器收到的响应中状态码是3XX后,它就会自动从响应中寻找返回的新的url并发起请求。

重定向又有两种:

永久重定向:状态码为301。

临时重定向:状态码为302和307。

临时重定向和永久重定向本质是影响客户端的标签,决定客户端是否需要更新目标地址。

如果某个网站是永久重定向,那么第一次访问该网站时由浏览器帮你进行重定向,但后续再访问该网站时就不需要浏览器再进行重定向了,此时直接访问的就是重定向后的网站。

而如果某个网站是临时重定向,那么每次访问该网站时都需要浏览器来帮我们完成重定向跳转到目标网站。

当浏览器发起请求访问index.html的时候,收到的响应中,状态码是307,所以浏览器不解释index.html,而是根据响应报头中的Location属性,得到新的url,再向新的url发起请求,得到新的响应


相关推荐
梅见十柒9 分钟前
wsl2中kali linux下的docker使用教程(教程总结)
linux·经验分享·docker·云原生
Koi慢热12 分钟前
路由基础(全)
linux·网络·网络协议·安全
传而习乎22 分钟前
Linux:CentOS 7 解压 7zip 压缩的文件
linux·运维·centos
我们的五年32 分钟前
【Linux课程学习】:进程程序替换,execl,execv,execlp,execvp,execve,execle,execvpe函数
linux·c++·学习
IT果果日记1 小时前
ubuntu 安装 conda
linux·ubuntu·conda
Python私教1 小时前
ubuntu搭建k8s环境详细教程
linux·ubuntu·kubernetes
羑悻的小杀马特1 小时前
环境变量简介
linux
小陈phd2 小时前
Vscode LinuxC++环境配置
linux·c++·vscode
是阿建吖!2 小时前
【Linux】进程状态
linux·运维
明明跟你说过2 小时前
Linux中的【tcpdump】:深入介绍与实战使用
linux·运维·测试工具·tcpdump