socket模块、黏包现象、struct模块
一、socket模块(套接字)
socket上应用层与TCP/IP协议通信中间的软件抽象层,帮助我们编写基于网络进行数据交互的程序,否则意味着需要自己通过代码来控制OSI七层来进行数据传输。
1.socket简介
基于文件类型的套接字(单机): AF_UNIX
基于网络类型的套接字(联网): AF_INET
二、代码实现:TCP协议
1.初步实现
(1)服务端:
import socket
from socket import SOL_SOCKET,SO_REUSEADDR
"""
以后要养成查看源码编写代码的思路
"""
# 1 产生一个socket对象并指定采用的通信协议TCP
server = socket.socket() # 括号内不写参数,默认TCP协议 ,family = AF_INET基于网络的套接字, type = SOCK_STREAM流式协议即
# 2 绑定一个固定的地址(服务端必备的条件)
server.bind(('127.0.0.1', 8080)) # 127.0.0.1为本地回环地址,只有自己的电脑可以访问
# 3 设立半连接池(预防洪水攻击)
server.listen(5)
# 4 等待客人连接,如果没有客人则程序会停在这里
sock, addr = server.accept() # accept方法返回sock, addr
print(sock, addr) # sock就是双向通道,addr就是客户端地址
# 5 服务客人
data = sock.recv(1024) # 接收客户端发送过来的消息,1024字节
print(data.decode('utf8'))
sock.send('你好好哈哈哈哈'.encode('utf8')) # 给客户端发消息,消息必须是bytes类型
# 6 关闭双向通道
sock.close() # 四次挥手
# 7 关闭服务器
server.close() # 关闭服务器
(2)客户端
"只要想用网络通信就要使用socket模块"
import socket
# 1 产生一个socket对象并指定采用的通信协议TCP
client = socket.socket()
# 2 通过服务端的地址连接服务端
client.connect(('127.0.0.1', 8080)) # 参数是元组,ip地址是服务端的地址
# 3 直接给服务端发送消息
client.send('吃了没'.encode('utf8'))
# 4 接收服务端发送过来到的消息
data = client.recv(1024)
print(data.decode('utf8'))
# 5 断开与服务端的连接
client.close()
2.代码优化
(1)服务端
"代码优化"
import socket
from socket import SOL_SOCKET, SO_REUSEADDR
# 1 产生一个socket对象并指定采用的通信协议TCP
server = socket.socket() # 括号内不写参数,默认TCP协议 ,family = AF_INET基于网络的套接字, type = SOCK_STREAM流式协议即
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
server.bind(('127.0.0.1', 8080)) # 127.0.0.1为本地回环地址,只有自己的电脑可以访问
server.listen(5)
while True: # 添加循环,保证可以多客户端申请访问,在连接一个客户端的同时,其他客户端可以在半连接池中等待
sock, addr = server.accept() # accept方法返回sock, addr
while True: # 添加循环,可以循环接收客户端发送的消息
try: # 添加异常处理2 windows系统: 当当前客户端不发消息了,就重新接收别的客人的消息,防止win系统报错
data = sock.recv(1024) # 接收客户端发送过来的消息,1024字节
if len(data) == 0: # 异常处理1 macos系统:当客户端发送空消息,则重新接收下一条消息
break
print(f'来自客户端{addr}的字节>>>', data.decode('utf8'))
msg = input('消息发送:').strip()
sock.send(msg.encode('utf8')) # 给客户端发消息,消息必须是bytes类型
except BlockingIOError: # 异常处理
break
(2)客户端
client = socket.socket()
client.connect(('127.0.0.1', 8080))
while True: # 添加循环:可以多次发消息
msg = input('发送给服务端>>>:').strip()
if len(msg) == 0:
print('不能发空消息')
continue
client.send(msg.encode('utf8'))
data = client.recv(1024)
print('来自客户端的消息>>>', data.decode('utf8'))
优化内容:
1.聊天内容自定义:
采用input语句来获取消息,可以自定义消息
2.重复发送消息:
添加循环,将聊天的部分用循环包起来
3.用户发送消息不能为空:
本质其实是两边不能都是recv或者send,一定是一方收一方发
4.服务端多次重启可能报错:macos系统
Address already in use
解决方式1:改端口号
解决方式2:博客里面代码拷贝即可
5.当客户端异常断开的情况下 如何让服务端继续服务其他客人
windows服务端会直接报错
mac服务端会有一段时间反复接收空消息延迟报错
异常处理、空消息判断
三、黏包现象
1.黏包现象简介
基于tcp协议传输数据的时候,如果接收端单次接收的数据长度小于发送的总长度,则会导致部分数据无法接收全,在第二次接收的时候会继续接收上次没接收完的数据,导致数据接收变混乱,这种现象就叫做黏包
或者连续发送小数据的时候,接收端一次接收所有收据,也导致数据接收变混乱,也叫黏包现象。
2.黏包现象产生的原因
1.接收数据时无法知道发送的数据的长短
2.TCP也称为流式协议:数据就像水流一样,没有间隔(TCP会针对数据量较小且发送间隔较短的多余数据一次性合并打包发送),并且在发送的数据传输的过程中还有缓存机制来避免数据丢失。
3.避免黏包现象的关键
如何确定即将接收的数据的大小,就是我们解决黏包现象的关键
解决的办法就是,客户端在发送数据之前先发送一条数据,告诉服务端这条数据长度是多少,服务器接收到这个数据之后反馈成功,客户端再发送真实数据。这样服务端就可以通过之前接收到数据长度来接收数据。
四、struct模块
1.解决黏包问题初级:
客户端:
1.将真实数据转成bytes类型并计算长度
2.利用struct模块将真实长度制作一个固定长度的报头
3.将固定长度的报头发送给服务端,服务端只需要在recv括号内填写固定长度的报头数据即可
4.然后在发送真实数据
服务端:
1.服务端线接收固定长度的报头
2.利用struct模块反向解析出真实数据长度
3.recv接收真实数据长度即可
但是struct模块对与打包数据的长度是有上限的
2.终极方案:
客户端:
1 制作真实数据的信息字典(数据长度、简介、名称)
2 利用struct模块制作字典的报头
3 发送固定长度的报头(解析出来的是字典的长度)
4 发送字典数据
5 发送真实数据
服务端:
1 接收固定长度的字典报头
2 解析出字典的长度并接收
3 通过字典获取到真实的数据的各项信息
4 接收真实数据的长度
3.发送图片防止黏包现象
客户端:
import json
import os
import socket
import struct
client = socket.socket()
client.connect(('127.0.0.1', 9090))
# 1 获取真实文件的大小
file_size = os.path.getsize(r'/Users/duoduo/Pictures/球球.jpeg')
print(file_size) # 242227
# 2 制作真实数据的信息字典
file_data_dict = {
'file_name': '球球小狗1.jpeg',
'file_size': file_size,
'file_desc': '白色小狗开心笑脸照片',
'file_info': '笑口常开好运自然来'
}
# 3 制作报头,也就是字典的信,利用json模块序列化,并转换二进制
data_head_to_bytes = json.dumps(file_data_dict).encode('utf8')
# 利用struct模块压缩为固定长度
data_head_pack = struct.pack('i', len(data_head_to_bytes))
# 4 发送字典报头
client.send(data_head_pack) # 此时报头已经被压缩为长度为 4 bytes的二进制数据
# 5 发送字典
client.send(data_head_to_bytes)
# 6 最后发送真实数据
with open(r'/Users/duoduo/Pictures/球球.jpeg', 'rb') as f:
for line in f:
client.send(line)
服务端:
import json
import socket
import struct
server = socket.socket()
server.bind(('127.0.0.1', 9090))
server.listen(8)
sock, addr = server.accept()
# 1 接收固定长度的报头
data_dict_head = sock.recv(4)
# 2 根据报头解析出字典数据的长度,收到的是元组一定要索引
data_dict_len = struct.unpack('i', data_dict_head)[0]
# 3 接收字典数据
data_dict_bytes = sock.recv(data_dict_len)
data_dict = json.loads(data_dict_bytes)
# 4 获取真实数据的各项信息,由于真实数据可能非常大,所以最好不要在recv的参数内直接填写真实数据的大小,而是可以分次接收,每次接收1024或其倍数
total_size = data_dict.get('file_size')
recv_size = 0
with open(data_dict.get('file_name'), 'wb') as f:
while recv_size < total_size:
data = sock.recv(1024)
f.write(data)
recv_size += len(data)
五、UDP协议
客户端
import socket
client = socket.socket(type=socket.SOCK_DGRAM)
server_addr = ('127.0.0.1', 8080)
while True:
msg = input('>>>:').strip()
client.sendto(msg.encode('utf8'), server_addr)
data, addr = client.recvfrom(1024)
print(data.decode('utf8'), addr)
服务端
import socket
server = socket.socket(type=socket.SOCK_DGRAM) # 指定为UDP协议
server.bind(('127.0.0.1', 8080))
while True:
data, addr = server.recvfrom(1024)
print('客户端地址>>>:', addr)
print('上述地址发送的消息>>>:', data.decode('utf8'))
msg = input('>>>:').strip()
server.sendto(msg.encode('utf8'), addr)