粘包现象,粘包解决方法

from socket import *
import subprocess

'''基于tcp服务的远程执行命令'''
# ip_port = ('127.0.0.1', 8000)
# buffer_size = 1024
# back_log = 10
#
# tcp_server = socket(AF_INET, SOCK_STREAM)
#
# tcp_server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
#
# tcp_server.bind(ip_port)
#
# tcp_server.listen(back_log)
#
# while True:
#  conn, addr = tcp_server.accept()
#  while True:
#     try:
#        data = conn.recv(buffer_size)
#        # if not data: break
#        print(data)
#        cmd = subprocess.Popen(data.decode('utf-8'),
#                               shell=True,
#                               stdout=subprocess.PIPE,
#                               stderr=subprocess.PIPE,
#                               stdin=subprocess.PIPE)
#        res = cmd.stdout.read() # 读取的信息本就是字节类型,所以发送的时候无需再进行对其编码
#        err = cmd.stderr.read()
#        if res:
#           conn.send(res)
#        elif err:
#           conn.send(err)
#        else:
#           conn.send('执行成功'.encode('gbk')) # 当客户端输入'cd..'时,cmd.stdout.read()返回为空;本身cmd命令中cd..是返回上一级目录
#     except Exception as e:
#        break
#  conn.close()
#
# tcp_server.close()


'''基于udp实现远程执行命令'''
# ip_port = ('127.0.0.1', 8080)
# buffer_size = 1024
#
# udp_server = socket(AF_INET, SOCK_DGRAM)
#
# udp_server.bind(ip_port)
#
# while True:
#  data, addr = udp_server.recvfrom(buffer_size)
#  print('客户端发来的消息:', data.decode('utf-8'))
#  cmd = subprocess.Popen(data.decode('utf-8'),
#                         shell=True,
#                         stdout=subprocess.PIPE,
#                         stderr=subprocess.PIPE,
#                         stdin=subprocess.PIPE)
#  res = cmd.stdout.read()
#  err = cmd.stderr.read()
#  if res:
#     udp_server.sendto(res, addr)
#  elif err:
#     udp_server.sendto(err, addr)
#  else:
#     udp_server.sendto('执行成功'.encode('gbk'), addr)
#
# udp_server.colse()


'''不粘包udp'''
# ip_port = ('127.0.0.1', 8080)
# buffer_size = 1024
#
# udp_server = socket(AF_INET, SOCK_DGRAM)
#
# udp_server.bind(ip_port)
#
# data, addr = udp_server.recvfrom(buffer_size)
# print('第一次接受到的信息:', data.decode('utf-8'))
#
# data1, addr1 = udp_server.recvfrom(buffer_size)
# print('第二次接受到的信息:', data1.decode('utf-8'))

# data2, add2r = udp_server.recvfrom(buffer_size)
# print('第三次接受到的信息:', data2.decode('utf-8'))


'''tcp粘包1'''
# ip_port = ('127.0.0.1', 8080)
# back_log = 5
#
# tcp_server = socket(AF_INET, SOCK_STREAM)
#
# tcp_server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
#
# tcp_server.bind(ip_port)
#
# tcp_server.listen(5)
#
# conn, addr = tcp_server.accept()
#
# data1 = conn.recv(1024)
# print('第一次收到的消息:', data1.decode('utf-8'))
#
# data2 = conn.recv(1024)
# print('第二次收到的消息:', data2.decode('utf-8'))
#
# data3 = conn.recv(1024)
# print('第三次收到的消息:', data3.decode('utf-8'))
#
# conn.close()
#
# tcp_server.close()


'''tcp粘包2'''
# ip_port = ('127.0.0.1', 8080)
# back_log = 5
#
# tcp_server = socket(AF_INET, SOCK_STREAM)
#
# tcp_server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
#
# tcp_server.bind(ip_port)
#
# tcp_server.listen(back_log)
#
# conn, addr = tcp_server.accept()
#
# data1 = conn.recv(2)
# print('第一次接收信息:', data1.decode('utf-8'))
#
# data2 = conn.recv(2)
# print('第二次接收信息:', data2.decode('utf-8'))
#
# data3 = conn.recv(2)
# print('第三次接收信息:', data3.decode('utf-8'))
#
# conn.close()
#
# tcp_server.close()


