Python开发基础----socket套接字基础2
基于UDP的socket
面向无连接的不可靠数据传输,可以没有服务器端,只不过没有服务器端,发送的数据会被直接丢弃,并不能到达服务器端
1 #客户端 2 import socket 3 ip_port=('127.0.0.1',8080) 4 BUFSIZE=1024 5 sock_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) #SOCK_DGRAM就是UDP 6 while True: 7 msg=input('>>').strip() 8 if not msg:continue 9 sock_client.sendto(msg.encode('utf-8'),ip_port) #UDP用的是sendto发送数据
UDP服务端+客户端
1 #服务端 2 import socket 3 ip_port=('127.0.0.1',8080) 4 BUFSIZE=1024 5 sock_server=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) 6 sock_server.bind(ip_port) 7 #对比TCP,缺少listen侦听地址,缺少accept等待连接的代码 8 while True: 9 msg,addr=sock_server.recvfrom(BUFSIZE) #UDP接收数据使用recvfrom接收 10 print('recv:',msg,addr) 11 sock_server.sendto(msg.upper(),addr) 12 13 #客户端 14 import socket 15 ip_port=('127.0.0.1',8080) 16 BUFSIZE=1024 17 sock_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) 18 while True: 19 msg=input('>>').strip() 20 if not msg:continue 21 sock_client.sendto(msg.encode('utf-8'),ip_port) 22 # back_msg,addr=sock_client.recvfrom(BUFSIZE) #一般UDP用于广播,不会接收数据,如果没有服务端,启用该行代码会出错 23 # print(back_msg.decode('utf-8'),addr)
由于UDP是面向无连接的(实际上有链接,不然通过什么去传数据去取数据),可以使用多个客户端连接服务端,但这并不是并发访问。
注意:
1. 发消息,都是将数据发送到己端的发送缓冲中,收消息都是从己端的缓冲区中收
tcp:send发消息,recv收消息
udp:sendto发消息,recvfrom收消息
2. tcp是基于数据流的,而udp是基于数据报的:
send(bytes_data):发送数据流,数据流bytes_data若为空,自己这段的缓冲区也为空,操作系统不会控制tcp协议发空包
sendinto(bytes_data,ip_port):发送数据报,bytes_data为空,还有ip_port,所有即便是发送空的bytes_data,数据报其实也不是空的,自己这端的缓冲区收到内容,操作系统就会控制udp协议发包。
3.1 tcp协议
(1)如果收消息缓冲区里的数据为空,那么recv就会阻塞(阻塞很简单,就是一直在等着收)
(2)只不过tcp协议的客户端send一个空数据就是真的空数据,客户端即使有无穷个send空,也跟没有一个样。
(3)tcp基于链接通信
- 基于链接,则需要listen(backlog),指定半连接池的大小
- 基于链接,必须先运行的服务端,然后客户端发起链接请求
- 对于mac系统:如果一端断开了链接,那另外一端的链接也跟着完蛋recv将不会阻塞,收到的是空(解决方法是:服务端在收消息后加上if判断,空消息就break掉通信循环)
- 对于windows/linux系统:如果一端断开了链接,那另外一端的链接也跟着完蛋recv将不会阻塞,收到的是空(解决方法是:服务端通信循环内加异常处理,捕捉到异常后就break掉通讯循环)
3.2 udp协议
(1)如果如果收消息缓冲区里的数据为“空”,recvfrom也会阻塞
(2)只不过udp协议的客户端sendinto一个空数据并不是真的空数据(包含:空数据+地址信息,得到的报仍然不会为空),所以客户端只要有一个sendinto(不管是否发送空数据,都不是真的空数据),服务端就可以recvfrom到数据。
(3)udp无链接
- 无链接,因而无需listen(backlog),更加没有什么连接池之说了
- 无链接,udp的sendinto不用管是否有一个正在运行的服务端,可以己端一个劲的发消息,只不过数据丢失
- recvfrom收的数据小于sendinto发送的数据时,在mac和linux系统上数据直接丢失,在windows系统上发送的比接收的大直接报错
- 只有sendinto发送数据没有recvfrom收数据,数据丢失
粘包
对昨天ssh的客户端代码做点手脚
1 #客户端动手脚 2 import socket 3 ssh_client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 4 ssh_client.connect(('127.0.0.1',8080)) 5 while True: #通讯循环 6 cmd=input('>>: ').strip() 7 if not cmd:continue 8 ssh_client.send(cmd.encode('utf-8')) 9 cmd_res = ssh_client.recv(100) #动手脚位置,将一次接收的数据大小改为100字节 10 print(cmd_res.decode('gbk')) #windows 11 # print(cmd_res.decode('utf-8')) #linux 12 ssh_client.close()
运行服务端后,执行客户端测试:
1 >>: dir 2 驱动器 C 中的卷没有标签。 3 卷的序列号是 5E42-F448 4 5 C:\Users\Mr.chai\Desktop\PythonProject\笔记\ 6 >>: pwd 7 2017.7.10\套接字_test 的目录 8 9 2017/07/11 16:58 <DIR> . 10 2017/07/11 16:58 <DIR> 11 >>: pwd 12 .. 13 2017/07/10 11:04 0 __init__.py 14 2017/07/11 16:58 711 客户 15 >>: pwd 16 端.py 17 2017/07/11 16:03 1,992 服务端.py 18 3 个文件 2,703 字节 19 20 >>: pwd 21 2 个目录 42,335,735,808 可用字节 22 'pwd' 不是内部或外部命令,也不是可运行的程序 23 或批处 24 >>:
对比没动手脚之前:
1 >>: dir 2 驱动器 C 中的卷没有标签。 3 卷的序列号是 5E42-F448 4 5 C:\Users\Mr.chai\Desktop\PythonProject\笔记\2017.7.10\套接字_test 的目录 6 7 2017/07/11 17:02 <DIR> . 8 2017/07/11 17:02 <DIR> .. 9 2017/07/10 11:04 0 __init__.py 10 2017/07/11 17:02 712 客户端.py 11 2017/07/11 16:03 1,992 服务端.py 12 3 个文件 2,704 字节 13 2 个目录 42,335,076,352 可用字节 14 15 >>: pwd 16 'pwd' 不是内部或外部命令,也不是可运行的程序 17 或批处理文件。 18 19 >>:
What happened?
发生了什么事?原因是这个样子。。
首先是socket数据传送和数据接收的原理:
粘包只会出现在TCP里
UDP在windows下会提示:
OSError: [WinError 10040] 一个在数据报套接字上发送的消息大于内部消息缓冲区或其他一些网络限制,或该用户用于接收数据报的缓冲区比数据报小
而在linux下会出现丢数据的情况:
>>aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa AAAAAAAAAA ('192.168.1.10', 8080) >>
问题出现在接收方,这是因为接收方不知道返回的消息之间的界限,不知道一次性提取多少字节的数据所造成的,第一次dir返回的消息远远大于100个字节,而懂了手脚后变成了一次只能从缓存中取100字节,再次取的时候会继续取缓存中没取完的数据
出现粘包的情况:
发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据很小,会合到一起,产生粘包)
接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
解决粘包的lowB方法
粘包的根源是接收端不知道发送端将要传送的字节流的长度,那么接收端提前把自己要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据
服务端
1 import socket 2 import subprocess 3 ip_addr=('127.0.0.1',8088) 4 BUFSIZE=1024 5 s_server=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 6 s_server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) 7 s_server.bind(ip_addr) 8 s_server.listen(5) 9 print('run server...') 10 11 while True: 12 conn,addr=s_server.accept() 13 print('客户端地址:',addr) 14 while True: 15 try: 16 client_res=conn.recv(BUFSIZE) 17 if len(client_res.decode('utf-8')) == 0:continue 18 res=subprocess.Popen(client_res.decode('utf-8'), 19 shell=True, 20 stdout=subprocess.PIPE, 21 stderr=subprocess.PIPE) 22 stdout=res.stdout.read() 23 stderr=res.stderr.read() 24 std_bytes=stdout+stderr #标准输出和标准错误组合 25 std_size=len(std_bytes) #计算总长度 26 conn.send(str(std_size).encode('utf-8')) #将总长度发给客户端,客户端收到该消息返回一个状态 27 status=conn.recv(BUFSIZE).decode('utf-8') #将返回来的状态赋值 28 if status: #如果该状态成立,那么开始发送所有数据 29 conn.send(std_bytes) 30 except Exception: 31 break 32 conn.close() 33 s_server.close()
客户端
1 import socket 2 ip_addr=('127.0.0.1',8088) 3 BUFSIZE=100 4 s_client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 5 s_client.connect(ip_addr) 6 while True: 7 cmd=input('>>').strip() 8 if not cmd:continue 9 s_client.send(cmd.encode('utf-8')) 10 11 std_size=int(s_client.recv(BUFSIZE).decode('utf-8')) #将接收的数据总长度转换成数字 12 s_client.send('True'.encode('utf-8')) #返回给服务器端一个状态True 13 res=b'' 14 get_size=0 15 while get_size < std_size: 16 if (std_size-get_size) < 100: #如果总长度比下载的长度小于定义的100,那么就取数据的最小值,否则按100取值 17 res+=s_client.recv(std_size-get_size) 18 else: 19 res+=s_client.recv(BUFSIZE) 20 get_size+=BUFSIZE #每取一次值加100,最后一次的值肯定大于总长度 21 print(res.decode('gbk')) 22 s_client.close()
解决粘包的高大上方法-自定义数据头
该方法是基于上面方法的改良,即在传输数据之前,在服务器端定一个固定长度数据头部,该数据头部封装了一系列关于该数据的信息,如数据的总长度,或者传输文件数据的用户信息、时间信息等等,客户端取得整个数据的时候,先取固定长度的数据头部读取信息,按照头部信息接收数据