socket编程
- socke编程又称套接字编程
TCP套接字编程模板
- server 服务端
# 导入模块
import socket
# 获取服务端对象
server = socket.socket()
# 获取IP 和 端口号
IP='127.0.0.1'
PORT=9888
# 将IP和套接字绑定给对象
server.bind((IP,PORT))
# 监听
server.listen(5)
while True:
# 接收
conn,addr=server.accept()
msg=conn.recv(1024)
msg=msg.decode('utf-8')
if msg=='q':
conn.close()
break
print(msg)
while True:
# 发送
send_msg=input("请输入发送的信息:").strip()
if len(send_msg)==0:
continue
conn.send(send_msg.encode('utf-8'))
break
server.close()
- client 客户端
# 导入模块
import socket
while True:
# 获取客户端对象
client=socket.socket()
# 获取IP 和 端口号
IP = '127.0.0.1'
PORT = 9888
# 绑定套接字
client.connect((IP,PORT))
# 发送信息
send_msg=input("请输入想要发送的信息:").strip()
if len(send_msg)==0:
continue
client.send(send_msg.encode('utf-8'))
if send_msg=='q':
client.close()
break
# 接收数据
msg=client.recv(1024)
print(msg.decode('utf-8'))
UDP套接字编程模板
- client 客户端
# 导入模块
import socket
# 获取客户端对象
client=socket.socket(type=socket.SOCK_DGRAM)
# 获取IP和端口号
IP='127.0.0.1'
PORT=9881
# 绑定套接字
client.connect((IP,PORT))
send_msg='我是客户端'
client.sendto(send_msg.encode('utf-8'),(IP,PORT))
# 接收信息
msg,addr=client.recvfrom(1024)
print(msg.decode('utf-8'))
print(addr)
- server 服务端
# 导入模块
import socket
# 获取服务端对象
server=socket.socket(type=socket.SOCK_DGRAM)
# 获取IP和端口号
IP='127.0.0.1'
PORT=9881
# 绑定套接字
server.bind((IP,PORT))
# 接收信息
msg,addr=server.recvfrom(1024)
print(msg.decode('utf-8'))
print(addr)
# 发送消息
send_msg='我是服务端'
server.sendto(send_msg.encode('utf-8'),addr)
粘包
(一)什么是粘包
-
只有TCP有粘包的现象,UDP永远不会粘包
(1)socket收发消息的原理
- socket收发消息的原理
- 发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或者6K数据,或者一次只提走几个字节的数据
- 也就是说,应用程序所看到的数据是一个整体,或说是一个流,一条消息有多少字节对应用程序时不可见的
- 因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因
- 而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的
(2)如何定义消息
- 可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
- 例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
- 此外
- 发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率
- 发送方往往要收集到足够多的数据后才发送一个TCP段。
- 若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据
(3)TCP
- TCP是面向连接的,面向流的,提供高可靠性服务
- 收发两端(客户端和服务端)都要有一一成对的socket
- 因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化算法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包
- 这样,接收端,就难于分辨出来了,必须提供科学的拆包机制
- 即面向流的通信时无消息保护边界的
(4)UDP
- UDP是无连接的,面向消息的,提供高效率服务
- 不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息)
- 这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
(5)小结
- tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头,实验略
- udp的recvfrom是阻塞的
- 一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠
- tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
- 两种情况下会发生粘包。
- 发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)
(二)什么是粘包问题
- 客户端发送需要执行的代码
- 服务端接收到客户端传过来的代码
- 服务端调用方法执行代码并拿到执行后的结果
- 服务端将执行后的结果进行返回
- 客户端接收到服务端返回的结果并做打印输出
(1)server 服务端
# 导入模块
import socket
import subprocess
# 创建服务对象
server=socket.socket()
# 获取IP和端口号
IP='127.0.0.1'
PORT=9882
# 创建连接桥梁
server.bind((IP,PORT))
# 指定半连接池大小
server.listen(5)
# 接收数据和发送消息
while True:
try:
# 接收数据
conn,addr=server.accept()
# 得到的数据
msg=conn.recv(1024)
# 不允许传过来得数据位空
if len(msg)==0:
break
# 接收客户端传过来得命令
# 接收执行命令得结果
msg_server=subprocess.Popen(msg.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
# 返回命令得结果,成功或者失败
true_msg=msg_server.stdout.read()
false_msg=msg_server.stderr.read()
# 反馈信息给客户端
conn.send(true_msg)
conn.send(false_msg)
except Exception as e:
break
conn.close()
(2)client 客户端
# 导入模块
import socket
import subprocess
# 创建服务对象
server=socket.socket()
# 获取IP和端口号
IP='127.0.0.1'
PORT=9882
# 创建连接桥梁
server.bind((IP,PORT))
# 指定半连接池大小
server.listen(5)
# 接收数据和发送消息
while True:
try:
# 接收数据
conn,addr=server.accept()
# 得到的数据
msg=conn.recv(1024)
# 不允许传过来得数据位空
if len(msg)==0:
break
# 接收客户端传过来得命令
# 接收执行命令得结果
msg_server=subprocess.Popen(msg.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
# 返回命令得结果,成功或者失败
true_msg=msg_server.stdout.read()
false_msg=msg_server.stderr.read()
# 反馈信息给客户端
conn.send(true_msg)
conn.send(false_msg)
except Exception as e:
break
conn.close()
(3)问题引入
- 服务端:
- 执行代码,代码为空会报错
- 执行代码,返回得数据肯操作空/报错信息
- 客户端:
- 输入得指令长度可能会超出范围
- 接收到服务端得反馈结果可能会特别多
- 如何打印超出数据范围(缓存到系统里)的数据
(4)粘包问题
- 在 TCP 协议中是流式协议,数据是源源不断的传入到客户端中,但是客户端可以接受到的信息的长度是有限的
- 当接收到指定长度的信息后,客户端进行打印输出
- 剩余的其他数据会被缓存到 内存中
- 当再次执行其他命令时
- 新的数据的反馈结果,会叠加到上一次没有完全打印完全的信息的后面,造成数据的错乱
- 当客户端想打印新的命令的数据时,打印的其实是上一次没有打印完的数据
- 对数据造成的错乱
(5)粘包问题解决思路
- 拿到数据的总大小
recv_total_size
recv_size = 0
,循环接收,每接收一次,recv_size += 接收的长度
- 直到
recv_size = recv_total_size
表示接受信息完毕,结束循环
(三)粘包存在问题
- UDP协议不存在粘包问题,TCP协议存在粘包问题
- 粘包问题出现的原因
- TCP 协议是流式协议,数据像水流一样粘在一起,没有任何边界之分
- 收数据没有接收干净,有残留,就会和下一次的结果混淆在一起
- 解决粘包问题的核心法门就是
- 每次都收干净
- 不造成数据的混淆
(1)UDP协议不存在粘包问题
(1)服务端
from socket import *
server = socket(AF_INET, SOCK_DGRAM)
server.bind(('127.0.0.1', 8080))
res_from_client = server.recvfrom(1024)
print(res_from_client)
# (b'world', ('127.0.0.1', 56852))
res_from_client_two = server.recvfrom(1024)
print(res_from_client_two)
# (b'hello', ('127.0.0.1', 61798))
res_from_client_three = server.recvfrom(5)
print(res_from_client_three)
# OSError: [WinError 10040] 一个在数据报套接字上发送的消息大于内部消息缓冲区或其他一些网络限制,或该用户用于接收数据报的缓冲区比数据报小。
# 无法接收到 数据以外的数据 报错
(2)客户端
import socket
from socket import *
# 创建client对象
client = socket(AF_INET, SOCK_DGRAM)
client.connect(('127.0.0.1', 8080))
client.send(b'world')
client.send(b'hello')
msg_from_server = client.recvfrom(1024)
print(msg_from_server)
(3)小结
- 当我们启动udp服务端后,由udp客户端向服务端发送两条数据
- 但是在udp服务端只接收到了一条数据
- 这是因为 udp 是报式协议,传送数据过程中会将数据打包直接发走,不会对数据进行拼接操作(没有Nagle算法)
(2)TCP协议存在粘包问题
(1)服务端
from socket import *
server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1', 8081))
server.listen(3)
conn, client_addr = server.accept()
msg_from_client = conn.recv(1024)
print(msg_from_client.decode('utf-8'))
# helloworld
conn.send(b'return')
conn.close()
(2)客户端
from socket import *
client = socket(AF_INET, SOCK_STREAM)
client.connect(('127.0.0.1', 8081))
client.send(b'hello')
client.send(b'world')
msg_from_server = client.recv(1024)
print(msg_from_server)
# b'return'
client.close()
(3)小结
- 从以上我们可以看到
- TCP协议传输过程中将我们的两次发送的数据拼接成了一个发送到服务端
- 通过比较我们可知,udp协议虽然不存在粘包问题,但是,udp协议的安全性有待考量
(四)TCP协议解决粘包问题基础
(1)解决思路
- 利用struct模块将传输过去的数据的总长度打包+到头部进行发送
(2)服务端
# 导入模块
import socket
import struct
import subprocess
# 获取服务端对象
server=socket.socket()
# 获取IP和端口号
IP='127.0.0.1'
PORT=9888
# 绑定给套接字-----建立连接
server.bind((IP,PORT))
# 指定半连接池大小
server.listen(5)
# 接收信息和发送信息
while True:
# 从半连接池中取出连接请求,建立双向连接,拿出连接对象
# 拿到连接对象和客户端的IP和端口号
conn,addr=server.accept()
while True:
# 检测可能会抛出的异常,并对异常做处理
try:
# 基于取出来的连接对象,进行通信接收数据
recv_msg=conn.recv(1024)
if len(recv_msg)==0:
break
# 执行客户端传过来的命令
# 接收执行命令的结果
msg_server=subprocess.Popen(recv_msg.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
# 返回的命令结果----成功或者失败
true_msg=msg_server.stdout.read()# 读取到成功的结果,二进制数据
false_msg=msg_server.stderr.read()# 读取到失败的结果,二进制数据
# 先发送头部消息(固定长度的bytes二进制数据:对数据信息的描述(包括数据的总长度))
total_size_from_server=len(true_msg)+len(false_msg)
# int 类型 ----固定长度的bytes
# 参数i 表示整型
total_size_from_server_pack=struct.pack('i',total_size_from_server)
conn.send(total_size_from_server_pack)
# 再将反馈信息发送给客户端
conn.send(true_msg)
conn.send(false_msg)
except Exception as e:
break
conn.close()
(3)客户端
# 导入模块
import socket
# 解指定数据长度
import struct
# 创建客户端对象
client=socket.socket()
# 获取IP和端口号
IP='127.0.0.1'
PORT=9888
# 绑定套接字---创建客户端连接
client.connect((IP,PORT))
while True:
msg=input("请输入想要发送的信息:").strip()
# 输入的数据不能位空
if len(msg)==0:
continue
# 发送的数据必须是二进制数据
client.send(msg.encode('utf-8'))
# 接收来自服务端返回的结果
# 解决粘包问题
# 1.先收到固定长度的头,将头部解析到数据的描述信息,拿到数据的总的大小
# 解析出接收到的总数据的长度
recv_total_size_msg=client.recv(4)
# 解包返回的数据是元组,元组第一个参数就是打包的数字
recv_total_size=struct.unpack('i',recv_total_size_msg)[0]
# 2.recv_size=0,循环接收,没接收一次,recv_size+=接收查长度
# 3.直到recv_siez=recv_total_size 表示接收信息完毕,结束循环
# 初始化数据长度
recv_size=0
while recv_size<recv_total_size:
# 接收的数据最多接收到1024字节的数据
msg_from_server=client.recv(1024)
# 本次接收到的打印的数据长度
recv_size+=len(msg_from_server)
# 对服务端接收到的信息继续解码
msg_from_server=msg_from_server.decode('gbk')
print(msg_from_server)
else:
print("命令结束!!")
client.close()
(五)TCP协议解决粘包问题进阶
-
通过json模式-----模板修改参数直接套用
-
server
# 导入模块
from socket import *
# 将数据打包成4个长度的数据
import struct
# 执行命令模块
import subprocess
# 将头部信息转换未json格式
import json
# 获取服务端对象
server=socket(AF_INET,SOCK_STREAM)
# 建立服务端连接
server.bind(('127.0.0.1',9698))
# 指定半连接池大小
server.listen(5)
# 接收消息 反馈信息
while True:
# 接收
conn,addr=server.accept()
while True:
# 检测可能会抛出的异常,并对异常进行处理
try:
# 接收信息
msg=conn.recv(1024)
# 不允许接收过来的信息为空
if len(msg)==0:
break
# 执行命令
# 接收执行命令的结果
msg_server=subprocess.Popen(msg.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
# 获取接收的结果
true_msg=msg_server.stdout.read()
false_msg=msg_server.stderr.read()
# 头部信息长度
total_size=len(true_msg)+len(false_msg)
# 自定义头部信息字典
headers_dict={
'total_size':total_size
}
# 将头部信息字典转为json字符串格式
json_data=json.dumps(headers_dict)
# 将json字符串转为二进制数据
json_data_bytes=json_data.encode('utf-8')
# 将json二进制数据打包
json_size_pack=struct.pack('i',len(json_data_bytes))
# 将打包数据发送出去
conn.send(json_size_pack)
# 将二进制数据发送出去
conn.send(json_data_bytes)
# 将执行命令得到的结果反馈给客户端
conn.send(true_msg)
conn.send(false_msg)
except Exception as e:
break
conn.close()
- client
# 导入模块
import json
import struct
from socket import *
# 导入解包模块
import subprocess
# 导入json模块
# 获取客户端对象
client=socket(AF_INET,SOCK_STREAM)
# 建立客户端连接
client.connect(('127.0.0.1',9698))
# 发送信息 接收信息
while True:
send_msg=input("请输入你想发送的信息:").strip()
# 发送信息不能为空
if len(send_msg)==0:
continue
# 发送信息
client.send(send_msg.encode('utf-8'))
#接收信息
# 接收服务端发来的打包数据
json_data_pack=client.recv(4)
# 将接收到的打包数据进行解包
json_data_bytes_size=struct.unpack('i',json_data_pack)[0]
# 接收二进制的头部json数据
json_data_bytes=client.recv(json_data_bytes_size)
# 将头部字典的二进制数据解码
json_data=json_data_bytes.decode('utf-8')
# 将解码后的json字符串进行反序列化 ----字典格式
headers_dict=json.loads(json_data)
# 取出头部数据的长度
total_size1=headers_dict['total_size']
# 定义一个数据大小
recv_size=0
# 循环打印出数据的内容
while recv_size<total_size1:
# 最多每次只能接收到1024个字节的数据
msg=client.recv(1024)
# 本次接收到的打印数据的长度
recv_size+=len(msg)
# 对接收的数据进行解码 gbk格式
print(msg.decode('gbk'))
else:
print("命令结束!!!")
client.close()
(补充)struct模块
-
struct.pack()
是Python内置模块
struct
中的一个函数
- 它的作用是将指定的数据按照指定的格式进行打包,并将打包后的结果转换成一个字节序列(byte string)
- 可以用于在网络上传输或者储存于文件中。
-
struct.pack(fmt, v1, v2, ...)
- 其中,
fmt
为格式字符串,指定了需要打包的数据的格式,后面的v1
,v2
,...则是需要打包的数据。 - 这些数据会按照
fmt
的格式被编码成二进制的字节串,并返回这个字节串。
- 其中,
-
fmt
的常用格式符如下:
x
--- 填充字节c
--- char类型,占1字节b
--- signed char类型,占1字节B
--- unsigned char类型,占1字节h
--- short类型,占2字节H
--- unsigned short类型,占2字节i
--- int类型,占4字节I
--- unsigned int类型,占4字节l
--- long类型,占4字节(32位机器上)或者8字节(64位机器上)L
--- unsigned long类型,占4字节(32位机器上)或者8字节(64位机器上)q
--- long long类型,占8字节Q
--- unsigned long long类型,占8字节f
--- float类型,占4字节d
--- double类型,占8字节s
--- char[]类型,占指定字节个数,需要用数字指定长度p
--- char[]类型,跟s
一样,但通常用来表示字符串?
--- bool类型,占1字节
-
具体的格式化规则可以在Python文档中查看(链接)。
import json
import struct
# 为了避免粘包,必须子定制报头
# 1T数据 文件路径 和 md5值
header = {
'file_siez':111111111,
'file_name':'/a/b/c/d/e/a.txt',
'md5':'8f6fbf8347faa4924a76856701edb0f3'
}
# 为了将报头传输出去,需要将子定制的报头字典序列化未json字符串
header_str=json.dumps(header)
# 传输数据必须时二进制
header_str_bytes=header_str.encode('utf-8')
# 为了人客户端知道报头的长度,用struct模块将报头长度转换为4个字节
# 这4个字节只包含了一个数字,就是报头的长度
header_str_bytes_pack=struct.pack('i',len(header_str_bytes))
header_str_bytes_size=struct.unpack('i',header_str_bytes_pack)
print(f"原本的数据{header}")
# 原本的数据{'file_siez': 111111111, 'file_name': '/a/b/c/d/e/a.txt', 'md5': '8f6fbf8347faa4924a76856701edb0f3'}
print(f"json序列化数据{header_str}")
# json序列化数据{"file_siez": 111111111, "file_name": "/a/b/c/d/e/a.txt", "md5": "8f6fbf8347faa4924a76856701edb0f3"}
print(f"压缩后的数据{header_str_bytes_pack}")
# 压缩后的数据b'd\x00\x00\x00'
print(f"解包后的数据{header_str_bytes_size}")# (100,)
传输视频练习
- server
# 导入socket 模块
import json
import socket
# 导入struct模块 将数据打包成4个长度的数据
import struct
# 导入json 模块将python对象转换为json字符串
# 导入os模 获取路径
import os
# 获取服务端对象
server=socket.socket()
# 建立服务端连接
server.bind(('127.0.0.1',9819))
# 指定半连接池大小
server.listen(5)
# 接收数据 和 反馈信息
while True:
conn,addr=server.accept()
while True:
# 检查是否出现异常,并处理异常
try:
# 接收过来的视频名称
file_name=conn.recv(1024)
if not file_name:
break
file_name=file_name.decode('utf-8')
# 获取视频的路径
BASE_DIR=os.path.dirname(__file__)
file_path=os.path.join(BASE_DIR,'video',file_name)
# 读出数据
with open(file_path,'rb') as f:
data=f.read()
# 获取报头长度
header_size=len(data)
# 自定义报头字典
header_dict={
'header_size':header_size
}
# 字典转为josn字符串
json_data=json.dumps(header_dict)
# 转为二进制
json_data_bytes=json_data.encode('utf-8')
# 打包
json_data_bytes_pack=struct.pack('i',len(json_data_bytes))
# 将打包好的数据反馈给客户端
conn.send(json_data_bytes_pack)
conn.send(json_data_bytes)
conn.send(data)
except Exception as e:
break
conn.close()
- client
# 导入socket模块
import socket
# 导入struct模块 解包
import struct
# 导入json模块 将json字符串转为python对象
import json
# 导入os模块,获取保存路径
import os
# 获取客户端对象
client=socket.socket()
# 建立客户端来南京
client.connect(('127.0.0.1',9819))
# 发送消息 和 接收反馈信息
while True:
file_name=input("请输入发送的信息:").strip()
if not file_name:
continue
client.send(file_name.encode('utf-8'))
# 获取新的保存路径
BASE_DIR=os.path.dirname(__file__)
new_name=f'new_{file_name}'
new_path=os.path.join(BASE_DIR,'video',new_name)
# 接收数据
json_data_bytes_pack=client.recv(4)
# 解包
json_data_bytes_size=struct.unpack('i',json_data_bytes_pack)[0]
# 获取二进制json对象
json_data_bytes=client.recv(json_data_bytes_size)
# 解码
json_data=json_data_bytes.decode('utf-8')
# 获取python对象
header_dict=json.loads(json_data)
# 获取数据的大小
header_size=header_dict['header_size']
# 定义初始化数据长度
recv_size=0
# 定义初始化二进制变量
while recv_size<header_size:
data=client.recv(1024)
recv_size+=len(data)
with open(new_path, 'ab') as f:
f.write(data)
client.close()
标签:socket,send,server,json,client,msg,粘包
From: https://www.cnblogs.com/suyihang/p/17971082