三次握手和四次挥手、UDP、TCP、粘包问题、模块回顾

【一】三次握手和四次挥手

【1】TCP协议的三次握手和四次挥手

  • TCP协议位于 osi 七层协议中的传输层

(1)使用三次握手来建立连接

  • 一次握手:
    • 客户端发送带有 SYN (SEQ=x)标志的数据包 ---》服务端,然后客户端进入 SYN_SEND 状态,等待服务器的确认。
  • 二次握手:
    • 服务端发送带有 SYN + ACK(SEQ=y,ACK=x+1)标志的数据包---》客户端,然后服务端进入 SYN_RECV 状态
  • 三次握手:
    • 客户端发送带有 ACK(ACK=y+1)标志的数据包---》服务端,然后客户端和服务端都进入 ESTABLISHED 状态,完成 TCP 三次握手。

(2)为什么要三次握手

  • 三次握手的目的是建立可靠的通信信道,简单来说就是数据的发送与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。
  • SYN:SYN=1 表示要建立连接
  • ACK:ACK=1 表示我收到了,允许
  • seq:随机数,建立连接无论客户端还是服务端要建立连接就要携带
  • ack:回应请求就要加1返回
  • FIN:表示断开连接
# 【1】第一次请求
# 由客户端发起请求 带SYN=1
# 表示我自己是客户端我要建立连接
# seq随机数带
# 发送给服务端
# 客户端 ---> 携带 SYN和SEQ ---> 发送给服务端
# 【2】第二次请求
# 服务端接收到客户端的请求
# ACK=1 表示收到了当前客户端发送给我的请求
# SYN=1 表示要建立连接
# seq:随机数
# 服务端 ---> 接收到客户端的请求,同意建立连接 ---> 发送给客户端
# 【3】第三次请求
# 客户端接收到了服务端的请求
# ACK=1 表示收到了当前服务端发送给我的请求
# SYN=1 表示要建立连接
# seq:随机数
# 和服务端建立连接成功

(3)使用四次挥手断开连接

  • 断开一个 TCP 连接则需要四次挥手,缺一不可:

  • 第一次挥手:

    • 客户端发送一个 FIN(SEQ=x)标志的数据包---》服务端,用来关闭客户端到服务器的数据传送。然后客户端进入 FIN-WAIT-1 状态。
  • 第二次挥手:

    • 服务器收到这个 FIN(SEQ=x)标志的数据包,它发送一个 ACK(ACK=x+1)标志的数据包---》客户端。然后服务端进入 CLOSE-WAIT 状态,客户端进入 FIN-WAIT-2 状态。
  • 第三次挥手:

    • 服务端发送一个 FIN(SEQ=y)标志的数据包---》客户端,请求关闭连接,然后服务端进入 LAST-ACK 状态。
  • 第四次挥手:

    • 客户端发送 ACK (ACK=y+1)标志的数据包->服务端,然后客户端进入TIME-WAIT 状态,服务端在收到 ACK (ACK=y+1)标志的数据包后进入 CLOSE 状态。此时如果客户端等待 2MSL 后依然没有收到回复,就证明服务端已正常关闭,随后客户端也可以关闭连接了。
# 【1】第一次
# 客户端向服务端发送请求,我想要断开连接
# 【2】第二次
# 服务端接收到客户端的请求
# 表示同意断开连接
# 【3】第三次
# 服务单向客户端发送请求,请求的原因是当前还有数据没有传输完成
# 请求等待,等待数据传输完成
# 发起请求,断开连接
# 服务端向客户端发送请求,请求断开连接
# 【4】第四次
# 客户端接收到服务端的请求
# 直接断开连接

【二】UDP协议模型

settings.py

# Author : chosen-yn
IP = '127.0.0.1'
# localhost 和 127.0.0.1区别
# DNS解析 --> 一串字符解析成IP
# localhost 就是一个域名 只能在本地使用
PORT = 8002
ADDR = (IP, PORT)

# 【一】解决办法一:通用 : 换 端口号
# 直接将启动的IP和端口号中的端口号更改
# 【二】解决办法二:加一个配置
# address already in use
# 【1】加配置
# SO_REUSEADDR : re 重新 use 使用 addr 地址
# server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 绑定IP和PORT
# 【2】绑定端口
# server.bind(settings.ADDR)
# 【三】解决办法三:# OSError: [WinError 10048] 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。
# 【1】Windows
# 查看当前端口(cmd)对应的进程,杀死进程
# netstat -ano|findstr 8002
# 杀死当前进程PID对应的进程
# taskkill /pid PID号 /F

