前言

尽管我更喜欢ZeroMQ,但Socket无处不在。

经常会用到它,在此做个梳理。

socket

诡异的翻译

Network socket被翻译作网络套接字,是不是一脸懵逼?我始终没搞懂套接字是什么意思, 中文里有这个词吗?它是音译吗? 就像沙发(sofa)? 看起来高深莫测。

在网络上搜了一圈,得出的结论是: 莫名其妙的翻译。我喜欢陈振玥在考证这个词出处时吐槽的:

其实我也很不喜欢这种生涩的译名,比如context设备上下文、handle句柄、macro宏,我小时候自己看计算机书籍好辛苦的,都是汉字但就是不知道在说什么。

socket的中文翻译是插座、灯泡插口、插孔。似乎可有看作接入电力系统的interface。从这个意义来说,Network socket的含义是非常清晰的: 网络插座, 网络的接口,就像插座是电力系统的接口。

ps: 台湾直接采用字面直译,把socket依然译成插座。

是什么

wikipedia给出的解释是很清晰的:

socket是一种操作系统提供的进程间通信机制。

socket接口是TCP/IP网络最为通用的API。

现代常见的socket接口大多源自Berkeley sockets标准。在socket接口中,以IP地址及通信端口组成socket address。

此外,一些细节值得注意:

在同一台计算机上,TCP协议与UDP协议可以同时使用相同的port而互不干扰。

简史

Socket Programming in Python (Guide)一文给出了Socket极简史:

Socket 有一段很长的历史,最初是在1971 年被用于 ARPANET,随后就成了 1983 年发布的 Berkeley Software Distribution (BSD) 操作系统的 API,并且被命名为 Berkeleysocket

测试工具

一个理想的测试工具,往往是学习相关领域知识的利器。

有测试工具在手,意味着可以随时动手来实践理论和尝试新想法,如《Mindstorms》说的,通过亲身操作,将知识内化

netcat(nc)ncat是非常理想的socket测试工具,其中netcat被称为网络工具中的瑞士军刀, 但我更偏好ncat(更清晰)。他们既可以轻松构建socket server,又可以轻松构建socket client。如果每次做实验都要一堆Python样板代码来运行client、server,而且许多参数还可能忘记,让人顿感疲惫,就不愿动手了。

socket client

为了让socket client能够工作,我们首先得有一个socket server。实际上今天的http server基本都是socket server。所以我们可以从这儿开始。

我们使用ncat作为socket client,请求模式为ncat <host/ip> <port>:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
➜  ~ ncat baidu.com 80
GET / HTTP/1.0

HTTP/1.1 200 OK
---
Date: Mon, 10 Jun 2019 10:15:29 GMT
Server: Apache
Last-Modified: Tue, 12 Jan 2010 13:48:00 GMT
ETag: "51-47cf7e6ee8400"
Accept-Ranges: bytes
Content-Length: 81
Cache-Control: max-age=86400
Expires: Tue, 11 Jun 2019 10:15:29 GMT
Connection: Close
Content-Type: text/html

<html>
<meta http-equiv="refresh" content="0;url=http://www.baidu.com/">
</html>

GET / HTTP/1.1是我们client给server的内容。http server将返回响应内容,这是一种请求、应答模式。

自建socket server

有很多中方式构建socket server,我们从一个最简单的开始,使用ncat构建一个tcp echo server。服务会将我们发送过去的任何信息回传。

ncat -l 8000 --keep-open --exec "/bin/cat"

新开一个shell,与serve通信:ncat 127.0.0.1 8000

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
➜  ~ ncat 127.0.0.1 8000
hello
hello
world
world
1
1
2
2
3
3
4
4

发送的所有消息都将被原样返回(echo)。

有了这个简易可靠的tcp echo server,之后测试socket client也要简单多了,不必再临时搭建socket server,到时出了问题,还要排查究竟是在server一侧还是client一侧。

当然你也可以使用在线的tcp echo server: tcpbin, 辅助测试。

socket client

有了socket server,就可以在实操中学习socket了。一般构建socket client开始。让我们暂时忘掉socket server, 只需要知道我们现在有了一个无论我们发送什么都会原样返回的服务即可。把精力集中在socket client上吧。

Pharo socket client

Pharo是smalltalk的现代方言,我现在喜欢在smalltalk里学习任何编程相关概念, 这种可探索的沉浸式编程环境提供了绝佳的学习体验。

