欢迎光临seo外链资源网站,我们为你英文友情链接的信息及服务

seo外链资源

一个资源好的推广优化外链发布网站,为你解决外链获客难题

计算机网络基础(下)

作者:jcmp      发布时间:2021-04-20      浏览量:0
一、前篇: 米纳尔:计算机网络基

一、前篇: 米纳尔:计算机网络基础(上)

二、手写 HTTP 1.x 协议

接下来就是本文最重要的部分:用 TCP 手写 HTTP 1.x 服务器。之所以只手写 HTTP 1.x,是因为它体量小,但是又能充分体现 TCP 的原理,非常适合计算机网络知识的学习。

三、从 TCP 角度看 HTTP

首先,我们来复习一下 一次 HTTP/1.0 请求会经过哪些步骤 :

从 TCP 的角度,逐个翻译上面的每一个步骤,那就是:

如果加上 HTTP/1.1 呢?HTTP/1.1 在网络方面的改进就是加入了长连接。具体的表现是,服务器发送的响应报文中指定了响应体的长度,从而不需要服务端关闭连接,客户端就可以正确拿到响应体。

那稍微修改上面的步骤,一个 HTTP/1.1 的请求就是这样的:

四、HTTP/1.0 的实现

首先我们先编写一个非常简单的 TCP 服务器和客户端,然后再加以改造:

五、报文生成器

HTTP 报文有严格的格式,手动编写不仅复杂,更容易出错,因此我们需要一个报文生成器。并且请求报文和响应报文的第一行的格式还有微小的不同,所以我们需要为它们分别创建生成器。

先复习一下 请求报文的格式 :

这里我们需要用到的参数有:方法名、访问路径、HTTP 的版本、请求头、请求体,用代码表示为:

接下来就是拼接这些参数了。首先是第一行,直接用模板字符串拼接即可。不要忘了这里需要使用 Buffer.from 来包裹:

由于 Buffer 的合并操作比较繁琐,所以我们写一个合并 Buffer 的工具函数:

然后是请求头,其实和响应头的格式是一样的,我们写一个头部生成器,将头部数据变成报文形式。

由于头部数据是 key: value 格式,所以此处我们采用对象来表示请求头。记住,头部写完以后需要单独空一行代表头部的结束:

然后直接将头部对象传入合并:

如果 body 是字符串,body.length 代表是这个字符串的 文字个数 ,比如“你好 a”这个字符串,有两个汉字和一个英文字母,body.length 为 3。

而 Content-Length 代表的是 字符个数 ,对于汉字来说,1 个汉字=2 个字符,所以其实“你好 a”这个字符串的 Content-Length 应该为 4。所以,如果 body 是个字符串,我们应该把它转换成 Buffer,这样它的长度就会取字符的长度了:

六、完整报文解析器

假设服务器或客户端拿到一段完整的报文后,应该怎么解析其中的 HTTP 版本、头部属性等字段呢?所以我们还需要一个报文解析器。

首先,请求报文和响应报文的格式只有第一行不同,所以我们拿到一段报文后,先取出它的第一行来针对请求或响应报文分别解析,其他的部分可以放入通用的逻辑中。

而报文的第一行会有三个属性,这三个属性又是通过空格来分隔的。

所以,我们可以先来解析第一行,解析完成以后再把未解析的部分重新还原成字符串:

现在变量 segment 中已经没有第一行的内容了,后续可以走通用的逻辑,于是我们新创建一个 commonSegmentResolver 函数用来解析格式相同的部分。在这个函数中,我们可以接着将 headers 和 body 分隔开。根据规则,headers 结束的标志是一个额外的换行,也就是连续两个\r\n,因此我们以\r\n\r\n 为分隔符,将 header 和 body 分隔开:

此时 headers 就是一个有多行的字符串,每一行就是一个 header 属性,属性名和属性值用“:[空格]”隔离,因此我们可以继续分割 headers,然后将每个属性放入 headersObj 这个对象里,完成 headers 的解析。最后把 headersObj 和 body 返回:

将 headers 解析器拆分成 headersResolver,即:

然后把 commonSegmentResolver 放入 requestSegmentResolver 和 responseSegmentResolver 即可:

注意,此时的报文是一个完整的报文。实际情况下,服务器会在报文没有接收完毕之前就开始解析,下面我们再来实现流式报文的解析逻辑。

七、流式报文解析器

流式报文解析器的逻辑大概是:

首先,解析需要分成三个阶段:解析首行、解析 headers、解析 body。所以我们应该在定义一个 stage 字段来代表当前的解析进度,并且设置几个变量存放解析结果。下面以请求报文解析器来举例:

首先要判断第一行是否已经收到,即判断是否有\r\n,如果有,则将第一行取出并解析后放入变量中:

接下来就要开始解析 headers 了,在完整报文解析器中的 commonSegmentResolver 函数已经实现了这个逻辑,不过那是针对于完整报文的,所以我们还需要判断 headers 是否接收完毕:

