Socket 编程

Socket 相关的编程,一般也叫 网络编程。为什么这样叫?因为 Socket 的存在,基本上就意味着联网了。
当你不明白 Socket 是什么的时候,对 网络编程 这个名词可能产生很多其它的联想,这是专有术语作为自然文本语言表达不精准导致的。以此为例,未来你可能会碰到其它不少技术领域的专有术语,本身可能只是简单的意思,反而会因为专有术语的存在,而多了一些费解。
对了,Socket 的中文翻译,叫 套接字 ... 所以,忘记这个翻译吧,理解 Socket 是什么东西,且记住 Socket 这个英文的名词就可以了。

写点 Demo

写一个服务端

import socket
server_address = ('127.0.0.1', 10199)

def run_server():
    s = socket.socket()
    s.bind(server_address)
    print('server is started!')
    s.listen(50)  # 设定连接排队数
    while True:
        connection, addr = s.accept()  # 建立连接
        print('connected from {0}'.format(addr))
        connection.send('Hello World from server')
        connection.close()

if __name__ == '__main__':
    run_server()

你应该是在 PyCharm 中运行这个程序,那么可能需要先准备一个文件夹,作为一个临时尝试的项目目录,然后在 PyCharm 中 Open...
上面的代码,逐行看下来应该就能明白,整个服务端的逻辑就是: 等待客户端连接进来,然后告诉客户端 Hello World from server ,之后就直接关闭连接了。
While True 这里就是一个死循环,但是也不用担心,因为 s.accept() 会 block 住整个循环,只有 s.accept() 后,也就是 connection 进来了,循环才会继续下去。有些时候,我们也会在代码中写成 Demo 中类似的『死循环』,但为了避免进入无限 (吃掉性能) 的死循环,一般也会手工增加一个 time.sleep(1)

如果遇到 socket.error: [Errno 48] Address already in use 的提示,说明已经有程序占用当前的端口了,也有可能是之前运行了本 Demo 程序但未完全退出,这个时候可以打开 MacOS 自带的活动监视器 (Monitor) 中关闭 python 的进程。

也可以使用命令行进行操作,先用 lsof -i:具体的端口 找到占用端口程序的 PID (Progress ID),然后再杀死这个进程就可以了。

关于 address 的补充说明: 在 Demo 中,我们声明 server_address = ('127.0.0.1', 10199),127.0.0.1 是 IP, 10199 是端口,127.0.0.1 这个 IP 只有本地能访问到。如果改成 0.0.0.0 则是全局能访问到;如果留空 (空字符'') 效果等同 0.0.0.0;如果本地有多个 IP,且指定某个 IP,则只有访问到这个 IP 的 connection 才能进来。 再则,并非一定都是 IP+Port 的模式,如果是本地系统,可能就是一个 .sock 的文件路径,它走的并非 TCP 协议,而是 IPC (Unix domain socket),倒不用去深究,大概知道有这么一回事就可以了。

写一个客户端

当我们真正去写一个服务端的 Socket 服务时,客户端并不是必须的。
一般作为教程而言,一个 server 端的 demo,也必然意味着一个 client 端的 demo,但是,我们要注意,两者并非密不可分的逻辑。
比如说,一个 websocket 的服务端由 A module 来实现,而对应的 client 端可能就是 B module 来实现,因为它们在实际的使用场景未必相同,也就各有倾向了。
再举一个例子,比如说 MySQL (一个数据库软件) 安装并运行了,我们使用 Python 去调用它,本质上就是一个 client 端的,而且也是通过 socket 的逻辑去连接服务端进行数据交换。此时,server 端和 client 端就相当于各自独立的东西了。

我们来看 Socket 客户端的 Demo 吧,也是简单的:

import socket
server_address = ('127.0.0.1', 10199)

def connect_to_server():
    s = socket.socket()
    s.connect(server_address)
    # Demo 的 server 端会自动断开连接,但一般实际应用场景会由客户端往服务端发信息
    # s.send('hello from client.')
    # print s.recv(1024)
    # s.close()

if __name__ == '__main__':
    for i in range(10):
        connect_to_server()

一些补充

Demo 代码中需要注意的地方

  1. 真正服务端的产品,我们是不会使用 print 这个函数的,它仅限于开发、Debug 时使用,或者临时用用。
  2. 一定会使用异步的模式,比如说使用 Gevent,以避免多个 connections 逐次执行且需要等待。
  3. 一般来说,真正需要手写 Socket 逻辑的场景,对多数人而言,是很少见的,通常已经由第三方 module 处理掉了。

什么时候需要用到 Socket 编程?

凡是通过 socket 进行数据传输的,都是 Socket 编程的范畴;但一般来说,这些 Socket 的数据传输都是技术的基础层面,直接使用某个 module 就可以了,不需要重新再去写代码处理 Socket 相关的逻辑。
还有情况比较特殊,就是构建一个相对私有的网络通讯、数据同步,那么 socket 之间如何传输数据、如何加密解密,就需要代码中对 Socket 相关的内容做深度定制。
但一般来说,Socket 的编程,从产品整体的角度来看,大多是偏底层性质的,换句话说,大部分时候,我们是不用去写这部分内容的。

很多 drive 本质上就是连 Socket

上文用 MySQL 举了一个例子,说明很多 drive 都是连接 socket 来实现 API 调用的。所谓的 drive (驱动) 可以简单理解为 module,比如使用 Python 的某个 module 去连接 MySQL,那么这个 module 就叫 drive 了;不但 Python 有,其它程序语言,比如 Ruby、PHP、Node.js、Go 等等也会有对应的 drive。
比如说 Web 的请求吧,表面上是一个 URL 的请求过去,其实也可以原生地通过 socket 逻辑去访问对应的端口,也能返回页面的最终内容数据。你可以试一试,这样说不定对 HTML 的协议有更深一点的了解;但另一方面也是无用功,已经有很多不错的 module 存在,真没有必要再钻木取火写着玩了。