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

    一操作系统基础

      操作系统:(Operating System,简称OS) 是管理和控制计算机硬件和软件资源的计算机程序,是直接运行在"裸机"上的最基本的系统软件,任何其他软件都必须在操作系统的支持下才能运行

      精简的说:操作形同就是一个协调,管理和控制计算机硬件资源的软件资源的控制程序,操作系统所在的位置,下图

      

     操作系统位于计算机硬件与应用软件之间,本质也是一个软件,操作系统由操作系统的内核(运行于内核态,管理硬件资源)以及系统调用(运行于用户态,为应运程序员写的应运程序提供系统调用接口)两部分组成,所以,单纯的说 操作系统是运行于内核态的,是不准确的
    View Code

        

      细说的话,操作系统应该分成两部分功能:

        

    1.影藏了丑陋的硬件调用接口(键盘,鼠标,音响等等怎么实现的额,就不需要多考虑了),为应运程序员调用硬件资源的更好,更简单,更清晰的模型(系统调用接口),应运程序员有了这些接口后,就不再用考虑操作硬件的细节,专心开发自己的应运程序就好了.
    例如:操作系统提供了文件这个抽象概念,对文件的操作就是对磁盘的读写控制,有了文件我们无需再去考虑关于磁盘的读写控制(比如磁盘的转动,移动磁头读写数据等细节)
    
    2.将应运程序对硬件资源的静态请求变得有序化
    例如:很多应运软件其实是共享一套计算机硬件,比方说有可能有3个应运程序同时需要申请打印机来输出内容,那么 a 程序竞争到了打印机资源就打印,然后可能是 b 竞争到了打印机资源,也可能是c,这就导致了无序,打印机有可能打印了一段a,打印一段b,再去打印一段 c.....,操作系统的作用就是讲这种无序变得有序
    View Code

       注: 计算机(硬件)  --> os --> 应用软件

        有关操作系统详细的介绍 和 原理 请参照 操作系统详解

    二 为什么学习 socket

      怎么通过自己写的程序来和其他计算机通信???

      当你使用自己的计算机和其他计算机 进行联系和发送消息 和文件等操作就是 网络通信

    三 CS架构,BS架构

      客户端英文名称:Client

      浏览器英文名称:Browser

      服务器英文名称:Server

    C/S架构就是说的  Client/Server架构, 客户端与服务器之间的架构,例如:你计算机的应用软件(淘宝,京东,暴风影音等等app) 和 服务器之间的 通信

    B/S架构就是说的 Browser/Server架构,浏览器与客户端之间的架构,例如:各大网站(网页版淘宝,京东等等) 和 服务器之间的 通信

    四 osi七层

      

      一般我们把 应用层,表示层,会话层合称为 应用层,从 tcp/ip 五层协议的角度来阐述每层的由来与功能,搞清楚每层的协议,有助于我们理解整个互联网通信原理

        1.物理层: 主要是基于电器特性发送高低电压(电信号),高电压对用数字 1,低电压对应数字 0.

               计算机要取得联系就必须接入 internet ,就是完成组网

        2.数据链路层: 由来: 单纯的电信号 0 和1 ,是没有任何意义的,必须分组,魅族什么意思.

              功能 : 定义了电信号的分组方式

            以太网协议: 早期的时候,各个公司都有自己的分组方式,后来形成了统一的标准,就是 以太网协议

            Ethernet 规定:一组电信号构成一个 数据报,叫做 "帧"

            每一数据帧分成 : 报头 head 和数据 date 两部分

                1.head(固定18字节) 包含:

                    发送者 / 源地址,6字节

                    接受者/目标地址,6字节

                    数据类型 , 6字节

                 2. date 包含:(最短 46 字节,最长 1500字节):

                    数据报的内容: head长度 + date 长度  = 最短 64 字节,最长 1518 字节,超过最大长度就分片发送

      Mac 地址:

        head中包含的源目标地址由来: Ethernet 规定 接入 Internet 的设备都必须具备网卡,发送端和接收端的地址就是网卡的地址,就是 Mac地址

        Mac地址:每块网卡出厂时候被烧制上一个世界唯一的Mac地址,长度为 48位 2进制,通常有12位 16进制数表示,(前6位是厂商编号,后6位是流水线号)

      广播:

        有了Mac地址,同一网络的两台计算机就有通信了,(一台计算机通过arp 协议获取到 另一台计算机的 Mac地址)

        Ethernet 采用 最原始的方式,广播的方式进行传播,即计算机的通信 基本 靠 吼

            

      3.网络层:

         

        网络层由来:有了 Ethernet,Mac 地址,广播的方式,世界上的计算机就实现通信了,问题是世界范围的互联网是由一个个彼此隔离的小的局域网组成的,那么如果所有的通信都采用以太网的广播方式,那么一台计算机发送的包全世界都会受到,这就不仅仅是效率低的问题了,这就是一种灾难了,图1..
    
    网络层 功能; 引入一套新的地址用来区分不同的广播域/子网,就是 网络地址
    
    IP协议:
        规定网络地址的协议叫 IP协议,它定义的地址被称为 IP地址,广泛采用 v4版本即 ipv4,规定网络地址 由 32位 2进制表示
        范围 0.0.0.0 - 255.255.255.255.255
        一个 IP地址 通常写成 四段 十进制数,例如:172.16.10.1
    
    IP 地址分为两部分:
        网络部分 : 标识子网
        主机部分: 表示主机
        注意:单纯的 IP地址段只是标识了 IP地址的种类,从网络部分或主机部分        
               都无法辨识一个 IP 所处的子网
          例如:172.16.10.1 与 172.16.10.2 并不能确定 二者处于同一子网
    
    子网掩码:
           就是表示子网特征的 一个参数,他在形式上等同于 IP地址,也是一个32位二进制的数字,他的网络部分全部是1,主机部分全部是 0
            例如: IP地址 172.16.10.1,如果已知网络部分是前24位,主机部分是    
                后8位,那么子网掩码是: 
               11111111.11111111.11111111.00000000,写成十进制就是 
                255.255.255.0。
    
    知道”子网掩码”,我们就能判断,任意两个IP地址是否处在同一个子网络。方法是将两个IP地址与子网掩码分别进行AND运算(两个数位都为1,运算结果为1,否则为0),然后比较结果是否相同,如果是的话,就表明它们在同一个子网络中,否则就不是。
    
    比如,已知IP地址172.16.10.1和172.16.10.2的子网掩码都是255.255.255.0,请问它们是否在同一个子网络?两者与子网掩码分别进行AND运算,
    
    172.16.10.1:10101100.00010000.00001010.000000001
    
    255255.255.255.0:11111111.11111111.11111111.00000000
    
    AND运算得网络地址结果:10101100.00010000.00001010.000000001->172.16.10.0
    
     
    
    172.16.10.2:10101100.00010000.00001010.000000010
    
    255255.255.255.0:11111111.11111111.11111111.00000000
    
    AND运算得网络地址结果:10101100.00010000.00001010.000000001->172.16.10.0
    
    结果都是172.16.10.0,因此它们在同一个子网络。
    
    总结一下,IP协议的作用主要有两个,一个是为每一台计算机分配IP地址,另一个是确定哪些地址在同一个子网络。
    
    IP数据包
    
        IP数据包也分为head和data部分,无须为ip包定义单独的栏位,直接放入以太网包的data部分
    
    head:长度为20到60字节
    
    data:最长为65,515字节。
    
    而以太网数据包的”数据”部分,最长只有1500字节。因此,如果IP数据包超过了1500字节,它就需要分割成几个以太网数据包,分开发送了。
         以太网头  +  IP 头  + IP 数据
    
    ARP协议
    
    arp协议由来:计算机通信基本靠吼,即广播的方式,所有上层的包到最后都要封装上以太网头,然后通过以太网协议发送,在谈及以太网协议时候,我门了解到
    
    通信是基于mac的广播方式实现,计算机在发包时,获取自身的mac是容易的,如何获取目标主机的mac,就需要通过arp协议
    
    arp协议功能:广播的方式发送数据包,获取目标主机的mac地址
    
     
    
    协议工作方式:每台主机ip都是已知的
    
    例如:主机172.16.10.10/24访问172.16.10.11/24
    
    一:首先通过ip地址和子网掩码区分出自己所处的子网
    
    
        场景        数据包地址
    同一子网    目标主机mac,目标主机ip
    不同子网    网关mac,目标
    
    二:分析172.16.10.10/24与172.16.10.11/24处于同一网络(如果不是同一网络,那么下表中目标ip为172.16.10.1,通过arp获取的是网关的mac)
    
    
         源mac    目标mac    源ip    目标ip    数据部分
    发送端主机    发送端mac    FF:FF:FF:FF:FF:FF    172.16.10.10/24    172.16.10.11/24    数据
     
    
    三:这个包会以广播的方式在发送端所处的自网内传输,所有主机接收后拆开包,发现目标ip为自己的,就响应,返回自己的mac
    View Code

     

     

      4.传输层:

        

    传输层的由来:网络层的ip帮我们区分子网,以太网层的mac帮我们找到主机,然后大家使用的都是应用程序,你的电脑上可能同时开启qq,暴风影音,等多个应用程序,
    
    那么我们通过ip和mac找到了一台特定的主机,如何标识这台主机上的应用程序,答案就是端口,端口即应用程序与网卡关联的编号。
    
    传输层功能:建立端口到端口的通信
    
    补充:端口范围0-65535,0-1023为系统占用端口
    
     
    
    tcp协议:
    
    可靠传输,TCP数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常TCP数据包的长度不会超过IP数据包的长度,以确保单个TCP数据包不必再分割。
    
    以太网头    ip 头                  tcp头                  数据                                                    
     
    
    
    udp协议:
    
    不可靠传输,”报头”部分一共只有8个字节,总长度不超过65,535字节,正好放进一个IP数据包。
    
    以太网头    ip头                         udp头                               数据   
    
    
    
    
    
                        
    View Code

         tcp 报头

          

      tcp 三次握手 四次挥手

          

      5.应运层:

        

    应用层由来:用户使用的都是应用程序,均工作于应用层,互联网是开发的,大家都可以开发自己的应用程序,数据多种多样,必须规定好数据的组织形式 
    
    应用层功能:规定应用程序的数据格式。
    
    例:TCP协议可以为各种各样的程序传递数据,比如Email、WWW、FTP等等。那么,必须有不同协议规定电子邮件、网页、FTP数据的格式,这些应用程序协议就构成了”应用层”。
    View Code

        

     五,socket

      看socket之前,先来回顾一下五层通讯流程:

        

        实际上 从传输层 开始以下,都是操作系统帮我们完成的,下面的各种包头封装的过程,不用我们自己完成...

          

        

    Socket又称为套接字,它是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。当我们使用不同的协议进行通信时就得使用不同的接口,还得处理不同协议的各种细节,这就增加了开发的难度,软件也不易于扩展(就像我们开发一套公司管理系统一样,报账、会议预定、请假等功能不需要单独写系统,而是一个系统上多个功能接口,不需要知道每个功能如何去实现的)。于是UNIX BSD就发明了socket这种东西,socket屏蔽了各个协议的通信细节,使得程序员无需关注协议本身,直接使用socket提供的接口来进行互联的不同主机间的进程的通信。这就好比操作系统给我们提供了使用底层硬件功能的系统调用,通过系统调用我们可以方便的使用磁盘(文件操作),使用内存,而无需自己去进行磁盘读写,内存管理。socket其实也是一样的东西,就是提供了tcp/ip协议的抽象,对外提供了一套接口,同过这个接口就可以统一、方便的使用tcp/ip协议的功能了。
    
      其实站在你的角度上看,socket就是一个模块。我们通过调用模块中已经实现的方法建立两个进程之间的连接和通信。也有人将socket说成ip+port,因为ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序。 所以我们只要确立了ip和port就能找到一个应用程序,并且使用socket模块来与之通信。
    View Code

    六 套接字发展史及分类

      

    套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。 因此,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。一开始,套接字被设计用在同 一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。 
    
    基于文件类型的套接字家族
    
    套接字家族的名字:AF_UNIX
    
    unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
    
    基于网络类型的套接字家族
    
    套接字家族的名字:AF_INET
    
    (还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET)
    View Code

     七 套接字的工作流程(基于 TCP 和 UDP 两个协议)

      1 tcp  和 udp对比

        

    TCP(Transmission Control Protocol)可靠的、面向连接的协议(eg:打电话)、传输效率低全双工通信(发送缓存&接收缓存)、面向字节流。使用TCP的应用:Web浏览器;文件传输程序。
    
    UDP(User Datagram Protocol)不可靠的、无连接的服务,传输效率高(发送前时延小),一对一、一对多、多对一、多对多、面向报文(数据包),尽最大努力服务,无拥塞控制。使用UDP的应用:域名系统 (DNS);视频流;IP语音(VoIP)。
    View Code

      2.tcp 协议下的 socket

         来个生活中的场景。你要打电话给一个朋友,先拨号,朋友听到电话铃声后提起电话,这时你和你的朋友就建立起了连接,就可以讲话了。

          等交流结束,挂断电话结束此次交谈。 生活中的场景就解释了这工作原理  

          

        先从服务器端说起,服务器先初始化 socket,然后与端口绑定,对端口进行监听,调用 accept 阻塞,等待客户端连接,在这时如果有个客户端初始化一个

          socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了,客户端发送数据请求,服务端接收请求并处理,然后把

          回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互完成

        细说 socket() 模块函数用法

            

    import socket
    
    socket.socket(socket_family,socket_type,protocal=0)
     socket_family 可以是 AF_UNIX 或 AF_INET。socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM。protocol 一般不填,默认值为 0。
    
     获取tcp/ip套接字
    tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    获取udp/ip套接字
    udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    
    由于 socket 模块中有太多的属性。我们在这里破例使用了'from module import *'语句。使用 'from socket import *',我们就把 socket 模块里的所有属性都带到我们的命名空间里了,这样能 大幅减短我们的代码。
    例如tcpSock = socket(AF_INET, SOCK_STREAM)
    服务端套接字函数
    s.bind()    绑定(主机,端口号)到套接字
    s.listen()  开始TCP监听
    s.accept()  被动接受TCP客户的连接,(阻塞式)等待连接的到来
    
    客户端套接字函数
    s.connect()     主动初始化TCP服务器连接
    s.connect_ex()  connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
    
    公共用途的套接字函数
    s.recv()            接收TCP数据
    s.send()            发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完)
    s.sendall()         发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)
    s.recvfrom()        接收UDP数据
    s.sendto()          发送UDP数据
    s.getpeername()     连接到当前套接字的远端的地址
    s.getsockname()     当前套接字的地址
    s.getsockopt()      返回指定套接字的参数
    s.setsockopt()      设置指定套接字的参数
    s.close()           关闭套接字
    
    面向锁的套接字方法
    s.setblocking()     设置套接字的阻塞与非阻塞模式
    s.settimeout()      设置阻塞套接字操作的超时时间
    s.gettimeout()      得到阻塞套接字操作的超时时间
    
    面向文件的套接字的函数
    s.fileno()          套接字的文件描述符
    s.makefile()        创建一个与该套接字相关的文件
    View Code

        第一版,单个客户端 与 服务器端 通信

            

    import socket
    
    # 类似打电话
    
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
    # 避免出现 接口不能重复使用的情况,也可以改变一下 8888 的数值
    # 1-1024 操作系统占用 1025 -8000 一般都是APP 使用接口,避免冲突使用
    #  8000 - 65535 
    phone.bind(("127.0.0.1",8888))
    phone.listen(5)  # 表示监听,挂起5个链接
    
    conn,addr = phone.accept()  # 等待链接
    conn.recv(1024) # 接收 1024 个字节,1024 表示最大能接收量
    conn.send("发送内容,字节形式")
    conn.close() # 断开管道连接
    phone.close() # 关闭 链接
    服务端
    import socket
    
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    phone.connect(("127.0.0.1"8888))
    
    phone.send("发送字节类型的数据")
    ret= phone.recv(1024) # 接收 bytes 类型的 数据
    print(ret.decode("utf-8")
    
    phone.close()
    客户端

       第二版,通信循环

        

    import socket 
    
    server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    server.bind(("127.0.0.1",8888))
    
    server.listen(5)
    
    conn,addr = server.accept()
    print(addr)
    while 1:
        try:
            from_client_date = conn.recv(1024)
            print(from_client_date.decode("utf-8"))
            conn.send(from_client_date + b"good")
        except ConnectionResetError:
            break
    
    conn.close()
    server.close()
    服务端
    import socket
    
    client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    client.connect(("127.0.0.1",8888))
    
    while 1:
        to_server_date = input("输入你要发送的内容:").strip()
        client.send(to_server_date.encode("utf-8"))
        from_server_date = client.recv(1024)
        print(from_server_date.decode("utf-8"))
    client.close()
    客户端

         第三版,通信 和 连接循环

    import socket
    
    server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    server.bind(("127.0.0.1",8888))
    
    server.listen(5)
    
    while 1:
        conn,addr = server.accept()
        print(addr)
        while 1:
            try:
                from_client_date = conn.recv(1024)
                print(from_client_date.decode("utf-8"))
                conn.send(from_client_date + b"good")
            except ConnectionResetError:
                break
    
        conn.close()
    server.close()
    服务端
    import socket
    
    client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    client.connect(("127.0.0.1",8888))
    
    while 1:
        to_server_date = input("输入你要发送的内容:").strip()
        client.send(to_server_date.encode("utf-8"))
        from_server_date = client.recv(1024)
        print(from_server_date.decode("utf-8"))
    client.close()
    客户端

         详解 recv 的工作原理  

    '''
    源码解释:
    Receive up to buffersize bytes from the socket.
    接收来自socket缓冲区的字节数据,
    For the optional flags argument, see the Unix manual.
    对于这些设置的参数,可以查看Unix手册。
    When no data is available, block untilat least one byte is available or until the remote end is closed.
    当缓冲区没有数据可取时,recv会一直处于阻塞状态,直到缓冲区至少有一个字节数据可取,或者远程端关闭。
    When the remote end is closed and all data is read, return the empty string.
    关闭远程端并读取所有数据后,返回空字符串。
    '''
    # ----------服务端------------:
    # 1,验证服务端缓冲区数据没有取完,又执行了recv执行,recv会继续取值。
    
    import socket
    
    phone =socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    
    phone.bind(('127.0.0.1',8080))
    
    phone.listen(5)
    
    conn, client_addr = phone.accept()
    from_client_data1 = conn.recv(2)
    print(from_client_data1)
    from_client_data2 = conn.recv(2)
    print(from_client_data2)
    from_client_data3 = conn.recv(1)
    print(from_client_data3)
    conn.close()
    phone.close()
    
    # 2,验证服务端缓冲区取完了,又执行了recv执行,此时客户端20秒内不关闭的前提下,recv处于阻塞状态。
    
    import socket
    
    phone =socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    
    phone.bind(('127.0.0.1',8080))
    
    phone.listen(5)
    
    conn, client_addr = phone.accept()
    from_client_data = conn.recv(1024)
    print(from_client_data)
    print(111)
    conn.recv(1024) # 此时程序阻塞20秒左右,因为缓冲区的数据取完了,并且20秒内,客户端没有关闭。
    print(222)
    
    conn.close()
    phone.close()
    
    
    # 3 验证服务端缓冲区取完了,又执行了recv执行,此时客户端处于关闭状态,则recv会取到空字符串。
    
    import socket
    
    phone =socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    
    phone.bind(('127.0.0.1',8080))
    
    phone.listen(5)
    
    conn, client_addr = phone.accept()
    from_client_data1 = conn.recv(1024)
    print(from_client_data1)
    from_client_data2 = conn.recv(1024)
    print(from_client_data2)
    from_client_data3 = conn.recv(1024)
    print(from_client_data3)
    conn.close()
    phone.close()
    # ------------客户端------------
    # 1,验证服务端缓冲区数据没有取完,又执行了recv执行,recv会继续取值。
    import socket
    import time
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    phone.connect(('127.0.0.1',8080))
    phone.send('hello'.encode('utf-8'))
    time.sleep(20)
    
    phone.close()
    
    
    
    # 2,验证服务端缓冲区取完了,又执行了recv执行,此时客户端20秒内不关闭的前提下,recv处于阻塞状态。
    import socket
    import time
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    phone.connect(('127.0.0.1',8080))
    phone.send('hello'.encode('utf-8'))
    time.sleep(20)
    
    phone.close()
    
    # 3,验证服务端缓冲区取完了,又执行了recv执行,此时客户端处于关闭状态,则recv会取到空字符串。
    import socket
    import time
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    phone.connect(('127.0.0.1',8080))
    phone.send('hello'.encode('utf-8'))
    phone.close()
    recv 工作原理

         远程执行命令

    import socket
    import subprocess
    
    server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    server.bind(("127.0.0.1",8888))
    
    server.listen(5)
    
    while 1:
        conn,addr = server.accept()
        print(addr)
        while 1:
            try:
                cmd = conn.recv(1024)
                ret = subprocess.Popen(cmd.decode("utf-8"),
                                       shell = True,
                                       stdout = subprocess.PIPE,
                                       stderr = subprocess.PIPE
                     )
                right_msg = ret.stdout.read()
                error_msg = ret.stderr.read()
                conn.send(right_msg + error_msg)
    
            except ConnectionResetError:
                break
    
        conn.close()
    server.close()
    服务端
    import socket
    
    client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    client.connect(("127.0.0.1",8888))
    
    while 1:
        cmd = input("输入你要发送的内容:").strip()
        client.send(cmd.encode("utf-8"))
        from_server_date = client.recv(1024)
        print(from_server_date.decode("gbk"))
        # 为什么要用 gbk 呢? 因为 你获得的 cmd 内容,是计算机发送过来的
        #  Windows 系统 默认 gbk 模式,Linux  默认  utf-8
    client.close()
    客户端

      当用这样的模式 传输的时候,会发现,如果 server 端 发送的数据 超过 1024 的时候,client端 只接收 1024 字节的数据,另外的数据伴随着你下一次的输入

        返回出来,这就是粘包现象,那什么是粘包呢??出现粘包现象的机理是什么??怎么解决粘包问题呢??

    八 粘包

      为什么会出现粘包问题呢?先看下 socket 缓存区的问题:

      

      

    每个 socket 被创建后,都会分配两个缓冲区,输入缓存区和输出缓存区。
    
    write()/send() 并不立即向网络中传输数据,而是先将数据写入缓存区中,再由TCP协议将数据从缓存区发送到目标机器。一旦将数据写入到缓存区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。
    
    TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓存区就发送到网络,也可能在缓存区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。
    
    read()/recv() 函数也是如此,也从输入缓存区中读取数据,而不是直接从网络中读取。
    
    这些I/O缓冲区特性可整理如下:
    
    1.I/O缓存区在每个TCP套接字中单独存在;
    2.I/O缓存区在创建套接字时自动生成;
    3.即使关闭套接字也会继续传送输出缓存区中遗留的数据;
    4.关闭套接字将丢失输入缓存区中的数据。
    
    输入输出缓存区的默认大小一般都是 8K,可以通过 getsockopt() 函数获取:
    
    1.unsigned optVal;
    2.int optLen = sizeof(int);
    3.getsockopt(servSock, SOL_SOCKET, SO_SNDBUF,(char*)&optVal, &optLen);
    4.printf("Buffer length: %d
    ", optVal);
    
    socket缓存区解释
    socket 缓存区的详细解释
    import socket
    server = socket.socket()
    server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)  # 重用ip地址和端口
    server.bind(('127.0.0.1',8010))
    server.listen(3)
    print(server.getsockopt(socket.SOL_SOCKET,socket.SO_SNDBUF))  # 输出缓冲区大小
    print(server.getsockopt(socket.SOL_SOCKET,socket.SO_RCVBUF))  # 输入缓冲区大小
    
    # 代码查看缓冲区大小
    查看 缓存区大小

         注意: 只有 TCP有粘包现象,UDP永远不会粘包

    发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
    
    例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束
    
    所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
    
    此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
    
    TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
    UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
    tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头,实验略
    udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠
    
    tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
    具体原因

       两种情况会发生粘包问题:

        1.接收方没有及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿

          上次遗留的数据,产生粘包) 

    import socket
    import subprocess
    
    server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    server.bind(("127.0.0.1",8888))
    
    server.listen(5)
    
    while 1:
        conn,addr = server.accept()
        print(addr)
        while 1:
            try:
                cmd = conn.recv(1024)
                ret = subprocess.Popen(cmd.decode("utf-8"),
                                       shell = True,
                                       stdout = subprocess.PIPE,
                                       stderr = subprocess.PIPE
                     )
                right_msg = ret.stdout.read()
                error_msg = ret.stderr.read()
                conn.send(right_msg + error_msg)
    
            except ConnectionResetError:
                break
    
        conn.close()
    server.close()
    服务端
    import socket
    
    client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    client.connect(("127.0.0.1",8888))
    
    while 1:
        cmd = input("输入你要发送的内容:").strip()
        client.send(cmd.encode("utf-8"))
        from_server_date = client.recv(1024)
        print(from_server_date.decode("gbk"))
        # 为什么要用 gbk 呢? 因为 你获得的 cmd 内容,是计算机发送过来的
        #  Windows 系统 默认 gbk 模式,Linux  默认  utf-8
    client.close()
    客户端

             2.发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据也很小,会合到一起,产生粘包)

    import socket
    
    
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    phone.bind(('127.0.0.1', 8080))
    
    phone.listen(5)
    
    conn, client_addr = phone.accept()
    
    frist_data = conn.recv(1024)
    print('1:',frist_data.decode('utf-8'))  # 1: helloworld
    second_data = conn.recv(1024)
    print('2:',second_data.decode('utf-8'))
    
    
    conn.close()
    phone.close()
    服务端
    import socket
    
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  
    
    phone.connect(('127.0.0.1', 8080)) 
    
    phone.send(b'hello')
    phone.send(b'world')
    
    phone.close()  
    
    # 两次返送信息时间间隔太短,数据小,造成服务端一次收取
    客户端

              3. 粘包的解决方案:

          先介绍下 struct 模块

            这个模块 可以把 一个类型,如数字,转成固定长度的 bytes

            

    import struct
    # 将一个数字转化成等长度的bytes类型。
    ret = struct.pack('i', 183346)
    print(ret, type(ret), len(ret))
    
    # 通过unpack反解回来
    ret1 = struct.unpack('i',ret)[0]
    print(ret1, type(ret1), len(ret1))
    
    
    # 但是通过struct 处理不能处理太大
    
    ret = struct.pack('l', 4323241232132324)
    print(ret, type(ret), len(ret))  # 报错
    struct 代码示例

     解决粘包问题:

    方案1:

    import socket
    import subprocess
    import struct
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    phone.bind(('127.0.0.1', 8080))
    
    phone.listen(5)
    
    while 1:
        conn, client_addr = phone.accept()
        print(client_addr)
        
        while 1:
            try:
                cmd = conn.recv(1024)
                ret = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                correct_msg = ret.stdout.read()
                error_msg = ret.stderr.read()
                
                # 1 制作固定报头
                total_size = len(correct_msg) + len(error_msg)
                header = struct.pack('i', total_size)
                
                # 2 发送报头
                conn.send(header)
                
                # 发送真实数据:
                conn.send(correct_msg)
                conn.send(error_msg)
            except ConnectionResetError:
                break
    
    conn.close()
    phone.close()
    
    
    # 但是low版本有问题:
    # 1,报头不只有总数据大小,而是还应该有MD5数据,文件名等等一些数 
    #
    # 2,通过struct模块直接数据处理,不能处理太大。
    服务端
    import socket
    import struct
    
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    
    phone.connect(('127.0.0.1',8080))
    
    
    while 1:
        cmd = input('输入内容:').strip()
        if not cmd: continue
        phone.send(cmd.encode('utf-8'))
        
        # 1,接收固定报头
        header = phone.recv(4)
        
        # 2,解析报头
        total_size = struct.unpack('i', header)[0]  # 返回的是(xxx,)类型
        
        # 3,根据报头信息,接收真实数据
        recv_size = 0
        res = b''
        
        while recv_size < total_size:
            
            recv_data = phone.recv(1024)
            res += recv_data
            recv_size += len(recv_data)
    
        print(res.decode('gbk'))
    
    phone.close()
    客户端

    方案2:自定义报头

    整个流程的大致解释:
    我们可以把报头做成字典,字典里包含将要发送的真实数据的描述信息(大小啊之类的),然后json序列化,然后用struck将序列化后的数据长度打包成4个字节。
    我们在网络上传输的所有数据 都叫做数据包,数据包里的所有数据都叫做报文,报文里面不止有你的数据,还有ip地址、mac地址、端口号等等,其实所有的报文都有报头,这个报头是协议规定的,看一下
    
    发送时:
    先发报头长度
    再编码报头内容然后发送
    最后发真实内容
    
    接收时:
    先手报头长度,用struct取出来
    根据取出的长度收取报头内容,然后解码,反序列化
    从反序列化的结果中取出待取数据的描述信息,然后去取真实的数据内容
    整体流程
    import socket
    import subprocess
    import json
    import struct
    
    server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
    server.bind(("127.0.0.1",8888))
    server.listen(5)
    
    while 1:
        conn,addr = server.accept()
        print(addr)                    # ('127.0.0.1', 50652)
        while 1:
            try:
                cmd = conn.recv(1024)
                ret = subprocess.Popen(cmd.decode("utf-8"),
                                       shell = True,
                                       stdout = subprocess.PIPE,
                                       stderr = subprocess.PIPE
                )
                right_msg = ret.stdout.read()
                error_msg = ret.stderr.read()
    
                dic = {
                    "filename":"哈哈哥",
                    "md5":"我是哈哈哥",
                    "file_size":len(right_msg + error_msg)
                }
    
                bytes_dic = json.dumps(dic).encode("utf-8")       #  字节类型的字典
                bytes_dic_size = struct.pack("i",len(bytes_dic))   # 字节类型的字典 转换成 固定的
    
                a = conn.send(bytes_dic_size)
                b = conn.send(bytes_dic)
    
                conn.send(right_msg + error_msg)
            except ConnectionResetError:
                break
        conn.close()
    server.close()
    服务端
    import socket
    import struct
    import json
    
    client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    client.connect(("127.0.0.1",8888))
    
    while 1:
        try:
            date = input("输入命令:").strip().encode("utf-8")
            if not date:continue
            client.send(date)
    
            bytes_dic_size = client.recv(4)
            dic_size = struct.unpack("i",bytes_dic_size)[0]
            print(dic_size)
    
            bytes_dic = client.recv(dic_size)
            dic = json.loads(bytes_dic.decode("utf-8"))
            print(dic)
    
    
            recv_msg_size = 0
            recv_msg = b""
            while recv_msg_size < dic["file_size"]:
                ret = client.recv(1024)
                recv_msg += ret
                recv_msg_size += len(ret)
            print(recv_msg.decode("gbk"))
    
        except ConnectionResetError:
            break
    client.close()
    客户端

    文件上传下载功能实现(简单版本):

       

    import socket
    import struct
    import json
    import subprocess
    import os
    
    class MYTCPServer:
        address_family = socket.AF_INET
    
        socket_type = socket.SOCK_STREAM
    
        allow_reuse_address = False
    
        max_packet_size = 8192
    
        coding='utf-8'
    
        request_queue_size = 5
    
        server_dir='file_upload'
    
        def __init__(self, server_address, bind_and_activate=True):
            """Constructor.  May be extended, do not override."""
            self.server_address=server_address
            self.socket = socket.socket(self.address_family,
                                        self.socket_type)
            if bind_and_activate:
                try:
                    self.server_bind()
                    self.server_activate()
                except:
                    self.server_close()
                    raise
    
        def server_bind(self):
            """Called by constructor to bind the socket.
            """
            if self.allow_reuse_address:
                self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            self.socket.bind(self.server_address)
            self.server_address = self.socket.getsockname()
    
        def server_activate(self):
            """Called by constructor to activate the server.
            """
            self.socket.listen(self.request_queue_size)
    
        def server_close(self):
            """Called to clean-up the server.
            """
            self.socket.close()
    
        def get_request(self):
            """Get the request and client address from the socket.
            """
            return self.socket.accept()
    
        def close_request(self, request):
            """Called to clean up an individual request."""
            request.close()
    
        def run(self):
            while True:
                self.conn,self.client_addr=self.get_request()
                print('from client ',self.client_addr)
                while True:
                    try:
                        head_struct = self.conn.recv(4)
                        if not head_struct:break
    
                        head_len = struct.unpack('i', head_struct)[0]
                        head_json = self.conn.recv(head_len).decode(self.coding)
                        head_dic = json.loads(head_json)
    
                        print(head_dic)
                        #head_dic={'cmd':'put','filename':'a.txt','filesize':123123}
                        cmd=head_dic['cmd']
                        if hasattr(self,cmd):
                            func=getattr(self,cmd)
                            func(head_dic)
                    except Exception:
                        break
    
        def put(self,args):
            file_path=os.path.normpath(os.path.join(
                self.server_dir,
                args['filename']
            ))
    
            filesize=args['filesize']
            recv_size=0
            print('----->',file_path)
            with open(file_path,'wb') as f:
                while recv_size < filesize:
                    recv_data=self.conn.recv(self.max_packet_size)
                    f.write(recv_data)
                    recv_size+=len(recv_data)
                    print('recvsize:%s filesize:%s' %(recv_size,filesize))
    
    
    tcpserver1=MYTCPServer(('127.0.0.1',8080))
    
    tcpserver1.run()
    
    
    
    
    
    
    #下列代码与本题无关
    class MYUDPServer:
    
        """UDP server class."""
        address_family = socket.AF_INET
    
        socket_type = socket.SOCK_DGRAM
    
        allow_reuse_address = False
    
        max_packet_size = 8192
    
        coding='utf-8'
    
        def get_request(self):
            data, client_addr = self.socket.recvfrom(self.max_packet_size)
            return (data, self.socket), client_addr
    
        def server_activate(self):
            # No need to call listen() for UDP.
            pass
    
        def shutdown_request(self, request):
            # No need to shutdown anything.
            self.close_request(request)
    
        def close_request(self, request):
            # No need to close anything.
            pass
    View Code
    import socket
    import struct
    import json
    import os
    
    
    
    class MYTCPClient:
        address_family = socket.AF_INET
    
        socket_type = socket.SOCK_STREAM
    
        allow_reuse_address = False
    
        max_packet_size = 8192
    
        coding='utf-8'
    
        request_queue_size = 5
    
        def __init__(self, server_address, connect=True):
            self.server_address=server_address
            self.socket = socket.socket(self.address_family,
                                        self.socket_type)
            if connect:
                try:
                    self.client_connect()
                except:
                    self.client_close()
                    raise
    
        def client_connect(self):
            self.socket.connect(self.server_address)
    
        def client_close(self):
            self.socket.close()
    
        def run(self):
            while True:
                inp=input(">>: ").strip()
                if not inp:continue
                l=inp.split()
                cmd=l[0]
                if hasattr(self,cmd):
                    func=getattr(self,cmd)
                    func(l)
    
    
        def put(self,args):
            cmd=args[0]
            filename=args[1]
            if not os.path.isfile(filename):
                print('file:%s is not exists' %filename)
                return
            else:
                filesize=os.path.getsize(filename)
    
            head_dic={'cmd':cmd,'filename':os.path.basename(filename),'filesize':filesize}
            print(head_dic)
            head_json=json.dumps(head_dic)
            head_json_bytes=bytes(head_json,encoding=self.coding)
    
            head_struct=struct.pack('i',len(head_json_bytes))
            self.socket.send(head_struct)
            self.socket.send(head_json_bytes)
            send_size=0
            with open(filename,'rb') as f:
                for line in f:
                    self.socket.send(line)
                    send_size+=len(line)
                    print(send_size)
                else:
                    print('upload successful')
    
    
    
    
    client=MYTCPClient(('127.0.0.1',8080))
    
    client.run()
    View Code

    socket 的更多方法介绍

    服务端套接字函数
    s.bind()    绑定(主机,端口号)到套接字
    s.listen()  开始TCP监听
    s.accept()  被动接受TCP客户的连接,(阻塞式)等待连接的到来
    
    客户端套接字函数
    s.connect()     主动初始化TCP服务器连接
    s.connect_ex()  connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
    
    公共用途的套接字函数
    s.recv()            接收TCP数据
    s.send()            发送TCP数据
    s.sendall()         发送TCP数据
    s.recvfrom()        接收UDP数据
    s.sendto()          发送UDP数据
    s.getpeername()     连接到当前套接字的远端的地址
    s.getsockname()     当前套接字的地址
    s.getsockopt()      返回指定套接字的参数
    s.setsockopt()      设置指定套接字的参数
    s.close()           关闭套接字
    
    面向锁的套接字方法
    s.setblocking()     设置套接字的阻塞与非阻塞模式
    s.settimeout()      设置阻塞套接字操作的超时时间
    s.gettimeout()      得到阻塞套接字操作的超时时间
    
    面向文件的套接字的函数
    s.fileno()          套接字的文件描述符
    s.makefile()        创建一个与该套接字相关的文件
    View Code

    验证客户端链接的合法性

      如果你想在分布式系统中实现一个简单的客户端链接认证功能,又不像 SSL 那么复杂,那么利用

      hmac+ 加盐的方式来实现

    #_*_coding:utf-8_*_
    from socket import *
    import hmac,os
    
    secret_key=b'linhaifeng bang bang bang'
    def conn_auth(conn):
        '''
        认证客户端链接
        :param conn:
        :return:
        '''
        print('开始验证新链接的合法性')
        msg=os.urandom(32)
        conn.sendall(msg)
        h=hmac.new(secret_key,msg)
        digest=h.digest()
        respone=conn.recv(len(digest))
        return hmac.compare_digest(respone,digest)
    
    def data_handler(conn,bufsize=1024):
        if not conn_auth(conn):
            print('该链接不合法,关闭')
            conn.close()
            return
        print('链接合法,开始通信')
        while True:
            data=conn.recv(bufsize)
            if not data:break
            conn.sendall(data.upper())
    
    def server_handler(ip_port,bufsize,backlog=5):
        '''
        只处理链接
        :param ip_port:
        :return:
        '''
        tcp_socket_server=socket(AF_INET,SOCK_STREAM)
        tcp_socket_server.bind(ip_port)
        tcp_socket_server.listen(backlog)
        while True:
            conn,addr=tcp_socket_server.accept()
            print('新连接[%s:%s]' %(addr[0],addr[1]))
            data_handler(conn,bufsize)
    
    if __name__ == '__main__':
        ip_port=('127.0.0.1',9999)
        bufsize=1024
        server_handler(ip_port,bufsize)
    服务端
    #_*_coding:utf-8_*_
    __author__ = 'Linhaifeng'
    from socket import *
    import hmac,os
    
    secret_key=b'linhaifeng bang bang bang'
    def conn_auth(conn):
        '''
        验证客户端到服务器的链接
        :param conn:
        :return:
        '''
        msg=conn.recv(32)
        h=hmac.new(secret_key,msg)
        digest=h.digest()
        conn.sendall(digest)
    
    def client_handler(ip_port,bufsize=1024):
        tcp_socket_client=socket(AF_INET,SOCK_STREAM)
        tcp_socket_client.connect(ip_port)
    
        conn_auth(tcp_socket_client)
    
        while True:
            data=input('>>: ').strip()
            if not data:continue
            if data == 'quit':break
    
            tcp_socket_client.sendall(data.encode('utf-8'))
            respone=tcp_socket_client.recv(bufsize)
            print(respone.decode('utf-8'))
        tcp_socket_client.close()
    
    if __name__ == '__main__':
        ip_port=('127.0.0.1',9999)
        bufsize=1024
        client_handler(ip_port,bufsize)
    客户端合法
    #_*_coding:utf-8_*_
    __author__ = 'Linhaifeng'
    from socket import *
    
    def client_handler(ip_port,bufsize=1024):
        tcp_socket_client=socket(AF_INET,SOCK_STREAM)
        tcp_socket_client.connect(ip_port)
    
        while True:
            data=input('>>: ').strip()
            if not data:continue
            if data == 'quit':break
    
            tcp_socket_client.sendall(data.encode('utf-8'))
            respone=tcp_socket_client.recv(bufsize)
            print(respone.decode('utf-8'))
        tcp_socket_client.close()
    
    if __name__ == '__main__':
        ip_port=('127.0.0.1',9999)
        bufsize=1024
        client_handler(ip_port,bufsize)
    客户端非法,不知道加密方式
    #_*_coding:utf-8_*_
    __author__ = 'Linhaifeng'
    from socket import *
    import hmac,os
    
    secret_key=b'linhaifeng bang bang bang1111'
    def conn_auth(conn):
        '''
        验证客户端到服务器的链接
        :param conn:
        :return:
        '''
        msg=conn.recv(32)
        h=hmac.new(secret_key,msg)
        digest=h.digest()
        conn.sendall(digest)
    
    def client_handler(ip_port,bufsize=1024):
        tcp_socket_client=socket(AF_INET,SOCK_STREAM)
        tcp_socket_client.connect(ip_port)
    
        conn_auth(tcp_socket_client)
    
        while True:
            data=input('>>: ').strip()
            if not data:continue
            if data == 'quit':break
    
            tcp_socket_client.sendall(data.encode('utf-8'))
            respone=tcp_socket_client.recv(bufsize)
            print(respone.decode('utf-8'))
        tcp_socket_client.close()
    
    if __name__ == '__main__':
        ip_port=('127.0.0.1',9999)
        bufsize=1024
        client_handler(ip_port,bufsize)
    客户端非法,不知道secret_key
  • 相关阅读:
    秋色园QBlog技术原理解析:系列终结篇:最后的AOP策略(十九)
    半解TextBox灵异事件背后神秘的深度灵异事件
    SQLite julianday DateTime日期时区问题小记录
    Winform 多组合老板键Alt_Ctrl_Shift
    性能杀手之异常霸气外露!找死!
    DataReader不奇怪,该出手时就出手!
    DBImport v3.0 中文版发布:支持各大数据库数据互导(IT人员必备工具)
    文本数据库.Net界未来的一朵奇葩
    TextBox灵异事件之背后神秘的深度灵异事件真相揭秘
    秋色园QBlog技术原理解析:性能优化篇:读写分离与文本数据库(十八)
  • 原文地址:https://www.cnblogs.com/wenqi2121/p/10424363.html
Copyright © 2011-2022 走看看