'''解决粘包1'''
import struct

ip_port = ('127.0.0.1', 8080)
back_log = 5
buffer_size = 1024

tcp_server = socket(AF_INET, SOCK_STREAM)

tcp_server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)

tcp_server.bind(ip_port)

tcp_server.listen(back_log)

while True:
   conn, addr = tcp_server.accept()
   while True:
      try:
         data = conn.recv(buffer_size) # 收到客户端发送的命令消息
         if not data: break
         print('客户端发送的消息是:', data.decode('utf-8'))
         cmd = subprocess.Popen(data.decode('utf-8'),
                               shell=True,
                               stdout=subprocess.PIPE,
                               stderr=subprocess.PIPE,
                               stdin=subprocess.PIPE)
         out = cmd.stdout.read()
         err = cmd.stderr.read()
         # print(out) # cd..这样的命令,这两个结果都是为空,即b''
         # print(err)

         # if out:
         #  conn.send('1'.encode('utf-8')) # 如果命令正确,发送1
         #  data_1 = conn.recv(buffer_size) # 收到客户端发送过来的确认消息
         #  num = len(out)  # 计算出消息的总长度
         #  conn.send(str(num).encode('utf-8'))  # 将计算出的消息总长度发送给客户端
         #  msg = conn.recv(buffer_size) # 收到客户端发过来的确认长度信息
         #  print('客户端收到的长度值为:', msg.decode('utf-8'))
         #  conn.send(out) # 发送命令执行结果给客户端
         # elif err:
         #  conn.send('2'.encode('utf-8')) # 如果命令错误,发送2
         #  data_1 = conn.recv(buffer_size) # 收到客户端发送过来的确认消息
         #  conn.send(err) # 发送错误消息结果给客户端
         # else:
         #  conn.send('3'.encode('utf-8')) # 如果是cd..这样无返回结果的命令,发送3
         #  data_1 = conn.recv(buffer_size) # 收到客户端发送过来的确认消息
         #  conn.send('执行成功无需等待,请继续输入...'.encode('gbk')) # 发送指定消息给客户端

         '''代码优化'''
         # if out:
         #  cmd_res = out
         # elif err:
         #  cmd_res = err
         # else:
         #  cmd_res = '执行成功无需等待,请继续输入...'.encode('gbk')
         # num = len(cmd_res)
         # conn.send(str(num).encode('utf-8'))
         # conn.recv(buffer_size)
         # conn.send(cmd_res)

         '''第二种方法'''
         if out:
            cmd_res = out
         elif err:
            cmd_res = err
         else:
            cmd_res = '执行成功无需等待,请继续输入...'.encode('gbk')
         conn.send(struct.pack('i', len(cmd_res))) # struct.pack(),第一个参数为类型(参照他人博客图),第二个参数为数字,得到结果为固定4个字节
         conn.send(cmd_res)

      except Exception:
         break
   conn.close()
tcp_server.close()
# 另一个模块代码
from socket import *

'''基于tcp服务的远程执行命令'''
# ip_port = ('127.0.0.1', 8000)
# buffer_size = 102400
#
# tcp_client = socket(AF_INET, SOCK_STREAM)
#
# tcp_client.connect(ip_port)
#
# while True:
#  msg = input('请输入:').strip()
#  if not msg: continue
#  if msg == 'quit': break
#  tcp_client.send(msg.encode('utf-8'))
#  data = tcp_client.recv(buffer_size)
#  print('命令执行结果是:', data.decode('gbk')) # 此处解码要用gbk
#
# tcp_client.close()
# 总结:当tcp客户端recv()收录的数据量较小时,也就是一次性读取不完缓冲区的数据,那么会出现粘包的现象(udp不会粘包);什么意思呢?就是说一次读取不完的数据下次
# 执行时会读取剩下的数据,这就是所谓的粘包;那么buffer_size这个数据到底该设置多少合适呢?最好不要超过8192(8k),太大的话会失帧


