网络

如何在 URL 中表示数组

我们知道 URL 后面的 query string 实际上是一个字典的形式。URL 的任何一个规范中都没有定义如何在 query 中传递数组,但是这个需求也是实际存在的,于是就诞生各种奇葩的形式,本文做一个总结。

常见的形式

http://www.baidu.com/search?q=url&tag=foo

这是一个正常的 URL,这里解析出来应该是一个字典 {"q": "url", "foo": "bar"}。但是 Python 会强行解析成数组 {"q": ["url"], "tag": ["foo"]}。

使用 URL 表示数组有以下几种常见形式:

http://www.baidu.com/search?q=url&tag=foo&tag=bar

重复键表示数组,Python/Node 中可以正确解析成数组,Java 只读取第一个值,PHP 只读取最后一个值。

http://www.baidu.com/search?q=url&tag[]=foo&tag[]=bar

键后增加[]并重复表示数组。PHP/Node 可以解析为 tag=[foo, bar]。Python 会解析成

PHP 的 http_build_query 会生成这种格式。

In [6]: from urllib.parse import parse_qs

In [7]: parse_qs("tag=foo&tag=bar")
Out[7]: {'tag': ['foo', 'bar']}

In [8]: parse_qs("tag[]=foo&tag[]=bar")
Out[8]: {'tag[]': ['foo', 'bar']}

In [9]: parse_qs("tag=foo")
Out[9]: {'tag': ['foo']}

http://www.baidu.com/search?q=url&tag[0]=foo&tag[1]=bar

使用数组形式表示。貌似没有原因能够处理,但是用的还挺多的。

http://www.baidu.com/search?q=url&tag=foo,bar

使用逗号分隔。貌似没有语言默认会处理这种,需要自己手工处理。但是我最喜欢这种。

一个更奇葩的例子

https://www.doi.gov/careers/explore-careers?f[0]=bureaus:20&f[1]=competencies:1638&f[2]=competencies:1642&f[3]=competencies:1648&f[4]=competencies:1656&f[5]=competencies:1661&f[6]=gs_levels:17&f[7]=gs_levels:158

总之,在不同的语言中,乃至于不同的 web 框架中对以上形式有不同的解析,非常混乱。

参考资料

  1. https://stackoverflow.com/questions/6243051/how-to-pass-an-array-within-a-query-string
  2. https://stackoverflow.com/questions/11889997/how-to-send-an-array-in-url-request/11890080
  3. https://stackoverflow.com/questions/1763508/passing-arrays-as-url-parameter
  4. https://stackoverflow.com/questions/1746507/authoritative-position-of-duplicate-http-get-query-keys

HAR 格式解析

HAR(HTTP Archive) 文件是一种常见的用来保存 HTTP 请求和响应的格式。本质上,HAR 文件其实就是一个 JSON 文件。

每一个 HAR Entry 都可以有以下记录存在:

– log
– creator
– browser
– pages
– pageTimings
– entries
– request
– queryString
– postData
– params
– response
– cookies
– headers
– content
– cache
– timings

# log

这个是一个 HAR 文件的根字段,其他字段都是该字段的子字段

“`
{
"log": {
"version" : "1.2",
"creator" : {},
"browser" : {},
"pages": [],
"entries": [],
"comment": ""
}
}
“`

# creator

“`
"creator": {
"name": "Firebug",
"version": "1.6",
"comment": ""
}
“`

# browser

同 creator 结构完全一样

# pages

“`
"pages": [
{
"startedDateTime": "2009-04-16T12:07:25.123+01:00",
"id": "page_0",
"title": "Test Page",
"pageTimings": {…},
"comment": ""
}
]
“`

## pageTimings

“`
"pageTimings": {
"onContentLoad": 1720,
"onLoad": 2500,
"comment": ""
}
“`

# entries

“`
"entries": [
{
"pageref": "page_0",
"startedDateTime": "2009-04-16T12:07:23.596Z",
"time": 50,
"request": {…},
"response": {…},
"cache": {…},
"timings": {},
"serverIPAddress": "10.0.0.1",
"connection": "52492",
"comment": ""
}
]
“`

## request

“`
"request": {
"method": "GET",
"url": "http://www.example.com/path/?param=value",
"httpVersion": "HTTP/1.1",
"cookies": [],
"headers": [],
"queryString" : [],
"postData" : {},
"headersSize" : 150,
"bodySize" : 0,
"comment" : ""
}
“`

