TCP 协议 粘包现象 和解决方案

黏包现象

让我们基于 tcp 先制作一个远程执行命令的程序(命令 ls -l ; lllllll ; pwd)

执行远程命令的模块

需要用到模块 subprocess

subprocess 通过子进程来执行外部指令,并通过 input/output/error 管道,获取子进程的执行的返回信息。

  1. import os
  2. import subprocess
  3. ret = os.popen('dir').read()
  4. print(ret)
  5. print('*'*50)
  6. ret = subprocess.Popen('dir',shell=True,stdout=subprocess.PIPE,
  7. stderr=subprocess.PIPE)
  8. print(ret.stdout)
  9. print(ret.stderr)

shell= True 可以执行一个普通系统命令

stdout 表示一个容器,返回正常的信息

stderr 存放错误信息的容器

执行输出:

  1. 驱动器 E 中的卷是 file
  2. 卷的序列号是 8077-D7B9
  3. E:\python_script\day30\黏 的目录
  4. 2018/05/07 14:54 <DIR> .
  5. 2018/05/07 14:54 <DIR> ..
  6. 2018/05/07 14:54 236 a.py
  7. 1 个文件 236 字节
  8. 2 个目录 183,394,840,576 可用字节
  9. **************************************************
  10. <_io.BufferedReader name=3>
  11. <_io.BufferedReader name=4>

执行一个错误的命令

  1. import os
  2. import subprocess
  3. ret = os.popen('ls').read()
  4. print(ret)
  5. print('*'*50)
  6. ret = subprocess.Popen('ls',shell=True,stdout=subprocess.PIPE,
  7. stderr=subprocess.PIPE)
  8. print('out:',ret.stdout.read().decode('gbk'))
  9. print('err:',ret.stderr.read().decode('gbk'))

执行输出:

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597974260884-d79d8f9a-0f23-4e94-bd10-88a7046d0b43.png)

os.popen() 执行一个错误的命令,显示乱码

而 subprocess 则不会,它还是比较完善的。

基于 tcp 协议实现的黏包

用 server 端,让客户端执行一个命令

server.py

  1. import socket
  2. sk = socket.socket()
  3. sk.bind(('127.0.0.1',9000))
  4. sk.listen()
  5. conn,addr = sk.accept()
  6. while True:
  7. cmd = input('>>>')
  8. conn.send(cmd.encode('utf-8'))
  9. if cmd == 'q': break
  10. ret1 = conn.recv(1024)
  11. print('stdout : ', ret1.decode('gbk'))
  12. ret2 = conn.recv(1024)
  13. print('stderr : ',ret2.decode('gbk'))
  14. conn.close()
  15. sk.close()

client.py

  1. import socket
  2. import subprocess
  3. sk = socket.socket()
  4. sk.connect(('127.0.0.1',9000))
  5. while True:
  6. cmd = sk.recv(1024).decode('utf-8')
  7. print(cmd)
  8. if cmd == 'q':break
  9. ret = subprocess.Popen(cmd,shell=True,
  10. stdout=subprocess.PIPE,
  11. stderr=subprocess.PIPE)
  12. out = ret.stdout.read()
  13. err = ret.stderr.read()
  14. print(out,'*****\n',err)
  15. sk.send(b'out :'+out)
  16. sk.send(b'error :'+err)
  17. sk.close()

先执行 server.py,再执行 client.py,执行效果如下:

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597974260811-2419246a-a157-4412-b82b-79e1f16594cc.png)

首先是执行了 help 命令,再执行 dir 命令

但是为什么都是显示 help 的命令结果呢?

这就是黏包现象

因为每次执行,固定为 1024 字节。它只能接收到 1024 字节,那么超出部分怎么办?

等待下一次执行命令 dir 时,优先执行上一次,还没有传完的信息。传完之后,再执行 dir 命令

总结:

发送过来的一整条信息

由于 server 端没有及时接受

后来发送的数据和之前没有接收完的数据黏在了一起

这就是著名的黏包现象

那么 udp 会发现黏包现象吗?实践一下,就知道了

基于 udp 协议实现的黏包

