听说你要做网站

现代 Web 后端技术超入门

引言

现在房价这么高,作为一个程序员只能靠做个网站看能不能卖出 100 万这样搏一手了。这里尝试介绍一下现代 Web 后端并解释一些常见术语,希望能够帮助平常不做 Web 的程序员迅速上手。

我非常确信这篇文章里有很多概念不太对,如果发现哪里不太对的话请跟我讲...

实现一个手动的 Web 服务器!

这里的手动真的就是字面上的意思,这里我们要实现的效果是:

  1. 在命令行中运行 Server。
  2. 在浏览器中打开 http://localhost ,也就是你本机的地址。
  3. 在命令行中输入你想在浏览器中显示的结果,然后...
  4. 在浏览器里看到你之前输入的结果!

更棒的是整个过程我们一行代码都不用写!

ncat

如果你是在 Windows 上的话,请在这里下载 ncat,放到任何地方并在命令行里执行。ncat 的基本功能一方面是用来像任意 TCP/UDP 服务器发送数据,另一方面可以作为一个非常简单基本的 TCP/UDP 服务器来接受任何数据并显示出来。这里我们要用的显然是后者。

首先,在命令行里运行 ncat --listen 80,意思是监听 80 端口,并把接受到的数据显示在命令行里。其中 80 就是著名的 HTTP 端口。如果你使用的是 Linux 的话这里需要 sudo,而且如果你的机器上已经有网站在跑的话那么这里可以把 80 改成 8080,对应浏览器里的地址也需要用 http://localhost:8080/

C:\Bin>ncat -l
_ <---- 闪动的光标

然后,打开你最喜欢的浏览器打开地址 http://localhost/ 。这时你应该会看到浏览器显示正在链接,而命令行窗口里应该是可以看到这样的东西:

