网络编程 套接字socket 及 粘包
sockt 初识
- 五层协议 : 从传输层包括传输层以下 , 都是操作系统帮我们封装的各种head
- socket套接字充当的就是内置模块的角色
- socket 套接字,它存在于传输层与应用层之间的抽象层
- 避免你学习各层的接口以及协议的使用, socket已经封装好了所有的接口 .
- 直接使用这些接口或者方法即可 , 使用起来方便,提升开发效率
- socket 就是一个模块 , 通过使用学习模块提供的功能 , 建立客户端与服务端的通信
套接字的工作流程(基于TCP和 UDP两个协议)
TCP协议下的socket
socket()模块函数用法
import socket
socket.socket(socket_family,socket_type,protocal=0)
socket_family 可以是 AF_UNIX 或 AF_INET。socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM。protocol 一般不填,默认值为 0。
# 获取tcp/ip套接字
tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
tcpSock = socket.socket() # 括号里可以不写 默认
# 获取udp/ip套接字
udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
由于 socket 模块中有太多的属性。我们在这里破例使用了'from module import *'语句。使用 'from socket import *',我们就把 socket 模块里的所有属性都带到我们的命名空间里了,这样能 大幅减短我们的代码。
例如tcpSock = socket(AF_INET, SOCK_STREAM)
##============服务端套接字函数
s.bind() # 绑定(主机,端口号)到套接字
s.listen() # 开始TCP监听
s.accept() # 被动接受TCP客户的连接,(阻塞式)等待连接的到来
##============客户端套接字函数
s.connect() # 主动初始化TCP服务器连接
s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
##============公共用途的套接字函数
s.recv() # 接收TCP数据
s.send() # 发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完)
s.sendall() 发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)
s.recvfrom() 接收UDP数据
s.sendto() 发送UDP数据
s.getpeername() 连接到当前套接字的远端的地址
s.getsockname() 当前套接字的地址
s.getsockopt() 返回指定套接字的参数
s.setsockopt() 设置指定套接字的参数
s.close() # 关闭套接字
面向锁的套接字方法
s.setblocking() 设置套接字的阻塞与非阻塞模式
s.settimeout() 设置阻塞套接字操作的超时时间
s.gettimeout() 得到阻塞套接字操作的超时时间
面向文件的套接字的函数
s.fileno() 套接字的文件描述符
s.makefile() 创建一个与该套接字相关的文件
第一版 单个客户端与服务端通信(low版)
# server端
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 创建socket 对象
phone.bind(('127.0.0.1',8080)) # 8000 ~ 65535 绑定ip地址和端口
phone.listen(5) # TCP 开始监听
# conn相当于管道
conn, client_addr = phone.accept() # 被动接受TCP客户的连接,(阻塞式)等待连接的到来
print(conn, client_addr, sep='
') # 打印通道 和 客户端的地址
from_client_data = conn.recv(1024) # 每次 接收 客户端内容 的 最大限制 1024 bytes
print(from_client_data.decode('utf-8'))
conn.send(from_client_data.upper()) # 向客户端发送 信息
conn.close() # 挂电话
phone.close() # 关闭套接字
# client
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 买电话
phone.connect(('127.0.0.1',8080)) # 与客户端建立连接, 拨号
phone.send('hello'.encode('utf-8'))
from_server_data = phone.recv(1024)
print(from_server_data.decode('utf-8'))
phone.close() # 挂电话
第二版,通信循环
# server
import socket
photo = socket.socket()
photo.bind(('127.0.0.1',8080))
phone.listen(5)
conn, client_addr = phone.accept()
print(conn, client_addr, sep='
')
while 1: # 循环收发消息
try:
from_client_data = conn.recv(1024)
print(from_client_data.decode('utf-8'))
conn.send(from_client_data + b'SB')
except ConnectionResetError:
break
conn.close()
phone.close()
# client
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 买电话
phone.connect(('127.0.0.1',8080)) # 与客户端建立连接, 拨号
while 1: # 循环收发消息
client_data = input('>>>')
phone.send(client_data.encode('utf-8'))
from_server_data = phone.recv(1024)
print(from_server_data.decode('utf-8'))
phone.close() # 挂电话
第三版 通信 / 连接循环 ***
# server
import socket
phone = socket.socket()
phone.bind(('127.0.0.1', 8080))
phone.listen(5)
while 1: # 循环连接客户端
conn, client_addr = phone.accept()
print(conn,client_addr)
while 1:
try:
from_client_data = conn.recv(1024)
print(from_client_data.decode('utf-8'))
conn.send(from_client_data + b'SB')
except ConnectionResetError:
break
conn.close()
phone.close()
# client
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 买电话
phone.connect(('127.0.0.1',8080)) # 与客户端建立连接, 拨号
while 1:
client_data = input('>>>')
phone.send(client_data.encode('utf-8'))
from_server_data = phone.recv(1024)
print(from_server_data.decode('utf-8'))
phone.close() # 挂电话
远程执行命令的示例
# server
import socket
import subprocess
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.bind(('127.0.0.1',8080))
phone.listen(5)
while 1 : # 循环连接客户端
conn, client_addr = phone.accept()
print(client_addr)
while 1:
try:
cmd = conn.recv(1024)
ret = subprocess.Popen(cmd.decode('utf-8'),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
correct_msg = ret.stdout.read()
error_msg = ret.stderr.read()
conn.send(correct_msg + error_msg)
except ConnectionResetError:
break
conn.close()
phone.close()
# client
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 买电话
phone.connect(('127.0.0.1',8080)) # 与客户端建立连接, 拨号
while 1:
cmd = input('>>>')
phone.send(cmd.encode('utf-8'))
from_server_data = phone.recv(1024)
print(from_server_data.decode('gbk'))
phone.close() # 挂电话
粘包
粘包概念
- 粘包只会出现在tcp传输中, 是两种不同的数据连接在一起无法正常分离,就会出现粘包
粘包现象
- 粘包现象的根本原因 : 系统缓冲区
''' 服务端 发出 客户端 接收
第一次dir 数据418 < 1024 数据全部发出 数据全部接收
第二次ipconifg 数据1517 > 1024 数据只发送1024个字节 数据只接收1024个字节
第三次 dir 数据418 < 1024 数据是谁? 数据接收493个字节 '''
socket中造成粘包的原因
基于tcp协议的socket
- recv时客户端一次接收不完, 下次继续接收(如果间隔时间过长,后续的数据会与之前剩余的数据黏在一起)
- send数据时,连续的发送少量的数据(时间间隔很短),这些数据会积压在一起发送出去
- 发生粘包的原因主要是tcp是流式传输 , 无法判定不同数据的边界 ,传送到输入或输出缓冲区后发生积压现象
- 有两种情况
第一种是从输入缓冲区抓取数据时,无法将缓冲区的数据全部取出,导致下一次的数据传入缓冲区连接在一起
第二种是连续发送短数据,使输出缓冲区中的数据被积压,最后一起被发送
系统缓冲区
缓冲区的定义
- 缓冲区是内存中的一块区域
缓冲区的作用
- 如果你的网络出现短暂的异常或者波动接收数据就会出现短暂的中断,影响你下载或者上传的效率
- 缓冲高速设备与低速设备之间的速度差异,就socket的缓冲区而言,缓冲区的作用就是释放cpu,使cpu不用时刻接收发来的数据,cpu只需要在缓冲区中有数据时才去接受数据
- 但是 凡事都是双刃剑 , 缓冲区解决了上传下载的传输效率的问题 , 同时带来了粘包现象
提高上传下载的效率 , 保持稳定性 , 释放cpu ,减少与磁盘的交互
粘包的解决方案
-
错误示例
- 扩大recv的上限. recv(10240000000000) / 不是解决这个问题的根本原因 , 缓冲内容一直占内存中
- 故意延长recv的时间 , sleep 这样会非常影响效率
-
思路
分析一下功能
send的多次, recv一次 (不是一发一收制)
-
解决粘包现象的思路分析
-
当我第二次给服务器发送命令之前 , 我应该 循环 recv 直至将所有的数据全部取完
问题 :
如何限制循环次数?
当你发送的总bytes个数,与接收的总bytes个数相等时 , 循环结束
如何获取发送的总bytes 个数 : len() ---->3400字节 int
所以 :
服务端
send(总个数)
send(总数据)
-
-
总个数是什么类型? int() 3400 , send 需要发送bytes类型
send(总个数)
将int 转外成 bytes 即可 b' 3400
方案一:
str(3400) --->''3400"---->b'3400
解决统一头部问题
无论总字节个数是多少?
将不固定的int类型, 转化成固定长度的bytes类型 , 方便获取头部信息
用len获取bytes的总字节数
s1 = "alexsb"
b1 = s1.encode("utf-8")
print(b1)
print(len(b1)) # 6 len 可以计算 utf-8 的 字节数
import struct
import struct
# 将一个数字转化成等长度的bytes类型。
ret = struct.pack('i', 183346)
print(ret, type(ret), len(ret)) # b'2xccx02x00' <class 'bytes'> 4
# 通过unpack反解回来
ret1 = struct.unpack('i',ret)
print(ret1,type(ret), len(ret)) # (183346,) <class 'bytes'> 4
print(ret1[0])
# 但是通过struct 处理不能处理太大
#
ret2 = struct.pack('i', 1234567890) # 极限长度 再长就报错
print(ret2, type(ret2), len(ret2)) # b'xd2x02x96I' <class 'bytes'> 4
# 报错长度
ret2 = struct.pack('i', 12345678900) # 极限长度 再长就报错
print(ret2, type(ret2), len(ret2)) # 报错
- 总结 : 将 len 与 struct 结合使用
方案一 low版解决方案
# server 模拟远程控制主机cmd命令
import socket
import struct
import subprocess
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
while 1:
while 1:
conn, addr = server.accept()
try:
cmd = conn.recv(1024)
obj = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
ret = obj.stdout.read() + obj.stderr.read()
print(f'准备发送{len(ret)}个字节')
ret_len = struct.pack('i',len(ret)) # 包装固定4个字节 报头
conn.send(ret_len) # 发送报头
conn.send(ret) # 发送真实数据
except Exception:
break
conn.close()
server.close()
# 但是low版本有问题:
# 1,报头不只有总数据大小,而是还应该有MD5数据,文件名等等一些数据。
# 2,通过struct模块直接数据处理,不能处理太大。
# client
import socket
import struct
client = socket.socket()
client.connect(('127.0.0.1', 8080))
while 1:
cmd = input(">>>").strip()
client.send(cmd.encode('utf-8')) # 发送命令
from_server_head = client.recv(4) # 接收server包装过的固定报头
from_server_head_int = struct.unpack('i',from_server_head)[0] # 反序列报头是 元组 , +[0]才是报头
# 解析报头
print(f'准备接收{from_server_head_int}个字节')
from_server_data = b''
while from_server_head_int > len(from_server_data):
from_server_data += client.recv(1024) # 接收真实数据
print(from_server_data.decode('gbk'))
print(f'客户端接收了{len(from_server_data)}个字节')
client.close()
low版的局限性
问题一
- 非常大的数据 直接用struct 会报错
问题二
- 报头信息不可能只包含数据总大小 / md5 , 文件名 , 路径ct
方法二 旗舰版 / 可自定制报头版
传输流程
整个流程的大致解释:
我们可以把 ,字典里包含将要发送的真实数据的描述信息(大小啊之类的),然后 json序列化,然后用 struck
将序列化后的数据长度打包成
我们在网络上传输的所有数据 都叫做 数据包 ,数据包里的所有数据都叫做 报文,报文里面不止有你的数据,还有ip地址、mac地址、端口号等等,其实所有的报文都有报头,这个报头是协议规定的,看一下
发送时:
- 先发报头长度
- 再编码报头内容然后发送
- 最后发真实内容
接收时:
- 先手报头长度,用struct取出来
- 根据取出的长度收取报头内容,然后 解码,反序列化
- 从反序列化的结果中取出待取数据的描述信息,然后去取真实的数据内容
服务(器)端
# server
import socket
import subprocess
import struct
import json
server = socket.socket()
server.bind(('127.0.0.1', 8088))
server.listen(5)
while 1:
conn,addr = server.accept()
print('start')
try:
cmd = conn.recv(1024)
print(f'{cmd.decode("utf-8")}')
obj = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
ret = obj.stdout.read() + obj.stderr.read()
ret = ret.decode('gbk').encode('utf-8')
# 1.制作 字典式 报头
head_dict = {'md5':'jrwiwfwr432knd324n23n2j131k',
'file_name':'朝花夕拾',
'file_size':len(ret)}
# 2.将报头字典转化成json字符串
head_dict_json = json.dumps(head_dict)
# 3. 将json字符串 转化成bytes
head_dict_json_bytes = head_dict_json.encode('utf-8')
# 4. 获取报头的长度
head_len = len(head_dict_json_bytes)
# 5.将长度转化成固定的4个字节
head_len_bytes = struct.pack('i',head_len)
# 6. 发送固定的4个字节报头
conn.send(head_len_bytes)
# 7. 发送字典
conn.send(head_dict_json_bytes)
# 8. 发送原数据
conn.send(ret)
except ConnectionResetError:
break
conn.close()
server.close()
客户端
import socket
import struct
import json
client = socket.socket()
client.connect(('127.0.0.1', 8088))
# 发消息
while 1:
cmd = input('>>>')
client.send(cmd.encode('utf-8'))
# 1. 接收报头
head_4 = client.recv(4)
# 2. 将报头反解回int类型
head_size = struct.unpack('i',head_4)[0] # 接收 并 解析4个字节的报头
ret = json.loads(client.recv(head_size).decode('utf-8')) # 接收 并 转化 固定长度的字典
print(ret) # 字典
recv_date = b''
while len(recv_date) < head_size:
recv_date += client.recv(1024)
print(recv_date.decode("utf-8"))
client.close()