server.py

  1. #_*_coding:utf-8_*_
  2. from socket import *
  3. import subprocess
  4. ip_port=('127.0.0.1',9000)
  5. bufsize=1024
  6. udp_server=socket(AF_INET,SOCK_DGRAM)
  7. udp_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
  8. udp_server.bind(ip_port)
  9. while True:
  10. #收消息
  11. cmd,addr=udp_server.recvfrom(bufsize)
  12. print('用户命令----->',cmd)
  13. #逻辑处理
  14. res=subprocess.Popen(cmd.decode('utf-8'),shell=True,stderr=subprocess.PIPE,stdin=subprocess.PIPE,stdout=subprocess.PIPE)
  15. stderr=res.stderr.read()
  16. stdout=res.stdout.read()
  17. #发消息
  18. udp_server.sendto(stderr,addr)
  19. udp_server.sendto(stdout,addr)
  20. udp_server.close()

client.py

  1. from socket import *
  2. ip_port=('127.0.0.1',9000)
  3. bufsize=1024
  4. udp_client=socket(AF_INET,SOCK_DGRAM)
  5. while True:
  6. msg=input('>>: ').strip()
  7. udp_client.sendto(msg.encode('utf-8'),ip_port)
  8. err,addr=udp_client.recvfrom(bufsize)
  9. out,addr=udp_client.recvfrom(bufsize)
  10. print(err)
  11. if err:
  12. print('error : %s'%err.decode('gbk'),end='')
  13. if out:
  14. print(out.decode('gbk'), end='')

先执行 server.py,再执行 client.py,执行效果如下:

: ipconfig

Traceback (most recent call last): File “E:/python_script/day30/黏包/client.py”, line 11, in out,addr=udp_client.recvfrom(bufsize) OSError: [WinError 10040] 一个在数据报套接字上发送的消息大于内部消息缓冲区或其他一些网络限制,或该用户用于接收数据报的缓冲区比数据报小。

在客户端执行 ipconfig,就报错了,提示缓冲区过大。所以说 udp 不会出现黏包

总结:

只有 TCP 有粘包现象,UDP 永远不会粘包

subprocess 不能运行 windows help 命令,不是因为 udp 问题,而是 subprocess 问题。

黏包原因

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 协议特点的黏包现象成因

socket数据传输过程中的用户态与内核态说明

发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据。

也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。

而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。

怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。

socket数据传输过程中的用户态与内核态说

例如基于 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函数时,数据长度参数不受限制。而实际上,所指定的这段数据并不一定会一次性发送出去,如果这段数据比较长,会被分段发送,如果比较短,可能会等待和下一次数据一起发送。

会发生黏包的两种情况

一、发送方的缓存机制

发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597974261048-d2f36e4b-7a91-4025-ac89-0f3101aad297.png)

发送端内核态

如果数据包过小,不会立即发送。先缓存了一小下,通过优化算法,将 2 次或者多次数据包,一次发送。

如果数据包过大,分配发送。

如果这个时候,再来一个大的数据包,也会拆分包。那么就发生黏包了。

server.py

  1. from socket import *
  2. ip_port = ('127.0.0.1', 8080)
  3. tcp_socket_server = socket(AF_INET, SOCK_STREAM)
  4. tcp_socket_server.bind(ip_port)
  5. tcp_socket_server.listen(5)
  6. conn, addr = tcp_socket_server.accept()
  7. data1 = conn.recv(10)
  8. data2 = conn.recv(10)
  9. print('----->', data1.decode('utf-8'))
  10. print('----->', data2.decode('utf-8'))
  11. conn.close()

client.py

  1. import socket
  2. BUFSIZE = 1024
  3. ip_port = ('127.0.0.1', 8080)
  4. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  5. res = s.connect_ex(ip_port)
  6. s.send('hello'.encode('utf-8'))
  7. s.send('egg'.encode('utf-8'))

先执行 server.py,再执行 client.py

server.py 输出:

——-> helloegg

——->

从代码中,可以看出。client 发送了 2 次,第一次发送 hello,第二次发送 egg

服务端接收时了 2 次,但是第一次接收,直接是 helloegg。第二次接收内容为空。

为什么呢?这个是因为发送端的优化机制,导致的黏包

二、接收方的缓存机制

接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597974260986-91578568-b9b6-4c9d-a53b-7a4dd11c5f7c.png)