# 【2】MacOS/ linux
# ps aux|grep 端口号
# kill 进程号 -9
import hashlib


def encrypt_data(data, salt):
    data = str(data) + str(salt)
    data = data.encode()
    md5 = hashlib.md5()
    md5.update(data)
    return md5.hexdigest()


import struct

# 定义一个包含不同类型字段的格式字符串
format_string = 'i'


# 对数据进行打包
def pack_data(data_length: int):
    # 使用 struct.pack 将数据打包成二进制字节串
    packed_data = struct.pack(format_string, data_length)
    return packed_data


# 对数据解包
def unpack_data(data):
    data_length = struct.unpack(format_string, data)
    return data_length[0]
from conf import settings
# 【一】引入socket模块
import socket

# 【二】创建一个client对象
# AF_INET:当前连接是基于网络的套接字
# SOCK_DGRAM:连接模式是UDP协议的报式模式
client = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
# 【三】直接发送数据
to_server_send_data = f'这是来自客户端的一条消息!'
to_server_send_data = to_server_send_data.encode()
client.sendto(to_server_send_data, settings.ADDR)
print(f'client :>>>> {client}')
# client :>>>> <socket.socket fd=428, family=AddressFamily.AF_INET, type=SocketKind.SOCK_DGRAM, proto=0, laddr=('0.0.0.0', 63316)>
# 【四】接收到服务端回的消息
from_server_recv_data,addr = client.recvfrom(1024)
from_server_recv_data = from_server_recv_data.decode()
print(f'from_server_recv_data :>>>> {from_server_recv_data}')
# from_server_recv_data :>>>> 这是来自服务端的一条消息!
# 【五】关闭连接对象
client.close()
from conf import settings
# 【一】引入socket模块
import socket

# 【二】创建一个server对象
# AF_INET:当前连接是基于网络的套接字
# SOCK_DGRAM:连接模式是UDP协议的报式模式
# 只会发送一次数据
server = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
# 【三】绑定IP和PORT
server.bind(settings.ADDR)
print(f'server :>>>>{server}')
# server :>>>><socket.socket fd=332, family=AddressFamily.AF_INET, type=SocketKind.SOCK_DGRAM, proto=0, laddr=('127.0.0.1', 8002)>
# 【四】接收到客户端的数据
from_client_recv_data, addr = server.recvfrom(1024)
from_client_recv_data = from_client_recv_data.decode()
print(f'from_client_recv_data :>>>> {from_client_recv_data}')
# from_client_recv_data :>>>> 这是来自客户端的一条消息!
print(f'addr :>>>>{addr}')
# addr :>>>>('127.0.0.1', 58450)
# 【五】返回给客户端数据
to_client_send_data = '这是来自服务端的一条消息!'
to_client_send_data = to_client_send_data.encode()
server.sendto(to_client_send_data, addr)
# 【六】关闭连接和服务
server.close()

【三】TCP协议模型

from conf import settings
# 【一】引入socket模块
import socket

# 【二】创建一个client对象
# AF_INET:当前连接是基于网络的套接字
# SOCK_STREAM:连接模式是TCP协议的流式模式
client = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
# 【三】绑定IP和PORT
client.connect(settings.ADDR)
while True:
    # 【四】直接发送数据
    to_server_send_data = input("请输入发送给服务端的数据:>>>>>").strip()
    if not to_server_send_data:
        print(f'不允许发送空的数据')
        continue
    if to_server_send_data == 'q':
        print(f'当前连接退出!')
        break
    to_server_send_data = to_server_send_data.encode()
    client.send(to_server_send_data)
    # 【五】接收到服务端回的消息
    from_server_recv_data = client.recv(1024)
    from_server_recv_data = from_server_recv_data.decode()
    if from_server_recv_data == 'q':
        print(f'退出连接!')
        break
    print(f'这是来自服务端的数据:>>>>>{from_server_recv_data}')
# 【六】关闭连接对象
client.close()
from conf import settings
# 【一】引入socket模块
import socket

