socket套接字编程、半连接层、粘包
一、传输层
1.TCP与UDP协议
规定了数据传输所遵循的规则
数据传输能够遵循的协议有很多,TCP和UDP是常见的俩个
2.TCP协议
TCP协议
'''
基于TCP传输数据非常的安全,因为有双向通道
基于TCP传输数据,数据不容易丢失,不容易丢失的原因在于二次确认
每次发送数据都需要返回确认消息,否则在一定的时间会反复发送
'''
3.TCP协议--三次握手
P传输数据,数据不容易丢失,不容易丢失的原因在于二次确认
'''三次握手:'''
所谓三次握手,是指建立一个TCP连接时,需要客户端和服务器总共发送3个包
是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号并交换 TCP 窗口大小信息.在socket编程中,客户端执行connect()时。将触发三次握手。
建立双向通道,数据基于这个通道来回发送
'''洪水攻击:'''
同时让大量的客户端朝服务端发送建立TCP连接的请求
'''三次握手过程'''
(1)客户端向服务器端发送连接请求包SYN(syn=j),等待服务器回应;
(2)服务器端收到客户端连接请求包SYN(syn=j)后,将客户端的请求包SYN(syn=j)放入到自己的未连接队列,此时服务器需要发送两个包给客户端;
(3)客户端收到服务器的ACK(ack=j+1)和SYN(syn=k)包后,知道了服务器同意建立连接,此时需要发送连接已建立的消息给服务器;
向服务器发送连接建立的确认包ACK(ack=k+1),回应服务器的SYN(syn=k)告诉服务器,我们之间已经建立了连接,可以进行数据通信。
ACK(ack=k+1)包发送完毕,服务器收到后,此时服务器与客户端进入ESTABLISHED状态,开始进行数据传送。
4.TCP协议--四次挥手
'''四次握手特点:'''
断开双向通道,中间的俩步是不能合并的需要有检查的时间
'''四次握手过程 '''
(1)当服务端或者客户端不想再与对方进行通信之后,双方任意一方都可以主动发起断开链接的请求,我们还
是以客户端主动发起为例
(2)客户端由于已经没有任何需要发送给服务端的消息了,所以发起断开客户端到服务端的通道请求
(3)服务端收到该请求后同意了 至此客户端到服务端的单项通道断开
(4)服务端这个时候不会立刻朝客户端发器请求说那我也断开到你家的通道吧,同时它也不能拒绝人家断开的请求,服务端会去查看自己还有没有需要给客户端发送的数据,如果还有的话,就不会立马断开,先把数据发完才能断
(5)等服务端检查完毕之后也没有数据要发送给客户端了,这个时候就会朝客户端发起断开服务端到客户端的
通道请求
(6)客户端确认该请求,至此四次挥手完成
(7)挥手必须是四次,'中间的两次不能合并成一次,原因就在于需要检查是否还有数据需要给对方发送'
5.为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?
这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的连接请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送;
但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;
但未必你所有的数据都全部发送给对方了,所以你可能未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。
5.UDP协议
UDP协议
'''
TCP类似于打电话:你一句我一句
UDP类似于发短信:反正我发了,你看不看回不回复没关系
'''
UDP不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地。
UDP在传输数据报前不用在客户和服务器之间建立一个连接,没有任何的通道也没有任何的限制
UDP发送数据没有TCP安全,很随意
例如:QQ就是采用的就是UDP
6.TCP协议 VS UDP协议
TCP和UDP的主要区别:
TCP传输数据稳定可靠,适用于对网络通讯质量要求较高的场景,需要准确无误的传输给对方,
比如,传输文件,发送邮件,浏览网页等等
UDP的优点是速度快,但是可能产生丢包,所以适用于对实时性要求较高但是对少量丢包并没有太大要求的场景。
二、应用层
主要取决于程序员自己采用什么策略和协议
常见协议:HTTP、HTTPS、FTP.....
三、socket套接字编程
1.基于文件类型的套接字家族
套接字家族的名字:AF_UNIX
unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
2.基于网络类型的套接字家族
套接字家族的名字:AF_INET
AF_INET6被用于ipv6,还有一些其他的地址家族,在网络编程中,大部分时候我们只使用AF_INET
1.简易版本的俩个程序之间的交互
'''服务端'''
import socket
# 1.创建一个socket对象
server = socket.socket() # 括号内什么都不写 默认就是基于网络的TCP套接字
# 2.绑定一个固定的地址(ip\port)
server.bind(('127.0.0.1', 8080))
# 3.半连接层
server.listen(5)
# 4.开业,等待接客
sock, address = server.accept()
print(sock, address)
# 5.数据交互
sock.send(b'welcome to shanxi~')
data = sock.recv(1024) # 接收客户端发送的数据 1024bytes
print(data)
# 6.断开连接
sock.close() # 断连接
server.close() # 关机
'''客户端'''
import socket
# 1.创建一个socket对象
client = socket.socket() # 括号内什么都不写 默认就是基于网络的TCP套接字
# 2.连接服务端
client.connect(('127.0.0.1', 8080))
# 3.数据交互
data = client.recv(1024)
print(data)
client.send(b'so beautiful!!!')
# 6.断开连接
client.close() # 断连接
client.close() # 关机
2.优化版本的俩个程序之间的交互
1.send与recv
客户端与服务端不能同时执行同一个
有一个收 另外一个就是发
有一个发 另外一个就是收
不能同时收或者发!!!
2.消息自定义
input获取用户数据即可(主要编码解码)
3.循环通信
给数据交互环节添加循环即可
4.服务端能够持续提供服务
不会因为客户端断开连接而报错
异常捕获 一旦客户端断开连接 服务端结束通信循环 调到连接处等待
5.消息不能为空
判断是否为空 如果是则重新输入(主要针对客户端)
6.服务端频繁重启可能会报端口被占用的错(主要针对mac电脑)
from socket import SOL_SOCKET,SO_REUSEADDR
server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
7.客户端异常退出会发送空消息(针对mac linux)
针对接收的消息加判断处理即可
import socket
# 1.创建一个socket对象
server = socket.socket() # 括号内什么都不写 默认就是基于网络的TCP套接字
# 2.绑定一个固定的地址(ip\port)
server.bind(('127.0.0.1', 8080))
# 3.半连接层
server.listen(5)
# 4.开业,等待接客
sock, address = server.accept()
print(sock, address)
# 5.数据交互
while True:
try:
msg = input('请输入要发送给客户端的信息>>>:').strip()
sock.send(msg.encode('utf8'))
data = sock.recv(1024) # 接收客户端发送的数据 1024bytes
print(data.decode('utf8'))
except ConnectionError:
sock.close()
break
import socket
# 1.创建一个socket对象
client = socket.socket() # 括号内什么都不写 默认就是基于网络的TCP套接字
# 2.连接服务端
client.connect(('127.0.0.1', 8080))
# 3.数据交互
while True:
data = client.recv(1024)
print(data.decode('utf8'))
msg = input('请输入要发送给服务端的信息>>>:').strip()
if len(msg) == 0: continue
client.send(msg.encode('utf8'))
四、半连接层
当服务器在响应了客户端的第一次请求后会进入等待状态,会等客户端发送的ack信息,这时候这个连接就称之为半连接
半连接池其实就是一个容器,系统会自动将半连接放入这个容器中,可以避免半连接过多而保证资源耗光
产生半连接池的俩种情况:
客户端无法返回ACK信息
服务器来不及处理客户端的连接请求
设置的最大等待人数 >>>: 节省资源 提高效率
server.listen(5) 指定5个等待席位
主要是为了做缓冲 避免太多无效等待
五、粘包现象
1.粘包现象产生的本质
sock.send(b'jason')
data = client.recv(1024)
print(data)
对方只给了5个字符,但是后面我们返回是要返回1024个
粘包现象的产生是因为
1.TCP的特性产生的
流水协议:所有的数据类似于流水,连接在一起
触发条件是数据量很小 并且时间间隔很多,那么就会自动组织到一起
2.recv
我们不知道即将要接收的数据量多大 如果知道的话不会产生也不会产生黏包
粘包是接收长度没对上导致的
控制recv接收的字节数与之对应(你发多少字节我收多少字节)
2.粘包问题解决方案
struct模块
struct模块无论数据长度是多少 都可以帮你打包成固定长度
然后基于该固定长度 还可以反向解析出真实长度
这里利用struct模块里的
struct.pack() 方法来实现打包(将真实数据长度变为固定长度的数字)
struct.unpack() 方法解包(将该数字解压出打包前真实数据的长度)
pack unpack模式参数对照表(standard size 转换后的长度)
import struct
info = '下午上课 以后可能是常态!'
print(len(info)) # 13 数据原本的长度
res = struct.pack('i', len(info)) # 将数据原本的长度打包
print(len(res)) # 4 打包之后的长度是4
ret = struct.unpack('i', res) # 将打包之后固定长度为4的数据拆包
print(ret[0]) # 13 又得到了原本数据的长度
info1 = '打起精神啊 下午也需要奋斗 也需要认真听 客服困难 你困我也困!!!'
print(len(info1)) # 34
res = struct.pack('i', len(info1)) # 将数据原本的长度打包
print(len(res)) # 4 打包之后的长度是4
ret = struct.unpack('i', res)
print(ret[0]) # 34
struct模块针对数据量特别大的数字没有办法打包
粘包问题解决思路
思路
1.先将真实数据的长度制作成固定长度 4
2.先发送固定长度的报头
3.再发送真实数据
1.先接收固定长度的报头 4
2.再根据报头解压出真实长度
3.根据真实长度接收即可
终极方案
服务器端
1.先制作一个发送给客户端的字典
2.制作字典的报头
3.发送字典的报头
4.发送字典
5.再发真实数据
客户端
1.先接收字典的报头
2.解析拿到字典的数据长度
3.接收字典
4.从字典中获取真实数据的长度
5.循环获取真实数据
作业
--------------------------server---------------------------
import json
import socket
import os
import struct
# 创建一个socket对象
server = socket.socket()
# 绑定一个固定的地址 本地回环地址
server.bind(('127.0.0.1', 8080))
# 半连接池
server.listen(5)
while True:
sock, address = server.accept()
while True:
try:
video_address = r'C:\Users\zhangran\Desktop\blackpink.mp4'
# 1.构造数据文件的字典
file_dict = {
'file_name': 'blackpink.txt',
'file_size': os.path.getsize(video_address),
'file_desc': '内容很精彩 一定不要错过',
}
json_dict = json.dumps(file_dict)
b_dict = bytes(json_dict, 'utf8')
dict_len = len(b_dict)
res_w = struct.pack('i', dict_len)
sock.send(res_w)
# 4.发送真实字典数据
sock.send(b_dict)
# 5.发送真实数据
with open(r'%s' % video_address, 'rb') as f:
sock.send(f.read())
except Exception:
break
--------------------------client---------------------------
import json
import socket
# 1.创建一个socket对象
import struct
client = socket.socket() # 括号内什么都不写 默认就是基于网络的TCP套接字
# 2.连接服务端
client.connect(('127.0.0.1', 8080))
# 1.先接受长度为4的报头数据
header_len = client.recv(4)
# 2.根据报头解包出字典的长度
dict_len = struct.unpack('i', header_len)[0]
# 3.直接接收字典数据
dict_data = client.recv(dict_len)
# 4.解码并反序列化出字典
real_dict = json.loads(dict_data)
# 5.从数据字典中获取真实数据的各项信息
total_size = real_dict.get('file_size')
blackpink = client.recv(total_size)
with open('blackpink.mp4', 'wb') as f:
f.write(blackpink)