TCP与UDP协议、 socket套接字编程、 通信相关操作(cs架构软件) TCP黏包问题及解决思路
OSI七层协议
传输层
1.PORT协议:前面讲过
2.TCP协议与UDP协议:规定了数据传输所遵循的规则(数据传输能够遵循的协议有很多,TCP和UDP是较为常见的两个)
TCP协议
基于TCP传输数据是非常安全的,是因为数据不容易丢失,并非是因为有双向通道。不容易丢失的原因在于二次确认机制,每次发送数据都需要返回确认消息,否则在一定时间会反复发送
三次握手--建立双向通道
过程
1.客户端向服务端发送请求,询问是否可以建立一个数据通道
2.服务端就向客户端发送确认,允许客户端建立一个通向服务端的数据通道
ps:此时数据通道是单向的,只允许客户端向服务端发送信息
3.服务端向客户端发送请求,询问是否可以建立一个数据通道
4.客户端就向服务端发送确认,允许服务端建立一个通向服务端的数据通道
ps:此时数据通道是双向的,允许客户端、服务端互相发送信息
上述有四步,可以将2、3不合并成一步,因为服务端向客户端发送确认的同时,也可以发送请求。将三步称之为三次握手
四次挥手--断开双向通道
过程
1.客户端向服务端发送请求,询问是否可以关闭客户端到服务端的数据通道
2.服务端就向客户端发送确认,允许关闭客户端到服务端的数据通道
ps:此时数据通道由双向变为单向,只有服务端到客户端的通道了
ps:这是服务端会检查还有没有数据没有发送完毕,没有发送完毕就继续发送,发送完毕就发送请求关闭通道
3.服务端向客户端发送请求,询问是否可以关闭服务端到客户端的数据通道
4.客户端就向服务端发送确认,允许关闭服务端到客户端的数据通道
ps:此时数据通道就完全关闭了
上述四个步骤称之为四次挥手,关闭双向通道,2、3步不能合并,因为需要有检查时间,看看数据是否发送完毕没有
UDP协议
基于UDP协议发送数据,没有任何的通道也没有任何的限制,UDP发送数据没有TCP安全,因为没有二次确认机制
总结
TCP类似于打电话,有来有往;UDP类似于发送短信,只要发送了,剩下的什么都不管
应用层
主要取决于程序员自己采用什么策略和协议,常见的协议有HTTP、HTTPS、FTP...
socket套接字
基于文件类型的套接字家族的名字是:AF_UNIX;基于网络类型的套接字家族的名字是:AF_INET
代码--基础
运行程序的时候,先确保服务端运行,之后才是客户端
服务端
1.创建一个socket对象
import socket
server = socket.socket()
ps:socket是一个类,socket()此时就相当于实例化对象 def __init__(self, family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None):
2.绑定一个固定的地址(ip\port)
server.bind(('127.0.0.1',8080))
ps:1.bind() >>> def bind(self, address: Union[_Address, bytes]) -> None: ...>>>里面可以填元组,字符串,一般填写元组,字符串会出错在后续
2. 127.0.0.1 ip地址,这里是本地回环地址,只允许自己的机器访问
8080 端口号
3.半连接池
主要是为了做缓冲,避免太多的无效等待
server.listen(5)
4.开业 等待接客
sock,address = server.accept()
print(sock,address)
ps: 1. 类里面的函数accept()返回值是 return sock, addr
2.sock是双向通道,address是客户端地址
5.数据交互
sock.send(b"hello client ,i'm server")
data = sock.recv(1024)
print(data)
ps: 1. sock.send()是操客户端发送数据
2. sock.recv()是接收客户端发送的数据 1024代表接收1024bytes
6.断开连接
sock.close() # 断连接
server.close() # 关机
客户端
1.产生一个socket对象
import socket
client = socket.socket()
2.连接服务器(拼接服务端的ip和port)
client.connect(('127.0.0.1',8080))
3.数据交互
data = client.recv(1024)
print(data)
client.send(b"hello server ,i'm client")
ps: client.recv(1024) 是接收服务端发送的数据 1024代表接收1024bytes
client.send()朝服务端发送数据
4.关闭
client.close()
代码--问题及优化
1.客户端与服务端不能同时执行同一个send或recv
意思就是如果客户端先接收(recv()),那么服务端就先发(send());如果客户端先发(send()),那么服务端就先收(recv())
2.消息自定义
用input获取用户数据(编码解码即可)
服务端
msg= input("请输入你想给客户端发送的内容>>>:").strip()
sock.send(msg.encode('utf8'))
data = sock.recv(1024)
print(data.decode('utf8'))
客户端
data = client.recv(1024)
print(data.decode('utf8'))
msg= input("请输入你想给服务端发送的内容>>>:").strip()
client.send(msg.encode('utf8'))
3.循环通信
给数据交互环节添加循环即可
服务端
while True:
msg= input("请输入你想给客户端发送的内容>>>:").strip()
sock.send(msg.encode('utf8'))
data = sock.recv(1024)
print(data.decode('utf8'))
客户端
while True:
data = client.recv(1024)
print(data.decode('utf8'))
msg= input("请输入你想给服务端发送的内容>>>:").strip()
client.send(msg.encode('utf8'))
4.服务端能够持续提供服务
也就是不会因为客户端断开连接而报错;我们就可以使用异常捕获:一旦客户断开连接,服务端就结束通信循环,跳到连接等待接客处
服务端
while True:
sock,address = server.accept()
print(sock,address)
# 数据交互
while True:
try:
msg= input("请输入你想给客户端发送的内容>>>:").strip()
sock.send(msg.encode('utf8'))
data = sock.recv(1024)
print(data.decode('utf8'))
except ConnectionResetError :
sock.close()
break
5.消息不能为空
解决方式是判断待发送的消息是否为空,如果是则重新输入(主要针对客户端)
客户端
while True:
data = client.recv(1024)
print(data.decode('utf8'))
msg= input("请输入你想给服务端发送的内容>>>:").strip()
if len(msg) == 0:
msg = '手抖了一下'
client.send(msg.encode('utf8'))
6.服务端频繁重启可能会报端口号被占用的错误(主要针对mac电脑)
在服务端写一下代码
from socket import SOL_SOCKET,SO_REUSEADDR
server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind()前加
7.客户端异常退出就会发送空消息(针对mac linux)
针对接收的消息加判断处理即可
黏包问题
服务端接收数据,客户端发送数据,黏包问题更明显
服务端
import socket
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
sock, address = server.accept()
print(sock.recv(1024))
print(sock.recv(1024))
print(sock.recv(1024))
客户端
import socket
client = socket.socket()
client.connect(('127.0.0.1', 8080))
client.send(b'jason')
client.send(b'kevin')
client.send(b'jerry')
产生原因
1.TCP特性
流式协议:内容与内容之间没有明确的分界标志,所有的数据类似于水流连接在一起的
ps:数据量小 并且时间间隔很多,那么就会自动组织到一起
2.recv() 括号里规定了读取的字节数。由于我们不知道即将要接收的数据量多大,就容易产生黏包问题
struct模块
struct模块无论数据长度多少,都可以帮你打包成固定的长度;然后基于固定的长度,还可以反向解析出真实的长度
import struct
info1 = '你好 world'
print(len(info1)) # 8
res1 = struct.pack('i',len(info1))
print(res1) #b'\x08\x00\x00\x00'
print(len(res1)) # 4
res2 = struct.unpack('i',res1)
print(res2) # (8,)
print(res2[0]) # 8
import struct
info1 = '你好啊 你是我的全世界'
print(len(info1)) # 11
res1 = struct.pack('i',len(info1))
print(res1) #b'\x0b\x00\x00\x00'
print(len(res1)) # 4
res2 = struct.unpack('i',res1)
print(res2) # (11,)
print(res2[0]) # 11
struct模块很对数据量特别大的数字没有办法打包
file_size = 11203495674265677
res = struct.pack('i',file_size)
print(len(res)) # struct.error: argument out of range
可以以字典的形式,解决数据量大的问题
file_dic ={
'size':1223456575567567685,
'name':'学习视频.mp4'
}
print(len(file_dic)) # 2
res = struct.pack('i',len(file_dic))
print(len(res)) # 4
res1 = struct.unpack('i',res)
print(res1[0]) # 2
struct知识点总结
1.struct.pack('i',len(info1)) 是将数据原来的长度打包,打印返回值是二进制,用len()方法打印出来是4, i是模式
2.struct.unpack('i',res1) 将打包之后固定长度为4的数据拆包,打印返回值是一个元组,用索引取值取值出来的就是原来数据的长度
思路
1.学习了struct模块,对解决黏包问题有了一定的思路
1.先将真实数据的长度制作成固定的长度>>>struct.pack()
2.先发送固定长度的报头 >>>sock.send()
3.再发送真实的数据
1.先接收固定长度的报头 >>> sock.recv(4)
2.再根据报头解压出真实的长度 >>>struct.unpack()
3.根据真实长度接收即可
2.问题:struct模块很对数据量特别大的数字没有办法打包
file_size = 11203495674265677
res = struct.pack('i',file_size)
print(len(res)) # struct.error: argument out of range
3.解决:可以以字典的形式,解决数据量大的问题
file_dic ={
'size':1223456575567567685,
'name':'学习视频.mp4'
}
print(len(file_dic)) # 2
res = struct.pack('i',len(file_dic))
print(len(res)) # 4
res1 = struct.unpack('i',res)
print(res1[0]) # 2
4.终极解决黏包方法
服务端
1.先构造一个数据的详细字典
2.对字典数据进行打包处理,得到一个固定长度的数据>>>struct.pack() 4
3.将上述打包之后的数据发送给客户端 >>>sock.send()
4.将字典数据发送给客户端
5.将真实数据发送给客户端
客户端
1.先接收固定长度的数据 >>> sock.recv(4)
2.根据固定长度解析出即将要接收的字典真实长度 >>>struct.unpack()
3.接收字典数据
4.根据字典数据,获取出真实数据的长度
5.接收真实数据长度
解决
服务端
import json
import os
import socket
import struct
server = socket.socket()
server.bind(('127.0.0.1',8080))
server.listen(5)
while True:
sock,address = server.accept()
while True:
# 1.先构造数据文件的字典
file_dict = {
'name':'luo编程视频合集.txt',
'size':os.path.getsize(r'../xxx视频合集.txt'),
}
# 将字典打包成固定长度的数据
dict_json = json.dumps(file_dict) # 将字典转换成json字符串
file_bytes_dict = dict_json.encode('utf8') # 将json字符串格式转换成二进制
dict_len_bytes= struct.pack('i',len(file_bytes_dict))
# 发送固定长度的字典报头
sock.send(dict_len_bytes)
# 发送真实字典数据
sock.send(dict_json.encode('utf8'))
# 发送真实数据
with open(r'../xxx视频合集.txt','rb') as f:
for line in f:
sock.send(line)
break
客户端
import json
import socket
import struct
client = socket.socket()
client.connect(('127.0.0.1',8080))
while True:
# 先接收长度为4的报头数据
header_len = client.recv(4) # 接收的是dict_len_bytes
# 根据报头解包出字典的长度
dict_len = struct.unpack('i',header_len)[0] # 68
# 直接接收字典数据
dict_data = client.recv(dict_len) # b'{"name": "luo\\u7f16\\u7a0b\\u89c6\\u9891\\u5408\\u96c6.txt", "size": 511}''
# 解码并反序列化出字典
real_dict = json.loads(dict_data) # {'name': 'luo编程视频合集.txt', 'size': 511}
# 从数据字典中获取真实数据的各项信息
total_size = real_dict.get('size')
file_size = 0
with open(r'%s' % real_dict.get('name'),'wb') as f:
while file_size < total_size:
data = client.recv(1024)
f.write(data)
file_size += len(data)
print('文件接收完毕')
break