Windows 11 将默认采用 ReFS 文件系统微软正准备改进 Windows 11 的安全功能，他们用 Rust 重新编码内核，并使用弹性文件系统（ReFS）而不是 NTFS 作为默认文件系统。让 Windows 在内核中使用 Rust 启动这件事我们之前已经 报道 过了。ReFS 发布于 2012 年，但之前只支持 Windows Server 等企业级操作系统。它在很多方面都优于 1993 年发布的 NTFS，包括自动完整性检查和数据清理、避免需要运行 chkdsk、内置硬盘驱动器故障和冗余的处理、集成 RAID 功能等等。它将在未来几个月提供给 Windows 11 用户。消息来源：Windows Latest老王点评：感觉 Windows 真正开始变的更好了。欧洲核子研究中心庆祝将 Web 释放到公共领域 30 周年1990 年，欧洲核子研究中心的研究员蒂姆·伯纳斯-李开始着手“使用 NeXTStep 开发环境开发一个超文本 GUI 浏览器+编辑器。他创造了 ‘WorldWideWeb’ 作为该程序的名称”。到 1993 年 1 月，世界上有大约 50 个 HTTP 服务器。接下来的一个月，第一个图形浏览器 Mosaic 出现了。1993 年 4 月 30 日，欧洲核子研究中心签署了一项决定，即万维网 —— 在其屋顶下创建的客户端、服务器和代码库 —— 属于全人类（该信于 5 月 3 日正式盖章）。消息来源：The Register老王点评：正是这些科学家的无私，才有了今天的网络世界。其意义影响深远。纽约警局敦促市民购买 AirTag 以应对汽车盗窃案之前，我们报道过，由于 CAN 总线的安全缺陷，通过简单的设备很容易就可以盗窃汽车，尤其是现代和奇亚汽车。纽约市警察局召开新闻发布会称，今年到目前为止，纽约市有 966 辆现代汽车和奇亚汽车被盗，已经超过了 2022 全年总数。他们号召市民为他们的车辆配备苹果 AirTag，“参与”打击汽车盗窃。配备 AirTag 的被盗车辆的用户将看到警方使用“无人机、StarChase 技术和良好的老式警察工作来安全地找回你的被盗汽车。”他们在 Twitter 上说，“助人自助，买个标签”。消息来源：Ars Technica老王点评：苹果推出 AirTag 时恐怕没想过它居然成了随车设备。
大家好！几天前我写了篇 小型的个人程序 的文章，里面提到了调用没有文档说明的“秘密” API 很有意思，你需要从你的浏览器中把 cookies 复制出来才能访问。有些读者问如何实现，因此我打算详细描述下，其实过程很简单。我们还会谈谈在调用没有文档说明的 API 时，可能会遇到的错误和道德问题。我们用谷歌 Hangouts 举例。我之所以选择它，并不是因为这个例子最有用（我认为官方的 API 更实用），而是因为在这个场景中更有用的网站很多是小网站，而小网站的 API 一旦被滥用，受到的伤害会更大。因此我们使用谷歌 Hangouts，因为我 100% 肯定谷歌论坛可以抵御这种试探行为。我们现在开始！第一步：打开开发者工具，找一个 JSON 响应我浏览了 https://hangouts.google.com，在Firefox的开发者工具中打开“ruby网络rtNetwork/rt/ruby”标签，找到一个 JSON 响应。你也可以使用 Chrome 的开发者工具。打开之后界面如下图：找到其中一条 “ 类型 Type ” 列显示为 json 的请求。为了找一条感兴趣的请求，我找了好一会儿，突然我找到一条 “people” 的端点，看起来是返回我们的联系人信息。听起来很有意思，我们来看一下。第二步：复制为 cURL下一步，我在感兴趣的请求上右键，点击 “复制Copy” -> “ 复制为 cURL Copy as cURL ”。然后我把 curl 命令粘贴到终端并运行。下面是运行结果：$ curl 'https://people-pa.clients6.google.com/v2/people/?key=REDACTED' -X POST ........ （省略了大量请求标头） Warning: Binary output can mess up your terminal. Use "--output -" to tell Warning: curl to output it to your terminal anyway, or consider "--output Warning: <FILE>" to save to a file. 你可能会想 —— 很奇怪，“二进制的输出在你的终端上无法正常显示” 是什么错误？原因是，浏览器默认情况下发给服务器的请求头中有 Accept-Encoding: gzip, deflate 参数，会把输出结果进行压缩。我们可以通过管道把输出传递给 gunzip 来解压，但是我们发现不带这个参数进行请求会更简单。因此我们去掉一些不相关的请求头。第三步：去掉不相关的请求头下面是我从浏览器获得的完整 curl 命令。有很多行！我用反斜杠（\）把请求分开，这样每个请求头占一行，看起来更清晰：curl 'https://people-pa.clients6.google.com/v2/people/?key=REDACTED' \ -X POST \ -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:96.0) Gecko/20100101 Firefox/96.0' \ -H 'Accept: */*' \ -H 'Accept-Language: en' \ -H 'Accept-Encoding: gzip, deflate' \ -H 'X-HTTP-Method-Override: GET' \ -H 'Authorization: SAPISIDHASH REDACTED' \ -H 'Cookie: REDACTED' -H 'Content-Type: application/x-www-form-urlencoded' \ -H 'X-Goog-AuthUser: 0' \ -H 'Origin: https://hangouts.google.com' \ -H 'Connection: keep-alive' \ -H 'Referer: https://hangouts.google.com/' \ -H 'Sec-Fetch-Dest: empty' \ -H 'Sec-Fetch-Mode: cors' \ -H 'Sec-Fetch-Site: same-site' \ -H 'Sec-GPC: 1' \ -H 'DNT: 1' \ -H 'Pragma: no-cache' \ -H 'Cache-Control: no-cache' \ -H 'TE: trailers' \ --data-raw 'personId=101777723309&personId=1175339043204&personId=1115266537043&personId=116731406166&extensionSet.extensionNames=HANGOUTS_ADDITIONAL_DATA&extensionSet.extensionNames=HANGOUTS_OFF_NETWORK_GAIA_GET&extensionSet.extensionNames=HANGOUTS_PHONE_DATA&includedProfileStates=ADMIN_BLOCKED&includedProfileStates=DELETED&includedProfileStates=PRIVATE_PROFILE&mergedPersonSourceOptions.includeAffinity=CHAT_AUTOCOMPLETE&coreIdParams.useRealtimeNotificationExpandedAcls=true&requestMask.includeField.paths=person.email&requestMask.includeField.paths=person.gender&requestMask.includeField.paths=person.in_app_reachability&requestMask.includeField.paths=person.metadata&requestMask.includeField.paths=person.name&requestMask.includeField.paths=person.phone&requestMask.includeField.paths=person.photo&requestMask.includeField.paths=person.read_only_profile_info&requestMask.includeField.paths=person.organization&requestMask.includeField.paths=person.location&requestMask.includeField.paths=person.cover_photo&requestMask.includeContainer=PROFILE&requestMask.includeContainer=DOMAIN_PROFILE&requestMask.includeContainer=CONTACT&key=REDACTED' 第一眼看起来内容有很多，但是现在你不需要考虑每一行是什么意思。你只需要把不相关的行删掉就可以了。我通常通过删掉某行查看是否有错误来验证该行是不是可以删除 —— 只要请求没有错误就一直删请求头。通常情况下，你可以删掉 Accept*、Referer、Sec-*、DNT、User-Agent 和缓存相关的头。在这个例子中，我把请求删成下面的样子：curl 'https://people-pa.clients6.google.com/v2/people/?key=REDACTED' \ -X POST \ -H 'Authorization: SAPISIDHASH REDACTED' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -H 'Origin: https://hangouts.google.com' \ -H 'Cookie: REDACTED'\ --data-raw 'personId=101777723309&personId=1175339043204&personId=1115266537043&personId=116731406166&extensionSet.extensionNames=HANGOUTS_ADDITIONAL_DATA&extensionSet.extensionNames=HANGOUTS_OFF_NETWORK_GAIA_GET&extensionSet.extensionNames=HANGOUTS_PHONE_DATA&includedProfileStates=ADMIN_BLOCKED&includedProfileStates=DELETED&includedProfileStates=PRIVATE_PROFILE&mergedPersonSourceOptions.includeAffinity=CHAT_AUTOCOMPLETE&coreIdParams.useRealtimeNotificationExpandedAcls=true&requestMask.includeField.paths=person.email&requestMask.includeField.paths=person.gender&requestMask.includeField.paths=person.in_app_reachability&requestMask.includeField.paths=person.metadata&requestMask.includeField.paths=person.name&requestMask.includeField.paths=person.phone&requestMask.includeField.paths=person.photo&requestMask.includeField.paths=person.read_only_profile_info&requestMask.includeField.paths=person.organization&requestMask.includeField.paths=person.location&requestMask.includeField.paths=person.cover_photo&requestMask.includeContainer=PROFILE&requestMask.includeContainer=DOMAIN_PROFILE&requestMask.includeContainer=CONTACT&key=REDACTED' 这样我只需要 4 个请求头：Authorization、Content-Type、Origin 和 Cookie。这样容易管理得多。第四步：在 Python 中发请求现在我们知道了我们需要哪些请求头，我们可以把 curl 命令翻译进 Python 程序！这部分是相当机械化的过程，目标仅仅是用 Python 发送与 cUrl 相同的数据。下面是代码实例。我们使用 Python 的 requests 包实现了与前面 curl 命令相同的功能。我把整个长请求分解成了元组的数组，以便看起来更简洁。import requests import urllib data = [ ('personId','101777723'), # I redacted these IDs a bit too ('personId','117533904'), ('personId','111526653'), ('personId','116731406'), ('extensionSet.extensionNames','HANGOUTS_ADDITIONAL_DATA'), ('extensionSet.extensionNames','HANGOUTS_OFF_NETWORK_GAIA_GET'), ('extensionSet.extensionNames','HANGOUTS_PHONE_DATA'), ('includedProfileStates','ADMIN_BLOCKED'), ('includedProfileStates','DELETED'), ('includedProfileStates','PRIVATE_PROFILE'), ('mergedPersonSourceOptions.includeAffinity','CHAT_AUTOCOMPLETE'), ('coreIdParams.useRealtimeNotificationExpandedAcls','true'), ('requestMask.includeField.paths','person.email'), ('requestMask.includeField.paths','person.gender'), ('requestMask.includeField.paths','person.in_app_reachability'), ('requestMask.includeField.paths','person.metadata'), ('requestMask.includeField.paths','person.name'), ('requestMask.includeField.paths','person.phone'), ('requestMask.includeField.paths','person.photo'), ('requestMask.includeField.paths','person.read_only_profile_info'), ('requestMask.includeField.paths','person.organization'), ('requestMask.includeField.paths','person.location'), ('requestMask.includeField.paths','person.cover_photo'), ('requestMask.includeContainer','PROFILE'), ('requestMask.includeContainer','DOMAIN_PROFILE'), ('requestMask.includeContainer','CONTACT'), ('key','REDACTED') ] response = requests.post('https://people-pa.clients6.google.com/v2/people/?key=REDACTED', headers={ 'X-HTTP-Method-Override': 'GET', 'Authorization': 'SAPISIDHASH REDACTED', 'Content-Type': 'application/x-www-form-urlencoded', 'Origin': 'https://hangouts.google.com', 'Cookie': 'REDACTED', }, data=urllib.parse.urlencode(data), ) print(response.text) 我执行这个程序后正常运行 —— 输出了一堆 JSON 数据！太棒了！你会注意到有些地方我用 REDACTED 代替了，因为如果我把原始数据列出来你就可以用我的账号来访问谷歌论坛了，这就很不好了。运行结束！现在我可以随意修改 Python 程序，比如传入不同的参数，或解析结果等。我不打算用它来做其他有意思的事了，因为我压根对这个 API 没兴趣，我只是用它来阐述请求 API 的过程。但是你确实可以对返回的一堆 JSON 做一些处理。curlconverter 看起来很强大有人评论说可以使用 https://curlconverter.com/ 自动把 curl 转换成 Python（和一些其他的语言！），这看起来很神奇 —— 我都是手动转的。我在这个例子里使用了它，看起来一切正常。追踪 API 的处理过程并不容易我不打算夸大追踪 API 处理过程的难度 —— API 的处理过程并不明显！我也不知道传给这个谷歌论坛 API 的一堆参数都是做什么的！但是有一些参数看起来很直观，比如 requestMask.includeField.paths=person.email 可能表示“包含每个人的邮件地址”。因此我只关心我能看懂的参数，不关心看不懂的。（理论上）适用于所有场景可能有人质疑 —— 这个方法适用于所有场景吗？答案是肯定的 —— 浏览器不是魔法！浏览器发送给你的服务器的所有信息都是 HTTP 请求。因此如果我复制了浏览器发送的所有的 HTTP 请求头，那么后端就会认为请求是从我的浏览器发出的，而不是用 Python 程序发出的。当然，我们去掉了一些浏览器发送的请求头，因此理论上后端是可以识别出来请求是从浏览器还是 Python 程序发出的，但是它们通常不会检查。这里有一些对读者的告诫 —— 一些谷歌服务的后端会通过令人难以理解（对我来说是）方式跟前端通信，因此即使理论上你可以模拟前端的请求，但实际上可能行不通。可能会遭受更多攻击的大型 API 会有更多的保护措施。我们已经知道了如何调用没有文档说明的 API。现在我们再来聊聊可能遇到的问题。问题 1：会话 cookie 过期一个大问题是我用我的谷歌会话 cookie 作为身份认证，因此当我的浏览器会话过期后，这个脚本就不能用了。这意味着这种方式不能长久使用（我宁愿调一个真正的 API），但是如果我只是要一次性快速抓取一小组数据，那么可以使用它。问题 2：滥用如果我正在请求一个小网站，那么我的 Python 脚本可能会把服务打垮，因为请求数超出了它们的处理能力。因此我请求时尽量谨慎，尽量不过快地发送大量请求。这尤其重要，因为没有官方 API 的网站往往是些小网站且没有足够的资源。很明显在这个例子中这不是问题 —— 我认为在写这篇文章的过程我一共向谷歌论坛的后端发送了 20 次请求，他们肯定可以处理。如果你用自己的账号身份过度访问这个 API 并导致了故障，那么你的账号可能会被暂时封禁（情理之中）。我只下载我自己的数据或公共的数据 —— 我的目的不是寻找网站的弱点。请记住所有人都可以访问你没有文档说明的 API我认为本文最重要的信息并不是如何使用其他人没有文档说明的 API。虽然很有趣，但是也有一些限制，而且我也不会经常这么做。更重要的一点是，任何人都可以这么访问你后端的 API！每个人都有开发者工具和网络标签，查看你传到后端的参数、修改它们都很容易。因此如果一个人通过修改某些参数来获取其他用户的信息，这不值得提倡。我认为提供公开 API 的大部分开发者们都知道，但是我之所以再提一次，是因为每个初学者都应该了解。: )via: https://jvns.ca/blog/2022/03/10/how-to-use-undocumented-web-apis/作者：Julia Evans 选题：lujun9972 译者：lxbwolf 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
互联网诞生之时我的个人故事。最近，我和大家 分享了 我在 1994 年获得英国文学和神学学位离开大学后，如何在一个人们还不知道 Web 服务器是什么的世界里成功找到一份运维 Web 服务器的工作。我说的“世界”，并不仅仅指的是我工作的机构，而是泛指所有地方。Web 那时当真是全新的 —— 人们还正尝试理出头绪。那并不是说我工作的地方（一家学术出版社）特别“懂” Web。这是个大部分人还在用 28.8K 猫（调制解调器，俗称“猫”）访问网页的世界。我还记得我拿到 33.6K 猫时有多激动。至少上下行速率不对称的日子已经过去了， [1] 以前 1200/300 的带宽描述特别常见。这意味着（在同一家机构的）印刷人员制作的设计复杂、色彩缤纷、纤毫毕现的文档是完全不可能放在 Web 上的。我不能允许在网站的首页出现大于 40k 的 GIF 图片，这对我们的许多访问者来说是很难接受的。大于大约 60k 图片的会作为独立的图片，以缩略图链接过去。如果说市场部只有这一点不喜欢，那是绝对是轻描淡写了。更糟的是布局问题。“浏览器决定如何布局文档，”我一遍又一遍地解释，“你可以使用标题或者段落，但是文档在页面上如何呈现并不取决于文档，而是取决于渲染器！”他们想控制这些，想要不同颜色的背景。后来明白了那些不能实现。我觉得我就像是参加了第一次讨论层叠样式表（CSS）的 W3C 会议，并进行了激烈地争论。关于文档编写者应控制布局的建议真令人厌恶。 [2] CSS 花了一些时间才被人们采用，与此同时，关心这些问题的人搭上了 PDF 这种到处都是安全问题的列车。如何呈现文档不是唯一的问题。作为一个实体书出版社，对于市场部来说，拥有一个网站的全部意义在于，让客户（或者说潜在的客户）不仅知道一本书的内容，而且知道买这本书需要花多少钱。但这有一个问题，你看，互联网，包括快速发展的万维网，是开放的，是所有都免费的自由之地，没有人会在意钱；事实上，在那里谈钱是要回避和避免的。我和主流“网民”的看法一致，认为没必要把价格信息放在线上。我老板，以及机构里相当多的人都持相反的意见。他们觉得消费者应该能够看到书要花多少钱。他们也觉得我的银行经理也会想看到我的账户里每个月进了多少钱，如果我不认同他们的观点的话，那我的收入就可能堪忧。幸运的是，在我被炒鱿鱼之前，我已经自己认清了一些 —— 可能是在我开始迈入 Web 的几星期之后，Web 已经发生变化，有其他人公布他们的产品价格信息。这些新来者通常被那些从早期就开始运行 Web 服务器的老派人士所看不起， [3] 但很明显，风向是往那边吹的。然而，这并不意味着我们的网站就赢得了战争。作为一个学术出版社，我们和大学共享一个域名（在 “ac.uk” 下）。大学不太相信发布价格信息是合适的，直到出版社的一些资深人士指出，普林斯顿大学出版社正在这样做，如果我们不做……看起来是不是有点傻？有趣的事情还没完。在我担任站点管理员（“webmaster@…”）的短短几个月后，我们和其他很多网站一样开始看到了一种令人担忧的趋势。某些访问者可以轻而易举地让我们的 Web 服务器跪了。这些访问者使用了新的网页浏览器：网景浏览器（Netscape）。网景浏览器实在太恶劣了，它居然是多线程的。这为什么是个问题呢？在网景浏览器之前，所有的浏览器都是单线程。它们一次只进行一个连接，所以即使一个页面有五张 GIF 图， [4] 也会先请求 HTML 基本文件进行解析，然后下载第一张 GIF，完成，接着第二张，完成，如此类推。事实上，GIF 的顺序经常出错，使得页面加载得非常奇怪，但这也是常规思路。而粗暴的网景公司的人决定，它们可以同时打开多个连接到 Web 服务器，比如说，可以同时请求所有的 GIF！为什么这是个问题呢？好吧，问题就在于大多数 Web 服务器都是单线程的。它们不是设计来一次进行多个连接的。确实，我们运行的 HTTP 服务的软件（MacHTTP）是单线程的。尽管我们花钱购买了它（最初是共享软件），但我们用的这版无法同时处理多个请求。互联网上爆发了大量讨论。这些网景公司的人以为他们是谁，能改变世界的运作方式？它应该如何工作？大家分成了不同阵营，就像所有的技术争论一样，双方都用各种技术热词互丢。问题是，网景浏览器不仅是多线程的，它也比其他的浏览器更好。非常多 Web 服务器代码维护者，包括 MacHTTP 作者 Chuck Shotton 在内，开始坐下来认真地在原有代码基础上更新了多线程测试版。几乎所有人立马转向测试版，它们变得稳定了，最终，浏览器要么采用了这种技术，变成多线程，要么就像所有过时产品一样销声匿迹了。 [5] 对我来说，这才是 Web 真正成长起来的时候。既不是网页展示的价格，也不是设计者能定义你能在网页上看到什么， [6] 而是浏览器变得更易用，以及成千上万的浏览者向数百万浏览者转变的网络效应，使天平向消费者而不是生产者倾斜。在我的旅程中，还有更多故事，我将留待下次再谈。但从这时起，我的雇主开始看我们的月报，然后是周报、日报，并意识到这将是一件大事，真的需要关注。它们又是怎么回来的？ ↩︎你可能不会惊讶，我还是在命令行里最开心。 ↩︎大约六个月前。 ↩︎莽撞，没错，但它确实发生了 [7] ↩︎没有真正的沉寂：总有一些坚持他们的首选解决方案具有技术上的优势，并哀叹互联网的其他人都是邪恶的死硬爱好者。 [8] ↩︎我会指出，为那些有各种无障碍需求的人制造严重而持续的问题。 ↩︎噢，不，是 GIF 或 BMP，JPEG 还是个好主意，但还没有用上。 ↩︎我不是唯一一个说“我还在用 Lynx”的人。 ↩︎via: https://opensource.com/article/19/3/when-web-grew作者：Mike Bursell 选题：lujun9972 译者：XYenChi 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
我最近深入研究了网站归档，因为有些朋友担心遇到糟糕的系统管理或恶意删除时失去对放在网上的内容的控制权。这使得网站归档成为系统管理员工具箱中的重要工具。事实证明，有些网站比其他网站更难归档。本文介绍了对传统网站进行归档的过程，并阐述在面对最新流行单页面应用程序（SPA）的现代网站时，它有哪些不足。转换为简单网站手动编码 HTML 网站的日子早已不复存在。现在的网站是动态的，并使用最新的 JavaScript、PHP 或 Python 框架即时构建。结果，这些网站更加脆弱：数据库崩溃、升级出错或者未修复的漏洞都可能使数据丢失。在我以前是一名 Web 开发人员时，我不得不接受客户这样的想法：希望网站基本上可以永久工作。这种期望与 web 开发“快速行动和破除陈规”的理念不相符。在这方面，使用 Drupal 内容管理系统（CMS）尤其具有挑战性，因为重大更新会破坏与第三方模块的兼容性，这意味着客户很少承担的起高昂的升级成本。解决方案是将这些网站归档：以实时动态的网站为基础，将其转换为任何 web 服务器可以永久服务的纯 HTML 文件。此过程对你自己的动态网站非常有用，也适用于你想保护但无法控制的第三方网站。对于简单的静态网站，古老的 Wget 程序就可以胜任。然而镜像保存一个完整网站的命令却是错综复杂的：$ nice wget --mirror --execute robots=off --no-verbose --convert-links \ --backup-converted --page-requisites --adjust-extension \ --base=./ --directory-prefix=./ --span-hosts \ --domains=www.example.com,example.com http://www.example.com/以上命令下载了网页的内容，也抓取了指定域名中的所有内容。在对你喜欢的网站执行此操作之前，请考虑此类抓取可能对网站产生的影响。上面的命令故意忽略了 robots.txt 规则，就像现在归档者的习惯做法，并以尽可能快的速度归档网站。大多数抓取工具都可以选择在两次抓取间暂停并限制带宽使用，以避免使网站瘫痪。上面的命令还将获取 “页面所需（LCTT 译注：单页面所需的所有元素）”，如样式表（CSS）、图像和脚本等。下载的页面内容将会被修改，以便链接也指向本地副本。任何 web 服务器均可托管生成的文件集，从而生成原始网站的静态副本。以上所述是事情一切顺利的时候。任何使用过计算机的人都知道事情的进展很少如计划那样；各种各样的事情可以使程序以有趣的方式脱离正轨。比如，在网站上有一段时间很流行日历块。内容管理系统会动态生成这些内容，这会使爬虫程序陷入死循环以尝试检索所有页面。灵巧的归档者可以使用正则表达式（例如 Wget 有一个 --reject-regex 选项）来忽略有问题的资源。如果可以访问网站的管理界面，另一个方法是禁用日历、登录表单、评论表单和其他动态区域。一旦网站变成静态的，（那些动态区域）也肯定会停止工作，因此从原始网站中移除这些杂乱的东西也不是全无意义。JavaScript 噩梦很不幸，有些网站不仅仅是纯 HTML 文件构建的。比如，在单页面网站中，web 浏览器通过执行一个小的 JavaScript 程序来构建内容。像 Wget 这样的简单用户代理将难以重建这些网站的有意义的静态副本，因为它根本不支持 JavaScript。理论上，网站应该使用渐进增强技术，在不使用 JavaScript 的情况下提供内容和实现功能，但这些指引很少被人遵循 —— 使用过 NoScript 或 uMatrix 等插件的人都知道。传统的归档方法有时会以最愚蠢的方式失败。在尝试为一个本地报纸网站（pamplemousse.ca）创建备份时，我发现 WordPress 在包含 的 JavaScript 末尾添加了查询字符串（例如：?ver=1.12.4）。这会使提供归档服务的 web 服务器不能正确进行内容类型检测，因为其靠文件扩展名来发送正确的 Content-Type 头部信息。在 web 浏览器加载此类归档时，这些脚本会加载失败，导致动态网站受损。随着 web 向使用浏览器作为执行任意代码的虚拟机转化，依赖于纯 HTML 文件解析的归档方法也需要随之适应。这个问题的解决方案是在抓取时记录（以及重现）服务器提供的 HTTP 头部信息，实际上专业的归档者就使用这种方法。创建和显示 WARC 文件在 互联网档案馆 Internet Archive 网站，Brewster Kahle 和 Mike Burner 在 1996 年设计了 ARC （即 “ARChive”）文件格式，以提供一种聚合其归档工作所产生的百万个小文件的方法。该格式最终标准化为 WARC（“Web ARChive”）规范，并在 2009 年作为 ISO 标准发布，2017 年修订。标准化工作由 国际互联网保护联盟 International Internet Preservation Consortium （IIPC）领导，据维基百科称，这是一个“为了协调为未来而保护互联网内容的努力而成立的国际图书馆组织和其他组织”；它的成员包括 美国国会图书馆 US Library of Congress 和互联网档案馆等。后者在其基于 Java 的 Heritrix crawler（LCTT 译注：一种爬虫程序）内部使用了 WARC 格式。WARC 在单个压缩文件中聚合了多种资源，像 HTTP 头部信息、文件内容，以及其他元数据。方便的是，Wget 实际上提供了 --warc 参数来支持 WARC 格式。不幸的是，web 浏览器不能直接显示 WARC 文件，所以为了访问归档文件，一个查看器或某些格式转换是很有必要的。我所发现的最简单的查看器是 pywb，它以 Python 包的形式运行一个简单的 web 服务器提供一个像“ 时光倒流机网站 Wayback Machine ”的界面，来浏览 WARC 文件的内容。执行以下命令将会在 http://localhost:8080/ 地址显示 WARC 文件的内容：$ pip install pywb $ wb-manager init example $ wb-manager add example crawl.warc.gz $ wayback顺便说一句，这个工具是由 Webrecorder 服务提供者建立的，Webrecoder 服务可以使用 web 浏览器保存动态页面的内容。很不幸，pywb 无法加载 Wget 生成的 WARC 文件，因为它遵循的 1.0 规范不一致，1.1 规范修复了此问题。就算 Wget 或 pywb 修复了这些问题，Wget 生成的 WARC 文件对我的使用来说不够可靠，所以我找了其他的替代品。引起我注意的爬虫程序简称 crawl。以下是它的调用方式：$ crawl https://example.com/（它的 README 文件说“非常简单”。）该程序支持一些命令行参数选项，但大多数默认值都是最佳的：它会从其他域获取页面所需（除非使用 -exclude-related 参数），但肯定不会递归出域。默认情况下，它会与远程站点建立十个并发连接，这个值可以使用 -c 参数更改。但是，最重要的是，生成的 WARC 文件可以使用 pywb 完美加载。未来的工作和替代方案这里还有更多有关使用 WARC 文件的资源。特别要提的是，这里有一个专门用来归档网站的 Wget 的直接替代品，叫做 Wpull。它实验性地支持了 PhantomJS 和 youtube-dl 的集成，即允许分别下载更复杂的 JavaScript 页面以及流媒体。该程序是一个叫做 ArchiveBot 的复杂归档工具的基础，ArchiveBot 被那些在 ArchiveTeam 的“零散离群的归档者、程序员、作家以及演说家”使用，他们致力于“在历史永远丢失之前保存它们”。集成 PhantomJS 好像并没有如团队期望的那样良好工作，所以 ArchiveTeam 也用其它零散的工具来镜像保存更复杂的网站。例如，snscrape 将抓取一个社交媒体配置文件以生成要发送到 ArchiveBot 的页面列表。该团队使用的另一个工具是 crocoite，它使用无头模式的 Chrome 浏览器来归档 JavaScript 较多的网站。如果没有提到称做“网站复制者”的 HTTrack 项目，那么这篇文章算不上完整。它工作方式和 Wget 相似，HTTrack 可以对远程站点创建一个本地的副本，但是不幸的是它不支持输出 WRAC 文件。对于不熟悉命令行的小白用户来说，它在人机交互方面显得更有价值。同样，在我的研究中，我发现了叫做 Wget2 的 Wget 的完全重制版本，它支持多线程操作，这可能使它比前身更快。和 Wget 相比，它舍弃了一些功能，但是最值得注意的是拒绝模式、WARC 输出以及 FTP 支持，并增加了 RSS、DNS 缓存以及改进的 TLS 支持。最后，我个人对这些工具的愿景是将它们与我现有的书签系统集成起来。目前我在 Wallabag 中保留了一些有趣的链接，这是一种自托管式的“稍后阅读”服务，意在成为 Pocket（现在由 Mozilla 拥有）的免费替代品。但是 Wallabag 在设计上只保留了文章的“可读”副本，而不是一个完整的拷贝。在某些情况下，“可读版本”实际上不可读，并且 Wallabag 有时无法解析文章。恰恰相反，像 bookmark-archiver 或 reminiscence 这样其他的工具会保存页面的屏幕截图以及完整的 HTML 文件，但遗憾的是，它没有 WRAC 文件所以没有办法更可信的重现网页内容。我所经历的有关镜像保存和归档的悲剧就是死数据。幸运的是，业余的归档者可以利用工具将有趣的内容保存到网上。对于那些不想麻烦的人来说，“互联网档案馆”看起来仍然在那里，并且 ArchiveTeam 显然正在为互联网档案馆本身做备份。via: https://anarc.at/blog/2018-10-04-archiving-web-sites/作者：Anarcat 选题：lujun9972 译者：fuowang 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
Web 服务器总是忙忙碌碌的，从不下班，这似乎比运维工程师还要辛苦。每一个线程都在忙着，然而也有不太一样的，比如那个被数据库操作拖在那里的，就只能发呆；而那个被糟糕的代码搞得堆栈溢出的，看起来已经要崩溃了。处理完请求之后，Web 服务器会给出生成的页面和 Cookie（饼干），如果下次带着这些饼干的编号来，那就可以很快地找到你要的饼干——这就是用饼干保存的会话。这就是 Tomcat Web 服务器里面的生活。via: http://turnoff.us/geek/life-in-a-web-server/作者：Daniel Stori 译者：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
个人 Web 服务器即 “云”，只不过是你拥有和控制它，而不是一个大型公司。拥有一个自己的云有很多好处，包括可定制、免费存储、免费的互联网服务、通往开源软件之路、高安全性、完全控制您的内容、快速更改的能力、实验代码的地方等等。 这些好处大部分是无法估量的，但在财务上，这些好处可以为您每个月节省超过 100 美元。我本可以选择 AWS ，但我更喜欢完全自由且安全性可控，并且我可以学一下这些东西是如何搭建的。私有 Web 托管：而非 BlueHost 或 DreamHost云存储：而非 Dropbox、Box、Google Drive、Microsoft Azure、iCloud 或是 AWS自主部署安全HTTPS：Let’s Encrypt分析： GoogleOpenVPN：不需要专有互联网连接（预计每个月花费 $7）我所使用的物品清单：树莓派 3 代 Model BMicroSD 卡（推荐使用 32 GB， 兼容树莓派的 SD 卡）USB microSD 卡读卡器以太网络线连接上 Wi-Fi 的路由器树莓派盒子亚马逊倍思的 MicroUSB 数据线苹果的充电器USB 鼠标USB 键盘HDMI 线材显示器 (支持接入 HDMI)MacBook Pro步骤 1： 启动树莓派下载最新发布的 Raspbian (树莓派的操作系统)。 Raspbian Jessie 的 ZIP 包就可以用 [脚注 1]。解压缩或提取下载的文件然后把它拷贝到 SD 卡里。使用 Pi Filler 可以让这些过程变得更简单。下载 Pi Filer 1.3 或最新的版本。解压或提取下载文件之后打开它，你应该会看到这样的提示：确保 USB 读卡器这时还没有插上。如果已经插上了那就先弹出。点 “Continue” 继续下一步。你会看到一个让你选择文件的界面，选择你之前解压缩后的树莓派系统文件。然后你会看到另一个提示，如图所示：把 MicroSD 卡（推荐 32 GB ，至少 16GB）插入到 USB MicroSD 卡读卡器里。然后把 USB 读卡器接入到你的电脑里。你可以把你的 SD 卡重命名为 “Raspberry” 以区别其他设备。然后点击 “Continue”。请先确保你的 SD 卡是空的，因为 Pi Filler 会在运行时 擦除 所有事先存在 SD 卡里的内容。如果你要备份卡里的内容，那你最好就马上备份。当你点 “Continue” 的时候，Raspbian OS 就会被写入到 SD 卡里。这个过程大概会花费一到三分钟左右。当写入完成后，推出 USB 读卡器，把 SD 卡拔出来插入到树莓派的 SD 卡槽里。把电源线接上，给树莓派供电。这时树莓派就会自己启动。树莓派的默认登录账户信息是：用户名： pi密码：raspberry当树莓派首次启动完成时，会跳出一个标题为 “ 设置选项 Setup Options ” 的配置界面，就像下面的图片一样 [脚注 2]：选择 “ 扩展文件系统 Expand Filesystem ” 这一选项并回车 [脚注 3]。 同时，我还推荐选择第二个选项 “ 修改密码 Change User Password ”。这对保证安全性来说尤为重要。它还能个性化你的树莓派。在选项列表中选择第三项 “ 启用引导到桌面 Enable Boot To Desktop/Scratch ” 并回车。这时会跳到另一个标题为 “ 选择引导选项 Choose boot option ” 的界面，就像下面这张图这样：在这个界面选择第二个选项 “ 以用户‘pi’登录图形化桌面 Desktop log in as user 'pi' at the graphical desktop ” 并回车 [脚注 4]。完成这个操作之后会回到之前的 “ 设置选项 Setup Options ” 界面。如果没有回到之前的界面的话就选择当前界面底部的 “OK” 按钮并回车。当这些操作都完成之后，选择当前界面底部的 “Finish” 按钮并回车，这时它就会自动重启。如果没有自动重启的话，就在终端里使用如下命令来重启。$ sudo reboot 接上一步的重启，如果所有步骤都顺利进行的话，你会进入到类似下面这样桌面环境中。当你进入了桌面之后，在终端中执行如下命令来更新树莓派的固件。$ sudo apt-get update $ sudo apt-get upgrade-y $ sudo apt-get dist-upgrade -y $ sudo rpi-update 这些操作可能会花费几分钟时间。完成之后，现在运行着的树莓派就是最新的了。步骤 2： 配置树莓派SSH 指的是 Secure Shell，是一种加密网络协议，可让你在计算机和树莓派之间安全地传输数据。 你可以从 Mac 的命令行控制你的树莓派，而无需显示器或键盘。要使用 SSH，首先需要你的树莓派的 IP 地址。 打开终端并输入：$ sudo ifconfig 如果你在使用以太网，看 eth0 部分。如果你在使用 Wi-Fi, 看 wlan0 部分。查找 inet addr，后跟一个 IP 地址，如 192.168.1.115，这是本篇文章中使用的默认 IP。有了这个地址，在终端中输入 :$ ssh pi@192.168.1.115 对于 PC 上的 SSH，请参见 [脚注 5]。出现提示时输入默认密码 raspberry，除非你之前更改过密码。现在你已经通过 SSH 登录成功。远程桌面使用 GUI（图形用户界面）有时比命令行更容易。 在树莓派的命令行（使用 SSH）上键入：$ sudo apt-get install xrdp xrdp 支持 Mac 和 PC 的 Microsoft Remote Desktop 客户端。在 Mac 上，在 App store 中搜索 “Microsoft Remote Desktop”。 下载它。 （对于 PC，请参见 [脚注 6]。）安装完成之后，在你的 Mac 中搜索一个叫 “Microsoft Remote Desktop” 的应用并打开它，你会看到 :点击 “New” 新建一个远程连接，在空白处填写如下配置。关闭 “New” 窗口就会自动保存。你现在应该看到 “My Desktop” 下列出的远程连接。 双击它。简单加载后，你应该在屏幕上的窗口中看到你的树莓派桌面，如下所示：好了，现在你不需要额外的鼠标、键盘或显示器就能控制你的树莓派。这是一个更为轻量级的配置。静态化本地 IP 地址有时候你的本地 IP 地址 192.168.1.115 会发生改变。我们需要让这个 IP 地址静态化。输入:$ sudo ifconfig 从 eth0 部分或 wlan0 部分，记下 inet addr（树莓派当前 IP），bcast（广播 IP 范围）和 mask（子网掩码地址）。 然后输入：$ netstat -nr 记下 destination 和 gateway/network。大概应该是这样子的:net address 192.168.1.115 bcast 192.168.1.255 mask 255.255.255.0 gateway 192.168.1.1 network 192.168.1.1 destination 192.168.1.0 有了这些信息，你可以很简单地设置一个静态 IP。输入:$ sudo nano /etc/dhcpcd.conf 不要去动 /etc/network/interfaces。剩下要做的就是把这些内容追加到这个文件的底部，把 IP 换成你想要的 IP 地址。interface eth0 static ip_address=192.168.1.115 static routers=192.168.1.1 static domain_name_servers=192.168.1.1 一旦你设置了静态内部 IP 地址，这时需要通过如下命令重启你的树莓派 :$ sudo reboot 重启完成之后，在终端中输入 :$ sudo ifconfig 这时你就可以看到你的树莓派上的新的静态配置了。静态化全局 IP 地址如果您的 ISP（互联网服务提供商）已经给您一个静态外部 IP 地址，您可以跳到端口转发部分。 如果没有，请继续阅读。你已经设置了 SSH、远程桌面和静态内部 IP 地址，因此现在本地网络中的计算机将会知道在哪里可以找到你的树莓派。 但是你仍然无法在本地 Wi-Fi 网络外部访问你的树莓派。 你需要树莓派可以从互联网上的任何地方公开访问。这需要静态的外部 IP 地址 [脚注 7]。联系您的 ISP 并请求静态的外部（有时称为静态全局）IP 地址可能会是一个非常敏感的过程。 ISP 拥有决策权，所以我会非常小心处理。 他们可能拒绝你的的静态外部 IP 地址请求。 如果他们拒绝了你的请求，你不要怪罪于他们，因为这种类型的请求有法律和操作风险。 他们特别不希望客户运行中型或大型互联网服务。 他们可能会明确地询问为什么需要一个静态的外部 IP 地址。 最好说实话，告诉他们你打算主办一个低流量的个人网站或类似的小型非营利互联网服务。 如果一切顺利，他们应该会建立一个工单，并在一两个星期内给你打电话。端口转发这个新获得的 ISP 分配的静态全局 IP 地址是用于访问路由器。 树莓派现在仍然无法访问。 你需要设置端口转发才能访问树莓派。端口是信息在互联网上传播的虚拟途径。 你有时需要转发端口，以使计算机像树莓派一样可以访问 Internet，因为它位于网络路由器后面。 VollmilchTV 专栏在 YouTube 上的一个视频，名字是什么是 TCP/IP，端口，路由，Intranet，防火墙，互联网，可以帮助你更好地了解端口。端口转发可用于像 树莓派 Web 服务器或 VoIP 或点对点下载的应用程序。 有 65000个以上的端口可供选择，因此你可以为你构建的每个 Internet 应用程序分配一个不同的端口。设置端口转发的方式取决于你的路由器。 如果你有 Linksys 的话，Gabriel Ramirez 在 YouTbue 上有一个标题叫 如何让你的 Apache Ubuntu 服务器连到互联网 的视频解释了如何设置。 如果您没有 Linksys，请阅读路由器附带的文档，以便自定义和定义要转发的端口。你需要转发 SSH 以及远程桌面端口。如果你认为你已经过配置端口转发了，输入下面的命令以查看它是否正在通过 SSH 工作：$ ssh pi@your_global_ip_address 它应该会提示你输入密码。检查端口转发是否也适用于远程桌面。 打开 Microsoft Remote Desktop。 你之前的的远程连接设置应该已经保存了，但需要使用静态的外部 IP 地址（例如 195.198.227.116）来更新 “PC 名称” 字段，而不是静态的内部地址（例如 192.168.1.115）。现在，尝试通过远程桌面连接。 它应该简单地加载并显示树莓派的桌面。好了， 树莓派现在可以从互联网上访问了，并且已经准备好进行高级项目了。作为一个奖励选项，您可以保持到您的 Pi 的两个远程连接。 一个通过互联网，另一个通过 LAN（局域网）。很容易设置。在 Microsoft Remote Desktop 中，保留一个称为 “Pi Internet” 的远程连接，另一个称为 “Pi Local”。 将 Pi Internet 的 “PC 名称” 配置为静态外部 IP 地址，例如 195.198.227.116。 将 Pi Local 的 “PC 名称” 配置为静态内部 IP 地址，例如 192.168.1.115。 现在，您可以选择在全局或本地连接。如果你还没有看过由 Gabriel Ramirez 发布的 如何让你的 Apache Ubuntu 服务器连到互联网，那么你可以去看一下，作为过渡到第二个项目的教程。 它将向您展示项目背后的技术架构。 在我们的例子中，你使用的是树莓派而不是 Ubuntu 服务器。 动态 DNS 位于域名公司和您的路由器之间，这是 Ramirez 省略的部分。 除了这个微妙之处外，视频是在整体上解释系统的工作原理。 您可能会注意到本教程涵盖了树莓派设置和端口转发，这是服务器端或后端。 查看原始来源，涵盖域名，动态 DNS，Jekyll（静态 HTML 生成器）和 Apache（网络托管）的更高级项目，这是客户端或前端。脚注[1] 我不建议从 NOOBS 操作系统开始。 我更喜欢从功能齐全的 Raspbian Jessie 操作系统开始。[2] 如果没有弹出 “Setup Options”，可以通过打开终端并执行该命令来始终找到它：$ sudo-rasps-config [3] 我们这样做是为了将 SD 卡上存在的所有空间用作一个完整的分区。 所有这一切都是扩大操作系统以适应 SD 卡上的整个空间，然后可以将其用作树莓派的存储内存。[4] 我们这样做是因为我们想启动进入熟悉的桌面环境。 如果我们不做这个步骤，树莓派每次会进入到终端而不是 GUI 中。[5]下载并运行 PuTTY 或 Windows 的其它 SSH 客户端。 在该字段中输入你的 IP 地址，如上图所示。 将默认端口保留为 22。 回车，PuTTY 将打开一个终端窗口，提示你输入用户名和密码。 填写然后开始在树莓派上进行你的远程工作。[6] 如果尚未安装，请下载 Microsoft Remote Desktop。 搜索您的计算机上的的 Microsoft Remote Desktop。 运行。 提示时输入 IP 地址。 接下来，会弹出一个 xrdp 窗口，提示你输入用户名和密码。[7] 路由器具有动态分配的外部 IP 地址，所以在理论上，它可以从互联网上暂时访问，但是您需要 ISP 的帮助才能使其永久访问。 如果不是这样，你需要在每次使用时重新配置远程连接。原文出自 Mitchell McLaughlin's Full-Stack Computer Projects。作者简介：Mitchell McLaughlin - 我是一名开放网络的贡献者和开发者。我感兴趣的领域很广泛，但我特别喜欢开源软件/硬件，比特币和编程。 我住在旧金山，我有过一些简短的 GoPro 和 Oracle 工作经验。via: https://opensource.com/article/17/3/building-personal-web-server-raspberry-pi-3作者：Mitchell McLaughlin 译者：chenxinlong 校对：jasminepeng本文由 LCTT 原创编译，Linux中国 荣誉推出
“只有在创造中才能够学到更多。” ——皮亚杰在本系列的第二部分中，你创造了一个可以处理基本 HTTP GET 请求的、朴素的 WSGI 服务器。当时我问了一个问题：“你该如何让你的服务器在同一时间处理多个请求呢？”在这篇文章中，你会找到答案。系好安全带，我们要认真起来，全速前进了！你将会体验到一段非常快速的旅程。准备好你的 Linux、Mac OS X（或者其他 *nix 系统），还有你的 Python。本文中所有源代码均可在 GitHub 上找到。服务器的基本结构及如何处理请求首先，我们来回顾一下 Web 服务器的基本结构，以及服务器处理来自客户端的请求时，所需的必要步骤。你在第一部分及第二部分中创建的轮询服务器只能够一次处理一个请求。在处理完当前请求之前，它不能够接受新的客户端连接。所有请求为了等待服务都需要排队，在服务繁忙时，这个队伍可能会排的很长，一些客户端可能会感到不开心。这是轮询服务器 webserver3a.py 的代码：##################################################################### # 轮询服务器 - webserver3a.py # # # # 使用 Python 2.7.9 或 3.4 # # 在 Ubuntu 14.04 及 Mac OS X 环境下测试通过 # ##################################################################### import socket SERVER_ADDRESS = (HOST, PORT) = '', 8888 REQUEST_QUEUE_SIZE = 5 def handle_request(client_connection): request = client_connection.recv(1024) print(request.decode()) http_response = b"""\ HTTP/1.1 200 OK Hello, World! """ client_connection.sendall(http_response) def serve_forever(): listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) listen_socket.bind(SERVER_ADDRESS) listen_socket.listen(REQUEST_QUEUE_SIZE) print('Serving HTTP on port {port} ...'.format(port=PORT)) while True: client_connection, client_address = listen_socket.accept() handle_request(client_connection) client_connection.close() if __name__ == '__main__': serve_forever() 为了观察到你的服务器在同一时间只能处理一个请求的行为，我们对服务器的代码做一点点修改：在将响应发送至客户端之后，将程序阻塞 60 秒。这个修改只需要一行代码，来告诉服务器进程暂停 60 秒钟。这是我们更改后的代码，包含暂停语句的服务器 webserver3b.py：###################################################################### # 轮询服务器 - webserver3b.py # # # # 使用 Python 2.7.9 或 3.4 # # 在 Ubuntu 14.04 及 Mac OS X 环境下测试通过 # # # # - 服务器向客户端发送响应之后，会阻塞 60 秒 # ###################################################################### import socket import time SERVER_ADDRESS = (HOST, PORT) = '', 8888 REQUEST_QUEUE_SIZE = 5 def handle_request(client_connection): request = client_connection.recv(1024) print(request.decode()) http_response = b"""\ HTTP/1.1 200 OK Hello, World! """ client_connection.sendall(http_response) time.sleep(60) ### 睡眠语句，阻塞该进程 60 秒 def serve_forever(): listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) listen_socket.bind(SERVER_ADDRESS) listen_socket.listen(REQUEST_QUEUE_SIZE) print('Serving HTTP on port {port} ...'.format(port=PORT)) while True: client_connection, client_address = listen_socket.accept() handle_request(client_connection) client_connection.close() if __name__ == '__main__': serve_forever() 用以下命令启动服务器：$ python webserver3b.py 现在，打开一个新的命令行窗口，然后运行 curl 语句。你应该可以立刻看到屏幕上显示的字符串“Hello, World!”：$ curl http://localhost:8888/hello Hello, World! 然后，立刻打开第二个命令行窗口，运行相同的 curl 命令：$ curl http://localhost:8888/hello 如果你在 60 秒之内完成了以上步骤，你会看到第二条 curl 指令不会立刻产生任何输出，而只是挂在了哪里。同样，服务器也不会在标准输出流中输出新的请求内容。这是这个过程在我的 Mac 电脑上的运行结果（在右下角用黄色框标注出来的窗口中，我们能看到第二个 curl 指令被挂起，正在等待连接被服务器接受）：当你等待足够长的时间（60 秒以上）后，你会看到第一个 curl 程序完成，而第二个 curl 在屏幕上输出了“Hello, World!”，然后休眠 60 秒，进而终止。这样运行的原因是因为在服务器在处理完第一个来自 curl 的请求之后，只有等待 60 秒才能开始处理第二个请求。这个处理请求的过程按顺序进行（也可以说，迭代进行），一步一步进行，在我们刚刚给出的例子中，在同一时间内只能处理一个请求。现在，我们来简单讨论一下客户端与服务器的交流过程。为了让两个程序在网络中互相交流，它们必须使用套接字。你应当在本系列的前两部分中见过它几次了。但是，套接字是什么？ 套接字 （ socket ） 是一个通讯通道 端点 （ endpoint ） 的抽象描述，它可以让你的程序通过文件描述符来与其它程序进行交流。在这篇文章中，我只会单独讨论 Linux 或 Mac OS X 中的 TCP/IP 套接字。这里有一个重点概念需要你去理解：TCP 套接字对 （ socket pair ） 。TCP 连接使用的套接字对是一个由 4 个元素组成的元组，它确定了 TCP 连接的两端：本地 IP 地址、本地端口、远端 IP 地址及远端端口。一个套接字对唯一地确定了网络中的每一个 TCP 连接。在连接一端的两个值：一个 IP 地址和一个端口，通常被称作一个套接字。（引自《UNIX 网络编程 卷1：套接字联网 API （第3版）》）所以，元组 {10.10.10.2:49152, 12.12.12.3:8888} 就是一个能够在客户端确定 TCP 连接两端的套接字对，而元组 {12.12.12.3:8888, 10.10.10.2:49152} 则是在服务端确定 TCP 连接两端的套接字对。在这个例子中，确定 TCP 服务端的两个值（IP 地址 12.12.12.3 及端口 8888），代表一个套接字；另外两个值则代表客户端的套接字。一个服务器创建一个套接字并开始建立连接的基本工作流程如下：服务器创建一个 TCP/IP 套接字。我们可以用这条 Python 语句来创建：listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 服务器可能会设定一些套接字选项（这个步骤是可选的，但是你可以看到上面的服务器代码做了设定，这样才能够在重启服务器时多次复用同一地址）：listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 然后，服务器绑定一个地址。绑定函数 bind 可以将一个本地协议地址赋给套接字。若使用 TCP 协议，调用绑定函数 bind 时，需要指定一个端口号，一个 IP 地址，或两者兼有，或两者全无。（引自《UNIX网络编程 卷1：套接字联网 API （第3版）》）listen_socket.bind(SERVER_ADDRESS) 然后，服务器开启套接字的监听模式。listen_socket.listen(REQUEST_QUEUE_SIZE) 监听函数 listen 只应在服务端调用。它会通知操作系统内核，表明它会接受所有向该套接字发送的入站连接请求。以上四步完成后，服务器将循环接收来自客户端的连接，一次循环处理一条。当有连接可用时，接受请求函数 accept 将会返回一个已连接的客户端套接字。然后，服务器从这个已连接的客户端套接字中读取请求数据，将数据在其标准输出流中输出出来，并向客户端回送一条消息。然后，服务器会关闭这个客户端连接，并准备接收一个新的客户端连接。这是客户端使用 TCP/IP 协议与服务器通信的必要步骤：下面是一段示例代码，使用这段代码，客户端可以连接你的服务器，发送一个请求，并输出响应内容：import socket ### 创建一个套接字，并连接值服务器 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(('localhost', 8888)) ### 发送一段数据，并接收响应数据 sock.sendall(b'test') data = sock.recv(1024) print(data.decode()) 在创建套接字后，客户端需要连接至服务器。我们可以调用连接函数 connect 来完成这个操作：sock.connect(('localhost', 8888)) 客户端只需提供待连接的远程服务器的 IP 地址（或主机名），及端口号，即可连接至远端服务器。你可能已经注意到了，客户端不需要调用 bind 及 accept 函数，就可以与服务器建立连接。客户端不需要调用 bind 函数是因为客户端不需要关注本地 IP 地址及端口号。操作系统内核中的 TCP/IP 协议栈会在客户端调用 connect 函数时，自动为套接字分配本地 IP 地址及本地端口号。这个本地端口被称为 临时端口 （ ephemeral port ） ，即一个短暂开放的端口。服务器中有一些端口被用于承载一些众所周知的服务，它们被称作 通用 （ well-known ） 端口：如 80 端口用于 HTTP 服务，22 端口用于 SSH 服务。打开你的 Python shell，与你在本地运行的服务器建立一个连接，来看看内核给你的客户端套接字分配了哪个临时端口（在尝试这个例子之前，你需要运行服务器程序 webserver3a.py 或 webserver3b.py）：>>> import socket >>> sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) >>> sock.connect(('localhost', 8888)) >>> host, port = sock.getsockname()[:2] >>> host, port ('127.0.0.1', 60589) 在上面的例子中，内核将临时端口 60589 分配给了你的套接字。在我开始回答我在第二部分中提出的问题之前，我还需要快速讲解一些概念。你很快就会明白这些概念为什么非常重要。这两个概念，一个是进程，另外一个是文件描述符。什么是进程？进程就是一个程序执行的实体。举个例子：当你的服务器代码被执行时，它会被载入内存，而内存中表现此次程序运行的实体就叫做进程。内核记录了进程的一系列有关信息——比如进程 ID——来追踪它的运行情况。当你在执行轮询服务器 webserver3a.py 或 webserver3b.py 时，你其实只是启动了一个进程。我们在终端窗口中运行 webserver3b.py：$ python webserver3b.py 在另一个终端窗口中，我们可以使用 ps 命令获取该进程的相关信息：$ ps | grep webserver3b | grep -v grep 7182 ttys003 0:00.04 python webserver3b.py ps 命令显示，我们刚刚只运行了一个 Python 进程 webserver3b.py。当一个进程被创建时，内核会为其分配一个进程 ID，也就是 PID。在 UNIX 中，所有用户进程都有一个父进程；当然，这个父进程也有进程 ID，叫做父进程 ID，缩写为 PPID。假设你默认使用 BASH shell，那当你启动服务器时，就会启动一个新的进程，同时被赋予一个 PID，而它的父进程 PID 会被设为 BASH shell 的 PID。自己尝试一下，看看这一切都是如何工作的。重新开启你的 Python shell，它会创建一个新进程，然后在其中使用系统调用 os.getpid() 及 os.getppid() 来获取 Python shell 进程的 PID 及其父进程 PID（也就是你的 BASH shell 的 PID）。然后，在另一个终端窗口中运行 ps 命令，然后用 grep 来查找 PPID（父进程 ID，在我的例子中是 3148）。在下面的屏幕截图中，你可以看到一个我的 Mac OS X 系统中关于进程父子关系的例子，在这个例子中，子进程是我的 Python shell 进程，而父进程是 BASH shell 进程：另外一个需要了解的概念，就是文件描述符。什么是文件描述符？文件描述符是一个非负整数，当进程打开一个现有文件、创建新文件或创建一个新的套接字时，内核会将这个数返回给进程。你以前可能听说过，在 UNIX 中，一切皆是文件。内核会按文件描述符来找到一个进程所打开的文件。当你需要读取文件或向文件写入时，我们同样通过文件描述符来定位这个文件。Python 提供了高层次的操作文件（或套接字）的对象，所以你不需要直接通过文件描述符来定位文件。但是，在高层对象之下，我们就是用它来在 UNIX 中定位文件及套接字，通过这个整数的文件描述符。一般情况下，UNIX shell 会将一个进程的标准输入流（STDIN）的文件描述符设为 0，标准输出流（STDOUT）设为 1，而标准错误打印（STDERR）的文件描述符会被设为 2。我之前提到过，即使 Python 提供了高层次的文件对象或类文件对象来供你操作，你仍然可以在对象上使用 fileno() 方法，来获取与该文件相关联的文件描述符。回到 Python shell 中，我们来看看你该怎么做到这一点：>>> import sys >>> sys.stdin <open file '<stdin>', mode 'r' at 0x102beb0c0> >>> sys.stdin.fileno() 0 >>> sys.stdout.fileno() 1 >>> sys.stderr.fileno() 2 当你在 Python 中操作文件及套接字时，你可能会使用高层次的文件/套接字对象，但是你仍然有可能会直接使用文件描述符。下面有一个例子，来演示如何用文件描述符做参数来进行一次写入的系统调用：>>> import sys >>> import os >>> res = os.write(sys.stdout.fileno(), 'hello\n') hello 下面是比较有趣的部分——不过你可能不会为此感到惊讶，因为你已经知道在 Unix 中，一切皆为文件——你的套接字对象同样有一个相关联的文件描述符。和刚才操纵文件时一样，当你在 Python 中创建一个套接字时，你会得到一个对象而不是一个非负整数，但你永远可以用我之前提到过的 fileno() 方法获取套接字对象的文件描述符，并可以通过这个文件描述符来直接操纵套接字。>>> import socket >>> sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) >>> sock.fileno() 3 我还想再提一件事：不知道你有没有注意到，在我们的第二个轮询服务器 webserver3b.py 中，当你的服务器休眠 60 秒的过程中，你仍然可以通过第二个 curl 命令连接至服务器。当然 curl 命令并没有立刻输出任何内容而是挂在哪里，但是既然服务器没有接受连接，那它为什么不立即拒绝掉连接，而让它还能够继续与服务器建立连接呢？这个问题的答案是：当我在调用套接字对象的 listen 方法时，我为该方法提供了一个 BACKLOG 参数，在代码中用 REQUEST_QUEUE_SIZE 常量来表示。BACKLOG 参数决定了在内核中为存放即将到来的连接请求所创建的队列的大小。当服务器 webserver3b.py 在睡眠的时候，你运行的第二个 curl 命令依然能够连接至服务器，因为内核中用来存放即将接收的连接请求的队列依然拥有足够大的可用空间。尽管增大 BACKLOG 参数并不能神奇地使你的服务器同时处理多个请求，但当你的服务器很繁忙时，将它设置为一个较大的值还是相当重要的。这样，在你的服务器调用 accept 方法时，不需要再等待一个新的连接建立，而可以立刻直接抓取队列中的第一个客户端连接，并不加停顿地立刻处理它。欧耶！现在你已经了解了一大块内容。我们来快速回顾一下我们刚刚讲解的知识（当然，如果这些对你来说都是基础知识的话，那我们就当复习好啦）。轮询服务器服务端套接字创建流程（创建套接字，绑定，监听及接受）客户端连接创建流程（创建套接字，连接）套接字对套接字临时端口及通用端口进程进程 ID（PID），父进程 ID（PPID），以及进程父子关系文件描述符套接字的 listen 方法中，BACKLOG 参数的含义如何并发处理多个请求现在，我可以开始回答第二部分中的那个问题了：“你该如何让你的服务器在同一时间处理多个请求呢？”或者换一种说法：“如何编写一个并发服务器？”在 UNIX 系统中编写一个并发服务器最简单的方法，就是使用系统调用 fork()。下面是全新出炉的并发服务器 webserver3c.py 的代码，它可以同时处理多个请求（和我们之前的例子 webserver3b.py 一样，每个子进程都会休眠 60 秒）：####################################################### # 并发服务器 - webserver3c.py # # # # 使用 Python 2.7.9 或 3.4 # # 在 Ubuntu 14.04 及 Mac OS X 环境下测试通过 # # # # - 完成客户端请求处理之后，子进程会休眠 60 秒 # # - 父子进程会关闭重复的描述符 # # # ####################################################### import os import socket import time SERVER_ADDRESS = (HOST, PORT) = '', 8888 REQUEST_QUEUE_SIZE = 5 def handle_request(client_connection): request = client_connection.recv(1024) print( 'Child PID: {pid}. Parent PID {ppid}'.format( pid=os.getpid(), ppid=os.getppid(), ) ) print(request.decode()) http_response = b"""\ HTTP/1.1 200 OK Hello, World! """ client_connection.sendall(http_response) time.sleep(60) def serve_forever(): listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) listen_socket.bind(SERVER_ADDRESS) listen_socket.listen(REQUEST_QUEUE_SIZE) print('Serving HTTP on port {port} ...'.format(port=PORT)) print('Parent PID (PPID): {pid}\n'.format(pid=os.getpid())) while True: client_connection, client_address = listen_socket.accept() pid = os.fork() if pid == 0: ### 子进程 listen_socket.close() ### 关闭子进程中复制的套接字对象 handle_request(client_connection) client_connection.close() os._exit(0) ### 子进程在这里退出 else: ### 父进程 client_connection.close() ### 关闭父进程中的客户端连接对象，并循环执行 if __name__ == '__main__': serve_forever() 在深入研究代码、讨论 fork 如何工作之前，先尝试运行它，自己看一看这个服务器是否真的可以同时处理多个客户端请求，而不是像轮询服务器 webserver3a.py 和 webserver3b.py 一样。在命令行中使用如下命令启动服务器：$ python webserver3c.py 然后，像我们之前测试轮询服务器那样，运行两个 curl 命令，来看看这次的效果。现在你可以看到，即使子进程在处理客户端请求后会休眠 60 秒，但它并不会影响其它客户端连接，因为他们都是由完全独立的进程来处理的。你应该看到你的 curl 命令立即输出了“Hello, World!”然后挂起 60 秒。你可以按照你的想法运行尽可能多的 curl 命令（好吧，并不能运行特别特别多 ^_^），所有的命令都会立刻输出来自服务器的响应 “Hello, World!”，并不会出现任何可被察觉到的延迟行为。试试看吧。如果你要理解 fork()，那最重要的一点是：你调用了它一次，但是它会返回两次 —— 一次在父进程中，另一次是在子进程中。当你创建了一个新进程，那么 fork() 在子进程中的返回值是 0。如果是在父进程中，那 fork() 函数会返回子进程的 PID。我依然记得在第一次看到它并尝试使用 fork() 的时候，我是多么的入迷。它在我眼里就像是魔法一样。这就好像我在读一段顺序执行的代码，然后“砰！”地一声，代码变成了两份，然后出现了两个实体，同时并行地运行相同的代码。讲真，那个时候我觉得它真的跟魔法一样神奇。当父进程创建出一个新的子进程时，子进程会复制从父进程中复制一份文件描述符：你可能注意到，在上面的代码中，父进程关闭了客户端连接：else: ### 父进程 client_connection.close() # 关闭父进程的副本并循环 不过，既然父进程关闭了这个套接字，那为什么子进程仍然能够从来自客户端的套接字中读取数据呢？答案就在上面的图片中。内核会使用描述符引用计数器来决定是否要关闭一个套接字。当你的服务器创建一个子进程时，子进程会复制父进程的所有文件描述符，内核中该描述符的引用计数也会增加。如果只有一个父进程及一个子进程，那客户端套接字的文件描述符引用数应为 2；当父进程关闭客户端连接的套接字时，内核只会减少它的引用计数，将其变为 1，但这仍然不会使内核关闭该套接字。子进程也关闭了父进程中 listen_socket 的复制实体，因为子进程不需要关注新的客户端连接，而只需要处理已建立的客户端连接中的请求。listen_socket.close() ### 关闭子进程中的复制实体 我们将会在后文中讨论，如果你不关闭那些重复的描述符，会发生什么。你可以从你的并发服务器源码中看到，父进程的主要职责为：接受一个新的客户端连接，复制出一个子进程来处理这个连接，然后继续循环来接受另外的客户端连接，仅此而已。服务器父进程并不会处理客户端连接——子进程才会做这件事。打个岔：当我们说两个事件并发执行时，我们所要表达的意思是什么？当我们说“两个事件并发执行”时，它通常意味着这两个事件同时发生。简单来讲，这个定义没问题，但你应该记住它的严格定义：如果你不能在代码中判断两个事件的发生顺序，那这两个事件就是并发执行的。（引自《信号系统简明手册 (第二版): 并发控制深入浅出及常见错误》）好的，现在你又该回顾一下你刚刚学过的知识点了。在 Unix 中，编写一个并发服务器的最简单的方式——使用 fork() 系统调用；当一个进程分叉（fork）出另一个进程时，它会变成刚刚分叉出的进程的父进程；在进行 fork 调用后，父进程和子进程共享相同的文件描述符；系统内核通过描述符的引用计数来决定是否要关闭该描述符对应的文件或套接字；服务器父进程的主要职责：现在它做的只是从客户端接受一个新的连接，分叉出子进程来处理这个客户端连接，然后开始下一轮循环，去接收新的客户端连接。进程分叉后不关闭重复的套接字会发生什么？我们来看看，如果我们不在父进程与子进程中关闭重复的套接字描述符会发生什么。下面是刚才的并发服务器代码的修改版本，这段代码（webserver3d.py 中，服务器不会关闭重复的描述符）：####################################################### # 并发服务器 - webserver3d.py # # # # 使用 Python 2.7.9 或 3.4 # # 在 Ubuntu 14.04 及 Mac OS X 环境下测试通过 # ####################################################### import os import socket SERVER_ADDRESS = (HOST, PORT) = '', 8888 REQUEST_QUEUE_SIZE = 5 def handle_request(client_connection): request = client_connection.recv(1024) http_response = b"""\ HTTP/1.1 200 OK Hello, World! """ client_connection.sendall(http_response) def serve_forever(): listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) listen_socket.bind(SERVER_ADDRESS) listen_socket.listen(REQUEST_QUEUE_SIZE) print('Serving HTTP on port {port} ...'.format(port=PORT)) clients = [] while True: client_connection, client_address = listen_socket.accept() ### 将引用存储起来，否则在下一轮循环时，他们会被垃圾回收机制销毁 clients.append(client_connection) pid = os.fork() if pid == 0: ### 子进程 listen_socket.close() ### 关闭子进程中多余的套接字 handle_request(client_connection) client_connection.close() os._exit(0) ### 子进程在这里结束 else: ### 父进程 # client_connection.close() print(len(clients)) if __name__ == '__main__': serve_forever() 用以下命令来启动服务器：$ python webserver3d.py 用 curl 命令连接服务器：$ curl http://localhost:8888/hello Hello, World! 好，curl 命令输出了来自并发服务器的响应内容，但程序并没有退出，而是仍然挂起。到底发生了什么？这个服务器并不会挂起 60 秒：子进程只处理客户端连接，关闭连接然后退出，但客户端的 curl 命令并没有终止。所以，为什么 curl 不终止呢？原因就在于文件描述符的副本。当子进程关闭客户端连接时，系统内核会减少客户端套接字的引用计数，将其变为 1。服务器子进程退出了，但客户端套接字并没有被内核关闭，因为该套接字的描述符引用计数并没有变为 0，所以，这就导致了连接终止包（在 TCP/IP 协议中称作 FIN）不会被发送到客户端，所以客户端会一直保持连接。这里也会出现另一个问题：如果你的服务器长时间运行，并且不关闭文件描述符的副本，那么可用的文件描述符会被消耗殆尽：使用 Control-C 关闭服务器 webserver3d.py，然后在 shell 中使用内置命令 ulimit 来查看系统默认为你的服务器进程分配的可用资源数：$ ulimit -a core file size (blocks, -c) 0 data seg size (kbytes, -d) unlimited scheduling priority (-e) 0 file size (blocks, -f) unlimited pending signals (-i) 3842 max locked memory (kbytes, -l) 64 max memory size (kbytes, -m) unlimited open files (-n) 1024 pipe size (512 bytes, -p) 8 POSIX message queues (bytes, -q) 819200 real-time priority (-r) 0 stack size (kbytes, -s) 8192 cpu time (seconds, -t) unlimited max user processes (-u) 3842 virtual memory (kbytes, -v) unlimited file locks (-x) unlimited 你可以从上面的结果看到，在我的 Ubuntu 机器中，系统为我的服务器进程分配的最大可用文件描述符（文件打开）数为 1024。现在我们来看一看，如果你的服务器不关闭重复的描述符，它会如何消耗可用的文件描述符。在一个已有的或新建的终端窗口中，将你的服务器进程的最大可用文件描述符设为 256：$ ulimit -n 256 在你刚刚运行 ulimit -n 256 的终端窗口中运行服务器 webserver3d.py：$ python webserver3d.py 然后使用下面的客户端 client3.py 来测试你的服务器。####################################################### # 测试客户端 - client3.py # # # # 使用 Python 2.7.9 或 3.4 # # 在 Ubuntu 14.04 及 Mac OS X 环境下测试通过 # ####################################################### import argparse import errno import os import socket SERVER_ADDRESS = 'localhost', 8888 REQUEST = b"""\ GET /hello HTTP/1.1 Host: localhost:8888 """ def main(max_clients, max_conns): socks = [] for client_num in range(max_clients): pid = os.fork() if pid == 0: for connection_num in range(max_conns): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(SERVER_ADDRESS) sock.sendall(REQUEST) socks.append(sock) print(connection_num) os._exit(0) if __name__ == '__main__': parser = argparse.ArgumentParser( description='Test client for LSBAWS.', formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) parser.add_argument( '--max-conns', type=int, default=1024, help='Maximum number of connections per client.' ) parser.add_argument( '--max-clients', type=int, default=1, help='Maximum number of clients.' ) args = parser.parse_args() main(args.max_clients, args.max_conns) 在一个新建的终端窗口中，运行 client3.py 然后让它与服务器同步创建 300 个连接：$ python client3.py --max-clients=300 过一会，你的服务器进程就该爆了。这是我的环境中出现的异常截图：这个例子很明显——你的服务器应该关闭描述符副本。僵尸进程但是，即使你关闭了描述符副本，你依然没有摆脱险境，因为你的服务器还有一个问题，这个问题在于“ 僵尸 （ zombies ） ”！没错，这个服务器代码确实在制造僵尸进程。我们来看看怎么回事。重新运行你的服务器：$ python webserver3d.py 在另一个终端窗口中运行以下 curl 命令：$ curl http://localhost:8888/hello 现在，运行 ps 环境，来查看正在运行的 Python 进程。下面是我的环境中 ps 的运行结果：$ ps auxw | grep -i python | grep -v grep vagrant 9099 0.0 1.2 31804 6256 pts/0 S+ 16:33 0:00 python webserver3d.py vagrant 9102 0.0 0.0 0 0 pts/0 Z+ 16:33 0:00 [python] <defunct> 你看到第二行中，pid 为 9102，状态为 Z+，名字里面有个 <defunct> 的进程了吗？那就是我们的僵尸进程。这个僵尸进程的问题在于：你无法将它杀掉！就算你尝试使用 kill -9 来杀死僵尸进程，它们仍旧会存活。自己试试看，看看结果。这个僵尸到底是什么，为什么我们的服务器会造出它们呢？一个 僵尸进程 （ zombie ） 是一个已经结束的进程，但它的父进程并没有等待（waited）它结束，并且也没有收到它的终结状态。如果一个进程在父进程退出之前退出，系统内核会把它变为一个僵尸进程，存储它的部分信息，以便父进程读取。内核保存的进程信息通常包括进程 ID、进程终止状态，以及进程的资源占用情况。OK，所以僵尸进程确实有存在的意义，但如果服务器不管这些僵尸进程，你的系统将会被壅塞。我们来看看这个会如何发生。首先，关闭你运行的服务器；然后，在一个新的终端窗口中，使用 ulimit 命令将最大用户进程数设为 400（同时，要确保你的最大可用描述符数大于这个数字，我们在这里设为 500）：$ ulimit -u 400 $ ulimit -n 500 在你刚刚运行 ulimit -u 400 命令的终端中，运行服务器 webserver3d.py：$ python webserver3d.py 在一个新的终端窗口中，运行 client3.py，并且让它与服务器同时创建 500 个连接：$ python client3.py --max-clients=500 然后，过一会，你的服务器进程应该会再次爆了，它会在创建新进程时抛出一个 OSError: 资源暂时不可用 的异常。但它并没有达到系统允许的最大进程数。这是我的环境中输出的异常信息截图：你可以看到，如果服务器不管僵尸进程，它们会引发问题。接下来我会简单探讨一下僵尸进程问题的解决方案。我们来回顾一下你刚刚掌握的知识点：如果你不关闭文件描述符副本，客户端就不会在请求处理完成后终止，因为客户端连接没有被关闭；如果你不关闭文件描述符副本，长久运行的服务器最终会把可用的文件描述符（最大文件打开数）消耗殆尽；当你创建一个新进程，而父进程不等待（wait）子进程，也不在子进程结束后收集它的终止状态，它会变为一个僵尸进程；僵尸通常都会吃东西，在我们的例子中，僵尸进程会吃掉资源。如果你的服务器不管僵尸进程，它最终会消耗掉所有的可用进程（最大用户进程数）；你不能杀死（kill）僵尸进程，你需要等待（wait）它。如何处理僵尸进程？所以，你需要做什么来处理僵尸进程呢？你需要修改你的服务器代码，来等待（wait）僵尸进程，并收集它们的终止信息。你可以在代码中使用系统调用 wait 来完成这个任务。不幸的是，这个方法离理想目标还很远，因为在没有终止的子进程存在的情况下调用 wait 会导致服务器进程阻塞，这会阻碍你的服务器处理新的客户端连接请求。那么，我们有其他选择吗？嗯，有的，其中一个解决方案需要结合信号处理以及 wait 系统调用。这是它的工作流程。当一个子进程退出时，内核会发送 SIGCHLD 信号。父进程可以设置一个信号处理器，它可以异步响应 SIGCHLD 信号，并在信号响应函数中等待（wait）子进程收集终止信息，从而阻止了僵尸进程的存在。顺便说一下，异步事件意味着父进程无法提前知道事件的发生时间。修改你的服务器代码，设置一个 SIGCHLD 信号处理器，在信号处理器中等待（wait）终止的子进程。修改后的代码如下（webserver3e.py）：####################################################### # 并发服务器 - webserver3e.py # # # # 使用 Python 2.7.9 或 3.4 # # 在 Ubuntu 14.04 及 Mac OS X 环境下测试通过 # ####################################################### import os import signal import socket import time SERVER_ADDRESS = (HOST, PORT) = '', 8888 REQUEST_QUEUE_SIZE = 5 def grim_reaper(signum, frame): pid, status = os.wait() print( 'Child {pid} terminated with status {status}' '\n'.format(pid=pid, status=status) ) def handle_request(client_connection): request = client_connection.recv(1024) print(request.decode()) http_response = b"""\ HTTP/1.1 200 OK Hello, World! """ client_connection.sendall(http_response) ### 挂起进程，来允许父进程完成循环，并在 "accept" 处阻塞 time.sleep(3) def serve_forever(): listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) listen_socket.bind(SERVER_ADDRESS) listen_socket.listen(REQUEST_QUEUE_SIZE) print('Serving HTTP on port {port} ...'.format(port=PORT)) signal.signal(signal.SIGCHLD, grim_reaper) while True: client_connection, client_address = listen_socket.accept() pid = os.fork() if pid == 0: ### 子进程 listen_socket.close() ### 关闭子进程中多余的套接字 handle_request(client_connection) client_connection.close() os._exit(0) else: ### 父进程 client_connection.close() if __name__ == '__main__': serve_forever() 运行服务器：$ python webserver3e.py 使用你的老朋友——curl 命令来向修改后的并发服务器发送一个请求：$ curl http://localhost:8888/hello 再来看看服务器：刚刚发生了什么？accept 调用失败了，错误信息为 EINTR。当子进程退出并触发 SIGCHLD 事件时，父进程的 accept 调用被阻塞了，系统转去运行信号处理器，当信号处理函数完成时，accept 系统调用被打断：别担心，这个问题很好解决。你只需要重新运行 accept 系统调用即可。这是修改后的服务器代码 webserver3f.py，它可以解决这个问题：####################################################### # 并发服务器 - webserver3f.py # # # # 使用 Python 2.7.9 或 3.4 # # 在 Ubuntu 14.04 及 Mac OS X 环境下测试通过 # ####################################################### import errno import os import signal import socket SERVER_ADDRESS = (HOST, PORT) = '', 8888 REQUEST_QUEUE_SIZE = 1024 def grim_reaper(signum, frame): pid, status = os.wait() def handle_request(client_connection): request = client_connection.recv(1024) print(request.decode()) http_response = b"""\ HTTP/1.1 200 OK Hello, World! """ client_connection.sendall(http_response) def serve_forever(): listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) listen_socket.bind(SERVER_ADDRESS) listen_socket.listen(REQUEST_QUEUE_SIZE) print('Serving HTTP on port {port} ...'.format(port=PORT)) signal.signal(signal.SIGCHLD, grim_reaper) while True: try: client_connection, client_address = listen_socket.accept() except IOError as e: code, msg = e.args ### 若 'accept' 被打断，那么重启它 if code == errno.EINTR: continue else: raise pid = os.fork() if pid == 0: ### 子进程 listen_socket.close() ### 关闭子进程中多余的描述符 handle_request(client_connection) client_connection.close() os._exit(0) else: ### 父进程 client_connection.close() ### 关闭父进程中多余的描述符，继续下一轮循环 if __name__ == '__main__': serve_forever() 运行更新后的服务器 webserver3f.py：$ python webserver3f.py 用 curl 来向更新后的并发服务器发送一个请求：$ curl http://localhost:8888/hello 看到了吗？没有 EINTR 异常出现了。现在检查一下，确保没有僵尸进程存活，调用 wait 函数的 SIGCHLD 信号处理器能够正常处理被终止的子进程。我们只需使用 ps 命令，然后看看现在没有处于 Z+ 状态（或名字包含 <defunct> ）的 Python 进程就好了。很棒！僵尸进程没有了，我们很安心。如果你创建了一个子进程，但是不等待它，它就会变成一个僵尸进程；使用 SIGCHLD 信号处理器可以异步地等待子进程终止，并收集其终止状态；当使用事件处理器时，你需要牢记，系统调用可能会被打断，所以你需要处理这种情况发生时带来的异常。正确处理 SIGCHLD 信号好的，一切顺利。是不是没问题了？额，几乎是。重新尝试运行 webserver3f.py 但我们这次不会只发送一个请求，而是同步创建 128 个连接：$ python client3.py --max-clients 128 现在再次运行 ps 命令：$ ps auxw | grep -i python | grep -v grep 看到了吗？天啊，僵尸进程又出来了！这回怎么回事？当你同时运行 128 个客户端，建立 128 个连接时，服务器的子进程几乎会在同一时间处理好你的请求，然后退出。这会导致非常多的 SIGCHLD 信号被发送到父进程。问题在于，这些信号不会存储在队列中，所以你的服务器进程会错过很多信号，这也就导致了几个僵尸进程处于无主状态：这个问题的解决方案依然是设置 SIGCHLD 事件处理器。但我们这次将会用 WNOHANG 参数循环调用 waitpid 来替代 wait，以保证所有处于终止状态的子进程都会被处理。下面是修改后的代码，webserver3g.py：####################################################### # 并发服务器 - webserver3g.py # # # # 使用 Python 2.7.9 或 3.4 # # 在 Ubuntu 14.04 及 Mac OS X 环境下测试通过 # ####################################################### import errno import os import signal import socket SERVER_ADDRESS = (HOST, PORT) = '', 8888 REQUEST_QUEUE_SIZE = 1024 def grim_reaper(signum, frame): while True: try: pid, status = os.waitpid( -1, ### 等待所有子进程 os.WNOHANG ### 无终止进程时，不阻塞进程，并抛出 EWOULDBLOCK 错误 ) except OSError: return if pid == 0: ### 没有僵尸进程存在了 return def handle_request(client_connection): request = client_connection.recv(1024) print(request.decode()) http_response = b"""\ HTTP/1.1 200 OK Hello, World! """ client_connection.sendall(http_response) def serve_forever(): listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) listen_socket.bind(SERVER_ADDRESS) listen_socket.listen(REQUEST_QUEUE_SIZE) print('Serving HTTP on port {port} ...'.format(port=PORT)) signal.signal(signal.SIGCHLD, grim_reaper) while True: try: client_connection, client_address = listen_socket.accept() except IOError as e: code, msg = e.args ### 若 'accept' 被打断，那么重启它 if code == errno.EINTR: continue else: raise pid = os.fork() if pid == 0: ### 子进程 listen_socket.close() ### 关闭子进程中多余的描述符 handle_request(client_connection) client_connection.close() os._exit(0) else: ### 父进程 client_connection.close() ### 关闭父进程中多余的描述符，继续下一轮循环 if __name__ == '__main__': serve_forever() 运行服务器：$ python webserver3g.py 使用测试客户端 client3.py：$ python client3.py --max-clients 128 现在来查看一下，确保没有僵尸进程存在。耶！没有僵尸的生活真美好 ^_^。大功告成恭喜！你刚刚经历了一段很长的旅程，我希望你能够喜欢它。现在你拥有了自己的简易并发服务器，并且这段代码能够为你在继续研究生产级 Web 服务器的路上奠定基础。我将会留一个作业：你需要将第二部分中的 WSGI 服务器升级，将它改造为一个并发服务器。你可以在这里找到更改后的代码。但是，当你实现了自己的版本之后，你才应该来看我的代码。你已经拥有了实现这个服务器所需的所有信息。所以，快去实现它吧 ^_^。然后要做什么呢？乔希·比林斯说过：“就像一枚邮票一样——专注于一件事，不达目的不罢休。”开始学习基本知识。回顾你已经学过的知识。然后一步一步深入。“如果你只学会了方法，你将会被这些方法所困。但如果你学会了原理，那你就能发明出新的方法。”——拉尔夫·沃尔多·爱默生“有道无术，术尚可求也，有术无道，止于术”——中国古代也有这样的话，LCTT 译注下面是一份书单，我从这些书中提炼出了这篇文章所需的素材。他们能助你在我刚刚所述的几个方面中发掘出兼具深度和广度的知识。我极力推荐你们去搞到这几本书看看：从你的朋友那里借，在当地的图书馆中阅读，或者直接在亚马逊上把它买回来。下面是我的典藏秘籍：《UNIX 网络编程 卷1：套接字联网 API （第3版）》《UNIX 环境高级编程（第3版）》《Linux/UNIX 系统编程手册》《TCP/IP 详解 卷1：协议（第2版）《信号系统简明手册 (第二版): 并发控制深入浅出及常见错误》，这本书也可以从作者的个人网站中免费下载到。顺便，我在撰写一本名为《搭个 Web 服务器：从头开始》的书。这本书讲解了如何从头开始编写一个基本的 Web 服务器，里面包含本文中没有的更多细节。订阅原文下方的邮件列表，你就可以获取到这本书的最新进展，以及发布日期。via: https://ruslanspivak.com/lsbaws-part3/作者：Ruslan 译者：StdioA 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
自从我写了上一篇博文之后，就再也找不到空闲时间写文章了。今天我终于可以抽出时间写一些关于 HTTP 的东西。我认为每一个 web 开发者都应该对这个支撑了整个 Web 世界的 HTTP 协议有所了解，这样才能帮助你更好的完成开发任务。在这篇文章中，我将讨论什么是 HTTP，它是怎么产生的，它的地位，以及我们应该怎么使用它。HTTP 是什么首先我们要明白 HTTP 是什么。HTTP 是一个基于 TCP/IP 的应用层通信协议，它是客户端和服务端在互联网互相通讯的标准。它定义了内容是如何通过互联网进行请求和传输的。HTTP 是在应用层中抽象出的一个标准，使得主机（客户端和服务端）之间的通信得以通过 TCP/IP 来进行请求和响应。TCP 默认使用的端口是 80，当然也可以使用其它端口，比如 HTTPS 使用的就是 443 端口。HTTP/0.9 - 单行协议 (1991)HTTP 最早的规范可以追溯到 1991 年，那时候的版本是 HTTP/0.9，该版本极其简单，只有一个叫做 GET的请求方式。如果客户端要访问服务端上的一个页面，只需要如下非常简单的请求：GET /index.html 服务端对应的返回类似如下：(response body) (connection closed) 就这么简单，服务端捕获到请求后立马返回 HTML 并且关闭连接，在这之中没有 头信息 （ headers ） 仅支持 GET 这一种请求方法必须返回 HTML如同你所看到的，当时的 HTTP 协议只是一块基础的垫脚石。HTTP/1.0 - 1996在 1996 年，新版本的 HTTP 对比之前的版本有了极大的改进，同时也被命名为 HTTP/1.0。与 HTTP/0.9 只能返回 HTML 不同的是，HTTP/1.0 支持处理多种返回的格式，比如图片、视频、文本或者其他格式的文件。它还增加了更多的请求方法（如 POST 和 HEAD），请求和响应的格式也相应做了改变，两者都增加了头信息；引入了状态码来定义返回的特征；引入了字符集支持；支持 多段类型 （ multi-part ） 、用户验证信息、缓存、内容编码格式等等。一个简单的 HTTP/1.0 请求大概是这样的：GET / HTTP/1.0 Host: kamranahmed.info User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) Accept: */* 正如你所看到的，在请求中附带了客户端中的一些个人信息、响应类型要求等内容。这些是在 HTTP/0.9 无法实现的，因为那时候没有头信息。一个对上述请求的响应例子如下所示：HTTP/1.0 200 OK Content-Type: text/plain Content-Length: 137582 Expires: Thu, 05 Dec 1997 16:00:00 GMT Last-Modified: Wed, 5 August 1996 15:55:28 GMT Server: Apache 0.84 (response body) (connection closed) 从 HTTP/1.0 （HTTP 后面跟的是版本号）早期开始，在状态码 200 之后就附带一个原因短语（你可以用来描述状态码）。在这个较新一点的版本中，请求和响应的头信息仍然必须是 ASCII 编码，但是响应的内容可以是任意类型，如图片、视频、HTML、文本或其他类型，服务器可以返回任意内容给客户端。所以这之后，HTTP 中的“ 超文本 （ Hyper Text ） ”成了名不副实。 HMTP （ 超媒体传输协议 （ Hypermedia transfer protocol ） ）可能会更有意义，但是我猜我们还是会一直沿用这个名字。HTTP/1.0 的一个主要缺点就是它不能在一个连接内拥有多个请求。这意味着，当客户端需要从服务器获取东西时，必须建立一个新的 TCP 连接，并且处理完单个请求后连接即被关闭。需要下一个东西时，你必须重新建立一个新的连接。这样的坏处在哪呢？假设你要访问一个有 10 张图片，5 个 样式表 （ stylesheet ） 和 5 个 JavaScript 的总计 20 个文件才能完整展示的一个页面。由于一个连接在处理完成一次请求后即被关闭，所以将有 20 个单独的连接，每一个文件都将通过各自对应的连接单独处理。当连接数量变得庞大的时候就会面临严重的性能问题，因为 TCP 启动需要经过三次握手，才能缓慢开始。三次握手三次握手是一个简单的模型，所有的 TCP 连接在传输应用数据之前都需要在三次握手中传输一系列数据包。SYN - 客户端选取一个随机数，我们称为 x，然后发送给服务器。SYN ACK - 服务器响应对应请求的 ACK 包中，包含了一个由服务器随机产生的数字，我们称为 y，并且把客户端发送的 x+1，一并返回给客户端。ACK - 客户端在从服务器接受到 y 之后把 y 加上 1 作为一个 ACK 包返回给服务器。一旦三次握手完成后，客户端和服务器之间就可以开始交换数据。值得注意的是，当客户端发出最后一个 ACK 数据包后，就可以立刻向服务器发送应用数据包，而服务器则需要等到收到这个 ACK 数据包后才能接受应用数据包。请注意，上图有点小问题，客户端发回的最后一个 ACK 包仅包含 y+1，上图应该是 ACK:y+1 而不是 ACK:x+1,y+1然而，某些 HTTP/1.0 的实现试图通过新引入一个称为 Connection: keep-alive 的头信息来克服这一问题，这个头信息意味着告诉服务器“嘿，服务器，请不要关闭此连接，我还要用它”。但是，这并没有得到广泛的支持，问题依然存在。除了无连接之外，HTTP 还是一个无状态的协议，即服务器不维护有关客户端的信息。因此每个请求必须给服务器必要的信息才能完成请求，每个请求都与之前的旧的请求无关。所以，这增加了推波助澜的作用，客户端除了需要新建大量连接之外，在每次连接中还需要发送许多重复的数据，这导致了带宽的大量浪费。HTTP/1.1 - 1999HTTP/1.0 经过仅仅 3 年，下一个版本，即 HTTP/1.1 就在 1999 年发布了，改进了它的前身很多问题，主要的改进包括：增加了许多 HTTP 请求方法，包括 PUT、PATCH、HEAD、OPTIONS、DELETE。主机标识符 Host 在 HTTP/1.0 并不是必须的，而在 HTTP/1.1 是必须的。如上所述的持久连接。在 HTTP/1.0 中每个连接只有一个请求并在该请求结束后被立即关闭，这导致了性能问题和增加了延迟。 HTTP/1.1 引入了持久连接，即连接在默认情况下是不关闭并保持开放的，这允许多个连续的请求使用这个连接。要关闭该连接只需要在头信息加入 Connection: close，客户通常在最后一个请求里发送这个头信息就能安全地关闭连接。新版本还引入了“ 管线化 （ pipelining ） ”的支持，客户端可以不用等待服务器返回响应，就能在同一个连接内发送多个请求给服务器，而服务器必须以接收到的请求相同的序列发送响应。但是你可能会问了，客户端如何知道哪里是第一个响应下载完成而下一个响应内容开始的地方呢？要解决这个问题，头信息必须有 Content-Length，客户可以使用它来确定哪些响应结束之后可以开始等待下一个响应。值得注意的是，为了从持久连接或管线化中受益， 头部信息必须包含 Content-Length，因为这会使客户端知道什么时候完成了传输，然后它可以发送下一个请求（持久连接中，以正常的依次顺序发送请求）或开始等待下一个响应（启用管线化时）。但是，使用这种方法仍然有一个问题。那就是，如果数据是动态的，服务器无法提前知道内容长度呢？那么在这种情况下，你就不能使用这种方法中获益了吗？为了解决这个问题，HTTP/1.1 引进了分块编码。在这种情况下，服务器可能会忽略 Content-Length 来支持分块编码（更常见一些）。但是，如果它们都不可用，那么连接必须在请求结束时关闭。在动态内容的情况下分块传输，当服务器在传输开始但无法得到 Content-Length 时，它可能会开始按块发送内容（一块接一块），并在传输时为每一个小块添加 Content-Length。当发送完所有的数据块后，即整个传输已经完成后，它发送一个空的小块，比如设置 Content-Length 为 0 ，以便客户端知道传输已完成。为了通知客户端块传输的信息，服务器在头信息中包含了 Transfer-Encoding: chunked。不像 HTTP/1.0 中只有 Basic 身份验证方式，HTTP/1.1 包括 摘要验证方式 （ digest authentication ） 和 代理验证方式 （ proxy authentication ） 。缓存。 范围请求 （ Byte Ranges ） 。字符集。 内容协商 （ Content Negotiation ） 。客户端 cookies。支持压缩。新的状态码。等等。我不打算在这里讨论所有 HTTP/1.1 的特性，因为你可以围绕这个话题找到很多关于这些的讨论。我建议你阅读 HTTP/1.0 和 HTTP/1.1 版本之间的主要差异，希望了解更多可以读原始的 RFC。HTTP/1.1 在 1999 年推出，到现在已经是多年前的标准。虽然，它比前一代改善了很多，但是网络日新月异，它已经垂垂老矣。相比之前，加载网页更是一个资源密集型任务，打开一个简单的网页已经需要建立超过 30 个连接。你或许会说，HTTP/1.1 具有持久连接，为什么还有这么多连接呢？其原因是，在任何时刻 HTTP/1.1 只能有一个未完成的连接。 HTTP/1.1 试图通过引入管线来解决这个问题，但它并没有完全地解决。因为一旦管线遇到了缓慢的请求或庞大的请求，后面的请求便被阻塞住，它们必须等待上一个请求完成。为了克服 HTTP/1.1 的这些缺点，开发人员开始实现一些解决方法，例如使用 spritesheets、在 CSS 中编码图像、单个巨型 CSS / JavaScript 文件、域名切分等。SPDY - 2009谷歌走在业界前列，为了使网络速度更快，提高网络安全，同时减少网页的等待时间，他们开始实验替代的协议。在 2009 年，他们宣布了 SPDY。SPDY 是谷歌的商标，而不是一个缩写。显而易见的是，如果我们继续增加带宽，网络性能开始的时候能够得到提升，但是到了某个阶段后带来的性能提升就很有限了。但是如果把这些优化放在等待时间上，比如减少等待时间，将会有持续的性能提升。这就是 SPDY 优化之前的协议的核心思想，减少等待时间来提升网络性能。对于那些不知道其中区别的人，等待时间就是延迟，即数据从源到达目的地需要多长时间（单位为毫秒），而带宽是每秒钟数据的传输量（比特每秒）。SPDY 的特点包括：复用、压缩、优先级、安全性等。我不打算展开 SPDY 的细节。在下一章节，当我们将介绍 HTTP/2，这些都会被提到，因为 HTTP/2 大多特性是从 SPDY 受启发的。SPDY 没有试图取代 HTTP，它是处于应用层的 HTTP 之上的一个传输层，它只是在请求被发送之前做了一些修改。它开始成为事实标准，大多数浏览器都开始支持了。2015年，谷歌不想有两个相互竞争的标准，所以他们决定将其合并到 HTTP 协议，这样就导致了 HTTP/2 的出现和 SPDY 的废弃。HTTP/2 - 2015现在想必你明白了为什么我们需要另一个版本的 HTTP 协议了。 HTTP/2 是专为了低延迟地内容传输而设计。主要特点和与 HTTP/1.1 的差异包括：使用二进制替代明文多路传输 - 多个异步 HTTP 请求可以使用单一连接报头使用 HPACK 压缩服务器推送 - 单个请求多个响应请求优先级安全性1. 二进制协议HTTP/2 通过使其成为一个二进制协议以解决 HTTP/1.x 中存在的延迟问题。作为一个二进制协议，它更容易解析，但可读性却不如 HTTP/1.x。 帧 （ frames ） 和 流 （ stream ） 的概念组成了 HTTP/2 的主要部分。帧和流现在 HTTP 消息是由一个或多个帧组成的。HEADERS 帧承载了 元数据 （ meta data ） ，DATA 帧则承载了内容。还有其他类型的帧（HEADERS、DATA、RST_STREAM、SETTINGS、PRIORITY 等等），这些你可以通过 HTTP/2 规范来了解。每个 HTTP/2 请求和响应都被赋予一个唯一的流 ID，并切分成帧。帧就是一小片二进制数据。帧的集合称为流，每个帧都有个标识了其所属流的流 ID，所以在同一个流下的每个帧具有共同的报头。值得注意的是，​除了流 ID 是唯一的之外，​由客户端发起的请求使用了奇数作为流 ID，从来自服务器的响应使用了偶数作为流 ID。除了 HEADERS 帧和 DATA 帧，另一个值得一提的帧是 RST_STREAM。这是一个特殊的帧类型，用来中止流，即客户可以发送此帧让服务器知道，我不再需要这个流了。在 HTTP/1.1 中让服务器停止给客户端发送响应的唯一方法是关闭连接，这样造成了延迟增加，因为之后要发送请求时，就要必须打开一个新的请求。而在 HTTP/2，客户端可以使用 RST_STREAM 来停止接收特定的数据流，而连接仍然打开着，可以被其他请求使用。2. 多路传输因为 HTTP/2 是一个二进制协议，而且如上所述它使用帧和流来传输请求与响应，一旦建立了 TCP 连接，相同连接内的所有流都可以同过这个 TCP 连接异步发送，而不用另外打开连接。反过来说，服务器也可以使用同样的异步方式返回响应，也就是说这些响应可以是无序的，客户端使用分配的流 ID 来识别数据包所属的流。这也解决了 HTTP/1.x 中请求管道被阻塞的问题，即客户端不必等待占用时间的请求而其他请求仍然可以被处理。3. HPACK 请求头部压缩RFC 花了一篇文档的篇幅来介绍针对发送的头信息的优化，它的本质是当我们在同一客户端上不断地访问服务器时，许多冗余数据在头部中被反复发送，有时候仅仅是 cookies 就能增加头信息的大小，这会占用许多宽带和增加传输延迟。为了解决这个问题，HTTP/2 引入了头信息压缩。不像请求和响应那样，头信息中的信息不会以 gzip 或者 compress 等格式压缩。而是采用一种不同的机制来压缩头信息，客户端和服务器同时维护一张头信息表，储存了使用了哈夫曼编码进行编码后的头信息的值，并且后续请求中若出现同样的字段则忽略重复值（例如 用户代理 （ user agent ） 等），只发送存在两边信息表中它的引用即可。我们说的头信息，它们同 HTTP/1.1 中一样，并在此基础上增加了一些伪头信息，如 :scheme，:host 和 :path。4. 服务器推送服务器推送是 HTTP/2 的另一个巨大的特点。对于服务器来说，当它知道客户端需要一定的资源后，它可以把数据推送到客户端，即使客户端没有请求它。例如，假设一个浏览器在加载一个网页时，它解析了整个页面，发现有一些内容必须要从服务端获取，然后发送相应的请求到服务器以获取这些内容。服务器推送减少了传输这些数据需要来回请求的次数。它是如何做到的呢？服务器通过发送一个名字为 PUSH_PROMISE 特殊的帧通知到客户端“嘿，我准备要发送这个资源给你了，不要再问我要了。”这个 PUSH_PROMISE 帧与要产生推送的流联系在一起，并包含了要推送的流 ID，也就是说这个流将会被服务器推送到客户端上。5. 请求优先级当流被打开的时候，客户端可以在 HEADERS 帧中包含优先级信息来为流指定优先级。在任何时候，客户端都可以发送 PRIORITY 帧来改变流的优先级。如果没有任何优先级信息，服务器将异步地无序地处理这些请求。如果流分配了优先级，服务器将基于这个优先级来决定需要分配多少资源来处理这个请求。6. 安全性在是否强制使用 TLS 来增加安全性的问题上产生了大范围的讨论，讨论的结果是不强制使用。然而大多数厂商只有在使用 TLS 时才能使用 HTTP/2。所以 HTTP/2 虽然规范上不要求加密，但是加密已经约定俗成了。这样，在 TLS 之上实现 HTTP/2 就有了一些强制要求，比如，TLS 的最低版本为 1.2，必须达到某种级别的最低限度的密钥大小，需要布署 ephemeral 密钥等等。到现在 HTTP/2 已经完全超越了 SPDY，并且还在不断成长，HTTP/2 有很多关系性能的提升，我们应该开始布署它了。如果你想更深入的了解细节，请访问该规范的链接和 HTTP/2 性能提升演示的链接。请在留言板写下你的疑问或者评论，最后如果你发现有错误，请同样留言指出。这就是全部了，我们之后再见~via: http://kamranahmed.info/blog/2016/08/13/http-in-depth/作者：Kamran Ahmed 译者：NearTan 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
Nginx 是最快和最强大的 Web 服务器之一，以其高性能和低资源占用率而闻名。它既可以被安装为一个独立的 Web 服务器，也可以安装成反向代理 Web 服务器。在这篇文章，我将讨论在安装了 cPanel 管理系统的 Centos 7 服务器上安装 Nginx 作为 Apache 的反向代理服务器。Nginx 作为前端服务器用反向代理为静态文件提供服务，Apache 作为后端为动态文件提供服务。这个设置将整体提高服务器的性能。让我们过一遍在已经安装好 cPanel 11.52 的 CentOS 7 x86\_64 服务器上配置 Nginx 作为反向代理的安装过程。首先，我们需要安装 EPEL 库来启动这个进程第一步: 安装 EPEL 库root@server1 [/usr]# yum -y install epel-release Loaded plugins: fastestmirror, tsflags, universal-hooks Loading mirror speeds from cached hostfile * EA4: 66.23.237.210 * base: mirrors.linode.com * extras: mirrors.linode.com * updates: mirrors.linode.com Resolving Dependencies --> Running transaction check ---> Package epel-release.noarch 0:7-5 will be installed --> Finished Dependency Resolution Dependencies Resolved ======================================================================================== Package Arch Version Repository Size ======================================================================================== Installing: epel-release noarch 7-5 extras 14 k 第二步: 可以安装 nDeploy 的 CentOS RPM 库来安装我们所需的 nDeploy Web 类软件和 Nginx 插件root@server1 [/usr]# yum -y install http://rpm.piserve.com/nDeploy-release-centos-1.0-1.noarch.rpm Loaded plugins: fastestmirror, tsflags, universal-hooks nDeploy-release-centos-1.0-1.noarch.rpm | 1.7 kB 00:00:00 Examining /var/tmp/yum-root-ei5tWJ/nDeploy-release-centos-1.0-1.noarch.rpm: nDeploy-release-centos-1.0-1.noarch Marking /var/tmp/yum-root-ei5tWJ/nDeploy-release-centos-1.0-1.noarch.rpm to be installed Resolving Dependencies --> Running transaction check ---> Package nDeploy-release-centos.noarch 0:1.0-1 will be installed --> Finished Dependency Resolution Dependencies Resolved ======================================================================================== Package Arch Version Repository Size ======================================================================================== Installing: nDeploy-release-centos noarch 1.0-1 /nDeploy-release-centos-1.0-1.noarch 110 第三步：安装 nDeploy 和 Nginx nDeploy 插件root@server1 [/usr]# yum --enablerepo=ndeploy install nginx-nDeploy nDeploy Loaded plugins: fastestmirror, tsflags, universal-hooks epel/x86_64/metalink | 9.9 kB 00:00:00 epel | 4.3 kB 00:00:00 ndeploy | 2.9 kB 00:00:00 (1/4): ndeploy/7/x86_64/primary_db | 14 kB 00:00:00 (2/4): epel/x86_64/group_gz | 169 kB 00:00:00 (3/4): epel/x86_64/primary_db | 3.7 MB 00:00:02 Dependencies Resolved ======================================================================================== Package Arch Version Repository Size ======================================================================================== Installing: nDeploy noarch 2.0-11.el7 ndeploy 80 k nginx-nDeploy x86_64 1.8.0-34.el7 ndeploy 36 M Installing for dependencies: PyYAML x86_64 3.10-11.el7 base 153 k libevent x86_64 2.0.21-4.el7 base 214 k memcached x86_64 1.4.15-9.el7 base 84 k python-inotify noarch 0.9.4-4.el7 base 49 k python-lxml x86_64 3.2.1-4.el7 base 758 k Transaction Summary ======================================================================================== Install 2 Packages (+5 Dependent packages) 通过以上这些步骤，我们完成了在我们的服务器上 Nginx 插件的安装。现在我们可以配置 Nginx 作为反向代理和为已有的 cPanel 用户账户创建虚拟主机，为此我们可以运行如下脚本。第四步：启动 Nginx 作为默认的前端 Web 服务器，并创建默认的配置文件root@server1 [/usr]# /opt/nDeploy/scripts/cpanel-nDeploy-setup.sh enable Modifying apache http and https port in cpanel httpd restarted successfully. Created symlink from /etc/systemd/system/multi-user.target.wants/nginx.service to /usr/lib/systemd/system/nginx.service. Created symlink from /etc/systemd/system/multi-user.target.wants/ndeploy_watcher.service to /usr/lib/systemd/system/ndeploy_watcher.service. Created symlink from /etc/systemd/system/multi-user.target.wants/ndeploy_backends.service to /usr/lib/systemd/system/ndeploy_backends.service. ConfGen:: saheetha ConfGen:: satest 你可以看到这个脚本将修改 Apache 的端口从 80 到另一个端口来让 Nginx 作为前端 Web 服务器，并为现有的 cPanel 用户创建虚拟主机配置文件。一旦完成，确认 Apache 和 Nginx 的状态。Apache 状态：root@server1 [/var/run/httpd]# systemctl status httpd ● httpd.service - Apache Web Server Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled; vendor preset: disabled) Active: active (running) since Mon 2016-01-18 06:34:23 UTC; 12s ago Process: 25606 ExecStart=/usr/sbin/apachectl start (code=exited, status=0/SUCCESS) Main PID: 24760 (httpd) CGroup: /system.slice/httpd.service ‣ 24760 /usr/local/apache/bin/httpd -k start Jan 18 06:34:23 server1.centos7-test.com systemd[1]: Starting Apache Web Server... Jan 18 06:34:23 server1.centos7-test.com apachectl[25606]: httpd (pid 24760) already running Jan 18 06:34:23 server1.centos7-test.com systemd[1]: Started Apache Web Server. Nginx 状态：root@server1 [~]# systemctl status nginx ● nginx.service - nginx-nDeploy - high performance web server Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; vendor preset: disabled) Active: active (running) since Sun 2016-01-17 17:18:29 UTC; 13h ago Docs: http://nginx.org/en/docs/ Main PID: 3833 (nginx) CGroup: /system.slice/nginx.service ├─ 3833 nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf ├─25473 nginx: worker process ├─25474 nginx: worker process └─25475 nginx: cache manager process Jan 17 17:18:29 server1.centos7-test.com systemd[1]: Starting nginx-nDeploy - high performance web server... Jan 17 17:18:29 server1.centos7-test.com nginx[3804]: nginx: the configuration file /etc/nginx/nginx.conf syntax is ok Jan 17 17:18:29 server1.centos7-test.com nginx[3804]: nginx: configuration file /etc/nginx/nginx.conf test is successful Jan 17 17:18:29 server1.centos7-test.com systemd[1]: Started nginx-nDeploy - high performance web server. Nginx 作为前端服务器运行在 80 端口，Apache 配置被更改为监听 http 端口 9999 和 https 端口 4430。请看他们的情况：root@server1 [/usr/local/src]# netstat -plan | grep httpd tcp 0 0 0.0.0.0:4430 0.0.0.0:* LISTEN 17270/httpd tcp 0 0 0.0.0.0:9999 0.0.0.0:* LISTEN 17270/httpd tcp6 0 0 :::4430 :::* LISTEN 17270/httpd tcp6 0 0 :::9999 :::* LISTEN 17270/httpd root@server1 [/usr/local/src]# netstat -plan | grep nginx tcp 0 0 127.0.0.1:80 0.0.0.0:* LISTEN 17802/nginx: master tcp 0 0 45.79.183.73:80 0.0.0.0:* LISTEN 17802/nginx: master 为已有用户创建的虚拟主机的配置文件在 “/etc/nginx/sites-enabled”。 这个文件路径包含了 Nginx 主要配置文件。root@server1 [/etc/nginx/sites-enabled]# ll | grep .conf -rw-r--r-- 1 root root 311 Jan 17 09:02 saheetha.com.conf -rw-r--r-- 1 root root 336 Jan 17 09:02 saheethastest.com.conf 一个域名的示例虚拟主机：server { listen 45.79.183.73:80; #CPIPVSIX:80; # ServerNames server_name saheetha.com www.saheetha.com; access_log /usr/local/apache/domlogs/saheetha.com main; access_log /usr/local/apache/domlogs/saheetha.com-bytes_log bytes_log; include /etc/nginx/sites-enabled/saheetha.com.include; } 我们可以启动浏览器查看网站来确定 Web 服务器的工作状态。安装后，请阅读服务器上的 web 服务信息。root@server1 [/home]# ip a | grep -i eth0 3: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000 inet 45.79.183.73/24 brd 45.79.183.255 scope global dynamic eth0 root@server1 [/home]# nginx -v nginx version: nginx/1.8.0 Nginx 将会为任何最新在 cPanel 中创建的账户创建虚拟主机。通过这些简单的的步骤，我们能够在一台 CentOS 7 / cPanel 的服务器上配置 Nginx 作为反向代理。Nginx 作为反向代理的优势便于安装和配置。效率高、性能好。防止 Ddos 攻击。支持使用 .htaccess 作为 PHP 的重写规则。我希望这篇文章对你们有用。感谢你看它。我非常高兴收到你的宝贵意见和建议，并进一步改善。via: http://linoxide.com/linux-how-to/set-nginx-reverse-proxy-centos-7-cpanel/作者：Saheetha Shameer 译者：bestony 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
在第一部分中，我提出了一个问题：“如何在你刚刚搭建起来的 Web 服务器上适配 Django, Flask 或 Pyramid 应用，而不用单独对 Web 服务器做做出改动以适应各种不同的 Web 框架呢？”我们可以从这一篇中找到答案。曾几何时，你所选择的 Python Web 框架会限制你所可选择的 Web 服务器，反之亦然。如果某个框架及服务器设计用来协同工作的，那么一切正常：但你可能正面对着（或者曾经面对过）尝试将一对无法适配的框架和服务器搭配在一起的问题：基本上，你需要选择那些能够一起工作的框架和服务器，而不能选择你想用的那些。所以，你该如何确保在不对 Web 服务器或框架的代码做任何更改的情况下，让你的 Web 服务器和多个不同的 Web 框架一同工作呢？这个问题的答案，就是 Python Web 服务器网关接口 （ Web Server Gateway Interface ） （缩写为 WSGI，念做“wizgy”）。WSGI 允许开发者互不干扰地选择 Web 框架及 Web 服务器的类型。现在，你可以真正将 Web 服务器及框架任意搭配，然后选出你最中意的那对组合。比如，你可以使用 Django，Flask 或者 Pyramid，与 Gunicorn，Nginx/uWSGI 或 Waitress 进行结合。感谢 WSGI 同时对服务器与框架的支持，我们可以真正随意选择它们的搭配了。所以，WSGI 就是我在第一部分中提出，又在本文开头重复了一遍的那个问题的答案。你的 Web 服务器必须实现 WSGI 接口的服务器部分，而现代的 Python Web 框架均已实现了 WSGI 接口的框架部分，这使得你可以直接在 Web 服务器中使用任意框架，而不需要更改任何服务器代码，以对特定的 Web 框架实现兼容。现在，你已经知道 Web 服务器及 Web 框架对 WSGI 的支持使得你可以选择最合适的一对来使用，而且它也有利于服务器和框架的开发者，这样他们只需专注于其擅长的部分来进行开发，而不需要触及另一部分的代码。其它语言也拥有类似的接口，比如：Java 拥有 Servlet API，而 Ruby 拥有 Rack。这些理论都不错，但是我打赌你在说：“Show me the code!” 那好，我们来看看下面这个很小的 WSGI 服务器实现：### 使用 Python 2.7.9，在 Linux 及 Mac OS X 下测试通过 import socket import StringIO import sys class WSGIServer(object): address_family = socket.AF_INET socket_type = socket.SOCK_STREAM request_queue_size = 1 def __init__(self, server_address): ### 创建一个监听的套接字 self.listen_socket = listen_socket = socket.socket( self.address_family, self.socket_type ) ### 允许复用同一地址 listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) ### 绑定地址 listen_socket.bind(server_address) ### 激活套接字 listen_socket.listen(self.request_queue_size) ### 获取主机的名称及端口 host, port = self.listen_socket.getsockname()[:2] self.server_name = socket.getfqdn(host) self.server_port = port ### 返回由 Web 框架/应用设定的响应头部字段 self.headers_set = [] def set_app(self, application): self.application = application def serve_forever(self): listen_socket = self.listen_socket while True: ### 获取新的客户端连接 self.client_connection, client_address = listen_socket.accept() ### 处理一条请求后关闭连接，然后循环等待另一个连接建立 self.handle_one_request() def handle_one_request(self): self.request_data = request_data = self.client_connection.recv(1024) ### 以 'curl -v' 的风格输出格式化请求数据 print(''.join( '< {line}\n'.format(line=line) for line in request_data.splitlines() )) self.parse_request(request_data) ### 根据请求数据构建环境变量字典 env = self.get_environ() ### 此时需要调用 Web 应用来获取结果， ### 取回的结果将成为 HTTP 响应体 result = self.application(env, self.start_response) ### 构造一个响应，回送至客户端 self.finish_response(result) def parse_request(self, text): request_line = text.splitlines()[0] request_line = request_line.rstrip(' ') ### 将请求行分成几个部分 (self.request_method, # GET self.path, # /hello self.request_version # HTTP/1.1 ) = request_line.split() def get_environ(self): env = {} ### 以下代码段没有遵循 PEP8 规则，但这样排版，是为了通过强调 ### 所需变量及它们的值，来达到其展示目的。 ### ### WSGI 必需变量 env['wsgi.version'] = (1, 0) env['wsgi.url_scheme'] = 'http' env['wsgi.input'] = StringIO.StringIO(self.request_data) env['wsgi.errors'] = sys.stderr env['wsgi.multithread'] = False env['wsgi.multiprocess'] = False env['wsgi.run_once'] = False ### CGI 必需变量 env['REQUEST_METHOD'] = self.request_method # GET env['PATH_INFO'] = self.path # /hello env['SERVER_NAME'] = self.server_name # localhost env['SERVER_PORT'] = str(self.server_port) # 8888 return env def start_response(self, status, response_headers, exc_info=None): ### 添加必要的服务器头部字段 server_headers = [ ('Date', 'Tue, 31 Mar 2015 12:54:48 GMT'), ('Server', 'WSGIServer 0.2'), ] self.headers_set = [status, response_headers + server_headers] ### 为了遵循 WSGI 协议，start_response 函数必须返回一个 'write' ### 可调用对象（返回值.write 可以作为函数调用）。为了简便，我们 ### 在这里无视这个细节。 ### return self.finish_response def finish_response(self, result): try: status, response_headers = self.headers_set response = 'HTTP/1.1 {status} '.format(status=status) for header in response_headers: response += '{0}: {1} '.format(*header) response += ' ' for data in result: response += data ### 以 'curl -v' 的风格输出格式化请求数据 print(''.join( '> {line}\n'.format(line=line) for line in response.splitlines() )) self.client_connection.sendall(response) finally: self.client_connection.close() SERVER_ADDRESS = (HOST, PORT) = '', 8888 def make_server(server_address, application): server = WSGIServer(server_address) server.set_app(application) return server if __name__ == '__main__': if len(sys.argv) < 2: sys.exit('Provide a WSGI application object as module:callable') app_path = sys.argv[1] module, application = app_path.split(':') module = __import__(module) application = getattr(module, application) httpd = make_server(SERVER_ADDRESS, application) print('WSGIServer: Serving HTTP on port {port} ...\n'.format(port=PORT)) httpd.serve_forever() 当然，这段代码要比第一部分的服务器代码长不少，但它仍然很短（只有不到 150 行），你可以轻松理解它，而不需要深究细节。上面的服务器代码还可以做更多——它可以用来运行一些你喜欢的框架写出的 Web 应用，可以是 Pyramid，Flask，Django 或其它 Python WSGI 框架。不相信吗？自己来试试看吧。把以上的代码保存为 webserver2.py，或直接从 Github 上下载它。如果你打算不加任何参数而直接运行它，它会抱怨一句，然后退出。$ python webserver2.py Provide a WSGI application object as module:callable 它想做的其实是为你的 Web 应用服务，而这才是重头戏。为了运行这个服务器，你唯一需要的就是安装好 Python。不过，如果你希望运行 Pyramid，Flask 或 Django 应用，你还需要先安装那些框架。那我们把这三个都装上吧。我推荐的安装方式是通过 virtualenv 安装。按照以下几步来做，你就可以创建并激活一个虚拟环境，并在其中安装以上三个 Web 框架。$ [sudo] pip install virtualenv $ mkdir ~/envs $ virtualenv ~/envs/lsbaws/ $ cd ~/envs/lsbaws/ $ ls bin include lib $ source bin/activate (lsbaws) $ pip install pyramid (lsbaws) $ pip install flask (lsbaws) $ pip install django 现在，你需要创建一个 Web 应用。我们先从 Pyramid 开始吧。把以下代码保存为 pyramidapp.py，并与刚刚的 webserver2.py 放置在同一目录，或直接从 Github 下载该文件：from pyramid.config import Configurator from pyramid.response import Response def hello_world(request): return Response( 'Hello world from Pyramid!\n', content_type='text/plain', ) config = Configurator() config.add_route('hello', '/hello') config.add_view(hello_world, route_name='hello') app = config.make_wsgi_app() 现在，你可以用你自己的 Web 服务器来运行你的 Pyramid 应用了：(lsbaws) $ python webserver2.py pyramidapp:app WSGIServer: Serving HTTP on port 8888 ... 你刚刚让你的服务器去加载 Python 模块 pyramidapp 中的可执行对象 app。现在你的服务器可以接收请求，并将它们转发到你的 Pyramid 应用中了。在浏览器中输入 http://localhost:8888/hello ，敲一下回车，然后看看结果：你也可以使用命令行工具 curl 来测试服务器：$ curl -v http://localhost:8888/hello ... 看看服务器和 curl 向标准输出流打印的内容吧。现在来试试 Flask。运行步骤跟上面的一样。from flask import Flask from flask import Response flask_app = Flask('flaskapp') @flask_app.route('/hello') def hello_world(): return Response( 'Hello world from Flask!\n', mimetype='text/plain' ) app = flask_app.wsgi_app 将以上代码保存为 flaskapp.py，或者直接从 Github 下载，然后输入以下命令运行服务器：(lsbaws) $ python webserver2.py flaskapp:app WSGIServer: Serving HTTP on port 8888 ... 现在在浏览器中输入 http://localhost:8888/hello ，敲一下回车：同样，尝试一下 curl，然后你会看到服务器返回了一条 Flask 应用生成的信息：$ curl -v http://localhost:8888/hello ... 这个服务器能处理 Django 应用吗？试试看吧！不过这个任务可能有点复杂，所以我建议你将整个仓库克隆下来，然后使用 Github 仓库中的 djangoapp.py 来完成这个实验。这里的源代码主要是将 Django 的 helloworld 工程（已使用 Django 的 django-admin.py startproject 命令创建完毕）添加到了当前的 Python 路径中，然后导入了这个工程的 WSGI 应用。（LCTT 译注：除了这里展示的代码，还需要一个配合的 helloworld 工程才能工作，代码可以参见 Github 仓库。）import sys sys.path.insert(0, './helloworld') from helloworld import wsgi app = wsgi.application 将以上代码保存为 djangoapp.py，然后用你的 Web 服务器运行这个 Django 应用：(lsbaws) $ python webserver2.py djangoapp:app WSGIServer: Serving HTTP on port 8888 ... 输入以下链接，敲回车：你这次也可以在命令行中测试——你之前应该已经做过两次了——来确认 Django 应用处理了你的请求：$ curl -v http://localhost:8888/hello ... 你试过了吗？你确定这个服务器可以与那三个框架搭配工作吗？如果没试，请去试一下。阅读固然重要，但这个系列的内容是重新搭建，这意味着你需要亲自动手干点活。去试一下吧。别担心，我等着你呢。不开玩笑，你真的需要试一下，亲自尝试每一步，并确保它像预期的那样工作。好，你已经体验到了 WSGI 的威力：它可以使 Web 服务器及 Web 框架随意搭配。WSGI 在 Python Web 服务器及框架之间提供了一个微型接口。它非常简单，而且在服务器和框架端均可以轻易实现。下面的代码片段展示了 WSGI 接口的服务器及框架端实现：def run_application(application): """服务器端代码。""" ### Web 应用/框架在这里存储 HTTP 状态码以及 HTTP 响应头部， ### 服务器会将这些信息传递给客户端 headers_set = [] ### 用于存储 WSGI/CGI 环境变量的字典 environ = {} def start_response(status, response_headers, exc_info=None): headers_set[:] = [status, response_headers] ### 服务器唤醒可执行变量“application”，获得响应头部 result = application(environ, start_response) ### 服务器组装一个 HTTP 响应，将其传送至客户端 … def app(environ, start_response): """一个空的 WSGI 应用""" start_response('200 OK', [('Content-Type', 'text/plain')]) return ['Hello world!'] run_application(app) 这是它的工作原理：Web 框架提供一个可调用对象 application （WSGI 规范没有规定它的实现方式）。Web 服务器每次收到来自客户端的 HTTP 请求后，会唤醒可调用对象 applition。它会向该对象传递一个包含 WSGI/CGI 变量的环境变量字典 environ，以及一个可调用对象 start_response。Web 框架或应用生成 HTTP 状态码和 HTTP 响应头部，然后将它传给 start_response 函数，服务器会将其存储起来。同时，Web 框架或应用也会返回 HTTP 响应正文。服务器将状态码、响应头部及响应正文组装成一个 HTTP 响应，然后将其传送至客户端（这一步并不在 WSGI 规范中，但从逻辑上讲，这一步应该包含在工作流程之中。所以为了明确这个过程，我把它写了出来）这是这个接口规范的图形化表达：到现在为止，你已经看过了用 Pyramid、Flask 和 Django 写出的 Web 应用的代码，你也看到了一个 Web 服务器如何用代码来实现另一半（服务器端的） WSGI 规范。你甚至还看到了我们如何在不使用任何框架的情况下，使用一段代码来实现一个最简单的 WSGI Web 应用。其实，当你使用上面的框架编写一个 Web 应用时，你只是在较高的层面工作，而不需要直接与 WSGI 打交道。但是我知道你一定也对 WSGI 接口的框架部分感兴趣，因为你在看这篇文章呀。所以，我们不用 Pyramid、Flask 或 Django，而是自己动手来创造一个最朴素的 WSGI Web 应用（或 Web 框架），然后将它和你的服务器一起运行：def app(environ, start_response): """一个最简单的 WSGI 应用。 这是你自己的 Web 框架的起点 ^_^ """ status = '200 OK' response_headers = [('Content-Type', 'text/plain')] start_response(status, response_headers) return ['Hello world from a simple WSGI application!\n'] 同样，将上面的代码保存为 wsgiapp.py 或直接从 Github 上下载该文件，然后在 Web 服务器上运行这个应用，像这样：(lsbaws) $ python webserver2.py wsgiapp:app WSGIServer: Serving HTTP on port 8888 ... 在浏览器中输入下面的地址，然后按下回车。这是你应该看到的结果：你刚刚在学习如何创建一个 Web 服务器的过程中自己编写了一个最朴素的 WSGI Web 框架！棒极了！现在，我们再回来看看服务器传给客户端的那些东西。这是在使用 HTTP 客户端调用你的 Pyramid 应用时，服务器生成的 HTTP 响应内容：这个响应和你在本系列第一部分中看到的 HTTP 响应有一部分共同点，但它还多出来了一些内容。比如说，它拥有四个你曾经没见过的 HTTP 头部：Content-Type, Content-Length, Date 以及 Server。这些头部内容基本上在每个 Web 服务器返回的响应中都会出现。不过，它们都不是被严格要求出现的。这些 HTTP 请求/响应头部字段的目的在于它可以向你传递一些关于 HTTP 请求/响应的额外信息。既然你对 WSGI 接口了解的更深了一些，那我再来展示一下上面那个 HTTP 响应中的各个部分的信息来源：我现在还没有对上面那个 environ 字典做任何解释，不过基本上这个字典必须包含那些被 WSGI 规范事先定义好的 WSGI 及 CGI 变量值。服务器在解析 HTTP 请求时，会从请求中获取这些变量的值。这是 environ 字典应该有的样子：Web 框架会利用以上字典中包含的信息，通过字典中的请求路径、请求动作等等来决定使用哪个视图来处理响应、在哪里读取请求正文、在哪里输出错误信息（如果有的话）。现在，你已经创造了属于你自己的 WSGI Web 服务器，你也使用不同 Web 框架做了几个 Web 应用。而且，你在这个过程中也自己创造出了一个朴素的 Web 应用及框架。这个过程真是累人。现在我们来回顾一下，你的 WSGI Web 服务器在服务请求时，需要针对 WSGI 应用做些什么：首先，服务器开始工作，然后会加载一个可调用对象 application，这个对象由你的 Web 框架或应用提供然后，服务器读取一个请求然后，服务器会解析这个请求然后，服务器会使用请求数据来构建一个 environ 字典然后，它会用 environ 字典及一个可调用对象 start_response 作为参数，来调用 application，并获取响应体内容。然后，服务器会使用 application 返回的响应体，和 start_response 函数设置的状态码及响应头部内容，来构建一个 HTTP 响应。最终，服务器将 HTTP 响应回送给客户端。这基本上是服务器要做的全部内容了。你现在有了一个可以正常工作的 WSGI 服务器，它可以为使用任何遵循 WSGI 规范的 Web 框架（如 Django、Flask、Pyramid，还有你刚刚自己写的那个框架）构建出的 Web 应用服务。最棒的部分在于，它可以在不用更改任何服务器代码的情况下，与多个不同的 Web 框架一起工作。真不错。在结束之前，你可以想想这个问题：“你该如何让你的服务器在同一时间处理多个请求呢？”敬请期待，我会在第三部分向你展示一种解决这个问题的方法。干杯！顺便，我在撰写一本名为《搭个 Web 服务器：从头开始》的书。这本书讲解了如何从头开始编写一个基本的 Web 服务器，里面包含本文中没有的更多细节。订阅邮件列表，你就可以获取到这本书的最新进展，以及发布日期。via: https://ruslanspivak.com/lsbaws-part2/作者：Ruslan 译者：StdioA 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出