Web 客户端
所有向服务器开口要东西的一方。
Chrome 是 Web 客户端,一行 curl 也是。这一章里你会亲手写一份 HTTP 请求,把它包成 Shell 脚本,认真学会 curl,最后打开 Chrome 开发者工具,看着同样的文字在眼前飞过。
先开口的那一方就是客户端
客户端和服务器是角色,不是机器。先开口、要东西的一方是客户端;等着回答的一方是服务器。浏览器是客户端,curl 是客户端,手机应用是,去调 API 的另一台服务器也是。
所以 Web 客户端可以只有一行。它不需要窗口,不需要渲染引擎,也不需要鼠标 —— 它只要连上去、送出请求、读回内容。
| 问题 | 客户端 | 服务器 |
|---|---|---|
| 谁先开口 | 打开连接并送出请求。 | 守在端口上,没人连上来就一直不出声。 |
| 谁需要地址 | 需要知道服务器在哪 —— 域名或 IP,再加一个端口。 | 需要一个固定地址,这正是服务器有域名而客户端通常没有的原因。 |
| 一次几条 | 通常一次一条对话,不过浏览器为了把页面加载得快些会同时开好几条。 | 同时很多条,真正的服务器软件之所以复杂,大半原因就在这里。 |
一台去取地图或去扣款的 Web 服务器,在那次请求里扮演的是客户端。角色随时在换,所以该问的是这一次请求往哪个方向走,而不是两端各摆着什么机器。
在终端里做一个 Web 客户端
要最快地相信 HTTP 只是文字,就自己动手打一份请求出来。先把上一主题里的 Shell 服务器跑起来,再开第二个终端,亲手跟它说话。
用 nc 手写一份请求
netcat 连上端口,把你敲的字原样送上线路。打完两行、按两次回车,服务器就会回答 —— 你刚刚亲手做了浏览器的活。
# connect, then type the two lines below and press Enter once more
nc localhost 8084
GET / HTTP/1.1
Host: localhost
# or send it all at once (printf's \r\n is HTTP's line break)
printf 'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n' | nc localhost 8084结尾那个空行很重要。它告诉服务器请求到此为止,所以你要多按一次回车。 HTTP 要求每行以回车加换行结束。手打也能通,是因为多数服务器比较宽容;而 printf 那一版送出的正是标准所要求的样子。 Host 那一行是 HTTP/1.1 强制要求的。对着真实站点少写它,通常换回来的是一个错误,而不是页面。
把它包成脚本
既然请求已经只是一句 printf,一个可以反复用的客户端也就是几行的事。下面这份接收主机、端口和路径,送出请求,并把头部和正文分开显示。
#!/bin/bash
# usage: bash client.sh localhost 8084 /
HOST=${1:-localhost}
PORT=${2:-8084}
PATH_=${3:-/}
printf 'GET %s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n' "$PATH_" "$HOST" \
| nc "$HOST" "$PORT" \
| tr -d '\r' \
| awk 'BODY { print; next } /^$/ { BODY = 1; next } { print "[header] " $0 }'tr 去掉 HTTP 留在每行末尾的回车,awk 则在遇到空行之前把每行都标成头部 —— 空行之后的一切都是正文。这跟浏览器开始绘制之前所做的切分完全一样。
一个终端跑 Shell 服务器,另一个跑这个客户端,你就同时握有一场自己写出来的对话的两端。改一改服务器的响应,客户端立刻就看得见 —— 因为中间再没有别的东西。
curl:你每天都会用到的客户端
我们那个脚本粗糙地做到的事,curl 做得规规矩矩:重定向、HTTPS、头部、上传、超时、Cookie。几乎每台机器上都有它,而它正是绕开浏览器直接验证服务器的标准手段。
# body only
curl http://localhost:8084
# body plus the response headers
curl -i http://localhost:8084
# the whole exchange, request headers included
curl -v http://localhost:8084真正要紧的选项
| 选项 | 作用 |
|---|---|
| -i | 连响应头部一起显示,而不只是正文。页面和预期不符时,第一个该加上的就是它。 |
| -v | 详细模式:把送出的请求和收到的响应逐行显示,连建立连接的过程也在内。 |
| -L | 跟随重定向。不加它,遇到 301 时你拿到的是一份几乎空白的响应,而不是想要的页面。 |
| -o FILE | 把响应存成文件而不是打印出来。这就是把 curl 当下载工具用。 |
| -H "Name: value" | 添加一个请求头部 —— 内容类型、鉴权令牌、语言偏好等等。 |
| -X POST -d "data" | 不只是问,还要送数据。带上数据即意味着 POST,这正是在终端里测试表单或 API 的方式。 |
| -s | 安静模式:不显示进度条。凡是要把输出接给别的命令或脚本,就都加上它。 |
| -A "name" | 假装成某个浏览器。有些站点会因为来问的是谁而给出不同的回答。 |
调用 API
如今应用打交道的多半不是页面,而是返回 JSON 的 API。curl 负责取回来,jq 负责整理得能读 —— 这两样合起来,就是在写下第一行应用代码之前先验通接口的办法。
# fetch JSON and pick one field out of it with jq
curl -s https://api.github.com/repos/sveltejs/svelte | jq '.stargazers_count'
# send JSON with POST
curl -X POST https://httpbin.org/post \
-H 'Content-Type: application/json' \
-d '{"name":"PES","level":1}'
# download to a file
curl -L -o page.html https://getpes.com养成先用 curl 试的习惯。curl 拿到正确答案,说明服务器没问题、错在你的程序;curl 拿到的答案也不对,那你就省下了半天往错的方向找的工夫。
Chrome 也是同一种客户端,只是规模不同
Chrome 送出的,正是你用 nc 送出的那份请求。之后它才做了那件工作量惊人的事:把回来的答案变成人能看的画面 —— 真正的区别只在这个「之后」。
这些步骤值得记住,因为每一步都是页面可能出岔子的地方。有东西没显示出来时,该问的永远是:是哪一步失败了。
从地址栏到画面
- 解析名字 DNS 把域名换成 IP 地址。这一步失败,你根本没碰到服务器,报错说的也是名字而不是页面。
- 连接并加密 它打开连接;如果是 https,在送出任何东西之前先核对证书。这里出现的警告,意思是加密不可信,而不是页面不存在。
- 送出请求 和你手打的那几行一样,只是头部多得多:Cookie、可接受的格式、偏好的语言。
- 解析并取回其余部分 它读 HTML,按你的标签搭出一棵元素树,然后把页面引用到的东西统统要过来 —— 样式表、图片、字体、脚本,通常还是同时要好几样。
- 上样式、排版、绘制 它套用 CSS,算出每个盒子的位置,把结果画出来,再运行那些随后又能把一切改掉的 JavaScript。
终端客户端,还是浏览器
Chrome、Edge、Brave 和 Opera 都建在 Chromium 之上,所以在一个里能用的页面,通常在其他几个里也能用。Safari 和 Firefox 用的是别的引擎 —— 这正是你在说「做完了」之前,至少该在两种引擎里各打开一次的原因。
开发者工具:亲眼看着这些文字来回
Chrome 自带了世上最好的 Web 调试工具,而且已经装好了。你先前用 nc 一封封手送的东西,在这里被逐条列出来,旁边还配着响应。
按 F12;macOS 上是 Command-Option-I,其他系统是 Control-Shift-I。在页面任意位置右键选「检查」,打开时那个元素已经选中了 —— 这是最快的入口。
各个面板
现在就能试的三件事
- 找到你自己的那条请求 打开 Network 面板,刷新这个页面,点开最上面那一条。里面的请求和响应头部,就是你先前敲进 nc 的那些行。
- 复制成 curl 右键任意一条请求,选 Copy as cURL,粘到终端里 —— 你就把浏览器刚做的事一模一样重现了一遍,连头部和 Cookie 都在。
- 故意弄坏它 在 Elements 面板里删掉一个标题,看着它消失;在 Network 里把速度调慢再刷新。两件都很安全,也都比读文字学到得多。
打开第一个主题里写的那份 HTML 文件,检查一下,你会认出树里的每一个标签。写出来、送出去、要回来、再看清楚 —— 这一圈就是整个 Web 开发的缩影。
这一栏由你自己标记,这里不打分。