Deep into Pharo一书的Chapter 4部分对Socket做了详细讲解

1
2
3
4
5
6
7
8
|localAddress clientSocket data|
clientSocket := Socket newTCP.
localAddress := NetNameResolver addressForName: '127.0.0.1'.
clientSocket
    connectTo: localAddress port: 8000;
    waitForConnectionFor: 10.
clientSocket sendData: 'Hello server'.
clientSocket receiveData.

Python socket client

Python官方文档里提供了最小例子:

1
2
3
4
5
6
7
8
9
import socket

HOST = '127.0.0.1'    # The remote host
PORT = 8000              # The same port as used by the server
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect((HOST, PORT))
    s.sendall(b'Hello, world')
    data = s.recv(1024)
print('Received', repr(data))

推荐使用jupyter做实验。

socket server

我们来展示使用代码构建socket server.

Pharo socket server

展示Deep into Pharo一书中的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
| connectionSocket interactionSocket receivedData |
"Prepare socket for handling client connection requests"
connectionSocket := Socket newTCP. connectionSocket listenOn: 8000 backlogSize: 10.
"Build a new socket for interaction with a client which connection request is accepted"
interactionSocket := connectionSocket waitForAcceptFor: 60.
"Get rid of the connection socket since it is useless for the rest of this example"
connectionSocket closeAndDestroy.
"Get and display data from the client"
receivedData := interactionSocket receiveData. receivedData crLog.
"Send echo back to client and finish interaction"
interactionSocket sendData: 'ECHO: ', receivedData. interactionSocket closeAndDestroy.

使用ncat与其交互:

1
2
3
➜  /tmp ncat 127.0.0.1 8000
hello
ECHO: hello

Python socket server

来自Python官方文档里提供的最小例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Echo server program
import socket

HOST = '0.0.0.0'                 # Symbolic name meaning all available interfaces
PORT = 8000              # Arbitrary non-privileged port
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind((HOST, PORT))
    s.listen(1)
    conn, addr = s.accept()
    with conn:
        print('Connected by', addr)
        while True:
            data = conn.recv(1024)
            if not data: break
            conn.sendall(data)

Python的非阻塞socket

由于Python的生态比Pharo成熟,对非阻塞/异步socket的支持,Python社区目前已经非常成熟。

因此我们,将在python中讨论非阻塞socket相关的内容。

Socket Programming in Python (Guide)一文围绕Python中的socket有许多精彩讨论。

multiconn-server.py是很有代表性的一个socket server,对其的讨论请参考前头的文章。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import sys
import socket
import selectors
import types

sel = selectors.DefaultSelector()


def accept_wrapper(sock):
    conn, addr = sock.accept()  # Should be ready to read
    print("accepted connection from", addr)
    conn.setblocking(False)
    data = types.SimpleNamespace(addr=addr, inb=b"", outb=b"")
    events = selectors.EVENT_READ | selectors.EVENT_WRITE
    sel.register(conn, events, data=data)


def service_connection(key, mask):
    sock = key.fileobj
    data = key.data
    if mask & selectors.EVENT_READ:
        recv_data = sock.recv(1024)  # Should be ready to read
        if recv_data:
            data.outb += recv_data
        else:
            print("closing connection to", data.addr)
            sel.unregister(sock)
            sock.close()
    if mask & selectors.EVENT_WRITE:
        if data.outb:
            print("echoing", repr(data.outb), "to", data.addr)
            sent = sock.send(data.outb)  # Should be ready to write
            data.outb = data.outb[sent:]


if len(sys.argv) != 3:
    print("usage:", sys.argv[0], "<host> <port>")
    sys.exit(1)

host, port = sys.argv[1], int(sys.argv[2])
lsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
lsock.bind((host, port))
lsock.listen()
print("listening on", (host, port))
lsock.setblocking(False)
sel.register(lsock, selectors.EVENT_READ, data=None)

try:
    while True:
        events = sel.select(timeout=None)
        for key, mask in events:
            if key.data is None:
                accept_wrapper(key.fileobj)
            else:
                service_connection(key, mask)
except KeyboardInterrupt:
    print("caught keyboard interrupt, exiting")
finally:
    sel.close()

为了理解multiconn-server.py,我们可以将其运行起来之后,使用多个客户端来测试它:ncat 127.0.0.1 8000

todo

  • 对Python非阻塞socket编程做更多细致的讨论。
  • 加入对stream的讨论。

参考