ホームページ >バックエンド開発 >Python チュートリアル >ソケットプログラミングの練習
ソケットとは英語で「溝(二つの物をつなぐ)」を意味し、「目の窩」を意味するアイソケットと同じく「ソケット」という意味もあります。コンピューター サイエンスでは、ソケットは通常、接続の 2 つのエンドポイントを指します。ここでの接続は、UNIX ドメイン ソケットのように同じマシン上に存在する場合もあれば、ネットワーク ソケットのように異なるマシン上に存在する場合もあります。
この記事では、ネットワーク モデルにおける位置、API プログラミング パラダイム、一般的なエラーなど、最も一般的に使用されるネットワーク ソケットに焦点を当て、最後に Python 言語のソケット API を使用していくつかの実用的な例を実装します。ソケットは中国語では一般に「ソケット」と訳されますが、これは「忠実でエレガントな」訳を期待していなかったので、この記事では英語の表現をそのまま使用しています。この記事のすべてのコードは、socket.py リポジトリにあります。
概要
ソケットは、一般的な技術仕様として、1983 年にバークレー大学によって 4.2BSD Unix 用に初めて提供され、その後徐々に POSIX 標準に進化しました。ソケット API は、アプリケーションがソケット テクノロジの使用を制御できるようにするオペレーティング システムによって提供されるプログラミング インターフェイスです。 Unix の哲学には、すべてがファイルであるという原則があるため、ソケットとファイルの API の使用法は非常に似ており、読み取り、書き込み、開く、閉じるなどの操作を実行できます。
現在のネットワーク システムは階層化されており、理論的には OSI モデル、業界では TCP/IP プロトコル スイートが使用されます。比較は次のとおりです:
各層には対応するプロトコルがあります。ソケット API は、ネットワーク プログラミングのためにオペレーティング システムによって提供される単なるインターフェイスであり、アプリケーション間で動作します。平
通常、Web サイトで電子メールを送信するために使用される HTTP プロトコルは、Socket API に基づいて構築されています。 ソケットには 2 つの必要なコンポーネントが含まれています: アドレス。192.168.0.1:80 のように、IP とポートで構成されます。 プロトコル、ソケットで使用される送信プロトコルには、現在 TCP、UDP、Raw IP の 3 種類があります。 アドレスとプロトコルによってソケットが決定されます。1 つのマシン上に存在できる同一のソケットは 1 つだけです。 TCP ポート 53 のソケットと UDP ポート 53 のソケットは 2 つの異なるソケットです。 ソケットがデータを送信するさまざまな方法 (使用されるさまざまなプロトコル) に応じて、ソケットは次の 3 つのタイプに分類できます: 「接続指向」ソケットとしても知られるストリーム ソケットは、TCP プロトコルを使用します。実際の通信には接続が必要であり、送信されるデータは特定の構造を持たないため、上位プロトコルでデータセパレーター自体を定義する必要がありますが、データが信頼できるという利点があります。 「コネクションレス」ソケットとも呼ばれるデータグラム ソケットは、UDP プロトコルを使用します。実際の通信の前に接続する必要はありません。利点の 1 つは、UDP データ パケットが自己区切りであることです。つまり、各データ パケットがデータの始まりと終わりをマークします。欠点は、データの信頼性が低いことです。 生のソケットは通常、ルーターやその他のネットワーク機器で使用されます。この種のソケットは、TCP/IP プロトコル スイートのトランスポート層を経由せず、インターネット層からアプリケーション層に直接接続されます。現時点では、tcp または udp ヘッダー情報は含まれません。 PythonソケットAPIPythonは、ソケットのアドレス属性を表すために(ip, port)のタプルを使用し、プロトコルタイプを表すためにAF_*を使用します。 データ通信には、send/recv または read/write の 2 つの動詞セットから選択できます。読み取り/書き込みメソッドは Java でも使用されます。このメソッドについてはここではあまり説明しませんが、次の点に注意してください: 読み取り/書き込みはバッファーのある「ファイル」に対して動作するため、読み取りと書き込みの後、実際にデータを送信または読み取るには、flush メソッドを呼び出す必要があります。そうしないと、データはバッファ内に残ります。 TCPソケット TCPソケットは接続する前に接続を確立する必要があるため、そのモードはUDPソケットよりも責任があります。詳細は次のとおりです。 各 API の具体的な意味についてはここでは説明しません。Python 言語で実装された echo サーバーを参照してください。
# 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)上記の単純なエコー サーバー コードで注意すべき点は、TIME_WAIT 状態のソケットがすぐに使用できるように、サーバー側のソケットが SO_REUSEADDR に設定されていることです。状態は後ほど図を変更する際に詳しく紹介します。 UDPソケット🎜
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 <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.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 这一概念我们很少直接与其打交道,但是当我们的系统出现问题时,往往是对底层的协议认识不足造成的,希望这篇文章能对大家编程网络方面的程序有所帮助。