一、字符与字节
Python 中有两种表示字节的方法:
第一种是使用一个介于0~255之间的整数;
第二种是使用一个字节字符串(字符串的唯一内容就是该字节本身)。
字节字符串的实现通过bytes()实现:
首先说说最流行的ASCII编码方式:ASCII定义了0~127的字符代码,对应7位二进制位,所以最高位永远是0。
Python常用的时Unicode的编码方式,所以在Python中:
(1)encodeing:对字符进行编码,即将真正的Unicode字符串转化为字节字符串
(2)decodeing:对字节数据进行解码,即将字节字符串转化为真正的Unicode字符。
世界上的编码方式大致可以分为两类:
(1)单字节编码(最简单):最多可表示256个独立的字符,不过可以保证每个字符都能唯一映射到一个单独的字节。
(2)多字节编码:一些多字节编码会使用固定的字节数表示一个字符,比如UTF-32。优点是表示每个字符的字节数都是相同的。目前最常用的是UTF-8,和ASCII码差不多,唯一区别是,它包含的字符都是从大于127的代码开始的。
二、封帧与引用
- 如果使用UDP数据报进行通信,那么协议本身就会使用独立的、可识别的模块进行数据传输。
- 如果使用TCP进行通信,就要面对封帧问题。即如何分割消息,让接受方可以识别消息的开始与结束。recv()就必须运行多次才能收到完整的数据包。
关于封帧的六种模式:
- 模式一:只涉及数据的发送,不关注响应。
发送方循环发送数据,直到所有数据都被传递给sendall()为止,然后使用close()关闭套接字。接收方只需要不断调用recv(),直到recv()最后返回一个空字符串(表示发送方已经关闭了套接字)为止。
#!/usr/bin/env python3
# Foundations of Python Network Programming, Third Edition
# https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter05/streamer.py
# Client that sends data then closes the socket, not expecting a reply.
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)
sc.shutdown(socket.SHUT_WR) #单向关闭写入
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)
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 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))
结果:
由于这个套接字并没有准备接受任何数据,因此,但C和S不再进行某一方向的通信时会立即关闭该方向的连接。这一做法避免了在另一方向上使用套接字。
- 模式二:在两个方向上都通过流发送信息。
套接字最开始在两个方向上都是打开的。首先通过流在一个方向上发送信息,然后关闭该方向。接着在另一方向上通过流发送信息,最后关闭套接字。
注:一定要先完成一个方向上的数据传输,然后再反过来再另一个方向上通过流发送数据,否则容易死锁。
- 模式三:使用定长消息
可以使用sendall()发送字节字符串,然后自己设计一个recvall()循环确保接收完整的消息。
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
- 模式四:使用特殊字符来划分消息的边界。
比方遇到 ‘.’ 判定消息发送结束。
- 模式五:在每个消息前加上其长度作为前缀。
只要接收方读取并解码了长度,就能进入循环,重复调用recv(),知道整个消息都传达为止。
- 模式六(模式五的拓展):因为实际上我们并非发送单个消息,而是多个数据块。所以,在每个数据块前加上数据块长度作为其前缀。