目录


前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的相互转换呢?通常有两种方法:

  1. 把数据转成str再编码成bytes。例如4253先转成‘4253’,再转成b‘4253’通过sender发送给receiver,receiver收到后先decode成‘4253’,再通过计算获得4253。这个过程虽然在20世纪60年代比较昂贵,但是现代由于计算机CPU的高速发展,现在计算成本已经大大降低了,这也就是为什么现在网络protocol主要是通过这种str方式传输数据。

  2. 直接把数据转成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-endianlittle-endian之分:

  1. < i表示little-endian,即左边最低位,右边最高位;i表示integer。

  2. < 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码。但是特殊符号有几个问题需要小心处理:

  1. Quoted Delimter(引用界定符):如果特殊符号包括在数据中,如何区别。

  2. 区别出来如何原封不动的返回原始数据。

  3. 处理数据长度的时候要考虑特殊符号。

第五种方法:在数据前加上数据长度的信息。当然,对于长度信心,你得用上面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种方法:

  1. 第4种,它用\r\n\r\n这个delimiter来界定header;

  2. 第5种,而header里有Content-Length这个field来告诉receiver data的长度;

  3. 第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:

  1. OSError:这是socket模块的主要Exception,可以发生在socket的任何阶段;

  2. 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的方法:

  1. 完全不处理。让caller自己catch,这样可以让caller了解会遇到什么low-level的Exception。

  2. 封装low-level Exception到你自己的Exception里。这对于不了解实现细节的caller格外有用,并且可以细分Exception,毕竟DestinationError和SourceError比gaierror提供更多的error信息。

2.2 Catching and Reporting Network Exceptions

对于异常的处理,有两种通用方法:

  1. granular(颗粒化):用try,except包围每一个网络请求。这适用于短小的程序。

  2. blanket(全体化):将网络请求分成几个大任务,每一个任务用try,except包围,可以加上自定义的Exception。这有利于Exception的排查,适用于较长的网络请求。

3 总结

Network Data & Error Summary

4 参考资料


Share Post

Twitter Google+

Shunmian

The only programmers in a position to see all the differences in power between the various languages are those who understand the most powerful one.