C:\Users\jagt>ncat --listen 80
GET / HTTP/1.1
Host: localhost
User-Agent: Chrome
Accept: */*
_ <---- 闪动的光标

这里显示的东西就是 HTTP 请求,也就是浏览器发送的数据。那么现在我们要输入想要显示在浏览器里的内容。很容易想像,既然请求看起来有些格式,我们输入的内容也必须有些格式才行。拷贝下面的内容到命令行中,如果没反应就多按下几次回车。

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 14 

Hello Browser!

然后再看看你的浏览器里面,应该就出现了 Hello Browser!,也就是你刚刚输入的内容。

Web 后端基础原理

事实上无论是怎样的网站,只要你用浏览器去访问的时候就会有一台远方的电脑再以非常快的速度重复着我们上面手动完成的步骤。整个过程简单概括的话大概就是这样几个步骤:

  1. 用户在浏览器里输入网站,浏览器负责找到这个网址对应的计算机,并尝试连接其 80 端口。
  2. 服务器接收请求,建立 TCP 连接。
  3. 浏览器通过建立的的连接发送 HTTP 请求 (HTTP Request),也就是上面例子中 ncat 里最开始显示的内容。
  4. 服务器根据请求的内容返回 HTTP 响应 (HTTP Response),也就是我们上面手动输入的东西。
  5. 浏览器根据返回的内容来显示页面。

对于这个过程额外有几点很重要的是:

  • 浏览器总是连接 80 端口 (HTTPS 除外),但是你应该知道标准的 TCP 连接监听的是一个端口(在这个例子是 80),而真正传送数据的是另外一个随机的高位端口(比如 54321)。如果每个用户都占用 80 端口传数据的话那就麻烦了。
  • 虽然 TCP 协议是两边都可以主动发起消息,但 HTTP 额外规定了一个 HTTP 连接的过程是,浏览器发送 HTTP 请求,服务器收到请求全文后,返回 HTTP 响应,在浏览器接受到之后就算结束。可以注意到的是,浏览器和服务器只有一次互动的机会,而且一定是浏览器主动发起,而服务器只能被动的根据收到的请求内容返回结果。
  • 这里讲的请求 (HTTP Request) 和响应 (HTTP Response) 真的就是上面看到的那样是纯文本。浏览器和服务器之间除了在 Request 和 Response 内的内容外不应该需要额外的信息。

上面描述的基本就是 HTTP 协议 的工作方式。现在流行的很多轻量级的 Web 框架都在按照这个模式进行抽象。首先看看当下最新潮的 node.js 官网上的例子:

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World\n');
}).listen(1337, '127.0.0.1');

其中的 reqres 完完全全对应的就是 HTTP Request 和 Response。里面的函数最主要的工作就是读 req 的内容,把你想显示在用户浏览器里的东西写到 res 里面。这里也反映了很多 HTTP 协议的特性,在一个连接的过程中你的起始条件就只有 Request,唯一能做的就是把需要显示的东西写到 Response 里。你没机会函数里面再向浏览器索取信息,也不能在 Response 写完后再向浏览器发送任何内容。

把这段代码里的内容跟最终的 HTTP 响应对比一下:

HTTP/1.1 200 OK
Content-Type: text/plain

Hello World!

其中的 200 所在的位置是 HTTP Status Code,常见的就是 200 OK404 Not Found,浏览器会根据其值来做各种行为,像如果是 301 Moved Permanently 的话浏览器就会自动跳转到另外一个页面。Content-Type 这里先不管他... 下面空一行后面的就是 HTTP message body data,很明显就是显示在浏览器中的内容。

接下来是 Python 的 flask 的例子:

@app.route('/')
def index():
    username = request.cookies.get('username')
    return 'hello, ', username

应该可以很明显的感觉 flask 抽象的层次的要比 node.js 更高一点。这里回头看看最开始在 ncat 里面看到的 HTTP 请求中的第一行:

GET / HTTP/1.1

这基本上也是其中最重要的一部分,读出来就是 "我要 GET 这个网站上地址为 / 的页面"。其中 GET 是请求的方法 (method),而 / 就是路径。由于 GET 是最常见的一个方法,flask 中可以省略这个参数。这个请求根据方法和路径被 route 到了我们这个 index() 函数。其中 request 毫无悬念的对应的就是 HTTP 请求。整个东西的意思,读出来就是 "遇到要 GET 路径为 / 的请求,从 request 里面拿到用户名并返回响应 hello, 加上它的用户名。"

如果上面的都明白了的话,到这里就只剩下一个问题了:request 里面的 cookies 是从哪里来的?其实 cookies 也是包含在 HTTP 请求的内容里的,下面是一个例子:

GET / HTTP/1.1
Cookie: name=value; name2=value2

这里 Cookie: name=value1; name2=value2 这种格式的东西叫做 HTTP Headers。HTTP 标准中规定了一系列的请求和响应中可能会出现的键和值的规则,用于控制服务器和浏览器的行为,比如:

  • Response 中的 Content-Type: 你应该发现有时候你打开一个链接会显示页面,有时候却会弹出另存为的对话框。决定这个行为的就是这个 Header,其值是一个 MIME Type。如果是 Content-Type: text/html 的话,浏览器就会把后面的文字显示出来。如果是 Content-Type: application/octet-stream 的话,一般就会弹出"另存为"的对话框。
  • Response 中的 Set-Cookie:浏览器看到 Set-Cookie: 12345 的话,浏览器再之后的访问该网站的 HTTP 请求中都会带上 Cookie: 12345。像用户登录验证等等很多都是靠 Set-CookieCookie 来实现的。
  • Request 中的 User-Agent:记得原来刚上网的时候有那种很流弊的图片,可以显示你的地理位置,浏览器版本和操作系统。其实这个就是用 User-Agent 来实现的。一个例子:Mozilla/5.0 (Windows NT 6.1; WOW64) Chrome/33.0.1750.154,很容易看到相关的信息。
  • Request 中的 Referer:记得原来刚上网的时候有那种防盗链的图片,如果图片是被嵌入在别的网站上的话会变成什么“此图片来自于哪里哪里”这样。这个就是用 Referer 实现的,其内容是你链到这里之前访问的一个地址。

这里的中心思想就是,大部分这些控制行为的东西都是用 HTTP Headers 来实现的。好消息是大部分 Web 框架中你不太需要显示的控制他们。

HTTP 小结

再回头看看最开始手动 HTTP 服务器里的请求和响应,这里我们给每个部分加上注释。

 请求的方法
 | 
 |  请求的地址
 |  |
GET / HTTP/1.1
User-Agent: Chrome          -|
Host: localhost:1337        -|
Accept: */*                 -|--- 请求的 Headers

         响应的 status code
         |