headers 解析完毕以后,就可以根据 Content-Length 属性来判断 body 是否存在、接收完成。在请求报文中,如果没有 Content-Length 但却有 body,或者 body 长度大于 Content-Length,则需要报错。注意,此处 Content-Length 为字符串类型,我们需要转换成数字:

最后,由于是流式报文,我们接收到的参数 segment 只是一个报文片段,未处理完的报文我们会存放在 restSegment 变量中,因此,每一轮处理的内容应该是“上一次未处理完的报文+本次接收到的新报文”,即 restSegment + segment。每一轮处理完了,我们应该返回当前的处理进度和处理结果:

请求流式报文解析器就写完了,关于流式响应报文解析器,只需要改造两点:

最后,为了把这个代码块导出为模块,我们要把这个块放到函数中形成闭包:

然而,我们的报文解析器不是一次性的,解析完一个报文后肯定还要解析新的报文。所以我们还需要写一个函数来清除解析器的状态:

以上就是流式报文解析器的逻辑,当报文数据源源不断的接收时,循环调用 streamSegmentResolver,直到 stage === 3 或者 HTTP/1.0 中服务器主动关闭连接,报文就解析完了。解析完以后,手动调用 clear 来清除解析器的状态。

八、客户端发送报文

有了报文生成器,接下来就可以发送报文了。现在应该让客户端发送一段请求报文给服务器,同时服务器能把报文显示在屏幕上。

我们直接调用 client.write 来发送一个路由为/的 HTTP/1.0 GET 请求:

运行代码,可以看到服务器端有以下输出:

这和我们预期的报文格式是一致的。

九、服务器解析报文

有了流式报文解析器以后,我们只需要每次接收到数据后放入解析器,等待 stage === 3 就可以了:

运行代码,可以看到服务端有以下结果:

这也和我们预期的是一致的、

十、服务端响应报文

当服务端数据接收完毕以后,就可以处理信息并且返回给客户端结果了。由于有响应报文生成器,我们可以很快的将想要发送的数据转换成报文。我们这里让服务端返回“收到啦,Content-Length 为\${Content-Length}”这样一个报文:

测试代码,可以看到以下输出结果:

服务端:

客户端:

结果是符合预期的。

既然服务端和客户端都能正常工作,那浏览器应该也能正常工作。于是我们打开 http://127.0.0.1:8080 进行测试:

可以看到响应结果乱码了,这是因为浏览器默认的编码是 ISO-8859-1,也叫作 Latin1,顾名思义就是只支持拉丁语系,中文是不支持的。所以,我们在响应头中要加上一个 Content-Type 属性来指定它的编码。由于这个头是通用的,所以我们写在 responseSegmentGenerator 函数里:

重启服务器,再次打开浏览器,可以看到以下结果:

浏览器工作正常了,一个简单的 HTTP/1.0 服务器就写完了。

十一、错误处理

之前我们说过,请求报文中如果 Content-Length 和 body 长度不一致,会返回 400 错误。这个错误我们是在 streamResponseSegmentResolver 函数中抛出的,因此,我们应该将该函数以及后续逻辑用 try-catch 块包裹,如果检测到错误就返回给客户端:

然后在 requestSegmentGenerator 函数中我们将 Content-Length 的值改成 body.length - 1 ,运行代码:

服务端:

客户端:

可以看到,错误已经正常抛出并返回给客户端了。

十二、长连接支持

关于长连接的支持,需要改造三个地方:

十三、服务端改造

针对第一点,responseSegmentGenerator 已经帮我们改造好了。而针对第二点,我们需要在服务端 stage === 3 的时候判断 HTTP 的版本,然后用 socket.write 代替 socket.end :

修改客户端的 HTTP 版本,执行代码,可以看到以下输出:

HTTP/1.0:

HTTP/1.1:

可以看到,HTTP/1.1 的客户端在接收完数据后并没有断开连接,服务端改造成功了。

十四、客户端改造

和服务端类似,客户端也要创建一个 streamResponseSegmentResolver 来解析响应报文,而 streamResponseSegmentResolver 已经实现了判断 HTTP 版本的逻辑:

执行代码可以看到客户端有以下输出:

服务端在没有关闭连接的情况下,客户端就已经判断数据接收完成,客户端改造成功了。

用浏览器测试,也是正常的:

至此,手写 HTTP/1.1 服务器+客户端就完成了。至于平时用到的其他特性,如缓存、Gzip 压缩、Range 属性等,都是拿到数据以后再做的程序逻辑,和计算机网络无关,故本文不做讲解。

十五、总结

虽然在日常工作中,直接和网络底层打交道的情况不多,但是这并不代表它不重要。学习网络原理知识,会让前端开发者对浏览器的原理、协议的封装、打包工具的意义、性能优化等都有所了解,在面向大型应用和云的时代,学会计算机网络知识是十分必要的。