• 开发工程师必备————【Day2】网络编程之TCP与UDP协议,黏包处理


    今日内容概要

    • TCP与UDP协议
    • socket套接字编程
    • 半连接池
    • TCP黏包问题及解决思路

    TCP与UDP协议

    1.规定了数据传输所遵循的规则;
    数据传输能够遵循的协议有很多,TCP和UDP是常见的两个。

    2.TCP协议:
    (1)三次握手
    建立双向通道
    请添加图片描述

    ps:洪水攻击——同时让大量的客户朝服务端发送建立TCP连接的请求。
    (2)四次挥手
    断开双向通道
    请添加图片描述

    中间的两步不能合并(需要有检查的时间)
    (3)基于TCP传输数据非常的安全,因为有双向通道这句话是否正确?
    答:不是因为有双向通道传输数据才安全,基于TCP传输数据,数据不容易丢失,其原因在于二次确认机制。
    每次发送数据都需要返回确认消息,否则在一定的时间会反复发送。

    3.UDP协议
    基于UDP协议发送数据,没有任何的通道也没有任何的机制;
    UDP发送数据没有TCP安全(没有二次确认机制)

    socket套接字

    1.套接字
    基于文件类型的套接字家族。
    套接字家族的名字:AF_UNIX
    基于网络类型的套接字家族。
    套接字家族的名字:AF_INEF

    2.实例展示之简单代码
    (1)服务端

    import socket
    
    
    # 创建一个socket对象
    server = socket.socket()   # 括号内什么都不写,默认就是基于网络的TCP套接字
    # 绑定一个固定的地址(ip\port)
    server.bind(('127.0.0.1', 8088))  # 127.0.0.1本地回环地址(只允许自己的机器访问)
    # 半连接池
    server.listen(5)
    sock,address = server.accept()
    print(sock,address) # sock是双向通道,address是客户端地址
    # 数据交互
    sock.send(b'hello word')  # 朝客户发送数据
    
    data = sock.recv(1024) # 接收客户端发送的数据 1024bytes
    print(data)
    # 断开连接
    sock.close()   # 断链接
    server.close()  # 关机
    
    
    
    D:\Python36\python36.exe "E:/pythonProject/Day37/01 socket模块/socket模块之服务端.py"
    <socket.socket fd=560, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8088), raddr=('127.0.0.1', 49827)> ('127.0.0.1', 49827)
    b'hello python'
    
    Process finished with exit code 0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    (2)客户端

    import socket
    
    # 产生一个socker对象
    client = socket.socket()
    # 连接服务器(拼接服务端的ip和port)
    client.connect(('127.0.0.1',8088))
    # 数据交互
    data = client.recv(1024)   # 接收服务端发送的数据
    print(data)
    client.send(b'hello python')   # 朝服务端发送数据
    # 关闭
    client.close()
    
    
    
    D:\Python36\python36.exe "E:/pythonProject/Day37/01 socket模块/socket模块之客户端.py"
    b'hello word'
    
    Process finished with exit code 0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    3.实例展示之代码优化处理
    (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)
    针对接收的消息加判断处理即可
    (8)代码展示
    (1)服务端

    import socket
    from socket import SOL_SOCKET, SO_REUSEADDR
    
    # 1.创建一个socket对象
    server = socket.socket()  # 括号内什么都不写 默认就是基于网络的TCP套接字
    # 2.绑定一个固定的地址(ip\port)
    server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)  # 就是它,在bind前加
    server.bind(('127.0.0.1', 8088))  # 127.0.0.1本地回环地址(只允许自己的机器访问)
    # 3.半连接池(暂且忽略)
    server.listen(5)
    # 4.开业 等待接客
    while True:
       sock, address = server.accept()
       print(sock, address)  # sock是双向通道 address是客户端地址
       # 5.数据交互
       while True:
           try:
               msg = input('请输入发送给客户端的消息>>>:').strip()
               if len(msg) == 0: continue
               sock.send(msg.encode('utf8'))  # 朝客户端发送数据
               data = sock.recv(1024)  # 接收客户端发送的数据 1024bytes
               if len(data) == 0:  #
                  break
               print(data.decode('utf8'))
          except ConnectionResetError:
              sock.close()
               break
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    (2)客户端

    import socket
    
    # 1.产生一个socket对象
    client = socket.socket()
    # 2.连接服务端(拼接服务端的ip和port)
    client.connect(('127.0.0.1', 8088))
    # 3.数据交互
    while True:
       data = client.recv(1024)  # 接收服务端发送的数据
       print(data.decode('utf8'))
       msg = input('请输入发送给客户端的消息>>>:').strip()
       if len(msg) == 0:
           msg = '手抖了一下 暂无消息'
       client.send(msg.encode('utf8'))  # 朝服务端发送数据
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    半连接池

    server.listen(5)
    主要是为了缓冲,避免太多的无效等待!!!

    黏包问题

    1.问题
    在前面我们知道tcp容易产生黏包的问题,而udp不会产生黏包的问题,但是会产生丢包的问题,tcp应用的场景很多所以黏包问题必须要解决。
    (1)TCP协议特性:
    流程协议:所有的数据类似于水流,连接在一起;
    会将数据量比较小并且时间间隔比较短的数据整合到一起发送
    并且还会受制于recv括号内的数字大小。

    (2)recv
    我们不知道即将要接收的数据量多大 如果知道的话不会产生也不会产生黏包

    2.解决方法一:
    1.解决黏包问题第一种方法,我们知道黏包问题是由于tcp的优化算法将两个不太大的数据包合并了一起发送的,这种情况一般出现在连续使用几个send()出现的,所以我们如果知道要发送的数据有多大我们就可以设置接收的大小,这样就可以刚好能把所有的数据接收完。下面是具体的步骤细节见代码
    server端代码

    import struct
    import socket
    sk = socket.socket()
    sk.bind(('127.0.0.1',8080))
    sk.listen()
    conn,addr = sk.accept()
    while True:
       cmd = input('>>>')
       conn.send(bytes(cmd,encoding='utf-8'))
       num = conn.recv(1024).decode('utf-8')   #接收client端计算好的数据长度
       conn.send(bytes('ok',encoding='utf-8'))
       #发送一个确认防止发送num的时候跟后面的send内容合并了
       ret = conn.recv(num)
       print(ret.decode('gbk'))
    conn.close()
    sk.close()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    client端代码

    import socket
    import struct
    import subprocess
    sk = socket.socket()
    sk.connect(('127.0.0.1',8080))
    while True:
       cmd = sk.recv(1024).decode('utf-8')
       ret = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
       std_out = ret.stdout.read()
       std_err = ret.stderr.read()
      sk.send(bytes(str(len(std_err)+len(std_out)),encoding='utf-8'))
       #上面计算字符串的长度发送给server端在接收的时候刚好接收那么长的数据
       sk.recv(1024) #ok  这一步主要的目的是为了将num的发送跟后面的send分割开防止黏包现象
       sk.send(std_out)
       sk.send(std_err)
    sk.close()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    3.解决方法二:
    用struct模块解决黏包现象
    server端代码

    #tcp黏包现象的解决 struct
    import struct
    import socket
    sk = socket.socket()
    sk.bind(('127.0.0.1',8080))
    sk.listen()
    conn,addr = sk.accept()
    while True:
       cmd = input('>>>')
       conn.send(bytes(cmd,encoding='utf-8'))
       num = conn.recv(1024)   #接收数据
       num = struct.unpack('i',num)[0]#进行解包,解包的结果是一个元组类型取第一个数据
       ret = conn.recv(num)
       print(ret.decode('gbk'))
    conn.close()
    sk.close()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    client端代码

    import socket
    import struct
    import subprocess
    sk = socket.socket()
    sk.connect(('127.0.0.1',8080))
    while True:
       cmd = sk.recv(1024).decode('utf-8')
       ret = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
       std_out = ret.stdout.read()
       std_err = ret.stderr.read()
       num = len(std_err) + len(std_out)
       num = struct.pack('i',num)  #利用struct模块将一个数据转换成bytes类型 i代表int型
       sk.send(num)
       sk.send(std_out)
       sk.send(std_err)
    sk.close()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    4.思路与总结
    (1)思路
    服务端:
    1.先将真实数据的长度制作成固定长度
    2.先发送固定长度的报头
    3.再发送真实数据
    客户端:
    1.先接收固定长度的报头
    2.再根据报头解压出真实长度
    3.根据真实长度接收即可
    (2)解决黏包问题的终极方案
    服务端
    1.先构造一个数据的详细字典
    2.对字典数据进行打包处理 得到一个固定长度的数据
    3.将上述打包之后的数据发送给客户端
    4.将字典数据发送给客户端
    5.将真实数据发送给客户端
    客户端
    1.先接收固定长度的数据
    2.根据固定长度解析出即将要接收的字典真实长度
    3.接收字典数据
    4.根据字典数据 获取出真实数据的长度
    5.接收真实数据长度
    (3)总结
    struct模块无论数据长度是多少 都可以帮你打包成固定长度
    然后基于该固定长度 还可以反向解析出真实长度
    struct模块针对数据量特别大的数字没有办法打包!!!

  • 相关阅读:
    模电和爱情一样,都很难懂!
    垃圾邮件(短信)分类算法实现 机器学习 深度学习 计算机竞赛
    为什么别人家的ChatGPT比我家的更聪明?
    选择适合您的项目管理软件:哪个更好?
    chromedriver下载地址
    ThreadLocal 是什么?它的实现原理呢?
    光芯片-汽车-自动驾驶-新能源分析
    SpringBoot项目整合
    Profinet现场总线耦合器模拟量扩展IO
    swing的Jlist列表滚动条以及增加元素的问题
  • 原文地址:https://blog.csdn.net/DiligentGG/article/details/126183253