HTTP/1.1 200 OK
Content-Type: text/html     -|
Content-Length: 14          -|--- 响应的 Headers

Hello Browser!              ----- 响应的 Message Body

应该就很清楚了。接下来我们不妨直接来看看当下最流行的 Web 后端开发技术。

"我的网站要用 nginx + node.js + mongodb!"

不管怎样,这里先分析一下这三样现在很有代表性的技术在后端中处于什么样的层面。

nginx

nginx 官网的描述是 "HTTP 和反向代理服务器",所以这两个部分可以分开来看。"HTTP 服务器" 的意思我觉得就是它可以接受 HTTP 请求,根据其内容作出不同的处理。一个非常重要的例子就是用来 "serve" 对静态内容(图片,css) 。无论你的网站用什么语言写,最终部署上线的时候静态内容都应当是由 nginx 这一层来处理,因为这是它的核心功能之一,效率和安全性都会很有保证。而 "反响代理服务器" 看起来很玄乎其实也很好理解。我们知道向一台机器上的 80 端口只能有一个进程在监听,那么比如你有两个应用,一个是你重要的网站程序,一个是记录你开发心路历程的博客,你可以用两个地址分别访问到:

明显的你不希望用户在访问的时候也要输入端口号码。如果把 nginx 放在最前面来监听 80 端口的话,它可以把一个请求根据你的配置来"反向代理"到你的网站或者博客。针对上面这个例子,我们可以这样配置:

server {
    listen 80;
    location /site {
        proxy_pass http://localhost:8080;
    }

    location /blog {
        proxy_pass http://localhost:9090;
    }
}

这样两个应用的现在分别可以用更好看的地址访问到:

当然现实生活中你的应用代码里可能也要根据这样的配置做一些处理。但是总体感觉就是这样。nginx 还有很多高端功能可以来控制反向代理的细节,可以很灵活的做各种事。所以预先的考虑把 nginx 放在最前面是很有帮助的。当你真的要用的时候可以到处搜搜会有很多教程。

node.js

node.js 可能是当下最火热的后端技术。你的网站逻辑就是要在这里来写了。比起之前其他的 web 技术 node.js 其实位置有点特殊,它给人的感觉应该像是 "可以用 JavaScript 控制逻辑的 nginx" 这样。上手相当容易,看看教程写个程序然后 node foo.js,打开浏览器就能看到了。

因为 node.js 功能是偏向底层的,所以当你真正要写网站的时候可能需要一个高层一点点的框架,可以自行搜索。

关于 node.js 还有一点需要提到的就是其 "async" 的意思。它这里的 "async" 仅仅是说的是 IO,即读文件写文件和一些先关的操作是异步的,跟多线程并行什么的是一点关系都没有。举一个例子:

http.createServer(function (req, res) {
    setTimeout(function(){
        res.writeHead(200, {'Content-Type': 'text/plain'});
        res.end('Hello World\n');
    }, 10000) // 返回前 "sleep" 10秒
}).listen(1337, '127.0.0.1');

打开浏览器访问发现要等 10 秒。在等待过程中你可以再开一个窗口也访问这个地址,你会发现在之前那个请求结束之前新的那一个也会在等待。如果这个行为跟你想象的不太一样的话,你可能需要看一看 js "Event-Based Programming" 是怎么回事。

mongodb

mongodb 是一个面向文档的数据库。跟传统 SQL 数据库比起来 mongodb 刚开始给人的感觉是更容易入门,但实际情况感觉也是有好有坏。你可以这样想,你网站的复杂度是固定的,那么其实不论用什么技术要解决的问题其实是差不多的,所以技术的选择在我们这种入门级选手来说其实不会是致命的。

