一 粘包现象(基于TCP协议实现远程执行命令)

1、TCP协议,会出现粘包现象

例:ipconfig命令,客户端收到的字符串比较短,客户端能够收完整,

  tasklist命令,客户端收到的字符串超过1024,客户端收到信息不完整,就是粘包问题

服务端:

# 服务端应该满足两个特点
# 1、一直对外提供服务
# 2、并发的服务多个客户端
import subprocess
from socket import * server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1', 8081))
server.listen(5) # 服务端应该做两件事
# 第一件事:循环地从半链接池中取出链接请求与其建立双向链接,拿到链接对象
while True:
conn, client_addr = server.accept() # 第二件事:拿到链接对象,与其进行通信循环
while True:
try:
cmd = conn.recv(1024)
if len(cmd) == 0:
break
obj = subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
stdout_res = obj.stdout.read()
stderr_res = obj.stderr.read()
print(len(stdout_res)+len(stderr_res))
print(stderr_res.decode('gbk'))
print(stdout_res.decode('gbk'))
conn.send(stdout_res+stderr_res)
# with open('1.mp4',model='rb') as f:
# for line in f:
# conn.send(line)
except Exception:
break
conn.close()

客户端:

from socket import *

client = socket(AF_INET, SOCK_STREAM)
client.connect(('127.0.0.1', 8081)) while True:
msg = input('请输入命令>>>:').strip()
if len(msg) == 0:
continue
client.send(msg.encode('utf-8')) # 解决粘包问题思路:
# 1、拿到数据的总大小total_size
# 2、recv_size=0,循环接受,每接受一次,recv_size+=接受的长度
# 3、直到recv_size=total_size,结束循环 cmd_res = client.recv(1024) # 本次接受,最大接受1024Bytes
print(cmd_res.decode('gbk')) # windows系统用gbk # 粘包问题出现的原因
# 1、tcp是流式协议,数据像水流一样粘在一起,没有任何边界区分
# 2、收数据没收干净,有残余,就会下一次结果混淆在一起。 # 解决的核心方法就是:每次都收干净,不要任何残留

2、UDP协议,不会出现粘包现象

一个sendto对应一个recvfrom,不会出现粘包的底层原理是,不需要建立三次握手,发信息类似于集装箱

超出的集装箱部分的信息,就扔掉,所以不会出现粘包现象,是不可靠传输。

服务端:

import socket

server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server.bind(('127.0.0.1', 8080))
res1 = server.recvfrom(1024) print(res1) res2 = server.recvfrom(1024)
print(res2) server.close()

客户端:

import socket

client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
client.sendto(b'hello', ('127.0.0.1', 8080))
client.sendto(b'world', ('127.0.0.1', 8080)) client.close() # UDP协议一个sendto 对应 一个服务端的recvfrom

二 什么是粘包

发送端可以是一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时才会清除缓冲区内容。数据是可靠的,但是会粘包。

两种情况下会发生粘包。

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

服务端:

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

客户端:

import socket
BUFSIZE=1024
ip_port=('127.0.0.1',8080) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(ip_port) s.send('hello'.encode('utf-8'))
s.send('feng'.encode('utf-8'))

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

服务端:

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

客户端:

import socket
BUFSIZE=1024
ip_port=('127.0.0.1',8080) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(ip_port) s.send('hello feng'.encode('utf-8'))

三、解决粘包的方法

1、struct模块

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

struct.pack('i',1111111111111)

2、解决办法

为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据

服务端:

import subprocess
import struct
from socket import * server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1', 8081))
server.listen(5) # 服务端应该做两件事
# 第一件事:循环地从半链接池中取出链接请求与其建立双向链接,拿到链接对象
while True:
conn, client_addr = server.accept() # 第二件事:拿到链接对象,与其进行通信循环
while True:
try:
cmd = conn.recv(1024)
if len(cmd) == 0:
break
obj = subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
stdout_res = obj.stdout.read()
stderr_res = obj.stderr.read()
total_size = len(stdout_res) + len(stderr_res) # 1、先发头信息(固定长度的bytes):对数据描述信息
# int--->固定长度的bytes
header = struct.pack('i', total_size)
conn.send(header)
# 2、再发真实的数据
conn.send(stdout_res)
conn.send(stderr_res)
except Exception:
break
conn.close()

客户端:

import struct
from socket import * client = socket(AF_INET, SOCK_STREAM)
client.connect(('127.0.0.1', 8081)) while True:
msg = input('请输入命令>>>:').strip()
if len(msg) == 0:
continue
client.send(msg.encode('utf-8')) # 解决粘包问题思路:
# 一、先收固定长度的头:解析出数据的描述信息,包括数据的总大小total_size
header = client.recv(4)
total_size = struct.unpack('i', header)[0] # 解析出来,是个小元组
# 二、根据解析出的描述信息,接受真实的数据
# 2、recv_size=0,循环接受,每接受一次,recv_size+=接受的长度
# 3、直到recv_size=total_size,结束循环
recv_size = 0
while recv_size < total_size:
recv_data = client.recv(1024) # 本次接受,最大接受1024Bytes
recv_size += len(recv_data)
print(recv_data.decode('gbk'), end='') # windows系统用gbk
else:
print() # 粘包问题出现的原因
# 1、tcp是流式协议,数据像水流一样粘在一起,没有任何边界区分
# 2、收数据没收干净,有残余,就会下一次结果混淆在一起。 # 解决的核心方法就是:每次都收干净,不要任何残留