server.py

  1. from socket import *
  2. ip_port = ('127.0.0.1', 8080)
  3. tcp_socket_server = socket(AF_INET, SOCK_STREAM)
  4. tcp_socket_server.bind(ip_port)
  5. tcp_socket_server.listen(5)
  6. conn, addr = tcp_socket_server.accept()
  7. data1 = conn.recv(2) # 一次没有收完整
  8. data2 = conn.recv(10) # 下次收的时候,会先取旧的数据,然后取新的
  9. print('----->', data1.decode('utf-8'))
  10. print('----->', data2.decode('utf-8'))
  11. conn.close()

client.py

  1. import socket
  2. BUFSIZE = 1024
  3. ip_port = ('127.0.0.1', 8080)
  4. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  5. res = s.connect_ex(ip_port)
  6. s.send('hello egg'.encode('utf-8'))

先执行 server.py,再执行 client.py

server.py 输出:

——-> he

——-> llo egg

从代码上来,client 发送了 2 次数据给 server 端

server 端,第一次接收 2 字节,第二次接收 10 字节。

所以第一次返回 he,第二次,接收剩余的,返回 llo egg

注意:conn 永远不会接收到空数据,conn 断开连接的时候 recv 收到一个空,那么连接就会等待

总结

黏包现象只发生在 tcp 协议中:

1.从表面上看,黏包问题主要是因为发送方和接收方的缓存机制、tcp 协议面向流通信的特点。

2.实际上,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的

黏包的解决方案

解决方案一

问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据。

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597974260971-6f4e0640-39a2-4136-8655-ab53195e1aba.png)

原理:

黏包现象的成因

你不知道在哪儿断句

解决问题

在发送数据的时候,先告诉对方要发送的大小就可以了

自定义协议

先和服务端商量好,发送多少字节,再传输数据。

服务端:

  1. # 原理
  2. # 黏包现象的成因
  3. # 你不知道在哪儿断句
  4. # 解决问题
  5. # 在发送数据的时候,先告诉对方要发送的大小就可以了
  6. # 在发送的时候 先发送数据的大小 在发送内容
  7. # 在接受的时候 先接受大小 再根据大小接受内容
  8. # 自定义协议
  9. #_*_coding:utf-8_*_
  10. from socket import *
  11. ip_port=('127.0.0.1',8080)
  12. tcp_socket_server=socket()
  13. tcp_socket_server.bind(ip_port)
  14. tcp_socket_server.listen(5)
  15. conn,addr=tcp_socket_server.accept()
  16. lenth = conn.recv(1) # 接收1个字节,返回 b'5'
  17. #print(lenth)
  18. lenth = int(lenth.decode('utf-8')) # 转化字符串,返回5
  19. data1=conn.recv(lenth) # 接收5字节,返回 b'hello'
  20. lenth2 = conn.recv(1) # 接收1个字节
  21. lenth2 = int(lenth2.decode('utf-8')) # 转化字符串,返回3
  22. data2=conn.recv(lenth2) # 接收3个字节,返回b'egg'
  23. print('----->',data1.decode('utf-8'))
  24. print('----->',data2.decode('utf-8'))
  25. conn.close()
  26. tcp_socket_server.close()

客户端:

  1. import socket
  2. BUFSIZE=1024
  3. ip_port=('127.0.0.1',8080)
  4. s=socket.socket()
  5. res=s.connect_ex(ip_port) # 功能与connect(address)相同,但是成功返回0,失败返回errno的值
  6. lenth = str(len('hello')).encode('utf-8') # 获取hello的字符的长度,并转化为str,最后编码
  7. s.send(lenth) # 发送数字5
  8. s.send('hello'.encode('utf-8')) # 发送hello
  9. lenth = str(len('egg')).encode('utf-8') # 获取长度,结果为3
  10. s.send(lenth) # 发送3
  11. s.send('egg'.encode('utf-8')) # 发送egg
  12. s.close()

先执行服务端,再执行客户端,执行输出:

——-> hello

——-> egg

存在的问题:

程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗

解决方案进阶

刚刚的方法,问题在于我们我们在发送

