Socket 编程实战

本文原发于我的博客html

Socket 在英文中的含义为“(链接两个物品的)凹槽”,像the eye socket,意为“眼窝”,此外还有“插座”的意思。在计算机科学中,socket 一般是指一个链接的两个端点,这里的链接能够是同一机器上的,像unix domain socket,也能够是不一样机器上的,像network socketpython

本文着重介绍如今用的最多的 network socket,包括其在网络模型中的位置、API 的编程范式、常见错误等方面,最后用 Python 语言中的 socket API 实现几个实际的例子。Socket 中文通常翻译为“套接字”,不得不说这是个让人摸不着头脑的翻译,我也没想到啥“信达雅”的翻译,因此本文直接用其英文表述。本文中全部代码都可在 socket.py 仓库中找到。linux

概述

Socket 做为一种通用的技术规范,首次是由 Berkeley 大学在 1983 为 4.2BSD Unix 提供的,后来逐渐演化为 POSIX 标准。Socket API 是由操做系统提供的一个编程接口,让应用程序能够控制使用 socket 技术。Unix 哲学中有一条一切皆为文件,因此 socketfile 的 API 使用很相似:能够进行readwriteopenclose等操做。git

如今的网络系统是分层的,理论上有OSI模型,工业界有TCP/IP协议簇。其对好比下:github

图片描述

每层上都有其相应的协议,socket API 不属于TCP/IP协议簇,只是操做系统提供的一个用于网络编程的接口,工做在应用层与传输层之间:shell

图片描述

咱们日常浏览网站所使用的http协议,收发邮件用的smtp与imap,都是基于 socket API 构建的。数据库

一个 socket,包含两个必要组成部分:编程

  1. 地址,由 ip 与 端口组成,像192.168.0.1:80浏览器

  2. 协议,socket 所是用的传输协议,目前有三种:TCPUDPraw IP网络

地址与协议能够肯定一个socket;一台机器上,只容许存在一个一样的socket。TCP 端口 53 的 socket 与 UDP 端口 53 的 socket 是两个不一样的 socket。

根据 socket 传输数据方式的不一样(使用协议不一样),能够分为如下三种:

  1. Stream sockets,也称为“面向链接”的 socket,使用 TCP 协议。实际通讯前须要进行链接,传输的数据没有特定的结构,因此高层协议须要本身去界定数据的分隔符,但其优点是数据是可靠的。

  2. Datagram sockets,也称为“无链接”的 socket,使用 UDP 协议。实际通讯前不须要链接,一个优点时 UDP 的数据包自身是可分割的(self-delimiting),也就是说每一个数据包就标示了数据的开始与结束,其劣势是数据不可靠。

  3. Raw sockets,一般用在路由器或其余网络设备中,这种 socket 不通过TCP/IP协议簇中的传输层(transport layer),直接由网络层(Internet layer)通向应用层(Application layer),因此这时的数据包就不会包含 tcp 或 udp 头信息。

图片描述

Python socket API

Python 里面用(ip, port)的元组来表示 socket 的地址属性,用AF_*来表示协议类型。
数据通讯有两组动词可供选择:send/recvread/writeread/write 方式也是 Java 采用的方式,这里不会对这种方式进行过多的解释,可是须要注意的是:

read/write 操做的具备 buffer 的“文件”,因此在进行读写后须要调用flush方法去真正发送或读取数据,不然数据会一直停留在缓冲区内。

TCP socket

TCP socket 因为在通向前须要创建链接,因此其模式较 UDP socket 负责些。具体以下:
图片描述

每一个API 的具体含义这里不在赘述,能够查看手册,这里给出 Python 语言的实现的 echo server。

# echo_server.py
# coding=utf8
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置 SO_REUSEADDR 后,能够当即使用 TIME_WAIT 状态的 socket
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('', 5500))
sock.listen(5)
def handler(client_sock, addr):
    print('new client from %s:%s' % addr)
    msg = client_sock.recv(1024)
    client_sock.send(msg)
    client_sock.close()
    print('client[%s:%s] socket closed' % addr)

if __name__ == '__main__':
    while 1:
        client_sock, addr = sock.accept()
        handler(client_sock, addr)
# echo_client.py
# coding=utf8
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('', 5500))
sock.send('hello socket world')
print sock.recv(1024)

