import subprocess res = subprocess.Popen( ' dir ' ,shell=True,stdout=subprocess.PIPE,stderr= subprocess.PIPE) print ( ' Stdout: ' ,res.stdout.read().decode( ' gbk ' )) print ( ' Stderr: ' ,res.stderr.read().decode( ' gbk ' ))
PIPE把输出的东西装到一个'水管'里,如果在windows中的编码格式是gbk,执行结果:
Stdout: 驱动器 C 中的卷是 系统 卷的序列号是 85C0 - 669A C:\Users\Administrator\PycharmProjects\Internet_program 的目录 2019/09/16 13:48. 2019/09/16 13:48 .. 2019/09/16 13:47 .idea 2019/09/16 13:46 21 Client1.py 2019/09/16 13:42 0 Client2.py 2019/09/16 13:48 207 Sever1.py 2019/09/16 01:41 70 time_test.py 2019/09/14 23:51 venv 4 个文件 298 字节 4 个目录 45,863,636,992 可用字节 Stderr:
在这里也可以使用os.popen()但是它会不管正确和错误的结果都放在一起,而用subprocess能够分别拿到正确和错误的信息
基于TCP实现的黏包
Sever:
import socket sk = socket.socket() sk.bind(( ' 127.0.0.1 ' ,8092 )) sk.listen() conn,addr = sk.accept() while True: cmd = input( ' <<< ' ) conn.send(cmd.encode( ' gbk ' )) ret = conn.recv(1024).decode( ' gbk ' ) print (ret) conn.close() sk.close()
Client:
import socket import subprocess sk = socket.socket() sk.connect(( ' 127.0.0.1 ' ,8092 )) while True: cmd = sk.recv(1024).decode( ' gbk ' ) ret = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr= subprocess.PIPE) std_out = ' stdout: ' + (ret.stdout.read()).decode( ' gbk ' ) std_err = ' stderr: ' + (ret.stderr.read()).decode( ' gbk ' ) print (std_out) print (std_err) sk.send(std_out.encode( ' gbk ' )) sk.send(std_err.encode( ' gbk ' )) sk.close()
执行结果:
Sever:
<<< dir;ls stdout: 驱动器 C 中的卷是 系统 卷的序列号是 85C0 - 669A C:\Users\Administrator\PycharmProjects\Internet_program 的目录 <<< ipconfig stderr:找不到文件 <<<
Client:
stdout: 驱动器 C 中的卷是 系统 卷的序列号是 85C0 - 669A C:\Users\Administrator\PycharmProjects\Internet_program 的目录 stderr:找不到文件 stdout: Windows IP 配置 以太网适配器 Bluetooth 网络连接 2 : 媒体状态 . . . . . . . . . . . . : 媒体已断开 连接特定的 DNS 后缀 . . . . . . . : 以太网适配器 本地连接: 媒体状态 . . . . . . . . . . . . : 媒体已断开 连接特定的 DNS 后缀 . . . . . . . : 无线局域网适配器 无线网络连接: 连接特定的 DNS 后缀 . . . . . . . : 本地链接 IPv6 地址. . . . . . . . : fe80::8c6:36a9:6fa6: 8018%14 IPv4 地址 . . . . . . . . . . . . : 192.168.43.216 子网掩码 . . . . . . . . . . . . : 255.255.255.0 默认网关. . . . . . . . . . . . . : 192.168.43.1 隧道适配器 本地连接 * 3 : 媒体状态 . . . . . . . . . . . . : 媒体已断开 连接特定的 DNS 后缀 . . . . . . . : stderr:
当我们在sever端输入dir;ls命令时,只有stdout的结果跑出来,而当我们输入ipconfig这个命令时,系统将上一次dir;ls未执行完的stderr的结果给跑出来。像这样没有接受完全或者接受多了的就是黏包现象。
TCP会有黏包现象但是它不丢包。
基于UDP实现的黏包
Sever:
import socket sk = socket.socket(type= socket.SOCK_DGRAM) sk.bind(( ' 127.0.0.1 ' ,8092 )) msg,addr = sk.recvfrom(10240 ) while 1 : cmd = input( ' <<< ' ) if cmd == ' q ' : break sk.sendto(cmd.encode( ' gbk ' ),addr) msg,addr = sk.recvfrom(10240 ) print (msg.decode( ' gbk ' )) sk.close()
Client:
import socket import subprocess sk = socket.socket(type= socket.SOCK_DGRAM) addr = ( ' 127.0.0.1 ' ,8092 ) sk.sendto( ' Start ' .encode( ' utf-8 ' ),addr) while 1 : cmd,addr = sk.recvfrom(10240 ) ret = subprocess.Popen(cmd.decode( ' gbk ' ),shell=True,stderr=subprocess.PIPE,stdout= subprocess.PIPE) std_out = ' Stdout: ' + (ret.stdout.read()).decode( ' gbk ' ) std_err = ' Stderr: ' + (ret.stderr.read()).decode( ' gbk ' ) print (std_out) print (std_err) sk.sendto(std_out.encode( ' gbk ' ),addr) sk.sendto(std_err.encode( ' gbk ' ),addr) sk.close()
执行结果:
Sever:
<<< dir;ls Stdout: 驱动器 C 中的卷是 系统 卷的序列号是 85C0 - 669A C:\Users\Administrator\PycharmProjects\Internet_program 的目录 <<< dir Stderr:找不到文件 <<< ipconfig Stdout: 驱动器 C 中的卷是 系统 卷的序列号是 85C0 - 669A C:\Users\Administrator\PycharmProjects\Internet_program 的目录 2019/09/16 14:43. 2019/09/16 14:43 .. 2019/09/16 14:37 .idea 2019/09/16 14:43 553 Client1.py 2019/09/16 13:42 0 Client2.py 2019/09/16 14:43 306 Sever1.py 2019/09/16 01:41 70 time_test.py 2019/09/14 23:51 venv 4 个文件 929 字节 4 个目录 45,855,449,088 可用字节 <<< pwd Stderr: <<< ip Stdout: Windows IP 配置 以太网适配器 Bluetooth 网络连接 2 : 媒体状态 . . . . . . . . . . . . : 媒体已断开 连接特定的 DNS 后缀 . . . . . . . : 以太网适配器 本地连接: 媒体状态 . . . . . . . . . . . . : 媒体已断开 连接特定的 DNS 后缀 . . . . . . . : 无线局域网适配器 无线网络连接: 连接特定的 DNS 后缀 . . . . . . . : 本地链接 IPv6 地址. . . . . . . . : fe80::8c6:36a9:6fa6: 8018%14 IPv4 地址 . . . . . . . . . . . . : 192.168.43.216 子网掩码 . . . . . . . . . . . . : 255.255.255.0 默认网关. . . . . . . . . . . . . : 192.168.43.1 隧道适配器 本地连接 * 3 : 媒体状态 . . . . . . . . . . . . : 媒体已断开 连接特定的 DNS 后缀 . . . . . . . : <<<
Client:
Stdout: 驱动器 C 中的卷是 系统 卷的序列号是 85C0 - 669A C:\Users\Administrator\PycharmProjects\Internet_program 的目录 Stderr:找不到文件 Stdout: 驱动器 C 中的卷是 系统 卷的序列号是 85C0 - 669A C:\Users\Administrator\PycharmProjects\Internet_program 的目录 2019/09/16 14:43. 2019/09/16 14:43 .. 2019/09/16 14:37 .idea 2019/09/16 14:43 553 Client1.py 2019/09/16 13:42 0 Client2.py 2019/09/16 14:43 306 Sever1.py 2019/09/16 01:41 70 time_test.py 2019/09/14 23:51 venv 4 个文件 929 字节 4 个目录 45,855,449,088 可用字节 Stderr: Stdout: Windows IP 配置 以太网适配器 Bluetooth 网络连接 2 : 媒体状态 . . . . . . . . . . . . : 媒体已断开 连接特定的 DNS 后缀 . . . . . . . : 以太网适配器 本地连接: 媒体状态 . . . . . . . . . . . . : 媒体已断开 连接特定的 DNS 后缀 . . . . . . . : 无线局域网适配器 无线网络连接: 连接特定的 DNS 后缀 . . . . . . . : 本地链接 IPv6 地址. . . . . . . . : fe80::8c6:36a9:6fa6: 8018%14 IPv4 地址 . . . . . . . . . . . . : 192.168.43.216 子网掩码 . . . . . . . . . . . . : 255.255.255.0 默认网关. . . . . . . . . . . . . : 192.168.43.1 隧道适配器 本地连接 * 3 : 媒体状态 . . . . . . . . . . . . : 媒体已断开 连接特定的 DNS 后缀 . . . . . . . : Stderr: Stdout: Stderr: ' pwd ' 不是内部或外部命令,也不是可运行的程序 或批处理文件。 Stdout: Stderr: ' ip ' 不是内部或外部命令,也不是可运行的程序 或批处理文件。
可以看出UDP不会有黏包现象,会产生丢包现象,没发完就不发了,不完整也不可靠。
黏包成因
TCP协议的数据传送
拆包机制
当发送端缓冲区的长度大于网卡的MTU时,TCP会将这次发送的数据拆成几个数据包发送出去。MTU是Maximum Transmission Unit的缩写,意思是网络上传送最大数据包,MTU是字节单位,大部分网络设备的MTU都是1500. 如果本机的MTU比网关的MTU大,大的数据包就会被拆开来传送,这样会产生很多数据包碎片,增加丢包率,降低网络速度。
在正常情况下它的拆包可理解为:
面向流的通信特点和Nagle算法
TCP(transport control protocol,传输控制协议),是面向连接的,面向流的,提供高可靠性的服务。收发两端(客户端和服务端)都要有一一成对的socket,因此发送端为了将多个发往接收端的包,更有效地发往对方,使用了优化算法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样接收端就难于分辨出来了,必须提供科学的拆包机制。即面向流的通信是无消息保护边界的。
对于空消息:TCP是基于数据流的,于是收发消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而UDP协议是基于数据报的,即便是你输入的是空内容(直接回车),也可以被发送,UDP协议会帮你封装上消息然后发出去。
可靠黏包的TCP协议:TCP协议数据不会丢,没有收完包,就会下次接收,会继续上次继续接受。
基于tcp协议特点的黏包现象成因
当我们在socket服务端发送值1、2,然后根据优化算法,它会把1先放到这个缓存当中等一等,然后再把2一起封装起来,然后再发出去,因此我们看到的就是黏包现象
这种现象的表面现象是两个send太近且发送的消息太短
发送端可以使1K1K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据。也就是说,应用程序所看到的数据是一个整体,或者说是一个流(stream),一条消息有多少字节对应程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现黏包问题的原因。而UDP协议是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。
解决黏包的方法
解决方案一:
Sever:
import socket sk = socket.socket() sk.bind(( ' 127.0.0.1 ' ,8080 )) sk.listen() conn,addr = sk.accept() while True: cmd = input( ' <<< ' ) if cmd == ' q ' : conn.send(b ' q ' ) break conn.send(cmd.encode( ' gbk ' )) num = conn.recv(1024).decode( ' utf-8 ' ) conn.send(b ' ok ' ) res = conn.recv(int(num)).decode( ' gbk ' ) print (res) conn.close() sk.close()
Client:
import socket import subprocess sk = socket.socket() sk.connect(( ' 127.0.0.1 ' ,8080 )) while True: cmd = sk.recv(1024).decode( ' gbk ' ) if cmd == ' q ' : break res = subprocess.Popen(cmd,shell= True, stdout = subprocess.PIPE, stderr = subprocess.PIPE) std_out = res.stdout.read() std_err = res.stderr.read() sk.send(str(len(std_out) + len(std_err)).encode( ' utf-8 ' )) sk.recv( 1024 ) print ( ' Stdout: ' + std_out.decode( ' gbk ' )) print ( ' Stderr: ' + std_err.decode( ' gbk ' )) sk.send(std_out) sk.send(std_err) sk.close()
执行结果:
Sever:
<<< dir 驱动器 C 中的卷是 系统 卷的序列号是 85C0 - 669A C:\Users\Administrator\PycharmProjects\Internet_program 的目录 2019/09/17 15:33. 2019/09/17 15:33 .. 2019/09/17 15:31 .idea 2019/09/17 15:33 623 Client1.py 2019/09/16 13:42 0 Client2.py 2019/09/17 15:21 389 Sever1.py 2019/09/16 01:41 70 time_test.py 2019/09/14 23:51 venv 4 个文件 1,082 字节 4 个目录 45,031,833,600 可用字节 <<< ls ' ls ' 不是内部或外部命令,也不是可运行的程序 或批处理文件。 <<< ipconfig Windows IP 配置 以太网适配器 Bluetooth 网络连接 2 : 媒体状态 . . . . . . . . . . . . : 媒体已断开 连接特定的 DNS 后缀 . . . . . . . : 以太网适配器 本地连接: 媒体状态 . . . . . . . . . . . . : 媒体已断开 连接特定的 DNS 后缀 . . . . . . . : 无线局域网适配器 无线网络连接: 连接特定的 DNS 后缀 . . . . . . . : 本地链接 IPv6 地址. . . . . . . . : fe80::8c6:36a9:6fa6: 8018%14 IPv4 地址 . . . . . . . . . . . . : 192.168.43.216 子网掩码 . . . . . . . . . . . . : 255.255.255.0 默认网关. . . . . . . . . . . . . : 192.168.43.1 隧道适配器 本地连接 * 3 : 媒体状态 . . . . . . . . . . . . : 媒体已断开 连接特定的 DNS 后缀 . . . . . . . : <<<
Client:
Stdout: 驱动器 C 中的卷是 系统 卷的序列号是 85C0 - 669A C:\Users\Administrator\PycharmProjects\Internet_program 的目录 2019/09/17 15:33. 2019/09/17 15:33 .. 2019/09/17 15:31 .idea 2019/09/17 15:33 623 Client1.py 2019/09/16 13:42 0 Client2.py 2019/09/17 15:21 389 Sever1.py 2019/09/16 01:41 70 time_test.py 2019/09/14 23:51 venv 4 个文件 1,082 字节 4 个目录 45,031,833,600 可用字节 Stderr: Stdout: Stderr: ' ls ' 不是内部或外部命令,也不是可运行的程序 或批处理文件。 Stdout: Windows IP 配置 以太网适配器 Bluetooth 网络连接 2 : 媒体状态 . . . . . . . . . . . . : 媒体已断开 连接特定的 DNS 后缀 . . . . . . . : 以太网适配器 本地连接: 媒体状态 . . . . . . . . . . . . : 媒体已断开 连接特定的 DNS 后缀 . . . . . . . : 无线局域网适配器 无线网络连接: 连接特定的 DNS 后缀 . . . . . . . : 本地链接 IPv6 地址. . . . . . . . : fe80::8c6:36a9:6fa6: 8018%14 IPv4 地址 . . . . . . . . . . . . : 192.168.43.216 子网掩码 . . . . . . . . . . . . : 255.255.255.0 默认网关. . . . . . . . . . . . . : 192.168.43.1 隧道适配器 本地连接 * 3 : 媒体状态 . . . . . . . . . . . . : 媒体已断开 连接特定的 DNS 后缀 . . . . . . . : Stderr:
这种写法的好处就是能确定Sever到底要接受多少数据,不好的地方就是多了一次交互
解决方案二: 使用struct模块
用struct模块我们能把一个数据类型转换成固定长度的bytes
这里以数字类型举例,'i'代表int类型:
import struct print (struct.pack( ' i ' ,2048),len(struct.pack( ' i ' ,2048))) # b'\x00\x08\x00\x00' 4 print (struct.pack( ' i ' ,204800),len(struct.pack( ' i ' ,204800))) # b'\x00 \x03\x00' 4 print (struct.pack( ' i ' ,2048000),len(struct.pack( ' i ' ,2048000))) # b'\x00@\x1f\x00' 4
当后面的数值戳过一定范围的时候程序就会报错
Sever:
import socket import struct sk = socket.socket() sk.bind(( ' 127.0.0.1 ' ,8080 )) sk.listen() conn,addr = sk.accept() while True: cmd = input( ' <<< ' ) if cmd == ' q ' : conn.send(b ' q ' ) break conn.send(cmd.encode( ' gbk ' )) num = conn.recv(4 ) num = struct.unpack( ' i ' ,num)[0] res = conn.recv(int(num)).decode( ' gbk ' ) print (res) conn.close() sk.close()
Client:
import socket import subprocess import struct sk = socket.socket() sk.connect(( ' 127.0.0.1 ' ,8080 )) while True: cmd = sk.recv(1024).decode( ' gbk ' ) if cmd == ' q ' : break res = subprocess.Popen(cmd,shell= True, stdout = subprocess.PIPE, stderr = subprocess.PIPE) std_out = res.stdout.read() std_err = res.stderr.read() len_num = len(std_out) + len(std_err) num_by = struct.pack( ' i ' ,len_num) print ( ' Stdout: ' + std_out.decode( ' gbk ' )) print ( ' Stderr: ' + std_err.decode( ' gbk ' )) sk.send(num_by) sk.send(std_out) sk.send(std_err) sk.close()
执行结果:
<<< dir 驱动器 C 中的卷是 系统 卷的序列号是 85C0 - 669A C:\Users\Administrator\PycharmProjects\Internet_program 的目录 2019/09/17 16:25. 2019/09/17 16:25 .. 2019/09/17 16:23 .idea 2019/09/17 16:25 659 Client1.py 2019/09/16 13:42 0 Client2.py 2019/09/17 16:22 400 Sever1.py 2019/09/17 16:08 288 time_test.py 2019/09/14 23:51 venv 4 个文件 1,347 字节 4 个目录 45,025,951,744 可用字节 <<< configip ' configip ' 不是内部或外部命令,也不是可运行的程序 或批处理文件。 <<< ipconfig Windows IP 配置 以太网适配器 Bluetooth 网络连接 2 : 媒体状态 . . . . . . . . . . . . : 媒体已断开 连接特定的 DNS 后缀 . . . . . . . : 以太网适配器 本地连接: 媒体状态 . . . . . . . . . . . . : 媒体已断开 连接特定的 DNS 后缀 . . . . . . . : 无线局域网适配器 无线网络连接: 连接特定的 DNS 后缀 . . . . . . . : 本地链接 IPv6 地址. . . . . . . . : fe80::8c6:36a9:6fa6: 8018%14 IPv4 地址 . . . . . . . . . . . . : 192.168.43.216 子网掩码 . . . . . . . . . . . . : 255.255.255.0 默认网关. . . . . . . . . . . . . : 192.168.43.1 隧道适配器 本地连接 * 3 : 媒体状态 . . . . . . . . . . . . : 媒体已断开 连接特定的 DNS 后缀 . . . . . . . : <<<
Stdout: 驱动器 C 中的卷是 系统 卷的序列号是 85C0 - 669A C:\Users\Administrator\PycharmProjects\Internet_program 的目录 2019/09/17 16:25. 2019/09/17 16:25 .. 2019/09/17 16:23 .idea 2019/09/17 16:25 659 Client1.py 2019/09/16 13:42 0 Client2.py 2019/09/17 16:22 400 Sever1.py 2019/09/17 16:08 288 time_test.py 2019/09/14 23:51 venv 4 个文件 1,347 字节 4 个目录 45,025,951,744 可用字节 Stderr: Stdout: Stderr: ' configip ' 不是内部或外部命令,也不是可运行的程序 或批处理文件。 Stdout: Windows IP 配置 以太网适配器 Bluetooth 网络连接 2 : 媒体状态 . . . . . . . . . . . . : 媒体已断开 连接特定的 DNS 后缀 . . . . . . . : 以太网适配器 本地连接: 媒体状态 . . . . . . . . . . . . : 媒体已断开 连接特定的 DNS 后缀 . . . . . . . : 无线局域网适配器 无线网络连接: 连接特定的 DNS 后缀 . . . . . . . : 本地链接 IPv6 地址. . . . . . . . : fe80::8c6:36a9:6fa6: 8018%14 IPv4 地址 . . . . . . . . . . . . : 192.168.43.216 子网掩码 . . . . . . . . . . . . : 255.255.255.0 默认网关. . . . . . . . . . . . . : 192.168.43.1 隧道适配器 本地连接 * 3 : 媒体状态 . . . . . . . . . . . . : 媒体已断开 连接特定的 DNS 后缀 . . . . . . . : Stderr:
实现一个大文件的传输和下载
当我们在网络上传输所有数据时,这些数据都叫数据包,数据包里的所有数据都叫报文,报文里不止有你的数据还有IP地址、MAC地址、端口号等,所有的报文都有报头,这是由协议规定的。所有在网络上传播数据包的协议里都有一个报头。什么时候需要自己定制报文?比如说复杂的应用上就会应用到、传输文件的时候(文件名、文件大小、文件类型、存储路径等)...
其实在网络传输的过程当中处处有协议,协议就是一堆报头和报文(都由字节组成)。
Sever:
import socket import struct import json buffer = 1024 sk = socket.socket() sk.bind(( ' 127.0.0.1 ' ,8080 )) sk.listen() conn,addr = sk.accept() head_len = conn.recv(4 ) struct.unpack( ' i ' ,head_len)[0] json_head = conn.recv(head_len).decode( ' utf-8 ' ) head = json.loads(json_head) file_size = head[ ' fileSize ' ] with open(r ' dir\%s ' %head[ ' fileName ' ], ' wb ' ) as f: while file_size: if file_size >= buffer: content = conn.recv(buffer) f.write(content) file_size -= buffer else : content = conn.recv(buffer) f.write(content) break conn.close() sk.close()
Client:
import socket import struct import os import json sk = socket.socket() sk.connect(( ' 127.0.0.1 ' ,8080 )) buffer = 1024 head = { ' filePath ' : r ' C:\Users\Administrator\Desktop\专题\海報資料夾\专题海报 ' , ' fileName ' : r ' 专题海报 ' , ' fileSize ' : None} file_path = os.path.join(head[ ' filePath ' ],head[ ' fileName ' ]) file_size = os.path.getsize(file_path) head[ ' fileSize ' ] = file_size json_head = json.dumps(head) # 字典转成字典 bytes_head = json_head.encode( ' utf-8 ' ) # 字符串转bytes head_len = len(bytes_head) # 报头的长度 pack_len = struct.pack( ' i ' ,head_len) sk.send(pack_len) # 先发报头的长度 sk.send(bytes_head) # 再发bytes类型的报头 with open(r ' dir\%s ' %file_path, ' rb ' ) as f: while file_size: if file_size >= buffer: content = f.read(buffer) # 每次文件读出的内容 sk.send(content) file_size -= buffer else : content = f.read(file_size) sk.send(content) break sk.close()