应该如何准备需要传输的数据?
应该如何对数据进行编码与格式化?
Py程序需要提供哪些类型的错误?
5.1. 字节与字符串
PC与网卡都支持将字节作为通用传输单元。字节将8比特的信息封装起来,作为信息存储的通用单位。
但是,RAM芯片与网卡不同,程序运行中,Py能隐藏RAM中的int、str、list、dict的具体实现细节,除非使用特殊调试工具,否则无法查看存储的字节,只能看到外部表现。
1)位(bit)是信息的最小单元。每位可以是0/1,位一般通过高电压和低电压来实现
2)8位组成1字节(byte)
1)使用一个正好介于0~255的整数
2)使用一个字节字符串,字符串的唯一内容就是改字节本省,可使用Py源代码支持的任何常用进制来输入字节表示的数字
0b1100010
98
0b1100010 == 0o142 == 98 == 0x62
True
b = bytes([0, 1, 98, 99, 100])
len(b)
5
type(b)
<class 'bytes'>
list(b)
[0, 1, 98, 99, 100]
</class>
字节字符串对象的repr()函数,使用ASCII字符作为简写形式,表示数组中,字节值正好与可打印的ASCII字符对应的元素。对于没有对应可打印ASCII字符的元素,使用显示的十六进制格式\xNN来表示
b
b'\x00\x01bcd'
字节字符串在语义上并不表示ASCII字符,只用来表示8个二进制位组成的字节。
通过socket传输一个符号串,需要使用ASCII编码方法,为每个符号分配一个确切的字节值。
ASCII定义了0到127的字符代码,可对应7个二进制位。因此,使用字节存储ASCII字符时,最高位始终是0。
1)标点符号与各位数字
2)包含大写字母
3)包含小写字母
for i in range(32, 128, 32):
print(' '.join(chr(j) for j in range(i , i+32)))
! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ?
@ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _
` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~
左上角是空格,字符代码32, 右下角是DEl,不可见,代码为127的删除符。
位是按顺序排列的,要得到某个位的算术值,只需将字符代码的该位设为0,用原来的字符代码值减去置零后的值即可。另外,通过翻转表示十进制32的二进制位,可以完成大小写字母的转换,也可通过把str中所有字母的字符值中,表示十进制数32的二进制位设为1或0,将所有字母转换为大写或小写。
1)对字符进行编码(encoding):将真正的Unicode字符串转换为字节字符串。Py程序会将这些字节发送给外部的真实世界
2)对字节进行解码(decoding):将字节字符转换为真正的Unicode字符
可以认为外部世界由字节组成,这些字节通过某种密码保存。要在Py程序中的数据传输到外部世界,就需将这种密码翻译/破解。要将Py程序中的数据传输到外部世界,需将数据编码成外部世界理解的字节编码;把数据从外部世界转移到Py程序中,必须将其解码
Py内置的多数单字节编码都是ASCII的扩展,把剩下的128个值用于特定地域的字母/符号。
ASCII字母N、a、m、r、i分布在表示非ASCII字符的字节值之间。
操作已编码的文本时,会遇到两种字符错误:
1)要尝试载入的已编码字节字符串,不符合提供的用于解释的编码规则;
2)字符无法使用,提供的编码方式表示
b'\x80'.decode('ascii')
Traceback (most recent call last):
File "<input>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0x80 in position 0: ordinal not in range(128)
</module>
一般从两个方面来处理这样的错误:
1)确认是否使用了错误的编码方式
2)弄清楚为什么数据没有想我们预期的那样符合提供的编码方式,
如果这两种方式都么解决问题,代码必须从程序逻辑上兼容编码方式与实际字符串或数据不能匹配的情况。
需要阅读标准库文档,来了解一些处理错误的替代方法,而不使用异常处理机制。
b'ab\x80def'.decode('ascii', 'replace')
'ab�def'
b'ab\x80def'.decode('ascii', 'ignore')
'abdef'
codecs模块的标准库文档对此做出了描述,如果编码时对某些字符使用了多字节编码方式,那么对部分接收的信息进行解码是很危险的。因为,已经接收到的信息,与尚未传达的数据包之间的某个字符,可能已经被分隔了。
如果只想通过网络发送文本,只需要考虑编码与封帧问题就可以。
有时,可能希望用一种更紧凑的格式来表示数据,使用文本无法达到该目的。另外,编写的Py代码可能会,与某个已经使用原始二进制数据的服务进行交互,需担心一个新问题---网络字节顺序
许多协议会简单把这个整数当做str’4253’来传输,当做4个单独的字符来传输。无论使用任何一种常见的文本编码方式,4个字符至少需要4个字节来传输,十进制还会引入一些计算开销,数字并不是以十进制存储的,程序会使用反复的除法运算来检查余数,这个程序中,会对要发送的值进行反复除法,发现它其实是由4个1000、2个100、5个10、3个1构成的。当接收方接收到长度为4的字符串’4253’时,需要进行反复的加法与10的幂的乘法,把收到的文本转换为数字。
使用纯文本表示数字是如今web上最流行的技术。如,当获取一个网页时,HTTP就会使用一个包含十进制的字符串(如’4253′)表示结果的内容长度,虽然要付出一定的花销,但是网络Serv和Cli都会完成该str与十进制数的准换。
过去的20年,网络的发展其实就是把二进制替换成了更容易阅读的协议,尽管要比以前付出更多的计算花销
Py使用一个int来表示’4253’这个数值,PC将它存储为一个二进制数,使用多个连续字节中的位来表示1、2、4;
可以在Py使用内置的hex()函数来查看int的存储方式
hex(4253)
'0x109d'
每个十六进制位都对应4个二进制位,因此每2个十六进制位表示1个字节的数据。这个数字没有通过4个十进制来存储(4、2、5、3),如果使用十进制,第一个4就是最高位,3就是最低位。然而该数字是按16进制存储的,0x10是最高位字节,0x9d是最低位字节,两个字节在内存中直接相邻
只要使用Py的struct模块,可以方便简单地看到两种方法的区别,提供了用于将数据与流行的二进制格式进行相互转换所需的各种操作,先用小端法表示4253,然后是大端法的表示形式
import struct
struct.pack('<i',4253) # 小端法 b'\x9d\x10\x00\x00' struct.pack('>i', 4253) # 大端法
b'\x00\x00\x10\x9d'
</i',4253)>
使用了struct模块的格式化代码i,表示使用4字节存储一个整数。对于4253的这样较小的数字,前两个高位字节为0。可把struct表示端模式的两个符号
>>> import struct
>>> struct.unpack('>i', b'\x00\x00\x10\x9d')
(4253,)
1)使用struct模块用于网络传输的二进制数据,接收方收到数据后,使用struct模块进行解码
2)要自己控制网络传输的数据格式的话,在选择网络字节顺序时使用!前缀
3)如果其他人设计了协议并使用小端法,必须使用<
5.2. 封帧与引用
使用UDP数据报进行通信,协议本身就会使用独立的,可识别的块进行数据传输,不过,如果网络出现问题,就必须自己重新排列并发送这些数据块。
封帧,需要考虑的问题是:接收方何时最终停止调用recv()才是安全的?
整个消息/数据何时才能完整无缺的传达?
何时才能将接收到的信息作为一个整体来解析/处理?
5-1 streamer.py
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)
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)
message = b''
while True:
more = sc.recv(8192) # arbitrary value of 8k
if not more: # socket has closed when recv() returns ''
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)
socket.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 the 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))
使用脚本运行Serv、Cli,数据完好无损地发送给了服务器,Cli关闭socket后,生成了文件结束符,表示这次通信唯一需要的帧。
$ 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', 54458)
Received 98 bytes
Received zero bytes - end of file
Message:
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
这个socket没有准备接收任何数据,当Cli和Serv不再进行某一方向的通信时,会立即关闭该方向的连接。避免了另一方向上使用socket,否则可能会在缓冲区队列中,填入太多未读取的数据,造成死锁。
Cli和Serv,其中之一调用socket的shutdown()方法也是相当必要的,在Cli和Serv的socket上都调用shutdown,不仅会对称,也能提高冗余性。
1)通过流在一个方向上发送信息,然后关闭该方向。
2)在另一方向上通过流发送数据。
3)关闭socket。
4)一定要先完成一个方向上的数据传输,再反过来在另一方向上通过流发送数据,否则,可能使Serv和Cli发生死锁。
def recvall(sock, length):
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
因为很少有数据是用于静态边界,所以定长消息少见。不过,在传输特定二进制数据时(发送一个始终产生同样长度的数据块的struct格式),可能会发现,定长消息在某些情况下是不错的选择
1)接收方会进入recv()循环并不断等待,直到不断累加的返回str包含表示消息结束的定界符为止。
2)确保消息汇总的字节或字符,在特定的有限范围内,自然就可以选择该范围外的某个符号作为消息的结束符。如,正在发送的ASCII字符串,可以选择空字符’\o’作为定界符,也可选择像’\xff’处于ASCII字符范围外的字符。
3)如果消息可以包含任意数据,那么定界符的使用就是一个问题了:
要是用作定界符的字符,作为数据的一部分出现了,该怎么办?
答案:引用,和Py-str中使用’来表示单引号类似,’All’s well taht ends well.’
只有在消息使用的字母表有限,才能使用定界符机制。
既想利用5.2.1.5的简洁高效,又无法事先得知每个消息的长度(发送者无法从数据源中事先得到消息长度),该怎么办?
5-2展示了5.2.1.6.的想法,只在一个方向上发送数据–从Cli像Serv发送。但使用的数据结构:每个消息前面都加上了一个struct作为前缀。struct包含了使用4B表示的长度,由于I表示使用32位的无符号整数,因此每个帧的长度最大为4GB,像Serv发送3个连续的数据块,然后发送一个长度为0的消息,由长度字段0+空消息数据组成,表示所有数据块已经发送完成。
5-2 blocks.py
import socket, struct
from argparse import ArgumentParser
header_struct = struct.Struct('!I') # messages up to 2**32 -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)
sc, sockname = sock.accept()
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)
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: %(defaults)s)')
parser.add_argument('-c', action='store_true', help='run as the client')
parser.add_argument('-p', type=int, metavar='port', default=1060)
args = parser.parse_args()
function = client if args.c else server
function((args.hostname, args,p))
只有在一个循环中调用recv(),该代码才是正确的,能够不断接受更多数据(以防万一),直至所有4B的数据都被接收为止。
5.3. pickle与自定义定界符的格式
>>> import pickle
>>> pickle.dumps([5, 6, 7])
b'\x80\x03]q\x00(K\x05K\x06K\x07e.'
输出数据中的字符串末尾的.字符,用于标记一个pickle的结束。遇到.后,加载器将停止读取,并立刻返回数据。在上述pickle的末尾加上一些奇怪的数据,可以发现loads()会完全忽略加上的数据,并返回原始的列表。
>>> pickle.loads(b'\x80\x03]q\x00(K\x05K\x06K\x07e.blahahahah')
[5, 6, 7]
这种使用loads()的方式在处理网络数据时是无用的,我们并不知道重新加载这个pickle时,需要处理多少字节的数据,也不知道str中有多少属于pickle数据。
>>> from io import BytesIO
>>> f = BytesIO(b'\x80\x03]q\x00(K\x05K\x06K\x07e.hahahahhahaha')
>>> pickle.load(f)
[5, 6, 7]
>>> f.tell()
14
>>> f.read()
b'hahahahhahaha'
使用pickle处理大型数据结构时,涉及很多细节,尤其是数据结构中包含除了int、str、list、dict等简单内置类型外的Py对象时,更是如此。
5.4. XML与JSON
如果要涉及支持其他编程语言的协议/只是希望使用通用标准,而不是特定于Py的格式,JSON/XML两种数据格式都是很流行的选择。这两种格式本身都不支持封帧,在处理网络数据前,先要使用某种方法,提取出完整的文本str
JSON是用于两种不同PC语言间,发送数据的最佳选择之一,Py2.6开始,标准库提供了对JSON的支持,封装在名为json的模块中,该模块提供了用于对简单数据结构进行序列化的通用技术
>>> import json
>>> json.dumps([51, 'Namarie!'])
'[51, "Namarie!"]'
>>> json.dumps([51, 'Namarie!'], ensure_ascii=False)
'[51, "Namarie!"]'
>>> json.loads('{"name": "Lancelot", "quest": "grail"}')
{'name': 'Lancelot', 'quest': 'grail'}
5.5. 压缩
数据在网络传输中所需的时间,通常远远多于CPU准备所用的时间。在发送前对数据进行压缩,通常是非常值得的,HTTP协议会让Serv和Cli自己来确认它们是否支持压缩。
>>> import zlib
>>> 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.'
>>> len(data)
28
大多数压缩机制,在接收的数据量极小时,得到的结果都比原始数据更长,而不是更短。由于为了进行压缩而额外需要的数据量,反而超过了压缩掉的数据量。
假设这28B是以每个数据包8B的形式,发送至接收方。在处理完第一个数据包后,解压缩对象的unused_data槽仍然是空的,表示还有数据尚未处理。
>>> d = zlib.decompressobj()
>>> d.decompress(data[0:8]), d.unused_data
(b'Pytho', b'')
希望再次运行socket的recv(),当把第二个包含8个字符的数据块,传递给解压缩对象时,除了会返回想要的压缩数据外,还会返回一个非空的unused_data值,表示已经接收到了b’.’这一字节
>>> d.decompress(data[8:16]), d.unused_data
(b'n', b'.x')
无论第一部分压缩数据后,还有什么数据,接下来的一个字符一定是第二部分数据的第一个字节。
由于正在等待更多压缩数据,会把’x’传递给一个新的解压缩对象,再把后面两个模拟的8B”数据包”传递给该压缩对象。
>>> d = zlib.decompressobj()
>>> d.decompress(b'x'), d.unused_data
(b'', b'')
>>> d.decompress(data[16:24]), d.unused_data
(b'zlib', b'')
>>> d.decompress(data[24:]), d.unused_data
(b'', b'.')
此时,unused_data再次变得非空,表示已经读取到了第二部分压缩数据的结尾。由于已经知道数据完整无缺地传达到了,边可以对数据内容进行处理了。
5.6. 网络异常
示例中,一般只捕捉可能会发生的异常,2-2说明socket超时的时候,捕捉了socket.timeout异常,因为socket发出超时通知时,使用的就是socket.timeout。忽略了其他所有异常,如命令行提供非法hostname;调用bind()时提供远程IP;bind()要使用的port已经被占用;无法连接通信对方/对方停止响应
正在运行中的socket会引发哪些错误?
>>> import socket
>>> s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>>> try:
... s.connect(('nonexistent.hostname.foo.bar', 80))
... except socket.gaierror as e:
... raise
...
Traceback (most recent call last):
...
socket.gaierror: [Errno -2] Name or service not known
>>> e.errno
-2
>>> e.strerror
'Name or service not known'
使用Py提供的基于socket的高层协议有一个重要问题:是否允许在代码中直接处理原始socket错误?
是否要捕捉原始socket错误,将它们转换为协议特定的错误类型?
Py标准库本身的实现,就存在这两种方法。如,httplib认为自己是相对底层的,在连接到未知hostname时能够看到底层socket错误
>>> import http.client
>>> h = http.client.HTTPConnection('nonexistent.hostname.foo.bar')
>>> h.request('GET', '/')
Traceback (most recent call last):
...
socket.gaierror: [Error -2] Name or service not known
urlib2把相同的错误隐藏了起来,并抛出一个URLError。可能是因为urllib2认为自己是一个用于将URL解析为文档的系统,所以希望保持相应的语义。
>>> import urllib.request
>>> urllib.request.urlopen('http://nonexistent.hostname.foo.bar/')
Traceback (most recent call last):
...
socket.gaierror: [Errno -2] Name or service not known
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
...
urllib.error.URLError: <urlopen error [errno -2] name or service not known < code></urlopen>
故,根据使用协议的不同,有时只需处理协议特定的异常,有时则可能既需要处理协议特定的异常,又需处理原始socket错误。
1)有时会将异常封装,提供给其他调用我们API的程序员使用;
2)有时会中途拦截某些异常,把合适的信息提供给终端用户。
将异常传递给使用API的用户时,有两种方法。使用简明的方法处理异常是很有帮助的。
如,编写一个小型的mycopy()方法,用于再远程机器间复制文件。如只使用socket.error,调用方就无从得知错误是源于源机器的连接问题,还是目标机器的连接问题/任何其他问题。自己定义了与API语义有紧密联系的异常(如SourceError和DestinationError),就好多了。可以使用raise…from语句在异常链中,包含原始socket错误,即使API使用这希望深入查看错误信息也没问题。
class DestinationError(Exception):
def __str__(self):
return '%s: %s' % (self.args[0], self.__cause__.strerror)
...
try:
host = sock.connect(address)
except socket.error as e:
raise DestinationError('Error connecting to destination') from e
假设DestinationError只封装继承自OSError的异常(如socket.error),否则异常原因包含的文本信息,除了strerror外还有别的属性,那么__str__()函数就更复杂了。
此例说明了这一模式的原理,即调用者捕捉DestinationError后,可通过__cause__来获取它们实际捕捉到的,包含丰富语义的异常背后的网络错误。
捕捉异常,有两种基本方法:granular异常处理程序、blanket异常处理程序
1)整个程序都用于连接许可证Serv
2)这个函数中的所有socket都用于从SQL获取响应
3)最后一部分代码都用来进行清理与关闭操作
外部程序(收集输入、命令行参数、配置信息,调用代码段的程序)使用try…except语句调用这些代码段,如下:
import sys
...
try:
deliver_updated_keyfiles(...)
except (socket.error, socket.gaierror) as e:
print('cannot deliver remote keyfiles: {}'.format(e), file=sys.stderr)
exit(1)
最好在代码中,抛出自己设计的表示程序终止,并为用户打印出错误信息的异常。
except:
FatalError('cannot send replies: {}'.format(e))
在程序的顶层捕捉抛出的所有FatalError异常,并打印出错误信息。如果有一天,希望增加一个命令行选项,把严重错误发送到系统的错误日志,而不是直接打印到屏幕,只需修改一处的代码即可,无需到处修改。
如,一个工具程序,每隔一段时间就将它的状态通过电子邮件发送出去,不想关闭这个程序。相反,可能会让发送电子邮件的线程,把错误输出到日志,等待几分钟,重新尝试发送。
这种情况下,将特定的多个连续网络操作结合起来,将其看做单个操作。可能成功,也可能失败。把它看成一个整体,使用try…except为其编写异常处理程序。”如果发生任何问题,先暂停,等待10min,重新尝试发送电子邮件”,进行的网络操作的结构和逻辑,将会决定如何部署try…except从句
5.7. 小结
Original: https://www.cnblogs.com/wangxue533/p/11996956.html
Author: 罗生堂下
Title: 读书笔记_python网络编程3_(5)
原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/591362/
转载文章受原作者版权保护。转载请注明原作者出处!