3、自制报头,报头是字典情况

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

发送时:

先发报头长度

再编码报头内容然后发送

最后发真实内容

接收时:

先手报头长度,用struct取出来

根据取出的长度收取报头内容,然后解码,反序列化

从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容

服务端:

import subprocess
import struct
import json
from socket import * server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1', 8081))
server.listen(5) # 服务端应该做两件事
# 第一件事:循环地从半链接池中取出链接请求与其建立双向链接,拿到链接对象
while True:
conn, client_addr = server.accept() # 第二件事:拿到链接对象,与其进行通信循环
while True:
try:
cmd = conn.recv(1024)
if len(cmd) == 0:
break
obj = subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
stdout_res = obj.stdout.read()
stderr_res = obj.stderr.read()
total_size = len(stdout_res) + len(stderr_res) # 1、制作头
header_dic = {
'filename': 'a.txt',
'total_size': total_size,
'md5': '12sddeio'
}
json_str = json.dumps(header_dic)
json_str_bytes = json_str.encode('utf-8')
# 2、先发头信息(固定长度的bytes):对数据描述信息
# int--->固定长度的bytes
header_size = struct.pack('i', len(json_str_bytes))
# 发头的长度信息
conn.send(header_size) # 3、发头信息
conn.send(json_str_bytes)
# 4、再发真实的数据
conn.send(stdout_res)
conn.send(stderr_res)
except Exception:
break
conn.close()

客户端:

import struct
import json
from socket import * client = socket(AF_INET, SOCK_STREAM)
client.connect(('127.0.0.1', 8081)) while True:
msg = input('请输入命令>>>:').strip()
if len(msg) == 0:
continue
client.send(msg.encode('utf-8')) # 解决粘包问题思路:
# 一、先收固定长度的头:解析出数据的描述信息,包括数据的总大小total_size
header_size = client.recv(4)
header_len = struct.unpack('i', header_size)[0] json_str_bytes = client.recv(header_len)
json_str = json_str_bytes.decode('utf-8')
header_dic = json.loads(json_str)
print(header_dic)
total_size = header_dic['total_size']
# 二、根据解析出的描述信息,接受真实的数据
# 2、recv_size=0,循环接受,每接受一次,recv_size+=接受的长度
# 3、直到recv_size=total_size,结束循环
recv_size = 0
while recv_size < total_size:
recv_data = client.recv(1024) # 每次接受,最大接受1024Bytes
recv_size += len(recv_data)
print(recv_data.decode('gbk'), end='') # windows系统用gbk
else:
print() # 粘包问题出现的原因
# 1、tcp是流式协议,数据像水流一样粘在一起,没有任何边界区分
# 2、收数据没收干净,有残余,就会下一次结果混淆在一起。 # 解决的核心方法就是:每次都收干净,不要任何残留

四、socketserver实现并发

1、TCP协议的并发

服务端:

import socketserver

class MyRequestHandle(socketserver.BaseRequestHandler):
def handle(self):
print(self.request) # 如果TCP协议,self.request--->conn
print(self.client_address)
while True:
try:
msg = self.request.recv(1024)
if len(msg) == 0:
break
self.request.send(msg.upper())
except Exception:
break
self.request.close() # 服务端应该做两件事
# 第一件事:循环地从半链接池中取出链接请求与其建立双向链接,拿到链接对象
s = socketserver.ThreadingTCPServer(('127.0.0.1', 8080), MyRequestHandle)
s.serve_forever()
# 等同于
# while True:
# conn, client_addr = server.accept() # 第二件事:拿到链接对象,与其进行通信循环----》handle方法

客户端:

from socket import *

client = socket(AF_INET, SOCK_STREAM)
client.connect(('127.0.0.1', 8080)) while True:
msg = input('请输入命令>>>:').strip()
if len(msg) == 0:
continue
client.send(msg.encode('utf-8')) res = client.recv(1024)
print(res.decode('utf-8'))

2、UDP协议

服务端:

import socketserver

class MyRequesthandle(socketserver.BaseRequestHandler):
def handle(self):
client_data = self.request[0]
server = self.request[1]
client_address = self.client_address
print('客户端发来的数据{}'.format(client_data))
server.sendto(client_data.upper(), client_address) s = socketserver.ThreadingUDPServer(('127.0.0.1', 8080), MyRequesthandle)
s.serve_forever() # 相当于:只负责循环的收
# while True:
# data, server_addr = client.recvfrom(1024)
# 启动一个线程处理后续的事情(data,client_addr)

