[date: 2018-01-30 11:48] [visits: 6]

WEB服务器的实现包含哪些内容

本文根据自己的经验,整理实现一个WEB服务器可能包含的内容,每一项都点到为止,不做过多展开。以下顺序无重要程度区分,皆按实际场景考虑是否需要。

路由

WEB服务器的路由,本质上是根据HTTP请求携带信息,决定执行何种逻辑。

最简单的if-else语句,也是一种路由方式:

// 路由依据通常使用请求method和url
if (method === 'GET' && url === '/robots.txt') {
    // do something
}
else {
    // do other things
}

不同平台不同框架,路由定义的方式会有差异,但本质相同:通过HTTP请求所携带的信息,决定不同的执行逻辑。拿Node.js下Express举例:

const express = require('express');
const app = express();

// GET:/home will match funcA
app.get('/home', funcA);

// POST:/home will match funcB
app.post('/home', funcB);

// GET:/params/a or GET:/params/b will be match funcC
app.get('/params/:id', funcC); 

// GET:/regexp/list-1.html or GET:/regexp/list-2.html will be match funcD
app.get('/regexp/list-[0-9].html', funcD); 

以上代码就是Node.js + Express的一种简单路由定义方式,但在真实场景下不推荐这样一行一行添加路由的写法,尽量做到按类型、业务拆分定义,也可以抽成独立于语言框架的一种配置项。

日志

日志用来记录跟请求相关的信息,经常作为数据统计,问题定位等用途,可分为以下几种:

访问日志是最常见的基础日志,记录每一个request的部分请求信息和响应信息,通常会记录的信息有请求者IP、请求方式、请求URL、请求主体大小、请求Host、请求处理时长、响应状态码、响应主体大小等,这些日志信息可用做访问统计、问题定位等

业务日志的内容,根据业务需要输出,可用于业务统计、追踪、问题定位等

请求处理过程中发生错误、异常,只要能被捕捉,都应该输出到错误日志中,并且包含尽量多的上下文信息,方便开发人员定位BUG

会话保持

HTTP是一种无状态协议,但在某些业务场景下,需要保持会话信息,即请求需要跟踪并唯一标识请求者。

现代WEB服务器都能实现会话保持功能,实现原理并不难,保证每一个请求都包含本次处理所需要的所有信息上下文即可。实现原理:

这个ID的承载体通常是一个HTTP头部,在浏览器中分别为Set-Cookie、Cookie,前者在响应时使用,后者在请求时使用。同时浏览器针对Set-Cookie还有很多其它特性,如:过期时间,作用范围(域名、路径),协议(HTTP、HTTPS)等

有一个常见的面试题:Cookie与Session的区别。以下是我的理解:

Session通常是与SessionId一一对应,SessionId的传输依靠Set-Cookie/Cookie头部,SessionId对应的数据可以存放在内存或数据库中,前者难以跨进程共享故不推荐,后者因为读写高频经常选用Redis。

缓存

系统不同的部分或层级,获取数据的资源开销和速度经常会有显著差异,为追求更高的性能与更快的速度,于是就有了缓存这个概念,在很多应用场景下,使用缓存都可以显著提升系统整体性能。

跟HTTP相关的缓存,常出现的地方有:

客户端,通常也就是浏览器,会根据一系列HTTP缓存规则去进行资源缓存,当资源命中缓存时,其获取速度是最快的,具体缓存策略参考HTTP缓存策略

请求在客户端没有命中缓存时,会向服务器发送请求,但有可能经过不同的HTTP代理,有些HTTP代理实现了缓存功能,甚至有些专门用作缓存的HTTP代理。缓存代理在缓存未过期的情况下,不会去真实请求服务器资源,而是使用本地命中的缓存响应客户端,用于提升资源获取速度,同时减少网络负载

反向代理,是现代WEB服务器架构中常见的一个名词,代理真实服务器与客户端进行通信。在反向代理服务器上通常会有缓存功能,减少真实服务器的负载,提供更高性能、更快速的资源访问能力

版本控制

