python-web-5

python-web-5

网络数据和网络错误

如果只想通过网络发送文本的话,那么只需要考虑编码与封帧问题就可以了,但这时一个新的问题就出来了:字节顺序的问题。

比如说 4523。尽管所有处理器都认同内存中的字节要有序,它们也都会以 4 作为开始字符,以 3 作为结束的字符,但是它们存储二进制数字的字节顺序是不同的。一些计算机使用大端(big-endian),将最高位存储在最前面。其他处理器(如x86架构)则使用小端(little-endian),将最低位字节存储在前面。(前面指内存低地址字节)

1
2
3
4
5
6
7
import struct

print(struct.pack("<i", 4353)) # 小端
print(struct.pack(">i", 4353)) # 大端

# b'\x01\x11\x00\x00'
# b'\x00\x00\x11\x01'

封帧与引用

如果使用 UDP 数据进行通信,那么协议本身就会使用独立的,可识别的的快进行数据传输。不过,如果网络出现了问题,我们就必须自己重新排列并重新发送这些数据块。然而如果我们使用更为常用的 TCP 进行通信,那么我们就要应对封帧(framing)问题,即如何分割消息使得接收方可以识别消息的开始和结束。

由于传递给 sendall() 的数据可能在实际网络中传输时被分割为多个数据包,接收消息的程序可能需要进行多个 recv() 调用才能读取完整的信息。如果个包传达时,操作系统都能够再次调度运行 recv() 的进程,那么可能不需要进行多个 recv() 的调用。

关于封帧,需要考虑的问题是这样的:接收方何时最终停止调用 recv() 才是安全的?整个消息或数据何时才能完整无缺的传达?何时才能将接收到的信息作为一个整体来处理?

模式一

这个方法用于一些及其简单的网络协议,只涉及数据的发送,而不关注响应。因此,接收方永远不会认为数据已经够了,然后向发送方发送响应。在这种情况下,可以使用这种模式:发送方循环发送数据,直到所有的数据都被传递给 sendall() 为止,然后使用 close() 关闭套接字。接收方不断的调用 recv(),直到最后返回一个空字符串(表示发送方已经关闭了套接字)为止。

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
import socket
from argparse import ArgumentParser


def server(address):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 加入socket配置,重用ip和端口
sock.bind(address)
sock.listen(1)
print("Run this script in another window with \"-c\" to connect")
print("Listening at: ", sock.getsockname())
sc, sockname = sock.accept() # 接受客户端的连接
print("Accepted connection from: ", sockname)
sc.shutdown(socket.SHUT_WR)
message = b""
while True:
more = sc.recv(8192) # 8k
if not more: # recv 返回 ""
print("Received zero bytes - end of file")
break
print("Received {} bytes".format(len(more)))
message += more
print("Message:\n")
print(message.decode("ascii"))
sc.close()
sock.close()


def client(address):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(address)
# 只停止在某个方向上的数据传输,而一个方向上的数据传输继续进行
# SHUT_RDWR:关闭读写,即不可以使用send/write/recv/read
# SHUT_RD:关闭读,即不可以使用recv/read
# SHUT_WR:关闭写,即不可以使用send/write
sock.shutdown(socket.SHUT_RD)
sock.sendall(b"Beautiful is better than ugly.\n")
sock.sendall(b"Explicit is better than implicit.\n")
sock.sendall(b"Simple is better than complex.\n")
sock.close()


if __name__ == "__main__":
parser = ArgumentParser(description="Transmit & receive a data stream")
parser.add_argument("hostname", nargs="?", default="127.0.0.1",
help="IP address or hostname (default:%(default)s")
parser.add_argument("-c", action="store_true", help="run as a client")
parser.add_argument("-p", type=int, metavar="port", default=1060,
help="TCP port number (default:%(default)s")

args = parser.parse_args()
function = client if args.c else server
function((args.hostname, args.p))

运行一下服务端然后再开一个另外的命令行窗口加上 -c 参数来运行客户端程序,得到下面的输出

1
2
3
4
5
6
7
8
9
10
11
(venv) D:\my_py36\Python-Web\network_data>python streamer.py
Run this script in another window with "-c" to connect
Listening at: ('127.0.0.1', 1060)
Accepted connection from: ('127.0.0.1', 56077)
Received 96 bytes
Received zero bytes - end of file
Message:

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.

使用客户端关闭套接字后形成的文件结束符来表示这次通信唯一需要的一个帧。

需要注意的是,由于这个套接字并没有准备接收任何数据,因此当客户端和服务端不在进行某一方向的通信时会立即关闭该方向的连接。这一做法避免了在另一方向上使用套接字。否则的话,可能会由于在缓冲队列中填入太多的数据从而导致最终死锁。客户端和服务器至少其中一方调用 shutdown() 方法是相当必要的。

模式二

使用定长消息。可以使用 sendall() 发送字节字符串,然后使用自己设计的 recv() 循环来确保接受完整的消息。

1
2
3
4
5
6
7
8
def recvall(sock, length):
data = ""
while len(data) < length:
more = sock.recv(length - len(data))
if not more:
raise EOFError("socket closed {} bytes into a {}-byte message".format(len(data), length))
data += more
return data

模式三

使用特殊的字符来划分消息的边界,如果可以确保消息中的字节或字符在特定的有限范围内,那么自然就可以选择该范围外的某个符号作为消息的结束符,比如如果正在发送 ascii 字符,那么可以选择空字符 '\0' 作为定界符,也可以选择像 \xff 这种处于ascii字符之外的字符。

模式四