上面简单的echo server 代码中有一点须要注意的是:server 端的 socket 设置了SO_REUSEADDR为1,目的是能够当即使用处于TIME_WAIT状态的socket,那么TIME_WAIT又是什么意思呢?后面在讲解 tcp 状态变动图时再作详细介绍。

UDP socket

图片描述

UDP socket server 端代码在进行bind后,无需调用listen方法。

# udp_echo_server.py
# coding=utf8
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 设置 SO_REUSEADDR 后,能够当即使用 TIME_WAIT 状态的 socket
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('', 5500))
# 没有调用 listen

if __name__ == '__main__':
    while 1:
        data, addr = sock.recvfrom(1024)

        print('new client from %s:%s' % addr)
        sock.sendto(data, addr)

# udp_echo_client.py
# coding=utf8
import socket

udp_server_addr = ('', 5500)

if __name__ == '__main__':
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    data_to_sent = 'hello udp socket'
    try:
        sent = sock.sendto(data_to_sent, udp_server_addr)
        data, server = sock.recvfrom(1024)
        print('receive data:[%s] from %s:%s' % ((data,) + server))
    finally:
        sock.close()

常见陷阱

忽略返回值

本文中的 echo server 示例由于篇幅限制,也忽略了返回值。网络通讯是个很是复杂的问题,一般没法保障通讯双方的网络状态,颇有可能在发送/接收数据时失败或部分失败。因此有必要对发送/接收函数的返回值进行检查。本文中的 tcp echo client 发送数据时,正确写法应该以下:

total_send = 0
content_length = len(data_to_sent)
while total_send < content_length:
    sent = sock.send(data_to_sent[total_send:])
    if sent == 0:
        raise RuntimeError("socket connection broken")
    total_send += total_send + sent

send/recv操做的是网络缓冲区的数据,它们没必要处理传入的全部数据。

通常来讲,当网络缓冲区填满时,send函数就返回了;当网络缓冲区被清空时,recv 函数就返回。
当 recv 函数返回0时,意味着对端已经关闭。

能够经过下面的方式设置缓冲区大小。

s.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, buffer_size)

认为 TCP 具备 framing

TCP 不提供 framing,这使得其很适合于传输数据流。这是其与 UDP 的重要区别之一。UDP 是一个面向消息的协议,能保持一条消息在发送者与接受者之间的完备性。

图片描述
代码示例参考:framing_assumptions

TCP 的状态机