# 【二】创建一个server对象
# AF_INET:当前连接是基于网络的套接字
# SOCK_STREAM:连接模式是TCP协议的流式模式
server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
# 【三】绑定IP和PORT
server.bind(settings.ADDR)
# 【四】监听连接对象
server.listen(5)
# 【五】建立连接对象
# 放在这里 进入的 while 循环中的哪个连接对象会一致不变
conn,addr = server.accept()
while True:
    try:
        # 这里面交流的对象永远是上面接收到的那一个
        # conn, addr = server.accept() : 接收到新的对象,忘记上一个连接过的对象
        # 【六】接收到客户端的数据
        from_client_recv_data = conn.recv(1024)
        from_client_recv_data = from_client_recv_data.decode()
        if not from_client_recv_data:
            break
        print(f'这是来自客户端的数据:>>>>{from_client_recv_data}')
        # 【七】返回给客户端数据
        while True:
            to_client_send_data = input("请输入发送给客户端的数据:>>>>>").strip()
            if not to_client_send_data:
                print(f'不允许发送空的数据')
            if to_client_send_data == 'q':
                print(f'退出系统')
            to_client_send_data = to_client_send_data.encode()
            conn.send(to_client_send_data)
            break
    except Exception as e:
        break
# 【八】关闭连接和服务
conn.close()
server.close()

【四】粘包问题

【1】粘包问题介绍

(1)粘包问题的背景

  • 只有 TCP 有粘包现象,UDP 永远不会粘包
  • TCP协议 ---》流式协议---》不断地传输数据
    • 例如有一盒颜料:先放红色的颜料,放了一盒,但是有一勺的量没有放进去
    • 没有放进去的红色颜料会和下一盒的绿色颜料混在一起
  • UDP协议---》报式协议---》一次性传输数据
    • 例如有一个大缸:先放红色的颜料放了一缸但是还有一勺没有放进去
    • 直接将这勺颜料扔了

(2)粘包问题

  • 客户端发送的数据远远超出服务端的接受范围,就导致了不同数据之间的数据混乱问题

(3)实例

  • 执行本地的 ipconfig 命令会获取到当前执行的结果
  • 将结果传递给服务端
  • 服务端接收到数据,只能接收一部分,另外一部分发现接收不到
  • 只能和第二次的数据合并到一起发送

(4)解决办法

  • 解决思路:
    • 问题产生在服务端
    • 原因是客户端向服务端发送数据,但是服务端不知道总的数据大小,只能按照默认的数据大小接收
  • 所以:
    • 客户端在发送数据的时候将数据的总大小一起发送给服务单
    • 服务单接收到总的大小的数据长度,根据自己的容量大小分批次接收

【2】问题演示

from conf import settings
# 【一】引入socket模块
import socket
import subprocess


def run_cmd(command):
    result = subprocess.run(
        command,  # 子进程要执行的命令
        shell=True,  # 执行的是shell的命令
        # 存放的是执行命令成功的结果
        stdout=subprocess.PIPE,
        # 存放的是执行命令失败的结果
        stderr=subprocess.PIPE,
        encoding="gbk",
        timeout=1)
    # returncode属性是run()函数返回结果的状态。
    if result.returncode == 0:
        return result.stdout
    else:
        return result.stderr


# 【二】创建一个client对象
# AF_INET:当前连接是基于网络的套接字
# SOCK_STREAM:连接模式是TCP协议的流式模式
client = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
# 【三】绑定IP和PORT
client.connect(settings.ADDR)
while True:
    # 【四】直接发送数据
    command = input("请输入需要执行的命令 :>>>> ").strip()
    if not command:
        print(f'不允许发送空的数据')
        continue
    if command == 'q':
        print(f'当前连接已退出!')
        break
    to_server_send_data = run_cmd(command=command)
    to_server_send_data = to_server_send_data.encode()
    client.send(to_server_send_data)
    # 【五】接收到服务端回的消息
    from_server_recv_data = client.recv(1024)
    from_server_recv_data = from_server_recv_data.decode()
    if from_server_recv_data == 'q':
        break
    print(f'这是来自服务端的数据 :>>>>  \n{from_server_recv_data}')
# 【六】关闭连接对象
client.close()
from conf import settings
# 【一】引入socket模块
import socket

# 【二】创建一个server对象
# AF_INET:当前连接是基于网络的套接字
# SOCK_STREAM:连接模式是TCP协议的流式模式
server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 【三】绑定IP和PORT
server.bind(settings.ADDR)
# 【四】监听连接对象
server.listen(5)
# 【五】建立连接对象
# 放在这里 进入的 while 循环中的哪个连接对象会一致不变
conn, addr = server.accept()
while True:
    try:
        # 这里面交流的对象永远是上面接收到的那一个
        # conn, addr = server.accept() : 接收到新的对象,忘记上一个连接过的对象
        # 【六】接收到客户端的数据
        from_client_recv_data = conn.recv(1024)
        from_client_recv_data = from_client_recv_data.decode()
        if not from_client_recv_data:
            break
        print(f'这是来自客户端的数据 :>>>>  \n {from_client_recv_data}')
        # 【七】返回给客户端数据
        while True:
            to_client_send_data = input("请输入发送给客户端的数据 :>>>> ").strip()
            if not to_client_send_data:
                print(f'不允许发送空的数据')
                continue
            if to_client_send_data == 'q':
                print(f'当前连接已退出!')
            to_client_send_data = to_client_send_data.encode()
            conn.send(to_client_send_data)
            break
    except Exception as e:
        break
# 【八】关闭连接和服务
conn.close()
server.close()

【3】问题解决

from conf import settings
import socket
import subprocess
import json
import uuid


def run_cmd(command):
    result = subprocess.run(
        command,  # 子进程要执行的命令
        shell=True,  # 执行的是shell的命令
        # 存放的是执行命令成功的结果
        stdout=subprocess.PIPE,
        # 存放的是执行命令失败的结果
        stderr=subprocess.PIPE,
        encoding="gbk",
        timeout=1)
    # returncode属性是run()函数返回结果的状态。
    if result.returncode == 0:
        return result.stdout
    else:
        return result.stderr


# 【二】创建一个server对象
# AF_INET:当前连接是基于网络的套接字
# SOCK_STREAM:连接模式是TCP协议的流式模式
client = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
# 【三】绑定IP和PORT
client.connect(settings.ADDR)
while True:
    # 【四】直接发送数据
    command = input('请输入需要执行的命令:>>>>>').strip()
    if not command:
        print(f'不允许发送空的数据')
        continue
    if command == 'q':
        print('当前连接退出')
        break
    # 【1】执行本地的命令,获取到当前命令结果
    result = run_cmd(command=command)
    # 【2】对命令的结果进行编码---> 转成二进制数据
    result_bytes = result.encode()
    # 【3】计算长度
    data_length = len(result_bytes)
    # 【4】增加一个数据概览 --> 字典格式 做数据概览
    # 存储当前文件名 / 结果名 / md5加密盐(用来校验数据的完整性)
    salt = uuid.uuid4().hex
    encrypted = settings.encrypt_data(data=result_bytes, salt=salt)
    send_data_info = {
        'command': command,
        'data_length': data_length,
        'salt': salt,
        'encrypted': encrypted,
    }
    # 【5】将上面打包好的数据全部发送给服务端
    # (1)字典格式无法发送
    # 将字典转换为字符串数据 ----> json
    # dump : 处理文件数据
    # dumps : 做格式转换的
    json_str = json.dumps(send_data_info)
    # (2)将json字符串数据转换为二进制数据
    json_bytes = json_str.encode()
    # 【6】问题产生
    # JSON字符串转换为的二进制数据还是会很长
    # 让数据变短
    # struct 模块 ---> 将某几个数字转换为四个字节的二进制数据
    json_length_pack = settings.pack_data(data_length=len(json_bytes))
    # 【7】发送struct打包的数据(四个字节) + JSON数据 + 原始数据
    # JSON数据里面存的是所有数据信息而没有原始的二进制数据
    # 服务端接受的顺序取决于客户端发送的顺序
    # 先发送struct打包后的数据
    client.send(json_length_pack)  # 4 字节 --> 包含json二进制数据的长度
    # 发送 json_bytes 打包后的数据
    client.send(json_bytes)
    # 再发送 result_bytes 原始数据
    client.send(result_bytes)
    # 【五】接收到服务端回的消息
    from_server_recv_data = client.recv(1024)
    from_server_recv_data = from_server_recv_data.decode()
    if from_server_recv_data == 'q':
        break
    print(f"这是来自服务端的数据:>>>>>\n{from_server_recv_data}")
# 【六】关闭连接对象
client.close()
import json
from conf import settings
# 【一】引入socket模块
import socket

