从产品上线前的接口开发和调试,到上线后的 bug 定位、性能优化,网络编程知识贯穿着一个互联网产品的整个生命周期。不论你是前后端的开发岗位,还是 SQA、运维等其他技术岗位,掌握网络编程知识均是岗位的基础要求,即使是产品、设计等非技术岗位,在灰度环境体验产品时也需要理解页面缓存、Host 切换等网络基础概念。
「猫哥网络编程系列」一直是我想沉淀的一个技术知识点,因为我认为:网络编程相关知识(尤其是 HTTP 协议),是互联网产品开发当中最重要的基础知识(没有之一)。掌握这方面的基础知识,对一个新手程序员来说至关重要。本系列将会在我的微信公众号「猫哥学前班」上连载,并在 Github 上维护更新。
使用「详解 BAT 面试题」作为本文的副标题,是为了吸引更多的技术新人浏览此文,然而本文并非标题党。掌握本文所提到的知识点必将大幅提升程序员的面试成功率,因为「网络编程」方面的基础知识,是 BAT 面试的必考项目。
从 BAT 面试题说起
2009 年我在支付宝做前端开发时,参与草拟了一份非正式的前端岗位招聘要求。
这里有:
- 国内最大的第三方支付舞台,体验亿万资金穿梭代码的快感;
- 一群热爱前端技术的伙伴,最快的成长经历;
- 持续的培训体系,完善的项目开发环境,最具潜力的UED团队。
你需要:
- 热爱前端,热爱设计,对新鲜事物充满好奇心,喜欢捣鼓各种互联网应用;(兴趣、学习能力、创新能力)
- 自我管理能力强,健康的创业心态,乐于分享与沟通;(心态、分享、性格)
- 具备基本的前端素质,了解WEB标准化、性能优化方法,了解可用性、可访问性;(基本技能)
- 能和设计师谈产品设计,和后端开发研讨技术实现方案,制定服务接口,崇尚团队合作;(向前向后能力)
当时在面试时最流行问的前端技术问题是:Web 标准化、AJAX 与 YSlow。
- 问:一个 AJAX 请求从开始创建到最后的响应阶段,在其整个生命周期中,使用到了哪些 JavaScript 对象与方法?
- 问:YSlow 的 34 条性能优化建议中,哪些与 HTTP 协议相关,请尽可能多的列举出来,并说说你的理解?
2011 年我开始成为腾讯的前端开发面试官,负责腾讯电商的前端开发(网页重构方向)笔试出题与面试工作。在 2012 年的校招过程中,我发现不论是我出的笔试题,还是其他面试官出的题目,HTTP 协议相关的知识都是必考项。例如,
- 问:HTTP 协议中与缓存相关的 HTTP Header 有哪些?
- 问:列举出你所知道的 HTTP 状态码,并描述它们的含义与发生的场景?
后来我在学习百度 FIS 框架的过程中,无意间看到百度 FEX 团队的这份开源前端开发面试题,不出所料,同样有一道与网络编程相关的题目:
一个页面从输入 URL 到页面加载完的过程中都发生了什么事情?越详细越好
由此可见,网络编程相关知识的确是 BAT 前端面试题中的必考项,而对于负责输出 API 接口的后台开发岗位来说,更是如此:
- 问:一个 POST 请求的 Content-Type 有多少种,传输的数据格式有何区别?
- 问:什么是 RESTful API,如何设计一个 Open API 的接口?
解题思路
接下来我们一起探讨下具体的解题思路,浏览完本文之后,你将会首先掌握 HTTP 协议相关的前后端基础知识。
要掌握 HTTP ,就需要先看到 HTTP 到底长什么样?(不了解「网络七层协议模型」和 TCP 的同学先不着急,本系列的后面几篇会涉及到。)
1、安装 HTTP 抓包工具
在 Chrome 开发者工具下我们可以看到,打开一个网页后,浏览器会发起许多 HTTP 的请求(HTTP Request),这些请求经过服务器端处理后会返回对应的数据(HTTP Response),浏览器会按照这些数据的类型将它们渲染出来。
Chrome 中看到的 Request/Response Header 是其格式化之后的形式,要看到它们的原始模样(Raw Source),我们需要借助两个 HTTP 接口调试利器。
其中 Windows 系统下使用 Fiddler,Mac 系统下使用 Charles。Fiddler 具体的安装与使用教程,请自行百度(安装 Fiddler4 还需同时安装 .NET Framework 4),Charles 相关教程,推荐参考 iOS 大神唐巧的《Charles 从入门到精通》。使用 Linux 系统的说明已经是网络编程方面的大牛了,不需要继续往下看 :P
2、查看 HTTP 详细报文
运行 Fiddler(或 Charles) 之后,使用 Chrome 浏览器打开「猫哥学前班」的新浪微博主页:http://weibo.com/mgxqb
在 Fiddler 左侧面板下选中该条 HTTP 请求,再将右侧面板的请求部分和响应部分都切换到 Raw 标签页。如下图所示:
Charles 下的操作与 Fiddler 类似:
HTTP 协议规范由 W3C 制定,与具体的抓包工具无关,接下来我们主要以 Charles 为例,详细讲解下 HTTP 的报文格式,这对理解基于 HTTP 的 API 接口设计和网页性能优化有很大帮助。
我们先看一下请求头的源码(Request Raw),为了防止隐私泄露,我已删除部分 Cookie 信息:
代码语言:javascript复制GET /mgxqb HTTP/1.1
Host: weibo.com
Cache-Control: max-age=0
Accept: text/html,application/xhtml xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.73 Safari/537.36
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-US,en;q=0.8,zh-CN;q=0.6,zh;q=0.4
Cookie: YF-Page-G0=f70469e0b5607cacf38b47457e34254f; _s_tentry=passport.weibo.com
仔细观察以上源码,我们能大概总结出 HTTP 协议的格式规范:
- 第一行定义了请求类型(
GET
)、请求路径(/mgxqb
)与协议类型及其版本号(HTTP/1.1
),使用一个半角空格间隔这三块信息; - 示例源码的最后是两个空行。由于 HTTP 规范中要求一个合法的 HTTP 报文至少包含有一个空行,其中第一个空行用来间隔报文的头部信息(HTTP Request/Response Header)和主体信息(HTTP Request/Response Body)。在空行的下一行是报文的主体信息,由于本例为 GET 类型请求,其主体(Body)信息通常为空,这便是第二个空行的含义;
- 余下的部分有着相同的格式,即 「HTTP Header 字段名 半角冒号 半角空格 值」,我们可以把它看成 YAML 格式的简易版。其中 HTTP Header 在规范中有着明确的定义,具体参见 HTTP头字段列表。
这便是一个 HTTP 协议报文的源码格式,以下我们简单讲解下最常见的 HTTP header 的含义。
3、常见 HTTP header
User-Agent:客户端身份标识
User-Agent (以下简称 UA)字段记录了访问当前网页的用户浏览器的类型与版本、操作系统类型与版本。根据不同的 UA 信息,提供不同的站点内容是使用 UA 的常见场景。例如,如果用户使用手机访问魅族官网 www.meizu.com,浏览器会自动跳转至魅族手机官网 m.meizu.com。这种跳转实现既可以由前端 JavaScript 完成,也可以通过后端返回 302 重定向来完成。
JavaScript 访问 window.navigator.userAgent
属性即可获取该信息。虽然该属性是只读的,但有很多前端手段可以伪造 UA 。如下图,Chrome 开发者工具在模拟不同的手机机型时,也会改变浏览器 UA 值。由此可见,通过检测 HTTP User-Agent Header 来识别是否为爬虫程序,不是一个有效的方法。
在 PHP 中,所有的 HTTP Header 字段信息都保存在 $_SERVER
对象中,通过访问 $_SERVER['HTTP_USER_AGENT']
即可获取 User-Agent 的值。
Cookie:用户身份标识
由于 HTTP 协议最初被设计成一种无状态的数据传输协议,服务器端无法判断每次处理的请求相互之间以及与之前处理的请求之间的关系,Cookie 的设计就是为了解决这个问题。
用户在浏览器中首次访问一个站点时,会通过请求响应头或页面JS脚本生成一些用于标识用户身份的 Cookie 信息,这些信息会按照域名分类,存放在浏览器本地缓存文件当中。例如 Windows 系统下通过访问 「C:Users<用户名>AppDataLocalMicrosoftWindowsTemporary Internet Files」 目录可以查看到 IE 浏览器保存在本地的 Cookie 文件。当用户再次访问该站点时,这些 Cookie 信息会被浏览器自动添加到 HTTP Request Header 的 Cookie 字段中,服务器通过读取这些信息,来区分当前请求的用户身份与状态。
浏览器可以通过读写 document.cookie 属性来添加或删除 Cookie 信息,服务器端可以通过 HTTP Response Header(响应头)中的 Set-Cookie 来改写客户端的 Cookie 信息。每一条 Cookie 属性通常都会设置一个过期时间,过期之后的 Cookie 浏览器将会自动清理它们,不会再被携带在 HTTP Request Header(请求头)中。
例如,以下 PHP 语句可以通过设置 Cookie 过期时间为前一个小时来触发客户端 Cookie 过期,达到删除 Cookie 的目的:
代码语言:javascript复制setcookie('key', '', time() - 3600, '/');
由于 Cookie 通常用于记录用户「帐号信息」和用户的「操作记录」,所以泄露 Cookie 会带来个人帐号与隐私泄露的风险。这也是为什么你在百度上搜索「贷款」的关键词之后,访问其他网站时就能看到相关的推荐广告,甚至第二天就会有各种放贷电话找上门来。
又由于 Cookie 可以随意被客户端修改(通过修改 document.cookie 属性),因此浏览器厂商们一起制定了 HttpOnly 的 Cookie 机制。服务器端在 setcookie 时,通过设置 HttpOnly 的标识,可以防止客户端通过 JavaScript 修改 Cookie 的信息。不过这种方法对于基于 HTTP 协议进行篡改的方法来说无法防范,在之后的猫哥网络编程系列中,我将会介绍如何通过监控 Wi-Fi 流量来截取、伪造用户身份。
在 YSlow 性能优化最佳实践中,有两条与 Cookie 相关的建议:
- Reduce Cookie Size
- Use Cookie-free Domains for Components
虽然浏览器对 Cookie 的大小与数量有着较为严格的限制,但很多网站(尤其是包含登录态的)的 Cookie 信息量通常比其他所有 HTTP Header 加起来的还要多。为了减少不必要的 HTTP 数据传输量,YSlow 给出了以上两条优化建议。由于网页的静态资源(图片、CSS、JS)文件无需记录用户状态,因此通常会使用一个额外的域名(Cookie 是按域名来分类存储)来存放静态资源文件。
我们使用 Chrome 开发者工具查看「猫哥学前班」新浪微博主页,可以看到新浪微博使用了 img.t.sinajs.cn 的域名来存放它的 CSS 文件,这个域名发起的 HTTP Request Header 中没有自动带上 Cookie 字段信息 (因为前后端脚本都没有在这个域名上设置 Cookie,而是设置在了 weibo.com 域名上):
这里还需要引申一个知识点:Session,它和 Cookie 有什么关系?由于 Session 与本文所讲的 HTTP 协议关系不大,相关知识点请自行百度。
Cache-Control:浏览器资源缓存标识
网站性能优化中,最为关键的是缓存机制(又是没有之一)。在服务器端通常会使用 Memcached、Redis 等服务来缓存经常访问的数据。例如在一个电商网站中,用户经常访问的热卖商品数据会被缓存在内存中,用户在一定时间内访问商品详情页时,后台程序直接从缓存服务中获取这段数据,这种方法可以大幅降低数据库的访问压力。
在用户端,浏览器会有一系列机制通过缓存来提升页面加载速度。例如 IE/Chrome 都会缓存 GET 类型的 AJAX 请求,IE 甚至会缓存 POST 类型的请求,需要通过增加时间戳参数的方式来强制清除缓存。对于所有的静态资源文件来说,最佳实践是为它们增加一个 「Never Expires」(永不过期)的强(长)缓存,以下是一个强缓存静态资源服务器的 Nginx 配置示例:
代码语言:javascript复制server {
listen 80;
server_name yekai.net;
root /var/www/yekai.net;
location / {
index index.html index.htm;
}
location ~ .*.(gif|jpg|jpeg|png|bmp|swf|js|css)$ {
expires 365d;
}
}
通过配置 「expires 365d」,HTTP Response Header(响应头)中会返回 「Cache-Control: max-age=31536000」 的头字段,配合 Last-Modified 头字段。浏览器便可以自动完成资源的强缓存。
Cache-Control 是浏览器缓存机制中最为重要的一个配置,以下是浏览器加载静态资源文件时的缓存检查机制流程:
由此可见,静态资源缓存优化的最佳状态是:直接从本地缓存中读取 > 304 状态 > 200 状态。关于 HTTP 状态码,与网站性能优化有关的主要是以下几个。
- 尽量减少 200 状态码的请求。200 表示是一个正常的请求返回,此条优化规则要求尽可能多的减少页面的 HTTP Request 数量。常见的方法有:合并打包静态资源、使用 CSS Sprite 雪碧图合并、缓存 AJAX、使用 LocalStorage/UserData/Manifest 等本地缓存技术。
- 清理返回 301/302 状态码的入口链接。301 表示永久重定向,302 表示临时重定向。服务器端使用重定向返回通常是为了兼容一个旧的入口链接。我们能做的优化是,将调用旧入口的场景进行清理,直接调用重定向之后的新 URL 地址。
- 304 表示静态资源未更新,浏览器可直接使用本地缓存文件。通常 304 的产生与浏览器的处理机制以及服务器缓存头配置有一定的关系。304 虽然未传输文件主体内容,但 HTTP 请求的建立依然是一个可以避免的性能损耗。腾讯 KM(内部知识分享平台)上有一篇文章通过在真实海量业务场景(没记错的话是 Qzone 业务)中,正交验证 HTTP 1.0 与 1.1 协议中与缓存相关的 HTTP Header 配置,结合日志分析得出了一个最佳实践:关闭 Etag 配置,只启用 Cache-Control 与 Last-Modified 响应头。为了兼容老浏览器,可保留 Expires。因为 Etag 的缓存方案,在经过 CDN 及网关代理服务器后,会导致缓存命中率下降。从以上「浏览器缓存检查机制流程」图上可以看出,使用强缓存(Cache-Control max-age 设置为一年)后浏览器在资源过期前不会发起 HTTP 请求,那如何保证静态资源在服务器上更新后本地的缓存也能同步更新呢?可参考百度 FIS 的「文件指纹」方案。
- 清理返回 404 状态码的入口链接。静态资源文件的 404 调用需严格避免,而入口页面的 404 则在所难免。通过在全站 404 页面进行产品引导与体验优化,并结合数据上报记录来源页(HTTP Referer Header 或
document.referrer
),可以找到并清理 404 来源入口。对于由搜索引擎进入的来源,可通过主动提交新索引至搜索引擎,或使用 301/302 重定向的方式,有效利用起这些「被浪费的流量」。 - 502 服务器出错。如果是 Nginx FastCGI 的常见架构,通常是由于 Nginx 缓冲区溢出或服务器资源被耗尽引起,针对不同的业务场景进行 Nginx 的配置优化能显著提升服务器抗压性能。
如果你对上文提及的「网络性能优化」的知识点十分感兴趣,建议你通读 Steve Souders 的《高性能网站建设指南》与《高性能网站建设进阶指南》,Steve Souders 的个人网站上积累了很多性能优化的方法与案例。
如果你能看到这里,相信你已经知道如何解答前文提到的几道 BAT 网络编程面试题中,关于 「HTTP 协议、状态码、缓存与性能优化」相关的问题。