### queryString

“`
"queryString": [
{
"name": "param1",
"value": "value1",
"comment": ""
},
{
"name": "param1",
"value": "value1",
"comment": ""
}
]
“`

### postData

“`
"postData": {
"mimeType": "multipart/form-data",
"params": [],
"text" : "plain posted data",
"comment": ""
}
“`

#### params

“`
"params": [
{
"name": "paramName",
"value": "paramValue",
"fileName": "example.pdf",
"contentType": "application/pdf",
"comment": ""
}
]
“`

## response

“`
"response": {
"status": 200,
"statusText": "OK",
"httpVersion": "HTTP/1.1",
"cookies": [],
"headers": [],
"content": {},
"redirectURL": "",
"headersSize" : 160,
"bodySize" : 850,
"comment" : ""
}
“`

### content

“`
"content": {
"size": 33,
"compression": 0,
"mimeType": "text/html; charset=utf-8",
"text": "\n",
"comment": ""
}
“`

### cookies

“`
"cookies": [
{
"name": "TestCookie",
"value": "Cookie Value",
"path": "/",
"domain": "www.janodvarko.cz",
"expires": "2009-07-24T19:20:30.123+02:00",
"httpOnly": false,
"secure": false,
"comment": ""
}
]
“`

### headers

“`
"headers": [
{
"name": "Accept-Encoding",
"value": "gzip,deflate",
"comment": ""
},
{
"name": "Accept-Language",
"value": "en-us,en;q=0.5",
"comment": ""
}
]
“`

## cache

“`
"cache": {
"beforeRequest": {},
"afterRequest": {},
"comment": ""
}
“`

### beforeRequest

“`
"beforeRequest": {
"expires": "2009-04-16T15:50:36",
"lastAccess": "2009-16-02T15:50:34",
"eTag": "",
"hitCount": 0,
"comment": ""
}
“`

## timings

“`
"timings": {
"blocked": 0,
"dns": -1,
"connect": 15,
"send": 20,
"wait": 38,
"receive": 12,
"ssl": -1,
"comment": ""
}
“`

# 参考资料

1. http://www.softwareishard.com/blog/har-12-spec/#response

网络协议概述

网络每层的头部基本上就是添加上本层的地址,还有一些校验和控制位

运输层的 MAC 地址是点到点的,没传递一次就会把发送者和接受者用 ARP 转换,替换为经过的路由器的MAC地址。而 IP 层的地址和端口号是端到端,从发送到结束始终不变。除非经过上层协议改变了地址和端口号,比如代理服务器或者 NAPT 路由器。

交换机没有任何地址,只有端口的概念。内部有一个转发表,记录了物理端口和MAC的对应关系,通过自主学习来建立。每台主机上都有自己的高速 ARP 地址缓存和路由表。可以通过 `ip n` 和 `ip r` 命令查看。

VLAN 可以理解为逻辑上将一台交换机分割成数台虚拟交换机,且这些虚拟交换机互不相通。Vlan 是广播域,而通常两个广播域之间由路由器相连接,广播域之间来往的数据帧通过路由器中继。因此 Vlan间的通信也需要路由器(或者三层交换机)提供中继服务,即“Vlan间路由”。

OAuth2 协议详解

