8
19
2015
17

一个人也可以建立 TCP 连接呢

人们对 TCP 的误解

因为我们的教育总是只教人「怎么做」,而根本不管「为什么这么做」,所以造成了很多误解。

今天(恰巧是今天)看到有人在 SegmentFault 上问「TCP server 为什么一个端口可以建立多个连接?」。提问者认为 client 端就不能使用相同的本地端口了。理论上来说,确定一条链路,只要五元组(源IP、源端口号、目标IP、目标端口号、协议)唯一就可以了,所以这不应该是技术限制。而实际上,Linux 3.9 之后确实可以让客户端使用相同的地址来连接不同的目标,只不过要提前跟内核说好而已。

当然,你不能使用同一个 socket,不然调用connect连接的时候会报错:

[Errno 106] (EISCONN) Transport endpoint is already connected

man 2 connect里说了:

Generally, connection-based protocol sockets may successfully connect() only once; connectionless protocol sockets may use connect() multiple times to change their association.

想也是,一个 socket 连接到多个目标,那发送的时候到底发给谁呢?TCP 又不像 UDP 那样无状态的,以前做过什么根本不管。

那用多个 socket 就可以了嘛。服务端其实也一直是用多个 socket 来处理多个连接的不是么,每次accept都生成个新的 socket。

>>> import socket
>>> s = socket.socket()
# since Linux 3.9, 见 man 7 socket
>>> s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
>>> s2 = socket.socket()
>>> s2.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
>>> s.bind(('127.0.0.1', 12345))
>>> s2.bind(('127.0.0.1', 12345))
# 都可以使用同一本地地址来连接哦
>>> s.connect(('127.0.0.1', 80))
>>> s2.connect(('127.0.0.1', 4321))

连上去之后 netstat 的输出(4568 进程是上边这个程序,另两个进程一个是 nginx,另一个是我的另一个 Python 程序):

>>> netstat -npt | grep 12345
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
tcp        0      0 127.0.0.1:4321          127.0.0.1:12345         ESTABLISHED 18284/python3
tcp        0      0 127.0.0.1:12345         127.0.0.1:4321          ESTABLISHED 4568/python3
tcp        0      0 127.0.0.1:80            127.0.0.1:12345         ESTABLISHED -
tcp        0      0 127.0.0.1:12345         127.0.0.1:80            ESTABLISHED 4568/python3

当然你要是连接相同的地址会报错的:

OSError: [Errno 99] Cannot assign requested address

那个五元组已经被占用啦。

同时创建连接:恰巧你也在这里

有时候,我们不能一个劲地等待。主动出击也是可以的,即便对方并没有在等待。

这个在 TCP 里叫「simultaneous open」,用于 TCP 打洞。但是比起 UDP 打洞难多了,因为那个「simultaneous」字眼:必须同时调用connect,双方的 SYN 包要交叉,早了或者晚了都是会被拒绝的。

所以手工就办不到啦,在本地测试也不容易办到。我本地的系统时间是使用 NTP 同步的,再用一个时钟也和 NTP 同步的 VPS 就可以啦,我这里延迟 80ms 左右,足够那两个 SYN 「在空中会面」了。以下是代码:

#!/usr/bin/env python3

import time
import sys
import socket
import datetime

def wait_until(t):
  deadline = t.timestamp()
  to_wait = deadline - time.time()
  time.sleep(to_wait)

s = socket.socket()
s.bind(('', 1314))

if sys.argv[1] == 'local':
  ip = 'VPS 的地址'
else:
  ip = '我的地址'

t = datetime.datetime(2015, 8, 19, 22, 14, 30)
wait_until(t)
s.connect((ip, 1314))

s.send(b'I love you.')
print(s.recv(1024))

当然,我是公网 IP。在内网里包就不容易进来啦。

然后双方在约定的时间之前跑起来即可,结果是这样子的:

