网络

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间路由”。

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

TCP 握手常见考点

# 连接建立

![](https://ws3.sinaimg.cn/large/006tKfTcly1ftppcaaoz7j30i40bdgn0.jpg)

上面的图说的已经很好了,不再赘述。

## 为什么需要三次握手

这主要是为了防止已失效的连接请求报文段突然又传送到了服务端,服务端建立一个新的连接,因而产生错误。

所谓已失效的连接请求报文段是这样产生的。A发送连接请求,但因连接请求报文丢失而未收到确认,于是A重发一次连接请求,成功后建立了连接。数据传输完毕后就释放了连接。

现在假定A发出的第一个请求报文段并未丢失,而是在某个网络节点长时间滞留了,以致延误到连接释放以后的某个时间才到达B。本来这是一个早已失效的报文段。但B收到此失效的连接请求报文段后,就误以为A又发了一次新的连接请求,于是向A发出确认报文段,同意建立连接。假如不采用三次握手,那么只要B发出确认,新的连接就建立了。

由于A并未发出建立连接的请求,因此不会理睬B的确认,也不会向B发送数据。但B却以为新的运输连接已经建立了,并一直等待A发来数据,因此白白浪费了许多资源。

采用TCP三次握手的方法可以防止上述现象发生。例如在刚才的情况下,由于A不会向B的确认发出确认,连接就不会建立。

## 如果在TCP第三次握手中的报文段丢失了会发生什么情况?

Client认为这个连接已经建立,如果Client端向Server写数据,Server端将以RST包响应,方能感知到Server的错误。

# 链接释放

![](https://ws2.sinaimg.cn/large/006tKfTcly1ftppdqkfzij311g0rok0s.jpg)

需要注意的是,TCP 是全双工的协议,因此链接建立之后就没有客户端服务器的概念了,两边是对等的,都可以释放链接

MSS(最长报文长度), 由两端的较短值决定, 在以太网中的典型值为1460, 是以太网的 MTU(1500)减去 IP 的头部 40B 得到的。

![](https://ws1.sinaimg.cn/large/006tKfTcly1ftpt8b0upyj30f40lc765.jpg)

## 为什么需要 TIME_WAIT 状态

首先是另一个概念 MSL, 最长报文生命周期, 在 BSD 系统上一般设定为30s, 不过可以长到 2min。TIME_WAIT 状态的时间设定为2MSL。

TIME_WAIT 是主动关闭端进入的状态, 发送完最后一个 ACK 之后进入 TIME_WAIT 状态等待 2MSL 才进入 CLOSED 状态

TIME_WAIT 状态存在的两个理由:

1. 可靠地实现 TCP 全双工链接的终止; 假如对方没能收到最后一个 ACK, 那么他将会重新发送 FIN, 这时候如果客户端已经关闭了显然不能再次回复 ACK 了.
2. 保证上一个相同连接的数据包已经在网络上消失; 如果一个新的链接建立在了同一个端口上, 那么他将可能收到上一个进程的数据包, 这是我们为老连接保留了2MSL 的 TIME_WAIT 值, 那么就可以保证原有的链接都不存在网络上了.

## 如果服务端主动关闭链接,也需要等待两个 MSL,那么重启服务器怎么绑定原有端口?

主动关闭链接端相应的端口会在 2 MSL 内处于 TIME-WAIT 状态而不能用。如果是客户端,那么问题不大,客户端一般会重新使用一个新的端口。如果是在服务端,因为服务端使用的都是常用端口,不能改变,也就是需要等待两个 MSL 才能使用刚拿的端口,比如 Nginx 重启,内核会显示当前端口是 busy 的,不能使用,等待 4min 显然是不现实的。为了解决这个问题,可以强制复用端口,在创建 socket 的时候使用 SO_REUSEADDR 就可以了。不过需要注意的是,SO_REUSEADDR 仅对 TIME_WAIT 状态有用,如果 socket 在其他状态,是不能复用的。注意不是 SO_REUSEPORT。

## 多个进程绑定同一个端口

我们知道一个同一个端口只有一个进程可以 bind 成功,那么 web 服务器是如何做到多进程呢?传统方法是在主进程使用 listen 然后在其他进程使用 accept 接受链接。不过这样会导致当有链接来的时候,所有的进程都会被唤醒,影响性能。在 3.9 内核之后,添加了 SO_REUSEPORT 属性,可以使用 SO_REUSEPORT 让每一个进程有当单独的监听队列,这样当有链接来的时候,内核只会唤醒一个进程。

## 当网络断开时,TCP 链接会断开吗?

TCP 链接有 keepalive 的功能,需要手动打开,但是一般来说没有人用。所以可以认为一般情况下,当物理网络断开时的时候,如果仅考虑 TCP 层,那么这个链接永远不会断开。不过一般来说,通过其他层面会知道这个链接断开了。比如说一般的应用层协议都会有心跳包的机制,这样就可以知道链接断开了。

![](https://img-blog.csdn.net/20150907214517068)

参考:

1. https://serverfault.com/questions/329845/how-to-forcibly-close-a-socket-in-time-wait
2. http://www.unixguide.net/network/socketfaq/4.5.shtml
3. https://blog.csdn.net/Yaokai_AssultMaster/article/details/68951150
4. https://www.zhihu.com/question/53672815
5. https://blog.qiusuo.im/blog/2014/09/14/linux-so-reuseport/
6. https://blog.csdn.net/xy010902100449/article/details/48274635
7. https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/

RSS 和 Atom 协议详解和不足

# 关于CDATA

CDATA stands for Character Data and it means that the data in between these strings includes data that could be interpreted as XML markup, but should not be.

So we could use CDATA to smuggle some HTML into the XML document, so that the HTML doesn’t confuse the XML document structure, and then use XSLT later to pull it out and spit it into a HTML document that is being output.
 
*In short, you don’t have to escape all the < and & in CDATA section*   # RSS 2.0 ```

   
        Example Feed
        Insert witty or insightful remark here
        http://example.org/         Sat, 13 Dec 2003 18:30:02 GMT
        johndoe@example.com (John Doe)
       
            Atom-Powered Robots Run Amok
            http://example.org/2003/12/13/atom03             Sat, 13 Dec 2003 18:30:02 GMT             Some text.
           Shit News
       

       
   


“`

## RSS 协议的一些不足和改进方向

1. 没有标识文章重要度的字段
2. 没有途径把订阅数量等信息反馈给 RSS 提供方
3. 没有品牌特性
4. 没有机器推荐
5. 如果能够把 RSS 包装成像是 Amazon Prime 那样的服务,用户可能会很愿意付钱

实际上文章的增删改查是一套组合操作,而只使用一个 RSS 作为列表显然是不够的,必然要拓展。

现代的 RSS 阅读器需要做三个方面

1. 一个社区
2. 能够把所有服务都提供RSS,包括不提供RSS的站点
3. 评论服务
4. 转码。有的 RSS 只提供了文章的摘要,有的 RSS 有实效性,有的 RSS 有自己的字体

 
 
# Atom 1.0

“`


    Example Feed
    Insert witty or insightful remark here
        2003-12-13T18:30:02Z
   
        John Doe
        johndoe@example.com
   

    urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6
 
   
        Atom-Powered Robots Run Amok
                urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a
        2003-12-13T18:30:02Z
       

Some text.

   
 

“`
 
# reference
 
[1] http://www.intertwingly.net/wiki/pie/Rss20AndAtom10Compared