# 【二】创建一个server对象
# AF_INET:当前连接是基于网络的套接字
# SOCK_STREAM:连接模式是TCP协议的流式模式
server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 【三】绑定IP和PORT
server.bind(settings.ADDR)
# 【四】监听连接对象
server.listen(5)
# 【五】建立连接对象
# 放在这里 进入的 while 循环中的哪个连接对象会一致不变
conn, addr = server.accept()
while True:
    #try:
        # 这里面交流的对象永远是上面接收到的那一个
        # conn, addr = server.accept() : 接收到新的对象,忘记上一个连接过的对象
        # 【六】接收到客户端的数据
        # 【1】先接接收四个字节的数据 ---> struct打包好的四个字节的数据
        json_pack_data = conn.recv(4)
        if not json_pack_data:
            break
        json_bytes_length = settings.unpack_data(data=json_pack_data)
        # 【2】根据json二进制数据长度解出JSON二进制数据
        json_data_bytes = conn.recv(json_bytes_length)
        # 【3】将json二进制数据转为json字符串数据
        json_str = json_data_bytes.decode()
        # 【4】将json字符串数据转换为python的字典
        data_info = json.loads(json_str)
        # 【5】从字典中获取自定的参数
        # 获取到总的数据长度
        # 10000
        data_length = data_info.get('data_length')
        # 【6】定义参数
        # (1)总数据
        # b'': 存在all_data 里的是二进制数据
        all_data = b''
        # (2)每次接收的数据大小
        size = 1024
        # data_length : 5
        # size : 2
        # count : 2 , last_size : 1
        # divmod:是以 data_length 去除以 size 取 count 为商,last_size为余数
        count, last_size = divmod(data_length, size)
        # (3)已经接受的数据大小
        all_size = 0
        while all_size < count + 1:
            all_size += 1
            # 接收到每一次的数据并和总数据拼接
            if all_size == count + 1:
                all_data += conn.recv(last_size)
            else:
                all_data += conn.recv(size)
        from_client_recv_data = all_data.decode()
        print(f'这是客户端的数据: \n{from_client_recv_data}')
        # 【七】返回给客户端数据
        while True:
            to_client_send_data = input("请输入发送给客户端的数据:>>>>>").strip()
            if not to_client_send_data:
                print(f'不允许发送空的数据')
                continue
            if to_client_send_data == 'q':
                print('退出连接')
            to_client_send_data = to_client_send_data.encode()
            conn.send(to_client_send_data)
            break
    # except Exception as e:
    #     break
# 【八】关闭连接和服务
conn.close()
server.close()

【4】subprocess模块回顾

import subprocess


def run_cmd(command):
    result = subprocess.run(
        command,  # 子进程要执行的命令
        shell=True,  # 执行的是shell的命令
        # 存放的是执行命令成功的结果
        stdout=subprocess.PIPE,
        # 存放的是执行命令失败的结果
        stderr=subprocess.PIPE,
        encoding="gbk",
        timeout=1)
    # returncode属性是run()函数返回结果的状态。
    if result.returncode == 0:
        return result.stdout
    else:
        return result.stderr


if __name__ == '__main__':
    print(run_cmd(['dir']))

【5】struct模块

# 【一】模块介绍
# ● struct.pack()是Python内置模块struct中的一个函数
# ● 它的作用是将指定的数据按照指定的格式进行打包
# 并将打包后的结果转换成一个字节序列(byte string),可以用于在网络上传输或者储存于文件中。
# 【二】参数简介
# struct.pack(fmt, v1, v2, ...)
# ● 其中,fmt为格式字符串,指定了需要打包的数据的格式,后面的v1,v2,...则是需要打包的数据。
# ● 这些数据会按照fmt的格式被编码成二进制的字节串,并返回这个字节串。

# 【三】示例
import struct

# 定义一个包含不同类型字段的格式字符串
format_string = 'i'

# 示例数据:整数、四个字节的原始数据、短整数
data_to_pack = '十七dasdadsad asd 撒大撒多所adsaddasdadsa da dsa asad撒大大带我去大青蛙大大大大大萨达去问问恰饭恰饭放散阀昂发昂发沙发阿发发发放上千万请发送方三房启发法阿发发发ad sada dsa dsa dsa sa dsa dsa as ad sad ad ada顿撒大大三大撒打我前端'
data_to_pack_bytes = data_to_pack.encode()

data_to_pack_len = len(data_to_pack_bytes)
print(data_to_pack_len)
# 使用 struct.pack 将数据打包成二进制字节串
packed_data = struct.pack(format_string, data_to_pack_len)

# 41000000
# 64000000
# 19010000
print("Packed data:", len(packed_data))  # 打印打包后的十六进制表示

# 解析二进制字节串,恢复原始数据
unpacked_data = struct.unpack(format_string, packed_data)
#
print("Unpacked data:", unpacked_data)  # 打印解析后的数据
posted @ 2024-05-22 14:28  光头大炮  阅读(9)  评论(0编辑  收藏  举报