版本控制,在这是指API的版本控制,一旦生产环境投入使用,在后续的版本迭代过程中,相同接口如果要修改,改变输入或输出。由于依赖服务器的客户端难以保持同步更新,新接口部署时还需同时保持旧接口可用,可以有两种方式:

如果接口的性质、输入、输出都没有变化,只是内部实现的更新,则此方式可取。但如果因输入、输出变化,也在原有的老接口中揉合进新接口的功能代码,会导致代码难以阅读和维护,不推荐

重新定义接口,支持新场景,原有接口不改变,此方式即接口的版本控制,在约束和规范下定义并实现新的接口,旧接口确认不再使用或不再兼容之后,从系统中移除。

接口的版本控制虽非必需,但不表示不重要的,能够在系统设计之初考虑进去最好不过,具体如何实现版本控制,即如何确定约束和规范则因人而异,没有必然正确的标准。

访问控制

如果某些敏感资源,只有特定的访问者可以访问,就需要做访问控制,比如:内部系统不允许外部调用,用户资源需要登陆后才可以获取,管理系统需要管理员登陆才可访问,屏蔽某些已知的恶意IP访问等

针对以上场景,可能采用的方式有:

允许或屏蔽特定用户访问,该方式比较僵硬,但在某些情况下却非常高效和便捷

有各种各样的验证码形式,但主要目的都是为了区别对待机器与人

依靠用户登陆,识别用户身份,控制访问权限

单点调用频率限制

单点调用频率限制也是访问控制的一部分,有特殊性,也经常被忽视。单点调用频率限制,最终常会转变成识别机器与人,因为真人的正常操作往往难以达到调用频率限制门槛。

需要考虑单点调用频率限制的因素:

需要消耗过多计算机资源(CPU、IO、磁盘等)、过多带宽、额外成本(短信验证码,其它收费服务等)等,API如果具有以上特性,那么为避免因非法使用而导致系统成本浪费,在技术上就需要尽可能的去防范

在某些特定业务场景下,如限量抢购、投票等场景,为了维持公平性,可能需要加以限制单点调用频率

统一响应格式

统一响应格式主要指业务API的返回值,HTTP的标准状态码是一种方式,但并不推荐在业务场景中直接使用。现代服务器API大多使用JSON作为数据交换格式,如果再套用HTTP标准状态码,会使得前端逻辑处理异常繁琐。

例如根据文章ID获取文章,当文章ID对应的文章不存在,如果采用HTTP标准状态码,此时响应码应为404, 原因短语为Not Found,因为接口本身设计成JSON的返回值,返回404状态码会导致前端无法进入正常处理逻辑,需要额外逻辑去理解响应,并友好提示用户。

统一API的响应格式,可使用以下简单结构:

response = {
    code: number, // 0 表示成功,其它表示失败
    msg: string,  // 描述信息,code非0时表示错误描述
    result: var    // 成功时的响应数据,用于业务处理
};

所有能捕捉的异常,都可以包装成上面的结构返回,Content-Type: application/json。

对等

服务器实例部署需要保持对等,便于水平扩展,可分为两种:单机多进程、多机多进程。单机多进程,解决对单机的依赖(本地文件、本机网络资源、本机独有服务等),就可以转为多机多进程部署。

负载均衡

如果服务器实例实现了对等部署,也就是同一个服务,有多个实例在运行,通常在前面会存在一个负载均衡服务器,它会考虑到多个不同服务实例的负载情况,给它们带去不同数量的请求。可把负载均衡理解成另一个层次的路由。

反向代理

反向代理服务器是隐藏真实服务器相关信息,代理真实服务器与客户端打交道,同时可以集成其它功能的角色。出色代表:Nginx,性能稳定可靠,包含大部分非业务相关特性,如缓存、访问日志、负载均衡、路由转发等。

总结

一个现代WEB服务器,不一定包含上述所有内容,同时也不限于上述内容。上述每一项,都是根据自己的理解,简单描述一遍,不涉及具体实现与技术细节,因为每一项内容展开,都有太多太多内容。后续针对其中某些内容,再单独写文章进行更详细和深入的介绍。