# 本地
>>> python3 t.py local
b'I love you.'

# VPS 上
>>> python3 t.py remote
b'I love you.'

一个人也可以建立 TCP 连接呢

如果你没有 VPS,或者没有公网 IP,也是有活动可以参与的哦。即使只有一个 socket,也可以自己连接到自己的:

>>> import socket                                                               
>>> s = socket.socket()
>>> s.bind(('127.0.0.1', 1314))
>>> s.connect(('127.0.0.1', 1314))
>>> s.send(b'I love you.')
11
>>> s.recv(1024)
b'I love you.'

netstat 输出:

>>> netstat -npt | grep 1314
tcp        0      0 127.0.0.1:1314          127.0.0.1:1314          ESTABLISHED 8050/python  
Category: 网络 | Tags: python 网络 tcp 爱情
11
21
2013
8

虾米歌词下载、Python Requests 库,以及 HTTP Keep-Alive

Requests

这是我第二次用 Requests 了。上一次是个下小说的脚本。我已经不记得自己为什么路过了 httplib2,也路过了 urllib3,却最终买了 Requests 的账。也许是不喜欢 httplib2 那个 Google Code 的首页,也许是厌倦了 urllib* 这种名字。不过我想更多的是开门见山的首页,以及开篇那段让人无法拒绝的介绍:

Requests is an Apache2 Licensed HTTP library, written in Python, for human beings.

Python’s standard urllib2 module provides most of the HTTP capabilities you need, but the API is thoroughly broken. It was built for a different time — and a different web. It requires an enormous amount of work (even method overrides) to perform the simplest of tasks.

Things shouldn’t be this way. Not in Python.

相比之下,中文翻译太差了(而且已经陈旧了)。我在这里再译一个版本:

Requests 是使用 Apache2 许可证的 HTTP 库。用 Python 编写,为人类编写。

Python 标准库中的 urllib2 模块提供了你所需要的大多数 HTTP 功能,但是它的 API 烂出翔来了。它是为另一个时代、另一个互联网所创建的。它需要巨量的工作,甚至包括各种方法覆盖,来完成最简单的任务。

事情不应该是那样的,在 Python 世界里。

Requests 使用的是 urllib3,因此继承了它的所有特性。Requests 支持 HTTP 连接保持和连接池,支持使用 cookie 保持会话,支持文件上传,支持自动确定响应内容的编码,支持国际化的 URL 和 POST 数据自动编码。现代、国际化、人性化。相见恨晚

Keep-Alive

以前一直以为 Keep-Alive(连接保持)就是节省了 TCP 连接建立的时间。直到渐渐了解了 TCP 慢启动。直到自己偶然间亲自对比了一次。

使用 httrack 默认参数下载 PostgreSQL 9.3 文档,一千多个页面,29 分钟。后来才注意到 httrack 的 Keep-Alive 支持要写参数手动启用。

使用 wget,默认会使用 Keep-Alive 来复用已有连接。几乎同样的页面,只花了 12 分钟

urllib3 说 它使用 Keep-Alive 单连接从 Google 下载 15 个页面比使用 urllib 每次建立新连接快了一倍。我这里的结果比它的测试还要好呢。

也许,支持 Keep-Alive,是我的 nvchecker 使用 pycurl 要快很多的很重要的一个原因吧。

其它

可惜的是,Requests 不支持 Tornado 的异步调度框架。不过还好,那边我可以用 libcurl,虽然 API 不 Pythonic,至少 cookie 管理和连接管理都很健全。

对了,虾米歌词下载脚本还是在老地方,歌词的获取参考了 you-get 的代码。Requests 的作者 Kenneth Reitz 也有一些其它有意思的东西,包括之前我发现但没意识到是他做的的、用于 HTTP 客户端测试的 Httpbin 网站。

Category: python | Tags: python tcp http

Mastodon | Theme: Aeros 2.0 by TheBuckmaker.com