我们可以借助一个模块,这个模块可以把要发送的数据长度转换成固定长度的字节。这样客户端每次接收消息之前只要先接受这个固定长度字节的内容看一看接下来要接收的信息大小,那么最终接受的数据只要达到这个值就停止,就能刚好不多不少的接收完整的数据了。

struct 模块

该模块可以把一个类型,如数字,转成固定长度的 bytes

  1. >>> struct.pack('i',1111111111111)
  2. struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #这个是范围
  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597974260887-52921f7d-7d1e-4f13-bcfe-745493fd47be.png)
  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. values1 = (1, 'abc'.encode('utf-8'), 2.7)
  8. values2 = ('defg'.encode('utf-8'),101)
  9. s1 = struct.Struct('I3sf')
  10. s2 = struct.Struct('4sI')
  11. print(s1.size,s2.size)
  12. prebuffer=ctypes.create_string_buffer(s1.size+s2.size)
  13. print('Before : ',binascii.hexlify(prebuffer))
  14. # t=binascii.hexlify('asdfaf'.encode('utf-8'))
  15. # print(t)
  16. s1.pack_into(prebuffer,0,*values1)
  17. s2.pack_into(prebuffer,s1.size,*values2)
  18. print('After pack',binascii.hexlify(prebuffer))
  19. print(s1.unpack_from(prebuffer,0))
  20. print(s2.unpack_from(prebuffer,s1.size))
  21. s3=struct.Struct('ii')
  22. s3.pack_into(prebuffer,0,123,123)
  23. print('After pack',binascii.hexlify(prebuffer))
  24. print(s3.unpack_from(prebuffer,0))
  25. 关于struct的详细用法

简单介绍下用法:

  1. import struct
  2. ret = struct.pack('i',1000000) # i表示int类型
  3. print(ret)
  4. print(len(ret)) # 返回4
  5. ret1 = struct.unpack('i',ret) # 按照给定的格式(fmt)解析字节流string,返回解析出来的tuple
  6. print(ret1) # 返回一个元组

执行输出:

b’@B\x0f\x00’

4

(1000000,)

总结:

能够把范围内一个任意的整数转换成一个固定长度的字节(int 为 4 字节),范围是-2147483648~2147483647

还能转换回来,使用 unpack 方法

使用 struct 解决黏包

借助 struct 模块,我们知道长度数字可以被转换成一个标准大小的 4 字节数字。因此可以利用这个特点来预先发送数据长度。

发送时 接收时
先发报头长度 先收报头长度,用 struct 取出来
再编码报头内容然后发送 根据取出的长度收取报头内容,然后解码,反序列化
最后发真实内容 从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容

server.py

  1. import struct
  2. import socket
  3. sk = socket.socket()
  4. sk.bind(('127.0.0.1',9000))
  5. sk.listen()
  6. conn,addr = sk.accept()
  7. count = 0
  8. while True:
  9. count += 1
  10. ret = conn.recv(4) # int类型的struct长度固定为4字节
  11. #print(ret)
  12. length = struct.unpack('i',ret)[0] #反解struct数据,取元组第一个值
  13. msg = conn.recv(length) # 接收指定字节
  14. print(msg.decode('utf-8')) # 打印接收信息
  15. if count == 3: # 防止死循环
  16. break
  17. conn.close()

client.py

  1. import socket
  2. import struct
  3. sk = socket.socket()
  4. sk.connect(('127.0.0.1',9000))
  5. count = 0
  6. while True:
  7. count += 1
  8. msg = 'hello world'
  9. length = struct.pack('i',len(msg)) # 获取msg的长度,并转化为struct
  10. sk.send(length) # 发送struct数据
  11. sk.send(msg.encode('utf-8')) # 发送msg
  12. if count == 3: # 防止死循环
  13. break
  14. sk.close()

先执行 server.py,再执行 client.py

server.py 输出:

hello world

hello world

hello world

我们还可以把报头做成字典,字典里包含将要发送的真实数据的详细信息,然后 json 序列化,然后用 struck 将序列化后的数据长度打包成 4 个字节(4 个自己足够用了)

发送时 接收时
先发报头长度 先收报头长度,用 struct 取出来
再编码报头内容然后发送 根据取出的长度收取报头内容,然后解码,反序列化
最后发真实内容 从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容