今天有个项目需要用到 OAuth2 来处理一些东西,然而中文互联网有时候真是很难找到像样的文档,搜索 “OAuth 教程” 的到排名前两位的[教](https://aaronparecki.com/oauth-2-simplified/)[程](https://aaronparecki.com/oauth-2-simplified/)都是翻译自一个英文教程,翻译质量奇差无比就不说了,这个英文教程本身就是有问题的,无奈只好搜索 “OAuth tutorial” 才找到几个看得过去的英文教程,总结一下放在这里,算是为中文互联网引入一些正确的知识。

看到 OAuth2 这个词,一般人肯定会想,是不是还有个 OAuth 1 协议呢?是的,有 OAuth 1 协议,但是因为协议搞得太复杂了,所以没人用,市面上的基本都是根据 OAuth 2 来的。既然实际只有一个 OAuth,以下就简称 OAuth 了。

# 为什么要使用 OAuth —— 一个例子

大家最熟悉的例子就是第三方登录了。假设有个论坛叫做“91论坛”你没有注册过,也懒得填写邮箱,然后验证邮箱注册,那么这时候可以使用 QQ 登录,当然国外可能是 Facebook。那么问题来了,当你点击 “用 QQ 登录” 这个按钮的时候,论坛怎么安全地知道你使用的是哪个 QQ 号呢?会有下面几个问题:

1. 如果你随便输入一个 QQ 号,然后91论坛就信任了,那么你就可以伪造任意的 QQ 用户了,所以论坛需要去向 QQ 验证你是否是你提供的 QQ 号的所有者。
2. 你可以提供给论坛你的 QQ 号和密码,这样论坛使用你的 QQ 号和密码测试一下能否登录就可以了,但是这样论坛就有了你QQ号的所有权限,如果论坛偷偷在你的 QQ 空间发推广消息呢?所以你不希望直接把 QQ 号和密码都告诉论坛。
3. 现在陷入了两难境界,论坛无法信任你只提供 QQ 号,你也不能信任论坛拿走你的账户密码。如果这时候能让 QQ 作为中间人只提供给论坛部分信息就好了,OAuth 就是用来做这个的。

# OAuth2

简单来说,方案如下:

1. 91 论坛在QQ上注册一个app
2. 用户在QQ上登录,通过跳转,把一个一次性授权码给 91 论坛
3. 论坛利用这个授权码获得 access token,然后利用这个 token 读取用户信息

具体解决方案如下:

1. 91论坛的开发者在 QQ 处申请一个开发者账户,获得一个开发者标识,并提供了一个回调接口:

“`
{
‘client_id’: 91bbs,
‘client_secret’: 123456,
‘callback’: “http://91bbs.com/login_callback”
}
“`

2. 你在91论坛上点击用 QQ 登录,然后页面跳转到 QQ 域(qq.com)下,这样你可以安全的输入 QQ 密码,而不用被91论坛知道。

用 QQ 登录对应的地址:

“`
https://api.qq.com/v1/auth?
response_type=code&
client_id=91bbs&
callback=http://91bbs.com/login_callback&
scope=read
“`

注意其中标识了论坛在上一步 client_id。在这个页面上可能写着你是否授权XX论坛访问你的个人信息等等。

1. response_type 表示授权的类型,后面会讲到
2. client_id 向 QQ 表明是要登录91论坛这个网站
3. callback 指明了下一步QQ要回调91论坛的地址
4. scope 指定了当前授权的权限范围

3. 登录QQ后,点击授权通过,然后 QQ 会把你重定向到 redirect_uri 对应的页面,并附加参数 code=xxx,这个是一个临时的一次性授权码。

重定向到的页面:

“`
http://91bbs.com/login_callback&code=xxxxxx
“`

4. 访问这个页面,就会把这个 code 传递给91论坛,但是91论坛有了这个 code 还不能直接向 QQ 询问关于你的具体信息。
5. 91论坛使用这个 code 向 QQ 申请一个 access token,使用这个 access token 就可以获取你的 QQ 号等信息,具体获得什么信息,是在第二步的 scope 页面指定的。

访问:
“`
POST https://api.qq.com/v1/token
grant_type=authorization_code&
code=AUTH_CODE_HERE&
redirect_uri=REDIRECT_URI&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET
“`

注意其中的参数:
1. grant_type 指定了授权的类型,这里我们使用上一步获得的 authorization code 来获取 access token,所以grant type 就是 authorization code
2. code 就是上一步获得的 authorization code
3. 其他参数和上一步类似

QQ 返回给 91 论坛的信息:
“`
{
“access_token”:”ACCESS_TOKEN”,
“token_type”:”bearer”,
“expires_in”:2592000,
“refresh_token”:”REFRESH_TOKEN”,
“scope”:”read”,
}
“`

因为这个 access token 可以随时用来访问你的信息,所以设定了过期时间,这样即使泄露了攻击的时间窗口也不会很长。

6. 91论坛使用 access token 访问你的信息。access token 通常是放在 [Authorization](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) 这个 header 中。

比如使用 curl 来表示这个访问:

“`
curl -H ‘Authorization: Bearer 1.1Zgwxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=’ \
‘http://api.qq.com/v1/user/123456’
“`

如果 token 正确无误的话,QQ 服务器会返回相应的信息。

6. 论坛根据从 QQ 服务器得到的消息,从而知道你真的是 QQ 为 123456 的用户,然后为你创建账户。以后你需要登录也可以重复上面的流程,证明你的确是 QQ 123456 的用户就可以了。

# OAuth 中的术语

在上面的过程中,一共出现了四中角色:

1. 第三方程序,也就是 91论坛
2. 资源所有人,也就是用户
3. 授权服务器,也就是 QQ
4. 资源服务器,还是 QQ

其中资源指的就是用户的 QQ 信息,而授权服务器和资源服务器在复杂的结构中往往是分开的。

# 其他的授权类型

除了上面说过授权类型之外,还有一些微小差异的授权类型,比如 implict 授权,这里不再赘述。

除此之外,还可以直接使用账户密码获得 access token,方法比较简单,一般用于官方客户端直接登录:

“`
POST https://api.authorization-server.com/token
grant_type=password&
username=USERNAME&
password=PASSWORD&
client_id=CLIENT_ID
“`

当 access token 过期后,还可以使用 refresh token 刷新,获得新的有效的 access token,而不需要用户再次登录。虽然 refresh token 没有过期时间,或者过期时间远比 access token 长,但是因为使用次数少,所以也是相对比较安全的。

“`
POST https://cloud.digitalocean.com/v1/oauth/token?
grant_type=refresh_token&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET&
refresh_token=REFRESH_TOKEN
“`

# 参考

1. [OAuth2 Simplified](https://aaronparecki.com/oauth-2-simplified/)
2. [Introduction to OAuth2](https://www.digitalocean.com/community/tutorials/an-introduction-to-oauth-2)
3. [Refresh token](https://medium.com/@bantic/more-oauth-2-0-surprises-the-refresh-token-1831d71f4af6)

Get 和 Post 方法的选择和URL的设计

HTTP 中常用的方法有 GET/POST/PUT/DELETE 等,在设计API或者表单的时候我们需要选择合适的方法。一般有两种方案:

1. 只使用 GET 和 POST,GET 主要用来读取数据,POST 用来创建或者更新数据。
2. RESTful的方法,GET/POST/PUT/DELETE 分别用来增删改查。

# URL 的设计

为了探讨两种方案,首先我们来看一下 URL 的设计。URL 是 Universal Resource Locator 的缩写,也就是一个 URL 表示的是唯一的一个资源,所以这个资源的 id 或者说主键应该是放在 URL 路径中的。

比如一个好的设计:

“`
http://example.com/post/1234
“`

不好的设计

“`
http://example.com/post?id=1234
“`

而控制这个资源展示方式的其他字段可以作为参数:

“`
http://exmaple.com/post/1234?lang=zh&utm_source=google
“`

# HTTP 方法的含义

好多人对于 http 方法的理解是 GET 是参数在url里,而POST是参数在 body 里面,这样理解是不对的。

在上述的两种方案中,GET 都是用来读取资源的,一般来说不要对资源进行任何更新操作,也就是没有副作用。比如说

不好的设计:
“`
GET http://example.com/post/1234?action=delete
“`

上面的设计意图通过GET操作来删除一个资源,这样非常不好。比如说如果浏览器具有预缓存页面的功能,那么预先读取这个链接的时候就把对应的资源删掉了。

一般来说,GET 方法还要求幂等性,也就是无论多少次操作,最终结果和操作一次都是一样的。GET 操作的参数受到 url 长度的限制,当参数超过 1k 的时候,可以使用 POST 代替。不过这时候你首先应该想一下这么多参数是不是都有用,是不是设计有问题。

POST 方法可以用来创建资源,比如说:

“`
POST http://example.com/post/

content=xxxxxxx&author=xxxx&date=xxxx
“`

POST 操作具有副作用,也就是说会更改服务器上的状态。另外 POST 操作一般不是幂等的,每次 POST 操作都应该创建一个新的资源。

PUT 操作用来更新资源,也是幂等的。

“`
PUT http://example.com/post/1234

content=yyyyyy
“`

DELETE 用来删除资源,值得注意的是,根据规范 DELETE 方法不能带有 body。

“`
DELETE http://example.com/post/1234
“`

curio asks 源码解析

asks 是 Python 的异步框架 curio 中的 一个 http 库。它基于 h11 这个库来做 http 协议的解析,然后提供了在 curio 下的 IO 操作。下面按照功能逐个介绍其中的每个部分。

杂项

auth.py

该文件中主要包含了 http auth 相关函数, 支持了 Basic Auth 的 Digest Auth。值得注意的是,digest auth 作为一种既很复杂又不安全的认证方式,已经没有人用了。如果需要使用 http auth 的话,现在推荐的方式使用 https + basic auth。

base_funcs.py

提供了一些快捷方式函数,比如 curio.get。

cookie_utils.py

该文件主要包含了 CookieTracker, 对外的方法主要有两个 get_additional_cookies 用于获取域名对应的 cookie,_store_cookies 用于添加 cookie。

parse_cookies 函数主要用于解析 set-cookie 头部,并把解析到的 cookie 附加到 response 对象上。

errors.py

asks 中抛出的异常的类

http_utils.py

处理编码和压缩的两个函数。

请求与响应

request_object.py

该文件中主要是定义了 RequestProcessor 类。RequestProcessor 用于生成一个 HTTP 请求。

make_request 方法。hconnection定义和使用的地方相距太远了。cookie的生成应该使用join。之后调用 _request_io 发送请求

_request_io 调用 首先掉用 _send, 然后调用 _catch_response

_catch_response 调用 recv_event

_recv_event 不断调用 _async_lib.recv(self.sock, 10000) 从而不断产生数据,知道读完为之

sessions.py

session 类

request 调用 grab_connection 获取一个socket,然后把这个socket交给Request对象
grab_connection 调用 checkout_connection 获得一个socket或者,调用make_connection产生一个新的socket,注意其中有一个奇怪的 await sleep(0),可能意思是把循环交回给event loop

make_connection 调用 _connect 方法,并把host和port作为属性写到socket上

session 中有两个SocketQ的类,conn_pool, checked_out_sockets 分别用来保存已连接未使用的socket和正在使用中的socket

response_objects.py

Response 表示了一个响应。如果在发起请求的时候选择了 stream=True, response.body 会是一个 StreamBody 实例,StreamBody 用于流式处理响应。

Cookie 类表示了一个 Cookie,不知道这里为什么没有用标准库的 cookie。

Connection Pool

如果使用代理的话

req_structs.py

SocketQ 是一个 socket 的连接池。使用一个 deque 作为存储,实际上相当于又模拟了一个字典 {netloc => socket}(思考:为什么不使用OrderedDict呢?)index 返回指定 hostloc 对应的 index。pull 弹出指定 index 的 socket。__contains__ 遍历看是否包含对应的socket。需要注意的是这个类不是线程安全的,不过对于 curio 来说,线程安全似乎无关紧要,毕竟只有一个线程。

CaseIncesitiveDict 是一个对大小写不敏感的词典,直接从 requests 里面拿过来的。

curio 的网络通信

首先,需要引入curio.socket 而不是使用内置的socket

TCP通信,使用 sock.bind/listen/accept 等建立服务器,使用recv和sendall发送接收消息。
UDP通信,使用recvfrom和sendto函数通信

作为客户端使用 curio.open_connection 打开到服务器的链接,其中 ssl=True打开的是HTTPS连接诶

对于其他要使用ssl的情况,应该使用curio.ssl而不是标准库的ssl

curio.network.

ssl.wrap_socket 不支持server_hostname sslcontext.wrap_socket 支持

不要把 proxy 传递给 request 对象

添加 http 代理支持

asks 把繁重的 http 解析工作都用 h11 这个库巧妙的解决了,所以 asks 本身是非常轻量的一个库。很遗憾的是,在 asks 中尚未支持代理,下面我们尝试为 asks 增加 http 代理的支持 😛

在 http 代理模式中,每个请求都需要添加上 Proxy-Authorization 的 Header。而在 https 请求中,只有 Connect 的时候需要添加上 Proxy-Authorization 的 Header。

代理的 socket 池和真正的 socket 池要分开,这样设计还简单一点。

Linux 上的 DNS 缓存

Linux 内核中没有 DNS 缓存

Firefox 内置了 DNS 缓存

`nscd` 可以提供本地的 DNS 缓存,好多机器开了,但是据说这个服务有很多问题。

Python 使用了 `getaddrinfo` 函数,会使用系统的 DNS 缓存

像 `nslookup` 和是dig这样的工具会 bypass 掉 DNS 缓存。

另外 Go 语言好像也不会使用本机的 DNS 缓存,即使开了

https://wiki.archlinux.org/index.php/dnsmasq 可以用来做本地缓存

还可以使用systemd提供的resolved

1 https://stackoverflow.com/questions/11020027/dns-caching-in-linux

Linux 中的 epoll 和 nginx 中的应用

# epoll 的优势

select 和 poll 每次获取可读写的描述符都需要遍历所有的文件描述符,它们的时间复杂度都是 O(n),而 epoll 是基于回调的,每个 socket 上有事件发生都会调用回调函数放到 epoll 的就序列表中,因此 epoll_wait 只需要简单地读取这个列表,所以epoll的时间复杂度是 O(1) 的。

添加监控的socket只需要使用 epoll_ctl 添加一次,而获取消息 epoll 使用 mmap 加速内核与用户态的消息传递,不需要每次都把 socket 在内核态和用户态之间考来考取。

## epoll 的工作模式

epoll 中有两个模式,水平触发(LT)和边缘触发(ET)。其中水平触发如果不做任何操作,就会一直触发,而边缘触发只会触发一次。就好比电工电子里面的两种触发模式。默认模式是 LT

Level Triggered (LT) 水平触发

1. socket接收缓冲区不为空 有数据可读 读事件一直触发
2. socket发送缓冲区不满 可以继续写入数据 写事件一直触发

符合思维习惯,epoll_wait返回的事件就是socket的状态

Edge Triggered (ET) 边沿触发

1. socket的接收缓冲区状态变化时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件
2. socket的发送缓冲区状态变化时触发写事件,即满的缓冲区刚空出空间时触发读事件

仅在状态变化时触发事件

ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死(在while循环中调用read、write、accept,若是阻塞套接字,当资源不够时,进程会被阻塞,则其他准备就绪的文件描述符得不到处理,如果是非阻塞套接字,当资源不够时,上述系统调用返回-1,同时将errno设置为EAGAIN)

LT模式下开发基于epoll的应用要简单些,不太容易出错。而在ET模式下事件发生时,如果没有彻底地将缓冲区数据处理完,则会导致缓冲区中的用户请求得不到响应。

ET处理EPOLLOUT(socket 可写事件)方便高效些,LT不容易遗漏事件、不易产生bug。如果server的响应通常较小,一次性可以写完,不需要监听EPOLLOUT,那么适合使用LT,例如redis等、或者大多数的网络库。而nginx作为高性能的通用服务器,网络流量可以跑满达到1G,这种情况下很容易触发EPOLLOUT,则使用ET。关于某些场景下ET模式比LT模式效率更好,

## nginx 中的使用

nginx 使用的是边缘触发模式

epoll 常用于构建事件驱动的非阻塞异步的事件循环,但是需要注意,本身 epoll_wait 这个操作是同步的。elect/poll/epoll的意义在于同时等待多个socket上的活动。select/poll/epoll永远都是阻塞的,跟socket是否阻塞无关。当然一般来说 epoll 管理的 socket 要设置成非阻塞的。

nginx会一直(阻塞)等待epoll返回事件通知或者epoll_wait超时,一旦有事件触发,nginx就会调用关联的(read/write)handler处理事件。开发者必须保证每一个事件handler都不得包含任何阻塞调用。否则,nginx worker的主线程将会因为一个事件阻塞,导致队列里面可能还有一大堆事件不能及时处理,这会严重影响nginx的效率。所以 nginx 的 socket不能设置为阻塞的,如果socket是阻塞的,那么一个socket的IO事件就会阻塞后续所有的事件处理,CPU就会空转,等在那里没事干了。而在socket非阻塞调用期间,nginx可以继续处理其他的事件。

# epoll 的使用

epoll 总共有三个系统调用:epoll_create, epoll_ctl, epoll_wait。其中 epoll_create 在内核中创建一个 eventpoll 结构体,epoll_ctl 增加或者删除 socket 到 epoll 中,epoll_wait 等待事件发生。

创建 epoll 文件描述符
“`
int epoll_create(int size); // size 参数会被忽略
“`

添加 socket 到 epoll 对象中

“`
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
“`

1. epfd 是 epoll_create 创建的
2. op 是这三种常量,表示操作。
1. EPOLL_CTL_ADD:注册新的fd到epfd中;
2. EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
3. EPOLL_CTL_DEL:从epfd中删除一个fd;
3. fd,需要更改的 socket
4. events 是一些参数

监听事件

“`
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
“`
一般如果网络主循环是单独的线程的话,可以用-1来等(即阻塞调用epoll_wait),这样可以保证一些效率,如果是和主逻辑在同一个线程的话,则可以用0(立即返回)来保证主循环的效率。

参考:

1. https://blog.csdn.net/dongfuye/article/details/50880251
2. https://www.zhihu.com/question/63193746
3. epoll 详解:https://www.cnblogs.com/ljygoodgoodstudydaydayup/p/3916760.html
4. https://www.zhihu.com/question/22576054

HTTP 认证介绍

周末给一个库添加http代理的支持,发现对http basic auth不甚了解,阅读了一下相关的文档,写篇备忘。

http 中的认证主要是 basic auth 和 digest auth 两种,其中 digest auth 比较复杂,而且也没有提升安全性,已经不建议使用了。

RFC 7235 [1] 描述了客户端(通常是浏览器)和服务器如何通过http进行身份认证的一些机制。客户端和 http代理之间也可以使用 http auth 来做验证。
 
# 验证流程

1. 当客户端访问一个页面时,如果只有验证后才能访问,或者验证后有更多内容,服务器应该发送 401 Unauthorized,提出一个chanllenge,设定 `WWW-Authenticate` header,并指定验证的 type 和 realm,具体定义下文有讲。
2. 客户端这时通常应该提示用户输入密钥,一般是浏览器弹出用户名密码对话框供用户填写,然后使用`Authorization` header发送验证的密钥。如果验证通过的话,应该正常访问(200 OK),验证通过但是没有权限的话应该返回 403 Forbidden。
3. 如果验证不通过,应该服务器返回401,客户端可以重试。

注意,如果客户端已经知道需要密钥访问,那么可以在第一个请求直接发送对应的密钥,这样就避免了 401 Unauthorized。

![MDN上的流程图](https://mdn.mozillademos.org/files/14689/HTTPAuth.png)
 

# 代理验证的不同

如果代理服务器需要验证的话,流程是类似的,有两点细节不同:

1. 代理服务器应该发送407 Proxy Authentication Required 而不是 401。使用的headers也变成了 Proxy-Authenticate 和 Proxy-Authorization 。
2. 服务器的头部 WWW-Authenticate 是 end-to-end 的,也就是代理服务器不应该篡改,应该原样传递。而代理服务器的Proxy-头部是 hop-by-hop 的,也就是不能向下传递。

# 实现细节

服务器或者代理服务器随着4XX发送的头部为

“`
WWW-Authenticate: <type> realm=<realm>
or
Proxy-Authenticate: <type> realm=<realm>
“`

其中 type 指定了使用的验证的类型,也就是用户名和密码加密方式的不同,IANA钦定了一批方法[2]。然鹅,一般来说常用的只有两个 Basic 和 Digest。而其中 Digest 的实现可能会要求服务器明文存储密码,于是大家又angry了[3],这里也不推荐使用。所以这里只介绍 Basic类型。

realm 指定了验证的领域,也就是说相同realm下的用户名和密码是一样的,如果你访问的两个页面在同一个realm,那么浏览器在第二次访问就不会问你密码了。

客户端发送对应的头部和密钥来获得访问权限

“`
Authorization: <type> <credentials>
or
Proxy-Authorization: <type> <credentials>
“`

其中,type就是刚刚的那个 Basic 或者 Digest。credentials 按照对应的方法计算。对于Basic类型 `credentials = base64(username + ‘:’ + password)`

一个例子,假设用户名和密码分别是:aladdin和opensesame。那么客户端应该发送的header是:`Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l`

# 需要注意的地方

1. 因为http协议本身是无状态的,所以Auth应该是无状态的,所以每次请求都应该携带。
 
2. 如果是http协议的话,对于Basic Auth,那么密码都是明文发送的,可以使用https来避免这个问题。

3. 可以使用:https://username:password@www.example.com/ 这种形式来预先输入账号密码,但是这种形式已经不鼓励了。不过在设定一些环境变量时,比如 http_proxy,也只能用这种方法来制定用户名和密码

参考:

1. 对应的RFC https://tools.ietf.org/html/rfc7235
2. IANA 注册的auth类型 http://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml
3. 为什么不要使用digest验证  https://stackoverflow.com/questions/2384230/what-is-digest-authentication
4. MDN的文章还提供了如何让apache和nginx使用basic auth https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication