您好,欢迎访问一九零五行业门户网

Socket编程实战

socket 在英文中的含义为“(连接两个物品的)凹槽”,像the eye socket,意为“眼窝”,此外还有“插座”的意思。在计算机科学中,socket 通常是指一个连接的两个端点,这里的连接可以是同一机器上的,像unix domain socket,也可以是不同机器上的,像network socket。
本文着重介绍现在用的最多的 network socket,包括其在网络模型中的位置、api 的编程范式、常见错误等方面,最后用 python 语言中的 socket api 实现几个实际的例子。socket 中文一般翻译为“套接字”,不得不说这是个让人摸不着头脑的翻译,我也没想到啥“信达雅”的翻译,所以本文直接用其英文表述。本文中所有代码均可在 socket.py 仓库中找到。
概述
socket 作为一种通用的技术规范,首次是由 berkeley 大学在 1983 为 4.2bsd unix 提供的,后来逐渐演化为 posix 标准。socket api 是由操作系统提供的一个编程接口,让应用程序可以控制使用 socket 技术。unix 哲学中有一条一切皆为文件,所以 socket 和file 的 api 使用很类似:可以进行read、write、open、close等操作。
现在的网络系统是分层的,理论上有osi模型,工业界有tcp/ip协议簇。其对比如下:
每层上都有其相应的协议,socket api 不属于tcp/ip协议簇,只是操作系统提供的一个用于网络编程的接口,工作在应用层与传输层之间:
我们平常浏览网站所使用的http协议,收发邮件用的smtp与imap,都是基于 socket api 构建的。
一个 socket,包含两个必要组成部分:
地址,由 ip 与 端口组成,像192.168.0.1:80。
协议,socket 所是用的传输协议,目前有三种:tcp、udp、raw ip。
地址与协议可以确定一个socket;一台机器上,只允许存在一个同样的socket。tcp 端口 53 的 socket 与 udp 端口 53 的 socket 是两个不同的 socket。
根据 socket 传输数据方式的不同(使用协议不同),可以分为以下三种:
stream sockets,也称为“面向连接”的 socket,使用 tcp 协议。实际通信前需要进行连接,传输的数据没有特定的结构,所以高层协议需要自己去界定数据的分隔符,但其优势是数据是可靠的。
datagram sockets,也称为“无连接”的 socket,使用 udp 协议。实际通信前不需要连接,一个优势时 udp 的数据包自身是可分割的(self-delimiting),也就是说每个数据包就标示了数据的开始与结束,其劣势是数据不可靠。
raw sockets,通常用在路由器或其他网络设备中,这种 socket 不经过tcp/ip协议簇中的传输层(transport layer),直接由网络层(internet layer)通向应用层(application layer),所以这时的数据包就不会包含 tcp 或 udp 头信息。
python socket api
python 里面用(ip, port)的元组来表示 socket 的地址属性,用af_*来表示协议类型。
数据通信有两组动词可供选择:send/recv 或 read/write。read/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_1、fin_wait_2、closing、time_wait。
此外,由于 tcp 是可靠的传输协议,所以每次发送一个数据包后,都需要得到对方的确认(ack),有了上面这两个知识后,再来看下面的图:
在主动关闭连接的 socket 调用 close方法的同时,会向被动关闭端发送一个 fin
对端收到fin后,会向主动关闭端发送ack进行确认,这时被动关闭端处于 close_wait 状态
当被动关闭端调用close方法进行关闭的同时向主动关闭端发送 fin 信号,接收到 fin 的主动关闭端这时就处于 time_wait 状态
这时主动关闭端不会立刻转为 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
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.py、uds_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 差不多,主要区别是能够进行过滤(通过state与exclude关键字)。
$ 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)
这两个命令更多用法可以参考:
ss utility: quick intro
10 basic examples of linux netstat command
总结
我们的生活已经离不开网络,平时的开发也充斥着各种复杂的网络应用,从最基本的数据库,到各种分布式系统,不论其应用层怎么复杂,其底层传输数据的的协议簇是一致的。socket 这一概念我们很少直接与其打交道,但是当我们的系统出现问题时,往往是对底层的协议认识不足造成的,希望这篇文章能对大家编程网络方面的程序有所帮助。
其它类似信息

推荐信息