客户端

  1. from socket import *
  2. import struct,json
  3. ip_port=('127.0.0.1',8080)
  4. client=socket(AF_INET,SOCK_STREAM)
  5. client.connect(ip_port)
  6. while True:
  7. cmd=input('>>: ')
  8. if not cmd:continue
  9. client.send(bytes(cmd,encoding='utf-8'))
  10. head=client.recv(4)
  11. head_json_len=struct.unpack('i',head)[0]
  12. head_json=json.loads(client.recv(head_json_len).decode('utf-8'))
  13. data_len=head_json['data_size']
  14. recv_size=0
  15. recv_data=b''
  16. while recv_size < data_len:
  17. recv_data+=client.recv(1024)
  18. recv_size+=len(recv_data)
  19. print(recv_data.decode('gbk'))
  20. #print(recv_data.decode('gbk')) #windows默认gbk编码

执行效果如下:

  1. >>: dir
  2. 驱动器 E 中的卷是 file
  3. 卷的序列号是 8077-D7B9
  4. E:\python_script\day30\黏 的目录
  5. 2018/05/07 20:42 <DIR> .
  6. 2018/05/07 20:42 <DIR> ..
  7. 2018/05/07 20:42 663 client.py
  8. 2018/05/07 17:16 566 client1.py
  9. 2018/05/07 20:42 1,092 server.py
  10. 2018/05/07 17:20 381 server1.py
  11. 个文件 2,702 字节
  12. 个目录 183,394,832,384 可用字节
  13. >>:

简单的文件传送 :

文件的上传和下载

需要文件的名字,文件的大小,文件的内容

自定义一个文件传输协议:

  1. {'filesize':000,'filename':'XXXX'}
  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597974260935-8111541b-b17c-4c4f-b4d3-c2b0615c19fd.png)

使用 server.py 将一个文件传给 client

server.py

  1. import os
  2. import json
  3. import struct
  4. import socket
  5. # E:\BaiduYunDownload\AppleEthernet-master.zip
  6. sk = socket.socket()
  7. sk.bind(('127.0.0.1',9000))
  8. sk.listen()
  9. conn,addr = sk.accept()
  10. print(addr)
  11. dic = {'filename':'AppleEthernet-master.zip',
  12. 'filesize':os.path.getsize(r'E:\BaiduYunDownload\AppleEthernet-master.zip')}
  13. str_dic = json.dumps(dic).encode('utf-8')
  14. dic_len = struct.pack('i',len(str_dic))
  15. conn.send(dic_len)
  16. conn.send(str_dic)
  17. with open(r'E:\BaiduYunDownload\AppleEthernet-master.zip','rb') as f:
  18. content = f.read()
  19. conn.send(content)
  20. conn.close()
  21. sk.close()

client.py

  1. import json
  2. import struct
  3. import socket
  4. sk = socket.socket()
  5. sk.connect(('127.0.0.1',9000))
  6. dic_len = sk.recv(4)
  7. dic_len = struct.unpack('i',dic_len)[0]
  8. str_dic = sk.recv(dic_len).decode('utf-8')
  9. dic = json.loads(str_dic)
  10. with open(dic['filename'],'wb') as f:
  11. content = sk.recv(dic['filesize'])
  12. f.write(content)
  13. sk.close()

先运行 server.py,再运行 client.py

那么在当前目录中,就会多出一个文件 AppleEthernet-master.zip

server 的 IP 为 127.0.0.1。如果改成本机的 IP,比如 192.168.11.27

那么别的电脑,开启客户端,就可以接收到。

注意:

大文件的传输,不能一次性读到内存里

今日作业:

上传一个视频,几台电脑之间能互相传,视频要 3 个 G 左右。

进阶需求,加一个登陆功能

