课件
Web 的发展带来大量机会
近年来 Web 发展非常迅猛,它的发展带来了大量机会,经过多年的检验,客户端、服务器这种C/S结构一直是非常适用的,这个结构里面将前后端分离开来:
后端或者说服务端现在应用最普遍的就是 HTTP 服务器,为了更好地进行开发工作,后端服务器发展出了 MVC 和其他架构。
前端或者说客户端,经过发展有种浏览器一统天下的感觉,浏览器、微信之类带浏览器的客户端可以作为通用客户端,他们做的就是借助 HTML、CSS、JS 等前端手段完成客户端的呈现和交互,因为前端的迅猛发展,也出现了前端 MVC 之类的前端架构来提高前端开发的质量。
此外还有现在非常流行的各种 App,可以把它看做是专用客户端。所以,Web 系统的架构无非就分为前端和后端,细化来说,应该怎么设计 Web 系统的架构呢?
HTTP 服务器原理
先回顾下 HTTP 服务器的原理。
从 HTTP 协议的规定来说,客户端向服务器发起请求,服务器上的服务器程序收到请求后,对请求进行解析然后处理,完成后向客户端返回响应,这个过程中的数据流传输必须依照 HTTP 协议的规定。
下面用 Python 写一个非常简单的 HTTP 服务器:
它监听在本机的 8000 端口,所有的请求传递给 SimpleHttpRequestHandler 去处理,数据流的解码、编码在 BaseHTTPServer 里面都做掉了,这个服务器程序相当于就是把你的当前工作目录映射到 localhost 的 8000 端口,让它可以通过 HTTP 请求访问到程序的工作目录,当客户端比如刘览器向我们的这个服务器程序发起请求后,首先它会经过 BaseHTTPServer 进行简单的解码,然后交给 SimpleHTTPRequestHandler 进行处理,完成后写入数据流就成为了这个 HTP 请求的响应。
所以,BaseHTTPServer 里面做的就是监听在固定的端口然后接收 HTP 请求,接着对 HTTP 请求进行简单解码和解析,最后去调用 RequestHandler。而在 SimpleHttpRequestHandler 里面,做的其实就是业务逻辑的实现,这个例子里面的业务逻辑就是要读取请求的相应文件或者目录的内容,完成业务逻辑处理后就可以写入 HTTP 响应了。这里的业务逻辑很简单,如果业务逻辑复杂的话该怎么办?这时候就很有必要引入服务器架构,有了服务器架构之后开发人员就可以更好地关注在业务逻辑的处理上,可以更好的处理更复杂的业务逻辑。
MVC设计的HTTP后端服务器
HTTP 后端服务器的架构有很多种,上图是 MVC 设计的后端,这样的架构中HTTP 请求一般会先经过 router 或者说路由,它决定要把请求交给哪个具体的 Controller 去处理,具体的 Controller 收到请求后根据业务逻辑的需要,可能需要与数据库进行交互,比如存取相关的数据,这些操作都交给 Model 层去做,这样在 Model 层的辅助下,Controller 完成了业务逻辑的处理,它会把自己的处理结果交给 View 层,View 层收到处理结果后就据此向客户端发回响应,为了追求便利,后端服务器的 View 层会去渲染模板,比如 HTML 模板,甚至 CSS 模板和 JS 模板,最后的 HTTP 响应就由 View 层完成。这边的 View 层是很有问题的,因为在这里前后端发生了耦合,会给开发和维护带来更困难。
RESTful API
上面说的 View 层前后端耦合的本质是,前端的代码交给后端去通过模板来渲染,要解决这个问题,可以让后端成为 RESTful 的 API,因为前端的发展,如果还把前端模板放在后端服务器上,带来的困难程度远比以前高得多,所以,把前端模板从后端服务器剥离出来,这样后端就可以专注于业务逻辑的处理。
RESTful 的 API 说白了就是充分利用具体的四种 HTTP 请求,包括 GET、PUT、POST 和 DELETE:
客户端向服务器端发起请求,比如 GET/activity/Iist,以前后端服务器程序会给客户端返回对应的网页,但在 RESTful 的后端服务器,返回的不是具体网页,而是特定格式的响应,比如 JSON 格式、XML 格式之类的都可以,为了更好地让前端反应后端的出错情况等,可以让返回的响应由增加特定的字段,比 code 表示错误代码,message 代表错误信息,前面访问的 GET/activity/Iist 返回 code=0 代表没有出错,实际应该返回的数据在 data 字段里,返回的是一个数组,每个元素都是一个 object,里面有对应活动的信息,这样实际上后端向前端返回的响应就精简了非常多,以前需要返回整个网页,现在只需要返回前端需要的信息即可。更大的好处是完全把前端分离出来,前端的开发可不依赖后端进行,以很小的代价即可完成前后端的衔接。
有了这样 RESTful APIE 的后端,后端通信就可以在 Web 页面上充分利用 Ajax 的特性,不仅带来更好的用户体验,还让前后端更好地分离。而且,这样的后端对 App 等专用客户端也是通用的,在专用客户端发起 HTTP 请求,照样可以拿到需要的后端数据,一举多得。
不同的端口?跨域?
接下来的问题就在于 Web 页面服务要如何进行部署。对于 Web 页面而言必须让前后端能通过同一个域名的同一个端口访问到,否则会出现跨域的问题。跨域的问题主要是说不能用 Ajax 在一个域名的页面上获取另一个域名或者另一个端口提供的数据,因为这样会非常不安全,所以我们必须把前端页面和后端的 RESTful API 部署在相同域名的相同端口上,解决办法就是通过 Nginx 之类的Web 服务器托管我们的前端和后端。
举个例子,我们把 nginx 绑定在服务器的 80 端口上,客户端请求 80 端口会被 nginx 收到,根据请求的不同,Nginx 直接把前端文件内容返回给客户端,或者通过端口转发、管道文件,把请求转发给我们的 RESTful 后端,后端响应后,Nginx 再把响应返回给客户端就可以了。借助 nginx,可以有多个 RESTfull 后端同时用同一个域名的同一个端口来提供服务,只要在 nginxi 配置里设置好相应的路由就可以了。
异步任务
现在已经通过 RESTful API 解决了前后端难以分离开发的问题,同时还通过这个解决方案带来了更好的用户体验和更灵活的部署方式。但有时候我们会遇到这样的问题,因为 HTTP 请求是同步的,所以当它在服务器上响应之后,为了避免系统的复杂性,就不应该再做别的事了,而且浏览器一般对 HTTP 请求有超时的限制。
比如假设我们开发了一个自动根据用户上传的图片进行人脸识别并进行复杂分析的服务,每张图片经过优化以后还需要处理 1 分钟,但一个 HTTP 请求在浏览器限制 30 秒内必须响应,这种情况下我们就不得不把图片的处理变成一个异步的服务。
所谓异步的服务,一般是以异步任务的方式呈现的,比如我们的服务器程序收到了一个请求,可以告诉任务的管理程序说需要启动一个任务,管理程序说好呀,然后返回一个任务 ID,服务器程序就可以马上把任务 ID 作为响应返回给客户端了,而我们的管理程序一般会在返回任务 ID 之后再去启动任务。这样客户端程序就可以通过任务 ID 询问任务的进展情况,比如任务正在运行或者任务成功了、任务失败了。如果任务成功了,我们也可以得到任务的响应,如果任务失败了,我们也可以得到任务的错误信息。这样就很好地用异步的方式解决了问题。更棒的是我们可以设定一些定时器,让一些任务可以在设定的时间自动启动,比如自动发邮件之类的。
异步任务的实现方式
要实现异步任务,借助现成的成熟框架可很容易做到。比如异步任务队列框架 celery,借助它,既可以实现定时任务,也可以实现异步任务。当然,定时任务也可以通过操作系统的 cronjob 之类的来实现。
Node.JS 可以借助其异步的特点自行实现,但极不推荐,因为自己实现的往往难以管理,而且容易出错。
异步任务队列为系统规模化提升带来的增益
借助 celery 之类的异步任务队列不仅可以给我们带来异步服务的好处,还为系统的规模化提升带来增益。因为 celery 本身就是一个分布式任务队列,可以把异步任务分配到服务器集群去处理,借助集群的力量,以很快的速度消化掉大量异步任务。
高性能、高稳定性的服务
计算机体系结构里面划分了 7 个存储层级,其中网络访问是在最底层,数据库往往是在硬盘层,要实现系统的更好性能,可以利用缓存的思想,把经常访问但不经常变化的数据放到上一层存储层级即内存中去,这样借助内存缓存,就可以提高 Web 访问的速度。
为了实现内存缓存,可以借助内存型数据库来实现。内存型数据库往往比传统数据库简单不少,比如 redis、memcached 都是典型的Key-Value键值对存储,
它们的一个用途就是作为缓存来使用。
比如,假设我们发现 GET/activity 这个 API 的访问平均耗时要 500ms 左右,但这个 API 大多数情况下返回的都是相同的结果,就可以将它的 RESPONSE 存入redis 再次访问,直接从 redis 取结果,平均耗时降低至 100ms 以内。这里需要注意缓存的过期机制,比如修改了activity 的信息后,缓存过期或者在一些对实时性要求不那么高的场景下,可以直接给缓存设置固定的过期时间。这样在系统访问频繁的时候,通过缓存,可以有很好的性能表现。系统访问不频繁时,也可以让系统的负载适当均衡。