客户端:

import socket

client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # 数据报协议---》UDP协议

while True:
msg = input('>>>:').strip()
client.sendto(msg.encode('utf-8'), ('127.0.0.1', 8080))
data, server_addr = client.recvfrom(1024)
print(data.decode('utf-8'))
client.close()

python高级技术(网络编程二)的更多相关文章

  1. python高级之网络编程

    python高级之网络编程 本节内容 网络通信概念 socket编程 socket模块一些方法 聊天socket实现 远程执行命令及上传文件 socketserver及其源码分析 1.网络通信概念 说 ...

  2. 第六篇:python高级之网络编程

    python高级之网络编程   python高级之网络编程 本节内容 网络通信概念 socket编程 socket模块一些方法 聊天socket实现 远程执行命令及上传文件 socketserver及 ...

  3. python基础:网络编程

    一.网络编程 简而言之,就是通过代码打开一个url,获得返回结果并做处理.通常所说的python爬虫,就属于网络编程 二.urllib模块进行网络编程 这个方法很繁琐,不建议使用.了解 示例1: 获取 ...

  4. Python基础之网络编程:2、OSI协议之七层协议

    目录 Python基础之网络编程 一.网络编程前戏 二.OSI七层协议 简介: 1.物理连接层 2.数据链路层 网络相关专业名词 3.网络层 4.传输层 Python基础之网络编程 一.网络编程前戏 ...

  5. Linux网络编程(二)

    Linux网络编程(二) 使用多进程实现服务器并发访问. 采用多进程的方式实现服务器的并发访问的经典范例. 程序实现功能: 1.客户端从标准输入读入一行文字,发送到服务器. 2.服务器接收到客户端发来 ...

  6. Python之路 - 网络编程之粘包

    Python之路 - 网络编程之粘包 粘包

  7. Python之路 - 网络编程初识

    Python之路 - 网络编程初识 前言

  8. python部分 + 数据库 + 网络编程

    PS:附上我的博客地址,答案中略的部分我的博客都有,直接原标题搜索即可.https://www.cnblogs.com/Roc-Atlantis/ 第一部分 Python基础篇(80题) 为什么学习P ...

  9. python之Socket网络编程

    什么是网络? 网络是由节点和连线构成,表示诸多对象及其相互联系.在数学上,网络是一种图,一般认为专指加权图.网络除了数学定义外,还有具体的物理含义,即网络是从某种相同类型的实际问题中抽象出来的模型.在 ...

  10. python 之socket 网络编程

    socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过"套接字"向网络发出请求或者应答网络请求. socket起源于Un ...

随机推荐

  1. NC51097 Parity game

    题目链接 题目 题目描述 Now and then you play the following game with your friend. Your friend writes down a se ...

  2. javascript 对http的get请求参数编码encodeURIComponent、encodeURI 和Java 解码

    JavaScript 代码encode functionfindNE(){ var nd = document.getElementById("NE").value; nd = e ...

  3. Ubuntu20.04和22.04离线安装PostgreSQL14

    今天安装 Postgresql14 遇到一个问题, 目标服务器只有内网, 内网提供标准的apt仓库, 但是因为不能连接外网, 所以没法添加第三方仓库, 这样安装pg14就成了问题. 从pg的官网看, ...

  4. STM32F407VET6烧录出现flash download failed target dll has been cancelled

    今天在通过stlink烧录一个长时间未用的STM32F407VET6 Black Board的时候, 出现错误 Internal command error Flash download failed ...

  5. 【OpenGL ES】绘制立方体

    1 前言 ​ 本文主要介绍使用 OpenGL ES 绘制立方体,读者如果对 OpenGL ES 不太熟悉,请回顾以下内容: 绘制三角形 绘制彩色三角形 绘制正方形 绘制圆形 ​ 在绘制立方体的过程中, ...

  6. linux删除目录下指定文件方法

    1.删除当前目录下文件名含有2013的文件 ls | grep  2013 | xargs rm --To be continue...

  7. virtualbox安装oracle linux后找不到eth0

    用VirtualBox装oracle linux, ifconfig发现没有eth0: 按照以下步骤操作: 1 用ifconfig eth0 up启动网卡(默认未开启),执行ifconfig下看到et ...

  8. Java Console类

    用于从控制台设备读取字符信息,通常是文本和密码.尤其读取密码字符时是看不见的. 下面给出一个例子: import java.io.Console; /** * @author xusucheng * ...

  9. gin框架中的c.Next()/c.Abort()

    package main import ( "fmt" "github.com/gin-gonic/gin" ) func func1(c *gin.Conte ...

  10. Vue源码学习(十一):计算属性computed初步学习

    好家伙,   1.Computed实现原理 if (opts.computed) { initComputed(vm,opts.computed); } function initComputed(v ...