一.C3算法
mro 即 method resolution order(方法解释顺序),主要用于在多继承时判断属性的路径(来自于哪个类)
在python2.2版本中,算法基本思想是根据每个祖先类的继承结构编译出一张列表,包括搜索到的类,按策略删除重复的,但是,在维护单调性方面失败过(顺序保存),所以2.3版本中采用了新算法C3.
为什么采用C3算法
C3算法最早提出是用Lisp的,应用在python中是为了解决原来基于深度优先搜索算法不满足本地优先级,和单调性的问题.
本地优先级:指的是声明时父类的顺序,比如C(A,B),如果访问C类对象属性时,应该根据声明顺序,优先查找A类,然后在查找B类.
单调性:如果在C的解析顺序中,A排在B的前面,那么在C的所有子类型中,也必须满足这个顺序.
C3算法:判断mro要先确定一个线性序列,然后查找路径由序列中类的顺序决定,所以C3算法就是生成一个线性序列.
如果继承至一个基类:
class B(A)
这时B的mro序列为[B,A]
如果继承至多个基类
class B(A1,A2,A3...)
这时B的mro序列 mro(B) = [B] + merge(mro(A1),mro(A2),mro(A3)...,[A1,A2,A3])
merge操作就是C3算法的核心
遍历执行merge操作的序列,如果一个序列的第一个元素,时其他序列中的第一个元素,或不在其他序列出现.则从所有执行merge操作序列中删除这个元素,合并到当前的meo中.
merge操作后的序列,继续执行merge操作,知道merge操作的序列为空.
如果merge操作的序列无法为空,则说明不合法.
例如:
class A(O):pass
class B(O):pass
class C(O):pass
class E(A,B):pass
class F(B,C):pass
class G(E,F):pass
A,B,C都继承至一个基类,所以mro序列一次为[A,O],[B,O],[C,O]
mro (E) = [E] + merge (mro(A),mro(B),[A,B])
= [E] + merge([A,O],[B,O],[A,B])
执行merge操作的序列为[A,O],[B,O],[A,B]
A时序列[A,O]中的第一个元素,在序列[B,O]中不出现,在序列[A,B] 中也是第一个元素,所以从执行merge操作的序列([A,O],[B,O],[A,B])中删除A,合并到当前mro,[E]中.
mro(E) = [E,A] + merge([O],[B,O],[B])
再执行merge操作,O是序列[O]中的第一个元素,但O在序列[B,O]中出现并且不是其中第一个元素.继续查看[B,O]的第一个元素B,B 满足条件,所以从执行merge操作的序列中删除B,合并到[E,A]中.
mro[E] = [E,B,A] + merge([O],[O])
= [E,A,B,O]
实现C3算法代码
#-*- encoding:GBK -*-# def mro_C3(*cls): if len(cls)==1: if not cls[0].__bases__: return cls else: return cls+ mro_C3(*cls[0].__bases__) else: seqs = [list(mro_C3(C)) for C in cls ] +[list(cls)] res = [] while True: non_empty = list(filter(None, seqs)) if not non_empty: return tuple(res) for seq in non_empty: candidate = seq[0] not_head = [s for s in non_empty if candidate in s[1:]] if not_head: candidate = None else: break if not candidate: raise TypeError("inconsistent hierarchy, no C3 MRO is possible") res.append(candidate) for seq in non_empty: if seq[0] == candidate: del seq[0]
二.客户端/服务器架构
1.硬件C/S架构
C/S即:Client与Server,中文意思是:客户端与服务器端架构,这种架构也是从用户层面(也可以是物理层面)来划分的.
这里的客户端一般泛指客户端应用程序EXE,程序需要先安装后,才能运行在用户的电脑上,对用户的电脑操作系统环境依赖较大.
2.软件 C/S架构
B/S即:Browser与Server,中文意思:浏览器与服务端架构,这种架构是从用户层面来划分的.
Browser浏览器,其实也是一种Client客户端,只是这个客户端不需要大家全装什么应用,只需在浏览器上通过HTTP请求服务器端相关的资源(网页资源),客户端Browser浏览器就能进行增删改查.
三.网络基础
1.一个程序如何在网络上找到另一个程序
首先,程序必须要启动,其次,必须由这台机器的地址,我们都知道我们人的地址大概就是国家省市区街道楼门牌号这样子.那么每一台联网的机器在网络上也有自己的地址,他的地址是怎么表示的呢?
就是用一串数字来表示的,例如:192.168.1.1
IP地址是指互联网协议地址(英语:Internet Protocol Address,又译为网际协议地址),是IP Address的缩写。IP地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。 IP地址是一个32位的二进制数,通常被分割为4个“8位二进制数”(也就是4个字节)。IP地址通常用“点分十进制”表示成(a.b.c.d)的形式,其中,a,b,c,d都是0~255之间的十进制整数。例:点分十进IP地址(100.4.5.6),实际上是32位二进制数(01100100.00000100.00000101.00000110)。
"端口"是英文port的意译,可以认为是设备与外界通讯交流的出口。
因此ip地址精确到具体的一台电脑,两端口精确到具体的程序.
2.osi七层模型
引子
须知一个完整的计算机系统是由硬件,操作系统,应用软件三者组成的,具备了这三个条件,一台计算机系统就可以自己跟自己玩了(打个游戏,扫个雷....)
加入要和别人玩,那就需要上网,什么是互联网?
互联网的核心就是由一堆协议组成,协议就是标准,比如全世界人通信的标准呢是英语,如果把计算机比作人,互联网协议就是计算机界的英语.所有的计算机都学会了英语,那计算机就可以按照统一的标准去收发信息从而完成通信.
osi七层模型
人们按照分工不同把互联网协议从逻辑上划分了层级:
3.socket概念
socket层
理解socket
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口.在设计模式中.Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来 说,一组简单的接口就是全部,让Socket去组织数据,以符合指定协议.
站在你的角度上看Socket
其实站在你的角度上看,socket就是一个模块。我们通过调用模块中已经实现的方法建立两个进程之间的连接和通信。 也有人将socket说成ip+port,因为ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序。 所以我们只要确立了ip和port就能找到一个应用程序,并且使用socket模块来与之通信。
4.套接字(socket)的发展史
套接字起源于20世纪70年代加利福尼亚大学伯克利分校版本的unix,即人们所说的BSD unix.因此,有时人们也把套接字称为"伯克利套接字"或者"BSD"套接字.一开始,套接字被设计用在同一台主机上多个应用程序之间的通信,这也被称为进程间通讯,或IPC,套接字有两种(或者称为两个种族),分别是基于文件型和基于网络型的.
基于文件型的套接字家族
套接字家族名字:AF_UNIX
unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运 行在统一机器,可以通过访问统一文件系统间接完成通信.
基于网络类型的套接字家族
套接字家族名字:AF_INET
(还有AF_INET5被用于ipv6,还有一些其他的地址家族,不过他们要不用于某个平台,要么已经被废弃,或者是很少使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我们只使用AF_INET)
5.TCP协议和UDP协议
TCP(Transmission Control Protocol)可靠的.面向连接的协议(eg:打电话).传输效率低全双工通信(发送缓存&接收缓存).面向字节流.使用TCP的应用:Web浏览器.电子邮件,文件传输程序.
UDP(User Datagram Protocol) 不可靠的.无连接的服务,传输效率高(发送前延时小),一对一,一对多,多对一,多对多,面向报文,尽最大努力服务,无拥塞控制.使用UDP的应用:域名系统(DNS),视频流;IP语音(VoIP).
四.套接字的使用
基于TCP协议的的Socket
TCP是基于链接的,必须先启动服务器,然后在启动客户端在去连接服务端
server端
import socket sk = socket.socket() sk.bind(('127.0.0.1',8898)) #把地址绑定到套接字 sk.listen() #监听链接 conn,addr = sk.accept() #接受客户端链接 ret = conn.recv(1024) #接收客户端信息 print(ret) #打印客户端信息 conn.send(b'hi') #向客户端发送信息 conn.close() #关闭客户端套接字 sk.close() #关闭服务器套接字(可选)
client端
import socket sk = socket.socket() # 创建客户套接字 sk.connect(('127.0.0.1',8898)) # 尝试连接服务器 sk.send(b'hello!') ret = sk.recv(1024) # 对话(发送/接收) print(ret) sk.close() # 关闭客户套接字
服务端套接字函数
s.bind() 绑定(主机,端口号)到套接字
s.listen() 开始TCP监听
s.accept() 被动接收TCP客户的连接,阻塞式,等待连接的到来
客户端套接字函数
s.connect() 主动初始化TCP服务器连接
s.connect_ex() connect()函数的扩展版本,出错时,返回出错码,而不是抛出异常
公共用途的套接字函数
s.recv() 接收TCP 数据
s.send() 发送TCP 数据(send在等待发送数据量大于己端缓存区神域空间时,数据丢失,不会发完)
s.sendall() 发送完整的TCP数据(本质就是循环调用send,sendall在待发送数量大于己端缓存区剩余空间时,数据不会丢失,循环调用send直到发完)
s.recvfrom() 接收UDP 数据
s.sendto() 发送UDP 数据
s.getpeername() 连接到当前套接字的远端地址
s.getsockname() 当前套接字的地址
s.getsockopt() 返回指定套接字的参数
s.setsockopt() 设置指定套接字的参数
s.close() 关闭套接字
面向锁的套接字方法
s.setblocking() 设置套接字的阻塞与非阻塞模式
s.settimeout() 设置阻塞套接字操作的超时时间
s.gettimeout() 得到阻塞套接字操作的超市时间
面向文件的套接字的函数
s.fileno() 套接字的文件描述符
s.makefile() 创建一个与该套接字相关的文件
1:用打电话的流程快速描述socket通信 2:服务端和客户端加上基于一次链接的循环通信 3:客户端发送空,卡主,证明是从哪个位置卡的 服务端: from socket import * phone=socket(AF_INET,SOCK_STREAM) phone.bind(('127.0.0.1',8081)) phone.listen(5) conn,addr=phone.accept() while True: data=conn.recv(1024) print('server===>') print(data) conn.send(data.upper()) conn.close() phone.close() 客户端: from socket import * phone=socket(AF_INET,SOCK_STREAM) phone.connect(('127.0.0.1',8081)) while True: msg=input('>>: ').strip() phone.send(msg.encode('utf-8')) print('client====>') data=phone.recv(1024) print(data) 说明卡的原因:缓冲区为空recv就卡住,引出原理图 4.演示客户端断开链接,服务端的情况,提供解决方法 5.演示服务端不能重复接受链接,而服务器都是正常运行不断来接受客户链接的 6:简单演示udp 服务端 from socket import * phone=socket(AF_INET,SOCK_DGRAM) phone.bind(('127.0.0.1',8082)) while True: msg,addr=phone.recvfrom(1024) phone.sendto(msg.upper(),addr) 客户端 from socket import * phone=socket(AF_INET,SOCK_DGRAM) while True: msg=input('>>: ') phone.sendto(msg.encode('utf-8'),('127.0.0.1',8082)) msg,addr=phone.recvfrom(1024) print(msg) udp客户端可以并发演示 udp客户端可以输入为空演示,说出recvfrom与recv的区别,暂且不提tcp流和udp报的概念,留到粘包去说
五.黏包
1.黏包现象
让我们基于tcp先制作一个远程执行命令的程序(命令ls -l ; lllllll ; pwd)
res=subprocess.Popen(cmd.decode('utf-8'), shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE) 的结果的编码是以当前所在的系统为准的,如果是windows,那么res.stdout.read()读出的就是GBK编码的,在接收端需要用GBK解码 且只能从管道里读一次结果
同时执行多条命令之后,得到的结果很可能只有一部分,在执行其他命令的时候又接收到之前执行的另外一部分结果,这种现象就是黏包.
基于TCP协议实现的黏包
服务端
#_*_coding:utf-8_*_ from socket import * import subprocess ip_port=('127.0.0.1',8888) BUFSIZE=1024 tcp_socket_server=socket(AF_INET,SOCK_STREAM) tcp_socket_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) tcp_socket_server.bind(ip_port) tcp_socket_server.listen(5) while True: conn,addr=tcp_socket_server.accept() print('客户端',addr) while True: cmd=conn.recv(BUFSIZE) if len(cmd) == 0:break res=subprocess.Popen(cmd.decode('utf-8'),shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) stderr=res.stderr.read() stdout=res.stdout.read() conn.send(stderr) conn.send(stdout)
客户端
#_*_coding:utf-8_*_ import socket BUFSIZE=1024 ip_port=('127.0.0.1',8888) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) res=s.connect_ex(ip_port) while True: msg=input('>>: ').strip() if len(msg) == 0:continue if msg == 'quit':break s.send(msg.encode('utf-8')) act_res=s.recv(BUFSIZE) print(act_res.decode('utf-8'),end='')
基于UDP协议实现的黏包
服务端
#_*_coding:utf-8_*_ from socket import * import subprocess ip_port=('127.0.0.1',9000) bufsize=1024 udp_server=socket(AF_INET,SOCK_DGRAM) udp_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) udp_server.bind(ip_port) while True: #收消息 cmd,addr=udp_server.recvfrom(bufsize) print('用户命令----->',cmd) #逻辑处理 res=subprocess.Popen(cmd.decode('utf-8'),shell=True,stderr=subprocess.PIPE,stdin=subprocess.PIPE,stdout=subprocess.PIPE) stderr=res.stderr.read() stdout=res.stdout.read() #发消息 udp_server.sendto(stderr,addr) udp_server.sendto(stdout,addr) udp_server.close()
客户端
from socket import * ip_port=('127.0.0.1',9000) bufsize=1024 udp_client=socket(AF_INET,SOCK_DGRAM) while True: msg=input('>>: ').strip() udp_client.sendto(msg.encode('utf-8'),ip_port) err,addr=udp_client.recvfrom(bufsize) out,addr=udp_client.recvfrom(bufsize) if err: print('error : %s'%err.decode('utf-8'),end='') if out: print(out.decode('utf-8'), end='')
2.黏包形成的原因
TCP协议中的数据传递.
tcp的拆包机制:
当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。
MTU是Maximum Transmission Unit的缩写。意思是网络上传送的最大数据包。MTU的单位是字节。
大部分网络设备的MTU都是1500。如果本机的MTU比网关的MTU大,大的数据包就会被拆开来传送,
这样会产生很多数据包碎片,增加丢包率,降低网络速度。
面向流的通信特点和Nagle算法
TCP(transport control protocol,传输控制协议),是面向连接的,面向流的,提供可靠性服务. 收发两端(客户端和服务端),都要由一一对应的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,
使用了优化方法(Nagle算法),将多个间隔较小的数据,合并成一个大的数据块,然后进行封包. 这样,接收端,就难于分辨出来了,必须提供科学的拆包机制.即面向流的通信是无消息保护边界的. 对于空消息:tcp是基于数据流的,于是收发消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,
防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),也可以被发送,udp协议会帮你封装上消息头发过去.
可靠黏包的tcp协议:tcp协议数据不会丢失,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲
区内容,数据时可靠的,但是会黏包.
基于tcp协议特点的黏包现象成因
发送端是1k,1k的发送数据,而接收端的应用程序可以2k,2k的提走数据,当然也有可能一次提走3k或6k数据, 或者一次只提走几个字节的数据. 也就是说,应用程序所看到的数据是一个整体,或者说是一个流(stream),一条消息有多少字节对应用程序时不可见的,
因此TCP协议是面向流的协议,每个UDP段都是一条消息,应用程序必须是以消息为单位提取数据,不能一次提取任意字节的数据,
这一点和TCP是很不同的. 怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条消息的时候,
无论底层怎样分段分片,TCP协议层会把整条消息的数据段排序完成后才呈现在内核缓冲区.
例如基于TCP 协议的套接字客户端往服务端上传文件,发送时文件内容时按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何开始,在何处结束.
此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送放往往要收集到足够多的数据后采发送一个TCP字段.若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据.
UDP不会发生黏包
UDP(user datagram protocol,用户数据报协议),是无连接的,面向消息的,提供高效率服务. 不会使用块的合并优化算法,由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)
采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头,(消息来源地址,端口信息等),
这样,对于接收端来说,就容易进行区分处理了.即面向消息的通信是有消息保护边界的.
对于空消息:tcp是基于数据流的,于是手法的消息不能为空,这就需要在客户端和服务端都添加空消息处理机制,
防止程序卡主,而UDP是基于数据报的,即便是你输入的是空内容,也可以直接发,udp协议会帮你封装上消息头发下哦那个过去.
不可靠不粘包的udp协议:udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),
收完了x个字节的数据就算完成,若是y;x数据就会丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠
补充说明:
用udp协议发送时,用sendto函数是最大能发送数据的长度为:65535 - ip头(20) - UDP头(8) = 65507 字节.
用sendto函数发送数据时,如果发送数据长度大于该值,则函数会返回错误.(丢失这个包,在不会进行发送) 用TCP协议发送时,由于TCP协议是数据流协议,因此不存在包大小的限制(暂不考虑缓冲区的大小),这是指在用 send函数时,数据长度参数不受限制.而实际上,所指定的这段数据并不一定会一次性发送出去.如果这段数据比 较长,会被分段发送,如果比较短,可能会等待和下一次数据一起发送.
会发生黏包的两种情况
情况一发送方的缓存机制:
发送端需要等待缓冲区满才发送出去,造成粘包(发送时间间隔很短,数据量很小,会合到一起,产生粘包)
#_*_coding:utf-8_*_ from socket import * ip_port=('127.0.0.1',8080) tcp_socket_server=socket(AF_INET,SOCK_STREAM) tcp_socket_server.bind(ip_port) tcp_socket_server.listen(5) conn,addr=tcp_socket_server.accept() data1=conn.recv(10) data2=conn.recv(10) print('----->',data1.decode('utf-8')) print('----->',data2.decode('utf-8')) conn.close()
#_*_coding:utf-8_*_ import socket BUFSIZE=1024 ip_port=('127.0.0.1',8080) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) res=s.connect_ex(ip_port) s.send('hello'.encode('utf-8')) s.send('egg'.encode('utf-8'))
情况二接收方的缓存机制
接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区上拿上次遗留的数据,产生粘包)
1 #_*_coding:utf-8_*_ 2 from socket import * 3 ip_port=('127.0.0.1',8080) 4 5 tcp_socket_server=socket(AF_INET,SOCK_STREAM) 6 tcp_socket_server.bind(ip_port) 7 tcp_socket_server.listen(5) 8 9 10 conn,addr=tcp_socket_server.accept() 11 12 13 data1=conn.recv(2) #一次没有收完整 14 data2=conn.recv(10)#下次收的时候,会先取旧的数据,然后取新的 15 16 print('----->',data1.decode('utf-8')) 17 print('----->',data2.decode('utf-8')) 18 19 conn.close()
1 #_*_coding:utf-8_*_ 2 import socket 3 BUFSIZE=1024 4 ip_port=('127.0.0.1',8080) 5 6 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 7 res=s.connect_ex(ip_port) 8 9 10 s.send('hello egg'.encode('utf-8'))
总结:黏包现象只发生在tcp协议中:
1.从表面上看,黏包问题主要时因为发送方和接收方的缓存机制,tcp协议面向流通性的特点.
2.实际上,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的.
3.粘包的解决方案
解决方案一
问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕如何让发送 端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有的数据.
1 #_*_coding:utf-8_*_ 2 import socket,subprocess 3 ip_port=('127.0.0.1',8080) 4 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 5 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 6 7 s.bind(ip_port) 8 s.listen(5) 9 10 while True: 11 conn,addr=s.accept() 12 print('客户端',addr) 13 while True: 14 msg=conn.recv(1024) 15 if not msg:break 16 res=subprocess.Popen(msg.decode('utf-8'),shell=True, 17 stdin=subprocess.PIPE, 18 stderr=subprocess.PIPE, 19 stdout=subprocess.PIPE) 20 err=res.stderr.read() 21 if err: 22 ret=err 23 else: 24 ret=res.stdout.read() 25 data_length=len(ret) 26 conn.send(str(data_length).encode('utf-8')) 27 data=conn.recv(1024).decode('utf-8') 28 if data == 'recv_ready': 29 conn.sendall(ret) 30 conn.close()
1 #_*_coding:utf-8_*_ 2 import socket,time 3 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 4 res=s.connect_ex(('127.0.0.1',8080)) 5 6 while True: 7 msg=input('>>: ').strip() 8 if len(msg) == 0:continue 9 if msg == 'quit':break 10 11 s.send(msg.encode('utf-8')) 12 length=int(s.recv(1024).decode('utf-8')) 13 s.send('recv_ready'.encode('utf-8')) 14 send_size=0 15 recv_size=0 16 data=b'' 17 while recv_size < length: 18 data+=s.recv(1024) 19 recv_size+=len(data) 20 21 22 print(data.decode('utf-8'))
存在的问题:
程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送改字节流的长度,这种方式会 放大网络延迟带来的性能损耗.
解决方案进阶:
刚刚的方法,问题在于我们在发送
我们可以借助一个模块,这个模块可以把要发送的数据长度转换成固定长度的字节,这样客户端每次接收消息之 前只要先接收这个固定长度的内容看一看接下来要接收的信息大小,那么最终接收的数据只要达到这个值就停,就能刚好不多不少的接收完整个数据了.
struct模块
该模块可以把一个类型,如数字,转成固定长的bytes
>>> struct.pack('i',1111111111111) struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #这个是范围
1 import json,struct 2 #假设通过客户端上传1T:1073741824000的文件a.txt 3 4 #为避免粘包,必须自定制报头 5 header={'file_size':1073741824000,'file_name':'/a/b/c/d/e/a.txt','md5':'8f6fbf8347faa4924a76856701edb0f3'} #1T数据,文件路径和md5值 6 7 #为了该报头能传送,需要序列化并且转为bytes 8 head_bytes=bytes(json.dumps(header),encoding='utf-8') #序列化并转成bytes,用于传输 9 10 #为了让客户端知道报头的长度,用struck将报头长度这个数字转成固定长度:4个字节 11 head_len_bytes=struct.pack('i',len(head_bytes)) #这4个字节里只包含了一个数字,该数字是报头的长度 12 13 #客户端开始发送 14 conn.send(head_len_bytes) #先发报头的长度,4个bytes 15 conn.send(head_bytes) #再发报头的字节格式 16 conn.sendall(文件内容) #然后发真实内容的字节格式 17 18 #服务端开始接收 19 head_len_bytes=s.recv(4) #先收报头4个bytes,得到报头长度的字节格式 20 x=struct.unpack('i',head_len_bytes)[0] #提取报头的长度 21 22 head_bytes=s.recv(x) #按照报头长度x,收取报头的bytes格式 23 header=json.loads(json.dumps(header)) #提取报头 24 25 #最后根据报头的内容提取真实的数据,比如 26 real_data_len=s.recv(header['file_size']) 27 s.recv(real_data_len)
1 #_*_coding:utf-8_*_ 2 #http://www.cnblogs.com/coser/archive/2011/12/17/2291160.html 3 __author__ = 'Linhaifeng' 4 import struct 5 import binascii 6 import ctypes 7 8 values1 = (1, 'abc'.encode('utf-8'), 2.7) 9 values2 = ('defg'.encode('utf-8'),101) 10 s1 = struct.Struct('I3sf') 11 s2 = struct.Struct('4sI') 12 13 print(s1.size,s2.size) 14 prebuffer=ctypes.create_string_buffer(s1.size+s2.size) 15 print('Before : ',binascii.hexlify(prebuffer)) 16 # t=binascii.hexlify('asdfaf'.encode('utf-8')) 17 # print(t) 18 19 20 s1.pack_into(prebuffer,0,*values1) 21 s2.pack_into(prebuffer,s1.size,*values2) 22 23 print('After pack',binascii.hexlify(prebuffer)) 24 print(s1.unpack_from(prebuffer,0)) 25 print(s2.unpack_from(prebuffer,s1.size)) 26 27 s3=struct.Struct('ii') 28 s3.pack_into(prebuffer,0,123,123) 29 print('After pack',binascii.hexlify(prebuffer)) 30 print(s3.unpack_from(prebuffer,0)) 31 32 关于struct的详细用法
使用struct解决黏包
借助struct模块,我们知道长度数字可以被转换成标准大小的4字节数,因此可以利用这个特点来预先发送数 据长度.
发送时 | 接收时 |
先发送struct转换好的数据长度4字节 | 先接受4个字节使用struct转换成数字来获取要接收的数据长度 |
再发送数据 | 再按照长度接收数据 |
1 import socket,struct,json 2 import subprocess 3 phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 4 phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加 5 6 phone.bind(('127.0.0.1',8080)) 7 8 phone.listen(5) 9 10 while True: 11 conn,addr=phone.accept() 12 while True: 13 cmd=conn.recv(1024) 14 if not cmd:break 15 print('cmd: %s' %cmd) 16 17 res=subprocess.Popen(cmd.decode('utf-8'), 18 shell=True, 19 stdout=subprocess.PIPE, 20 stderr=subprocess.PIPE) 21 err=res.stderr.read() 22 print(err) 23 if err: 24 back_msg=err 25 else: 26 back_msg=res.stdout.read() 27 28 29 conn.send(struct.pack('i',len(back_msg))) #先发back_msg的长度 30 conn.sendall(back_msg) #在发真实的内容 31 32 conn.close()
1 #_*_coding:utf-8_*_ 2 import socket,time,struct 3 4 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 5 res=s.connect_ex(('127.0.0.1',8080)) 6 7 while True: 8 msg=input('>>: ').strip() 9 if len(msg) == 0:continue 10 if msg == 'quit':break 11 12 s.send(msg.encode('utf-8')) 13 14 15 16 l=s.recv(4) 17 x=struct.unpack('i',l)[0] 18 print(type(x),x) 19 # print(struct.unpack('I',l)) 20 r_s=0 21 data=b'' 22 while r_s < x: 23 r_d=s.recv(1024) 24 data+=r_d 25 r_s+=len(r_d) 26 27 # print(data.decode('utf-8')) 28 print(data.decode('gbk')) #windows默认gbk编码
我们还可以把包头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节.
发送时 | 接收时 |
先发报头长度 |
先收报头长度,用struct取出来 |
再编码报头内容然后发送 | 根据取出的长度收取报头内容,然后解码,反序列化 |
最后发真实内容 | 从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容 |
1 import socket,struct,json 2 import subprocess 3 phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 4 phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加 5 6 phone.bind(('127.0.0.1',8080)) 7 8 phone.listen(5) 9 10 while True: 11 conn,addr=phone.accept() 12 while True: 13 cmd=conn.recv(1024) 14 if not cmd:break 15 print('cmd: %s' %cmd) 16 17 res=subprocess.Popen(cmd.decode('utf-8'), 18 shell=True, 19 stdout=subprocess.PIPE, 20 stderr=subprocess.PIPE) 21 err=res.stderr.read() 22 print(err) 23 if err: 24 back_msg=err 25 else: 26 back_msg=res.stdout.read() 27 28 headers={'data_size':len(back_msg)} 29 head_json=json.dumps(headers) 30 head_json_bytes=bytes(head_json,encoding='utf-8') 31 32 conn.send(struct.pack('i',len(head_json_bytes))) #先发报头的长度 33 conn.send(head_json_bytes) #再发报头 34 conn.sendall(back_msg) #在发真实的内容 35 36 conn.close()
1 from socket import * 2 import struct,json 3 4 ip_port=('127.0.0.1',8080) 5 client=socket(AF_INET,SOCK_STREAM) 6 client.connect(ip_port) 7 8 while True: 9 cmd=input('>>: ') 10 if not cmd:continue 11 client.send(bytes(cmd,encoding='utf-8')) 12 13 head=client.recv(4) 14 head_json_len=struct.unpack('i',head)[0] 15 head_json=json.loads(client.recv(head_json_len).decode('utf-8')) 16 data_len=head_json['data_size'] 17 18 recv_size=0 19 recv_data=b'' 20 while recv_size < data_len: 21 recv_data+=client.recv(1024) 22 recv_size+=len(recv_data) 23 24 print(recv_data.decode('utf-8')) 25 #print(recv_data.decode('gbk')) #windows默认gbk编码
上传和下载
1 import socket 2 import struct 3 import json 4 import subprocess 5 import os 6 7 class MYTCPServer: 8 address_family = socket.AF_INET 9 10 socket_type = socket.SOCK_STREAM 11 12 allow_reuse_address = False 13 14 max_packet_size = 8192 15 16 coding='utf-8' 17 18 request_queue_size = 5 19 20 server_dir='file_upload' 21 22 def __init__(self, server_address, bind_and_activate=True): 23 """Constructor. May be extended, do not override.""" 24 self.server_address=server_address 25 self.socket = socket.socket(self.address_family, 26 self.socket_type) 27 if bind_and_activate: 28 try: 29 self.server_bind() 30 self.server_activate() 31 except: 32 self.server_close() 33 raise 34 35 def server_bind(self): 36 """Called by constructor to bind the socket. 37 """ 38 if self.allow_reuse_address: 39 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 40 self.socket.bind(self.server_address) 41 self.server_address = self.socket.getsockname() 42 43 def server_activate(self): 44 """Called by constructor to activate the server. 45 """ 46 self.socket.listen(self.request_queue_size) 47 48 def server_close(self): 49 """Called to clean-up the server. 50 """ 51 self.socket.close() 52 53 def get_request(self): 54 """Get the request and client address from the socket. 55 """ 56 return self.socket.accept() 57 58 def close_request(self, request): 59 """Called to clean up an individual request.""" 60 request.close() 61 62 def run(self): 63 while True: 64 self.conn,self.client_addr=self.get_request() 65 print('from client ',self.client_addr) 66 while True: 67 try: 68 head_struct = self.conn.recv(4) 69 if not head_struct:break 70 71 head_len = struct.unpack('i', head_struct)[0] 72 head_json = self.conn.recv(head_len).decode(self.coding) 73 head_dic = json.loads(head_json) 74 75 print(head_dic) 76 #head_dic={'cmd':'put','filename':'a.txt','filesize':123123} 77 cmd=head_dic['cmd'] 78 if hasattr(self,cmd): 79 func=getattr(self,cmd) 80 func(head_dic) 81 except Exception: 82 break 83 84 def put(self,args): 85 file_path=os.path.normpath(os.path.join( 86 self.server_dir, 87 args['filename'] 88 )) 89 90 filesize=args['filesize'] 91 recv_size=0 92 print('----->',file_path) 93 with open(file_path,'wb') as f: 94 while recv_size < filesize: 95 recv_data=self.conn.recv(self.max_packet_size) 96 f.write(recv_data) 97 recv_size+=len(recv_data) 98 print('recvsize:%s filesize:%s' %(recv_size,filesize)) 99 100 101 tcpserver1=MYTCPServer(('127.0.0.1',8080)) 102 103 tcpserver1.run() 104 105 106 107 108 109 110 #下列代码与本题无关 111 class MYUDPServer: 112 113 """UDP server class.""" 114 address_family = socket.AF_INET 115 116 socket_type = socket.SOCK_DGRAM 117 118 allow_reuse_address = False 119 120 max_packet_size = 8192 121 122 coding='utf-8' 123 124 def get_request(self): 125 data, client_addr = self.socket.recvfrom(self.max_packet_size) 126 return (data, self.socket), client_addr 127 128 def server_activate(self): 129 # No need to call listen() for UDP. 130 pass 131 132 def shutdown_request(self, request): 133 # No need to shutdown anything. 134 self.close_request(request) 135 136 def close_request(self, request): 137 # No need to close anything. 138 pass
1 import socket 2 import struct 3 import json 4 import os 5 6 7 8 class MYTCPClient: 9 address_family = socket.AF_INET 10 11 socket_type = socket.SOCK_STREAM 12 13 allow_reuse_address = False 14 15 max_packet_size = 8192 16 17 coding='utf-8' 18 19 request_queue_size = 5 20 21 def __init__(self, server_address, connect=True): 22 self.server_address=server_address 23 self.socket = socket.socket(self.address_family, 24 self.socket_type) 25 if connect: 26 try: 27 self.client_connect() 28 except: 29 self.client_close() 30 raise 31 32 def client_connect(self): 33 self.socket.connect(self.server_address) 34 35 def client_close(self): 36 self.socket.close() 37 38 def run(self): 39 while True: 40 inp=input(">>: ").strip() 41 if not inp:continue 42 l=inp.split() 43 cmd=l[0] 44 if hasattr(self,cmd): 45 func=getattr(self,cmd) 46 func(l) 47 48 49 def put(self,args): 50 cmd=args[0] 51 filename=args[1] 52 if not os.path.isfile(filename): 53 print('file:%s is not exists' %filename) 54 return 55 else: 56 filesize=os.path.getsize(filename) 57 58 head_dic={'cmd':cmd,'filename':os.path.basename(filename),'filesize':filesize} 59 print(head_dic) 60 head_json=json.dumps(head_dic) 61 head_json_bytes=bytes(head_json,encoding=self.coding) 62 63 head_struct=struct.pack('i',len(head_json_bytes)) 64 self.socket.send(head_struct) 65 self.socket.send(head_json_bytes) 66 send_size=0 67 with open(filename,'rb') as f: 68 for line in f: 69 self.socket.send(line) 70 send_size+=len(line) 71 print(send_size) 72 else: 73 print('upload successful') 74 75 76 77 78 client=MYTCPClient(('127.0.0.1',8080)) 79 80 client.run()
六.socketserver
1 import socketserver 2 class Myserver(socketserver.BaseRequestHandler): 3 def handle(self): 4 self.data = self.request.recv(1024).strip() 5 print("{} wrote:".format(self.client_address[0])) 6 print(self.data) 7 self.request.sendall(self.data.upper()) 8 9 if __name__ == "__main__": 10 HOST, PORT = "127.0.0.1", 9999 11 12 # 设置allow_reuse_address允许服务器重用地址 13 socketserver.TCPServer.allow_reuse_address = True 14 # 创建一个server, 将服务地址绑定到127.0.0.1:9999 15 server = socketserver.TCPServer((HOST, PORT),Myserver) 16 # 让server永远运行下去,除非强制停止程序 17 server.serve_forever()
1 import socket 2 3 HOST, PORT = "127.0.0.1", 9999 4 data = "hello" 5 6 # 创建一个socket链接,SOCK_STREAM代表使用TCP协议 7 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: 8 sock.connect((HOST, PORT)) # 链接到客户端 9 sock.sendall(bytes(data + " ", "utf-8")) # 向服务端发送数据 10 received = str(sock.recv(1024), "utf-8")# 从服务端接收数据 11 12 print("Sent: {}".format(data)) 13 print("Received: {}".format(received))