'''基于udp服务的远程执行命令'''
# ip_port = ('127.0.0.1', 8080)
# buffer_size = 102400 # 这里的数据量可以设置大一些
#
# udp_client = socket(AF_INET, SOCK_DGRAM)
#
# while True:
#  msg = input('请输入:').strip()
#  if msg == 'quit': break
#  if not msg: continue
#  udp_client.sendto(msg.encode('utf-8'), ip_port)
#  data, addr = udp_client.recvfrom(buffer_size)
#  print(data.decode('gbk'))
#
# udp_client.close()
# 总结:在win系统上会出现这样的错误'OSError: [WinError 10040] 一个在数据报套接字上发送的消息大于内部消息缓冲区或其他一些网络限制,或该用户用于接收数据报的缓冲区比数据报小。'
# 客户端换到其它系统则不会,而其它系统上执行结果是:udp客户端只展示根据recvfrom()收录的数据量大小,其余未收录的数据则不展示,且下一次的链接请求也只
# 展示下一次请求到的结果;如果断开服务端,客户端也能发送,但客户端收不到所以卡在了recvfrom(),不论有没有链接只管发不管能不能收,所以发的消息丢了,这也就说明为什么udp会丢包的原因


'''不粘包udp'''
# ip_port = ('127.0.0.1', 8080)
#
# udp_client = socket(AF_INET, SOCK_DGRAM)
#
# udp_client.sendto(b'hello', ip_port)
#
# udp_client.sendto(b'hei', ip_port)
#
# udp_client.sendto(b'ha', ip_port)
# 总结:当发送次数小于接收次数时,客户端自动断开了,而服务端还在运行等着下一个客户端的发送;当发送次数大于接收次数时,服务端断开了,而客户端也
# 不管数据能否正常发送到服务端,发送完毕后也断开了;udp是面向信息的协议,应用程序必须以消息为单位提取数据;udp的recvfrom是阻塞的,一个recvfrom(x)必须对应
# 一个sendto(y),收完了x个字节的数据就算完成,若是y>x,数据就会丢失,这意味着udp根本不会粘包,但是会丢失数据,不可靠


'''tcp粘包1'''
# ip_port = ('127.0.0.1', 8080)
#
# tcp_client = socket(AF_INET, SOCK_STREAM)
#
# tcp_client.connect(ip_port)
#
# tcp_client.send(b'hello')
#
# tcp_client.send(b'hei')
#
# tcp_client.send(b'ha')
#
# tcp_client.close()
# 总结:发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。
# 所以服务端虽然也有3此接收,但是第一次接收把所有信息都接收完毕了,而第二次和第三次接收为空(发和收都是在操作自己的缓冲区),为什么为空呢?因为客户端
# 发完消息后就把链接断开了,客户端链接断开,那么服务端的recv就没有任何意义了,所以也断开链接,接收为空


'''tcp粘包2'''
# ip_port = ('127.0.0.1', 8080)
#
# tcp_client = socket(AF_INET, SOCK_STREAM)
#
# tcp_client.connect(ip_port)
#
# tcp_client.send(b'hello')
#
# tcp_client.send(b'hehe')
#
# tcp_client.send(b'hahaha')
#
# tcp_client.close()
# 总结:接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)


'''解决粘包1'''
import struct

ip_port = ('127.0.0.1', 8080)
buffer_size = 1024

tcp_client = socket(AF_INET, SOCK_STREAM)

tcp_client.connect(ip_port)

