TCP与UDP协议、 socket套接字编程、 通信相关操作(cs架构软件) TCP黏包问题及解决思路

OSI七层协议

传输层

1.PORT协议:前面讲过
2.TCP协议与UDP协议:规定了数据传输所遵循的规则(数据传输能够遵循的协议有很多,TCP和UDP是较为常见的两个)

TCP协议

基于TCP传输数据是非常安全的,是因为数据不容易丢失,并非是因为有双向通道。不容易丢失的原因在于二次确认机制,每次发送数据都需要返回确认消息,否则在一定时间会反复发送

三次握手--建立双向通道

cs架构的是否需要中间件_cs架构的是否需要中间件

过程
1.客户端向服务端发送请求,询问是否可以建立一个数据通道
2.服务端就向客户端发送确认,允许客户端建立一个通向服务端的数据通道
ps:此时数据通道是单向的,只允许客户端向服务端发送信息
3.服务端向客户端发送请求,询问是否可以建立一个数据通道
4.客户端就向服务端发送确认,允许服务端建立一个通向服务端的数据通道
ps:此时数据通道是双向的,允许客户端、服务端互相发送信息
    
 上述有四步,可以将2、3不合并成一步,因为服务端向客户端发送确认的同时,也可以发送请求。将三步称之为三次握手
四次挥手--断开双向通道

cs架构的是否需要中间件_cs架构的是否需要中间件_02

过程
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知识点总结

cs架构的是否需要中间件_cs架构的是否需要中间件_03

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