1.字节与字符串。
计算机的内存芯片和网卡都支持将字节作为通用传输单元。字节将8比特的信息封装起来,作为信息存储的通用单元。
- 位是信息的最小单元。每位可以是0或者1。在电子学中,位一般通过高电压和低电压实现。
- 8 位 组 成 1 字 节 ( b y t e ) 。 \color{red}{8位组成1字节(byte)。} 8位组成1字节(byte)。
- 网 络 标 准 使 用 8 位 字 节 这 一 术 语 表 示 8 个 二 进 制 位 组 成 的 字 节 。 \color{red}{网络标准使用8位字节这一术语表示8个二进制位组成的字节。} 网络标准使用8位字节这一术语表示8个二进制位组成的字节。
在python中,通常有两种表示字节的方法:第一种是使用一个正好介于0~255的整数,第二种是使用一个字节字符串,字符串的唯一内容就是该字节本身。可以使用Python源代码支持的任何常用进制来输入字节表示的数字。
可以把一个包含这些数字的列表作为参数传给bytes(),这样就能够将其转换成字节字符串。通过遍历字节字符串,可以将其转换会原来的形式。
如果确实想要通过套接字传输一个符号串,那么就需要使用某种编码方法,来为每个符号分配一个确切的字节值。ACSII就是最流行的编码方式。下图就是三类ASCII字符。
左上角的是空格,字符代码为32,右下角是删除符字符代码为127。世界上有许多种可用的编码方式,可以将它们分为两大类:
- 单 字 节 编 码 : \color{red}{单字节编码:} 单字节编码:这种编码方式做多表示256个独立的字符,不过可以保证每个字符都能唯一映射到一个单独的字节。
- 多 字 节 编 码 : \color{red}{多字节编码:} 多字节编码:它表示每个字符的字节数都是相同的。
Python的所有编码方式都受惠于叫做Unicode的标准。Python把字符串看成是由Unicode字符组成的序列。就和常见的Python数据结构一样,Python也向我们小心地隐藏了Python字符串在RAM中的实际实现,因此在使用Python工作时无需考虑字符串的内部实现。
2.二进制数与网络字节顺序。
我们编写Python代码也可能会与某个已经使用原始二进制数据进行交互。其实无论你使用那种数据格式进行交互都得考虑一个问题——
网
络
字
节
顺
序
\color{red}{网络字节顺序}
网络字节顺序。例如:
要在网络上发送一个整数‘’4253‘’,无论你使用任何一种常见的编码方式,4个字符至少都需要4个字节来传输。由于数字在计算机中并不是以十进制存储的,因此程序会使用反复除法运算来检查余数。在这个程序中,会对要发送的值进行反复除法,然后发现它其实是由4个1000、2个100、5个10和3个1构成的。当接收方接收到长度为4的字符串‘4253’时,需要反复的加法和与10的幂的乘法,把收到的文本转换回数字。
Python使用一个整型变量来表示‘4253’这个数字,都会将其转化为二进制数据,它的存储方式如下图:
这个数字是按照十六进制存储的,0x10是最高位字节0x9d是最低位字节,两个字节在内存中直接相邻。但是这两个字节到底是以哪种顺序排列的呢?
一些计算机使用
大
端
法
\color{red}{大端法}
大端法,将最高位字节存储在最前面,就和书写十进制数时一样;还有的计算机使用
小
端
法
\color{red}{小端法}
小端法,将最低位字节存储在前面。使用Python的struct模块,可以看出两种方法的区别:
小于号为小端法,大于号为大端法。为了方便记忆我们可以把struct表示端模式的两个符号<和>看成是与两种字节排序对应的,箭头所指的方向就是字节字符串中的最低位。相反我们还可以使用unpack()操作将二进制数据转换回Python数字。
3.封帧与引用。
如果我们使用TCP进行通信,那么就得应对封帧的问题,即如何分割消息,使得接收方能够识别消息的开始与结束。
- 模式一:发送发循环发送数据,直到所有数据都被传递给sendall()为止,然后使用close()关闭套接字。接收方只需要不断调用recv(),直到recv()最后返回一个空字符串(表示发送方已经关闭了套接字)为止。
代码:
#!/usr/bin/python
#coding:utf-8
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('Rum 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)
if not more:
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)
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='Tran & recevied 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))
测试结果:
为了避免死锁情况,当客户端和服务端不再进行某一方向的通信时会立即关闭该方向的连接即客户端和服务器都调用了套接字的shutdown()方法。
- 模式二:即在两个方向上都通过流发送信息。套接字最开始在两个方向上都是打开的。首先通过流在一个方向上发送信息,然后关闭该方向。接着在另一方向上通过流发送数据。最后关闭套接字。
- 模式三:即发送定长数据。曾在TCP博文中用到过。
代码:
def recvall(sock,length):
data = b''
while len(data) < length:
more = sock.recv(length - len(data))
if not more:
raise EOFError('was expecting %d bytes but only received'
' %d bytes before the socket closed'
% (length,len(data)))
data += more
return data
- 模式四:使用特殊字符来划分消息边界。接收方会进入上面类似的recv()循环并不断等待,直到不断累加的返回字符串包含表示消息结束的定界符为止。
- 模式五:在每个消息前加上其长度作为前缀。通常会使用一个定长的二进制整数或是在变长的整数字符串后面加上一个文本定界符来表示长度。
- 模式六:发送多个数据块,并且在每个数据块前加上数据块长度作为其前缀。这意味着,每个新的信息块对发送者都是可见的,可以使用数据块长度作为其打上标签,然后将数据块置入发送流中。
代码:
#!/usr/bin/python
#coding:utf-8
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 {} 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 blocks 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 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))
测试结果:
代码在每个消息前面都加上了struct最为前缀。struct在包含了使用4B表示的长度。由于 I 表示使用32位无符号整数,因此每个帧的长度最大为4GB。代码向服务器发送了3个连续的数据块,然后发送一个长度为0的消息。长度为0的消息有长度字段0及其后跟的空消息组成,表示所有数据块都已发送完成。
4.网络异常。
针对套接字的异常如下:
- OSError:这是socket()模块的可能抛出的主要错误。网络传输的所有阶段可能发生的任何问题都有会抛出该异常。
- socket.gaierror():该异常在getaddrinfo()无法找到提供的名称或服务时被抛出,如果我们向bind()或connect()这样的调用传入一个主机名而不是IP地址的话,该异常也会在主机名查询失败的情况下抛出。
- socket.timeout:如果我们给套接字设置了超时参数或者使用的库设定了套接字超时参数并且等待时间超过了你设定的参数时,才会抛出这个异常。
异常捕获:
- 直接捕获异常:使用try…except语句,然后在except()从句中打印出简洁的错误信息。
- 将网络错误封装成自己的异常:即自己编写异常函数,然后使用raise…from语句在异常链中包含原始套接子错误。