在前面echo server 的示例中,提到了TIME_WAIT状态,为了正式介绍其概念,须要了解下 TCP 从生成到结束的状态机器。(图片来源

图片描述

这个状图转移图很是很是关键,也比较复杂,我本身为了方便记忆,对这个图进行了拆解,仔细分析这个图,能够得出这样一个结论,链接的打开与关闭都有被动(passive)与主动(active)两种,主动关闭时,涉及到的状态转移最多,包括FIN_WAIT_一、FIN_WAIT_二、CLOSING、TIME_WAIT。

此外,因为 TCP 是可靠的传输协议,因此每次发送一个数据包后,都须要获得对方的确认(ACK),有了上面这两个知识后,再来看下面的图:(图片来源

tcpclosesimul.png

  1. 在主动关闭链接的 socket 调用 close方法的同时,会向被动关闭端发送一个 FIN

  2. 对端收到FIN后,会向主动关闭端发送ACK进行确认,这时被动关闭端处于 CLOSE_WAIT 状态

  3. 当被动关闭端调用close方法进行关闭的同时向主动关闭端发送 FIN 信号,接收到 FIN 的主动关闭端这时就处于 TIME_WAIT 状态

  4. 这时主动关闭端不会马上转为 CLOSED 状态,而是须要等待 2MSL(max segment life,一个数据包在网络传输中最大的生命周期),以确保被动关闭端可以收到最后发出的 ACK。若是被动关闭端没有收到最后的 ACK,那么被动关闭端就会从新发送 FIN,因此处于TIME_WAIT的主动关闭端会再次发送一个 ACK 信号,这么一来(FIN来)一回(ACK),正好是两个 MSL 的时间。若是等待的时间小于 2MSL,那么新的socket就能够收到以前链接的数据。

前面 echo server 的示例也说明了,处于 TIME_WAIT 并非说必定不能使用,能够经过设置 socket 的 SO_REUSEADDR 属性以达到不用等待 2MSL 的时间就能够复用socket 的目的,固然,这仅仅适用于测试环境,正常状况下不要修改这个属性。

实战

HTTP UA

http 协议是现在万维网的基石,能够经过 socket API 来简单模拟一个浏览器(UA)是如何解析 HTTP 协议数据的。

#coding=utf8
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
baidu_ip = socket.gethostbyname('baidu.com')
sock.connect((baidu_ip, 80))
print('connected to %s' % baidu_ip)

req_msg = [
    'GET / HTTP/1.1',
    'User-Agent: curl/7.37.1',
    'Host: baidu.com',
    'Accept: */*',
]
delimiter = '\r\n'

sock.send(delimiter.join(req_msg))
sock.send(delimiter)
sock.send(delimiter)

print('%sreceived%s' % ('-'*20, '-'*20))
http_response = sock.recv(4096)
print(http_response)

运行上面的代码能够获得下面的输出

--------------------received--------------------
HTTP/1.1 200 OK
Date: Tue, 01 Nov 2016 12:16:53 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: Wed, 02 Nov 2016 12:16:53 GMT
Connection: Keep-Alive
Content-Type: text/html

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

http_response是经过直接调用recv(4096)获得的,万一真正的返回大于这个值怎么办?咱们前面知道了 TCP 协议是面向流的,它自己并不关心消息的内容,须要应用程序本身去界定消息的边界,对于应用层的 HTTP 协议来讲,有几种状况,最简单的一种时经过解析返回值头部的Content-Length属性,这样就知道body的大小了,对于 HTTP 1.1版本,支持Transfer-Encoding: chunked传输,对于这种格式,这里不在展开讲解,你们只须要知道, TCP 协议自己没法区分消息体就能够了。对这块感兴趣的能够查看 CPython 核心模块 http.client

Unix_domain_socket

UDS 用于同一机器上不一样进程通讯的一种机制,其API适用与 network socket 很相似。只是其链接地址为本地文件而已。

代码示例参考:uds_server.pyuds_client.py

ping

ping 命令做为检测网络联通性最经常使用的工具,其适用的传输协议既不是TCP,也不是 UDP,而是 ICMP,利用 raw sockets,咱们能够适用纯 Python 代码来实现其功能。

代码示例参考:ping.py

netstat vs ss

netstat 与 ss 是类 Unix 系统上查看 Socket 信息的命令。
netstat 是比较老牌的命令,我经常使用的选择有

  • -t,只显示 tcp 链接

  • -u,只显示 udp 链接

  • -n,不用解析hostname,用 IP 显示主机,能够加快执行速度

  • -p,查看链接的进程信息

  • -l,只显示监听的链接

ss 是新兴的命令,其选项和 netstat 差很少,主要区别是可以进行过滤(经过stateexclude关键字)。

$ ss -o state time-wait -n | head
Recv-Q Send-Q             Local Address:Port               Peer Address:Port
0      0                 10.200.181.220:2222              10.200.180.28:12865  timer:(timewait,33sec,0)
0      0                      127.0.0.1:45977                 127.0.0.1:3306   timer:(timewait,46sec,0)
0      0                      127.0.0.1:45945                 127.0.0.1:3306   timer:(timewait,6.621ms,0)
0      0                 10.200.181.220:2222              10.200.180.28:12280  timer:(timewait,12sec,0)
0      0                 10.200.181.220:2222              10.200.180.28:35045  timer:(timewait,43sec,0)
0      0                 10.200.181.220:2222              10.200.180.28:42675  timer:(timewait,46sec,0)
0      0                      127.0.0.1:45949                 127.0.0.1:3306   timer:(timewait,11sec,0)
0      0                      127.0.0.1:45954                 127.0.0.1:3306   timer:(timewait,21sec,0)
0      0               ::ffff:127.0.0.1:3306           ::ffff:127.0.0.1:45964  timer:(timewait,31sec,0)

这两个命令更多用法能够参考:

总结

咱们的生活已经离不开网络,平时的开发也充斥着各类复杂的网络应用,从最基本的数据库,到各类分布式系统,不论其应用层怎么复杂,其底层传输数据的的协议簇是一致的。Socket 这一律念咱们不多直接与其打交道,可是当咱们的系统出现问题时,每每是对底层的协议认识不足形成的,但愿这篇文章能对你们编程网络方面的程序有所帮助。

参考

相关文章
相关标签/搜索