《HTTP/2 基础教程》 读书笔记

最近粗线了不少 HTTP2 相关的帖子和讨论,感觉新一轮的潮流在形成,所以最近找了本 HTTP2 相关书籍做知识储备,刚好记成笔记以备后询 这本书本身不错,缺点就是翻译的有点蹩脚,另外因为是 2017 年出的书,所以有些内容时效性不太好,比如关于 Chrome 的部分,所以我根据 Chrome 的官方文档增加了点内容 ????1. HTTP进化史1.1 HTTP/0.9、HTTP/1.0、HTTP/1.1HTTP/0.9: 是个相当简单的协议。它只有一个方法(GET),没有首部,其设计目标也无非是获取 HTML(也就是说没有图片,只有文本)。HTTP/1.0: 多了很多功能,首部、错误码、重定向、条件请求等,但仍存在很多瑕疵,尤其是不能让多个请求共用一个连接、缺少强制的 Host 首部、缓存的选择也相当简陋,这三点影响了 Web 可扩展的方式。HTTP/1.1: 增加了缓存相关首部的扩展、OPTIONS 方法、Upgrade 首部、Range 请求、压缩和传输编码、管道化等功能。因为强制要求客户端提供 Host 首部,所以虚拟主机托管成为可能,也就是在一个 IP 上提供多个 Web 服务。另外使用了 keep-alive 之后,Web 服务器也不需要在每个响应之后关闭连接。这对于提升性能和效率而言意义重大,因为浏览器再也不用为每个请求重新发起 TCP 连接了。1.2 HTTP/2HTTP2 被希望达到以下特性:相比 HTTP/1.1,性能显著提高;解决 HTTP 中的队头阻塞问题;并行的实现机制不依赖与服务器建立多个连接,从而提升 TCP 连接的利用率,特别是在拥塞控制方面;保留 HTTP/1.1 的语义,可以利用已有的文档资源,包括(但不限于) HTTP 方法、状态码、URI 和首部字段;明确定义 HTTP/2.0 和 HTTP/1.x 交互的方法,特别是通过中介时的方法(双向);明确指出它们可以被合理使用的新的扩展点和策略。2. HTTP/2 快速入门2.1 启动并运行很多网站已经在用HTTP/2(h2)了,比如 Facebook、Instagram、Twitter 等,下面介绍以下如何自己搭建 h2 服务器。要运行 h2 服务器,主要分两步:获取并安装一个支持 h2 的 Web 服务器下载并安装一张 TLS 证书,让浏览器和服务器通过 h2 连接2.2 获取证书证书可以通过三种方式获取:使用在线资源自己创建一张证书从数字证书认证机构(CA)申请一张证书前两个方法 将创建自签名证书,仅用于测试,由于不是 CA 签发的,浏览器会报警后面关于创建 h2 服务器的步骤就不记了,可以百度下3. Web优化『黑魔法』的动机与方式3.1 当前的性能挑战3.1.1 剖析Web页面请求从用户在浏览器中点击链接到页面呈现在屏幕上,在此期间到底发生了什么?浏览器请求 Web 页面时,会执行重复流程,获取在屏幕上绘制页面需要的所有信息。为了更容易理解,我们把这一过程分成两部分:资源获取、页面解析/渲染。资源请求流程图:流程为:把待请求 URL 放入队列解析 URL 中域名的 IP 地址(A)建立与目标主机的 TCP 连接(B)如果是 HTTPS 请求,初始化并完成 TLS 握手(C)向页面对应的 URL 发送请求。资源响应流程图:接收响应如果(接收的)是主体 HTML,那么解析它,并针对页面中的资源触发优先获取机制(A)如果页面上的关键资源已经接收到,就开始渲染页面(B)接收其他资源,继续解析渲染,直到结束(C)页面上的每一次点击,都需要重复执行前面那些流程,给网络带宽和设备资源带来压力。Web 性能优化的的核心,就是加快甚至干脆去掉其中的某些步骤。3.1.2 关键性能指标下面网络级别的性能指标,它会影响整个 Web 页面加载。延迟: 指 IP 数据包从一个网络端点到另一个网络端点所花费的时间。带宽: 只要带宽没有饱和,两个网络端点之间的连接会一次处理尽可能多的数据量。DNS查询: 在客户端能够获取 Web 页面前,它需要通过域名系统(DNS)把主机名称转换成 IP 地址。建立连接时间: 在客户端和服务器之间建立连接需要三次握手。握手时间一般与客户端和服务器之间的延迟有关。TLS协商时间: 如果客户端发起 HTTPS 连接,它还需要进行传输层安全协议(TLS)协商,TLS 会造成额外的往返传输。首字节时间(TTFB): TTFB 是指客户端从开始定位到 Web 页面,至接收到主体页面响应的第一字节所耗费的时间。它包含了之前提到的各种耗时,还要加上服务器处理时间。对于主体页面上的资源,TTFB 测量的是从浏览器发起请求至收到其第一字节之间的耗时。内容下载时间: 等同于被请求资源的最后字节到达时间(TTLB)。开始渲染时间: 客户端的屏幕上什么时候开始显示内容?这个指标测量的是用户看到空白页面的时长。文档加载完成时间: 这是客户端浏览器认为页面加载完毕的时间。3.1.3 HTTP/1 的问题HTTP/1 的问题自然是 HTTP/2 要解决的核心问题1. 队头阻塞浏览器很少只从一个域名获取一份资源,一般希望能同时获取许多资源。h1 有个特性叫管道化(pipelining),允许一次发送一组请求,但是只能按照发送顺序依次接收响应。管道化备受互操作性和部署的各种问题的困扰,基本没有实用价值。在请求应答过程中,如果出现任何状况,剩下所有的工作都会被阻塞在那次请求应答之后。这就是『队头阻塞』,它会阻碍网络传输和 Web 页面渲染,直至失去响应。为了防止这种问题,现代浏览器会针对单个域名开启 6 个连接,通过各个连接分别发送请求。它实现了某种程度上的并行,但是每个连接仍会受到的影响。2. 低效的 TCP 利用传输控制协议(TCP)的设计思路是:对假设情况很保守,并能够公平对待同一网络的不同流量的应用。涉及的核心概念就是拥塞窗口(congestion window)。『拥塞窗口』是指,在接收方确认数据包之前,发送方可以发出的 TCP 包的数量。 例如,如果拥塞窗口指定为 1,那么发送方发出 1 个数据包之后,只有接收方确认了那个包,才能发送下一个。TCP 有个概念叫慢启动(Slow Start),它用来探索当前连接对应拥塞窗口的合适大小。慢启动的设计目标是为了让新连接搞清楚当前网络状况,避免给已经拥堵的网络继续添乱。它允许发送者在收到每个确认回复后额外发送 1 个未确认包。这意味着新连接在收到 1 个确认回复之后,可以发送 2 个数据包;在收到 2 个确认回复之后,可以发 4 个,以此类推。这种几何级数增长很快到达协议规定的发包数上限,这时候连接将进入拥塞避免阶段。这种机制需要几次往返数据请求才能得知最佳拥塞窗口大小。但在解决性能问题时,就这区区几次数据往返也是非常宝贵的时间成本。如果你把一个数据包设置为最大值下限 1460 字节,那么只能先发送 5840 字节(假定拥塞窗口为 4),然后就需要等待接收确认回复。理想情况下,这需要大约 9 次往返请求来传输完整个页面。除此之外,浏览器一般会针对同一个域名开启 6 个并发连接,每个连接都免不了拥塞窗口调节。传统 TCP 实现利用拥塞控制算法会根据数据包的丢失来反馈调整。如果数据包确认丢失了,算法就会缩小拥塞窗口。这就类似于我们在黑暗的房间摸索,如果腿碰到了桌子就会马上换个方向。如果遇到超时,也就是等待的回复没有按时抵达,它甚至会彻底重置拥塞窗口并重新进入慢启动阶段。新的算法会把其他因素也考虑进来,例如延迟,以提供更妥善的反馈机制。前面提到过,因为 h1 并不支持多路复用,所以浏览器一般会针对指定域名开启 6 个并发连接。这意味着拥塞窗口波动也会并行发生 6 次。TCP 协议保证那些连接都能正常工作,但是不能保证它们的性能是最优的。3. 臃肿的消息首部虽然 h1 提供了压缩被请求内容的机制,但是消息首部却无法压缩。消息首部可不能忽略,尽管它比响应资源小很多,但它可能占据请求的绝大部分(有时候可能是全部)。如果算上 cookie,就更大了。消息首部压缩的缺失也容易导致客户端到达带宽上限,对于低带宽或高拥堵的链路尤其如此。『体育馆效应』(Stadium Effect)就是一个经典例子。如果成千上万人同一时间出现在同一地点(例如重大体育赛事),会迅速耗尽无线蜂窝网络带宽。这时候,如果能压缩请求首部,把请求变得更小,就能够缓解带宽压力,降低系统的总负载。4. 受限的优先级设置如果浏览器针对指定域名开启了多个 socket(每个都会受队头阻塞问题的困扰),开始请求资源,这时候浏览器能指定优先级的方式是有限的:要么发起请求,要么不发起。然而 Web 页面上某些资源会比另一些更重要,这必然会加重资源的排队效应。这是因为浏览器为了先请求优先级高的资源,会推迟请求其他资源。但是优先级高的资源获取之后,在处理的过程中,浏览器并不会发起新的资源请求,所以服务器无法利用这段时间发送优先级低的资源,总的页面下载时间因此延长了。还会出现这样的情况:一个高优先级资源被浏览器发现,但是受制于浏览器处理的方式,它被排在了一个正在获取的低优先级资源之后。5. 第三方资源如今 Web 页面上请求的很多资源完全独立于站点服务器的控制,我们称这些为第三方资源。现代 Web 页面加载时长中往往有一半消耗在第三方资源上。虽然有很多技巧能把第三方资源对页面性能的影响降到最低,但是很多第三方资源都不在 Web 开发者的控制范围内,所以很可能其中有些资源的性能很差,会延迟甚至阻塞页面渲染。令人扫兴的是, h2 对此也束手无策。3.2 Web性能优化技术2010 年,谷歌把 Web 性能作为影响页面搜索评分的重要因素之一,性能指标开始在搜索引擎中发挥作用。对于很多 Web 页面,浏览器的大块时间并不是用于呈现来自网站的主体内容(通常是 HTML),而是在请求所有资源并渲染页面。因此,Web 开发者逐渐更多地关注通过减少客户端网络延迟和优化页面渲染性能来提升Web 性能。3.2.1 Web性能的最佳实践1. DNS 查询优化在与服务主机建立连接之前,需要先解析域名;那么,解析越快就越好。下面有一些方法:限制不同域名的数量。当然,这通常不是你能控制的保证低限度的解析延迟。了解你的 DNS 服务基础设施的结构,然后从你的最终用户分布的所有地域定期监控解析时间在 HTML 或响应中利用 DNS 预取指令。这样,在下载并处理 HTML 的同时,预取指令就能开始解析页面上指定的域名 <link rel=“dns-prefetch” href="//ajax.googleapis.com">2. 优化 TCP 连接本章前面提到过,开启新连接是一个耗时的过程。如果连接使用 TLS(也确实应该这么做),开销会更高。降低这种开销的方法如下尽早终止并响应。借助 CDN,在距离请求用户很近的边缘端点上,请求就可以获得响应,所以可以终止连接,大幅减少建立新连接的通信延迟。实施最新的 TLS 最佳实践来优化 HTTPS。利用 preconnect 指令,连接在使用之前就已经建立好了,这样处理流程的关键路径上就不必考虑连接时间了,preconnect 不光会解析 DNS,还会建立 TCP 握手连接和 TLS 协议(如果需要)<link rel=“preconnect” href="//fonts.example.com" crossorigin>如果要从同一个域名请求大量资源,浏览器将自动开启到服务器的并发连接,避免资源获取瓶颈。虽然现在大部分浏览器支持 6 个或更多的并发连接数目,但你不能直接控制浏览器针对同一域名的并发连接数。3. 避免重定向重定向通常触发与额外域名建立连接。在移动网络中,一次额外的重定向可能把延迟增加数百毫秒,这不利于用户体验,并最终会影响到网站上的业务。简单的解决方案就是彻底消灭重定向,因为对于重定向的使往往并没有合理原因。如果它们不能被直接消灭,你还有两个选择:利用 CDN 代替客户端在云端实现重定向如果是同一域名的重定向,使用 Web 服务器上的 rewrite 规则,避免重定向4. 客户端缓存没有什么比直接从本地缓存获取资源来得更快,因为它根本就不需要建立网络连接。所谓的纯静态内容,例如图片或带版本的数据,可以在客户端永久缓存。即便 TTL 被设置得很长,比如一个月,它还是会因为缓存提早回收或清理而过期,这时客户端可能不得不从源头再次获取。CSS/JS 和个性化资源,缓存时间大约是会话(交互)平均时间的两倍。这段时间足够长,保证大多数用户在浏览网站时能够从本地拉取资源;同时也足够短,几乎能保证下次会话时从网络上拉取最新内容。可以通过 HTTP 首部指定 cache control 以及键 max-age(以秒为单位),或者 expires 首部。5. 网络边缘的缓存个人信息(用户偏好、财务数据等)绝对不能在网络边缘缓存,因为它们不能共享。时间敏感的资源也不应该缓存,例如实时交易系统上的股票报价。这就是说,除此之外其他一切都是可以缓存的,即使仅仅缓存几秒或几分钟。对于那些不是经常更新,然而一旦有变化就必须立刻更新的资源,例如重大新闻,可以利用各大 CDN 厂商提供的缓存清理(purging)机制处理。这种模式被称为『一直保留,直到被通知』(Hold til Told),意思是永久缓存这些资源,等收到通知后才删除。6. 条件缓存如果缓存 TTL 过期,客户端会向服务器发起请求。在多数情况下,收到的响应其实和缓存的版本是一样的,重新下载已经在缓存里的内容也是一种浪费。HTTP 提供条件请求机制,客户端能以有效方式询问服务器:『如果内容变了,请返回内容本身;否则,直接告诉我内容没变。』当资源不经常变化时,使用条件请求可以显著节省带宽和性能;但是,保证资源的最新版迅速可用也是非常重要的。使用条件缓存可以通过以下方法。在请求中包含 HTTP 首部 Last-Modified-Since。仅当最新内容在首部中指定的日期之后被更新过,服务器才返回完整内容;否则只返回 304 响应码,并在响应首部中附带上新的时间戳 Date 字段。在请求体中包含实体校验码,或者叫 ETag;它唯一标识所请求的资源。ETag 由服务器 提供,内嵌于资源的响应首部中。服务器会比较当前 ETag 与请求首部中收到的 ETag,如果一致,就只返回 304 响应码;否则返回完整内容。一般来说,大多数 Web 服务器会对图片和 CSS/JS 使用这些技术,但你也可以将其用到其他资源。7. 压缩和代码极简化所有的文本内容(HTML、JS、CSS、SVG、XML、JSON、字体等),都可以压缩和极简化。这两种方法组合起来,可以显著减少资源大小。更少字节数对应着更少的请求与应答,也就意味着更短的请求时间。极简化(minification, 混淆)是指从文本资源中剥离所有非核心内容的过程。通常,要考虑方便人类阅读和维护,而浏览器并不关心可读性,放弃代码可读性反而能节省空间。在极简化的基础上,压缩可以进一步减少字节数。它通过可无损还原的算法减少资源大小。在发送资源之前,如果服务器进行压缩处理,可以节省 90% 的大小。8. 避免阻塞 CSS/JS在屏幕上绘制第一个像素之前,浏览器必须确保 CSS 已经下载完整。尽管浏览器的预处理器很智能,会尽早请求整个页面所需要的 CSS,但是把 CSS 资源请求放在页面靠前仍然是种最佳实践,具体位置是在文档的 head 标签里,而且要在任何 JS 或图片被请求和处理之前。默认情况下,如果在 HTML 中定位了 JS,它就会被请求、解析,然后执行。在浏览器处理完这个 JS 之前,会阻止其后任何资源的下载渲染。然而大多数时候,这种默认的阻塞行为导致了不必要的延迟,甚至会造成单点故障。为了减轻 JS 阻塞带来的潜在影响,下面针对己方资源(你能控制的)和第三方资源(你不能控制的)推荐了不同的策略定期校验这些资源的使用情况。随着时间的变迁,Web 页面可能会持续下载一些不再需要的 JS,这时最好去掉它。如果 JS 执行顺序无关紧要,并且必须在 onload 事件触发之前运行,那么可以设置 async 属性,像这样:<script async src="/js/myfile.js">只需做到下载 JS 与解析 HTML 并行,就能极大地提升整体用户体验。慎用 document.write 指令,因为很可能中断页面执行,所以需要仔细测试。如果 JS 执行顺序很重要,并且你也能承受脚本在 DOM 加载完之后运行,那么请使用 defer 属性。像这样<script defer src="/js/myjs.js">对不会影响到页面初次展示的 JS 脚本,必须在 onload 事件触发之后请求(处理)它。如果你不想延迟主页面的 onload 事件,可以考虑通过 iframe 获取 JS,因为它的处理独立于主页面。但是,通过 iframe 下载的 JS 访问不了主页面上的元素。9. 图片优化对大多数网站而言,图片的重要性和比重在不断增加。既然图片主导了多数现代网站,优化它们就能够获得最大的性能回报。所有图片优化手段的目标都是在达到指定视觉质量的前提下传输最少的字节。图片元信息,例如题材地理位置信息、时间戳、尺寸和像素信息,通常包含在二进制数据里,应该在发送给客户端之前去掉(务必保留版权和色彩描述信息)。这种无损处理能够在图片生成时完成。对于 PNG 图片,一般会节省大概 10% 的空间。图片过载(image overloading)是指,图片最终被浏览器自动缩小,要么因为原始尺寸超过了浏览器可视区中的占位大小,要么因为像素超过设备的显示能力。这不仅浪费带宽,消耗的 CPU 资源也很可观,这些计算资源有时在手持设备上相当宝贵。想要解决图片过载,可以使用技术手段,针对用户的设备、网络状况和预期的视觉质量,提供裁剪过的图片(就尺寸和质量而言)。3.2.2 反模式HTTP/2 对每个域名只会开启一个连接,所以 HTTP/1.1 下的一些诀窍对它来说只会适得其反。详细看 6.7 节3.3 小结HTTP/1.1 孕育了各种性能优化手段与诀窍,可以帮助我们深入理解 Web 及其内部实现。HTTP/2 的目标之一就是淘汰掉众多(并不是全部)此类诀窍。4. HTTP/2 迁移在升级到 HTTP/2 之前,你应该考虑:浏览器对 h2 的支持情况迁移到 TLS(HTTPS)的可能性对你的网站做基于 h2 的优化(可能对 h1 有反作用)网站上的第三方资源保持对低版本客户端的兼容4.1 浏览器的支持情况任何不支持 h2 的客户端都将简单地退回到 h1,并仍然可以访问你的站点基础设施。4.2 迁移到 TLS所有主流浏览器只能访问基于 TLS(即 HTTPS 请求)的 h2。4.3 撤销针对 HTTP/1.1 的优化Web 开发者之前花费了大量心血来充分使用 h1,并且已经总结了一些诀窍,例如资源合并、域名拆分、极简化、禁用 cookie 的域名、生成精灵图,等等。所以,当得知这些实践中有些在 h2 下变成反模式时,你可能会感到吃惊。例如,资源合并(把很多 CSS/JS 文件拼合成一个)能避免浏览器发出多个请求。对 h1 而言这很重要,因为发起请求的代价很高;但是在 h2 的世界里,这部分已经做了深度优化。放弃资源合并的结果可能是,针对单个资源发起请求的代价很低,但浏览器端可以进行更细粒度的缓存。详细看 6.7 节5. HTTP/2 协议本章将全面探讨 HTTP/2 的底层工作原理,深入到数据层传输的帧及其通信方式。5.1 HTTP/2 分层HTTP/2 大致可以分为两部分分帧层 即 h2 多路复用能力的核心部分,主要目的还是传输 HTTP数据或 http 层 其中包含传统上被认为是 HTTP 及其关联数据的部分,向后兼容 HTTP/1.1h2 有些特点需要关注一下:二进制协议 :h2 的分帧层是基于帧的二进制协议,这方便了机器解析,但是肉眼识别比较困难首部压缩 :仅使用二进制协议还不够,h2 的首部还会被深度压缩。这将显著减少传输中的冗余字节多路复用 :在调试工具里查看基于 h2 传输的连接的时候,你会发现请求和响应交织在一起加密传输 :线上传输的绝大部分数据是加密过的,所以在中途读取会更加困难5.2 连接与完全无状态的 h1 不同的是,h2 把它所承载的帧(frame)和流(stream)共同依赖的连接层元素捆绑在一起,其中既包含连接层设置也包含首部表。也就是说,与之前的 HTTP 版本不同,每个 h2 连接都有一定的开销。之所以这么设计,是考虑到收益远远超过其开销。在连接的时候,HTTP/2 提供两种协议发现的机制:在连接不加密的情况下,客户端会利用 Upgrade 首部来表明期望使用 h2。如果服务器也可以支持 h2,它会返回一个101 Switching Protocols(协议转换)响应。这增加了一轮完整的请求-响应通信如果连接基于 TLS,情况就不同了。客户端在 ClientHello 消息中设置 ALPN (Application-Layer Protocol Negotiation,应用层协议协商)扩展来表明期望使用 h2 协议,服务器用同样的方式回复。如果使用这种方式,那么 h2 在创建 TLS 握手的过程中完成协商,不需要多余的网络通信。在协议制定过程中,很早就把小数点去掉了,这表明未来的 HTTP 版本不能保证语义的向后兼容,也就是说只有 HTTP/2 没有 HTTP/2.0、HTTP/2.25.3 帧HTTP/2 是基于帧(frame)的协议,采用分帧是为了将重要信息都封装起来,让协议的解析方可以轻松阅读、解析并还原信息。 相比之下,h1 不是基于帧的,而是以文本分隔。所以解析 h1 的请求或响应可能会出现以下问题:一次只能处理一个请求或响应,完成之前不能停止解析无法预判解析需要多少内存。这会带来一系列问题:你要把一行读到多大的缓冲区里;如果行太长会发生什么;应该增加并重新分配内存,还是返回 400 错误从另一方面来说,有了帧,处理协议的程序就能预先知道会收到什么。下图是一个 HTTP/2 帧的结构前 9 个字节对于每个帧是一致的。解析时只需要读取这些字节,就可以准确地知道在整个帧中期望的字节数。其中每个字段的说明如下名称长度描述Length3 字节表示帧负载的长度(取值范围为 2^142^24-1 字节)。 请注意,214 字节是默认的最大帧大小,如果需要更大的帧,必须在 SETTINGS 帧中设置Type1 字节当前帧类型Flags1 字节具体帧类型的标识R1 位保留位,不要设置,否则可能带来严重后果Stream Identifier31 位每个流的唯一 IDFrame Payload长度可变真实的帧内容,长度是在 Length 字段中设置的相比依靠分隔符的 h1,h2 还有另一大优势:如果使用 h1 的话,你需要发送完上一个请求或者响应,才能发送下一个;由于 h2 是分帧的,请求和响应可以交错甚至多路复用。多路复用有助于解决类似队头阻塞的问题。h2 有十种不同的帧类型:名称ID (Type)描述DATA0x0数据帧,传输流的核心内容HEADERS0x1报头帧,包含 HTTP 首部,和可选的优先级参数PRIORITY0x2优先级帧,指示或者更改流的优先级和依赖RST_STREAM0x3流终止帧,允许一端停止流(通常由于错误导致的)SETTINGS0x4设置帧,协商连接级参数PUSH_PROMISE0x5推送帧,提示客户端,服务器要推送些东西PING0x6PING 帧,测试连接可用性和往返时延(RTT)GOAWAY0x7GOAWAY 帧,告诉另一端,当前端已结束WINDOW_UPDATE0x8窗口更新帧,协商一端将要接收多少字节(用于流量控制)CONTINUATION0x9延续帧,用以扩展 HEADER 数据块扩展帧 :HTTP/2 内置了名为扩展帧的处理新的帧类型的能力。依靠这种机制,客户端和服务器的实现者可以实验新的帧类型,而无需制定新协议。按照规范,任何客户端不能识别的帧都会被丢弃,所以网络上新出现的帧就不会影响核心协议。当然,如果你的应用程序依赖于新的帧,而中间代理会丢弃它,那么可能会出现问题。5.4 流HTTP/2 规范中的流(stream):HTTP/2 连接上独立的、双向的帧序列交换。你可以将流看作在连接上的一系列帧,用来传输一对请求/响应消息。如果客户端想要发出请求,它会开启一个新的流,服务器也在这个流上回复。这与 h1 的请求、响应流程类似,区别在于,因为有分帧,所以多个请求和响应可以交错,而不会互相阻塞。流 ID(帧首部的第 6~9 字节)用来标识帧所属的流。客户端到服务器的 h2 连接建立之后,通过发送 HEADERS 帧来启动新的流,如果首部需要跨多个帧,可能还发会送 CONTINUATION 帧。5.4.1 消息HTTP 消息泛指 HTTP 请求或响应。一个消息至少由 HEADERS 帧(用来初始化流)组成,并且可以另外包含 CONTINUATION 和 DATA 帧,以及其他的 HEADERS 帧。 下图是普通 GET 请求的示例流程POST 和 GET 的主要差别之一就是 POST 请求通常包含客户端发出的大量数据。下图是 POST 消息对应的各帧可能的样子h1 的请求和响应都分成消息首部和消息体两部分;与之类似,h2 的请求和响应分成 HEADERS 帧和 DATA 帧。HTTP/1 和 HTTP/2 消息的下列差别是需要注意一切都是header :h1 把消息分成请求/状态行、首部两部分。h2 取消了这种区分,并把这些行变成了魔法伪首部没有分块编码(chunked encoding) :只有在无法预先知道数据长度的情况下向对方发送数据时,才会用到分块。在使用帧作为核心协议的 h2 里,不再需要分块不再有101的响应 :Switching Protocol 响应是 h1 的边缘应用。它如今最常见的应用可能就是用以升级到 WebSocket 连接。ALPN 提供了更明确的协议协商路径,往返的开销也更小5.4.2 流量控制h2 的新特性之一是基于流的流量控制。h1 中只要客户端可以处理,服务端就会尽可能快地发送数据,h2 提供了客户端调整传输速度的能力,服务端也同样可以调整传输的速度。WINDOW_UPDATE 帧用于执行流量控制功能,可以作用在单独某个流上(指定具体 Stream Identifier)也可以作用整个连接 (Stream Identifier 为 0x0),只有 DATA 帧受流量控制影响。初始化流量窗口后,发送多少负载,流量窗口就减少多少,如果流量窗口不足就无法发送,WINDOW_UPDATE 帧可以增加流量窗口大小。流建立的时候,窗口大小默认 65535(2^16-1)字节。5.4.3 优先级流的最后一个重要特性是依赖关系。现代浏览器会尽量以最优的顺序获取资源,由此来优化页面性能。在没有多路复用的时候,在它可以发出对新对象的请求之前,需要等待前一个响应完成。有了 h2 多路复用的能力,客户端就可以一次发出所有资源的请求,服务端也可以立即着手处理这些请求。由此带来的问题是,浏览器失去了在 h1 时代默认的资源请求优先级策略。假设服务器同时接收到了 100 个请求,也没有标识哪个更重要,那么它将几乎同时发送每个资源,次要元素就会影响到关键元素的传输。h2 通过流的依赖关系来解决上面这个问题。通过 HEADERS 帧和 PRIORITY 帧,客户端可以明确地和服务端沟通它需要什么,以及它需要这些资源的顺序。这是通过声明依赖关系树和树里的相对权重实现的。依赖关系 为客户端提供了一种能力,通过指明某些对象对另一些对象有依赖,告知服务器这些对象应该优先传输权重 让客户端告诉服务器如何确定具有共同依赖关系的对象的优先级流可以被标记为依赖其他流,所依赖的流完成后再处理当前流。每个依赖 (dependency) 后都跟着一个权重 (weight),这一数字是用来确定依赖于相同的流的可分配可用资源的相对比例。其他时候也可以通过 PRIORITY 帧调整流优先级。设置优先级的目的是为了让端点表达它所期望对端在并发的多个流之间如何分配资源的行为。更重要的是,当发送容量有限时,可以使用优先级来选择用于发送帧的流。5.5 服务端推送升单个对象性能的最佳方式,就是在它被用到之前就放到浏览器的缓存里面。这正是 h2 服务端推送的目的。5.5.1 推送对象如果服务器决定要推送一个对象(RFC 中称为『推送响应』),会构造一个 PUSH_PROMISE 帧。这个帧有很多重要属性:PUSH_PROMISE 帧首部中的流 ID (Promised Stream ID)用来响应相关联的请求。推送的响应一定会对应到客户端已发送的某个请求。如果浏览器请求一个主体 HTML 页面,如果要推送此页面使用的某个 JavaScript 对象,服务器将使用请求对应的流 ID 构造 PUSH_PROMISE 帧。PUSH_PROMISE 帧的首部块与客户端请求推送对象时发送的首部块是相似的。所以客户端有办法放心检查将要发送的请求。被发送的对象必须确保是可缓存的。:method 首部的值必须确保安全。安全的方法就是幂等的那些方法,这是一种不改变任何状态的好办法。例如,GET 请求被认为是幂等的,因为它通常只是获取对象,而 POST 请求被认为是非幂等的,因为它可能会改变服务器端的状态。理想情况下,PUSH_PROMISE 帧应该更早发送,应当早于客户端接收到可能承载着推送对象的 DATA 帧。假设服务器要在发送 PUSH_PROMISE 之前发送完整的 HTML,那客户端可能在接收到 PUSH_PROMISE 之前已经发出了对这个资源的请求。h2 足够健壮,可以优雅地解决这类问题,但还是会有些浪费。PUSH_PROMISE 帧会指示将要发送的响应所使用的流 ID如果客户端对 PUSH_PROMISE 的任何元素不满意,就可以按照拒收原因选择重置这个流(使用 RST_STREAM),或者发送 PROTOCOL_ERROR (在 GOAWAY 帧中)。常见的情况是缓存中已经有了这个对象。假设客户端不拒收推送,服务端会继续进行推送流程,用 PUSH_PROMISE 中指明 ID 对应的流来发送对象 5.5.2 选择要推送的资源如果服务器接收到一个页面的请求,它需要决定是推送页面上的资源还是等客户端来请求。决策的过程需要考虑到如下方面资源已经在浏览器缓存中的概率从客户端看来,这些资源的优先级 (参见 5.4.3 节)可用的带宽,以及其他类似的会影响客户端接收推送的资源如果服务器选择正确,那就真的有助于提升页面的整体性能,反之则会损耗页面性能。5.6 首部压缩现代网页平均有很多请求,这些请求之间几乎没有新的的内容,这是极大的浪费。首部列表 (Header List) 是零个或多个首部字段 (Header Field) 的集合。当通过连接传送时,首部列表通过压缩算法(即下文 HPACK) 序列化成首部块 (Header Block),不用 GZIP 是因为它有泄漏加密信息的风险。HPACK 是种表查找压缩方案,它利用霍夫曼编码获得接近 GZIP 的压缩率。然后,序列化的首部块又被划分成一个或多个叫做首部块片段 (Header Block Fragment) 的字节序列,并通过 HEADERS、PUSH_PROMISE,或者 CONTINUATION 帧进行有效负载传送。假设客户端按顺序发送如下请求首部:Header1: fooHeader2: barHeader3: bat当客户端发送请求时,可以在首部数据块中指示特定首部及其应该被索引的值。它会创建一张表:索引首部名称值62Header1foo63Header2bar64Header3bat如果服务端读到了这些请求首部,它会照样创建一张表。客户端发送下一个请求的时候, 如果首部相同,它可以直接发送:62 63 64 ,服务器会查找先前的表格,并把这些数字还原成索引对应的完整首部。首部压缩机制中每个连接都维护了自己的状态。HPACK 的实现比这个要复杂得多,比如:请求端和响应端各维护了两张表格。其中之一是动态表,创建方法和上面差不 多。另一张是静态表,它由 61 个最常见的首部的键值组合而成。例如 :method: GET 在静态表中索引为 2。按规定,静态表包含 61 个条目,所以上例索引编号从 62 开始。关于字段如何索引,有很多控制规则:发送索引编号和文本值仅发送文本值,不对它们进行索引(对于一次性或敏感首部)发送索引的首部名,值用文本表示,但不进行索引处理(如:path: /foo.html,其值每次都不同)发送索引过的首部名和值(如上例中的第二个请求)使用打包方案的整数压缩,以实现极高的空间效率利用霍夫曼编码表进一步压缩字符串5.7 线上传输线上传输的 h2 信息是经过压缩的二进制数据。一个简单的GET请求下面是一个简单的 h2 的 get 请求:authority: www.akamai.com:method: GET:path: /:scheme: httpsaccept: text/html,application/xhtml+xml,…accept-language: en-US,en;q=0.8cookie: sidebar_collapsed=0; _mkto_trk=…upgrade-insecure-requests: 1user-agent: Mozilla/5.0 (Macintosh;…下面是 h2 的一个响应:status: 200cache-control: max-age=600content-encoding: gzipcontent-type: text/html;charset=UTF-8date: Tue, 31 May 2016 23:38:47 GMTetag: “08c024491eb772547850bf157abb6c430-gzip"expires: Tue, 31 May 2016 23:48:47 GMTlink: <https://c.go-mpulse.net>;rel=preconnectset-cookie: ak_bmsc=8DEA673F92AC…vary: Accept-Encoding, User-Agentx-akamai-transformed: 9c 237807 0 pmb=mRUM,1x-frame-options: SAMEORIGIN在这个响应中,服务器表示请求已成功受理(状态码 200),设置了 cookie(cookie 首部),表示返回的内容使用 gzip 压缩(content-encoding 首部)6. HTTP/2性能HTTP/2 大部分情况下传输 web 页面比 HTTP/1.1 快。对于包含很多小型资源的页面,h2 加载页面的时间比 h1 更短。这是因为 h1 下(有 6 个 TCP 连接)服务器只能并行发送 6 个资源(由于队头阻塞),而 h2 下多个流(stream)可以共用连接。进一步说,随着网络条件变差,h1 和 h2 下页面加载时间(PLT)都会增加;但是 h2 由于单连接,如果唯一的连接发生了丢包,所有工作都会受影响。对于包含少量大型资源的页面,在所有网络条件下,h1 性能上都比 h2 表现要好。这个多少令人感到意外的结果是初始拥塞窗口导致的。如果开启 6 个连接,h1 的初始拥塞窗口大小实际上是 h2 的 6 倍。因此在会话开始阶段,h2 的连接窗口尚未增长到最佳值,但 h1 早就能更快地传输更多数据了。这个问题目前仍在解决,因为它导致初始拥塞窗口对 h2 而言太小,然而对 h1 而言又太大。 此外,h2 比 h1 更容易受丢包的影响。对于包含一些极大型资源的 Web 页面,两者没有任何差异。h2 的初始拥塞窗口劣势被整体下载时长掩盖了,多路复用此时也不再具有优势。6.1 客户端实现网络条件相同,使用不同浏览器客户端,同样的网站页面加载性能可能差别很大。协议的具体实现很重要并非所有请求在任何情况下都会从 HTTP/2 受益,即便如此,URL 使用 h2 后性能提升的比例也依旧高于下降的比例6.2 延迟延迟是指数据包从一个端点到另一个端点所花的时间。有时,它也表示数据包到达接收方然后返回发送方所需的时间,又称为往返时延(RTT),长度一般以毫秒计。 影响延迟的因素众多,但有两个是最重要的:端点间的距离,以及所用传输介质两点之间的网线不会是笔直的,另外各种网关、路由器、交换机以及移动基站等(也包括服务器应用本身)都会增加延迟6.3 丢包如果网络中传输的数据包没有成功到达目的地,就会发生丢包,这通常是由网络拥堵造成的。频繁丢包会影响 h2 的页面传输,主要是因为 h2 开启单一 TCP 连接,每次有丢包/拥堵时,TCP 协议就会缩减 TCP 窗口。如果 TCP 流上丢了一个数据包,那么整个 h2 连接都会停顿下来,直到该数据包重发并被接收到。6.4 服务端推送服务端推送让服务器具备了在客户端请求之前就推送资源的能力。 测试表明,如果合理使用推送,页面渲染时间可以减少 20%50%。 然而,推送也会浪费带宽,这是因为服务端可能试图推送那些在客户端已经缓存的资源,导致客户端收到并不需要的数据。客户端确实可以发送 RST_STREAM 帧来拒绝服务器的 PUSH_PROMISE 帧,但是 RST_STREAM 并不会即刻到达,所以服务器还是会发送一些多余的信息。如果用户第一次访问页面时,就能向客户端推送页面渲染所需的关键 CSS 和 JS 资源,那么服务端推送的真正价值就实现了。不过,这要求服务器端实现足够智能,以避免『推送承诺』(push promise)与主体 HTML 页面传输竞争带宽。理想情况下,服务端正在处理 HTML 页面主体请求时才会发起推送。有时候,服务端需要做一些后台工作来生成 HTML 页面。这时候服务端在忙,客户端却在等待,这正是开始向客户端推送所需资源的绝佳时机。6.5 首字节时间首字节时间(TTFB)用于测量服务器的响应能力。是从客户端发起 HTTP 请求到客户端浏览器收到资源的第一个字节所经历的时间。由 socket 连接时间、发送 HTTP 请求所需时间、收到页面第一个字节所需时间组成。h1 中,客户端针对单个域名在每个连接上依次请求资源,而且服务器会按序发送这些资源。客户端只有接收了之前请求的资源,才会再请求剩下的资源,服务器接着继续响应新的资源请求。这个过程会一直重复,直到客户端接收完渲染页面所需的全部资源。与 h1 不同,通过 h2 的多路复用,客户端一旦加载了 HTML,就会向服务器并行发送大量请求。相比 h1,这些请求获得响应的时间之和一般会更短;但是因为是请求是同时发出的,而单个请求的计时起点更早,所以 h2 统计到的 TTFB 值会更高。HTTP/2 比 h1 确实做了更多的工作,其目的就是为了从总体上提升性能。下面是一些 h1 没有,但 h2 实现了的窗口大小调节依赖树构建维持首部信息的静态 / 动态表压缩 / 解压缩首部优先级调整(h2 允许客户端多次调整单一请求的优先级)预先推送客户端尚未请求的数据流下图是使用 h1 和 h2 加载同一个页面的加载时序对比,总体来说 h2 体验更好6.6 第三方资源许多网站会使用各种统计、跟踪、社交以及广告平台,就会引入各种第三方的资源。第三方请求往往通过不同域名发送;由于浏览器需要解析 DNS、建立 TCP 连接、协商 TLS,这将严重影响性能;因为第三方资源在不同域名下,所以请求不能从服务端推送、资源依赖、请求优先级等 h2 特性中受益。这些特性仅是为请求相同域名下的资源设计的;你无法控制第三方资源的性能,也无法决定它们是否会通过 h2 传输;6.7 HTTP/2反模式h1 下的一些性能调优办法在 h2 下会起到反作用。下面列出了一些用于优化 h1 请求的常用技巧,并标注了 h2 方面的考虑。名称描述备注资源合并把多个文件(JavaScript、CSS) 合成一个文件,以减少 HTTP 请求在 HTTP/2 下这并非必要,因为请求的传输字节数和时间成本更低,虽然这种成本仍然存在极简化去除 HTML、JavaScript、CSS 这类文件中无用的代码很棒的做法,在 HTTP/2 下也要保留域名拆分把资源分布到不同的域名上面去,让浏览器利用更多的 socket 连接HTTP/2 的设计意图是充分利用单个 socket 连接,而拆分域名会违背这种意图。建议取消域名拆分,但请注意本表格之后的附注框会介绍这个问题相关的各种复杂情况禁用 cookie 的域名为图片之类的资源建立单独的域名,这些域名不用 cookie,以尽可能减少请求尺寸应该避免为这些资源单独设立域名(参见域名拆分),但更重要的是,由于 HTTP/2 提供了首部压缩,cookie 的开销会显著降低生成精灵图把多张图片拼合为一个文件,使用 CSS 控制在 Web 页面上展示的部分与极简化类似,只不过用 CSS 实现这种效果的代价高昂;不推荐在 HTTP/2 中使用6.7.1 生成精灵图和资源合并/内联精灵图(spriting)是指把很多小图片拼合成一张大图,这样只需发起一个请求就可以覆盖多个图片元素。在 HTTP/2 中,针对特定资源的请求不再是阻塞式的,很多请求可以并行处理;就性能而言,生成精灵图已失去意义,因为多路复用和首部压缩去掉了大量的请求开销。与之类似,小的文本资源,例如 JS 和 CSS,会依照惯例合并成一份更大的资源,或者直接内嵌在主体 HTML 中,这也是为了减少客户端-服务器连接数。这种做法有个问题是,那些小的 CSS 或 JS 自身也许可缓存,但如果它们内嵌在不可缓存的 HTML 中的话,当然也就不可缓存了。把很多小的 JS 脚本合并成一个大文件可能仍旧对 h2 有意义,因为这样可以更好地压缩处理并节省 CPU。6.7.2 域名拆分域名拆分(sharding)是为了利用浏览器针对每个域名开启多个连接的能力来并行下载资源。对于包含大量小型资源的网站,普遍的做法是拆分域名,以利用现代浏览器针能对每个域名开启 6 个连接的特性,充分利用可用带宽。因为 HTTP/2 采取多路复用,所以域名拆分就不是必要的了,并且反而会让协议力图实现的目标落空。比较好的办法就是继续保持当前的域名拆分,但是确保这些域名共享同一张证书 [ 通配符 / 存储区域网络(SAN)],并保持服务器 IP 地址和端口相同,以便从浏览器网络归并(network coalescence)中收益,这样可以节省为单个域名连接建立的时间。6.7.3 禁用cookie的域名在 HTTP/1 下,请求和响应首部从不会被压缩。随着时间推移,首部大小已经增长了,超过单个 TCP 数据包的 cookie 可以说司空见惯。因此,在内容源和客户端之间来回传输首部信息的开销可能造成明显的延迟。 因此,对图片之类不依赖于 cookie 的资源,设置禁用 cookie 的域名是个合理的建议。 但是 HTTP/2 中,首部是被压缩的,并且客户端和服务器都会保留『首部历史』,避免重复传输已知信息。所以,如果你要重构站点,大可不必考虑禁用 cookie 的域名,这样能减少很多包袱。 静态资源也应该从同一域名提供;使用与主页面 HTTP 相同的域名,消除了额外的 DNS 查询以及(潜在的)socket 连接,它们都会减慢静态资源的获取。把阻塞渲染的资源放在同样的域名下,也可以提升性能。6.7.4 资源预取资源预取也是一项 Web 性能优化手段,它提示浏览器只要有可能就继续下载可缓存资源,并把这些资源缓存起来。尽管如此,如果浏览器很忙,或者资源下载花的时间太 长,预取请求将会被忽略。资源预取可以在 HTML 中插入 link 标签实现:<link rel=“prefetch” href="/important.css”>也可以使用 HTTP 响应中的 Link 首部: Link: </important.css>; rel=prefetch 资源预取与 h2 引入的服务端推送并没多少关联。服务端推送用于让资源更快到达浏览器, 而资源预取相比推送的优点之一是,如果资源已经在缓存里,浏览器就不会浪费时间和带宽重复请求它。所以,可以把它看作 h2 推送的补充工具,而不是将被替代的特性。6.8 现实情况中的性能网络丢包是 h2 的命门,一次丢包机会就会让它的所有优化泡汤。7. HTTP/2 实现7.1 桌面Web浏览器所有浏览器在进行 HTTP/2 传输时都需要使用 TLS(HTTPS),即使事实上HTTP/2 规范本身并没有强制要求 TLS。这个原因是:从之前对 WebSocket 和 SPDY 的实验看来,使用 Upgrade 首部,通过 80 端口(明文的 HTTP 端口)通信时,通信链路上代理服务器的中断等因素会导致非常高的错误率。如果基于 443 端口(HTTPS 端口)上的 TLS 发起请求,错误率会显著降低,并且协议通信也更简洁。人们越来越相信,考虑到安全和隐私,一切都应该被加密。HTTP/2 被视为一次推动全网加密通信发展的机会。7.1.2 禁用HTTP/2HTTP/2 毕竟是新鲜事物,现在很多浏览器都支持启用或禁用 h2。7.1.3 支持 HTTP/2 服务端推送服务端推送是 h2 中最令人兴奋也最难正确使用的特性之一,现在所有的主流浏览器都已经支持了此特性。7.1.4 连接归并如果需要建立一个新连接,而浏览器支持连接归并,那么通过复用之前已经存在的连接,就能够提升请求性能。这意味着可以跳过 TCP 和 TLS 的握手过程,改善首次请求新域名的性能。如果浏览器支持连接归并,它会在开启新连接之前先检查是否已经建立了到相同目的地的连接。相同目的地具体指的是:已经存在连接,其证书对新域名有效,此域名可以被解析成那个连接对应的 IP 地址。如果上述条件都满足,那么浏览器会在已建立的连接上向该域名发起 HTTP/2 请求。7.2 服务器、代理以及缓存如果要通过 h2 传输内容,我们有几个选择。支持 HTTP/2 的网络设施大致有以下两类。 Web服务器 :通常所说的提供静态和动态内容服务的程序。 代理/缓存 :一般处在服务器和最终用户之间,可以提供缓存以减轻服务器负载,或进行额外加工。许多代理也能扮演 Web 服务器的角色。 在选择 HTTP/2 服务器时,我们需要检查、评估一些关键点。除了基本的通用性能、操作系统支持、学习曲线、可扩展性以及稳定性,还应当关注 Web 请求的依赖项和优先级,以及对服务端推送的支持。7.3 内容分发网络 CDN内容分发网络(CDN)是反向代理服务器的全球性分布式网络,它部署在多个数据中心。CDN 的目标是通过缩短与最终用户的距离来减少请求往返次数,以此为最终用户提供高可用、高性能的内容服务。大多数主流 CDN 是支持 HTTP/2 的,选择 CDN 时主要考虑的两点是:对服务端推送的支持,以及它们处理优先级的方式。这两点对现实世界中的 Web 性能影响重大。8. HTTP/2调试8.1 chrome devtools 可视化Chrome 开发者工具中的 Network 栏,有助于简单直观地跟踪客户端和服务端的通讯,它 按下面表格的形式展示了若干信息:资源名资源大小状态码优先级总加载时间使用时间线方式分解加载时间打开 devtools 的 Network 栏,鼠标放在瀑布流 Waterfall 的资源上,就会看到资源加载过程中各个阶段的详细时间Connection Setup (连接设置)Queueing :请求被渲染引擎或者网络层延迟的时间,浏览器在以下情况下对请求排队存在更高优先级的请求。此源已打开六个 TCP 连接,达到限值。 仅适用于 HTTP/1.0 和 HTTP/1.1浏览器正在短暂分配磁盘缓存中的空间Connection Start (开始连接阶段)Stalled :请求可能会因 Queueing 中描述的任何原因而停止Proxy negotiation :浏览器与代理服务器协商请求花费的时间DNS Lookup :浏览器解析请求的 IP 地址花费的时间Request/Response (请求 / 响应)Request Sent :发送请求包含的数据花费的时间Waiting (TTFB) :等待初始响应花费的时间,也就是所说的首字节时间;这个数字包括等待服务器传输响应的时间,以及往返服务器的延迟Content Download :接收响应的数据所花费的时间Explanation (总时间)其他ServiceWorker Preparation :浏览器正在启动 Service WorkerRequest to ServiceWorker :正在将请求发送到 Service WorkerReceiving Push :浏览器正在通过 HTTP/2 服务器推送接收此响应的数据Reading Push :浏览器正在读取之前收到的本地数据9. 展望未来HTTP/2 的弱点之一就是依赖主流 TCP 实现。在 3.1.3 节中已经讨论过,TCP 连接受制于 TCP 慢启动、拥塞规避,以及不合理的丢包处理机制。用单个链接承载页面涉及的所有资源请求,就能享受多路复用带来的好处;然而面对 TCP 层级的队首阻塞时,我们还是束手无策。所以 Google 开发的 QUIC 采纳了 HTTP/2 的优点,并且避免了这些缺点。推介阅读:HTTP2 详解 | Wangriyu’s BlogPS:欢迎大家关注我的公众号【前端下午茶】,一起加油吧 ...

April 16, 2019 · 6 min · jiezi

【融云分析】 IM 即时通讯之链路保活

众所周知,IM 即时通讯是一项对即时性要求非常高的技术,而保障消息即时到达的首要条件就是链路存活。那么在复杂的网络环境和国内安卓手机被深度定制化的条件下,如何保障链路存活呢?本文详解了融云安卓端 SDK 在基于 TCP 协议实现链路保活方面的探索和经验。IM 系统整体框架如上图所示,为了保障链路存活,一套成熟的 IM 系统一般会包含消息链路和推送链路两条长连接通道。当有新消息到达时,消息服务首先会判断消息链路是否存活,如果消息链路处于存活状态,消息优先从消息链路下发到客户端,否则会被路由到推送服务器,由推送链路下发。综上所述,链路保活涉及到消息链路和推送链路两条链路的保活策略。基于这两条链路使用场景的不同,保活策略上除了心跳机制是相同的,其它保活策略各有不同。下面将详细讲解。链路保活的必要性基于 TCP 的 Socket 连接建立之后,如果不做任何处理,这个连接会长时间存在并且可用吗?答案是否定的。原因有两点:一、默认 Socket 连接无法及时探测到链路的异常情况,即使将 Socket 的属性参数 keepAlive 设置为 true 仍然无法及时获取到链路存活状态。这是因为 Socket 的连接状态是由一个状态机进行维护的,连接完毕后,双方都会处于建立状态。假如某台服务器因为某些原因导致负载超高,无法及时响应业务请求,这时 TCP 探测到的仍然是连接状态,而实际上此链路已经不可用了。二、国内运营商的 NAT 超时机制会把一定时间内没有数据交互的连接断开,这个时间可能只有几分钟,远无法满足我们的长连接需求。通用保活机制 - 心跳机制基于以上原因,要维持 Socket 连接长时间存活,就需要实现自己的保活机制。最通用的一种保活机制就是心跳机制。即客户端每隔一段时间给服务器发送一个很小的数据包,根据能否收到服务器的响应来判断链路的可用性。为了节省流量,这个包一般非常小,甚至没有内容。那么客户端如何实现定时发送心跳包呢?一般有两种方式:一种是通过 Java 里的 Timer 来实现。最基本的步骤如下:1、建立一个要执行的任务TimerTask。2、创建一个Timer实例,通过Timer提供的schedule()方法,将 TimerTask 加入到定时器Timer 中,设置每隔一段时间执行 TimerTask , 在 TimerTask 里发送心跳包。这种方式实现起来较简单,而且省电,不需要持有 WakeLock 。缺点也很明显,长时间在后台,进程被回收或者系统休眠后, Timer 机制随之失效。另外一种方式是利用安卓系统的定时任务管理器 AlarmManager 循环执行发送心跳包的任务。这种方式不会因为系统休眠而失效,系统休眠后仍然可以通过 WakeLock 唤醒,执行心跳任务,因此相对 Timer 机制,这种方式比较费电,使用的时候一定要注意如下几点:首先根据需求合理使用 AlarmManager 的闹钟参数。闹钟各参数的区别参考下表:其次 AlarmManager 提供了 cancel() 方法,在设置新的定时任务前,通过 cancel() 方法取消系统里设置的同类型任务,避免设置冗余任务。最后,安卓从 6.0 版本引入了 Doze 模式,并提供了新的闹钟设置方法 setExactAndAllowWhileIdle(),通过该方法设置的闹钟时间,系统会智能调度,将各个应用设置的事务统一在一次唤醒中处理,以达到省电的目的。推荐在安卓 6.0 以上系统中,优先使用该方法。消息链路保活机制消息链路作为收发消息的主要通道,需要最大程度保障链路的可用性。在链路不可用或者异常断开时,能及时探测并启动重连等保障机制。基于以上特性,消息链路除了前面所说的心跳机制外,还另外维护了两套链路优化机制:复合连接机制和重连机制。复合连接机制的基本步骤如下:客户端连接导航服务器,导航服务器会下发应用对应的配置信息,其中包括连接服务器的地址列表。客户端从第一个服务器地址尝试连接,并启动超时机制,如果连接失败或没有及时收到服务响应, 则继续尝试连接下一个直到成功连接,将成功连接的地址保存到本地,作为最优地址,后面连接时优先使用此地址。通过这种机制,能保障客户端优先选用最优链路,缩短连接时间。重连机制,则是指业务层在检测到与服务器的连接断开后,尝试 N 次重新连接服务器,首次断开 1 秒后会重新连接,如果仍然连接不成功,会在 2 秒后(重连间隔时间为上次重连间隔时间乘 2 )尝试重新连接服务器,以此类推当尝试重连 N 次后,仍然连不上服务器将不再尝试重新连接,只有在网络情况发生变化或重新打开应用时才会再次尝试重连。推送链路保活机制推送链路作为消息到达的补充手段,要求尽可能延长在后台的存活时间。即使被杀后,仍然能被再次唤醒。iOS 手机有 APNS 来达到以上效果,但安卓的官方推送系统 FCM 在国内基本不可用。那在国内安卓系统上如何保障推送到达呢?首先咱们需要先了解下安卓系统上进程管理的两大机制:一种是 LMK 机制,英文是 Low Memory Killer, 基于 Linux 的内存管理机制衍生而来。主要是通过进程的 oom_adj 值来判定进程的重要程度,从而决定是否回收这些进程。oom_adj 的值越低,代表重要度越高,比如 native 进程,framework 层启动的系统进程,优先级一般都为负数。其次是前台可见进程,系统也不会回收。然而可见进程退到后台后, oom_adj 的值会立即升高,在系统定时清理时被杀。另外一种机制是安卓原生的权限管理机制(AppOps),各大厂家在此基础上又进行了深度定制化,比如小米的安全中心,华为的手机管家等,都用来进行权限管理。该权限管理机制运行在安卓系统的框架层,上层各应用的进程如果想尝试重新启动,系统首先会去权限管理中心检查该进程有没有自启动权限,如果有,才准予启动。否则,从框架层直接限制系统的启动。基于以上两种机制,推送链路的保活也可分为两大类,一 进程保活。它的思路是根据 LMK 机制提高进程优先级,降低被杀的几率。主要有以下几种方法:监听黑屏事件,启动 1 像素透明 Activity ,使应用进程转为可视进程,降低被杀概率。在屏幕亮时,关闭该 Activity。双服务守护。A 服务以 startForeground() 形式启动,发送一个通知,B 服务同样以 startForeground() 形式启动,且发送和 A 相同 ID 的通知,然后在 B 服务里调用 stopForeground() 方法,取消通知。这样 A 服务就会以前台进程的形式存活,且不影响用户感知。根据文件锁互斥原理,监视 Java 进程存活状态,若被杀,Linux 层成功持有文件,则通过 exec() 命令,打开一个纯 Linux 的可执行文件,开启一个 Daemon 进程, 该进程因为从 Linux 层启动,在安卓 5.0 之前,优先级会比较高,不会被杀。在安卓 5.0 之后,该方式不再有效。二 进程拉活的策略和安卓系统的 AppOps 机制有关,一般有如下几种:一、利用 Service 本身的 Sticky 属性,在 Service 的 onStartCommand() 中返回START_STICKY,这样当 Service 被杀掉后,系统会自动尝试重启。不过在国内定制化的系统上,这种方式能成功重启的几率很低,需要用户在权限管理中心打开自启动等权限,才能成功拉活。二、也就是前面讲过的心跳机制,不过这里要求使用 AlarmManager 设置 ELAPSED_REALTIME_WAKEUP 属性的闹钟,在系统休眠后,才会正常接受到心跳事件,从而将进程拉活。三、通过监听网络切换,用户行为等事件,拉起进程。四、应用间互相拉活。比如系统里有好几个应用集成了同一个 SDK , 那么在用户启动其中某一个 App 的时候,SDK 会去扫描其它应用,把"兄弟姐妹" 拉活。这种方式对用户体验伤害非常大,会造成系统莫名其妙的耗电。随着安卓系统版本的迭代,对后台进程的启动管控越来越严。为了解决推送的问题,各手机厂家推出了自己的系统级推送服务。由厂家在 Framework 层统一维护一条推送通道,上层所有应用共同使用该推送链路,不需要再维护单独进程。当前支持系统级推送的厂家有:小米、华为、魅族、vivo、OPPO,这种系统级别的推送省电,省内存,到达率高。应用可以根据手机型号的不同,优先使用厂家系统级别的推送,再配合自身的保活机制,最大程度保障推送的到达率。集成第三方系统级推送之后,整个消息的收发流程可以参考下图: ...

April 4, 2019 · 1 min · jiezi

抓包分析 TCP 建立和断开连接的流程

⭐️ 更多前端技术和知识点,搜索订阅号 JS 菌 订阅HTTP 协议是基于 TCP 协议的。大家都知道发送 HTTP 报文需要首先建立客户端和服务端之间的 TCP 连接。TCP 三次握手建立连接,四次挥手断开连接,再熟悉不过。本文实践一下 TCP 建立和断开的整个流程,并通过抓包工具进行逐一分析。开始之前呢,先安装抓包工具,这里用的是 Wireshark 正常下载安装,不再赘述然后我们还需要 curl 如果在 windows 中没有这个模块,可以通过 Chocolatey 去安装,或者用 wget、浏览器啥的两个准备工作做好了,就可以开始分析工作了。TCP 建立连接首先回顾一下 HTTP 请求是怎么发送的:先是建立 TCP 连接首先,服务端准备接收客户端请求,状态变为 LISTEN;客户端发送建立连接请求包,携带一个 SYN,Seq=x;此时客户端状态为 SYN_SENT 状态服务端收到请求后,同意连接返回一个同意连接的包,携带一个 SYN,ACK,Seq=y,Ack=x+1;服务端状态变为 SYN_RCVD客户端收到确认后,还要发送一个确认的确认连接包,携带一个 ACK,Ack=y+1;服务端收到后,客户端服务端都为 ESTABLISHED 状态;连接建立成功客户端发送 HTTP 请求组装 HTTP 请求行、请求首部和实体⚠️ 一定要注意 ACK 和 Ack 是不同的概念,前者是 Acknowledgement 确认值,后者是 Acknowledgement Number 确认编号开始抓包:打开 Wireshark,左上角鲨鱼鳍标志,然后在终端中使用 curl 给发送一个 GET 请求,这里以 http://httpbin.org/json 为例回到 Wireshark,在过滤器中输入 http,只查看 http 应用层的信息:然后我们选择明显是 /json 网址的记录,右键选择 follow 子菜单中的 HTTP Stream:弹出的窗口是 HTTP 请求报文,先关闭窗口暂时用不到这些东西此时面板中就是整个 TCP 建立、发送 HTTP 请求并获取响应以及断开 TCP 连接的过程客户端发送请求建立连接第一条记录显示了我的电脑端口发送了一个 TCP 连接的包,这个包携带了一个 SYN flag,Seq 被设置为 0;这就是请求建立 TCP 连接的包所以客户端请求建立 TCP 连接时是发送 SYN 的包,其中 Seq 被设置为 0(实际上有可能不为 0)服务端返回确认信息第二条是第一条包的确认信息:看到这是一个确认包,这里的 flag 是 SYN 和 ACK,其中 Seq 为新设置的值为 0( ⚠️ 注意这里的 Seq 与此前发送的 Seq 不是一个值)另外确认序号 Ack 是之前为 0 的,接收到的那个序号 Seq + 1,值为 1客户端发送确认信息第三条就是第二条包的确认信息,表示确认收到服务端的确认信息第三个包可以看到有一个 ACK,同时序号 Seq 为第一次发送请求建立连接时候的 Seq + 1,值为 1( ⚠️ 注意这里的 Seq 与服务端返回的 Seq 不是一个值),Ack 确认序号就是收到的服务端发送的包 Seq + 1,值为 1至此 TCP 连接成功⚠️ 一定要注意区别开双方发送的 Seq 不是一个东西,Ack 是确认收到对方的包,在对方发送的这个包的 Seq 基础上增加 1。自己发送接下来的包,则是在自己发送的上一个包的 Seq 基础上增加 1;另外还要区别 Ack 和 ACK 是不同的;TCP 断开连接客户端主动断开 TCP 连接的过程如下:客户端发送断开连接的请求包,携带一个 FIN, ACK,Seq=x,Ack=y;此时客户端状态为 FIN_WAIT_1服务端同意断开连接,返回一个 ACK,Ack=x+1;服务端可能还有数据需要传送,继续传送并将状态变为 CLOSE_WAIT 状态;客户端收到并将状态变为 FIN_WAIT_2;继续接收数据。数据传输完毕,服务端发送一个 FIN,Seq=z+1(这里的 z 是最后一次服务端发送的 Seq 序号);服务端状态变为 LAST_ACK;客户端收到并将状态变为 TIME_WAIT数据接收到之后,客户端发送一个 ACK,这里的 Ack=z+2(就是最后一次接收到的序号 Seq 加一)Wireshark 抓包记录继续分析:首先客户端发送一个 FIN, ACK,切序号 Seq 为 80,Ack 为 650,请求断开连接服务端返回一个 ACK 和一个 FIN,因为没有更多数据传输,所以原本两个数据包被合并成一个,因此这里四次挥手因合并而变为“三次挥手”这里的 Seq 为 650,确认序号 Ack 为收到序号加一也就是 80 + 1 = 81最后客户端发送一个 ACK,就代表 TCP 连接正式断开,Ack 为收到序号加一也就是 650 + 1 = 651 整个 TCP 通信过程就是这样⚠️ Seq 序号和 Ack 确认序号比较乱;这里提个醒 Ack 始终为最后收到包的序号 Seq + 1;而 Seq 则是上一个发送出去的包的 Seq + 1有哪里有讲的不准确的地方也请指正谢谢请关注我的订阅号,不定期推送有关 JS 的技术文章,只谈技术不谈八卦 ???? ...

April 1, 2019 · 2 min · jiezi

WebSocket入门

WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。现在,很多网站为了实现推送技术,所用的技术都是 Ajax 轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。HTML5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。当你获取 Web Socket 连接后,你可以通过 send() 方法来向服务器发送数据,并通过 onmessage 事件来接收服务器返回的数据。DEMO如下// client side filelet socket = new WebSocket(‘ws://localhost:9999’);// 当连接成功执行回调函数socket.onopen = function () { console.log(‘客户端连接成功’); // 向服务器发送一个消息 socket.send(‘这是客户端发给服务器的消息’);}// 绑定事件是用加属性的方式socket.onmessage = function (res) { console.log(‘收到服务器端的响应是’, res);}// server side file/* npm i ws -S // 用ws模块启动一个websocket服务器,监听 9999 端口 /let WebSocketServer = require(‘ws’).Server; let wsServer = new WebSocketServer({port: 9999});wsServer.on(‘connection’, socket=>{ console.log(‘服务器连接成功’); socket.on(‘message’, msg=>{ console.log(‘从客户端接收到的信息为’+msg); socket.send(‘这是从服务器发送到客服端的信息’); })})简单模拟智能客服聊天<template> <div class=“wrap”> <ul> <li v-for="(item, index) in dialogs" :key=“index”> <p :class=“item.self ? ‘fr’: ‘fl’">{{item.sendContent || item.acceptContent}}</p> </li> </ul> <div class=“send-cont”> <input v-model=“sendCont”> <input type=“button” @click=“send” value=“发送”> </div> </div></template><script>let socket = new WebSocket(“ws://localhost:9999”);export default { data() { return { dialogs: [], sendCont: “”, isConnect: false }; }, mounted() { socket.onopen = () => { this.isConnect = true; }; }, methods: { send() { if (this.isConnect) { console.log(“客户端连接成功”); // 向服务器发送一个消息 socket.send(this.sendCont); this.dialogs.push({ sendContent: this.sendCont, self: true }); // 绑定事件是用加属性的方式 socket.onmessage = res => { setTimeout(() => { this.dialogs.push({ acceptContent: res.data }); }, Math.random() * 2000); }; } } }};</script><style scoped> { padding: 0; margin: 0;}.fl { float: left;}.fr { float: right;}li::before { content: “.”; display: block; height: 0; clear: both; visibility: hidden;}.wrap { position: relative; width: 380px; margin: auto; height: 600px; background-color: #eee; padding: 10px;}.wrap ul { overflow: auto; height: 550px;}li { list-style: none; margin: 2px;}li:nth-child(even) p { background-color: rgb(86, 107, 177);}li p { font-size: 20px; font-family: “Microsoft Yahei”, serif, Arial, Helvetica, sans-serif; border-radius: 6px; padding: 4px; margin: 2px 4px; white-space: wrap; text-align: left;}li p.fr { background-color: rgb(61, 185, 30);}.send-cont { position: absolute; bottom: 10px; z-index: 999; width: 98%; margin: auto;}.send-cont input { display: inline-block; outline: none; border: 2px solid #080; border-radius: 6px; line-height: 30px; font-size: 16px; text-align: left;}.send-cont input:first-child { width: 70%;}.send-cont input[type=“button”] { width: 20%; background-color: #080; color: #fff; text-align: center; padding: 1px;}</style>const contents = [‘你好’, ‘hi’, ‘hello’, ’nice to meet you’, ‘how are you’, ‘how do you do’]let WebSocketServer = require(‘ws’).Server; let wsServer = new WebSocketServer({port: 9999});wsServer.on(‘connection’, socket=>{ console.log(‘服务器连接成功’); socket.on(‘message’, msg=>{ console.log(‘从客户端接收到的信息为’+msg); socket.send(contents[parseInt(Math.random()*contents.length)]); })}) ...

March 30, 2019 · 2 min · jiezi

走进 mTCP

mTCP 是一款面向多核系统的用户态网络协议栈内核态协议栈的缺陷互联网的发展,使得用户对网络应用的性能需求越来越高。人们不断挖掘CPU处理能力加强,添加核的数量,但这并没有使得网络设备的吞吐率线性增加,其中一个原因是内核协议栈成为了限制网络性能提升的瓶颈。互斥上锁引起的开销互斥上锁是多核平台性能的第一杀手。现在的服务器端应用为了尽可能的实现高并发,通常都是采用多线程的方式监听客户端对服务端口发起的连接请求。首先,这会造成多个线程之间对accept队列的互斥访问。其次,线程间对文件描述符空间的互斥访问也会造成性能下降。报文造成的处理效率低下内核中协议栈处理数据报文都是逐个处理的, 缺少批量处理的能力。频繁的系统调用引起的负担频繁的短连接会引起大量的用户态/内核态模式切换,频繁的上下文切换会造成更多的Cache Miss用户态协议栈的引入用户态协议栈-即是将原本由内核完成了协议栈功能上移至用户态实现。通过利用已有的高性能Packet IO库 (以DPDK为例)旁路内核,用户态协议栈可以直接收发网络报文,而没有报文处理时用户态/内核态的模式切换。除此之外,由于完全在用户态实现,所以具有更好的可扩展性还是可移植性。mTCP 介绍mTCP作为一种用户态协议栈库的实现,其在架构如下图所示:mTCP以函数库的形式链接到应用进程,底层使用其他用户态的Packet IO库。总结起来,mTCP具有以下特性:良好的多核扩展性批量报文处理机制类epoll事件驱动系统BSD风格的socket API支持多种用户态Packet IO库传输层协议仅支持TCP多核扩展性为了避免多线程访问共享的资源带来的开销。mTCP将所有资源(如flow pool socket buffer)都按核分配,即每个核都有自己独有的一份。并且,这些数据结构都是cache对齐的。从上面的架构图可以看到,mTCP需要为每一个用户应用线程(如Thread0)创建一个额外的一个线程(mTCP thread0)。这两个线程都被绑定到同一个核(设置CPU亲和力)以最大程度利用CPU的Cache。批量报文处理机制由于内部新增了线程,因此mTCP在将报文送给用户线程时,不可避免地需要进行线程间的通信,而一次线程间的通信可比一次系统调用的代价高多了。因此mTCP采用的方法是批量进行报文处理,这样平均下来每个报文的处理代价就小多了。类epoll事件驱动系统对于习惯了使用epoll编程的程序员来说,mTCP太友好了,你需要做就是把epoll_xxx()换成mtcp_epoll_xxx()BSD 风格的 socket API同样的,应用程序只需要把BSD风格的Socket API前面加上mtcp_ 就足够了,比如mtcp_accept()支持多种用户态Packet IO库在mTCP中, Packet IO库也被称为IO engine, 当前版本(v2.1)mTCP支持DPDK(默认)、 netmap 、onvm、 psio 四种IO engine。mTCP的一些实现细节线程模型如前所述mTCP需要会为每个用户应用线程创建一个单独的线程,而这实际上需要每个用户应用线程显示调用下面的接口完成。mctx_t mtcp_create_context(int cpu);这之后,每个mTCP线程会进入各自的Main Loop,每一对线程通过mTCP创建的缓冲区进行数据平面的通信,通过一系列Queue进行控制平面的通信每一个mTCP线程都有一个负责管理资源的结构struct mtcp_manager, 在线程初始化时,它完成资源的创建,这些资源都是属于这个核上的这个线程的,包括保存连接四元组信息的flow table,套接字资源池socket pool监听套接字listener hashtable,发送方向的控制结构sender等等用户态 Socket既然是纯用户态协议栈,那么所有套接字的操作都不是用glibc那一套了,mTCP使用socket_map表示一个套接字,看上去是不是比内核的那一套简单多了!struct socket_map{ int id; int socktype; uint32_t opts; struct sockaddr_in saddr; union { struct tcp_stream *stream; struct tcp_listener *listener; struct mtcp_epoll *ep; struct pipe pp; }; uint32_t epoll; / registered events / uint32_t events; / available events */ mtcp_epoll_data_t ep_data; TAILQ_ENTRY (socket_map) free_smap_link;};其中的socketype表示这个套接字结构的类型,根据它的值,后面的联合体中的指针也就可以解释成不同的结构。注意在mTCP中,我们通常认为的文件描述符底层也对应这样一个socket_mapenum socket_type{ MTCP_SOCK_UNUSED, MTCP_SOCK_STREAM, MTCP_SOCK_PROXY, MTCP_SOCK_LISTENER, MTCP_SOCK_EPOLL, MTCP_SOCK_PIPE, };用户态 EpollmTCP实现的epoll相对于内核版本也简化地多,控制结构struct mtcp_epoll如下:struct mtcp_epoll{ struct event_queue *usr_queue; struct event_queue *usr_shadow_queue; struct event_queue mtcp_queue; uint8_t waiting; struct mtcp_epoll_stat stat; pthread_cond_t epoll_cond; pthread_mutex_t epoll_lock;};它内部保存了三个队列,分别存储发生了三种类型的事件的套接字。MTCP_EVENT_QUEUE表示协议栈产生的事件,比如LISTEN状态的套接字accept了,ESTABLISH的套接字有数据可以读取了USR_EVENT_QUEUE 表示用户应用的事件,现在就只有PIPE;USR_SHADOW_EVENT_QUEUE表示用户态由于没有处理完,而需要模拟产生的协议栈事件,比如ESTABLISH上的套接字数据没有读取完.TCP流mTCP使用tcp_stream表示一条端到端的TCP流,其中保存了这条流的四元组信息、TCP连接的状态、协议参数和缓冲区位置。tcp_stream存储在每线程的flow table中typedef struct tcp_stream{ socket_map_t socket; // code omitted… uint32_t saddr; / in network order / uint32_t daddr; / in network order / uint16_t sport; / in network order / uint16_t dport; / in network order / uint8_t state; / tcp state */ struct tcp_recv_vars *rcvvar; struct tcp_send_vars sndvar; // code omitted… } tcp_stream;发送控制器mTCP使用struct mtcp_sender完成发送方向的管理,这个结构是每线程每接口的,如果有2个mTCP线程,且有3个网络接口,那么一共就有6个发送控制器struct mtcp_sender{ int ifidx; / TCP layer send queues */ TAILQ_HEAD (control_head, tcp_stream) control_list; TAILQ_HEAD (send_head, tcp_stream) send_list; TAILQ_HEAD (ack_head, tcp_stream) ack_list; int control_list_cnt; int send_list_cnt; int ack_list_cnt;};每个控制器内部包含了3个队列,队列中元素是 tcp_streamControl 队列:负责缓存待发送的控制报文,比如SYN-ACK报文Send 队列:负责缓存带发送的数据报文ACK 队列:负责缓存纯ACK报文例子:服务端TCP连接建立流程假设我们的服务端应用在某个应用线程创建了一个epoll套接字和一个监听套接字,并且将这个监听套接字加入epoll,应用进程阻塞在mtcp_epoll_wait(),而mTCP线程在自己的main Loop中循环本机收到客户端发起的连接,收到第一个SYN报文。mTCP线程在main Loop中读取底层IO收到该报文, 在尝试在本线程的flow table搜索后,发现没有此四元组标识的流信息,于是新建一条tcp stream, 此时,这条流的状态为TCP_ST_LISTEN将这条流写入Control队列,状态切换为TCP_ST_SYNRCVD,表示已收到TCP的第一次握手mTCP线程在main Loop中读取Control队列,发现其中有刚刚的这条流,于是将其取出,组装SYN-ACK报文,送到底层IOmTCP线程在main Loop中读取底层收到的对端发来这条流的ACK握手信息,将状态改为TCP_ST_ESTABLISHED(TCP的三次握手完成),然后将这条流塞入监听套接字的accept队列由于监听套接字是加入了epoll的,因此mTCP线程还会将一个MTCP_EVENT_QUEUE事件塞入struct mtcp_epoll的mtcp_queue队列。此时用户线程在mtcp_epoll_wait()就能读取到该事件,然后调用mtcp_epoll_accept()从Control队列读取到连接信息,就能完成连接的建立。参考资料mTCP: a Highly Scalable User-level TCP Stack for Multicore Systems ...

March 19, 2019 · 1 min · jiezi

通过TCP与简易内存数据库进行数据交互的实现

目的运用TCP相关原理,实现一个简单的server端和client端的数据库交互程序,可以将client端输入的指令被server端解析,将返回信息又返送给client端。之前的简单内存数据库的实现:T-Tree、T*-Tree的理解与简单内存数据库的实现TCP/IP,socket等相关计算机网络原理七层网络模型七层网络模型TCP/IPTCP/IP是互联网协议簇的统称。TCP-transmission control protocal-传输控制协议IP-Internet Protocal-因特网协议UDP 是User Datagram Protocol 是无连接类型的传输层协议,socket是什么socket是对TCP/IP协议的封装,socket翻译为套接字,socket是一个接口/插座。TCPIP 是Socket的一种实现,Socket并不只有TCP/IP。两种socket:stream sockets,datagram socketsocket其实不止两种。stream socket:串流式socket。是有连接类型的,网页浏览器所使用的 HTTP 协议是用 stream sockets 取得网页。datagram socket:讯息式socket。是无连接类型的,用于语音通信,视频传输较多。TCP serverserver端socket()函数#include <sys/types.h>#include <sys/socket.h>int socket(int domain, int type, int protocol);domain一个地址描述。目前仅支持AF_INET格式,也就是说ARPA Internet地址格式。type指定socket类型。新套接口的类型描述类型,如TCP(SOCK_STREAM)和UDP(SOCK_DGRAM)。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等。protocol指定协议。套接口所用的协议。如不想指定,可用0。常用的协议有,IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。bind()函数#include <sys/types.h>#include <sys/socket.h>int bind(int sockfd, struct sockaddr *my_addr, int addrlen);bind()将一本地地址与一socket捆绑.sockfdsockfd 是 socket() 传回的 socket file descriptor。my_addrmy_addr是指向包含你的地址资料丶名称及 IP address 的 struct sockaddr 之指针。addrlenaddrlen 是以 byte 为单位的地址长度。connect#include <sys/types.h>#include <sys/socket.h>int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);connect()从client端连接到server端listen(),accept()int listen(int sockfd, int backlog);—————————————————————–#include <sys/types.h>#include <sys/socket.h>int accept(int sockfd, struct sockaddr *addr, socklen_t addrlen);send(),recv()int send(int sockfd, const void msg, int len, int flags);————————————————————int recv(int sockfd, void buf, int len, int flags);send() 会返回实际有送出的 byte 数,可能会少与所要传送的数目。recv()若返回0,则说明远端那边已经关闭了你的连接close()close(sockfd);关闭socket。一个tcp server 的示例代码:#include <netdb.h> #include <netinet/in.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <sys/types.h> #define MAX 80 #define PORT 8080 #define SA struct sockaddr // Function designed for chat between client and server. void func(int sockfd) { char buff[MAX]; int n; // infinite loop for chat for (;;) { bzero(buff, MAX); // read the message from client and copy it in buffer read(sockfd, buff, sizeof(buff)); // print buffer which contains the client contents printf(“From client: %s\t To client : “, buff); bzero(buff, MAX); n = 0; // copy server message in the buffer while ((buff[n++] = getchar()) != ‘\n’) ; // and send that buffer to client write(sockfd, buff, sizeof(buff)); // if msg contains “Exit” then server exit and chat ended. if (strncmp(“exit”, buff, 4) == 0) { printf(“Server Exit…\n”); break; } } } // Driver function int main() { int sockfd, connfd, len; struct sockaddr_in servaddr, cli; // socket create and verification sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == -1) { printf(“socket creation failed…\n”); exit(0); } else printf(“Socket successfully created..\n”); bzero(&servaddr, sizeof(servaddr)); // assign IP, PORT servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(PORT); // Binding newly created socket to given IP and verification if ((bind(sockfd, (SA)&servaddr, sizeof(servaddr))) != 0) { printf(“socket bind failed…\n”); exit(0); } else printf(“Socket successfully binded..\n”); // Now server is ready to listen and verification if ((listen(sockfd, 5)) != 0) { printf(“Listen failed…\n”); exit(0); } else printf(“Server listening..\n”); len = sizeof(cli); // Accept the data packet from client and verification connfd = accept(sockfd, (SA)&cli, &len); if (connfd < 0) { printf(“server acccept failed…\n”); exit(0); } else printf(“server acccept the client…\n”); // Function for chatting between client and server func(connfd); // After chatting close the socket close(sockfd); } client端一个tcp client的示例代码:// Write CPP code here #include <netdb.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #define MAX 80 #define PORT 8080 #define SA struct sockaddr void func(int sockfd) { char buff[MAX]; int n; for (;;) { bzero(buff, sizeof(buff)); printf(“Enter the string : “); n = 0; while ((buff[n++] = getchar()) != ‘\n’) ; write(sockfd, buff, sizeof(buff)); bzero(buff, sizeof(buff)); read(sockfd, buff, sizeof(buff)); printf(“From Server : %s”, buff); if ((strncmp(buff, “exit”, 4)) == 0) { printf(“Client Exit…\n”); break; } } } int main() { int sockfd, connfd; struct sockaddr_in servaddr, cli; // socket create and varification sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == -1) { printf(“socket creation failed…\n”); exit(0); } else printf(“Socket successfully created..\n”); bzero(&servaddr, sizeof(servaddr)); // assign IP, PORT servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = inet_addr(“127.0.0.1”); servaddr.sin_port = htons(PORT); // connect the client socket to server socket if (connect(sockfd, (SA)&servaddr, sizeof(servaddr)) != 0) { printf(“connection with the server failed…\n”); exit(0); } else printf(“connected to the server..\n”); // function for chat func(sockfd); // close the socket close(sockfd); } 通过TCP与简易内存数据库的数据交互为了让client端和server中的内存数据库,通信如果server端和client建立连接之后则进入一个大循环,大循环的退出条件是client端发去“EXIT”,client端随即断开连接。通过TCP和内存数据库通信的所以代码所有代码都在github里:slarsar/ttree_mmdb_tcp_server_client参考GeeksforGeeksBeej’s Guide to Network Programming 简体中文版 ...

March 18, 2019 · 3 min · jiezi

PHP面试常考之网络协议

你好,是我琉忆,欢迎您来到PHP面试专栏。本周(2019.2-25至3-1)的一三五更新的文章如下:周一:PHP面试常考之会话控制周三:PHP面试常考之网络协议周五:PHP面试常考题之会话控制和网络协议送2本书:《PHP程序员面试笔试真题解析》自己整理了一篇面试最爱问的一道题“从网址输入到看到内容背后的流程是什么样的?”的文章,关注公众号:“琉忆编程库”,回复:“http”,我发给你。以下正文的内容来自《PHP程序员面试笔试宝典》书籍,如果转载请保留出处:一、协议和TCP/IP在20世纪80年代,计算机网络诞生,它能够将一台台独立的计算机互相连接,使得位于不同地理位置的计算机之间可以进行通信,实现信息传递和资源共享,形成一组规模大、功能强的计算机系统。不过,计算机要想在网络中正常通信,必须遵守相关网络协议的规则,常用的网络协议有TCP、UDP、IP和HTTP等。协议协议可简单理解为计算机之间的一种约定,好比人与人之间对话所使用的语言。在国内,不同地区的人讲的方言都不同,如果要沟通,就要约定一种大家都会的语言,例如全国通用的普通话,普通话就相当于协议,沟通相当于通信,说话内容相当于数据信息。协议需要具备通用的特征,但在早期,每家计算机厂商都根据自己的标准来生产网络产品,这使得不同厂商制造的计算机之间难以通信,严重影响了用户的日常使用。为了应对这些问题,ISO(国际标准化组织)制定了一套国际标准OSI(开放式系统互联通信参考模型),将通信系统标准化。所谓标准化是指建立技术标准,企业按照这个标准来制造产品,这大大提升了产品的兼容性、互操作性以及易用性。OSI参考模型将复杂的协议分成了7层(见下表),每一层各司其职,并且能独立使用,这相当于软件中的模块化开发,有较强的扩展性和灵活性。分层是一种管理哲学,将同一类功能的网络协议分到一层中,使协议变得灵活可控。在7层OSI模型中,发送方从第7层的应用层到第1层的物理层,由上至下按顺序传输数据,而接收方则从第1层到第7层,由下至上接收数据,如右图所示。TCP/IPTCP/IP是为互联网服务的协议族,它是网络通信协议的统称,由IP、TCP、HTTP和FTP等协议组成。TCP/IP将通信过程抽象为4层,被视为简化的OSI参考模型(如右图所示,左边是OSI参考模型,右边是TCP/IP的模型),但负责维护这套协议族的不是ISO而是IETF(互联网工程任务组)。TCP/IP在标准化过程中注重开放性和实用性,需要标准化的协议会被放进RFC(Request For Comment)文档中,RFC文档详细记录了协议的实现、运用和实验等各方面的内容,并且这些文档可在线浏览。发送的数据会在分层模型内传递,并且每到一层,就会附加该层的包首部,包首部包含了该层协议的相关信息,例如MAC地址、IP地址和端口号。下图描绘了从传输层到互联网层,分别附加了TCP包首部和IP包首部。互联网一词现在已经家喻户晓,它是由许多网络互联构成的一个巨型网络。早期的网络仅仅是连接计算机,而现代的互联网连接的却是全世界的人。互联网已经不再是单纯的以数据为核心,而是以人为中心,渗透到生活中的方方面面,颠覆了许多传统模式,例如足不出户就能购物、社交或娱乐。 自己整理了一篇面试最爱问的一道题“从网址输入到看到内容背后的流程是什么样的?”的文章,关注公众号:“琉忆编程库”,回复:“http”,我发给你。二、HTTPHTTP(HyperText Transfer Protocol)即超文本传输协议,是一种获取网络资源(例如图像、HTML文档)的应用层协议,它是互联网数据通信的基础,由请求和响应构成(如右图所示)。通常,客户端发起HTTP请求(在请求报文中会指定资源的URL),然后用传输层的TCP协议建立连接,最后服务器响应请求,做出应答,回传数据报文。HTTP自问世到现在,经历了几次版本迭代,目前主流的版本是HTTP/1.1,新一代HTTP/2.0是HTTP/1.1的升级版,各方面都超越了前者,但新技术要做到软硬件兼容还需要假以时日。HTTP协议有三个特征,分别是持久连接、管道化以及无状态。(1)持久连接在HTTP的早期版本中,一次HTTP通信完成后就会断开连接,下一次再重新连接,如右图所示。在当时请求资源并不多的情况下,并不会造成大问题。但随着HTTP的普及,请求的资源越来越庞大,例如一个HTML文档中可能会包含多个CSS文件、JavaScript文件、图像甚至视频,如果还这么操作,会造成巨大的通信开销。为了解决上述问题,提出了持久连接,只要通信两端的任意一端没有明确提出断开,就保持连接状态,以便下一次通信复用该连接,这避免了重复建立和断开连接所造成的开销,加速了页面呈现,如下图所示。(2)管道化管道化是建立在持久连接上的进一步性能优化。过去,请求必须按照先进先出的队列顺序,也就是发送请求后,要等待并接收到响应,才能再继续下一个请求。启用管道化后,就会将队列顺序迁移到服务器,这样就能同时发送多个请求,然后服务器再按顺序一个接一个地响应,如下图所示。 (3)状态管理HTTP是一种无状态协议,请求和响应一一对应,不会出现两个请求复用一个响应的情况(如下图所示)。也就是说,每个请求都是独立的,即使在同一条连接中,请求之间也没有联系。 在有些业务场景中,需要请求有状态,例如后台登录。成功登录后就得保存登录状态,否则每次跳转进入其他页面都会要求重新登录。为了能管理状态,引入了Cookie技术,Cookie技术能让请求和响应的报文都附加Cookie信息,客户端将Cookie值发送出去,服务器接收并处理这个值,最终就能得到客户端的状态信息。预告:本周五更新PHP面试常考题之会话控制和网络协议,敬请期待。以上内容摘自《PHP程序员面试笔试宝典》书籍,该书已在天猫、京东、当当等电商平台销售。更多PHP相关的面试知识、考题可以关注公众号获取:琉忆编程库对本文有什么问题或建议都可以进行留言,我将不断完善追求极致,感谢你们的支持。

February 27, 2019 · 1 min · jiezi

TCP socket和web socket的区别

小编先习惯性的看了下某中文百科网站对Web Socket的介绍,觉得很囧。如果大家按照这个答案去参加BAT等互联网公司的前端开发面试,估计会被鄙视。还是让我们阅读一些英文材料吧。让我们直接看stackoverflow上的原文,然后翻译:原文地址:https://stackoverflow.com/que…这个讨论有超过8万的阅读量。首先我们来阅读这段有166个赞的回答:When you send bytes from a buffer with a normal TCP socket, the send function returns the number of bytes of the buffer that were sent. 当我们向一个通常的TCP套接字发送一段来自内存buffer中的字节数据时,send系统调用返回的是实际发送的字节数。If it is a non-blocking socket or a non-blocking send then the number of bytes sent may be less than the size of the buffer. 如果发送数据的目的方套接字是一个非阻塞套接字或者是对写操作非阻塞的套接字,那么send返回的已发送字节数可能小于buffer中待发送字节数。If it is a blocking socket or blocking send, then the number returned will match the size of the buffer but the call may block. 如果是阻塞套接字,两者会相等,因为顾名思义,如果send系统调用没有把所有待发送数据全部发送,则API调用不会返回。With WebSockets, the data that is passed to the send method is always either sent as a whole “message” or not at all. Also, browser WebSocket implementations do not block on the send call.而Web socket和TCP socket的区别,从发送的数据来看,不再是一系列字节,而是按照一个完整的"消息体"发送出去的,这个"消息体"无法进一步再分割,要么全部发送成功,要么压根就不发送,不存在像TCP套接字非阻塞操作那样出现部分发送的情况。换言之,Web Socket里对套接字的操作是非阻塞操作。这个区别在维基百科上也有清晰阐述:Websocket differs from TCP in that it enables a stream of messages instead of a stream of bytes再来看接收方的区别。原文:But there are more important differences on the receiving side of things. When the receiver does a recv (or read) on a TCP socket, there is no guarantee that the number of bytes returned correspond to a single send (or write) on the sender side. It might be the same, it may be less (or zero) and it might even be more (in which case bytes from multiple send/writes are received). With WebSockets, the receipt of a message is event driven (you generally register a message handler routine), and the data in the event is always the entire message that the other side sent.同理,在TCP套接字的场景下,接收方从TCP套接字读取的字节数,并不一定等于发送方调用send所发送的字节数。而WebSocket呢?WebSocket的接收方从套接字读取数据,根本不是像TCP 套接字那样直接用recv/read来读取, 而是采取事件驱动机制。即应用程序注册一个事件处理函数,当web socket的发送方发送的数据在接收方应用从内核缓冲区拷贝到应用程序层已经处于可用状态时 ,应用程序注册的事件处理函数以回调(callback)的方式被调用。看个例子:我通过WebSocket发送一个消息“汪子熙”:在调试器里看到的这个字符串作为回调函数的输入参数注入到函数体内:Chrome开发者工具里观察到的WebSocket消息体:下次面试被面试官问到TCP和WebSocket套接字的区别,相信大家应该能够知道如何回答了。要获取更多Jerry的原创文章,请关注公众号"汪子熙": ...

February 24, 2019 · 2 min · jiezi

docker swarm mode 下容器重启IP引发的 CLOSE_WAIT 问题

问题问题简述如下图. server docker restart后, client端写入的日志丢失, 并且无报错.因为不支持时序图, 把时序图代码嵌入在代码里.sequenceclient-&gt;server: log_dataclient-&gt;server: log_dataserver-&gt;server: docker restartserver-&gt;client: finclient-&gt;server: log_data loss without errortcp state diagram问题定位过程为什么卡在CLOSE_WAIT.看tcp状态转换图, 可以看到client收到了fin, 一直没有recv, 一直卡在CLOSE_WAIT. 和实际的代码是吻合的. 那么, 为什么在server docker restart 引发CLOSE_WAIT后, client发消息仍然不报错呢?因为:tcp协议允许client在收到fin后, 继续发送消息.server 在docker restart后 ip 改变, client还是往原来的ip发送消息, 没有主机通知client rst, 导致消息在系统buffer里积压.积压信息如下:root@9eeaefa7fe57:/# netstat -nap | grep 27017 | grep 10.0.0tcp 1 402 10.0.0.186:62281 10.0.0.16:27017 CLOSE_WAIT 4308/serverroot@9eeaefa7fe57:/# netstat -nap | grep 27017 | grep 10.0.0tcp 1 70125 10.0.0.186:62281 10.0.0.16:27017 CLOSE_WAIT 4308/server此时, 在elixir socket接口层面来看, 不管socket的状态, 还是发送, 都是ok的.iex(client@client.)25> socket |> :inet.port{:ok, 57395}iex(client@client.)26> socket |> :gen_tcp.send(“aaa”):ok如果主动close, 则会进入LAST_ACK状态iex(client@client.)27> socket |> :gen_tcp.close() :okroot@9eeaefa7fe57:/# netstat -nap | grep 27017 | grep 10.0.0tcp 1 70126 10.0.0.186:62281 10.0.0.16:27017 LAST_ACK - CLOSE_WAIT的恢复如果代码还是只发不收. 是检测不到CLOSE_WAIT的. 显然, 应用层心跳是一个解决方案. 那么, 不使用心跳, 只发不收的情况下, 什么时候才能检测到错误呢?send buffer 满todo 深究tcp keepalive, 不使用 keepalive情况下的 tcp 最大链接空闲时间. ...

February 22, 2019 · 1 min · jiezi

记一次性能优化,单台4核8G机器支撑5万QPS

前言这篇文章的主题是记录一次Python程序的性能优化,在优化的过程中遇到的问题,以及如何去解决的。为大家提供一个优化的思路,首先要声明的一点是,我的方式不是唯一的,大家在性能优化之路上遇到的问题都绝对不止一个解决方案。如何优化首先大家要明确的一点是,脱离需求谈优化都是耍流氓,所以有谁跟你说在xx机器上实现了百万并发,基本上可以认为是不懂装懂了,单纯的并发数完全是无意义的。其次,我们优化之前必须要有一个目标,需要优化到什么程度,没有明确目标的优化是不可控的。再然后,我们必须明确的找出性能瓶颈在哪里,而不能漫无目的的一通乱搞。需求描述这个项目是我在上家公司负责一个单独的模块,本来是集成在主站代码中的,后来因为并发太大,为了防止出现问题后拖累主站服务,所有由我一个人负责拆分出来。对这个模块的拆分要求是,压力测试QPS不能低于3万,数据库负责不能超过50%,服务器负载不能超过70%, 单次请求时长不能超过70ms,错误率不能超过5%。环境的配置如下:服务器:4核8G内存,centos7系统,ssd硬盘数据库:Mysql5.7,最大连接数800缓存: redis, 1G容量。以上环境都是购买自腾讯云的服务。压测工具:locust,使用腾讯的弹性伸缩实现分布式的压测。需求描述如下:用户进入首页,从数据库中查询是否有合适的弹窗配置,如果没有,则继续等待下一次请求、如果有合适的配置,则返回给前端。这里开始则有多个条件分支,如果用户点击了弹窗,则记录用户点击,并且在配置的时间内不再返回配置,如果用户未点击,则24小时后继续返回本次配置,如果用户点击了,但是后续没有配置了,则接着等待下一次。重点分析根据需求,我们知道了有几个重要的点,1、需要找出合适用户的弹窗配置,2、需要记录用户下一次返回配置的时间并记录到数据库中,3、需要记录用户对返回的配置执行了什么操作并记录到数据库中。调优我们可以看到,上述三个重点都存在数据库的操作,不只有读库,还有写库操作。从这里我们可以看到如果不加缓存的话,所有的请求都压到数据库,势必会占满全部连接数,出现拒绝访问的错误,同时因为sql执行过慢,导致请求无法及时返回。所以,我们首先要做的就是讲写库操作剥离开来,提升每一次请求响应速度,优化数据库连接。整个系统的架构图如下:将写库操作放到一个先进先出的消息队列中来做,为了减少复杂度,使用了redis的list来做这个消息队列。然后进行压测,结果如下:QPS在6000左右502错误大幅上升至30%,服务器cpu在60%-70%之间来回跳动,数据库连接数被占满tcp连接数为6000左右,很明显,问题还是出在数据库,经过排查sql语句,查询到原因就是找出合适用户的配置操作时每次请求都要读取数据库所导致的连接数被用完。因为我们的连接数只有800,一旦请求过多,势必会导致数据库瓶颈。好了,问题找到了,我们继续优化,更新的架构如下我们将全部的配置都加载到缓存中,只有在缓存中没有配置的时候才会去读取数据库。接下来我们再次压测,结果如下:QPS压到2万左右的时候就上不去了,服务器cpu在60%-80%之间跳动,数据库连接数为300个左右,每秒tpc连接数为1.5万左右。这个问题是困扰我比较久的一个问题,因为我们可以看到,我们2万的QPS,但是tcp连接数却并没有达到2万,我猜测,tcp连接数就是引发瓶颈的问题,但是因为什么原因所引发的暂时无法找出来。这个时候猜测,既然是无法建立tcp连接,是否有可能是服务器限制了socket连接数,验证猜测,我们看一下,在终端输入ulimit -n命令,显示的结果为65535,看到这里,觉得socket连接数并不是限制我们的原因,为了验证猜测,将socket连接数调大为100001.再次进行压测,结果如下:QPS压到2.2万左右的时候就上不去了,服务器cpu在60%-80%之间跳动,数据库连接数为300个左右,每秒tpc连接数为1.7万左右。虽然有一点提升,但是并没有实质性的变化,接下来的几天时间,我发现都无法找到优化的方案,那几天确实很难受,找不出来优化的方案,过了几天,再次将问题梳理了一遍,发现,虽然socket连接数足够,但是并没有全部被用上,猜测,每次请求过后,tcp连接并没有立即被释放,导致socket无法重用。经过查找资料,找到了问题所在,tcp链接在经过四次握手结束连接后并不会立即释放,而是处于timewait状态,会等待一段时间,以防止客户端后续的数据未被接收。好了,问题找到了,我们要接着优化,首先想到的就是调整tcp链接结束后等待时间,但是linux并没有提供这一内核参数的调整,如果要改,必须要自己重新编译内核,幸好还有另一个参数net.ipv4.tcp_max_tw_buckets, timewait 的数量,默认是 180000。我们调整为6000,然后打开timewait快速回收,和开启重用,完整的参数优化如下#timewait 的数量,默认是 180000。net.ipv4.tcp_max_tw_buckets = 6000net.ipv4.ip_local_port_range = 1024 65000#启用 timewait 快速回收。net.ipv4.tcp_tw_recycle = 1#开启重用。允许将 TIME-WAIT sockets 重新用于新的 TCP 连接。net.ipv4.tcp_tw_reuse = 1我们再次压测,结果显示:QPS5万,服务器cpu70%,数据库连接正常,tcp连接正常,响应时间平均为60ms,错误率为0%。结语到此为止,整个服务的开发、调优、和压测就结束了。回顾这一次调优,得到了很多经验,最重要的是,深刻理解了web开发不是一个独立的个体,而是网络、数据库、编程语言、操作系统等多门学科结合的工程实践,这就要求web开发人员有牢固的基础知识,否则出现了问题还不知道怎么分析查找。ps:服务端开启了 tcp_tw_recycle 和 tcp_tw_reuse是会导致一些问题的,我们为了优化选择牺牲了一部分,获得另一部分,这也是我们要明确的,具体的问题可以查看耗子叔的文章TCP 的那些事儿(上)

January 31, 2019 · 1 min · jiezi

web开发中,必须要了解的HTTP相关知识

本文已同步到github, web开发中,必须要了解的HTTP相关知识,欢迎收藏,欢迎start。本文主要记录与HTTP相关的具体概念和知识,关于HTTP协议的诞生和历史发展,不多做介绍,自己但是既然是写HTTP,顺带说两句,上下文也能衔接的上。CERN(欧洲核子研究组织)的蒂姆 • 伯纳斯 - 李(Tim BernersLee)博士提出了一种能让远隔两地的研究者们共享知识的设想,于是HTTP慢慢的诞生了。另外,HTTP协议是无状态可以,于是为了保存用户的状态,cookie诞生了。HTTP协议是建立在TCP连接之上的,当浏览器输入URL进行访问,浏览器冲URL中解析出主机名和端口,浏览器建立一条与web服务器的连接,然后才进行http请求。TCP连接的建立与终止建立TCP连接(三次握手)在客户端与服务端进行http通信之前,需要建立TCP连接,这时需要三次握手(1) 请求新的TCP连接,客户端发送一个小的TCP分组,这个分组设置一个特殊的SYN标记,表明是一个客户端请求。(2) 如果服务器接受这个连接,就会对一些连接参数进行计算,并向客户端回送一个TCP分组,发送SY和ACK标记,表明连接请求已经被接受(3) 最后,客户端向服务器回送一条确认消息,通知服务器连接已经建立。断开TCP连接(四次断开)建立一个连接需要三次握手,而终止一个连接要经过4次握手。这由TCP的半关闭(half-close)造成的。既然一个TCP连接是全双工(即数据在两个方向上能同时传递),因此每个方向必须单独地进行关闭。这原则就是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向连接。当一端收到一个FIN,它必须通知应用层另一端几经终止了那个方向的数据传送。发送FIN通常是应用层进行关闭的结果。(1) 客户端发送FIN标记到服务器,表明客户端发起关闭连接(2) 服务器接收客户端的FIN标记并,向客户端发送FIN的ACK确认标记(3) 服务器发送FIN到客户端,服务器关闭连接(4) 服务器端发送一个FIN的ACK确认标记,确认连接关闭建立持久连接的请求和响应交互:使用wireshark进行数据抓包:这里向大家推荐一款抓包软件Wireshark,可以用来分析TCP连接的建立和断开过程,以及抓取HTTP请求和相应的信息等,下面是我进行一次客户端和服务端通信的抓包数据截图:HTTP报文HTTP协议报文是应用程序之间发送的数据块,也就是客户端和服务端用于交互的信息。客户端的报文叫做请求报文,服务器端的报文叫做响应报文。HTTP报文组成HTTP报文由起始行、首部和实体的主体(也称报文主体或主体)组成。起始行和首部以一个回车符和换行符作为结束,主体部分可以是二进制数据,也可以为空。1. 起始行请求报文起始行:请求报文起始行说明了要做什么,由请求方法 、请求URI和协议版本构成。GET /index.html HTTP/1.1响应报文起始行:响应报文的起始行,由协议版本、状态码和原因短语构成。HTTP/1.1 200 OK // OK就是原因短语2. 首部首部字段分类1.通用首部客户端和服务端都可以使用的首部通用首部字段表:2.请求首部请求报文特有的首部,为服务器提供了一些额外的信息,补充了请求的附加内容、客户端信息、响应内容相关的优先级等信息。请求首部字段3.响应首部响应报文特有的字段响应首部字段表:4.实体首部用于针对请求报文和响应报文主体部分使用的首部5.扩展首部扩展首部是非标准的首部,由应用程序开发者创建,但还未添加到已批准的HTTP标准中去。http状态码状态码的职责是当客户端向服务器端发送请求时,描述返回的请求结果。借助状态码,用户可以知道服务器端是正常处理了请求,还是出现了错误。状态码分类:状态码区间类别100~199信息性状态码200~299成功状态码300~399重定向状态码400~499客户端错误状态码500~599服务器错误状态码常用状态码列表:状态码原因短语含义200OK表示从客户端发来的请求在服务器端被正常处理了204No Content该状态码代表服务器接收的请求已成功处理,但在返回的响应报文中不含实体的主体部分。另外,也不允许返回任何实体的主体。301Moved Permanently永久重定向,该状态码表示请求的资源已被分配了新的 URI,以后应使用资源现在所指的 URI302Found临时性重定向,该状态码表示请求的资源已被分配了新的 URI,希望用户(本次)能使用新的 URI 访问303See Other303 状态码和 302 Found 状态码有着相同的功能,但 303 状态码明确表示客户端应当采用 GET 方法获取资源,这点与 302 状态码有区别304Not Modified缓存307Temporary Redirect临时重定向,和302一样400Bad Request该状态码表示请求报文中存在语法错误。当错误发生时,需修改请求的内容后再次发送请求。另外,浏览器会像 200 OK 一样对待该状态码401Unauthorized该状态码表示发送的请求需要有通过 HTTP 认证(BASIC 认证、DIGEST 认证)的认证信息403Forbidden该状态码表明对请求资源的访问被服务器拒绝了404Not Found该状态码表明服务器上无法找到请求的资源500Internal Server Error该状态码表明服务器端在执行请求时发生了错误。也有可能是 Web应用存在的 bug 或某些临时的故障502Bad Gateway网关错误503Service Unavailable该状态码表明服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。如果事先得知解除以上状况需要的时间,最好写入RetryAfter 首部字段再返回给客户端HTTP中不同场景下,首部字段的作用1. CORS 跨域资源共享跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。 –MDN下面使用nodejs来搭建一个简单的服务器,来介绍一个跨域问题的解决方法// index.html<!DOCTYPE html><html><head> <meta charset=“utf-8” /> <title>CORS</title></head><body> Hello World<script> fetch(‘http://127.0.0.1:8081’)</script></body></html>// server.jsconst http = require(‘http’)http.createServer(function(req, res) { res.writeHead(‘200’, { ‘Access-Control-Allow-Origin’: ‘http://localhost:8082’ })}).listen(8081)在源地址为 http://localhost:8082 下,请求http://localhost:8081,是跨域请求,浏览器会自动在request Header中发送Origin首部字段,并把值设置为来自哪个源,本例为http://localhost:8081。服务器需要在响应头中设置Access-Control-Allow-Origin,来告知浏览器可以处理返回的数据。如果响应头中不设置Access-Control-Allow-Origin则会报错,但是返回状态码为200,跨域实际上是浏览器本身的一个安全机制。// server2.js// 启动8082端口服务,在浏览器中访问http://127.0.0.1:8082,会返回index.html内容const http = require(‘http’)const fs = require(‘fs’)http.createServer(function(req, res) { var page = fs.readFileSync(‘index.html’, ‘utf-8’) res.writeHead(200, { ‘Content-Type’: ’text/html’ }) res.end(page)}).listen(8082)关于CORS跨域请求的分类:1.简单请求:需要同时满足以下的条件就是简单请求(1)请求方法:GET、POST、HEAD(2)请求头不能为以下其他字段之外AcceptAccept-LanguageContent-LanguageContent-Type的值必须为application/x-www-form-urlencoded、multipart/form-data、text/plain之一2.非简单请求:非简单请求是当请求信息不满足简单请求的条件,浏览器就发送方法为OPTIONS的预请求,包含自己请求的方法及需要使用的请求头字段,在得到服务器响应允许之后,浏览器会按照想要使用的请求方法及头信息再发一次请求。现在修改以下上面的例子:// index.html<!DOCTYPE html><html><head> <meta charset=“utf-8” /> <title>CORS</title></head><body> Hello World<script> fetch(‘http://127.0.0.1:8081’, { method: ‘PUT’, headers: { X-Coustom-Head: ‘abc’ } })</script></body></html>// server.jsconst http = require(‘http’)http.createServer(function(req, res) { res.writeHead(‘200’, { ‘Access-Control-Allow-Origin’: ‘http://localhost:8082’ })}).listen(8081)如果服务端不进行相应的设置告诉浏览器允许跨域访问则会报错但是预请求返回状态码为200// server2.js// 启动8082端口服务,在浏览器中访问http://127.0.0.1:8082,会返回index.html内容const http = require(‘http’)const fs = require(‘fs’)http.createServer(function(req, res) { var page = fs.readFileSync(‘index.html’, ‘utf-8’) res.writeHead(200, { ‘Content-Type’: ’text/html’ }) res.end(page)}).listen(8082)现在我们修改以下 server.js// server.jsconst http = require(‘http’)http.createServer(function(req, res) { res.writeHead(‘200’, { ‘Access-Control-Allow-Origin’: ‘http://localhost:8082’, ‘Access-Control-Allow-Headers’: ‘X-Coustom-Head’, ‘Access-Control-Allow-Methods’: ‘PUT’ })}).listen(8081)重新启动node服务,访问http://locaohost:8082,可以看到在发送预请求后,浏览器会继续发送PUT请求关于CORS的其他设置这里就不多做介绍了,这里主要是用一个例子来说明以下http不同字段在跨域场景下的作用。2. 缓存 (Cache-Control的作用)本例依旧用node服务来讲解一下Cache-Control的作用,新建三个文件// index.html<!DOCTYPE html><html><head> <meta charset=“utf-8” /> <meta http-equiv=“X-UA-Compatible” content=“IE=edge”> <title>Cache-Control</title> <meta name=“viewport” content=“width=device-width, initial-scale=1”></head><body> <script src="/script.js"></script></body></html>// script.jsconsole.log(‘script.js’)// server.jsconst http = require(‘http’)const fs = require(‘fs’)http.createServer(function(req, res) { if (req.url === ‘/’) { let page = fs.readFileSync(‘index2.html’, ‘utf-8’) res.writeHead(200, { ‘Content-Type’: ’text/html’ }) res.end(page) } if (req.url === ‘/script.js’) { let page = fs.readFileSync(‘script.js’, ‘utf-8’) res.writeHead(200, { ‘Content-Type’: ’text/javascript’, ‘Cache-Control’: ‘max-age=10’ }) res.end(page) }}).listen(8082)在第一次请求script.js资源时,向服务器发送请求由于服务器返回响应时,设置Cache-Control: ‘max-age=10’时,修改script.js后,在10秒内继续请求script.js资源,则从缓存中读取,而打印信息依旧是’script.js’// script.jsconsole.log(‘script-modify.js’)更多关于缓存的知识在这里也不多介绍了,贴两张cache-control字段在请求和响应时可以设置的值和其表示含义:1. Cache-Control 缓存请求指令:2. Cache-Control 缓存响应指令:3. cookie指某些网站为了辨别用户身份、进行 session 跟踪而储存在用户本地终端上的数据(通常经过加密),当下次再访问时浏览器会将该网站的cookie发回给服务器端。cookie如果不设置过期时间,随浏览器关闭而失效,如果有需要可以设置过期时间,继续上代码例子????,新建两个文件如下// index.html<!DOCTYPE html><html><head> <meta charset=“utf-8” /> <title>Cookie</title></head><body> Cookie<script> console.log(document.cookie)</script></body></html>// server.jsconst http = require(‘http’)const fs = require(‘fs’)http.createServer(function(req, res) { if (req.url === ‘/’) { let page = fs.readFileSync(‘index.html’, ‘utf-8’) res.writeHead(200, { ‘Content-Type’: ’text/html’, ‘Set-Cookie’: [‘a=1;max-age:5’, ‘b=2;HTTPOnly’] }) res.end(page) }}).listen(8082)启动node服务,访问localhost:8082,可以看到成功设置了cookie并在响应头信息中设置了Set-Cookie字段另外关注以下打印信息,发现只有a=1,因为给b=2设置了HttpOnly属性,不允许JavaScript通过脚本来获取到cookie信息由于当再次请求时,cookie会在请求头中发送到服务器,由于cookie a=1设置了5秒后过期,在5秒后刷新页面,请求头中的cookie只有a=1在5秒内发送二次请求,cookie a=1没有失效,在请求头中cookie a=1;b=2都会发送到服务器另外对于cookie的其他设置如expires、domain等在这里也不多做介绍了4. 重定向当服务端返回301、302、307等状态码都代表资源已经被重定向到其他位置,301表示永久改变URI,302和307表示临时重定向到某个URI本例举一个服务器返回302状态码的例子,直接上代码:// server.jsconst http = require(‘http’);const fs = require(‘fs’)http.createServer((req, res) => { if (req.url === ‘/’) { res.writeHead(302, { ‘Location’: ‘/redirect’ }) res.end() } if (req.url === ‘/redirect’) { res.end(‘redirect’) }}).listen(8082);访问localhost:8082, 服务器返回302状态码时,在相应头中设置Location首部字段,浏览器会继续发送请求到重定向的地址HTTP与HTTPS的区别首先说一下什么是HTTPSHTTPS(全称:Hyper Text Transfer Protocol over Secure Socket Layer 或 Hypertext Transfer Protocol Secure,超文本传输安全协议),是以安全为目标的HTTP通道,简单讲是HTTP的安全版。即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。 –百度百科HTTPS = HTTP+ 加密 + 认证 + 完整性保护最主要是在应用层和传输层中间加了一个SSL(安全套阶层),通常,HTTP 直接和 TCP 通信。当使用 SSL 时,则演变成先和 SSL 通信,再由 SSL 和 TCP 通信。HTTP与HTTPS的区别:(1) HTTP是明文传输,HTTPS是经过SSL加密后进行传输,只有客户端和服务端根据公钥和私钥进行加密和解密能看到,中间任何传输环节无法获取传输信息,所以HTTPS比HTTP安全(2) HTTPS需要到数字证书认证机构进行购买(3) HTTP服务器默认端口是80,HTTPS服务器默认端口是443本文主要介绍HTTP,关于HTTPS主要就介绍这么多吧。HTTP2本想说点HTTP2的知识,奈何自己是小白,放个百度百科的链接吧 HTTP2。等后续随着不断的学习,再回来更新本文。另外放一个HTTP1.1与HTTP2请求与相应对比的demo的链接HTTP/2 is the future of the Web, and it is here!最后,本文主要介绍了一些HTTP在web开发中的基础知识,关于概念和图解流程的截图基本上都是来自《TCP/IP详解 卷1:协议》、《图解HTTP》、《HTTP权威指南》,可放心参考。笔者功力实在有限,如有问题,请大家多多指出,相互学习和进步,也希望通过我的学习与实践过程,整理出的笔记能对大家有所帮助,谢谢。本文参考链接:TCP/IP详解 卷1:协议图解HTTPHTTP权威指南跨域资源共享 CORS 详解–阮一峰 ...

January 22, 2019 · 2 min · jiezi

web开发中,必须要了解的HTTP相关知识

本文已同步到github, web开发中,必须要了解的HTTP相关知识,欢迎收藏,欢迎start。本文主要记录与HTTP相关的具体概念和知识,关于HTTP协议的诞生和历史发展,不多做介绍,自己但是既然是写HTTP,顺带说两句,上下文也能衔接的上。CERN(欧洲核子研究组织)的蒂姆 • 伯纳斯 - 李(Tim BernersLee)博士提出了一种能让远隔两地的研究者们共享知识的设想,于是HTTP慢慢的诞生了。另外,HTTP协议是无状态可以,于是为了保存用户的状态,cookie诞生了。HTTP协议是建立在TCP连接之上的,当浏览器输入URL进行访问,浏览器冲URL中解析出主机名和端口,浏览器建立一条与web服务器的连接,然后才进行http请求。TCP连接的建立与终止建立TCP连接(三次握手)在客户端与服务端进行http通信之前,需要建立TCP连接,这时需要三次握手(1) 请求新的TCP连接,客户端发送一个小的TCP分组,这个分组设置一个特殊的SYN标记,表明是一个客户端请求。(2) 如果服务器接受这个连接,就会对一些连接参数进行计算,并向客户端回送一个TCP分组,发送SY和ACK标记,表明连接请求已经被接受(3) 最后,客户端向服务器回送一条确认消息,通知服务器连接已经建立。断开TCP连接(四次断开)建立一个连接需要三次握手,而终止一个连接要经过4次握手。这由TCP的半关闭(half-close)造成的。既然一个TCP连接是全双工(即数据在两个方向上能同时传递),因此每个方向必须单独地进行关闭。这原则就是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向连接。当一端收到一个FIN,它必须通知应用层另一端几经终止了那个方向的数据传送。发送FIN通常是应用层进行关闭的结果。(1) 客户端发送FIN标记到服务器,表明客户端发起关闭连接(2) 服务器接收客户端的FIN标记并,向客户端发送FIN的ACK确认标记(3) 服务器发送FIN到客户端,服务器关闭连接(4) 服务器端发送一个FIN的ACK确认标记,确认连接关闭建立持久连接的请求和响应交互:使用wireshark进行数据抓包:这里向大家推荐一款抓包软件Wireshark,可以用来分析TCP连接的建立和断开过程,以及抓取HTTP请求和相应的信息等,下面是我进行一次客户端和服务端通信的抓包数据截图:HTTP报文HTTP协议报文是应用程序之间发送的数据块,也就是客户端和服务端用于交互的信息。客户端的报文叫做请求报文,服务器端的报文叫做响应报文。HTTP报文组成HTTP报文由起始行、首部和实体的主体(也称报文主体或主体)组成。起始行和首部以一个回车符和换行符作为结束,主体部分可以是二进制数据,也可以为空。1. 起始行请求报文起始行:请求报文起始行说明了要做什么,由请求方法 、请求URI和协议版本构成。GET /index.html HTTP/1.1响应报文起始行:响应报文的起始行,由协议版本、状态码和原因短语构成。HTTP/1.1 200 OK // OK就是原因短语2. 首部首部字段分类1.通用首部客户端和服务端都可以使用的首部通用首部字段表:2.请求首部请求报文特有的首部,为服务器提供了一些额外的信息,补充了请求的附加内容、客户端信息、响应内容相关的优先级等信息。请求首部字段3.响应首部响应报文特有的字段响应首部字段表:4.实体首部用于针对请求报文和响应报文主体部分使用的首部5.扩展首部扩展首部是非标准的首部,由应用程序开发者创建,但还未添加到已批准的HTTP标准中去。http状态码状态码的职责是当客户端向服务器端发送请求时,描述返回的请求结果。借助状态码,用户可以知道服务器端是正常处理了请求,还是出现了错误。状态码分类:状态码区间类别100~199信息性状态码200~299成功状态码300~399重定向状态码400~499客户端错误状态码500~599服务器错误状态码常用状态码列表:状态码原因短语含义200OK表示从客户端发来的请求在服务器端被正常处理了204No Content该状态码代表服务器接收的请求已成功处理,但在返回的响应报文中不含实体的主体部分。另外,也不允许返回任何实体的主体。301Moved Permanently永久重定向,该状态码表示请求的资源已被分配了新的 URI,以后应使用资源现在所指的 URI302Found临时性重定向,该状态码表示请求的资源已被分配了新的 URI,希望用户(本次)能使用新的 URI 访问303See Other303 状态码和 302 Found 状态码有着相同的功能,但 303 状态码明确表示客户端应当采用 GET 方法获取资源,这点与 302 状态码有区别304Not Modified缓存307Temporary Redirect临时重定向,和302一样400Bad Request该状态码表示请求报文中存在语法错误。当错误发生时,需修改请求的内容后再次发送请求。另外,浏览器会像 200 OK 一样对待该状态码401Unauthorized该状态码表示发送的请求需要有通过 HTTP 认证(BASIC 认证、DIGEST 认证)的认证信息403Forbidden该状态码表明对请求资源的访问被服务器拒绝了404Not Found该状态码表明服务器上无法找到请求的资源500Internal Server Error该状态码表明服务器端在执行请求时发生了错误。也有可能是 Web应用存在的 bug 或某些临时的故障502Bad Gateway网关错误503Service Unavailable该状态码表明服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。如果事先得知解除以上状况需要的时间,最好写入RetryAfter 首部字段再返回给客户端HTTP中不同场景下,首部字段的作用1. CORS 跨域资源共享跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。 –MDN下面使用nodejs来搭建一个简单的服务器,来介绍一个跨域问题的解决方法// index.html<!DOCTYPE html><html><head> <meta charset=“utf-8” /> <title>CORS</title></head><body> Hello World<script> fetch(‘http://127.0.0.1:8081’)</script></body></html>// server.jsconst http = require(‘http’)http.createServer(function(req, res) { res.writeHead(‘200’, { ‘Access-Control-Allow-Origin’: ‘http://localhost:8082’ })}).listen(8081)在源地址为 http://localhost:8082 下,请求http://localhost:8081,是跨域请求,浏览器会自动在request Header中发送Origin首部字段,并把值设置为来自哪个源,本例为http://localhost:8081。服务器需要在响应头中设置Access-Control-Allow-Origin,来告知浏览器可以处理返回的数据。如果响应头中不设置Access-Control-Allow-Origin则会报错,但是返回状态码为200,跨域实际上是浏览器本身的一个安全机制。// server2.js// 启动8082端口服务,在浏览器中访问http://127.0.0.1:8082,会返回index.html内容const http = require(‘http’)const fs = require(‘fs’)http.createServer(function(req, res) { var page = fs.readFileSync(‘index.html’, ‘utf-8’) res.writeHead(200, { ‘Content-Type’: ’text/html’ }) res.end(page)}).listen(8082)关于CORS跨域请求的分类:1.简单请求:需要同时满足以下的条件就是简单请求(1)请求方法:GET、POST、HEAD(2)请求头不能为以下其他字段之外AcceptAccept-LanguageContent-LanguageContent-Type的值必须为application/x-www-form-urlencoded、multipart/form-data、text/plain之一2.非简单请求:非简单请求是当请求信息不满足简单请求的条件,浏览器就发送方法为OPTIONS的预请求,包含自己请求的方法及需要使用的请求头字段,在得到服务器响应允许之后,浏览器会按照想要使用的请求方法及头信息再发一次请求。现在修改以下上面的例子:// index.html<!DOCTYPE html><html><head> <meta charset=“utf-8” /> <title>CORS</title></head><body> Hello World<script> fetch(‘http://127.0.0.1:8081’, { method: ‘PUT’, headers: { X-Coustom-Head: ‘abc’ } })</script></body></html>// server.jsconst http = require(‘http’)http.createServer(function(req, res) { res.writeHead(‘200’, { ‘Access-Control-Allow-Origin’: ‘http://localhost:8082’ })}).listen(8081)如果服务端不进行相应的设置告诉浏览器允许跨域访问则会报错但是预请求返回状态码为200// server2.js// 启动8082端口服务,在浏览器中访问http://127.0.0.1:8082,会返回index.html内容const http = require(‘http’)const fs = require(‘fs’)http.createServer(function(req, res) { var page = fs.readFileSync(‘index.html’, ‘utf-8’) res.writeHead(200, { ‘Content-Type’: ’text/html’ }) res.end(page)}).listen(8082)现在我们修改以下 server.js// server.jsconst http = require(‘http’)http.createServer(function(req, res) { res.writeHead(‘200’, { ‘Access-Control-Allow-Origin’: ‘http://localhost:8082’, ‘Access-Control-Allow-Headers’: ‘X-Coustom-Head’, ‘Access-Control-Allow-Methods’: ‘PUT’ })}).listen(8081)重新启动node服务,访问http://locaohost:8082,可以看到在发送预请求后,浏览器会继续发送PUT请求关于CORS的其他设置这里就不多做介绍了,这里主要是用一个例子来说明以下http不同字段在跨域场景下的作用。2. 缓存 (Cache-Control的作用)本例依旧用node服务来讲解一下Cache-Control的作用,新建三个文件// index.html<!DOCTYPE html><html><head> <meta charset=“utf-8” /> <meta http-equiv=“X-UA-Compatible” content=“IE=edge”> <title>Cache-Control</title> <meta name=“viewport” content=“width=device-width, initial-scale=1”></head><body> <script src="/script.js"></script></body></html>// script.jsconsole.log(‘script.js’)// server.jsconst http = require(‘http’)const fs = require(‘fs’)http.createServer(function(req, res) { if (req.url === ‘/’) { let page = fs.readFileSync(‘index2.html’, ‘utf-8’) res.writeHead(200, { ‘Content-Type’: ’text/html’ }) res.end(page) } if (req.url === ‘/script.js’) { let page = fs.readFileSync(‘script.js’, ‘utf-8’) res.writeHead(200, { ‘Content-Type’: ’text/javascript’, ‘Cache-Control’: ‘max-age=10’ }) res.end(page) }}).listen(8082)在第一次请求script.js资源时,向服务器发送请求由于服务器返回响应时,设置Cache-Control: ‘max-age=10’时,修改script.js后,在10秒内继续请求script.js资源,则从缓存中读取,而打印信息依旧是’script.js’// script.jsconsole.log(‘script-modify.js’)更多关于缓存的知识在这里也不多介绍了,贴两张cache-control字段在请求和响应时可以设置的值和其表示含义:1. Cache-Control 缓存请求指令:2. Cache-Control 缓存响应指令:3. cookie指某些网站为了辨别用户身份、进行 session 跟踪而储存在用户本地终端上的数据(通常经过加密),当下次再访问时浏览器会将该网站的cookie发回给服务器端。cookie如果不设置过期时间,随浏览器关闭而失效,如果有需要可以设置过期时间,继续上代码例子????,新建两个文件如下// index.html<!DOCTYPE html><html><head> <meta charset=“utf-8” /> <title>Cookie</title></head><body> Cookie<script> console.log(document.cookie)</script></body></html>// server.jsconst http = require(‘http’)const fs = require(‘fs’)http.createServer(function(req, res) { if (req.url === ‘/’) { let page = fs.readFileSync(‘index.html’, ‘utf-8’) res.writeHead(200, { ‘Content-Type’: ’text/html’, ‘Set-Cookie’: [‘a=1;max-age:5’, ‘b=2;HTTPOnly’] }) res.end(page) }}).listen(8082)启动node服务,访问localhost:8082,可以看到成功设置了cookie并在响应头信息中设置了Set-Cookie字段另外关注以下打印信息,发现只有a=1,因为给b=2设置了HttpOnly属性,不允许JavaScript通过脚本来获取到cookie信息由于当再次请求时,cookie会在请求头中发送到服务器,由于cookie a=1设置了5秒后过期,在5秒后刷新页面,请求头中的cookie只有a=1在5秒内发送二次请求,cookie a=1没有失效,在请求头中cookie a=1;b=2都会发送到服务器另外对于cookie的其他设置如expires、domain等在这里也不多做介绍了4. 重定向当服务端返回301、302、307等状态码都代表资源已经被重定向到其他位置,301表示永久改变URI,302和307表示临时重定向到某个URI本例举一个服务器返回302状态码的例子,直接上代码:// server.jsconst http = require(‘http’);const fs = require(‘fs’)http.createServer((req, res) => { if (req.url === ‘/’) { res.writeHead(302, { ‘Location’: ‘/redirect’ }) res.end() } if (req.url === ‘/redirect’) { res.end(‘redirect’) }}).listen(8082);访问localhost:8082, 服务器返回302状态码时,在相应头中设置Location首部字段,浏览器会继续发送请求到重定向的地址HTTP与HTTPS的区别首先说一下什么是HTTPSHTTPS(全称:Hyper Text Transfer Protocol over Secure Socket Layer 或 Hypertext Transfer Protocol Secure,超文本传输安全协议),是以安全为目标的HTTP通道,简单讲是HTTP的安全版。即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。 –百度百科HTTPS = HTTP+ 加密 + 认证 + 完整性保护最主要是在应用层和传输层中间加了一个SSL(安全套阶层),通常,HTTP 直接和 TCP 通信。当使用 SSL 时,则演变成先和 SSL 通信,再由 SSL 和 TCP 通信。HTTP与HTTPS的区别:(1) HTTP是明文传输,HTTPS是经过SSL加密后进行传输,只有客户端和服务端根据公钥和私钥进行加密和解密能看到,中间任何传输环节无法获取传输信息,所以HTTPS比HTTP安全(2) HTTPS需要到数字证书认证机构进行购买(3) HTTP服务器默认端口是80,HTTPS服务器默认端口是443本文主要介绍HTTP,关于HTTPS主要就介绍这么多吧。HTTP2本想说点HTTP2的知识,奈何自己是小白,放个百度百科的链接吧 HTTP2。等后续随着不断的学习,再回来更新本文。另外放一个HTTP1.1与HTTP2请求与相应对比的demo的链接HTTP/2 is the future of the Web, and it is here!最后,本文主要介绍了一些HTTP在web开发中的基础知识,关于概念和图解流程的截图基本上都是来自《TCP/IP详解 卷1:协议》、《图解HTTP》、《HTTP权威指南》,可放心参考。笔者功力实在有限,如有问题,请大家多多指出,相互学习和进步,也希望通过我的学习与实践过程,整理出的笔记能对大家有所帮助,谢谢。本文参考链接:TCP/IP详解 卷1:协议图解HTTPHTTP权威指南跨域资源共享 CORS 详解–阮一峰 ...

January 21, 2019 · 2 min · jiezi

快速理解TCP和UDP的差异

前言最头疼的问题莫过于到底该选TCP还是UDP作为传输层协议。通过快速对比分析 TCP 和 UDP 的区别,来帮助即时通讯初学者快速了解这些基础的知识点,从而在IM、消息推送等网络通信应用场景中能准确地选择合适的传输层协议。建立连接方式的差异TCP说到 TCP 建立连接,相信大多数人脑海里肯定可以浮现出一个词,没错就是–“三次握手”。TCP 通过“三次握手”来建立连接,再通过“四次挥手”断开一个连接。在每次挥手中 TCP 做了哪些操作呢?流程如下图所示(TCP的三次握手和四次挥手):上图就从客户端和服务端的角度,清楚的展示了 TCP 的三次握手和四次挥手。可以看到,当 TCP 试图建立连接时,三次握手指的是客户端主动触发了两次,服务端触发了一次。我们可以先明确一下 TCP 建立连接并且初始化的目标是什么呢?1初始化资源;2告诉对方我的序列号。所以三次握手的次序是这样子的:client端首先发送一个SYN包告诉Server端我的初始序列号是X;Server端收到SYN包后回复给client一个ACK确认包,告诉client说我收到了;接着Server端也需要告诉client端自己的初始序列号,于是Server也发送一个SYN包告诉client我的初始序列号是Y;Client收到后,回复Server一个ACK确认包说我知道了。其中的 2 、3 步骤可以简化为一步,也就是说将 ACK 确认包和 SYN 序列化包一同发送给 Client 端。到此我们就比较简单的解释了 TCP 建立连接的“三次握手”。UDP我们都知道 TCP 是面向连接的、可靠的、有序的传输层协议,而 UDP 是面向数据报的、不可靠的、无序的传输协议,所以 UDP 压根不会建立什么连接。就好比发短信一样,UDP 只需要知道对方的 ip 地址,将数据报一份一份的发送过去就可以了,其他的作为发送方,都不需要关心。数据发送方式的差异关于 TCP、UDP 之间数据发送的差异,可以体现二者最大的不同之处:TCP:由于 TCP 是建立在两端连接之上的协议,所以理论上发送的数据流不存在大小的限制。但是由于缓冲区有大小限制,所以你如果用 TCP 发送一段很大的数据,可能会截断成好几段,接收方依次的接收。UDP:由于 UDP 本身发送的就是一份一份的数据报,所以自然而然的就有一个上限的大小。那么每次 UDP 发送的数据报大小由哪些因素共同决定呢?UDP协议本身,UDP协议中有16位的UDP报文长度,那么UDP报文长度不能超过2^16=65536;以太网(Ethernet)数据帧的长度,数据链路层的MTU(最大传输单元);socket的UDP发送缓存区大小先来看第一个因素,UDP 本身协议的报文长度为 2^16 - 1,UDP 包头占 8 个字节,IP 协议本身封装后包头占 20 个字节,所以最终长度为: 2^16 - 1 - 20 - 8 = 65507 字节。只看第一个因素有点理想化了,因为 UDP 属于不可靠协议,我们应该尽量避免在传输过程中,数据包被分割。所以这里有一个非常重要的概念 MTU – 也就是最大传输单元。在 Internet 下 MTU 的值为 576 字节,所以在 internet 下使用 UDP 协议,每个数据报最大的字节数为: 576 - 20 - 8 = 548数据有序性的差异TCP对于 TCP 来说,本身 TCP 有着超时重传、错误重传、还有等等一系列复杂的算法保证了 TCP 的数据是有序的,假设你发送了数据 1、2、3,则只要发送端和接收端保持连接时,接收端收到的数据始终都是 1、2、3。UDP而 UDP 协议则要奔放的多,无论 server 端无论缓冲池的大小有多大,接收 client 端发来的消息总是一个一个的接收。并且由于 UDP 本身的不可靠性以及无序性,如果 client 发送了 1、2、3 这三个数据报过来,server 端接收到的可能是任意顺序、任意个数三个数据报的排列组合。可靠性的差异其实大家都知道 TCP 本身是可靠的协议,而 UDP 是不可靠的协议。TCPTCP 内部的很多算法机制让他保持连接的过程中是很可靠的。比如:TCP 的超时重传、错误重传、TCP 的流量控制、阻塞控制、慢热启动算法、拥塞避免算法、快速恢复算法 等等。所以 TCP 是一个内部原理复杂,但是使用起来比较简单的这么一个协议。UDPUDP 是一个面向非连接的协议,UDP 发送的每个数据报带有自己的 IP 地址和接收方的 IP 地址,它本身对这个数据报是否出错,是否到达不关心,只要发出去了就好了。所以来研究下,什么情况会导致 UDP 丢包:数据报分片重组丢失:在文章之前我们就说过,UDP 的每个数据报大小多少最合适,事实上 UDP 协议本身规定的大小是 64kb,但是在数据链路层有 MTU 的限制,大小大概在 5kb,所以当你发送一个很大的 UDP 包的时候,这个包会在 IP 层进行分片,然后重组。这个过程就有可能导致分片的包丢失。UDP 本身有 CRC 检测机制,会抛弃掉丢失的 UDP 包;UDP 缓冲区填满:当 UDP 的缓冲区已经被填满的时候,接收方还没有处理这部分的 UDP 数据报,这个时候再过来的数据报就没有地方可以存了,自然就都被丢弃了。使用场景总结在文章最后的一部分,聊聊 TCP、UDP 使用场景。先来说 UDP 的吧,有很多人都会觉得 UDP 与 TCP 相比,在性能速度上是占优势的。因为 UDP 并不用保持一个持续的连接,也不需要对收发包进行确认。但事实上经过这么多年的发展 TCP 已经拥有足够多的算法和优化,在网络状态不错的情况下,TCP 的整体性能是优于 UDP 的。那在什么时候我们非用 UDP 不可呢?对实时性要求高:比如实时会议,实时视频这种情况下,如果使用 TCP,当网络不好发生重传时,画面肯定会有延时,甚至越堆越多。如果使用 UDP 的话,即使偶尔丢了几个包,但是也不会影响什么,这种情况下使用 UDP 比较好;多点通信:TCP 需要保持一个长连接,那么在涉及多点通讯的时候,肯定需要和多个通信节点建立其双向连接,然后有时在NAT环境下,两个通信节点建立其直接的 TCP 连接不是一个容易的事情,而 UDP 可以无需保持连接,直接发就可以了,所以成本会很低,而且穿透性好。这种情况下使用 UDP 也是没错的。以上我们说了 UDP 的使用场景,在此之外的其他情况,使用 TCP 准没错。毕竟有一句话嘛:when in doubt,use TCP。 ...

January 20, 2019 · 1 min · jiezi

快速理解TCP协议一篇就够

前言TCP 是互联网的核心协议之一,鉴于它的重要性,本文将单独介绍它的基础知识,希望能加深您对TCP协议的理解。TCP 协议的作用互联网由一整套协议构成。TCP 只是其中的一层,有着自己的分工。TCP 是以太网协议和 IP 协议的上层协议,也是应用层协议的下层协议最底层的以太网协议(Ethernet)规定了电子信号如何组成数据包(packet),解决了子网内部的点对点通信。以太网协议解决了局域网的点对点通信但是,以太网协议不能解决多个局域网如何互通,这由 IP 协议解决。IP 协议可以连接多个局域网IP 协议定义了一套自己的地址规则,称为 IP 地址。它实现了路由功能,允许某个局域网的 A 主机,向另一个局域网的 B 主机发送消息。路由器就是基于 IP 协议。局域网之间要靠路由器连接路由的原理很简单。市场上所有的路由器,背后都有很多网口,要接入多根网线。路由器内部有一张路由表,规定了 A 段 IP 地址走出口一,B 段地址走出口二,……通过这套"指路牌",实现了数据包的转发。本机的路由表注明了不同 IP 目的地的数据包,要发送到哪一个网口(interface)IP 协议只是一个地址协议,并不保证数据包的完整。如果路由器丢包(比如缓存满了,新进来的数据包就会丢失),就需要发现丢了哪一个包,以及如何重新发送这个包。这就要依靠 TCP 协议。简单说,TCP 协议的作用是,保证数据通信的完整性和可靠性,防止丢包TCP 数据包的大小以太网数据包(packet)的大小是固定的,最初是1518字节,后来增加到1522字节。其中, 1500 字节是负载(payload),22字节是头信息(head)。IP 数据包在以太网数据包的负载里面,它也有自己的头信息,最少需要20字节,所以 IP 数据包的负载最多为1480字节。IP 数据包在以太网数据包里面,TCP 数据包在 IP 数据包里面TCP 数据包在 IP 数据包的负载里面。它的头信息最少也需要20字节,因此 TCP 数据包的最大负载是 1480 - 20 = 1460 字节。由于 IP 和 TCP 协议往往有额外的头信息,所以 TCP 负载实际为1400字节左右。因此,一条1500字节的信息需要两个 TCP 数据包。HTTP/2 协议的一大改进, 就是压缩 HTTP 协议的头信息,使得一个 HTTP 请求可以放在一个 TCP 数据包里面,而不是分成多个,这样就提高了速度。以太网数据包的负载是1500字节,TCP 数据包的负载在1400字节左右TCP 数据包的编号(SEQ)一个包1400字节,那么一次性发送大量数据,就必须分成多个包。比如,一个 10MB 的文件,需要发送7100多个包。发送的时候,TCP 协议为每个包编号(sequence number,简称 SEQ),以便接收的一方按照顺序还原。万一发生丢包,也可以知道丢失的是哪一个包。第一个包的编号是一个随机数。为了便于理解,这里就把它称为1号包。假定这个包的负载长度是100字节,那么可以推算出下一个包的编号应该是101。这就是说,每个数据包都可以得到两个编号:自身的编号,以及下一个包的编号。接收方由此知道,应该按照什么顺序将它们还原成原始文件当前包的编号是45943,下一个数据包的编号是46183,由此可知,这个包的负载是240字节TCP 数据包的组装收到 TCP 数据包以后,组装还原是操作系统完成的。应用程序不会直接处理 TCP 数据包。对于应用程序来说,不用关心数据通信的细节。除非线路异常,收到的总是完整的数据。应用程序需要的数据放在 TCP 数据包里面,有自己的格式(比如 HTTP 协议)。TCP 并没有提供任何机制,表示原始文件的大小,这由应用层的协议来规定。比如,HTTP 协议就有一个头信息Content-Length,表示信息体的大小。对于操作系统来说,就是持续地接收 TCP 数据包,将它们按照顺序组装好,一个包都不少。操作系统不会去处理 TCP 数据包里面的数据。一旦组装好 TCP 数据包,就把它们转交给应用程序。TCP 数据包里面有一个端口(port)参数,就是用来指定转交给监听该端口的应用程序。系统根据 TCP 数据包里面的端口,将组装好的数据转交给相应的应用程序。上图中,21端口是 FTP 服务器,25端口是 SMTP 服务,80端口是 Web 服务器。应用程序收到组装好的原始数据,以浏览器为例,就会根据 HTTP 协议的 Content-Length 字段正确读出一段段的数据。这也意味着,一次 TCP 通信可以包括多个 HTTP 通信。慢启动和 ACK服务器发送数据包,当然越快越好,最好一次性全发出去。但是,发得太快,就有可能丢包。带宽小、路由器过热、缓存溢出等许多因素都会导致丢包。线路不好的话,发得越快,丢得越多。最理想的状态是,在线路允许的情况下,达到最高速率。但是我们怎么知道,对方线路的理想速率是多少呢?答案就是慢慢试。TCP 协议为了做到效率与可靠性的统一,设计了一个慢启动(slow start)机制。开始的时候,发送得较慢,然后根据丢包的情况,调整速率:如果不丢包,就加快发送速度;如果丢包,就降低发送速度。Linux 内核里面设定了(常量TCP_INIT_CWND),刚开始通信的时候,发送方一次性发送10个数据包,即"发送窗口"的大小为10。然后停下来,等待接收方的确认,再继续发送。默认情况下,接收方每收到两个 TCP 数据包,就要发送一个确认消息。“确认"的英语是 acknowledgement,所以这个确认消息就简称 ACK。ACK 携带两个信息:期待要收到下一个数据包的编号;接收方的接收窗口的剩余容量。发送方有了这两个信息,再加上自己已经发出的数据包的最新编号,就会推测出接收方大概的接收速度,从而降低或增加发送速率。这被称为"发送窗口”,这个窗口的大小是可变的。每个 ACK 都带有下一个数据包的编号,以及接收窗口的剩余容量,双方都会发送 ACK注意:由于 TCP 通信是双向的,所以双方都需要发送 ACK。两方的窗口大小,很可能是不一样的。而且 ACK 只是很简单的几个字段,通常与数据合并在一个数据包里面发送。上图一共4次通信。第一次通信,A 主机发给B 主机的数据包编号是1,长度是100字节,因此第二次通信 B 主机的 ACK 编号是 1 + 100 = 101,第三次通信 A 主机的数据包编号也是 101。同理,第二次通信 B 主机发给 A 主机的数据包编号是1,长度是200字节,因此第三次通信 A 主机的 ACK 是201,第四次通信 B 主机的数据包编号也是201。即使对于带宽很大、线路很好的连接,TCP 也总是从10个数据包开始慢慢试,过了一段时间以后,才达到最高的传输速率。这就是 TCP 的慢启动数据包的遗失处理TCP 协议可以保证数据通信的完整性,这是怎么做到的?前面说过,每一个数据包都带有下一个数据包的编号。如果下一个数据包没有收到,那么 ACK 的编号就不会发生变化。举例来说,现在收到了4号包,但是没有收到5号包。ACK 就会记录,期待收到5号包。过了一段时间,5号包收到了,那么下一轮 ACK 会更新编号。如果5号包还是没收到,但是收到了6号包或7号包,那么 ACK 里面的编号不会变化,总是显示5号包。这会导致大量重复内容的 ACK。如果发送方发现收到三个连续的重复 ACK,或者超时了还没有收到任何 ACK,就会确认丢包,即5号包遗失了,从而再次发送这个包。通过这种机制,TCP 保证了不会有数据包丢失。Host B 没有收到100号数据包,会连续发出相同的 ACK,触发 Host A 重发100号数据包 ...

January 19, 2019 · 1 min · jiezi

协议简史:如何学习网络协议?

大学时,学到网络协议的7层模型时,老师教了大家一个顺口溜:物数网传会表应。并说这是重点,年年必考,5分的题目摆在这里,你们爱背不背。考试的时候,果然遇到这个问题,搜索枯肠,只能想到这7个字的第一个字,因为这5分,差点挂科。后来工作面试,面试官也是很喜欢七层模型,三次握手之类的问题,但是遇到这些问题时,总是觉得很心虚。1. 协议分层四层网络协议模型中,应用层以下一般都是交给操作系统来处理。应用层对于四层模型来说,仅仅是冰山一角。海面下巨复杂的三层协议,都被操作系统给隐藏起来了,一般我们在页面上发起一个ajax请求,看见了network面板多了一个http请求,至于底层是如何实现的,我们并不关心。应⽤层负责处理特定的应⽤程序细节。运输层运输层主要为两台主机上的应⽤程序提供端到端的通信。网络层处理理分组在⽹网络中的活动,例例如分组的选路链路层处理理与电缆(或其他任何传输媒介)的物理理接⼝口细节下面重点讲一下运输层和网络层1.1. 运输层的两兄弟运输层有两个比较重要的协议。tcp和udp。大哥tcp是比较严谨认真、温柔体贴、慢热内向的协议,发出去的消息,总是一个一个认真检查,等待对方回复和确认,如果一段时间内,对方没有回复确认消息,还会再次发送消息,如果对方回复说你发的太快了,tcp还会体贴的把发送消息的速度降低。弟弟udp则比较可爱呆萌、调皮好动、不负责任的协议。哥哥tcp所具有的特点,弟弟udp一个也没有。但是有的人说不清哪里好 但就是谁都替代不了,udp没有tcp那些复杂的校验和重传等复杂的步骤,所以它发送消息非常快,而且并不保证对方一定收到。如果对方收不到消息,那么udp就会呆萌的看着你,笑着对你说:我已经尽力了。一般语音而视频数据都是用udp协议传输的,因为音频或者视频卡了一下并不影响整体的质量,而对实时性的要求会更高。1.2. 运输层和网络层的区别运输层关注的是端到端层面,及End1到End2,忽略中间的任何点。网络层关注两点之间的层面,即hop1如何到hop2,hop2如何到hop3网络层并不保证消息可靠性,可靠性上层的传输层负责。TCP采用超时重传,分组确认的机制,保证消息不会丢失。从下图tcp, udp, ip协议中,可以发现传输层的tcp和udp都是有源端口和目的端口,但是没有ip字段源ip和目的ip只在ip数据报中理解各个协议,关键在于理解报文的各个字段的含义1.3. ip和端口号的真正含义上个章节讲到运输层和网络层的区别,其中端口号被封装在运输层,ip被封装到网络成,那么端口号和ip地址到底有什么区别呢?ip用来用来标记主机的位置端口号用来标记该数据应该被目标主机上的哪个应用程序去处理1.4. 数据在协议栈的流动 封装与分用当发送消息时,数据在向下传递时,经过不同层次的协议处理,打上各种头部信息当接受消息时,数据在向上传递,通过不同的头部信息字段,才知道要交给上层的那个模块来处理。比如一个ip包,如果没有头部信息,那么这个消息究竟是交给tcp协议来处理,还是udp来处理,就不得而知了2. 深入阅读,好书推荐《http权威指南》 有人说这本书太厚,偷偷告诉你,其实这本书并厚,因为这本书的后面的30%部分都是附录,这本书的精华是前50%的部分《图解http》、《图解tcp/ip》这两本图解的书,知识点讲的都是比较通俗易懂的,适合入门《tcp/ip 详解 卷1》这本书,让你知其然,更知其所以然《tcp/ip 基础》、《tcp/ip 路由技术》这两本书,会让你从不同角度思考协议《精通wireshark》、《wireshark网络分析实战》如果你看了很多书,却从来没有试过网络抓包,那你只是懂纸上谈兵罢了。你永远无法理解tcp三次握手的怦然心动,与四次分手的刻骨铭心。

January 19, 2019 · 1 min · jiezi

gen_tcp参数总结

动机在用elixir 写 rpc server/client时, 需要对传入gen_tcp的参数做一些考量. 如, 部分参数应该允许用户修改, 比如sndbuf recbuf, 让用户根据使用场景调节, 部分参数应该屏蔽, 减少使用理解成本.故, 深挖了一下gen_tcp的option代码版本erlang: OTP-21.0.9optionsAvailable options for tcp:connect%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Available options for tcp:connect%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%connect_options() -> [tos, tclass, priority, reuseaddr, keepalive, linger, sndbuf, recbuf, nodelay, header, active, packet, packet_size, buffer, mode, deliver, line_delimiter, exit_on_close, high_watermark, low_watermark, high_msgq_watermark, low_msgq_watermark, send_timeout, send_timeout_close, delay_send, raw, show_econnreset, bind_to_device].tostype of service下图来自tcp ip详解 卷1tclassIPV6_TCLASS{tclass, Integer}Sets IPV6_TCLASS IP level options on platforms where this is implemented. The behavior and allowed range varies between different systems. The option is ignored on platforms where it is not implemented. Use with caution.不知道具体含义, 忽略prioritySO_PRIORITY SO_PRIORITY Set the protocol-defined priority for all packets to be sent on this socket. Linux uses this value to order the networking queues: packets with a higher priority may be processed first depending on the selected device queueing discipline. Setting a priority outside the range 0 to 6 requires the CAP_NET_ADMIN capability.reuseaddrSO_REUSEPORT SO_REUSEPORT (since Linux 3.9) Permits multiple AF_INET or AF_INET6 sockets to be bound to an identical socket address. This option must be set on each socket (including the first socket) prior to calling bind(2) on the socket. To prevent port hijacking, all of the pro‐ cesses binding to the same address must have the same effec‐ tive UID. This option can be employed with both TCP and UDP sockets. For TCP sockets, this option allows accept(2) load distribu‐ tion in a multi-threaded server to be improved by using a dis‐ tinct listener socket for each thread. This provides improved load distribution as compared to traditional techniques such using a single accept(2)ing thread that distributes connec‐ tions, or having multiple threads that compete to accept(2) from the same socket. For UDP sockets, the use of this option can provide better distribution of incoming datagrams to multiple processes (or threads) as compared to the traditional technique of having multiple processes compete to receive datagrams on the same socket.keepalive [SO_KEEPALIVE][6] Enable sending of keep-alive messages on connection-oriented sockets. Expects an integer boolean flag. ...

January 12, 2019 · 2 min · jiezi

当我们在讨论TCP的连接运输管理时,我们在说什么

TCP连接管理概述TCP是面向连接的协议。运输连接是用来传送TCP报文的。TCP的连接和释放是每一次面向连接的通信中必不可少的过程。因此,运输连接就有三个阶段,即:连接建立,数据传输和连接释放。运输连接的管理就是使运输连接的建立和释放都可以正常的进行。在TCP连接的建立过程中要解决一下三个问题。1.要使每一方能够确知对方的存在。2.要允许双方协商一些参数(如窗口最大值、是否使用窗口扩大选项和时间戳选项以及服务质量等)。3.能够对运输实体资源(如缓存大小、连接中的项目等)进行分配。TCP连接的建立采用客户服务器方式。主动发起建立连接的应用叫做客户(Client),而被动等待连接建立的应用进程叫做服务器(server)。TCP连接的建立下图表示的是TCP连接建立的过程。主机A运行的是TCP客户端程序,而B运行TCP服务器端程序。最初的时候,双方的TCP进程都处于关闭(CLOSED)状态。然后A主动打开连接,而B被动打开连接。1.A发送一个SYN=1的TCP报文,序列号seq是x。A的状态由CLOSED进入到SYN-SENT。2.B收到之后发送SYN=1,ACK=1的TCP报文,同样为自己选一个序列号seq=y,确认号ack=x+1。B的状态由LISTEN进入到SYN-RCVD。3.A收到B的确认之后,需要给B确认。ACK=1,seq=x+1,确认号ack是y+1。A的状态由SYN-SENT进入到ESTABLISH,B在收到A的确认之后也由SYN-RCVD进入到ESTABLISH。以上的过程就是3次握手。这里一个常见的问题就是为什么在A发送建立连接的请求后还需要发送一个确认报文?这是因为主要是为了避免已经失效的连接请求报文段突然又传到了B而产生错误。失效的报文主要是在出现在网络不是很通畅的时候会产生,而三次握手机制避免了这种情况会产生的问题。TCP连接的释放在传输结束之后,通信双方都可以释放连接。A的应用进程先向其TCP发出连接释放报文段,并停止发送数据,主动关闭TCP连接。1.A将连接释放报文的终止控制位FIN设置为1,seq序列号为u。此时A的状态由ESTABLISH进入到FIN-WAIT-1。2.B在收到这个报文之后,随机发出确认。ACK=1 seq=v ack=u+1。此时B由ESTABLISH进入到CLOSE-WAIT状态。A收到B的确认之后进入到FIN-WAIT-2。此时B的TCP服务器进程会通知高层应用进程。这个时候A到B的传输就释放了,TCP的连接处于半关闭状态。A没有数据发送到B,若B还有数据,可以发送到A。3.B发送FIN=1 ACK=1 seq=w ack=u+1(必须重复上次的确认号)。B由CLOSE-WAIT进入到LAST-ACK状态。4.A收到B的连接释放报文之后,发送确认ACK=1 seq=u+1 ack=w+1。此时A由FIN-WAIT-2进入到TIME-WAIT。B在收到A的确认之后由LAST-ACK进入CLOSED状态。A在2MSL(大约是4分钟)之后进入到CLOSED状态。这里一个常见的问题就是A为什么需要2MSL的等待时间。第一是为了保证A最后发送的ACK报文到达B。第二是为了当时出现上文提到的失效的报文段。因为2MSL会使本次连接产生的报文段都从网络中消失。全文完。

January 2, 2019 · 1 min · jiezi

浏览器请求响应慢,该从哪些方面分析

查看网络面板响应比较慢可以从两个层次去考虑连接初始化阶段耗时请求和响应耗时查看关键指标:排队达到浏览器最大并发数量限制有更高优先级的请求插队,低优先级的任务被延后系统内存空间不足,浏览器使用磁盘空间拥堵 原因和排队中类似DNS查询 花在DNS查询上的时间Proxy negotiation. 代理协商Request sent. 请求被发送Request to ServiceWorker. 请求被发送到ServiceWorkWaiting (TTFB). 等待收到响应的第一个字节Content Download. 内容下载Receiving Push. 浏览器通过HTTP/2 Server Push接受数据Reading Push. 浏览器读取之前收到的数据常见问题现象及解决方法出现长时间的排队或者拥堵原因 浏览器对同一域名最大的TCP链接数有限制,超过限制的请求会被排队。参见浏览器同域名请求的最大并发数限制。为什么会达到最大并发数?一次性获取的资源数量太多资源体积太大,很多都在下载中有些请求响应太慢或者无响应。例如1分钟之内,每隔10秒钟发送一个无响应的请求,随着可用的请求慢慢被占满,正常的请求排队数量会越来越多。解决方法问题1和问题2比较容易发现,也比较好处理。主要从两个角度解决减少请求数量。可以移除不必要的请求,或者将多个请求合并成1个。例如雪碧图使用域名分片。例如使用不同的域名指向相同的资源,从而突破单域名的限制。例如img1.tt.cc/1.jpg和img2.tt.cc/1.jpg前端给每个ajax请求设置超时 防止过多的无响应请求占据着连接资源,可以在超时之后释放连接。有些ajax库,例如jQuery的ajax, 默认是没有设置超时时长的,当你在使用这些库时,最好明确的设置后端设置请求处理超时 后端接口应当设置最大超时时长长时间的TTFB出现这种问题要从两个方面排查客户端到服务端之间的网络通信比较慢服务端的响应比较慢,可能是服务端压力太大,到达带宽上线,内存溢出,高CPU, IOwait高, Recv-Q高,或者sql查询慢等各种原因。注意:对于同一个源的请求,如果有些请求很快,有些请求很慢。那么问题一般是服务端的问题。因为如果是出现网络通信比较慢,那么则所有的请求都会变慢。解决方案知道问题的真实原因,其实问题也就解决了一半了。参考network-performancenetwork issue guide浏览器同域名请求的最大并发数限制

January 2, 2019 · 1 min · jiezi

TCP协议之旅总览

可靠性、协议格式、tcpDump可靠性、协议格式、tcpDump处理连接处理连接数据传输数据传输超时处理超时处理局限性与定时器局限性 定时器

December 31, 2018 · 1 min · jiezi

我们来说一说TCP神奇的40ms

本文由云+社区发表TCP是一个复杂的协议,每个机制在带来优势的同时也会引入其他的问题。 Nagel算法和delay ack机制是减少发送端和接收端包量的两个机制, 可以有效减少网络包量,避免拥塞。但是,在特定场景下, Nagel算法要求网络中只有一个未确认的包, 而delay ack机制需要等待更多的数据包, 再发送ACK回包, 导致发送和接收端等待对方发送数据, 造成死锁, 只有当delay ack超时后才能解开死锁,进而导致应用侧对外的延时高。 其他文字已经介绍了相关的机制, 已经有一些文章介绍这种时延的场景。本文结合具体的tcpdump包,分析触发delay ack的场景,相关的内核参数, 以及规避的方案。背景给redis加了一个proxy层, 压测的时候发现, 对写入命令,数据长度大于2k后, 性能下降非常明显, 只有直连redis-server的1/10. 而get请求影响并不是那么明显。分析观察系统的负载和网络包量情况, 都比较低, 网络包量也比较小, proxy内部的耗时也比较短。 无赖只能祭出tcpdump神奇, 果然有妖邪。22号tcp请求包, 42ms后服务端才返回了ack。 初步怀疑是网络层的延时导致了耗时增加。Google和km上找资料, 大概的解释是这样: 由于客户端打开了Nagel算法, 服务端未关闭延迟ack, 会导致延迟ack超时后,再发送ack,引起超时。原理Nagel算法,转自维基百科if there is new data to send if the window size >= MSS and available data is >= MSS send complete MSS segment now else if there is unconfirmed data still in the pipe enqueue data in the buffer until an acknowledge is received else send data immediately end if end ifend if简单讲, Nagel算法的规则是:如果发送内容大于1个MSS, 立即发送;如果之前没有包未被确认, 立即发送;如果之前有包未被确认, 缓存发送内容;如果收到ack, 立即发送缓存的内容。延迟ACK的源码如下:net/ipv4/tcp_input.c基本原理是:如果收到的数据内容大于一个MSS, 发送ACK;如果收到了接收窗口以为的数据, 发送ACK;如果处于quick mode, 发送ACK;如果收到乱序的数据, 发送ACK;其他, 延迟发送ACK其他都比较明确, quick mode是怎么判断的呢? 继续往下看代码:影响quick mode的一个因素是 ping pong的状态。 Pingpong是一个状态值, 用来标识当前tcp交互的状态, 以预测是否是W-R-W-R-W-R这种交互式的通讯模式, 如果处于, 可以用延迟ack, 利用Read的回包, 将Write的回包, 捎带给发送方。如上图所示, 默认pingpong = 0, 表示非交互式的, 服务端收到数据后, 立即返回ACK, 当服务端有数据响应时,服务端将pingpong = 1, 以后的交互中, 服务端不会立即返回ack,而是等待有数据或者ACK超时后响应。问题按照前面的的原理分析,应该每次都有ACK延迟的,为什么我们测试小于2K的数据时, 性能并没有受到影响呢?继续分析tcpdump包:按照Nagel算法和延迟ACK机制, 上面的交互如下图所示, 由于每次发生的数据都包含了完整的请求, 服务端处理完成后, 向客户端返回命令响应时, 将请求的ACK捎带给客户端,节约一次网络包。再分析2K的场景:如下表所示, 第22个包发送的数据小于MSS, 同时,pingpong = 1, 被认为是交互模式, 期待通过捎带ACK的方式来减少网络的包量。 但是, 服务端收到的数据,并不是一个完整的包,不能产生一次应答。服务端只能在等待40ms超时后,发送ACK响应包。 同时,从客户端来看,如果在发送一个包, 也可以打破已收数据 > MSS的限制。 但是,客户端受Nagel算法的限制, 一次只能有一个包未被确认,其他的数据只能被缓存起来, 等待发送。触发场景一次tcp请求的数据, 不能在服务端产生一次响应,或者小于一个MSS规避方案只有同时客户端打开Nagel算法, 服务端打开tcp_delay_ack才会导致前面的死锁状态。 解决方案可以从TCP的两端来入手。服务端:关闭tcp_delay_ack, 这样, 每个tcp请求包都会有一个ack及时响应, 不会出现延迟的情况。 操作方式: echo 1 > /proc/sys/net/ipv4/tcp_no_delay_ack 但是, 每个tcp请求都返回一个ack包, 导致网络包量的增加,关闭tcp延迟确认后, 网络包量大概增加了80%,在高峰期影响还是比较明显。2.设置TCP_QUICKACK属性。 但是需要每次recv后再设置一次。 对应我们的场景不太适合,需要修改服务端redis源码。客户端:关闭nagel算法,即设置socket tcp_no_delay属性。static void _set_tcp_nodelay(int fd) { int enable = 1; setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, (void*)&enable, sizeof(enable)); } 避免多次写, 再读取的场景, 合并成一个大包的写;避免一次请求分成多个包发送, 最开始发送的包小于一个MSS,对我们的场景, 把第22号包的1424个字节缓存起来, 大于一个MSS的时候,再发送出去, 服务端立即返回响应, 客户端继续发送后续的数据, 完成交互,避免时延。此文已由作者授权腾讯云+社区发布 ...

December 24, 2018 · 1 min · jiezi

断开TCP连接

我们知道TCP通过三次握手建立可靠连接,通过四次挥手断开连接,TCP连接是比较昂贵的资源。为什么TCP需要通过三次握手才能建立可靠的连接?两次不行么?断开连接为什么需要四次?TCP连接昂贵在哪里?三次握手客户端:“喂,听得到吗?” 服务端:“我能听到,你能听到我吗?” 客户端:“恩,能听到。”为什么需要三次握手,对客户端而言,再收到服务端的ACK后,能确定我发的消息服务端能收到,服务端发的消息我也能收到了,那为什么还要第三次握手?这要从服务端考虑,服务端在接收到SYN后只能确定自己能收到客户端发来的消息,如果没有第三次握手,服务端是不确定对方是否能接收到自己这边发送的消息的,这种不确定势必影响到了信道的可靠性。既然三次就已经确保了信道的可靠性,如果在加一次肯定就增加了网络消耗从而影响了建立连接的效率。四次挥手客户端:“不说了,挂了吧。”服务端:“OK!”服务端:“你要注意身体啊!”服务端:“拜拜!”客户端:“拜拜!”断开连接是释放资源的过程,还是从客户端和服务端两个人的角度去分析挥手过程。首先建立连接是为了可靠的数据交付,现在连接建立已经有一段时间了,客户端说数据已经发完了,已经没什么要发送了,于是告诉操作系统,嘿,老兄,我数据已经发完了,你可以把我的发送资源释放啦,于是操作系统锁住了发送资源(比如发送队列)准备释放,并标记了TCP连接状态为FIN_WAIT_1,由于数据发送是双方的事情,客户端这边的发送资源已经释放,客户端有义务告知服务端这边的数据已经发送完毕,所以操作系统会发送一条FIN消息到服务端,告知服务端可以释放接收资源了,为了保证服务端确实收到了FIN消息并释放了接收资源,服务端也需要返回一条ACK消息给客户端,如果客户端没收到ACK消息,则重试刚刚的FIN消息。客户端一旦收到ACK消息,则说明服务端已经释放了接收资源,操作系统将TCP连接状态改为FIN_WAIT_2。到这里TCP连接已经关闭一半。上面的过程只是结束了客户端的数据发送,释放了发送数据需要的资源,但是客户端依然可以接收从服务端发来的数据,服务端只是结束了数据接收并释放相关资源,依然可以放数据,因为服务端处理完接收的数据后要反馈结果给客户端。等结果反馈完后,没有数据要处理了,服务端也要结束发送过程,同样也得告知客户端让其释放接收数据所需要的资源。服务端重复上面的过程。但不同的是,客户端接收到FIN消息并返回ACK消息后需要等一段时间,这是由于担心服务端没有收到ACK又重发了FIN消息。等过了一段时间后并没有收到重发的消息,客户端就会释放所有资源(这里就不管服务端到底有没有收到ACK了,如果一直管下去就是个死循环)。服务端也是一样,重试多次以后也就释放了所有资源(这里不清楚到底是不是释放了资源,也有可能有其他机制)。从上分析,安全可靠的断开连接至少需要四次,再多一次的意义不大。昂贵的资源上面分析可知,三次握手和四次挥手无疑会造成巨大的网络资源和CPU资源的消耗(如果连接没有被复用,这就是一种浪费),另外,客户端和服务端分别要维护各自的发送和接收缓存,各自在操作系统里面的变量(比如文件描述符,操作系统维护的一套数据结构),在阻塞式的网络模型中,服务端还要开启线程来处理这条连接。所以说TCP连接是比较昂贵的资源,需要连接池这种技术来提高它的复用性。TCP连接的异常断开以上都是在理想的情况下发生的,理想状态下,一个TCP连接可以被长期保持。但是现实总是很骨感,在保持TCP连接的过程中很可能出现各种意外的情况,比如网络故障,客户端崩溃或者异常重启,在这种情况下,如果服务端没有及时清理这些连接,服务端将发生连接泄露,直至服务端资源耗尽拒绝提供服务(connection refused exception)。因此在实际应用中,服务器端需要采取相应的方法来探测TCP连接是否已经断连。探测的原理就是心跳机制,可以是应用层面的心跳,也可以是第三方的心跳,但是绝大部分类Unix系统均在TCP中提供了相应的心跳检测功能(虽然并不是TCP规范中的一部分)。客户端程序崩溃或异常退出当客户端程序因未知原因崩溃或异常退出后,操作系统会给服务端发送一条RST消息,阻塞模型下,服务端内核无法主动通知应用层出错,只有应用层主动调用read()或者write()这样的IO系统调用时,内核才会利用出错来通知应用层对端RST(Linux系统报Connection reset by peer)。非阻塞模型下,服务端select或者epoll会返回sockfd可读,应用层对其进行读取时,read()会报错RST。哪些情况下,会收到来自对端的RST消息呢。connect一个不存在的端口,客户端会收到一条RST,报错Connection refused;程序崩溃或异常退出,会向对端发送。对端断电重启,send数据时会收到来自对端的RST。close(sockfd)时,直接丢弃接收缓冲区未读取的数据,并给对方发一个RST。这个是由SO_LINGER选项来控制的;TCP socket在任何状态下,只要收到RST包,即可释放连接资源。客户端断电或网络异常如果客户端断电或网络异常,并且连接通道内没有任何数据交互,服务端是感知不到客户端掉线的,此时需要借助心跳机制来感知这种状况,一般的做法是,服务端往对端发送一个心跳包并启动一个超时定时器,如果能正确收到对端的回应,说明在线,如果超时,可以进行一系列操作,比如重试、关闭连接等等。keep alive or heart beart借鉴一下大神的文章很多人都知道TCP并不会去主动检测连接的丢失,这意味着,如果双方不产生交互,那么如果网络断了或者有一方机器崩溃,另外一方将永远不知道连接已经不可用了。检测连接是否丢失的方法大致有两种:keepalive和heart-beat。Keepalive是很多的TCP实现提供的一种机制,它允许连接在空闲的时候双方会发送一些特殊的数据段,并通过响应与否来判断连接是否还存活着(所谓keep~~alive)。我曾经写过一篇关于keepalive的blog ,但后来我也发现,其实keepalive在实际的应用中并不常见。为何如此?这得归结于keepalive设计的初衷。Keepalive适用于清除死亡时间比较长的连接。 比如这样的场景:一个用户创建tcp连接访问了一个web服务器,当用户完成他执行的操作后,很粗暴的直接拨了网线。这种情况下,这个tcp连接已经断开了,但是web服务器并不知道,它会依然守护着这个连接。如果web server设置了keepalive,那么它就能够在用户断开网线的大概几个小时以后,确认这个连接已经中断,然后丢弃此连接,回收资源。采用keepalive,它会先要求此连接一定时间没有活动(一般是几个小时),然后发出数据段,经过多次尝试后(每次尝试之间也有时间间隔),如果仍没有响应,则判断连接中断。可想而知,整个周期需要很长的时间。所以,如前面的场景那样,需要一种方法能够清除和回收那些在系统不知情的情况下死去了很久的连接,keepalive是非常好的选择。 但是,在大部分情况下,特别是分布式环境中,我们需要的是一个能够快速或者实时监控连接状态的机制,这里,heart-beat才是更加合适的方案。 Heart-beat(心跳),按我的理解,它的原理和keepalive非常类似,都是发送一个信号给对方,如果多次发送都没有响应的话,则判断连接中断。它们的不同点在于,keepalive是tcp实现中内建的机制,是在创建tcp连接时通过设置参数启动keepalive机制;而heart-beat则需要在tcp之上的应用层实现。一个简单的heart-beat实现一般测试连接是否中断采用的时间间隔都比较短,可以很快的决定连接是否中断。并且,由于是在应用层实现,因为可以自行决定当判断连接中断后应该采取的行为,而keepalive在判断连接失败后只会将连接丢弃。关于heart-beat,一个非常有趣的问题是,应该在传输真正数据的连接中发送心跳信号,还是可以专门创建一个发送“心跳”信号的连接。比如说,A,B两台机器之间通过连接m来传输数据,现在为了能够检测A,B之间的连接状态,我们是应该在连接m中传输心跳信号,还是创建新的连接n来专门传输心跳呢?我个人认为两者皆可。如果担心的是端到端的连接状态,那么就直接在该条连接中实现心跳。但很多时候,关注的是网络状况和两台主机间的连接状态,这种情况下, 创建专门的心跳连接也未尝不可。Socket感知连接断开正常情况客户端正常关闭连接://发送FIN消息,说明客户端已经没有数据发送,服务端read时会返回-1或者nullsocket.shutdownOutput();//默认的SO_LINGER参数,客户端发送FIN消息,服务端read时会返回-1或者nullsocket.close();//设置了立即关闭,客户端发送RST消息,服务端read时会报connection rest by peer。socket.close();非正常情况客户端程序崩溃或异常退出:服务端read时会报connection rest by peer。断电重启:服务端发送心跳信息时,会收到客户端的RST消息,调用read时会报connection rest by peer。断电或网络中断:服务端发送心跳信息后超时。

December 18, 2018 · 1 min · jiezi

线上大量CLOSE_WAIT原因深入分析

这一次重启真的无法解决问题了:一次 MySQL 主动关闭,导致服务出现大量 CLOSE_WAIT 的全流程排查过程。近日遇到一个线上服务 socket 资源被不断打满的情况。通过各种工具分析线上问题,定位到问题代码。这里对该问题发现、修复过程进行一下复盘总结。先看两张图。一张图是服务正常时监控到的 socket 状态,另一张当然就是异常啦!图一:正常时监控图二:异常时监控从图中的表现情况来看,就是从 04:00 开始,socket 资源不断上涨,每个谷底时重启后恢复到正常值,然后继续不断上涨不释放,而且每次达到峰值的间隔时间越来越短。重启后,排查了日志,没有看到 panic ,此时也就没有进一步检查,真的以为重启大法好。情况说明该服务使用Golang开发,已经上线正常运行将近一年,提供给其它服务调用,主要底层资源有DB/Redis/MQ。为了后续说明的方便,将服务的架构图进行一下说明。图三:服务架构架构是非常简单。问题出现在早上 08:20 左右开始的,报警收到该服务出现 504,此时第一反应是该服务长时间没有重启(快两个月了),可能存在一些内存泄漏,没有多想直接进行了重启。也就是在图二第一个谷底的时候,经过重启服务恢复到正常水平(重启真好用,开心)。将近 14:00 的时候,再次被告警出现了 504 ,当时心中略感不对劲,但由于当天恰好有一场大型促销活动,因此先立马再次重启服务。直到后续大概过了1小时后又开始告警,连续几次重启后,发现需要重启的时间间隔越来越短。此时发现问题绝不简单。这一次重启真的解决不了问题老,因此立马申请机器权限、开始排查问题。下面的截图全部来源我的重现demo,与线上无关。发现问题出现问题后,首先要进行分析推断、然后验证、最后定位修改。根据当时的表现是分别进行了以下猜想。ps:后续截图全部来源自己本地复现时的截图推断一socket 资源被不断打满,并且之前从未出现过,今日突然出现,怀疑是不是请求量太大压垮服务经过查看实时 qps 后,放弃该想法,虽然量有增加,但依然在服务器承受范围(远远未达到压测的基准值)。推断二两台机器故障是同时发生,重启一台,另外一台也会得到缓解,作为独立部署在两个集群的服务非常诡异有了上面的的依据,推出的结果是肯定是该服务依赖的底层资源除了问题,要不然不可能独立集群的服务同时出问题。由于监控显示是 socket 问题,因此通过 netstat 命令查看了当前tcp链接的情况(本地测试,线上实际值大的多)/go/src/hello # netstat -na | awk ‘/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}‘LISTEN 2CLOSE_WAIT 23 # 非常异常TIME_WAIT 1发现绝大部份的链接处于 CLOSE_WAIT 状态,这是非常不可思议情况。然后用 netstat -an 命令进行了检查。图四:大量的CLOSE_WAITCLOSED 表示socket连接没被使用。 LISTENING 表示正在监听进入的连接。 SYN_SENT 表示正在试着建立连接。 SYN_RECEIVED 进行连接初始同步。 ESTABLISHED 表示连接已被建立。 CLOSE_WAIT 表示远程计算器关闭连接,正在等待socket连接的关闭。 FIN_WAIT_1 表示socket连接关闭,正在关闭连接。 CLOSING 先关闭本地socket连接,然后关闭远程socket连接,最后等待确认信息。 LAST_ACK 远程计算器关闭后,等待确认信号。 FIN_WAIT_2 socket连接关闭后,等待来自远程计算器的关闭信号。 TIME_WAIT 连接关闭后,等待远程计算器关闭重发。然后开始重点思考为什么会出现大量的mysql连接是 CLOSE_WAIT 呢?为了说清楚,我们来插播一点TCP的四次挥手知识。TCP四次挥手我们来看看 TCP 的四次挥手是怎么样的流程:图五:TCP四次挥手用中文来描述下这个过程:Client: 服务端大哥,我事情都干完了,准备撤了,这里对应的就是客户端发了一个FINServer:知道了,但是你等等我,我还要收收尾,这里对应的就是服务端收到 FIN 后回应的 ACK经过上面两步之后,服务端就会处于 CLOSE_WAIT 状态。过了一段时间 Server 收尾完了Server:小弟,哥哥我做完了,撤吧,服务端发送了FINClient:大哥,再见啊,这里是客户端对服务端的一个 ACK到此服务端就可以跑路了,但是客户端还不行。为什么呢?客户端还必须等待 2MSL 个时间,这里为什么客户端还不能直接跑路呢?主要是为了防止发送出去的 ACK 服务端没有收到,服务端重发 FIN 再次来询问,如果客户端发完就跑路了,那么服务端重发的时候就没人理他了。这个等待的时间长度也很讲究。Maximum Segment Lifetime 报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃这里一定不要被图里的 client/server 和项目里的客户端服务器端混淆,你只要记住:主动关闭的一方发出 FIN 包(Client),被动关闭(Server)的一方响应 ACK 包,此时,被动关闭的一方就进入了 CLOSE_WAIT 状态。如果一切正常,稍后被动关闭的一方也会发出 FIN 包,然后迁移到 LAST_ACK 状态。既然是这样, TCP 抓包分析下:/go # tcpdump -n port 3306# 发生了 3次握手11:38:15.679863 IP 172.18.0.5.38822 > 172.18.0.3.3306: Flags [S], seq 4065722321, win 29200, options [mss 1460,sackOK,TS val 2997352 ecr 0,nop,wscale 7], length 011:38:15.679923 IP 172.18.0.3.3306 > 172.18.0.5.38822: Flags [S.], seq 780487619, ack 4065722322, win 28960, options [mss 1460,sackOK,TS val 2997352 ecr 2997352,nop,wscale 7], length 011:38:15.679936 IP 172.18.0.5.38822 > 172.18.0.3.3306: Flags [.], ack 1, win 229, options [nop,nop,TS val 2997352 ecr 2997352], length 0# mysql 主动断开链接11:38:45.693382 IP 172.18.0.3.3306 > 172.18.0.5.38822: Flags [F.], seq 123, ack 144, win 227, options [nop,nop,TS val 3000355 ecr 2997359], length 0 # MySQL负载均衡器发送fin包给我11:38:45.740958 IP 172.18.0.5.38822 > 172.18.0.3.3306: Flags [.], ack 124, win 229, options [nop,nop,TS val 3000360 ecr 3000355], length 0 # 我回复ack给它… … # 本来还需要我发送fin给他,但是我没有发,所以出现了close_wait。那这是什么缘故呢?src > dst: flags data-seqno ack window urgent optionssrc > dst 表明从源地址到目的地址flags 是TCP包中的标志信息,S 是SYN标志, F(FIN), P(PUSH) , R(RST) “."(没有标记)data-seqno 是数据包中的数据的顺序号ack 是下次期望的顺序号window 是接收缓存的窗口大小urgent 表明数据包中是否有紧急指针options 是选项结合上面的信息,我用文字说明下:MySQL负载均衡器 给我的服务发送 FIN 包,我进行了响应,此时我进入了 CLOSE_WAIR 状态,但是后续作为被动关闭方的我,并没有发送 FIN,导致我服务端一直处于 CLOSE_WAIR 状态,无法最终进入 CLOSED 状态。那么我推断出现这种情况可能的原因有以下几种:负载均衡器 异常退出了,这基本是不可能的,他出现问题绝对是大面积的服务报警,而不仅仅是我一个服务MySQL负载均衡器 的超时设置的太短了,导致业务代码还没有处理完,MySQL负载均衡器 就关闭tcp连接了这也不太可能,因为这个服务并没有什么耗时操作,当然还是去检查了负载均衡器的配置,设置的是60s。代码问题,MySQL 连接无法释放目前看起来应该是代码质量问题,加之本次数据有异常,触发到了以前某个没有测试到的点,目前看起来很有可能是这个原因查找错误原因由于代码的业务逻辑并不是我写的,我担心一时半会看不出来问题,所以直接使用 perf 把所有的调用关系使用火焰图给绘制出来。既然上面我们推断代码中没有释放mysql连接。无非就是:确实没有调用close有耗时操作(火焰图可以非常明显看到),导致超时了mysql的事务没有正确处理,例如:rollback 或者 commit由于火焰图包含的内容太多,为了让大家看清楚,我把一些不必要的信息进行了折叠。图六:有问题的火焰图火焰图很明显看到了开启了事务,但是在余下的部分,并没有看到 Commit 或者是Rollback 操作。这肯定会操作问题。然后也清楚看到出现问题的是:MainController.update 方法内部,话不多说,直接到 update 方法中去检查。发现了如下代码:func (c *MainController) update() (flag bool) { o := orm.NewOrm() o.Using(“default”) o.Begin() nilMap := getMapNil() if nilMap == nil {// 这里只检查了是否为nil,并没有进行rollback或者commit return false } nilMap[10] = 1 nilMap[20] = 2 if nilMap == nil && len(nilMap) == 0 { o.Rollback() return false } sql := “update tb_user set name=%s where id=%d” res, err := o.Raw(sql, “Bug”, 2).Exec() if err == nil { num, _ := res.RowsAffected() fmt.Println(“mysql row affected nums: “, num) o.Commit() return true } o.Rollback() return false}至此,全部分析结束。经过查看 getMapNil 返回了nil,但是下面的判断条件没有进行回滚。if nilMap == nil { o.Rollback()// 这里进行回滚 return false}总结整个分析过程还是废了不少时间。最主要的是主观意识太强,觉得运行了一年没有出问题的为什么会突然出问题?因此一开始是质疑 SRE、DBA、各种基础设施出了问题(人总是先怀疑别人)。导致在这上面费了不少时间。理一下正确的分析思路:出现问题后,立马应该检查日志,确实日志没有发现问题;监控明确显示了socket不断增长,很明确立马应该使用 netstat 检查情况看看是哪个进程的锅;根据 netstat 的检查,使用 tcpdump 抓包分析一下为什么连接会被动断开(TCP知识非常重要);如果熟悉代码应该直接去检查业务代码,如果不熟悉则可以使用 perf 把代码的调用链路打印出来;不论是分析代码还是火焰图,到此应该能够很快定位到问题。那么本次到底是为什么会出现 CLOSE_WAIR 呢?大部分同学应该已经明白了,我这里再简单说明一下:由于那一行代码没有对事务进行回滚,导致服务端没有主动发起close。因此 MySQL负载均衡器 在达到 60s 的时候主动触发了close操作,但是通过tcp抓包发现,服务端并没有进行回应,这是因为代码中的事务没有处理,因此从而导致大量的端口、连接资源被占用。在贴一下挥手时的抓包数据:# mysql 主动断开链接11:38:45.693382 IP 172.18.0.3.3306 > 172.18.0.5.38822: Flags [F.], seq 123, ack 144, win 227, options [nop,nop,TS val 3000355 ecr 2997359], length 0 # MySQL负载均衡器发送fin包给我11:38:45.740958 IP 172.18.0.5.38822 > 172.18.0.3.3306: Flags [.], ack 124, win 229, options [nop,nop,TS val 3000360 ecr 3000355], length 0 # 我回复ack给它希望此文对大家排查线上问题有所帮助。为了便于帮助大家理解,下面附上正确情况下的火焰图与错误情况下的火焰图。大家可以自行对比。正确情况下的火焰图错误情况的火焰图我参考的一篇文章对这种情况提出了两个思考题,我觉得非常有意义,大家自己思考下:为什么一台机器几百个 CLOSE_WAIR 就导致不可继续访问?我们不是经常说一台机器有 65535 个文件描述符可用吗?为什么我有负载均衡,而两台部署服务的机器确几乎同时出了 CLOSE_WAIR ?参考文章:又见CLOSE_WAITTCP 4-times close个人公众号:dayuTalk ...

December 10, 2018 · 2 min · jiezi

值得收藏的TCP套接口编程文章

欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~本文由jackieluo发表于云+社区专栏TCP客户端-服务器典型事件下图是TCP客户端与服务器之间交互的一系列典型事件时间表:首先启动服务器,等待客户端连接启动客户端,连接到服务器客户端发送一个请求给服务器,服务器处理请求,响应客户端循环步骤3客户端给服务器发一个文件结束符,关闭客户端连接服务器也关闭连接基本TCP客户-服务器程序的套接口函数套接口编程基本函数socket 函数为了执行网络I/O,一个进程(无论是服务端还是客户端)必须做的第一件事情就是调用socket函数。#include <sys/socket.h> /* basic socket definitions /int socket(int family, int type, int protocol);/ 返回:非负描述字——成功,-1——出错 /family——协议族族解释AF_INETIPv4协议AF_INET6IPv6协议AF_LOCALUnix域协议AF_ROUTE路由套接口AF_KEY密钥套接口type——套接口类型类型解释SOCK_STREAM字节流套接口SOCK_DGRAM数据报套接口SOCK_RAW原始套接口下面是有效的family和type组合(简略版): AF_INETAF_INET6SOCK_STREAMTCPTCPSOCK_DGRAMUDPUDPSOCK_RAWIPv4IPv6socket函数返回一个套接口描述字,简称套接字(sockfd)。获取套接字无需指定地址,只需要指定协议族和套接口类型(如上表中的组合)。connect函数TCP客户用connect函数来建立一个与TCP服务器的连接。#include <sys/socket.h> / basic socket definitions /int connect(int sockfd, const struct sockaddr * servaddr, socklen_t addrlen);/ 返回:0——成功,-1——出错 /参数sockfd便是socket函数返回的套接口描述字。套接口地址结构servaddr必须包含服务器的IP地址和端口号。客户端不必非要绑定一个端口(调用bind函数),内核会选择源IP和一个临时端口。connect函数会触发TCP三次握手。有可能出现下面的错误情况:1.客户端未收到SYN分节的响应第一次发出未收到,间隔6s再发一次,再没收到,隔24秒再发一次,总共等待75s还没收到则返回错误( ETIMEDOUT)。可以用时间日期程序验证一下:查看本地网络信息:JACKIELUO-MC0:intro jackieluo$ ifconfigen0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500 ether f4:0f:24:2a:72:a6 inet6 fe80::1830:dbd:1b29:2989%en0 prefixlen 64 secured scopeid 0x6 inet 192.168.0.101 netmask 0xffffff00 broadcast 192.168.0.255 nd6 options=201<PERFORMNUD,DAD> media: autoselect status: active将程序指向本地地址192.168.0.101(确保时间日期服务器程序已运行),成功:JACKIELUO-MC0:intro jackieluo$ ./daytimetcpcli 192.168.0.101Sat Oct 6 17:06:55 2018将程序指向本地子网地址192.168.0.102,其主机ID(102)不存在,等待几分钟后超时返回:JACKIELUO-MC0:intro jackieluo$ ./daytimetcpcli 192.168.0.102connect error: Operation timed out2.收到RST即服务器主机在指定端口上没有等待连接的进程,这称为“hard error”,客户端一接收到RST,马上返回错误(ECONNREFUSED)。验证:关闭之前本机运行的daytimetcpsrv进程将程序指向本地地址192.168.0.101:JACKIELUO-MC0:intro jackieluo$ ./daytimetcpcli 192.168.0.101connect error: Connection refused3.发出的SYN在路由器上引发了目的不可达ICMP错误这个错误被称为“soft error”,最终返回EHOSTUNREACH或者ENETUNREACH。bind函数函数bind为套接口分配一个本地协议地址,包括IP地址和端口号。#include <sys/socket.h> / basic socket definitions /int bind(int sockfd, const struct sockaddr * servaddr, socklen_t addrlen);/ 返回:0——成功,-1——出错 /客户端可以不调用这个函数,由内核选择一个本地ip的临时端口就好。服务器一般都会调用bind函数绑定ip地址和端口,供客户端调用。一个例外是RPC(远程过程调用)服务器,它由内核为其选择临时端口。然后通过RPC端口映射器进行注册,客户端与该服务器连接之前,先通过端口映射器获取服务器的端口。进程可以把一个特定的IP地址捆绑到它的套接口上。对于客户端,它发送的请求,源IP地址就是这个地址;对于服务器,如果绑定了IP地址,则只接受目的地为此IP地址的客户连接。如果服务器不把IP地址绑定到套接口上,那么内核把客户端发送SYN所在分组的目的IP地址作为服务器的源IP地址。(即服务器收到SYN的IP)给函数bind指定用于捆绑的IP地址和/或端口号的结果:IP地址端口结果 0内核选择IP地址和端口 非0内核选择IP地址,进程指定端口本地IP地址0进程选择IP地址,内核指定端口本地IP地址非0进程选择IP地址和端口listen函数函数listen仅被TCP服务器调用。#include <sys/socket.h> / basic socket definitions /int listen(int sockfd, int backlog);/ 返回:0——成功,-1——出错 /调用函数socket函数创建的套接口,默认是主动方,下一步应是调用connect,CLOSED的下一个状态是SYN_SENT(见TCP状态转换图)。而函数listen将套接口转换成被动方,告诉内核,应接受指向此套接口的连接请求,CLOSED状态变成LISTEN。函数listen的第二个参数backlog表示内核为此套接口排队的最大连接数。对于给定的监听套接口,内核会维护两个队列:未完成连接队列(incomplete connection queue) SYN分节已由客户发出,到达服务器,正在进行TCP的三路握手。此时这些套接口处于SYN_RCVD状态。已完成连接队列(completed connection queue) SYN分节已由客户发出,到达服务器,并且已完成三路握手。此时这些套接口处于ESTABLISHED状态。当来自客户的SYN到达时,TCP在未完成连接队列中创建一个新条目,直到三路握手中,第三个分节(客户对服务SYN的ACK)到达,这个条目移到已完成连接队列的队尾。当进程调用accept函数时,已完成连接队列的头部条目返回给进程。两个队列之和不能超过backlog当一个客户SYN到达时,若这两个队列都是满的,TCP就忽略此分节,且不发送RST。客户TCP将重发SYN,期望不久就能在队列中找到空闲位置。 TCP为监听套接口维护的两个队列accept函数函数accept由TCP服务器调用,从已完成连接队列头部返回下一个已完成连接,若该队列为空,则进程睡眠(假定套接口为默认的阻塞方式)。#include <sys/socket.h> / basic socket definitions */int accept(int sockfd, struct sockaddr *cliaddr, socklen_t addrlen);/ 返回:非负描述字——成功,-1——出错 */函数accept的第一个参数和返回值都是套接口描述字。其中,第一个参数,称为监听套接口描述字,即由函数socket返回,也用于bind,listen的第一个参数。返回值,称为已连接套接口描述字。通常一个服务器,只生成一个监听套接口描述字,直到其关闭。而内核为每个被接受的客户连接,创建一个已连接套接口,当客户连接完成时,关闭该已连接套接口。注意到intro/daytimetcpsrv.c中,后两个参数传的都是空指针,这是因为我们不关注客户的身份,无需知道客户的协议地址。connfd = Accept(listenfd, (SA *) NULL, NULL);稍作修改,不再传入空指针,见intro/daytimetcpsrv1.c:socklen_t len;struct sockaddr_in servaddr, cliaddr;…connfd = Accept(listenfd, (SA *) &cliaddr, &len);printf(“connection from %s, port %d\n”, Inet_ntop(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff)), ntohs(cliaddr.sin_port));kill掉之前的daytimetcpsrv进程:$ sudo lsof -i -P | grep -i “listen"daytimetc 80986 root 3u IPv4 0xae12d925e4528793 0t0 TCP :13 (LISTEN)$ sudo kill -9 80986编译运行新的服务端程序:$ make daytimetcpsrv1.c daytimetcpsrv1$ ./daytimetcpsrv1重复执行客户端程序,发几个请求:$ ./daytimetcpcli 127.0.0.1Wed Sep 26 14:11:20 2018$ ./daytimetcpcli 127.0.0.1Wed Sep 26 14:17:06 2018查看服务端打印:connection from 127.0.0.1, port 58201connection from 127.0.0.1, port 58342注意到,由于客户端程序没有调用bind函数,内核为它的协议地址选择了源ip作为IP地址,临时端口号也发生了变化。fork和exec函数#include <unistd.h>pid_t fork(void);/ 返回:在子进程中为0,在父进程中为子进程ID,-1——出错 */fork函数调用一次,却返回两次。在调用它的进程(即父进程),它返回一次,返回值是派生出来的子进程的进程ID。 父进程可能有很多子进程,必须通过返回值跟踪记录子进程ID。在子进程,它还返回一次,返回值为0。 子进程只有一个父进程,总可以通过getppid来得到父进程的ID通过返回值可以判断当前进程是子进程还是父进程。父进程在调用fork之前打开的所有描述字在函数fork返回后都是共享的。网络服务器会利用这一特性:父进程调用accept。父进程调用fork,已连接套接口就在父进程与子进程间共享。(一般来说就是子进程读、写已连接套接口,而父进程关闭已连接套接口)。fork有两个典型应用:一个进程为自己派生一个拷贝,并发执行任务,这也是典型的并发网络服务器模型。一个进程想执行其他的程序,于是调用fork生成一个拷贝,利用子进程调用exec来执行新的程序。典型应用是shell。以文件形式存储在硬盘上的可执行程序若要被执行,需要由一个现有进程调用exec函数。我们将调用exec的进程称为调用进程,新程序的进程ID并不改变,仍处于当前进程。小结客户和服务器,从调用socket开始,返回一个套接口描述字。客户调用connect,服务器调用bind、listen、accept。最后套接口由close关闭。多数TCP服务器是调用fork来实现并发处理多客户请求的。多数UDP服务器则是迭代的。相关阅读系统重启后nginx reload不生效原因分析SRS开源直播服务 - StateThreads微线程框架学习高性能网络编程3—-TCP消息的接收 【每日课程推荐】机器学习实战!快速入门在线广告业务及CTR相应知识 ...

October 12, 2018 · 2 min · jiezi