while True:
      msg = input('请输入:').strip()
      if not msg: continue
      if msg == 'quit': break
      tcp_client.send(msg.encode('utf-8')) # 发送输入指令消息给服务端

      # data = tcp_client.recv(buffer_size) # 收到服务端发送过来的'1'或'2'或'3'
      # tcp_client.send(('收到分支值:%s' % data.decode('utf-8')).encode('utf-8')) # 发送消息给服务端,以确认收到发送过来的值'1'或'2'或'3'
      # if int(data.decode('utf-8')) == 1: # 如果值为'1',那么指令正确
      #  num = tcp_client.recv(buffer_size) # 收到服务端发送过来的消息,文本总长度值
      #  print('长度值为:', num.decode('utf-8')) # 4588
      #  tcp_client.send(num) # 发送消息给服务端,确认收到消息
      #  cmd_num = 0 # 定义初始变量0,用来对文本长度的一个比较,用作循环
      #  cmd_str = b'' # 定义为字节类型,直接可以和收到的服务端消息拼接
      #  while cmd_num < int(num.decode('utf-8')): # 作判断,如果这个值小于文本长度值,那么继续循环
      #     cmd_str += tcp_client.recv(buffer_size) # 对读取到的消息进行拼接
      #     cmd_num = len(cmd_str) # 因为cmd_str一直在拼接,文本长度在增加,值也在增加
      # else:
      #  cmd_str = tcp_client.recv(buffer_size) # 如果值为'2'或'3',那么直接读取消息

      '''代码优化'''
      # num = tcp_client.recv(buffer_size)
      # print(num.decode('utf-8')) # 4588
      # tcp_client.send('ok'.encode('utf-8'))
      # cmd_num = 0
      # cmd_str = b''
      # while cmd_num < int(num.decode('utf-8')):
      #  cmd_str += tcp_client.recv(buffer_size)
      #  cmd_num = len(cmd_str)

      '''第二种方法'''
      # 使用struct库,将计算出的长度变为固定的4个字节发送给客户端,再发送执行命令后的结果给客户端;这样就不需要过多的双方收发消息,节约时间(如并发过大)
      # www.cnblogs.com/coser/archive/2011/12/17/2291160.html浅析python中的struct模块
      num_b = tcp_client.recv(4) # 服务端发送了两次消息,产生了粘包,但是前面4个字节的消息肯定是长度转换的字节,所以第一次收就只收4个字节
      num = struct.unpack('i', num_b)[0] # struct.unpack(),第一个参数为struct.pack()定义类型,第二个参数为struct.pack()得到的字节,结果为一个元组,第一位就是数字
      cmd_num = 0
      cmd_str = b''
      while cmd_num < num:
         cmd_str += tcp_client.recv(buffer_size)
         cmd_num = len(cmd_str)

      print(cmd_str.decode('gbk'))
tcp_client.close()

# 第三种方法待实验
'''
须知:只有TCP有粘包现象,UDP永远不会粘包
发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,
应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现
粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是
很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,
TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。

例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束

所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。

此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,
通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

1.TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,
因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,
然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
2.UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,
所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,
就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
3.tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的
是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头,实验略

udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠

tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。

两种情况下会发生粘包:
1.发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)
2.接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)

拆包的发生情况:
当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。

补充问题一:为何tcp是可靠传输,udp是不可靠传输
基于tcp的数据传输请参考我的另一篇文章http://www.cnblogs.com/linhaifeng/articles/5937962.html,tcp在数据传输时,发送端先把数据发送到
自己的缓存中,然后协议控制将缓存中的数据发往对端,对端返回一个ack=1,发送端则清理缓存中的数据,对端返回ack=0,则重新发送数据,所以tcp是可靠的.
而udp发送数据,对端是不会返回确认信息的,因此不可靠

补充问题二:send(字节流)和recv(1024)及sendall
recv里指定的1024意思是从缓存里一次拿出1024个字节的数据
send的字节流是先放入己端缓存,然后由协议控制将缓存内容发往对端,如果待发送的字节流大小大于缓存剩余空间,那么数据丢失,用sendall就会循环调用send,数据不会丢失
https://blog.csdn.net/jing16337305/article/details/79856116此篇对send和sendall的讲解博文,意思就是将最好是尽量用sendall,用法和send一样
'''


while True: print('studying...')
原文地址:https://www.cnblogs.com/xuewei95/p/14802907.html