在每个消息前加上其长度作为前缀。如果使用该模式则无需进行分析,引用或者插入就能够一字不差的发送二进制数据块。因此对于高性能协议来说,这是一个很流行的选择。当然消息长度本身需要使用帧封装。封帧时可以使用前面提到的方法。通常会使用一个定长的二进制整数或者是在变长的整数字符串后面加上一个文本定界符来表示长度。无论哪种方法,只要对方读取并解码了长度,就能够进入循环,重复 recv() 直到整个消息都传达为止。

模式五

使用这个模式时,我们并非只发送单个信息,而是会发送多个数据块,而且在每个数据块前加上数据块长度作为其前缀。这意味着每个新的信息块对于发送者来说都是可见的,可以使用数据块的长度为其打上标签,然后将数据块置于发送流中,抵达信息结尾时,发送方可以发送一个与接收方事先约定好的信号告知发送完毕。

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
60
61
62
63
64
65
66
67
68
69
70
import socket, struct
from argparse import ArgumentParser
# 等于声明一个包含一个 unsigned int变量的结构体
header_struct = struct.Struct("!I") # message up to 2**23 - 1 in length


def recvall(sock, length):
blocks = []
while length:
block = sock.recv(length)
if not block:
raise EOFError("socket closed with %d bytes left in this block".format(length))
length -= len(block)
blocks.append(block)
return b''.join(blocks)


def get_block(sock):
data = recvall(sock, header_struct.size)
(block_length, ) = header_struct.unpack(data) # 解包
return recvall(sock, block_length)


def put_block(sock, message):
block_length = len(message)
sock.send(header_struct.pack(block_length)) # 打包数据对象转化为流
sock.send(message)


def server(address):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(address)
sock.listen(1)
print("Run this script in another window with \"-c\" to connect")
print("Listening at: ", sock.getsockname())
sc, sockname = sock.accept()
print("Accepted connection from: ", sockname)
sc.shutdown(socket.SHUT_WR)
while True:
block = get_block(sc)
if not block:
break
print("Block says: ", repr(block))
sc.close()
sock.close()


def client(address):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(address)
sock.shutdown(socket.SHUT_RD)
put_block(sock, b"Beautiful is better than ugly.")
put_block(sock, b"Explicit is better than implicit")
put_block(sock, b"Simple is better than complex")
put_block(sock, b"")
sock.close()


if __name__ == "__main__":
parser = ArgumentParser(description="Transmit & receive over TCP")
parser.add_argument("hostname", nargs="?", default="127.0.0.1",
help="IP address or hostname (default:%(default)s")
parser.add_argument("-c", action="store_true", help="run as a client")
parser.add_argument("-p", type=int, metavar="port", default=1060,
help="TCP port number (default:%(default)s")

args = parser.parse_args()
function = client if args.c else server
function((args.hostname, args.p))

和上面的例子同样的运行方法

1
2
3
4
5
6
7
(venv) D:\my_py36\Python-Web\network_data>python blocks.py
Run this script in another window with "-c" to connect
Listening at: ('127.0.0.1', 1060)
Accepted connection from: ('127.0.0.1', 56069)
Block says: b'Beautiful is better than ugly.'
Block says: b'Explicit is better than implicit'
Block says: b'Simple is better than complex'

struct 是 Python 的一个内置库,用于定义结构体以处理网络流,文件流的一些问题

按照指定格式将 Python 数据转换为字符串,该字符串为字节流,如网络传输时,不能传输 int,此时先将 int转化为字节流,然后再发送;
按照指定格式将字节流转换为 Python指定的数据类型;
处理二进制数据,如果用 struct 来处理文件的话,需要用’wb’,’rb’以二进制(字节流)写,读的方式来处理文件;
处理c语言中的结构体;

Struct 参数文档

Pickle 与自定义定界符格式

通过网络发送的某些数据可能已经包含了某种形式的内置定界符,如果要传输这样的数据,那么就不必再数据已有定界符的基础上再设计我们自己的封帧方案了。

一个很有意思的事情是,如果我们使用 pickle 的 load() 函数从文件中读入数据,那么文件指针会停留在 pickle 数据结尾,可以从结尾处开始读取后面的内容。

1
2
3
4
5
6
7
8
9
10
>>>from io import BytesIO
>>>import pickle
>>>f = BytesIO(b"\x80\x03]q\x00(K\x05K\x06K\x07e.balalabalabala)")
>>>pickle.load(f)
[5, 6, 7]
>>>f.tell()
14
>>>f.read()
b'balalabalabala)'

压缩

GNU 的 zilb 仍是当今互联网最普遍的压缩形式之一。能够自己进行封帧是 zlib 一个很有意思的特点。在传递一个压缩过的数据流时,zlib 能够识别出压缩数据何时达到结尾,如果后面还有未经压缩的数据,用户也可以直接访问。

1
2
3
4
5
6
>>>import zlib
>>>data = zlib.compress(b"Python") + b"." + zlib.compress(b"zlib")
>>>data
b'x\x9c\x0b\xa8,\xc9\xc8\xcf\x03\x00\x08\x97\x02\x83.x\x9c\xab\xca\xc9L\x02\x00\x04d\x01\xb2'
>>>len(data)
27

网络异常

要捕获异常,有两种基本方法:granular 异常处理和 blanket 异常处理。

granular 方法就是针对每个网络调用都采用 try…except 语句捕获异常,尽管对小的程序很好用,但在大项目里就显得有些冗余。

blanket 方法从程序外部调用这些方法时捕获异常,这需要我们识别出完成特定操作的代码块,以及其外部调用处。这种方法方便整体重新进行操作。如发邮件失败,在调用函数处捕获到了异常,就可以规定时间重新发送邮件,而不像内部出现错误而无法便于程序员编写代码来重复整个代码操作。

Author

Ctwo

Posted on

2020-10-13

Updated on

2020-10-25

Licensed under

Comments