👉
目录
1 IP、DNS 和 CDN
2 TCP、消息分包和协议设计
3 CGI 和 FastCGI
4
服务器模型谈
5 数据层的演进
当你在浏览器输入 qq.com 按下回车键,到页面呈现在你面前,整个过程发生了什么?我以前思考过这个问题,从最前面的浏览器到最后的 db 都梳理的一遍,触发了一次技术顿悟,将很多散落的知识点贯通起来了。
如果面试时问你「局域网 IP 有哪些 IP 段」,你怎么答?
先说个 QQ 的小故事。QQ 刚开发时也没想到 QQ 会发展成中国互联网基础设施,就用4字节整形表示 QQ 号了。早期内部的一些项目有用 int 表示 QQ 号,能表示的最大值是2^31-1,即21亿多。在 QQ 号发放近20亿时,即通搞了个22亿 QQ 号的专项,通知每个项目检查修改,使用 unsigned int 表示 QQ 号,以支持21亿以上的 QQ 号。
可以看出在底层和协议设计中,字段的扩大是非常麻烦的。IP 地址也有类似问题。目前广泛使用的是 IPv4,一个 IP 地址4个字节,理论上共有2^32个 IP 地址,接近43亿。这个数量还不到人均一个,远远不够,自然也不能每个设备一个公网 IP 了,所以 Internet 规定了 IPv4 地址空间的一部分供
专用地址使用
,这些地址永远不会被当做公用地址来分配,局域网内部 IP 就是使用这些专用地址。
常见专用地址有:10.x.x.x,172.16-31.x.x,192.168.x.x,另外127.0.0.1表示本地回环地址,代表设备的本地虚拟接口。
了解这个后,如果你发现你在公司的 IP 是192.168.0.100,在家里的IP也是192.168.0.100时,就不会诧异了。局域网内部 IP 只用于局域网内部通讯,如果要连接广域网,还要用到 NAT(网络地址转换)技术。
NAT 常用于局域网内部 IP 和局域网分配的公网 IP 之间进行转换,使用最多的是端口多路复用(PAT)方式,简单的描述就是,你在局域网内访问 qq 时,路由器会记录你的内网 IP 和端口(假设是192.168.0.100:12345),用路由器的公网IP和一个未使用的端口向公网发网络包(假设是202.96.134.133:23456),路由器还会把TCP~202.96.134.133:23456~192.168.0.100:12345配对保存起来。当 qq 的响应发到202.96.134.133:23456后,路由器通过查找配对表就知道是发给192.168.0.100:12345。
搞网络管理的同学对 DNS 比较熟悉,程序员也需要了解,不管是前端还是后端。
IP 地址不好记,于是就有了域名。浏览器访问 qq.com 时,会先做一次域名解析,把 qq.com 这个域名解析成 IP 地址,然后才能发出 IP 包。
在 windows 和 linux 下解析域名前,会先从本地 hosts 文件里查找网址映射关系,如果有,就先调用这个 IP 地址映射,完成域名解析。早期做 Web 开发时会用这种方式来切换开发环境、测试环境、预发布环境和正式环境。
如果你在腾讯云注册了一个个人用的域名,一般会直接用腾讯云的 DNS 服务器来管理该域名的解析。对于门户网站,则会自己搭建 DNS 服务器来管理域名。自建 DNS 服务器既方便管理,又能提高安全等级,防范 DDOS 域名攻击。
如果域名的访问量比较大,可以让域名对应多个 IP 地址,操作系统会随机选择其中一个,这是早期的
Web
负载均衡方式。但因为有些 DNS Server 不按 TTL 指示缓存,导致 DNS 变更生效时间最长可达到24~48小时。一旦某个 IP 的机器故障,而 DNS 又不能立即刷新,会让部分用户服务不可用。于是就有了用 LVS/nginx 来动态负载均衡的方式,LVS 的负载均衡基于 IP 层,nginx 则是用 HTTP 层的反向代理机制。
此外还要考虑到电信/联通/移动三大运营商跨网的问题。运营商之间的通讯带宽有限,如果你的服务器在电信机房,那么联通用户访问就会比较慢。所以就有了多线机房的出现。多线机房实际是一个机房有电信、联通、移动等多条线路接入。通过多线机房内部路由器设置,及 BGP 自动路由的分析,实现电信用户访问电信线路,联通用户访问联通线路,这样实现电信联通均可以快速访问 。
腾讯云的 CLB 选用 BGP 多线,就可实现电信/联通/移动多网统一接入和自动负载均衡。
为了给用户提供更快的访问速度,人们发明了
CDN
(Content Delivery Network,内容分发网络)。
简单
地
说就是,一个域名对应有多个 IP,这些 IP 分布在全国各地,用户访问域名时,DNS 服务器根据用户的来源 IP,返回一个就近的 IP 给用户,从而实现更快的访问速度。
从上面的描述可以知道,CDN 要求各节点的内容是一致的,这样才能让各地用户访问到一致的内容,所以 CDN 主要用于
Web
静态资源的分发和加速。
Web 性能优化方案一般会有一条动静分离,即把静态资源使用的域名和动态脚本的域名分开,有了 CDN 后,一般会把静态资源的域名托管给 CDN 以提高更快的访问速度和更低的成本。
现在也有动态 CDN,针对接口这种请求也能做就近接入,但原理是转入云厂商的内部链路做网络加速,一般不把数据缓存在边缘节点上。
一次网络请求经过 DNS 解析知道了目的 IP,现在就要发出网络包了。
讲网络编程的教科书一般都会对 TCP 的可靠传输做详细说明,但对于 TCP 是一种流式协议讲解的不多,但这背后隐藏着很重要的一个知识点。先做个名词定义方便交流,这里的 “消息”是指应用层的一个完整的协议包。
流式协议的特点是什么?就像流水连续不断那样,消息之间没有边界。例如 send 了3条消息,分别是100字节、50字节、80字节,recv 时可能收到的是230字节,就是说一次 recv 收到了3条消息,需要应用逻辑自己对 recv 到的数据进行分析,得出完整的消息。
能一次 recv 到多个消息,也可能一次 recv 到一个半消息或半个消息,都是有可能的,这就是流式协议的特点。有的文章讲的粘包也是这个概念。
既然 TCP 是一种流式协议,需要应用层自己来分析出完整的消息,那有哪些方式来确定一个完整消息呢?这个就是应用层通讯协议设计的工作了。
先看看最常见的 HTTP 协议是如何来分包的。HTTP 协议是一种文本协议(非二进制协议),用 \r\n\r\n 来分割消息头和消息体,HTTP 请求的消息头中有 Content-Length 来告知消息体有多大,如果没有该字段就表示无消息体,GET 请求大多是这样。HTTP 响应的消息头中,或者有 Content-Length,或者有 Transfer-Encoding: chunked 告知以 chunk 模式分析消息体。
HTTP 用 \r\n\r\n 来分割消息头和消息体,这种用特定字符/字符串来分割或分包的方式,还有不少协议用到。例如 FTP/SMTP/POP3 都是用 \n 来作为一个命令结束的标志。
这种消息分包的方式,需要应用层去扫描已 recv 到的数据,性能上还不够高效,代码不严谨的还容易被攻击。在需要自定义协议的项目中,不少选择用二进制协议,解析高效,安全性更好些。
最简单的二进制协议分包方式是消息的头4个字节表示消息的总长度。这种方式还需要对最大消息长度做个限制,例如 64K 或 1024K 大小,避免超大数据包对接收方缓冲区的破坏。更进一步的,可以加入简单校验方法。例如消息头1个字节固定式 0x2,消息的最后1个字节固定式 0x3,消息总长度放在第2~5字节。这样收到完整消息后,如果头尾不是0x2和0x3,就直接异常处理。
消息分包是协议设计的一个工作,协议设计的话题还不少,这里以 HTTP 协议为例,简要的说说里面设计的点,自己设计的协议也可以对照着有选择的使用,原理是共通的。
-
由消息头+消息体组成
:空行分割 HTTP head 和 body,HTTP 头的每一行以 \r\n 结尾,空行就是 \r\n\r\n 。
-
消息分包:
如上所述,HTTP 用 Content-Length 和 Transfer-Encodeing 来分包。
-
消息压缩:
请求中有 Accept-Encoding 字段,响应中用 Content-Encoding 字段表明压缩方式,一般采用 gzip 压缩。
-
消息加密:
https (SSL: Secure Socket Layer)。
-
消息 ID:
URL 就是消息 ID 。
-
响应的状态码:
第一个数字定义了响应的类别。
-
1xx:指示信息--表示请求已接收,继续处理
-
2xx:成功--表示请求已被成功接收、理解、接受
-
3xx:重定向--要完成请求必须进行更进一步的操作
-
4xx:客户端错误--请求有语法错误或请求无法实现
-
5xx:服务器端错误--服务器未能实现合法的请求
协议版本号:
HTTP/1.1中的1.1就是 HTTP 1.1 版本
长连接:
请求中 Connection: keep-alive 表示希望服务器保持连接,减少 TCP 连接的开销。
字符集:
Content-Type 字段表明了字符集,例如: Content-Type: text/html; charset=gb2312。
字符转义:
URL 中的参数需要做 URL 转义处理,例如 http://xx.com/do?name=t%2F%3F%23%3Daa 表示 name 为 t/?#=aa。
在我们自己设计协议时,可以有选择的使用,如果消息比较大,可以采用支持压缩;如果要兼容多个版本的协议,那版本号必不可少。如果采用二进制协议,字符集和字符转义的用处不大。
HTTP 协议是一种文本协议,也是一种 Name-Based 协议,就从这两方面来说。
文本协议 vs 二进制协议
文本协议的特点:
-
便于人。
-
易于阅读、理解、调试、构造。
-
解析复杂、冗余多。
-
需要考虑字符转义。
二进制协议的特点:
-
便于机器。
Name-Based vs Position-Based
Name-Based 协议的特点:
-
协议字段都用 Name 标识。
-
协议字段与位置无关。
-
协议字段可缺省。
-
新增协议字段比较方便。
-
解析复杂。
-
需要考虑字符转义。
Position-Based 协议的特点:
-
每个协议字段都有特定的位置。
-
新增协议字段需要做好协议版本管理(protobuf 这类就挺好)。
-
解析更高效。
消息经过网络传输,到达了服务器端,最常见的服务器是 Web 服务器,做 PHP 的同学都知道 FastCGI 模式的 PHP 比普通 PHP 更高效,其中的原理是什么呢?
Web 服务器能解析 HTTP 请求,返回静态资源(HTML 页、图片等),但要输出动态内容,必须得 PHP/C#/Ruby/Java/Python/C/C++ 这些外部程序来实现。
早期有个技术叫 CGI(Common Gateway Interface,通用网关接口),是用于 Web 服务器和外部程序之间传输数据的一种标准。一个简单的 CGI 程序(C++ 语言)如下:
#include
#include
int main()
{
printf("Content-type: text/html\r\n\r\n");
printf("your name is:%s\n", getenv("QUERY_STRING"));
return 0;
}
浏览器访问这个 CGI 程序,就会显示:your name is:name=xxx 。
CGI 规定了 Web 服务器如何和 CGI 程序之间传输数据,具体过程大体是这样:
-
Web 服务器收到的请求信息后,启动
CGI 程序(apache 是 fork 进程 exec CGI 程序);
-
Web 服务器通过环境变量和标准输入把请求信息传递给 CGI 程序;
-
CGI 程序执行业务逻辑后,通过标准输出和标准错误把响应数据返回给 Web 服务器,CGI 程序 exit;
-
Web 服务器再组织成 HTTP 响应包发给浏览器。
在上面的例子中,第一行 printf 是输出 HTTP 头(还记得 HTTP Header 和 Body 是用 \r\n\r\n 分割的么?),getenv("QUERY_STRING")是从环境变量获取 URL,printf 是通过标准输出返回内容。
Web 服务器会把哪些信息通过环境变量传递给 CGI 程序?常用的有这些:
-
CONTENT_LENGTH :向标准输入发送的数据的字节数(POST)。
-
QUERY_STRING:实际存放发送给 CGI 程序的数据(GET)。
-
REQUEST_METHOD:传送数据所用的 CGI 方法(GET或POST)。
-
HTTP_COOKIE:cookie 值。
-
REMOTE_ADDR:用户 IP。
-
SCRIPT_NAME:请求的 CGI。
可以看到 CGI 只是一种标准,可以用任何一种语言编写 CGI 程序,只要这种语言具有标准输入、标准输出和环境变量,比如:C/C++,perl,PHP、ruby。按照 CGI 标准要求,就能和 Web 服务器交互起来。
CGI 是通过环境变量/标准输入、标准输出/标准错误来传输数据,运行性能比较低,主要有两点:
-
每个请求都需要 Web 服务器去 fork
出
C
GI 程序,频繁
fork 进
程比较耗时。
-
CGI 程序每次都是从头运行,读配置、连接其他服务都得重新来,也比较耗时。
FastCGI 是对 CGI 的改进,FastCGI 模式下,Web 服务器和 FastCGI 程序传输数据的过程大体是:
-
Web 服务器收到的请求信息后,按 FastCGI 协议把请求信息通过 socket 发给 FastCGI 程序;
-
FastCGI 程序执行业务逻辑后,通过 socket 把响应数据返回给 Web 服务器,FastCGI 程序不 exit;
-
Web 服务器再组织成 HTTP 响应包发给浏览器。
对比 CGI 的通过,可以发现主要是少了每次 fork 的过程,并且用 socket 来传输数据,这是 FastCGI 接口更高效的原因。
FastCGI 有这些特点:
-
FastCGI 程序常驻内存,启动后可以反复处理请求。
-
FastCGI 就是进程池/线程池模型的通用同步服务器框架。
FastCGI 程序处理请求后不会退出,可以反复处理请求,那么在启动后就把配置解析、与其他后台的连接建立好,不用每次请求时搞一边,自然更快了。
至于这个 FastCGI 内部如何实现进城池/线程池,就是 FastCGI 进程管理器(FastCGI 引擎)的事情了。C/C++ FastCGI 常用 apache 的 mod_fastcgi 模块,PHP 常用 spawn-fcgi 和 PHP-FPM。
现在更多是用 nginx 的反向代理功能,把 HTTP 请求转发到后端的 trpc 服务直接处理。这里的 trpc 服务就有点 FastCGI 的感觉,但不用与 nginx 部署在一起了。
上节讲到 Web 服务器和 CGI/FastCGI 能动态输出内容,从而提供更强大的业务处理能力。Web 服务器这种架构,我称之为 Web 模式,与之相对的是 Svr 模式。Web 模式和 Svr 模式是互联网项目的后台最常见的两种模式。先介绍几个概念。
同步通讯是指在一个连接中,一个请求的应答没回来前,不能发送下一个请求,整个通讯过程是请求1-应答1-请求2-应答2……这种。异步通讯与同步通讯相反,在一个连接中,可以随意发送请求,而且收到应答的顺序可能与发送请求的顺序不一致。
从描述上就能理解,同步通讯的通讯性能比异步低,但好处是简单,不用考虑乱序应答的复杂情况。
同步逻辑是指在代码中遇到需要等待的调用时(例如向数据库查询数据),阻塞着,一直等待调用完成。异步逻辑则是不阻塞,继续执行后续代码。
我们常见的文件 IO 接口 read/write,网络 IO 接口 send/recv 默认都是同步的,需要执行特别的设置 API 才能变成非阻塞的。同步逻辑符合人脑的思维模式,写异步逻辑需要处理各种非阻塞和异常情况,极其挑战智力,就算采用有限状态机,也是件很具挑战的工作。
CGI/FastCGI 每次执行时,会从数据层(db 或数据 cache)获得数据,修改后再写回到数据层,也就是说 CGI/FastCGI 并不会缓存数据。这就是无状态。
无状态的架构中,请求是这台 Web 服务器处理,还是那台处理,都没有区别,因为数据都是从数据层获得的。这种架构的扩容非常方便,但需注意,要防范一个请求同时多并发时,可能出现的数据不一致的漏洞,即要做防并发处理。
有状态是与无状态相对的概念,是指服务器中缓存了数据。这种架构中,因为不需要反复的从数据层取数据,性能会高很多,但因为服务器缓存了数据,为了保持数据一致性,只能把该数据的请求都分发到这台服务器来处理。对于游戏来说,每个区的用户数据是独立的,对交互的实时性要求高,采用有状态的架构正好合适。
早期的浏览器如果要实现在线聊天,需要浏览器定时请求服务器获取聊天信息,Web 服务器无法主动给客户端推送消息。到 WebSocket 出来后才具备实时推送的能力。
Web 模式业务一般有这些特点:
-
是请求-应答式,即先客户端请求,才会有服务器应答(少数场景可借助 WebSocket 主动推送);
-
是同步通讯,一个连接里,只有收到应答后才能发下一个请求(HTTP2 可多路复用);
-
是同步逻辑,Web 模式较少采用异步逻辑;
-
是无状态架构,CGI/FastCGI 每次从数据层获取数据,修改后再写回到数据层。
Svr 模式就是与之相对的,客户端和服务器之间采用长连接,客户端的请求不一定会有应答,服务器还可以主动推送消息到客户端,通讯也不限定是同步的,客户端可以不断的发送请求,服务器的应答甚至可能与请求的顺序不一致。
Svr 模式相对 Web 模式来说,通讯性能更强,因为采用了长连接和异步通讯,还能主动推送消息,这是优势。但也因为采用了长连接和异步通讯,对客户端开发的要求就更高些,需要处理好断线重连和支持响应乱序。
Web 模式因为模式简单,Web 服务器自己实现了 HTTP 协议处理和 FastCGI 进程管理等通用操作,FastCGI 这些外部程序只需要处理业务逻辑就行,降低了很多门槛。而且因为是无状态的,扩容非常方便,直接加机器就能搞定,这个平滑扩容的优势在 Web 时代的作用非常大——搞性能优化、架构优化的时间成本比较大,而且不可控,加硬件就能快速抗住,是个好的方案。