socket套接字
day33
协议栈的上半部分有两块,分别是负责收发数据的 TCP 和 UDP 协议,它们两会接受应用层的委托执行收发数据的操作。
协议栈的下面一半是用 IP 协议控制网络包收发操作,在互联网上传数据时,数据会被切分成一块块的网络包,而将网络包发送给对方的操作就是由 IP 负责的。
此外 IP 中还包括 ICMP
协议和 ARP
协议。
ICMP
用于告知网络包传送过程中产生的错误以及各种控制信息。ARP
用于根据 IP 地址查询相应的以太网 MAC 地址。
IP 下面的网卡驱动程序负责控制网卡硬件,而最下面的网卡则负责完成实际的收发操作,也就是对网线中的信号执行发送和接收操作。
那具体 socket 有哪些接口呢?
socket 一般分为 TCP 网络编程和 UDP 网络编程。
TCP 网络编程
基于 TCP 协议的客户端和服务器工作
- 服务端和客户端初始化
socket
,得到文件描述符; - 服务端调用
bind
,将绑定在 IP 地址和端口; - 服务端调用
listen
,进行监听; - 服务端调用
accept
,等待客户端连接; - 客户端调用
connect
,向服务器端的地址和端口发起连接请求; - 服务端
accept
返回用于传输的socket
的文件描述符; - 客户端调用
write
写入数据;服务端调用read
读取数据; - 客户端断开连接时,会调用
close
,那么服务端read
读取数据的时候,就会读取到了EOF
,待处理完数据后,服务端调用close
,表示连接关闭。
这里需要注意的是,服务端调用 accept
时,连接成功了会返回一个已完成连接的 socket,后续用来传输数据。
所以,监听的 socket 和真正用来传送数据的 socket,是「两个」 socket,一个叫作监听 socket,一个叫作已完成连接 socket。
成功连接建立之后,双方开始通过 read 和 write 函数来读写数据,就像往一个文件流里面写东西一样。
服务端
import socket
server = socket.socket() # 初始化一个服务端
"""
通过查看源码得知
括号内不写参数默认就是基于网络的遵循TCP协议的套接字
"""
server.bind(('127.0.0.1', 8080)) # 绑定服务端(ip ,端口号)
"""
服务端应该具备的特征
固定的地址
...
127.0.0.1是计算机的本地回环地址 只有当前计算机本身可以访问 8080是端口号 我们写项目就输入8000以后的号码
"""
server.listen(5) # 设置最大等待数5
"""
半链接池>>>:设置最大等待数 目的是节约资源 提高效率
"""
sock, addr = server.accept() # 等待接收客户端(流式套接字)和客户端地址 如果没响应(程序阻塞)
"""
listen和accept对应TCP三次握手服务端的两个状态
流式套接字(SOCK_STREAM):该类套接字提供了面向连接的、可靠的、数据无错并且无重复的数据发送服务。而且发送的数据是按顺序接收的。
"""
print(addr) # 客户端的地址
data = sock.recv(1024) # 接收数据(流式套接字)
print(data.decode('utf8')) # 打印传输过来的解码后的二进制数据
sock.send('你好啊'.encode('utf8')) # 回传编码后的二进制数据
"""
recv和send接收和发送的都是bytes类型的数据
"""
sock.close() # 终止传输
server.close() # 终止服务端
半链接池
- 什么是半连接池:当服务器在响应了客户端的第一次请求后会进入等待状态,会等客户端发送的ack信息,这时候这个连接就称之为半连接
- 半连接池其实就是一个容器,系统会自动将半连接放入这个容器中,可以避免半连接过多 而保证资源耗光
- 产生半连接的两种情况:
- 客户端无法返回ACK信息
- 服务器来不及处理客户端的连接请求 就排队等候
客户端
import socket
client = socket.socket() # 初始化一个客户端
client.connect(('127.0.0.1', 8080)) # 链接服务端
client.send(b'hello sweet heart!!!') # 给服务端发送消息
data = client.recv(1024) # 接收服务端回复的消息
print(data.decode('utf8'))
client.close() # 关闭客户端
###########################
服务端与客户端首次交互
一边是recv那么另一边必须是send 两边不能相同 否则就'冷战'了
###########################
通信循环
1.先解决消息固定的问题
利用input获取用户输入
2.再解决通信循环的问题
将双方用于数据交互的代码循环起来
while True:
data = sock.recv(1024) # 接收数据
print(data.decode('utf8'))
msg = input('请回复消息>>>:').strip() # 输入要传输的数据
sock.send(msg.encode('utf8')) # 传输数据
while True:
msg = input('请输入你需要发送的消息>>>:').strip()
client.send(msg.encode('utf8')) # 给服务端发送数据
data = client.recv(1024) # 接收服务端回复的数据
print(data.decode('utf8')) # 打印回复的数据
from socket import SOL_SOCKET,SO_REUSEADDR
server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
客户端如果异常断开 服务端代码应该重新回到accept等待新客户端
data1 = conn.recv(1024)
print(data1)
data2 = conn.recv(1024)
print(data2)
data3 = conn.recv(1024)
print(data3)
client.send(b'hello')
client.send(b'jason')
client.send(b'kevin')
"""
三次打印的结果
b'hellojasonkevin'
b''
b''
通过运行结果发现原本想要分三行数据输出的 变成了一行输出 这就是黏包现象
"""
疫情下的封闭管理 数据黏包??粘包??
考虑到:
TCP协议的特点
会将数据量比较小并且时间间隔比较短的数据整合到一起发送
recv
会产生黏包(如果recv
接受的数据量(1024)小于发送的数据量,第一次只能接收规定的数据量1024,第二次接收剩余的数据量)
解决思路:
我们的核心问题是不知道即将要接收的数据多大
如果能够精准的知道数据量多大 那么黏包问题就自动解决了!!!
解决办法:
使用struct模块 (精准获取数据大小)
struct 模块是一个可以将任意大小的数字转换成一个固定长度编码的模块
例如 13321111 通过q 模式 转化之后是8个字节
133333 245 456 768 通过q 模式转化之后也是8个字节,不论数字大小
但是这个转化对数字的大小范围有一定的要求
i 模式转换的数字较小,转化之后的结果只有4个字节
q 模式转换的数字范围较大,转换之后的结果有8个字节
pack可以将任意长度的数字打包成固定长度
unpack可以将固定长度的数字解包成打包之前数据真实的长度
struct 只能转化数字,不能转化其他的字符串等类型
代码演示
import struct
data1 = 'hello world!'
print(len(data1)) # 12
res1 = struct.pack('i', len(data1)) # 第一个参数是格式 写i就可以了
print(len(res1)) # 4
ret1 = struct.unpack('i', res1)
print(ret1) # (12,)
data2 = 'hello baby baby baby baby baby baby baby baby'
print(len(data2)) # 45
res2 = struct.pack('i', len(data2))
print(len(res2)) # 4
ret2 = struct.unpack('i', res2)
print(ret2) # (45,)
最终解决方案:
使用字典封装数据打包成固定的长度
服务端:
1.先创建一个字典
2.制作报头存入真实数据信息(数据大小、数据描述、文件名)
3.将字典打包
4.将固定长度的字典(数据)发送给对方
5.发送真实的数据
客户端
1.先接收字典的报头
2.解析得到字典数据长度
3.接收字典
4.解析字典中真实数据信息
5.接收真实数据