目录
前4篇笔记我们介绍了如何设置和关闭TCP,UDP连接,如何通过DNS将hostname换成IP等。但是我们退后一步,思考一下:在网络传输数据之前,我们该如何准备,编码和格式化数据呢;并且对于网络错误我们程序要如何提前准备处理呢。
1 Netowork Data
1.1 Byte String vs. Character String
就像在内存里一样,网卡上传输的“货币”,即基本单位是bytes,即8-bit打包起来的数据单位。但是在内存里,Python将int,float,str,list,tuple,和dict等如何表示成bytes从开发者视角隐藏起来;而网络通信编程的时候,socket将bytes直接暴露出给开发者。
Byte String(字节串):string of bytes,简称为byte。
1
2
3
4
# byte strings: a string of byte
b = bytes([0,1,98,99,100]) #b'\x00\x01bcd'
len(b) #5
list(b) #[0, 1, 98, 99, 100]
Character String(字符串):string of characters,简称为String。分为单byte编码(ASCII)和多byte编码(Unicode,包括utf-8,utf-16.utf-32);后者在网络传输过程中要格外注意在多个byte间确定边界。
对于str在网络上的传输,只需要通过相应的编码转换成byte即可。
1.2 Byte String(Byte Order) vs. Number
上节讨论的是bytes和str的相互转换。那么如果涉及到数据(data)和bytes的相互转换呢?通常有两种方法:
-
把数据转成str再编码成bytes。例如4253先转成‘4253’,再转成b‘4253’通过sender发送给receiver,receiver收到后先decode成‘4253’,再通过计算获得4253。这个过程虽然在20世纪60年代比较昂贵,但是现代由于计算机CPU的高速发展,现在计算成本已经大大降低了,这也就是为什么现在网络protocol主要是通过这种str方式传输数据。
-
直接把数据转成bytes。Python3的7.1 struct module就是一个将数据和bytes相互转换的一个模块。
1
2
3
4
5
6
7
8
9
print(hex(4253)) #0x109d
#整型转bytes
print(struct.pack('< i',4253)) # b’\x9d\x10\x00\x00’,little-endian
print(struct.pack('> i',4253)) # b’x00\x00\x10\x9d’,big-endian
print(struct.pack('! i',4253)) # b’x00\x00\x10\x9d’,承认big-endian为默认的byte order,用!表示。
#bytes转整型
print(struct.unpack('< i',b'\x9d\x10\x00\x00')) # (4253,)
print(struct.unpack('> i',b'\x00\x00\x10\x9d')) # (4253,)
需要注意的是bytes有big-endian和little-endian之分:
-
< i
表示little-endian,即左边最低位,右边最高位;i表示integer。 -
< i
表示big-endian,即左边最高位,右边最低位。
其实说来有趣,过去20年的网络发展主要集中在如何将bytes转移到protocol上。例如HTTP的Content-Length:4253
一栏其实连同整个HTTP message都是用str来编码的(接收时得先解码成字符串‘4253’,然后计算出代表的数据4253),而不是将4253表示成二进制数据bytes。所以大部分情况下我们不需要人工转换。但若是你涉及的project需要你不通过protocol接触data,那么就要考虑数据和bytes的相互转换以及byte order了。
1.3 Framing and Quoting
对于client和server接收到的信息,有一个重要数据定界问题。
数据定界:从一个流里如何确定某个数据的开始和结束。
对于UDP来说,由于收到的数据是atomic的,即要么完整,要么有缺陷(丢失,数据顺序错误,重复等)从而被丢失。因此数据定界问题并不存在于UDP。即UDP的一个sendto(addr,data)
里的data被一个recvfrom()
atomic接收,要么全部在,要么全部不在。可以理解为数据是类似弹珠在相同直径的管道里discrete到达的。
而对于TCP来说,由于是reliable的传输,因此任何不可靠的数据都要重新发送,因此不是atomic的,数据是stream(水流一样)到达的。所以数据的定界问题就必须考虑。有几种常见的处理方法。
第一种方法:client发送完data,receiver接收后并不需要response。这种情况下,client只需要全部发送;receiver只要全部接收即可,也就是说并不需要数据定界,流过来的作为整体都接收。注意下面socket如何变双通道为单通道,其实client或server一方关闭即可,双方都关闭某个方向就提供了一致性和冗余。
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
import socket,argparse
def server(address):
sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
sock.bind(address)
print('Server binding at: {}'.format(address))
sock.listen(1)
while True:
conn, client = sock.accept()
print('Accept client from: {}'.format(conn.getpeername()))
conn.shutdown(socket.SHUT_WR)
data = b''
while True:
more = conn.recv(8192) #8K
if not more:
print('Reach the end of incoming data')
break
data += more
print('Message is:\n{}'.format(data.decode('ascii')))
conn.close()
sock.close()
def client(address):
sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sock.connect(address)
sock.shutdown(socket.SHUT_RD)
print('Client binding at:{}'.format(sock.getsockname()))
print('Client sending to:{}'.format(sock.getpeername()))
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')
print('Client sending data finished')
sock.close()
if __name__ == '__main__':
choices = {'server':server,'client':client}
parser = argparse.ArgumentParser(description="TCP framing: no response")
parser.add_argument('role',choices=choices,help='choose server or client')
parser.add_argument('address',help='the server address',nargs='?',default=(('127.0.0.1'),1060))
args = parser.parse_args()
function = choices[args.role]
function(args.address)
可以见到clientsendall()
都被server的recv()
接收。
第二种方法:是第一种的变体,server在接收后会response,client接收sever的response,重点在于server和receiver的send和receive必须atomically sequencially执行,例如基本TCP代码。Atomically指所有数据一起(而不是片段化)发送或接受;sequentially指的是client send request->server receive request,send response->client receive response的顺序不能错。 需要注意的是必须等server完全接收完毕后再向client发送response,否则会导致deadlock(client没发送完,执行不了接收;server片断化发送阻塞,因为client TCP的flow control)。
第三种方法:实现recvall(socket,length)
,见基本TCP代码。告诉receiver实际该接收多少数据,但是实际应用中用固定的length作为该函数参数是比较少见的,因为网络传输的数据大小通常是变化的。
第四种方法:用特殊符号来界定(delimit)数据流。例如用‘\0’来界定ASCII码。但是特殊符号有几个问题需要小心处理:
-
Quoted Delimter(引用界定符):如果特殊符号包括在数据中,如何区别。
-
区别出来如何原封不动的返回原始数据。
-
处理数据长度的时候要考虑特殊符号。
第五种方法:在数据前加上数据长度的信息。当然,对于长度信心,你得用上面4种方法来处理frame和quoted Delimiter。然后再进入一个循环调用recv()
直到获得某长度的数据。
第六种方法:如果receiver接收的数据长度未知呢,比如sender一直在读取某个文件,然后发送给receiver?可以将已有的数据分成几段block,prefixed with该block的长度,当数据传输完毕,可以最后发送一个提前商定好的符号,比如length field为0的block来告诉receiver该数据已经完全发送完毕。见下面代码。
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
import socket,struct,argparse
header_struct = struct.Struct('!I')
def recvall(sock,length):
blocks = b''
while length:
block = sock.recv(length)
if not block:
raise RuntimeError('length of bytes: expected {}, received {}'.format(length,len(block)))
length -= len(block)
blocks += block
return blocks
def get_block(sock):
block_length = recvall(sock,header_struct.size)
(block_length,) = header_struct.unpack(block_length)
block_data = recvall(sock,block_length)
return block_data
def put_block(sock,message):
block_length = len(message)
block_length = header_struct.pack(block_length)
sock.send(block_length)
sock.send(message.encode('utf-8'))
def server(address):
sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
sock.bind(address)
print('Server binding at: {}'.format(address))
sock.listen(1)
conn, client = sock.accept()
conn.shutdown(socket.SHUT_WR)
while True:
block = get_block(conn)
if not block:
print('Server receive data complete')
break
print('Block: {}'.format(block.decode('utf-8')))
conn.close()
sock.close()
def client(address):
sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sock.connect(address)
print('Client binding at: {}'.format(address))
print('Client binding to: {}'.format(sock.getpeername()))
sock.shutdown(socket.SHUT_RD)
put_block(sock, 'Beautiful is better than ugly.\n')
put_block(sock, 'Explicit is better than implicit.\n')
put_block(sock, 'Simple is better than complex.\n')
put_block(sock, '')
print('Client send finished')
sock.close()
if __name__ == '__main__':
choices = {'server':server,'client':client}
parser = argparse.ArgumentParser(description="TCP framing: no response")
parser.add_argument('role',choices=choices,help='choose server or client')
parser.add_argument('address',help='the server address',nargs='?',default=(('127.0.0.1'),1060))
args = parser.parse_args()
function = choices[args.role]
function(args.address)
到目前为止,你已经掌握了6种方法来界定数据。实际上很多protocol,都是混合使用以上几种来界定数据的。例如HTTP使用了第4,5,6种方法:
-
第4种,它用
\r\n\r\n
这个delimiter来界定header; -
第5种,而header里有
Content-Length
这个field来告诉receiver data的长度; -
第6种,如果server在streaming a response,则HTTP可以使用第6种“Chunked encoding”。
1.3.1 Pickles and Self-delimiting Formats
Python提供12.1 pickle作为一个序列化的包含界定符(.)的可持续化存储的模块。
1
2
dumps([5,6,7]) #b'\x80\x03]q\x00(K\x05K\x06K\x07e.'
loads(b'\x80\x03]q\x00(K\x05K\x06K\x07e.') #[5,6,7]
它可以帮我们免除自己实现界定符的麻烦。
1.4 JSON and XML
JSON是网络上传输数据的最流行格式。19.2 json是python序列化JSON的模块。
XML是适用于文档的数据格式(通过标记)。
除了XML和JSON,还有Google Protocol Buffers和Thrift等其他数据格式也是网络上流行的数据格式。
1.5 Compression
通常来说,数据在网络上传输的时间远比CPU准备数据的时间要长。因此数据在传输前经过压缩是更明智的一种选择。HTTP协议在数据传输前让client和server决定是否同时支持压缩,如果是,则数据就要先压缩后传输。
zlib是提供资料压缩之用的函式库。13.1 zlib是Python对zlib的封装的模块。
zlib有趣的一个特点是self-framing。如果你在经zlib压缩后的数据后面添加未压缩的数据,它能分辨出来压缩和未压缩的界限。见下面代码,.
在data
里。
1
2
3
>>> data = zlib.compress(b'Python') + b'.'+zlib.compress(b'zlib')+b'.'
>>> 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.'
2 Network Exceptions
socket模块有2个常见的Exception:
-
OSError:这是socket模块的主要Exception,可以发生在socket的任何阶段;
-
gaierror:getaddrinfo()调用的错误,可以是隐式和显式调用的异常;
基于这两个Exception,随之而来的问题是:对于high-level的使用socket的module来说,如果有socket的Exception出现,它是会raise socket的原来的Exception呢还是该module自己定义的Exception呢?答案是都有可能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#socket的Exception
>>> import http.client
>>> h =http.client.HTTPConnection('nonexistent.hostname.foo.bar')
>>> h.request('GET','/')
...
socket.gaierror: [Errno 8] nodename nor servname provided, or not known
#high-level的module的Exception
>>> import urllib.request
>>> urllib.request.urlopen('http://nonexistent.hostname.foo.bar')
...
socket.gaierror: [Errno 8] nodename nor servname provided, or not known
During handling of the above exception, another exception occurred:
urllib.error.URLError: < urlopen error [Errno 8] nodename nor servname provided, or not known >
2.1 Raising More Specific Exceptions
在你自己写API时,有两种处理Exception的方法:
-
完全不处理。让caller自己catch,这样可以让caller了解会遇到什么low-level的Exception。
-
封装low-level Exception到你自己的Exception里。这对于不了解实现细节的caller格外有用,并且可以细分Exception,毕竟DestinationError和SourceError比gaierror提供更多的error信息。
2.2 Catching and Reporting Network Exceptions
对于异常的处理,有两种通用方法:
-
granular(颗粒化):用
try,except
包围每一个网络请求。这适用于短小的程序。 -
blanket(全体化):将网络请求分成几个大任务,每一个任务用
try,except
包围,可以加上自定义的Exception。这有利于Exception的排查,适用于较长的网络请求。