Web 客户端
所有向服务器开口要东西的一方。
浏览器是 Web 客户端,一行 curl 也是。先用 nc 亲手写一个 HTTP 请求,再在 curl 和 Chrome 开发者工具里看同样的文字来回传递。
先开口的那一方就是客户端
和服务器是角色,不是机器。先开口、要东西的一方是客户端;等着回答的一方是服务器。浏览器是客户端,curl 是客户端,手机应用是,去调 的另一台服务器也是。
所以 Web 客户端可以只有一行。它不需要窗口,不需要,也不需要鼠标 —— 它只要连上去、送出、读回内容。
| 问题 | 客户端 | 服务器 |
|---|---|---|
| 谁先开口 | 打开连接并送出请求。 | 守在上,没人连上来就一直不出声。 |
| 谁需要地址 | 需要知道服务器在哪 —— 或 ,再加一个端口。 | 需要一个固定地址,这正是服务器有域名而客户端通常没有的原因。 |
| 一次几条 | 通常一次一条对话,不过浏览器为了把页面加载得快些会同时开好几条。 | 同时很多条,真正的服务器之所以复杂,大半原因就在这里。 |
一台去取地图或去扣款的 Web 服务器,在那次请求里扮演的是客户端。角色随时在换,所以该问的是这一次请求往哪个方向走,而不是两端各摆着什么机器。
在终端里做一个 Web 客户端
要最快地相信 只是文字,就自己动手打一份出来。先把上一主题里的 服务器跑起来,再开第二个,亲手跟它说话。
用 nc 手写一份请求
连上,把你敲的字原样送上线路。打完两行、按两次回车,服务器就会回答 —— 你刚刚亲手做了浏览器的活。
# 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;
# sleep keeps the line open a moment so the shell server can answer)
{ printf 'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n'; sleep 1; } | nc localhost 8084
- 很重要。它告诉服务器请求到此为止,所以你要多按一次回车。
- HTTP 要求每行以结束。手打也能通,是因为多数服务器比较宽容;而 那一版送出的正是标准所要求的样子。
- 是 强制要求的。对着真实站点少写它,通常换回来的是一个错误,而不是页面。
把它包成脚本
既然请求已经只是一句 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"; sleep 1; } \
| nc "$HOST" "$PORT" \
| tr -d '\r' \
| awk 'BODY { print; next } /^$/ { BODY = 1; next } { print "[header] " $0 }'
去掉 HTTP 留在每行末尾的回车, 则在遇到空行之前把每行都标成头部 —— 空行之后的一切都是正文。这跟浏览器开始绘制之前所做的切分完全一样。
一个终端跑 Shell 服务器,另一个跑这个客户端,你就同时握有一场自己写出来的对话的两端。发给 Shell 服务器时,要像上面的那样用 让连接多开一会儿,才收得到回答。改一改服务器的,客户端立刻就看得见 —— 因为中间再没有别的东西。
curl:你每天都会用到的客户端
我们那个脚本粗糙地做到的事,curl 做得规规矩矩:、、、上传、超时、。几乎每台机器上都有它,而它正是绕开浏览器直接验证服务器的标准手段。
# 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 | 跟随重定向。不加它,遇到 时你拿到的是一份几乎空白的响应,而不是想要的页面。 |
-o FILE | 把响应存成文件而不是打印出来。这就是把 curl 当下载工具用。 |
-H "Name: value" | 添加一个请求头部 —— 内容类型、鉴权、语言偏好等等。 |
-d "data" | 不只是问,还要送。只加 -d 就成了 (别的方法用 -X 来选),这正是在里测试或 的方式。 |
-s | 安静模式:不显示进度条。凡是要把输出接给别的命令或脚本,就都加上它。 |
-A "name" | 假装成某个浏览器。有些站点会因为来问的是谁而给出不同的回答。 |
调用 API
如今应用打交道的多半不是页面,而是返回 的 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":"SONUB","level":1}'
# download to a file
curl -L -o page.html https://sonub.com
养成先用 curl 试的习惯。curl 拿到正确答案,说明服务器没问题,错在你的。curl 拿到的答案也不对,问题就在服务器那边。这样你也不会浪费半天往错的地方找。
Chrome 也是同一种客户端,只是规模不同
送出的,正是你用 送出的那份。之后它才做了那件工作量惊人的事:把回来的答案变成人能看的画面 —— 真正的区别只在这个「之后」。
这些步骤值得记住,因为每一步都是页面可能出岔子的地方。有东西没显示出来时,该问的永远是:是哪一步失败了。
从地址栏到画面
- 解析名字 把换成 。这一步失败,你根本没碰到服务器,报错说的也是名字而不是页面。
- 它打开连接;如果是 ,在送出任何东西之前先。这里出现的警告,意思是加密不可信,而不是页面不存在。
- 送出请求和你手打的那几行一样,只是多得多:、可接受的格式、偏好的语言。
- 解析并取回其余部分它读 ,按你的搭出一棵。然后把页面引用到的东西统统要过来。样式表、图片、字体、脚本,通常同时要好几样。
- 上样式、、绘制它套用 ,算出每个盒子的位置并画出来。 在 HTML 还没读完时就会运行,画完之后也还能再改动页面。
终端客户端,还是浏览器
Chrome、、 和 都建在 之上,所以在一个里能用的页面,通常在其他几个里也能用。 和 用的是别的引擎 —— 这正是你在说「做完了」之前,至少该在两种引擎里各打开一次的原因。
开发者工具:亲眼看着这些文字来回
自带了 Web 工具,而且已经装好了。你先前用 一封封手送的东西,在这里被逐条列出来,旁边还配着。
按 ; 上是 Command-Option-I,其他系统是 Control-Shift-I。在页面任意位置右键选「检查」,打开时那个已经选中了 —— 这是最快的入口。
各个面板
现在就能试的三件事
- 找到你自己的那条请求打开 ,刷新这个页面,点开最上面那一条。里面的请求和部,内容和你先前敲进 nc 的那些行一样。在用 连接的站点上,第一行和 会显示为 、:path、:authority 这几个名字。
- 复制成 curl右键任意一条请求,选 Copy as cURL,粘到里 —— 你就把浏览器刚做的事一模一样了一遍,连头部和 Cookie 都在。
- 故意弄坏它在 里删掉一个标题,看着它消失;在 里把速度调慢再刷新。两件都很安全,也都比读文字学到得多。
打开第一个主题里写的那份 HTML 文件,检查一下,你会认出树里的每一个标签。写出来、送出去、要回来、再看清楚 —— 这一圈就是整个 Web 开发的缩影。
这一栏由你自己标记,这里不打分。
要把这一阶段标记为已理解吗?
目前还不能取消,请在真正理解后再标记。
已标记为理解
这一栏由你自己标记,这里不打分。