网站开发五人分工,是网站推广的案例,竣工验收备案查询,网站的维护及建设粘包问题概述 1.1 描述背景 采用TCP协议进行网络数据传送的软件设计中#xff0c;普遍存在粘包问题。这主要是由于现代操作系统的网络传输机制所产生的。我们知道#xff0c;网络通信采用的套接字(socket)技术#xff0c;其实现实际是由系统内核提供一片连续缓存(流缓冲)来…粘包问题概述 1.1 描述背景 采用TCP协议进行网络数据传送的软件设计中普遍存在粘包问题。这主要是由于现代操作系统的网络传输机制所产生的。我们知道网络通信采用的套接字(socket)技术其实现实际是由系统内核提供一片连续缓存(流缓冲)来实现应用层程序与网卡接口之间的中转功能。多个数据包被连续存储于连续的缓存中在对数据包进行读取时由于无法确定发生方的发送边界而采用某一估测值大小来进行数据读出若双方的size不一致时就会使数据包的边界发生错位导致读出错误的数据分包进而曲解原始数据含义。 1.2 粘包的概念 粘包问题的本质就是数据读取边界错误所致通过下图可以形象地理解其现象。 如图1所示当前的socket缓存中已经有6个数据分组到达其大小如图中数字。而应用程序在对数据进行收取时(如图2)采用了300字节的要求去读取则会误将pkg1和pkg2一起收走当做一个包来处理。而实际上很可能pkg1是一个文本文件的内容而pkg2则可能是一个音频内容这风马牛不相及的两个数据包却被揉进一个包进行处理显然有失妥当。严重时可能因为丢了pkg2而导致软件陷入异常分支产生乌龙事件。 因此粘包问题必须引起所有软件设计者项目经理的高度重视 那么或许会有读者发问为何不让接收程序按照100字节来读取呢我想如果您了解一些TCP编程的话就不会有这样的问题。网络通信程序中数据包通常是不能确定大小的尤其在软件设计阶段无法真的做到确定为一个固定值。比如聊天软件客户端若采用TCP传输一个用户名和密码到服务端进行验证登陆我想这个数据包不过是几十字节至多几百字节即可发送完毕而有时候要传输一个很大的视频文件即使分包发送也应该一个包在几千字节吧。据说某国电信平台的MW中见到过一次发送1.5万字节的电话数据这种情况下发送数据的分包大小无法固定接收端也就无法固定。所以一般采用一个较为合理的预估值进行轮询接收。网卡的MTU都是1500字节因此这个预估值一般为MTU的1~3倍。 粘包现象 说粘包之前我们先说两个内容1.缓冲区、2.windows下cmd窗口调用系统指令 9.1 缓冲区下面粘包现象的图里面还有关于缓冲区的解释 每个 socket 被创建后都会分配两个缓冲区输入缓冲区和输出缓冲区。write()/send() 并不立即向网络中传输数据而是先将数据写入缓冲区中再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区函数就可以成功返回不管它们有没有到达目标机器也不管它们何时被发送到网络这些都是TCP协议负责的事情。TCP协议独立于 write()/send() 函数数据有可能刚被写入缓冲区就发送到网络也可能在缓冲区中不断积压多次写入的数据被一次性发送到网络这取决于当时的网络情况、当前线程是否空闲等诸多因素不由程序员控制。read()/recv() 函数也是如此也从输入缓冲区中读取数据而不是直接从网络中读取。这些I/O缓冲区特性可整理如下1.I/O缓冲区在每个TCP套接字中单独存在
2.I/O缓冲区在创建套接字时自动生成
3.即使关闭套接字也会继续传送输出缓冲区中遗留的数据
4.关闭套接字将丢失输入缓冲区中的数据。输入输出缓冲区的默认大小一般都是 8K可以通过 getsockopt() 函数获取1.unsigned optVal;
2.int optLen sizeof(int);
3.getsockopt(servSock, SOL_SOCKET, SO_SNDBUF,(char*)optVal, optLen);
4.printf(Buffer length: %d\n, optVal);socket缓冲区解释 9.2 windows下cmd窗口调用系统指令(linux下没有写出来大家仿照windows的去摸索一下吧) a.首先ctrlr弹出左下角的下图输入cmd指令确定 b.在打开的cmd窗口中输入dirdir查看当前文件夹下的所有文件和文件夹你会看到下面的输出结果。 另外还有ipconfig查看当前电脑的网络信息在windows没有ls这个指令(ls在linux下是查看当前文件夹下所有文件和文件夹的指令和windows下的dir是类似的)那么没有这个指令就会报下面这个错误 linux shell中一个运行多个命令命令间用;隔开即可windows的命令提示符中运行多条命令用的是、||、aa bb
就是执行aa成功后再执行bbaa || bb
先执行aa若执行成功则不再执行bb若失败则执行bba b
表示执行a再执行b无论a是否成功“执行成功”的意思是返回的errorlevel0windows下执行多条指令 windows下执行多条指令 为什么要说这个系统指令呢是希望借助系统指令和指令输出的结果来模拟一下粘包现象那什么是粘包呢 9.3 粘包现象两种 先上图本图是我做出来为了让小白同学有个大致的了解用的其中很多地方更加的复杂那就需要将来大家有多余的精力的时候去做一些深入的研究了这里我就不带大家搞啦 关于MTU大家可以看看这篇文章 https://yq.aliyun.com/articles/222535 还有百度百科 MTU百科 MTU简单解释 MTU是Maximum Transmission Unit的缩写。意思是网络上传送的最大数据包。MTU的单位是字节。 大部分网络设备的MTU都是1500个字节也就是1500B。如果本机一次需要发送的数据比网关的MTU大大的数据包就会被拆开来传送这样会产生很多数据包碎片增加丢包率降低网络速度 关于上图中提到的Nagle算法等建议大家去看一看Nagle算法、延迟ACK、linux下的TCP_NODELAY和TCP_CORK这些内容等你们把python学好以后再去研究吧网络的内容实在太多啦也就是说大家需要努力的过程还很长加油 超出缓冲区大小会报下面的错误或者udp协议的时候你的一个数据包的大小超过了你一次recv能接受的大小也会报下面的错误tcp不会但是超出缓存区大小的时候肯定会报这个错误。 9.4 模拟一个粘包现象 在模拟粘包之前我们先学习一个模块subprocess。 import subprocess
cmd input(请输入指令)
res subprocess.Popen(cmd, #字符串指令dir,ipconfig,等等shellTrue, #使用shell就相当于使用cmd窗口stderrsubprocess.PIPE, #标准错误输出凡是输入错误指令错误指令输出的报错信息就会被它拿到stdoutsubprocess.PIPE, #标准输出正确指令的输出结果被它拿到
)
print(res.stdout.read().decode(gbk))
print(res.stderr.read().decode(gbk))subprocess的简单使用 subprocess的简单使用 注意 如果是windows那么res.stdout.read()读出的就是GBK编码的在接收端需要用GBK解码 且只能从管道里读一次结果PIPE称为管道。 下面是subprocess和windows上cmd下的指令的对应示意图subprocess的stdout.read()和stderr.read()拿到的结果是bytes类型所以需要转换为字符串打印出来看。 好既然我们会使用subprocess了那么我们就通过它来模拟一个粘包终于到模拟粘包现象了 tcp粘包演示(一) 先从上面粘包现象中的第一种开始接收方没有及时接收缓冲区的包造成多个包接收客户端发送了一段数据服务端只收了一小部分服务端下次再收的时候还是从缓冲区拿上次遗留的数据产生粘包 server端代码示例 cket import *
import subprocessip_port(127.0.0.1,8080)
BUFSIZE1024tcp_socket_serversocket(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,addrtcp_socket_server.accept()print(客户端,addr)while True:cmdconn.recv(BUFSIZE)if len(cmd) 0:breakressubprocess.Popen(cmd.decode(gbk),shellTrue,stdoutsubprocess.PIPE,stdinsubprocess.PIPE,stderrsubprocess.PIPE)stderrres.stderr.read()stdoutres.stdout.read()conn.send(stderr)conn.send(stdout)tcp_server.py client端代码示例 import socket
ip_port (127.0.0.1,8080)
size 1024
tcp_sk socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res tcp_sk.connect(ip_port)
while True:msginput(: ).strip()if len(msg) 0:continueif msg quit:breaktcp_sk.send(msg.encode(utf-8))act_restcp_sk.recv(size)print(接收的返回结果长度为,len(act_res))print(std,act_res.decode(gbk)) #windows返回的内容需要用gbk来解码因为windows系统的默认编码为gbktcp_client.py tcp粘包演示(二)发送数据时间间隔很短数据也很小会合到一起产生粘包 server端代码示例如果两次发送有一定的时间间隔那么就不会出现这种粘包情况试着在两次发送的中间加一个time.sleep(1) from socket import *
ip_port(127.0.0.1,8080)tcp_socket_serversocket(AF_INET,SOCK_STREAM)
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(5)
conn,addrtcp_socket_server.accept()
data1conn.recv(10)
data2conn.recv(10)print(-----,data1.decode(utf-8))
print(-----,data2.decode(utf-8))conn.close()tcp_server.py client端代码示例 import socket
BUFSIZE1024
ip_port(127.0.0.1,8080)
ssocket.socket(socket.AF_INET,socket.SOCK_STREAM)
# ress.connect_ex(ip_port)
ress.connect(ip_port)
s.send(hi.encode(utf-8))
s.send(meinv.encode(utf-8))tcp_server.py 示例二的结果全部被第一个recv接收了 udp粘包演示注意udp是面向包的所以udp是不存在粘包的 server端代码示例 import socket
from socket import SOL_SOCKET,SO_REUSEADDR,SO_SNDBUF,SO_RCVBUF
sk socket.socket(typesocket.SOCK_DGRAM)
# sk.setsockopt(SOL_SOCKET,SO_RCVBUF,80*1024)
sk.bind((127.0.0.1,8090))
msg,addr sk.recvfrom(1024)
while True:cmd input()if cmd q:breaksk.sendto(cmd.encode(utf-8),addr)msg,addr sk.recvfrom(1032)# print(, sk.getsockopt(SOL_SOCKET, SO_SNDBUF))# print(, sk.getsockopt(SOL_SOCKET, SO_RCVBUF))print(len(msg))print(msg.decode(utf-8))sk.close()udp_server.py client端代码示例 import socket
from socket import SOL_SOCKET,SO_REUSEADDR,SO_SNDBUF,SO_RCVBUF
sk socket.socket(typesocket.SOCK_DGRAM)
# sk.setsockopt(SOL_SOCKET,SO_RCVBUF,80*1024)
sk.bind((127.0.0.1,8090))
msg,addr sk.recvfrom(1024)
while True:cmd input()if cmd q:breaksk.sendto(cmd.encode(utf-8),addr)msg,addr sk.recvfrom(1024)# msg,addr sk.recvfrom(1218)# print(, sk.getsockopt(SOL_SOCKET, SO_SNDBUF))# print(, sk.getsockopt(SOL_SOCKET, SO_RCVBUF))print(len(msg))print(msg.decode(utf-8))sk.close()udp_server.py 在udp的代码中我们在server端接收返回消息的时候我们设置的recvfrom(1024)那么当我输入的执行指令为‘dir’的时候dir在我当前文件夹下输出的内容大于1024然后就报错了报的错误也是下面这个 解释原因是因为udp是面向报文的意思就是每个消息是一个包你接收端设置接收大小的时候必须要比你发的这个包要大不然一次接收不了就会报这个错误而tcp不会报错这也是为什么ucp会丢包的原因之一这个和我们上面缓冲区那个错误的报错原因是不一样的。 发送端可以是一K一K地发送数据而接收端的应用程序可以两K两K地提走数据当然也有可能一次提走3K或6K数据或者一次只提走几个字节的数据也就是说应用程序所看到的数据是一个整体或说是一个流stream一条消息有多少字节对应用程序是不可见的因此TCP协议是面向流的协议这也是容易出现粘包问题的原因。而UDP是面向消息的协议每个UDP段都是一条消息应用程序必须以消息为单位提取数据不能一次提取任意字节的数据这一点和TCP是很不同的。怎样定义消息呢可以认为对方一次性write/send的数据为一个消息需要明白的是当对方send一条信息的时候无论底层怎样分段分片TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。例如基于tcp的套接字客户端往服务端上传文件发送时文件内容是按照一段一段的字节流发送的在接收方看了根本不知道该文件的字节流从何处开始在何处结束所谓粘包问题主要还是因为接收方不知道消息之间的界限不知道一次性提取多少字节的数据所造成的。此外发送方引起的粘包是由TCP协议本身造成的TCP为提高传输效率发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去这样接收方就收到了粘包数据。1.TCPtransport control protocol传输控制协议是面向连接的面向流的提供高可靠性服务。收发两端客户端和服务器端都要有一一成对的socket因此发送端为了将多个发往接收端的包更有效的发到对方使用了优化方法Nagle算法将多次间隔较小且数据量小的数据合并成一个大的数据块然后进行封包。这样接收端就难于分辨出来了必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。2.UDPuser datagram protocol用户数据报协议是无连接的面向消息的提供高效率服务。不会使用块的合并优化算法, 由于UDP支持的是一对多的模式所以接收端的skbuff(套接字缓冲区采用了链式结构来记录每一个到达的UDP包在每个UDP包中就有了消息头消息来源地址端口等信息这样对于接收端来说就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。3.tcp是基于数据流的于是收发的消息不能为空这就需要在客户端和服务端都添加空消息的处理机制防止程序卡住而udp是基于数据报的即便是你输入的是空内容直接回车那也不是空消息udp协议会帮你封装上消息头实验略
udp的recvfrom是阻塞的一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是yx数据就丢失这意味着udp根本不会粘包但是会丢数据不可靠tcp的协议数据不会丢没有收完包下次接收会继续上次继续接收己端总是在收到ack时才会清除缓冲区内容。数据是可靠的但是会粘包。解释原因 解释原因 补充两个问题 补充问题一为何tcp是可靠传输udp是不可靠传输tcp在数据传输时发送端先把数据发送到自己的缓存中然后协议控制将缓存中的数据发往对端对端返回一个ack1发送端则清理缓存中的数据对端返回ack0则重新发送数据所以tcp是可靠的。而udp发送数据对端是不会返回确认信息的因此不可靠补充问题二send(字节流)和sendallsend的字节流是先放入己端缓存然后由协议控制将缓存内容发往对端如果待发送的字节流大小大于缓存剩余空间那么数据丢失用sendall就会循环调用send数据不会丢失一般的小数据就用send因为小数据也用sendall的话有些影响代码性能简单来讲就是还多while循环这个代码呢。用UDP协议发送时用sendto函数最大能发送数据的长度为65535- IP头(20) – UDP头(8)65507字节。用sendto函数发送数据时如果发送数据长度大于该值则函数会返回错误。丢弃这个包不进行发送 用TCP协议发送时由于TCP是数据流协议因此不存在包大小的限制暂不考虑缓冲区的大小这是指在用send函数时数据长度参数不受限制。而实际上所指定的这段数据并不一定会一次性发送出去如果这段数据比较长会被分段发送如果比较短可能会等待和下一次数据一起发送。 粘包的原因主要还是因为接收方不知道消息之间的界限不知道一次性提取多少字节的数据所造成的 粘包的解决方案 解决方案一 问题的根源在于接收端不知道发送端将要传送的字节流的长度所以解决粘包的方法就是围绕如何让发送端在发送数据前把自己将要发送的字节流总大小让接收端知晓然后接收端发一个确认消息给发送端然后发送端再发送过来后面的真实内容接收端再来一个死循环接收完所有数据。 看代码示例 server端代码 import socket,subprocess
ip_port(127.0.0.1,8080)
ssocket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)s.bind(ip_port)
s.listen(5)while True:conn,addrs.accept()print(客户端,addr)while True:msgconn.recv(1024)if not msg:breakressubprocess.Popen(msg.decode(utf-8),shellTrue,\stdinsubprocess.PIPE,\stderrsubprocess.PIPE,\stdoutsubprocess.PIPE)errres.stderr.read()if err:reterrelse:retres.stdout.read()data_lengthlen(ret)conn.send(str(data_length).encode(utf-8))dataconn.recv(1024).decode(utf-8)if data recv_ready:conn.sendall(ret)conn.close()tcp_server.py client端代码示例 import socket,time
ssocket.socket(socket.AF_INET,socket.SOCK_STREAM)
ress.connect_ex((127.0.0.1,8080))while True:msginput(: ).strip()if len(msg) 0:continueif msg quit:breaks.send(msg.encode(utf-8))lengthint(s.recv(1024).decode(utf-8))s.send(recv_ready.encode(utf-8))send_size0recv_size0databwhile recv_size length:datas.recv(1024)recv_sizelen(data)print(data.decode(utf-8))tcp_server.py 解决方案二 通过struck模块将需要发送的内容的长度进行打包打包成一个4字节长度的数据发送到对端对端只要取出前4个字节然后对这四个字节的数据进行解包拿到你要发送的内容的长度然后通过这个长度来继续接收我们实际要发送的内容。不是很好理解是吧哈哈没关系看下面的解释~~ 为什么要说一下这个模块呢因为解决方案一里面你发现我每次要先发送一个我的内容的长度需要接收端接收并切需要接收端返回一个确认消息我发送端才能发后面真实的内容这样是为了保证数据可靠性也就是接收双方能顺利沟通但是多了一次发送接收的过程为了减少这个过程我们就要使struck来发送你需要发送的数据的长度来解决上面我们所说的通过发送内容长度来解决粘包的问题。 关于struck的介绍 了解c语言的人一定会知道struct结构体在c语言中的作用不了解C语言的同学也没关系不影响其实它就是定义了一种结构里面包含不同类型的数据(int,char,bool等等)方便对某一结构对象进行处理。而在网络通信当中大多传递的数据是以二进制流binary data存在的。当传递字符串时不必担心太多的问题而当传递诸如int、char之类的基本数据的时候就需要有一种机制将某些特定的结构体类型打包成二进制流的字符串然后再网络传输而接收端也应该可以通过某种机制进行解包还原出原始的结构体数据。python中的struct模块就提供了这样的机制该模块的主要作用就是对python基本类型值与用python字符串格式表示的C struct类型间的转化This module performs conversions between Python values and C structs represented as Python strings.。 struck模块的使用struct模块中最重要的两个函数是pack()打包, unpack()解包。 pack()#我在这里只介绍一下i这个int类型上面的图中列举除了可以打包的所有的数据类型并且struck除了pack和uppack两个方法之外还有好多别的方法和用法大家以后找时间可以去研究一下这里我就不做介绍啦网上的教程很多~~ import struct
a12
# 将a变为二进制
bytesstruct.pack(i,a)
-------------------------------------------------------------------------------
struct.pack(i,1111111111111) 如果int类型数据太大会报错struck.error
struct.error: i format requires -2147483648 number 2147483647 #这个是范围 pack方法图解 unpack() # 注意unpack返回的是tuple !!a,struct.unpack(i,bytes) #将bytes类型的数据解包后拿到int类型数据 好到这里我们将struck这个模块将int类型的数据打包成四个字节的方法了那么我们就来使用它解决粘包吧。 先看一段伪代码示例 import json,struct
#假设通过客户端上传1T:1073741824000的文件a.txt#为避免粘包,必须自定制报头
header{file_size:1073741824000,file_name:/a/b/c/d/e/a.txt,md5:8f6fbf8347faa4924a76856701edb0f3} #1T数据,文件路径和md5值#为了该报头能传送,需要序列化并且转为bytes因为bytes只能将字符串类型的数据转换为bytes类型的所有需要先序列化一下这个字典字典不能直接转化为bytes
head_bytesbytes(json.dumps(header),encodingutf-8) #序列化并转成bytes,用于传输#为了让客户端知道报头的长度,用struck将报头长度这个数字转成固定长度:4个字节
head_len_bytesstruct.pack(i,len(head_bytes)) #这4个字节里只包含了一个数字,该数字是报头的长度#客户端开始发送
conn.send(head_len_bytes) #先发报头的长度,4个bytes
conn.send(head_bytes) #再发报头的字节格式
conn.sendall(文件内容) #然后发真实内容的字节格式#服务端开始接收
head_len_bytess.recv(4) #先收报头4个bytes,得到报头长度的字节格式
xstruct.unpack(i,head_len_bytes)[0] #提取报头的长度head_bytess.recv(x) #按照报头长度x,收取报头的bytes格式
headerjson.loads(json.dumps(header)) #提取报头#最后根据报头的内容提取真实的数据,比如
real_data_lens.recv(header[file_size])
s.recv(real_data_len)伪代码含解释 下面看正式的代码 server端代码示例报头就是消息的头部信息我们要发送的真实内容为报头后面的内容。 import socket,struct,json
import subprocess
phonesocket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #忘了这是干什么的了吧地址重用想起来了吗~phone.bind((127.0.0.1,8080))
phone.listen(5)
while True:conn,addrphone.accept()while True:cmdconn.recv(1024)if not cmd:breakprint(cmd: %s %cmd)ressubprocess.Popen(cmd.decode(utf-8),shellTrue,stdoutsubprocess.PIPE,stderrsubprocess.PIPE)errres.stderr.read()if err:back_msgerrelse:back_msgres.stdout.read()conn.send(struct.pack(i,len(back_msg))) #先发back_msg的长度conn.sendall(back_msg) #在发真实的内容#其实就是连续的将长度和内容一起发出去那么整个内容的前4个字节就是我们打包的后面内容的长度对吧conn.close(tcp_server.py自定制报头 client端代码示例 import socket,time,struct
ssocket.socket(socket.AF_INET,socket.SOCK_STREAM)
ress.connect_ex((127.0.0.1,8080))
while True:msginput(: ).strip()if len(msg) 0:continueif msg quit:breaks.send(msg.encode(utf-8)) #发送给一个指令ls.recv(4) #先接收4个字节的数据因为我们将要发送过来的内容打包成了4个字节所以先取出4个字节xstruct.unpack(i,l)[0] #解包是一个元祖第一个元素就是我们的内容的长度print(type(x),x)# print(struct.unpack(I,l))r_s0databwhile r_s x: #根据内容的长度来继续接收4个字节后面的内容。r_ds.recv(1024)datar_dr_slen(r_d)# print(data.decode(utf-8))print(data.decode(gbk)) #windows默认gbk编码tcp_client.py自定制报头 复杂一些的代码示例 server端 import socket,struct,json
import subprocess
phonesocket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)phone.bind((127.0.0.1,8080))
phone.listen(5)while True:conn,addrphone.accept()while True:cmdconn.recv(1024)if not cmd:breakprint(cmd: %s %cmd)ressubprocess.Popen(cmd.decode(utf-8),shellTrue,stdoutsubprocess.PIPE,stderrsubprocess.PIPE)errres.stderr.read()print(err)if err:back_msgerrelse:back_msgres.stdout.read()headers{data_size:len(back_msg)}head_jsonjson.dumps(headers)head_json_bytesbytes(head_json,encodingutf-8)conn.send(struct.pack(i,len(head_json_bytes))) #先发报头的长度conn.send(head_json_bytes) #再发报头conn.sendall(back_msg) #在发真实的内容
conn.close()tcp_server.py client端 from socket import *
import struct,jsonip_port(127.0.0.1,8080)
clientsocket(AF_INET,SOCK_STREAM)
client.connect(ip_port)while True:cmdinput(: )if not cmd:continueclient.send(bytes(cmd,encodingutf-8))headclient.recv(4)head_json_lenstruct.unpack(i,head)[0]head_jsonjson.loads(client.recv(head_json_len).decode(utf-8))data_lenhead_json[data_size]recv_size0recv_databwhile recv_size data_len:recv_dataclient.recv(1024)recv_sizelen(recv_data)#print(recv_data.decode(utf-8))print(recv_data.decode(gbk)) #windows默认gbk编码tcp_client.py 其实上面复杂的代码做了个什么事情呢就是自定制了报头 你为啥多次send啊其实多次send和将数据拼接起来send一次是一样的因为我们约定好了你接收的时候先接收4个字节然后再接收后面的内容。 整个流程的大致解释
我们可以把报头做成字典字典里包含将要发送的真实数据的描述信息(大小啊之类的)然后json序列化然后用struck将序列化后的数据长度打包成4个字节。
我们在网络上传输的所有数据 都叫做数据包数据包里的所有数据都叫做报文报文里面不止有你的数据还有ip地址、mac地址、端口号等等其实所有的报文都有报头这个报头是协议规定的看一下发送时
先发报头长度
再编码报头内容然后发送
最后发真实内容接收时
先手报头长度用struct取出来
根据取出的长度收取报头内容然后解码反序列化
从反序列化的结果中取出待取数据的描述信息然后去取真实的数据内容 FTP上传下载文件的代码简易版 import socket
import struct
import json
sk socket.socket()
# buffer 4096 # 当双方的这个接收发送的大小比较大的时候就像这个4096就会丢数据这个等我查一下再告诉大家改小了就ok的在linux上也是ok的。
buffer 1024 #每次接收数据的大小
sk.bind((127.0.0.1,8090))
sk.listen()conn,addr sk.accept()
#接收
head_len conn.recv(4)
head_len struct.unpack(i,head_len)[0] #解包
json_head conn.recv(head_len).decode(utf-8) #反序列化
head json.loads(json_head)
filesize head[filesize]
with open(head[filename],wb) as f:while filesize:if filesize buffer: #是因为如果刚好等于的情况出现也是可以的。content conn.recv(buffer)f.write(content)filesize - bufferelse:content conn.recv(buffer)f.write(content)breakconn.close()
sk.close()tcp_server.py import os
import json
import socket
import struct
sk socket.socket()
sk.connect((127.0.0.1,8090))
buffer 1024 #读取文件的时候每次读取的大小
head {filepath:rD:\打包程序, #需要下载的文件路径也就是文件所在的文件夹filename:xxx.mp4, #改成上面filepath下的一个文件filesize:None,}file_path os.path.join(head[filepath],head[filename])
filesize os.path.getsize(file_path)
head[filesize] filesize
# json_head json.dumps(head,ensure_asciiFalse) #字典转换成字符串
json_head json.dumps(head) #字典转换成字符串
bytes_head json_head.encode(utf-8) #字符串转换成bytes类型
print(json_head)
print(bytes_head)#计算head的长度因为接收端先接收我们自己定制的报头对吧
head_len len(bytes_head) #报头长度
pack_len struct.pack(i,head_len)
print(head_len)
print(pack_len)
sk.send(pack_len) #先发送报头长度
sk.send(bytes_head) #再发送bytes类型的报头#即便是视频文件也是可以按行来读取的也可以readline也可以for循环但是读取出来的数据大小就不固定了影响效率有可能读的比较小也可能很大像视频文件一般都是一行的二进制字节流。
#所有我们可以用read设定一个一次读取内容的大小一边读一边发一边收一边写
with open(file_path,rb) as f:while filesize:if filesize buffer: #是因为如果刚好等于的情况出现也是可以的。content f.read(buffer) #每次读取出来的内容sk.send(content)filesize - buffer #每次减去读取的大小else: #那么说明剩余的不够一次读取的大小了那么只要把剩下的读取出来发送过去就行了content f.read(filesize)sk.send(content)breaksk.close()tcp_client.py FTP上传下载文件的代码升级版(注咱们学完网络编程就留FTP作业这个代码可以参考当你用函数的方式写完之后再用面向对象进行改版却没有思路的时候再来看别骗自己昂~~) import socket
import struct
import json
import subprocess
import osclass MYTCPServer:address_family socket.AF_INETsocket_type socket.SOCK_STREAMallow_reuse_address Falsemax_packet_size 8192codingutf-8request_queue_size 5server_dirfile_uploaddef __init__(self, server_address, bind_and_activateTrue):Constructor. May be extended, do not override.self.server_addressserver_addressself.socket socket.socket(self.address_family,self.socket_type)if bind_and_activate:try:self.server_bind()self.server_activate()except:self.server_close()raisedef server_bind(self):Called by constructor to bind the socket.if self.allow_reuse_address:self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)self.socket.bind(self.server_address)self.server_address self.socket.getsockname()def server_activate(self):Called by constructor to activate the server.self.socket.listen(self.request_queue_size)def server_close(self):Called to clean-up the server.self.socket.close()def get_request(self):Get the request and client address from the socket.return self.socket.accept()def close_request(self, request):Called to clean up an individual request.request.close()def run(self):while True:self.conn,self.client_addrself.get_request()print(from client ,self.client_addr)while True:try:head_struct self.conn.recv(4)if not head_struct:breakhead_len struct.unpack(i, head_struct)[0]head_json self.conn.recv(head_len).decode(self.coding)head_dic json.loads(head_json)print(head_dic)#head_dic{cmd:put,filename:a.txt,filesize:123123}cmdhead_dic[cmd]if hasattr(self,cmd):funcgetattr(self,cmd)func(head_dic)except Exception:breakdef put(self,args):file_pathos.path.normpath(os.path.join(self.server_dir,args[filename]))filesizeargs[filesize]recv_size0print(-----,file_path)with open(file_path,wb) as f:while recv_size filesize:recv_dataself.conn.recv(self.max_packet_size)f.write(recv_data)recv_sizelen(recv_data)print(recvsize:%s filesize:%s %(recv_size,filesize))tcpserver1MYTCPServer((127.0.0.1,8080))tcpserver1.run()server.py server.py import socket
import struct
import json
import osclass MYTCPClient:address_family socket.AF_INETsocket_type socket.SOCK_STREAMallow_reuse_address Falsemax_packet_size 8192codingutf-8request_queue_size 5def __init__(self, server_address, connectTrue):self.server_addressserver_addressself.socket socket.socket(self.address_family,self.socket_type)if connect:try:self.client_connect()except:self.client_close()raisedef client_connect(self):self.socket.connect(self.server_address)def client_close(self):self.socket.close()def run(self):while True:inpinput(: ).strip()if not inp:continuelinp.split()cmdl[0]if hasattr(self,cmd):funcgetattr(self,cmd)func(l)def put(self,args):cmdargs[0]filenameargs[1]if not os.path.isfile(filename):print(file:%s is not exists %filename)returnelse:filesizeos.path.getsize(filename)head_dic{cmd:cmd,filename:os.path.basename(filename),filesize:filesize}print(head_dic)head_jsonjson.dumps(head_dic)head_json_bytesbytes(head_json,encodingself.coding)head_structstruct.pack(i,len(head_json_bytes))self.socket.send(head_struct)self.socket.send(head_json_bytes)send_size0with open(filename,rb) as f:for line in f:self.socket.send(line)send_sizelen(line)print(send_size)else:print(upload successful)clientMYTCPClient((127.0.0.1,8080))client.run()client.py client.py 转载于:https://www.cnblogs.com/H1050676808/p/10226438.html