zoukankan      html  css  js  c++  java
  • Python3之网络编程

    一、初识网络编程

    1.socket概念
    Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,
    它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

    
    



    2.两种家族套接字:基于文件的和面向网络的
    AF_UNIX 基于文件类型的套接字家族
    AF_INET 基于网络类型的套接字家族


    3.两种类型套接字连接
    SOCK_STREAM: TCP面向连接的套接字
    SOCK_DGRAM: UDP面向无连接的套接字


    4.socket()模块函数
    要创建套接字,必须使用socket.socket()函数,它一般的语法如下。
    socket(socket_family, socket_type, protocol=0)

    其中,socket_family 是AF_UNIX 或AF_INET(如前所述),socket_type 是SOCK_STREAM
    或SOCK_DGRAM(也如前所述)。protocol 通常省略,默认为0。

    创建TCP/IP 套接字
    tc pSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    创建UDP/IP 套接字
    ud pSock = socket.socket(socket.AF_INET, socket.SOCK_DGRA

    5.套接字对象(内置)方法

    服务器套接字方法
    s.bind() 将地址(主机名、端口号对)绑定到套接字上
    s.listen() 设置并启动TCP 监听器
    s.accept() 被动接受TCP 客户端连接,一直等待直到连接到达(阻塞)
    
    客户端套接字方法
    s.connect() 主动发起TCP 服务器连接
    s.connect_ex() connect()的扩展版本,此时会以错误码的形式返回问题,而不是抛出一个异常
    
    普通的套接字方法
    s.recv() 接收TCP 消息
    s.recv_into()   接收TCP 消息到指定的缓冲区
    s.send()        发送TCP 消息
    s.sendall()     完整地发送TCP 消息
    s.recvfrom()    接收UDP 消息
    s.recvfrom_into() 接收UDP 消息到指定的缓冲区
    s.sendto()      发送UDP 消息
    s.getpeername() 连接到套接字(TCP)的远程地址
    s.getsockname() 当前套接字的地址
    s.getsockopt() 返回给定套接字选项的值
    s.setsockopt() 设置给定套接字选项的值
    s.shutdown()    关闭连接
    s.close()       关闭套接字
    s.detach()     在未关闭文件描述符的情况下关闭套接字,返回文件描述符
    s.ioctl()      控制套接字的模式(仅支持Windows)
    
    面向阻塞的套接字方法
    s.setblocking()     设置套接字的阻塞或非阻塞模式
    s.settimeout()     设置阻塞套接字操作的超时时间
    s.gettimeout()     获取阻塞套接字操作的超时时间
    
    面向文件的套接字方法
    s.fileno()      套接字的文件描述符
    s.makefile()    创建与套接字关联的文件对象
    
    数据属性
    s.family 套接字家族
    s.type 套接字类型
    s.proto 套接字协议
    

    6.基于TCP的socket

    简单版
    
    TCP服务器端
    
    import socket
    sk = socket.socket()
    # sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
    sk.bind(('127.0.0.1',8898))  #把地址绑定到套接字
    sk.listen()          #监听链接
    conn,addr = sk.accept() #接受客户端链接
    ret = conn.recv(1024)  #接收客户端信息
    print(ret)       #打印客户端信息
    conn.send(b'hi')        #向客户端发送信息
    conn.close()       #关闭客户端套接字
    sk.close()        #关闭服务器套接字(可选)
    
    执行结果:
    b'hello!'
    
    
    client端
    
    import socket
    sk = socket.socket()           # 创建客户套接字
    sk.connect(('127.0.0.1',8898)) # 尝试连接服务器
    sk.send(b'hello!')
    ret = sk.recv(1024)            # 对话(发送/接收)
    print(ret)
    sk.close()                     # 关闭客户套接字
    
    执行结果:
    b'hi'
    

      

    实例2:实现客户端和服务端的简单尬聊

    服务端
    
    import socket
    sk = socket.socket()
    sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
    sk.bind(('127.0.0.1',8080))
    sk.listen()
    conn,addr = sk.accept()
    while True:
        ret = conn.recv(1024)
        print(ret.decode('utf-8'))
        if ret == b'bye':
            conn.send(b'bye')
            break
        info = input('>>>')
        conn.send(bytes(info,encoding='utf-8'))
    conn.close()
    sk.close()
    
    客户端
    
    import socket
    sk = socket.socket()
    sk.connect(('127.0.0.1',8080))
    while True:
        info = input('>>>')
        sk.send(bytes(info,encoding='utf-8'))
        ret = sk.recv(1024)
        if ret == b'bye':
            sk.send(b'bye')
            break
        print(ret.decode('utf-8'))
    
    sk.close()
    

      

     7.基于UDP的socket

    实例1:简单版通信

    UDP服务端
    
    import socket
    
    sk = socket.socket(type=socket.SOCK_DGRAM)
    ip_port = ('127.0.0.1',8080)
    sk.bind(ip_port)
    message,addr = sk.recvfrom(1024)
    print(message.decode('utf-8'))
    print(addr)
    sk.sendto(b'hello udp client,Im Server!',addr)
    sk.close()
    
    UDP客户端
    
    import socket
    sk = socket.socket(type=socket.SOCK_DGRAM)
    ip_port = ('127.0.0.1',8080)
    sk.sendto(b'hello udp Server,Im client1!',ip_port)
    info,addr = sk.recvfrom(1024)
    print(info.decode('utf-8'))
    print(addr)
    
    执行结果:
    服务端
    hello udp Server,Im client1!
    ('127.0.0.1', 62259)
    
    客户端
    hello udp client,Im Server!
    ('127.0.0.1', 8080)
    UDP的server 不需要进行监听也不需要建立连接
    在启动服务后只能被动的等待客户端发送消息过来
    客户端发送消息的同时还会自带地址消息
    消息回复的时候 不仅需要发送消息 还需要填上回复消息的地址

     

    实例2:尬聊

    服务端
    
    import socket
    sk = socket.socket(type=socket.SOCK_DGRAM)
    ip_port = ('127.0.0.1',8080)
    sk.bind(ip_port)
    print(socket.gethostname())
    while 1:
        info,addr = sk.recvfrom(1024)
        print(info.decode('utf-8'))
        print(addr)
        message = input('Server>>>').encode('utf-8')
        sk.sendto(message,addr)
    
    sk.close()
    
    客户端1:
    
    import socket
    sk = socket.socket(type=socket.SOCK_DGRAM)
    ip_port = ('127.0.0.1',8080)
    while 1:
        message = input('Client1>>>').encode('utf-8')
        sk.sendto(message,ip_port)
        info,addr = sk.recvfrom(1024)
        print(info.decode('utf-8'))
        print(addr)
    
    sk.close()
    
    客户端2:
    import socket
    
    sk = socket.socket(type=socket.SOCK_DGRAM)
    ip_port = ('127.0.0.1',8080)
    
    while 1:
        message = input('Client2>>>').encode('utf-8')
        sk.sendto(message,ip_port)
        info,addr = sk.recvfrom(1024)
        print(info.decode('utf-8'))
        print(addr)
    sk.close()
    
    
    执行结果:
    服务端
    CCHNCQL008
    你好 我是客户端1
    ('127.0.0.1', 51851)
    Server>>>你好 我是服务端
    你好 我是客户端2
    ('127.0.0.1', 51853)
    Server>>>你好 我是服务端
    
    客户端1:
    Client1>>>你好 我是客户端1
    你好 我是服务端
    ('127.0.0.1', 8080)
    Client1>>>
    
    客户端2:
    Client2>>>你好 我是客户端2
    你好 我是服务端
    ('127.0.0.1', 8080)
    Client2>>>
    

      

    实例3:简单实现“时间同步”功能

    服务端
    
    import time
    import socket
    
    sk = socket.socket(type=socket.SOCK_DGRAM)
    sk.bind(('127.0.0.1',9000))
    while True:
        msg,addr = sk.recvfrom(1024)
        # msg 客户端发送给server端的时间格式 "%Y-%m-%d %H:%M-%S"
        time_format = msg.decode('utf-8')
        time_str = time.strftime(time_format)
        sk.sendto(time_str.encode('utf-8'),addr)
    sk.close()
    
    客户端
    
    import time
    import socket
    sk = socket.socket(type = socket.SOCK_DGRAM)
    sk.sendto('%Y/%m/%d %H:%M:%S'.encode('utf-8'),('127.0.0.1',9000))  #定制获取时间的格式
    msg,addr = sk.recvfrom(1024)
    print(msg.decode('utf-8'))
    sk.close()
    
    
    执行结果:
    
    客户端
    2018/10/22 11:05:39
    

      

      TCP和UDP区别:

      TCP (Transmission Control Protocol)和UDP(User Datagram Protocol)协议属于传输层协议。

      其中TCP提供IP环境下的数据可靠传输,它提供的服务包括数据流传送、可靠性、有效流控、全双工操作和多路复用。通过面向连接、端到端和可靠的数据包发送。通俗说,它是事先为所发送的数据开辟出连接好的通道,然后再进行数据发送;

      而UDP为面向非连接协议,“面向非连接”就是在正式通信前不必与对方先建立连接,不管对方状态就直接发送。则不为IP提供可靠性、流控或差错恢复功能。

      一般来说,TCP对应的是可靠性要求高的应用,而UDP对应的则是可靠性要求低、传输经济的应用。TCP支持的应用协议主要有:Telnet、FTP、SMTP等;UDP支持的应用层协议主要有:NFS(网络文件系统)、SNMP(简单网络管理协议)、DNS(主域名称系统)、TFTP(通用文件传输协议)等。

    二、TCP连接的黏包

      1.粘包缘由?

    TCP:由于TCP协议本身的机制(面向连接的可靠地协议-三次握手机制)客户端与服务器会维持一个连接,数据在连接不断开的情况下,
    可以持续不断地将多个数据包发往服务器,但是如果发送的网络数据包太小,那么他本身会启用Nagle算法(可配置是否启用)对较小的数据包进行
    合并(基于此,TCP的网络延迟要UDP的高些)然后再发送(超时或者包大小足够)。那么这样的话,服务器在接收到消息(数据流)的时候就无法
    区分哪些数据包是客户端自己分开发送的,这样产生了粘包;服务器在接收到数据库后,放到缓冲区中,如果消息没有被及时从缓存区取走,下次
    在取数据的时候可能就会出现一次取出多个数据包的情况,造成粘包现象。

    UDP:本身作为无连接的不可靠的传输协议(适合频繁发送较小的数据包),他不会对数据包进行合并发送(也就没有Nagle算法之说了),
    他直接是一端发送什么数据,直接就发出去了,既然他不会对数据合并,每一个数据包都是完整的(数据+UDP头+IP头等等发一次数据封装一次)也就没有粘包一说了。

    分包产生的原因就简单的多:可能是IP分片传输导致的,也可能是传输过程中丢失部分包导致出现的半包,还有可能就是一个包可能被分成了两次传输,
    在取数据的时候,先取到了一部分(还可能与接收的缓冲区大小有关系),总之就是一个数据包被分成了多次接收。

    实例1:TCP的长连接

    # Server端
    import socket
    sk = socket.socket()
    sk.bind(('127.0.0.1',8080))
    sk.listen()
    
    info,addr = sk.accept()
    ret = info.recv(1024)
    print(ret)
    info.send(b'Hi Client!the message from Server')
    
    info.close()
    sk.close()
    
    # Clent1端
    sk = socket.socket()
    sk.connect(('127.0.0.1',8080))
    
    sk.send(b'hello,Server,I am from clent1!')
    info = sk.recv(1024)
    print(info)
    sk.close()
    
    
    # Clent2端
    sk = socket.socket()
    sk.connect(('127.0.0.1',8080))
    
    sk.send(b'hello,Server,I am from clent2!')
    info = sk.recv(1024)
    print(info)
    sk.close()
    
    当面向连接TCP通信时,客户端只有1个能与服务端通信,当客户端1与服务端连接时,说明他们之间已经保持连接的状态,客户端2就不能与服务端连接。只能中断和客户端1连接后,再与客户端2连接,这就是TCP长连接的问题。
    

    实例2:远程执行命令返回结果,因不知道数据的长度,不能一次性完整的接收数据,待下次接受后,才能完整显示,

    # Server 下发命令给客户端
    import socket
    ip_port = ('127.0.0.1',8080)
    sk = socket.socket()
    sk.bind((ip_port))
    sk.listen()
    conn,addr = sk.accept()
    while True:
        cmd = input('>>>')
        if cmd == 'Q' or cmd == 'q':
            break
        conn.send(cmd.encode('gbk'))
        res = conn.recv(2048)
        print(res.decode('gbk'))
    
    conn.close()
    sk.close()
    
    #client接收命令 并执行命令 返回给Server
    
    import socket
    import subprocess
    
    sk = socket.socket()
    
    ip_port = ('127.0.0.1',8080)
    sk.connect((ip_port))
    
    while True:
        cmd = sk.recv(1024).decode('gbk')
        if cmd == 'q' or cmd == 'Q':
            break
        ret = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
        ret_stdout = (ret.stdout.read())
        ret_stderr = (ret.stderr.read())
        print(cmd)
        print(ret_stdout.decode('gbk'))
        print(ret_stderr.decode('gbk'))
        sk.send(ret_stdout)
        sk.send(ret_stderr)
    
    sk.close()
    

     

    >>>dir
     驱动器 D 中的卷没有标签。
     卷的序列号是 8FA5-5F6B
    
     D:Python_learnStudy10.21exp1_tcp黏包 的目录
    
    2018/10/22  11:21    <DIR>          .
    2018/10/22  11:21    <DIR>          ..
    2018/10/22  11:19             4,860 client.py
    2018/10/22  11:21             3,938 server.py
    2018/10/21  18:47                 0 __init__.py
                   3 个文件          8,798 字节
                   2 个目录 43,630,231,552 可用字节
    
    >>>ipconfig /all
    
    Windows IP 配置
    
       主机名  . . . . . . . . . . . . . : CCHNCQL008
       主 DNS 后缀 . . . . . . . . . . . : 
       节点类型  . . . . . . . . . . . . : 混合
       IP 路由已启用 . . . . . . . . . . : 否
       WINS 代理已启用 . . . . . . . . . : 否
    
    无线局域网适配器 无线网络连接 2:
    
       媒体状态  . . . . . . . . . . . . : 媒体已断开
       连接特定的 DNS 后缀 . . . . . . . : 
       描述. . . . . . . . . . . . . . . : Microsoft Virtual WiFi Miniport Adapter
       物理地址. . . . . . . . . . . . . : AC-E0-10-55-39-D1
       DHCP 已启用 . . . . . . . . . . . : 是
       自动配置已启用. . . . . . . . . . : 是
    
    以太网适配器 Bluetooth 网络连接:
    
       媒体状态  . . . . . . . . . . . . : 媒体已断开
       连接特定的 DNS 后缀 . . . . . . . : 
       描述. . . . . . . . . . . . . . . : Bluetooth 设备(个人区域网)
       物理地址. . . . . . . . . . . . . : D0-53-49-D9-83-75
       DHCP 已启用 . . . . . . . . . . . : 是
       自动配置已启用. . . . . . . . . . : 是
    
    以太网适配器 本地连接 2:
    
       连接特定的 DNS 后缀 . . . . . . . : 
       描述. . . .
    >>>dir
     . . . . . . . . . . . : ASIX AX88772C USB2.0 to Fast Ethernet Adapter
       物理地址. . . . . . . . . . . . . : 00-0E-C6-A5-73-41
       DHCP 已启用 . . . . . . . . . . . : 是
       自动配置已启用. . . . . . . . . . : 是
       本地链接 IPv6 地址. . . . . . . . : fe80::c1fa:6a8a:172b:9bd%14(首选) 
       IPv4 地址 . . . . . . . . . . . . : 10.127.55.10(首选) 
       子网掩码  . . . . . . . . . . . . : 255.255.255.0
       获得租约的时间  . . . . . . . . . : 2018年10月22日 9:10:21
       租约过期的时间  . . . . . . . . . : 2018年10月23日 9:10:20
       默认网关. . . . . . . . . . . . . : 10.127.55.254
       DHCP 服务器 . . . . . . . . . . . : 10.127.55.253
       DHCPv6 IAID . . . . . . . . . . . : 436211398
       DHCPv6 客户端 DUID  . . . . . . . : 00-01-00-01-21-40-0D-2B-AC-E0-10-55-39-D1
       DNS 服务器  . . . . . . . . . . . : 221.5.203.98
                                           221.7.92.98
       TCPIP 上的 NetBIOS  . . . . . . . : 已启用
    
    以太网适配器 本地连接:
    
       媒体状态  . . . . . . . . . . . . : 媒体已断开
       连接特定的 DNS 后缀
    >>>dir
     . . . . . . . : 
       描述. . . . . . . . . . . . . . . : Realtek PCIe GBE Family Controller
       物理地址. . . . . . . . . . . . . : 30-8D-99-C4-0C-0D
       DHCP 已启用 . . . . . . . . . . . : 否
       自动配置已启用. . . . . . . . . . : 是
    
    无线局域网适配器 无线网络连接:
    
       媒体状态  . . . . . . . . . . . . : 媒体已断开
       连接特定的 DNS 后缀 . . . . . . . : 
       描述. . . . . . . . . . . . . . . : Broadcom BCM943228HMB 802.11abgn 2x2 Wi-Fi Adapter
       物理地址. . . . . . . . . . . . . : AC-E0-10-55-39-D1
       DHCP 已启用 . . . . . . . . . . . : 是
       自动配置已启用. . . . . . . . . . : 是
    
    隧道适配器 isatap.{45C597F0-B8B5-4F80-89CF-BD6E4D47D151}:
    
       媒体状态  . . . . . . . . . . . . : 媒体已断开
       连接特定的 DNS 后缀 . . . . . . . : 
       描述. . . . . . . . . . . . . . . : Microsoft ISATAP Adapter
       物理地址. . . . . . . . . . . . . : 00-00-00-00-00-00-00-E0
       DHCP 已启用 . . . . . . . . . . . : 否
       自动配置已启用. . . . . . . . . . : 是
    
    隧道适配器 isatap.{FB4AFF6C-8A37-40A3-A7A0-65422AC3
    >>>dir
    5F32}:
    
       媒体状态  . . . . . . . . . . . . : 媒体已断开
       连接特定的 DNS 后缀 . . . . . . . : 
       描述. . . . . . . . . . . . . . . : Microsoft ISATAP Adapter #2
       物理地址. . . . . . . . . . . . . : 00-00-00-00-00-00-00-E0
       DHCP 已启用 . . . . . . . . . . . : 否
       自动配置已启用. . . . . . . . . . : 是
    
    隧道适配器 Teredo Tunneling Pseudo-Interface:
    
       连接特定的 DNS 后缀 . . . . . . . : 
       描述. . . . . . . . . . . . . . . : Teredo Tunneling Pseudo-Interface
       物理地址. . . . . . . . . . . . . : 00-00-00-00-00-00-00-E0
       DHCP 已启用 . . . . . . . . . . . : 否
       自动配置已启用. . . . . . . . . . : 是
       IPv6 地址 . . . . . . . . . . . . : 2001:0:ddcc:f429:3025:38e1:f580:c8f5(首选) 
       本地链接 IPv6 地址. . . . . . . . : fe80::3025:38e1:f580:c8f5%15(首选) 
       默认网关. . . . . . . . . . . . . : ::
       TCPIP 上的 NetBIOS  . . . . . . . : 已禁用
    
    隧道适配器 isatap.{FEDA9CAA-E138-451C-BB19-2FD25E03B90F}:
    
       媒体状态  . . . . . . . . . . . . : 媒体已断开
       连接特定的 DNS 后缀 .
    >>>dir
     . . . . . . : 
       描述. . . . . . . . . . . . . . . : Microsoft ISATAP Adapter #3
       物理地址. . . . . . . . . . . . . : 00-00-00-00-00-00-00-E0
       DHCP 已启用 . . . . . . . . . . . : 否
       自动配置已启用. . . . . . . . . . : 是
     驱动器 D 中的卷没有标签。
     卷的序列号是 8FA5-5F6B
    
     D:Python_learnStudy10.21exp1_tcp黏包 的目录
    
    2018/10/22  11:21    <DIR>          .
    2018/10/22  11:21    <DIR>          ..
    2018/10/22  11:19             4,860 client.py
    2018/10/22  11:21             3,938 server.py
    2018/10/21  18:47                 0 __init__.py
                   3 个文件          8,798 字节
                   2 个目录 43,630,231,552 可用字节
     驱动器 D 中的卷没有标签。
     卷的序列号是 8FA5-5F6B
    
     D:Python_learnStudy10.21exp1_tcp黏包 的目录
    
    2018/10/22  11:21    <DIR>          .
    2018/10/22  11:21    <DIR>          ..
    2018/10/22  11:19             4,860 client.py
    2018/10/22  11:21             3,938 server.py
    2018/10/21  18:47                 0 __init__.py
                   3 个文件          8,79
    执行结果
    说明:
    (1)执行第一次命令 dir时,数据长度小于等于1024,能完整的接收返回的结果。
    (2)执行dir后,执行ipconfig /all命令,因为数据很长,接受1024个字节的长度并不能完整的接收数据。若服务端改为每次接收2048个字节数据也不能完整接收。因为并不知道接收的数据长度。
    (3)再次执行dir命令后,接收到的数据为:第二次执行的命令剩余的部分数据加上第三次执行dir命令的数据。说明第二次和第三次命令返回的数据发送黏包现象

     实例3:

    服务端
    import socket
    ip_port = ('127.0.0.1',8080)
    sk = socket.socket()
    sk.bind((ip_port))
    sk.listen()
    conn,addr = sk.accept()
    ret1 = conn.recv(4).decode('utf-8')
    ret2 = conn.recv(8).decode('utf-8')
    
    print(ret1)
    print(ret2)
    
    conn.close()
    sk.close()
    
    客户端
    import socket
    sk = socket.socket()
    ip_port = ('127.0.0.1',8080)
    sk.connect((ip_port))
    sk.send(b'felix.wang')
    
    sk.close()
    
    执行结果:
    服务端
    feli
    x.wang
    
    结果表示:不知道对端发送的数据的长度,分别接受两次也能接受全部数据
    

     

    实例4:

    服务端
    import socket
    ip_port = ('127.0.0.1',8080)
    sk = socket.socket()
    sk.bind((ip_port))
    sk.listen()
    conn,addr = sk.accept()
    ret1 = conn.recv(12).decode('utf-8')
    print(ret1)
    conn.close()
    sk.close()
    
    客户端
    import socket
    sk = socket.socket()
    ip_port = ('127.0.0.1',8080)
    sk.connect((ip_port))
    sk.send(b'felix')
    sk.send(b'wang')
    
    sk.close()
    
    执行结果:
    服务端
    
    执行多次会发现收到的数据有可能为 felix 或者 felixwang
    执行结果为 felixwang 表示数据内部做了优化算法 连续的小数据包会被合并
    多个send小的数据连在一起的时候,就有可能发生黏包现象,是TCP协议内部的优化算法造成的
    

      

    2.解决粘包方法

    (1)在接收数据之前,先接收数据的大小,再接收的时候按照数据大小接收

    # Server
    import socket
    ip_port = ('127.0.0.1',8080)
    sk = socket.socket()
    sk.bind((ip_port))
    sk.listen()
    conn,addr = sk.accept()
    while True:
        cmd = input('>>>')
        if cmd == 'Q' or cmd == 'q':
            break
        conn.send(cmd.encode('gbk'))
    
        data_length  = conn.recv(1024)  #接收客户端发来的数据长度
        print(data_length.decode('utf-8'))
        conn.send(b'ok')
        res = conn.recv(int(data_length))  #再按照数据长度大小接收
        print(res.decode('gbk'))
    
    conn.close()
    sk.close()
    
    # client端
    
    import socket
    import subprocess
    
    sk = socket.socket()
    
    ip_port = ('127.0.0.1',8080)
    sk.connect((ip_port))
    
    while True:
        cmd = sk.recv(1024).decode('gbk')
        if cmd == 'q' or cmd == 'Q':
            break
        ret = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
        ret_stdout = (ret.stdout.read())
        ret_stderr = (ret.stderr.read())
        data_length = len(ret_stdout) + len(ret_stderr)  #数据长度
        sk.send(str(data_length).encode('utf-8'))    #向server端发送数据长度大小
        print(cmd)
        cmd = sk.recv(1024)
        print(ret_stdout.decode('gbk'))
        print(ret_stderr.decode('gbk'))
        sk.send(ret_stdout)
        sk.send(ret_stderr)
    
    sk.close()
    
    
            
    

      

    说明:
    此方法为客户端先计算数据的大小,再将数据大小发送给服务端,服务端接收数据的大小后,按照数据的大小接收,就可以一次性完整的接收数据。
    优点:能完整接收数据,解决了粘包问题。
    缺点:服务端和客户端多了一次交互
       若数据太大,会导致消耗太多内存

    那么如何解决一次接受性数据太多,导致消耗内存的问题呢?

     (2)还是在接收数据之前,先接收数据的大小,再每次按照指定大小(1024)接收,若小于指定大小可一次性接收,若大于指定大小则每次接收指定大小,

    数据大小每次减去指定大小长度(1024),直到数据大小为0,则接收完整。

    #Server
    import socket
    ip_port = ('127.0.0.1',8080)
    sk = socket.socket()
    sk.bind((ip_port))
    sk.listen()
    conn,addr = sk.accept()
    while True:
        cmd = input('>>>')
        if cmd == 'Q' or cmd == 'q':
            break
        conn.send(cmd.encode('gbk'))
        data_length  = conn.recv(1024).decode('utf-8')
        data_length = int(data_length)
        print('要接收数据大小:{} 字节'.format(data_length))
        conn.send(b'ok')
    
        while data_length:
            if data_length < 1024:
                res = conn.recv(1024)
                print(res.decode('gbk'))
                data_length = 0
            else:
                res = conn.recv(1024)
                data_length -= 1024
                print(res.decode('gbk'))
                # print(data_length)
    
    conn.close()
    sk.close()
    
    
    #Client
    
    ip_port = ('127.0.0.1',8080)
    sk.connect((ip_port))
    while True:
        cmd = sk.recv(1024).decode('gbk')
        if cmd == 'q' or cmd == 'Q':
            break
        ret = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
        ret_stdout = (ret.stdout.read())
        ret_stderr = (ret.stderr.read())
        data_length = len(ret_stdout) + len(ret_stderr)
        sk.send(str(data_length).encode('utf-8'))
        print(cmd)
        cmd = sk.recv(1024)
        print(ret_stdout.decode('gbk'))
        print(ret_stderr.decode('gbk'))
        sk.send(ret_stdout)
        sk.send(ret_stderr)
    sk.close()
    

     

    >>>dir
    要接收数据大小:436 字节
     驱动器 D 中的卷没有标签。
     卷的序列号是 8FA5-5F6B
    
     D:Python_learnStudy10.21exp2_解决TCP粘包问题方法2 的目录
    
    2018/10/22  14:12    <DIR>          .
    2018/10/22  14:12    <DIR>          ..
    2018/10/22  14:12               659 client.py
    2018/10/22  14:01               768 server.py
    2018/10/22  13:34                 0 __init__.py
                   3 个文件          1,427 字节
                   2 个目录 43,510,009,856 可用字节
    
    >>>ipconfig /all
    要接收数据大小:4355 字节
    
    Windows IP 配置
    
       主机名  . . . . . . . . . . . . . : CCHNCQL008
       主 DNS 后缀 . . . . . . . . . . . : 
       节点类型  . . . . . . . . . . . . : 混合
       IP 路由已启用 . . . . . . . . . . : 否
       WINS 代理已启用 . . . . . . . . . : 否
    
    以太网适配器 Bluetooth 网络连接:
    
       媒体状态  . . . . . . . . . . . . : 媒体已断开
       连接特定的 DNS 后缀 . . . . . . . : 
       描述. . . . . . . . . . . . . . . : Bluetooth 设备(个人区域网)
       物理地址. . . . . . . . . . . . . : D0-53-49-D9-83-75
       DHCP 已启用 . . . . . . . . . . . : 是
       自动配置已启用. . . . . . . . . . : 是
    
    以太网适配器 本地连接 2:
    
       连接特定的 DNS 后缀 . . . . . . . : 
       描述. . . . . . . . . . . . . . . : ASIX AX88772C USB2.0 to Fast Ethernet Adapter
       物理地址. . . . . . . . . . . . . : 00-0E-C6-A5-73-41
       DHCP 已启用 . . . . . . . . . . . : 是
       自动配置已启用. . . . . . . . . . : 是
       本地链接 IPv6 地址. . . . . . . . : fe80::c1fa:6a8a:172b:9bd%14(首选) 
       IPv4 地址 . . . . . . . . . . . . : 10.127.55.10(首选) 
       
    子网掩码  . . . . . . . . . . . . : 255.255.255.0
       获得租约的时间  . . . . . . . . . : 2018年10月22日 13:31:18
       租约过期的时间  . . . . . . . . . : 2018年10月23日 13:31:18
       默认网关. . . . . . . . . . . . . : 10.127.55.254
       DHCP 服务器 . . . . . . . . . . . : 10.127.55.253
       DHCPv6 IAID . . . . . . . . . . . : 436211398
       DHCPv6 客户端 DUID  . . . . . . . : 00-01-00-01-21-40-0D-2B-AC-E0-10-55-39-D1
       DNS 服务器  . . . . . . . . . . . : 221.5.203.98
                                           221.7.92.98
       TCPIP 上的 NetBIOS  . . . . . . . : 已启用
    
    以太网适配器 本地连接:
    
       媒体状态  . . . . . . . . . . . . : 媒体已断开
       连接特定的 DNS 后缀 . . . . . . . : 
       描述. . . . . . . . . . . . . . . : Realtek PCIe GBE Family Controller
       物理地址. . . . . . . . . . . . . : 30-8D-99-C4-0C-0D
       DHCP 已启用 . . . . . . . . . . . : 否
       自动配置已启用. . . . . . . . . . : 是
    
    无线局域网适配器 无线网络连接:
    
       媒体状态  . . . . . . . . . . . . : 媒体已断开
       连接特定的 DNS 后缀 . . 
    . . . . . : 
       描述. . . . . . . . . . . . . . . : Broadcom BCM943228HMB 802.11abgn 2x2 Wi-Fi Adapter
       物理地址. . . . . . . . . . . . . : AC-E0-10-55-39-D1
       DHCP 已启用 . . . . . . . . . . . : 是
       自动配置已启用. . . . . . . . . . : 是
    
    隧道适配器 isatap.{ED5225D2-304E-4857-AB9F-C0D3EDA125CA}:
    
       媒体状态  . . . . . . . . . . . . : 媒体已断开
       连接特定的 DNS 后缀 . . . . . . . : 
       描述. . . . . . . . . . . . . . . : Microsoft ISATAP Adapter #2
       物理地址. . . . . . . . . . . . . : 00-00-00-00-00-00-00-E0
       DHCP 已启用 . . . . . . . . . . . : 否
       自动配置已启用. . . . . . . . . . : 是
    
    隧道适配器 Teredo Tunneling Pseudo-Interface:
    
       连接特定的 DNS 后缀 . . . . . . . : 
       描述. . . . . . . . . . . . . . . : Teredo Tunneling Pseudo-Interface
       物理地址. . . . . . . . . . . . . : 00-00-00-00-00-00-00-E0
       DHCP 已启用 . . . . . . . . . . . : 否
       自动配置已启用. . . . . . . . . . : 是
       IPv6 地址 . . . . . . . . . . . . : 2001:0:ddcc:f429:300c:353c:f580:c8f5(首选) 
      
     本地链接 IPv6 地址. . . . . . . . : fe80::300c:353c:f580:c8f5%15(首选) 
       默认网关. . . . . . . . . . . . . : ::
       TCPIP 上的 NetBIOS  . . . . . . . : 已禁用
    
    隧道适配器 isatap.{FEDA9CAA-E138-451C-BB19-2FD25E03B90F}:
    
       媒体状态  . . . . . . . . . . . . : 媒体已断开
       连接特定的 DNS 后缀 . . . . . . . : 
       描述. . . . . . . . . . . . . . . : Microsoft ISATAP Adapter #3
       物理地址. . . . . . . . . . . . . : 00-00-00-00-00-00-00-E0
       DHCP 已启用 . . . . . . . . . . . : 否
       自动配置已启用. . . . . . . . . . : 是
    
    隧道适配器 isatap.{4EF1428D-080C-44A6-B8B2-9FAEFD1DB5A8}:
    
       媒体状态  . . . . . . . . . . . . : 媒体已断开
       连接特定的 DNS 后缀 . . . . . . . : 
       描述. . . . . . . . . . . . . . . : Microsoft ISATAP Adapter #4
       物理地址. . . . . . . . . . . . . : 00-00-00-00-00-00-00-E0
       DHCP 已启用 . . . . . . . . . . . : 否
       自动配置已启用. . . . . . . . . . : 是
    
    隧道适配器 isatap.{FB4AFF6C-8A37-40A3-A7A0-65422AC35F32}:
    
       媒体状态  . . . . . . . . . . . . : 媒体已断开
    
       连接特定的 DNS 后缀 . . . . . . . : 
       描述. . . . . . . . . . . . . . . : Microsoft ISATAP Adapter #5
       物理地址. . . . . . . . . . . . . : 00-00-00-00-00-00-00-E0
       DHCP 已启用 . . . . . . . . . . . : 否
       自动配置已启用. . . . . . . . . . : 是
    
    >>>
    执行结果

    3.解决粘包的进阶方法(struct模块

     1 #Server
     2 import json
     3 import struct
     4 import socket
     5 
     6 sk = socket.socket()
     7 sk.bind(('127.0.0.1',8081))
     8 sk.listen()
     9 
    10 conn,addr = sk.accept()
    11 dic_len = conn.recv(4)  # 4个字节 数字的大小
    12 print(dic_len)
    13 dic_len = struct.unpack('i',dic_len)[0]
    14 print(dic_len)
    15 content = conn.recv(dic_len).decode('utf-8')  # 70
    16 print(content)
    17 content_dic = json.loads(content)
    18 print(content_dic)
    19 if content_dic['operate'] == 'upload':
    20     with open(content_dic['filename'],'wb') as f:
    21         while content_dic['filesize']:
    22             file = conn.recv(1024)
    23             f.write(file)
    24             content_dic['filesize'] -= len(file)
    25 conn.close()
    26 sk.close()
    27 
    28 #client
    29 import os
    30 import json
    31 import struct
    32 import socket
    33 
    34 sk = socket.socket()
    35 sk.connect(('127.0.0.1',8081))
    36 
    37 def get_filename(file_path):
    38     filename = os.path.basename(file_path)
    39     return filename
    40 
    41 #选择 操作
    42 operate = ['upload','download']
    43 for num,opt in enumerate(operate,1):
    44     print(num,opt)
    45 num = int(input('请输入您要做的操作序号 : '))
    46 if num == 1:
    47     '''上传操作'''
    48     file_path = input('请输入要上传的文件路径 : ')
    49     file_size = os.path.getsize(file_path)  # 获取文件大小
    50     file_name = get_filename(file_path)
    51     dic = {'operate': 'upload', 'filename': file_name,'filesize':file_size}
    52     str_dic = json.dumps(dic).encode('utf-8')
    53     print(str_dic)
    54     ret = struct.pack('i', len(str_dic))  # 将字典的大小转换成一个定长(4)的bytes
    55     print(ret)
    56     sk.send(ret + str_dic)
    57     print(ret+str_dic)
    58     with open(file_path,'rb') as  f:
    59         sum = 0
    60         count = 0
    61         s = file_size
    62         while file_size:
    63             count += 1
    64             content = f.read(1024)
    65             sk.send(content)
    66             file_size -= len(content)
    67             sum += 1024
    68             baifenshu = sum/s
    69             print('*',end='')
    70             if count%150 == 0:
    71                 print()
    72             # print('已上传%.2f%%'%(baifenshu*100),end='')
    73 
    74 elif num == 2:
    75     '''下载操作'''
    76 sk.close()
    文件上传
  • 相关阅读:
    在ubuntu下复制文件出现权限不够的解决方法
    Ubuntu安装ROS Melodic
    gedit文件操作
    Linux下强制删除文件和权限操作
    VMware Tools 继续运行脚本未能在虚拟机中成功运行 解决方式
    Linux解压命令
    Ubuntu 18.04.4 LTS(Bionic Beaver)安装
    Socket层实现系列 — send()类发送函数的实现
    iOS7 CookBook精彩瞬间(三)UIActivityViewController的基本使用及自定义Activity
    iOS7 CookBook精彩瞬间(二)NSSet、通过Subscript访问类成员等
  • 原文地址:https://www.cnblogs.com/Felix-DoubleKing/p/9828665.html
Copyright © 2011-2022 走看看