server.py

  1. import os
  2. import json
  3. import struct
  4. import socket
  5. import hashlib
  6. sk = socket.socket()
  7. sk.bind(('127.0.0.1',9999))
  8. sk.listen()
  9. conn,addr = sk.accept()
  10. print(addr)
  11. filename = '[电影天堂www.dy2018.com]移动迷宫3:死亡解药BD国英双语中英双字.mp4' # 文件名
  12. absolute_path = os.path.join('E:\BaiduYunDownload',filename) # 文件绝对路径
  13. buffer_size = 1024*1024 # 缓冲大小,这里表示1MB
  14. md5obj = hashlib.md5()
  15. with open(absolute_path, 'rb') as f:
  16. while True:
  17. content = f.read(buffer_size) # 每次读取指定字节
  18. if content:
  19. md5obj.update(content)
  20. else:
  21. break # 当内容为空时,终止循环
  22. md5 = md5obj.hexdigest()
  23. print(md5) # 打印md5值
  24. dic = {'filename':filename, 'filename_md5':str(md5),'buffer_size':buffer_size,
  25. 'filesize':os.path.getsize(absolute_path)}
  26. str_dic = json.dumps(dic).encode('utf-8') # 将字典转换为json
  27. dic_len = struct.pack('i', len(str_dic)) # 获取字典长度,转换为struct
  28. conn.send(dic_len) # 发送字典长度
  29. conn.send(str_dic) # 发送字典
  30. with open(absolute_path, 'rb') as f: # 打开文件
  31. while True:
  32. content = f.read(buffer_size) # 每次读取指定大小的字节
  33. if content: # 判断内容不为空
  34. conn.send(content) # 每次读取指定大小的字节
  35. else:
  36. break
  37. conn.close() # 关闭连接
  38. sk.close() # 关闭套接字

client.py

  1. import json
  2. import struct
  3. import socket
  4. import hashlib
  5. import time
  6. start_time = time.time()
  7. sk = socket.socket()
  8. sk.connect(('127.0.0.1',9999))
  9. dic_len = sk.recv(4) # 接收4字节,因为struct的int为4字节
  10. dic_len = struct.unpack('i',dic_len)[0] # 反解struct得到元组,获取元组第一个元素
  11. #print(dic_len) # 返回一个数字
  12. str_dic = sk.recv(dic_len).decode('utf-8') # 接收指定长度,获取完整的字典,并解码
  13. #print(str_dic) # json类型的字典
  14. dic = json.loads(str_dic) # 反序列化得到真正的字典
  15. #print(dic) # 返回字典
  16. md5 = hashlib.md5()
  17. with open(dic['filename'],'wb') as f:
  18. while True:
  19. content = sk.recv(dic['buffer_size'])
  20. if not content:
  21. break
  22. md5.update(content)
  23. md5 = md5.hexdigest()
  24. print(md5) # 打印md5值
  25. if dic['filename_md5'] == str(md5):
  26. f.write(content)
  27. print('md5校验正确--下载成功')
  28. else:
  29. print('文件验证失败')
  30. sk.close()
  31. end_time = time.time()
  32. print('本次下载花费了{}秒'.format(end_time-start_time))

先执行 server.py,再执行 client.py

server 输出:

(‘127.0.0.1’, 54230)

30e63a254cf081e8e93c036b21057347

client 输出:

30e63a254cf081e8e93c036b21057347

md5 校验正确—下载成功

本次下载花费了 25.687340021133423 秒

明日默写:

server.py

  1. import os
  2. import json
  3. import struct
  4. import socket
  5. # D:\python11\day35\1.复习.py
  6. sk = socket.socket()
  7. sk.bind(('192.168.11.92',9000))
  8. sk.listen()
  9. conn,addr = sk.accept()
  10. print(addr)
  11. dic = {'filename':'1.复习.py',
  12. 'filesize':os.path.getsize(r'D:\python11\day35\1.复习.py')}
  13. str_dic = json.dumps(dic).encode('utf-8')
  14. dic_len = struct.pack('i',len(str_dic))
  15. conn.send(dic_len)
  16. conn.send(str_dic)
  17. with open(r'D:\python11\day35\1.复习.py','rb') as f:
  18. content = f.read()
  19. conn.send(content)
  20. conn.close()
  21. sk.close()

client.py

  1. import json
  2. import struct
  3. import socket
  4. sk = socket.socket()
  5. sk.connect(('192.168.11.92',9000))
  6. dic_len = sk.recv(4)
  7. dic_len = struct.unpack('i',dic_len)[0]
  8. str_dic = sk.recv(dic_len).decode('utf-8')
  9. dic = json.loads(str_dic)
  10. with open(dic['filename'],'wb') as f:
  11. content = sk.recv(dic['filesize'])
  12. f.write(content)
  13. sk.close()