不过说到为什么做网站一定要用个数据库,我觉得原因是因为网站不像很多普通的应用程序有一个明显的开始和结束的过程。像一个命令行程序你可以把大部分的程序状态都放在局部变量里面,有需要再序列化到文件中;而网站的话你需要有一个可靠而持久的地方来存放数据。另一方面像 mongo 和 SQL 都有可以让你很方便的对数据进行处理的操作,对于很多类型的应用来说也是很必要的。

不过反过来讲,网站程序中最好不要使用全局变量或者类似的东西。这里有一个例子,比如你想实现一个计数器,每次有人访问显示的数量就会增加。node.js 的话可以写成这样:

var http = require('http');
var counter = 0;
http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end(""+counter++);
}).listen(1337, '127.0.0.1');

这样其实是可以正常工作的,先不考虑说断电,程序异常退出之类的高端问题,这种做法还是有一个很要命的问题。你在开发的时候命令行里面跑跑发现没问题,但是后来又听说 node.js 支持 cluster 或者类似的东西, 可以起多份进程来做服务,这样可以 "scale" 你的网站。结果试了试发现计数器显示的数字变的很随机,有增有减,这时候就蛋疼了。

发生这个的原因就是这里使用了对于单个进程内的全局变量,也就是多个进程之间是无法共享的东西。当 cluster 起了多份进程的时候,等于是每个里面自己都有一个 counter 变量。

实际上这个问题对于 node.js 还算好 (node.js 的整个模型都倾向于单线程)。但像 Python 或者其他这种,依靠某种协议来起 "worker" 的方式这种做法就会迅速出问题。所以网站的数据都会放到一个统一的,可以由多个进程共同使用的地方,一般就是各种数据库。

选择你的武器

我一直觉得做网站(或者任何开发)最振奋人心的时刻就是最开始选择技术 "stack" 的时候了。"用这个这个加上这个,高性能够灵活上手简单,肯定很牛逼!",这种感觉估计每个人都有。不过似乎对于业余开发来说真的不是太重要,选择熟悉的或者感兴趣的都可以,因为说到底差别都不是那么大,自己感觉好就可以了。

迈向人生巅峰的步骤

当你本地调试好了的时候,就要考虑上线了。在浏览器里输入一个地址就能看到你精心设计的页面,感觉不是一般的好。如果你是第一次的话,你需要按步骤做以下这些事:

  1. 购买一个 VPS,可以搜索 "vps 海外 支付宝"。不要考虑那种共享的 "支持 php, ruby" 这种,你需要的是一台可以 ssh 进去有 root 权限的 VPS。
  2. 购买一个域名,可以搜索 "域名 海外 支付宝"。一定不要买国内的。
  3. 把你的域名迁移到 DNSPod。注册后看文档有很详细的教程。
  4. 配置 Google Analytics, 可以监测你网站的流量和用户情况。如果担心国内抽风的话可以搜 "网站流量统计" 寻找国内的代替品。
  5. 在 VPS 上装好 nginx 或者 apache 或者什么别的都可以,配置到浏览器里输入你的域名能刷出页面来。
  6. 部署你的网站!最好能自动化这个过程不然会很痛苦。

最后

就我个人做过的软件开发里面,似乎只有 Web 后端是入门就要牵涉到多种进程或服务的,开发部署和环境配置跟通常写个程序然后点"编译"都差别很大,总的来说应该还是蛮有意思的。

=

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 67,564评论 12 114
  • Node.js是目前非常火热的技术,但是它的诞生经历却很奇特。 众所周知,在Netscape设计出JavaScri...
    w_zhuan阅读 1,861评论 2 39
  • 前端开发面试题 <a name='preface'>前言</a> 只看问题点这里 看全部问题和答案点这里 本文由我...
    袁俊亮技术博客阅读 3,230评论 0 72
  • Node.js是目前非常火热的技术,但是它的诞生经历却很奇特。 众所周知,在Netscape设计出JavaScri...
    Myselfyan阅读 2,201评论 2 57
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 122,629评论 15 534