• Python 网络编程之Socket


    socket是什么

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

    所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。

    socket参数介绍

    sk = socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)  必会

    Socket Families(地址簇)

    • socket.AF_INET  表示IPV4(默认)
    • socket.AF_INET6 表示IPV6
    • socket.AF_UNIX   只能用于单一的Unix系统进程间的通信

    Socket Types

    • socket.SOCK_STREAM  流式socket for TCP(默认)
    • socket.SOCK_DGRAM  数据格式socket,for UDP
    • socket.SOCK_RAW   原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以;其次,SOCK_RAW也可以处理特殊的IPv4报文;此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头
    • socket.SOCK_RDM  是一种可靠的UDP形式,即保证交付数据报但不保证顺序。SOCK_RAM用来提供对原始协议的低级访问,在需要执行某些特殊操作时使用,如发送ICMP报文。SOCK_RAM通常仅限于高级用户或管理员运行的程序使用
    • socket.SOCK_SEQPACKET  可靠的连续数据包服务,已废弃

    socket.getaddrinfo(hostportfamily=0type=0proto=0flags=0)  必会

      获取要连接的对端主机地址

    sk.bind(address)  必会

      sk.bind(address) 将套接字绑定到地址。address地址的格式取决于地址族。在AF_INET下,以元组(host,port)的形式表示地址。

    sk.listen(backlog)  必会

      开始监听传入连接。backlog指定在拒绝连接之前,可以挂起的最大连接数量

      backlog等于5,表示内核已经接到了连接请求,但服务器还没有调用accept进行处理的连接个数最大为5

      这个值不能无限大,因为要在内核中维护连接队列

    sk.setblocking(bool)  必会

      是否阻塞(默认True),如果设置False,那么accept和recv时一旦无数据,则报错。

    sk.accept()  必会

       接受连接并返回(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据。address是连接客户端的地址

      接收TCP 客户的连接(阻塞式)等待连接的到来

    sk.connect(address)  必会

       连接到address处的套接字。一般,address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。

    sk.connect_ex(address)

      同上,只不过会有返回值,连接成功时返回 0 ,连接失败时候返回编码,例如:10061

    sk.close()  必会

      关闭套接字

    sk.recv(bufsize[,flag])  必会

      接受套接字的数据。数据以字符串形式返回,bufsize指定最多可以接收的数量。flag提供有关消息的其他信息,通常可以忽略。

    sk.recvfrom(bufsize[.flag])

      与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。

    sk.send(string[,flag])  必会

      将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。即:可能未将指定内容全部发送。

    sk.sendall(string[,flag])  必会

      将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。

      内部通过递归调用send,将所有内容发送出去。

    sk.sendto(string[,flag],address)

      将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。该函数主要用于UDP协议。

    sk.settimeout(timeout)  必会

      设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如 client 连接最多等待5s )

    sk.getpeername()  必会

      返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。

    sk.getsockname()

      返回套接字自己的地址。通常是一个元组(ipaddr,port)

    sk.fileno()

      套接字的文件描述符

    socket.sendfile(fileoffset=0count=None)

      发送文件 ,但目前多数情况下并无什么卵用。

     

    如何使用socket

    基本socket实例

    #!/usr/bin/python3
    
    import socket
    
    server = socket.socket()    #获得socket实例
    
    server.bind(("localhost",9998))    #绑定ip port
    server.listen()    #开始监听
    print("等待客户端的连接...")
    conn,addr = server.accept()    #接受并建立与客户端的连接,程序在此处开始阻塞,直到有客户端连接进来...
    print("新连接:",addr )
    
    data = conn.recv(1024)
    print("收到消息:",data)
    
    server.close()
    socket_server.py
    #!/usr/bin/python3
    
    import socket
    
    client = socket.socket()
    client.connect(("localhost",9998))
    
    client.send(b"hey")
    
    client.close()
    socket_client.py

    上面的代码的有一个问题, 就是SocketServer.py运行起来后, 接收了一次客户端的data就退出了。。。, 但实际场景中,一个连接建立起来后,可能要进行多次往返的通信。

    多次的数据交互实现

    #!/usr/bin/python3
    
    import socket
    
    server = socket.socket() #获得socket实例
    server.bind(("localhost",9998)) #绑定ip port
    server.listen()  #开始监听
    print("等待客户端的连接...")
    conn,addr = server.accept() #接受并建立与客户端的连接,程序在此处开始阻塞,直到有客户端连接进来...
    print("新连接:",addr )
    
    while True:
        data = conn.recv(1024)
    
        print("收到消息:",data)
        conn.send(data.upper())
    
    server.close()
    服务端支持多次交互
    #!/usr/bin/python3
    
    import socket
    
    client = socket.socket()
    client.connect(("localhost",9998))
    
    while True:
        msg = input(">>:").strip()
        if len(msg) == 0:continue
        client.send( msg.encode("utf-8") )
        data = client.recv(1024)
        print("来自服务器:",data)
    
    client.close()
    客户端支持多次交互

    实现了多次交互,但你会发现一个小问题, 就是客户端一断开,服务器端就进入了死循环,为啥呢?

    看客户端断开时服务器端的输出

    等待客户端的连接...
    新连接: ('127.0.0.1', 62262)
    收到消息: b'hey'
    收到消息: b'you'
    收到消息: b''  #客户端一断开,服务器端就收不到数据了,但是不会报错,就进入了死循环模式。。。
    收到消息: b''
    收到消息: b''
    收到消息: b''
    收到消息: b''

    只需要加个判断服务器接到的数据是否为空就好了,为空就代表断了。。。

    #!/usr/bin/python3
    
    import socket
    
    server = socket.socket() #获得socket实例
    
    server.bind(("localhost",9998)) #绑定ip port
    server.listen()  #开始监听
    print("等待客户端的连接...")
    conn,addr = server.accept() #接受并建立与客户端的连接,程序在此处开始阻塞,直到有客户端连接进来...
    print("新连接:",addr )
    
    while True:
    
        data = conn.recv(1024)
        if not data:
            print("客户端断了...")
            break
        print("收到消息:",data)
        conn.send(data.upper())
    
    server.close()

    socket实现多连接处理

    上面的代码虽然实现了服务端与客户端的多次交互,但是你会发现,如果客户端断开了, 服务器端也会跟着立刻断开,因为服务器只有一个while 循环,客户端一断开,服务端收不到数据 ,就会直接break跳出循环,然后程序就退出了,这显然不是我们想要的结果 。实现多连接处理的代码如下所示:

    #!/usr/bin/python3
    
    import socket
    
    server = socket.socket() #获得socket实例
    
    server.bind(("localhost",9998)) #绑定ip port
    server.listen()  #开始监听
    while True: #第一层loop
        print("等待客户端的连接...")
        conn,addr = server.accept() #接受并建立与客户端的连接,程序在此处开始阻塞,直到有客户端连接进来...
        print("新连接:",addr )
    
        while True:
    
            data = conn.recv(1024)
            if not data:
                print("客户端断了...")
                break   #这里断开就会再次回到第一次外层的loop
            print("收到消息:",data)
            conn.send(data.upper())
    
    server.close()

    注意了, 此时服务器端依然只能同时为一个客户服务,其它客户来了,得排队(连接挂起)。

    通过socket实现简单的ssh

    注意注意注意:

    res = subprocess.Popen(cmd.decode('utf-8'),
    shell = True,
    stderr = subprocess.PIPE,
    stdout = subprocess.PIPE)

    的结果的编码是以当前所在的系统为准的,如果是windows,那么res.stdout.read()读出的就是GBK编码的,在接收端需要用GBK解码

    且只能从管道里读一次结果

    注意:命令ls -l ; lllllll ; pwd 的结果是既有正确stdout结果,又有错误stderr结果

    #!/usr/bin/python3
    #_*_coding:utf-8_*_
    #__author__ = 'Tengbibo'
    
    import socket
    import subprocess
    
    ip_port = ('127.0.0.1',6969)
    BUFSIZE = 1024
    
    tcp_socket_server = socket.socket()
    tcp_socket_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
    tcp_socket_server.bind(ip_port)
    tcp_socket_server.listen(5)
    
    while True:
        conn,addr = tcp_socket_server.accept()
        print('connect from:',addr)
    
        while True:
            cmd = conn.recv(BUFSIZE)
            if len(cmd) == 0:break
    
            res = subprocess.Popen(cmd.decode('utf-8'),shell = True,
                             stdout = subprocess.PIPE,
                             stdin = subprocess.PIPE,
                             stderr = subprocess.PIPE)
    
            stderr = res.stderr.read()
            stdout = res.stdout.read()
            conn.send(stderr)
            conn.send(stdout)
    
    tcp_socket_server.close()
    socket ssh server
    #!/usr/bin/python3
    #_*_coding:utf-8_*_
    #__author__ = 'Tengbibo'
    
    import socket
    BUFSIZE=1024
    ip_port=('127.0.0.1',6969)
    
    client = socket.socket()
    client.connect(ip_port)
    
    while True:
        cmd = input('>>: ').strip()
        if len(cmd) == 0:continue
        if cmd == 'quit':break
    
        client.send(cmd.encode('utf-8'))
        act_res=client.recv(BUFSIZE)
    
        print(act_res.decode('utf-8'),end='')
    
    client.close()
    socket ssh client

    client 端输出结果如下:

    上述程序是基于tcp的socket,在运行时会发生粘包

    什么是粘包

    须知:只有TCP有粘包现象,UDP永远不会粘包。

    首先需要掌握一个socket收发消息的原理

                      

    发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。

    例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束

    所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。

    此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

    1. TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
    2. UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
    3. tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头,实验略

    udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠

    tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。

    两种情况下会发生粘包。

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

    #!/usr/bin/python3
    #_*_coding:utf-8_*_
    #__author__ = 'Tengbibo'
    
    import socket
    
    ip_port = ('127.0.0.1',6969)
    BUFSIZE = 1024
    
    tcp_socket_server = socket.socket()
    tcp_socket_server.bind(ip_port)
    tcp_socket_server.listen(5)
    
    conn,addr = tcp_socket_server.accept()
    
    data1 = conn.recv(10)
    data2 = conn.recv(10)
    
    print('----->',data1.decode('utf-8'))
    print('----->',data2.decode('utf-8'))
    
    conn.close()
    服务端
    #!/usr/bin/python3
    #_*_coding:utf-8_*_
    #__author__ = 'Tengbibo'
    
    import socket
    
    ip_port = ('127.0.0.1',6969)
    BUFSIZE = 1024
    
    client = socket.socket()
    res = client.connect(ip_port)
    
    client.send('hello'.encode('utf-8'))
    client.send('world'.encode('utf-8'))
    客户端

    接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)

    #!/usr/bin/python3
    #_*_coding:utf-8_*_
    #__author__ = 'Tengbibo'
    
    import socket
    
    ip_port = ('127.0.0.1',6969)
    BUFSIZE = 1024
    
    tcp_socket_server = socket.socket()
    tcp_socket_server.bind(ip_port)
    tcp_socket_server.listen(5)
    
    conn,addr = tcp_socket_server.accept()
    
    data1 = conn.recv(2)    #一次没有收完整
    data2 = conn.recv(10)   #下次收的时候,会先取旧的数据,然后取新的
    
    print('----->',data1.decode('utf-8'))
    print('----->',data2.decode('utf-8'))
    
    conn.close()
    服务端
    #!/usr/bin/python3
    #_*_coding:utf-8_*_
    #__author__ = 'Tengbibo'
    
    import socket
    
    ip_port = ('127.0.0.1',6969)
    BUFSIZE = 1024
    
    client = socket.socket()
    res = client.connect(ip_port)
    
    client.send('hello wolrd'.encode('utf-8'))
    客户端

    拆包的发生情况

    当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。

    补充问题一:为何tcp是可靠传输,udp是不可靠传输

    tcp在数据传输时,发送端先把数据发送到自己的缓存中,然后协议控制将缓存中的数据发往对端,对端返回一个ack=1,发送端则清理缓存中的数据,对端返回ack=0,则重新发送数据,所以tcp是可靠的

    而udp发送数据,对端是不会返回确认信息的,因此不可靠

    补充问题二:send(字节流)和recv(1024)及sendall

    recv里指定的1024意思是从缓存里一次拿出1024个字节的数据

    send的字节流是先放入己端缓存,然后由协议控制将缓存内容发往对端,如果待发送的字节流大小大于缓存剩余空间,那么数据丢失,用sendall就会循环调用send,数据不会丢失

    解决粘包的处理方法

    为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据

    struct模块 

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

    >>> struct.pack('i',1111111111111)

    。。。。。。。。。

    struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #这个是范围

                        

     

    #_*_coding:utf-8_*_
    #参考http://www.cnblogs.com/coser/archive/2011/12/17/2291160.html
    
    import struct
    import binascii
    import ctypes
    
    values1 = (1, 'abc'.encode('utf-8'), 2.7)
    values2 = ('defg'.encode('utf-8'),101)
    s1 = struct.Struct('I3sf')
    s2 = struct.Struct('4sI')
    
    print(s1.size,s2.size)
    prebuffer=ctypes.create_string_buffer(s1.size+s2.size)
    print('Before : ',binascii.hexlify(prebuffer))
    # t=binascii.hexlify('asdfaf'.encode('utf-8'))
    # print(t)
    
    
    s1.pack_into(prebuffer,0,*values1)
    s2.pack_into(prebuffer,s1.size,*values2)
    
    print('After pack',binascii.hexlify(prebuffer))
    print(s1.unpack_from(prebuffer,0))
    print(s2.unpack_from(prebuffer,s1.size))
    
    s3=struct.Struct('ii')
    s3.pack_into(prebuffer,0,123,123)
    print('After pack',binascii.hexlify(prebuffer))
    print(s3.unpack_from(prebuffer,0))
    关于struct的详细用法

    我们可以把报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节(4个自己足够用了)

    发送时:

    先发报头长度

    再编码报头内容然后发送

    最后发真实内容

    接收时:

    先收报头长度,用struct取出来

    根据取出的长度收取报头内容,然后解码,反序列化

    从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容

    #!/usr/bin/python3
    #_*_coding:utf-8_*_
    #__author__ = 'Tengbibo'
    
    import json
    import socket
    import struct
    import subprocess
    
    ip_port = ('127.0.0.1',6969)
    BUFSIZE = 1024
    
    tcp_socket_server = socket.socket()
    tcp_socket_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
    tcp_socket_server.bind(ip_port)
    tcp_socket_server.listen(5)
    
    while True:
        conn, addr = tcp_socket_server.accept()
        print('connect from:', addr)
    
        while True:
            cmd = conn.recv(BUFSIZE)
            if not cmd:break
            res = subprocess.Popen(cmd.decode('utf-8'), shell = True,
                                   stdout = subprocess.PIPE,
                                   stdin = subprocess.PIPE,
                                   stderr = subprocess.PIPE)
            stdout = res.stdout.read()
            stderr = res.stderr.read()
    
            # 为避免粘包,必须自定制报头
            header_dict = {
                'total_size': len(stdout) + len(stderr)
            }
            # 为了该报头能传送,需要序列化并且转为bytes
            header_bytes = json.dumps(header_dict).encode('utf-8')
    
            # 为了让客户端知道报头的长度,用struct将报头长度这个数字转成固定长度:4个字节
            header_len = struct.pack('i', len(header_bytes))    #这4个字节里只包含了一个数字,该数字是报头的长度
    
            # 服务端开始发送
            conn.send(header_len)   #先发送报头的长度,4个bytes
            conn.send(header_bytes)     #再发送报头的字节格式
            conn.send(stdout)       #最后发送真实内容的字节格式
            conn.send(stderr)
    
    tcp_socket_server.close()
    服务端:定制稍微复杂一点的报头
    #!/usr/bin/python3
    #_*_coding:utf-8_*_
    #__author__ = 'Tengbibo'
    
    import json
    import socket
    import struct
    
    ip_port = ('127.0.0.1',6969)
    
    client = socket.socket()
    client.connect(ip_port)
    
    while True:
        cmd = input('请输入>>:').strip()
        if not cmd: continue
        client.send(cmd.encode('utf-8'))
    
        # 客户端开始接收
        res = client.recv(4)    #先收报头4个bytes,得到报头长度的字节格式
        header_size = struct.unpack('i', res)[0]    #对报头长度解压,提取报头的长度
        header_bytes = client.recv(header_size)     #按照报头长度header_size,收取报头的bytes格式
        header_dic = json.loads(header_bytes.decode('utf-8'))      #对报头字典进行反序列化,提取报头
        data_size = header_dic['total_size']       #最后根据报头的内容提取真实数据的长度
    
        # 根据报头数据长度对数据进行接收
        recv_size = 0
        total_data = b''
    
        while recv_size < data_size:
            if data_size - recv_size > 1024:
                BUFSIZE = 1024
            else:
                BUFSIZE = data_size - recv_size
            data_recv = client.recv(BUFSIZE)
            total_data += data_recv
            recv_size += len(data_recv)
    
        print(total_data.decode('gbk'))     #windows默认gbk编码
    
    client.close()
    客户端

    实现大文件传送

    #!/usr/bin/python3
    #_*_coding:utf-8_*_
    #__author__ = 'Tengbibo'
    
    import os
    import json
    import socket
    import struct
    import hashlib
    
    ip_port = ('127.0.0.1',8989)
    BUFSIZE = 1024
    
    tcp_socket_server = socket.socket()
    tcp_socket_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
    tcp_socket_server.bind(ip_port)
    tcp_socket_server.listen(5)
    
    while True:
        conn, addr = tcp_socket_server.accept()
        print('connect from:', addr)
    
        while True:
            cmd = conn.recv(BUFSIZE)
            if not cmd:break
    
            filename = '/opt/p13390677_112040_Linux-x86-64_1of7.zip'
    
            filesize = os.path.getsize(filename)
            header_dict = {
                'file_size':filesize
            }
    
            header_bytes = json.dumps(header_dict).encode('utf-8')
            header_len = struct.pack('i', len(header_bytes))
            conn.send(header_len)
            conn.send(header_bytes)
    
            f = open(filename,'rb')
            m = hashlib.sha256()
            for line in f:
                m.update(line)
                conn.send(line)
            print('file sha256',m.hexdigest())
            f.close()
            conn.send(m.hexdigest().encode())
    
    tcp_socket_server.close()
    socket server
    #!/usr/bin/python3
    #_*_coding:utf-8_*_
    #__author__ = 'Tengbibo'
    
    import json
    import socket
    import struct
    import hashlib
    
    ip_port = ('127.0.0.1',8989)
    
    client = socket.socket()
    client.connect(ip_port)
    
    while True:
        cmd = input('please input>>:').strip()
        if not cmd: continue
        client.send(cmd.encode('utf-8'))
    
        res = client.recv(4)
        header_size = struct.unpack('i', res)[0]
        header_bytes = client.recv(header_size)
        header_dic = json.loads(header_bytes.decode('utf-8'))
        file_total_size = header_dic['file_size']
    
        recv_size = 0
    
        f = open('test.tt','wb')
        m = hashlib.sha256()
        while recv_size < file_total_size:
            if file_total_size - recv_size > 1024:
                BUFSIZE = 1024
            else:
                BUFSIZE = file_total_size - recv_size
            data_recv = client.recv(BUFSIZE)
            m.update(data_recv)
            f.write(data_recv)
            recv_size += len(data_recv)
        else:
            new_file_sha256 = m.hexdigest()
            server_file_sha256 = client.recv(1024).decode()
            if new_file_sha256 == server_file_sha256:
                print('file has received done')
    
    client.close()
    socket client

    socketserver实现并发

    socketserver其实是对socket更高级的封装。socketserver模块简化了编写网络服务程序的任务,同时socketserver模块也 是Python标准库中很多服务器框架的基础。

    先来看socketserver源码的整体框架

    socketserver中主要包含了两种类,一种为服务类(server class),一种为请求处理类(request handle class)。前者提供了许多方法:像绑定,监听,运行…… (也就是建立连接的过程) 后者则专注于如何处理用户所发送的数据(也就是事务逻辑)。

    socketserver还提供了两个MixIn类:ForkingMixIn和ThreadingMixIn,用于扩展server,实现多进程和多线程。

    一般情况下,所有的服务,都是先建立连接,也就是建立一个服务类的实例,然后开始处理用户请求,也就是建立一个请求处理类的实例。

    class BaseServer:
    #我们创建服务类时,需要指定(地址,端口),服务处理类。
        def __init__(self, server_address, RequestHandlerClass):
            """Constructor.  May be extended, do not override."""
            self.server_address = server_address
            self.RequestHandlerClass = RequestHandlerClass
            self.__is_shut_down = threading.Event()
            self.__shutdown_request = False
    #…………此处省略n多代码,当我们执行server_forever方法时,里面就会调用很多服务类中的其他方法,但最终会调用finish_request方法。
    
        def finish_request(self, request, client_address):
            """Finish one request by instantiating RequestHandlerClass."""
            self.RequestHandlerClass(request, client_address, self)
    #finish_request方法中执行了self.RequestHandlerClass(request, client_address, self)。self.RequestHandlerClass是什么呢?
    #self.RequestHandlerClass = RequestHandlerClass(就在__init__方法中)。所以finish_request方法本质上就是创建了一个服务处理实例。
    
    
    class BaseRequestHandler:
        def __init__(self, request, client_address, server):
            self.request = request
            self.client_address = client_address
            self.server = server
            self.setup()
            try:
                self.handle()
            finally:
                self.finish()
    #当我们创建服务处理类实例时,就会运行handle()方法,而handle()方法则一般是我们处理事务逻辑的代码块。
    #…………此处省略n多代码
    分析下源码,来看一看服务类是如何与请求处理类建立联系的。

    先来看服务类:

    • BaseServer  不直接对外服务。
    • TCPServer  针对TCP套接字流
    • UDPServer  针对UDP数据报套接字
    • UnixStreamServer和UnixDatagramServer  针对UNIX域套接字,不常用。

     它们之间的继承关系如下图所示:

                

    服务类的方法:

    class SocketServer.BaseServer:这是模块中的所有服务器对象的超类。它定义了接口,如下所述,但是大多数的方法不实现,在子类中进行细化。
    
        BaseServer.fileno():返回服务器监听套接字的整数文件描述符。通常用来传递给select.select(), 以允许一个进程监视多个服务器。
    
        BaseServer.handle_request():处理单个请求。处理顺序:get_request(), verify_request(), process_request()。如果用户提供handle()方法抛出异常,将调用服务器的handle_error()方法。如果self.timeout内没有请求收到, 将调用handle_timeout()并返回handle_request()。
    
        BaseServer.serve_forever(poll_interval=0.5): 处理请求,直到一个明确的shutdown()请求。每poll_interval秒轮询一次shutdown。忽略self.timeout。如果你需要做周期性的任务,建议放置在其他线程。
    
        BaseServer.shutdown():告诉serve_forever()循环停止并等待其停止。python2.6版本。
    
        BaseServer.address_family: 地址家族,比如socket.AF_INET和socket.AF_UNIX。
    
        BaseServer.RequestHandlerClass:用户提供的请求处理类,这个类为每个请求创建实例。
    
        BaseServer.server_address:服务器侦听的地址。格式根据协议家族地址的各不相同,请参阅socket模块的文档。
    
        BaseServer.socketSocket:服务器上侦听传入的请求socket对象的服务器。
    
    服务器类支持下面的类变量:
    
        BaseServer.allow_reuse_address:服务器是否允许地址的重用。默认为false ,并且可在子类中更改。
    
        BaseServer.request_queue_size
    
    请求队列的大小。如果单个请求需要很长的时间来处理,服务器忙时请求被放置到队列中,最多可以放request_queue_size个。一旦队列已满,来自客户端的请求将得到 “Connection denied”错误。默认值通常为5 ,但可以被子类覆盖。
    
        BaseServer.socket_type:服务器使用的套接字类型; socket.SOCK_STREAM和socket.SOCK_DGRAM等。
    
        BaseServer.timeout:超时时间,以秒为单位,或 None表示没有超时。如果handle_request()在timeout内没有收到请求,将调用handle_timeout()。
    
    下面方法可以被子类重载,它们对服务器对象的外部用户没有影响。
    
        BaseServer.finish_request():实际处理RequestHandlerClass发起的请求并调用其handle()方法。 常用。
    
        BaseServer.get_request():接受socket请求,并返回二元组包含要用于与客户端通信的新socket对象,以及客户端的地址。
    
        BaseServer.handle_error(request, client_address):如果RequestHandlerClass的handle()方法抛出异常时调用。默认操作是打印traceback到标准输出,并继续处理其他请求。
    
        BaseServer.handle_timeout():超时处理。默认对于forking服务器是收集退出的子进程状态,threading服务器则什么都不做。
    
        BaseServer.process_request(request, client_address) :调用finish_request()创建RequestHandlerClass的实例。如果需要,此功能可以创建新的进程或线程来处理请求,ForkingMixIn和ThreadingMixIn类做到这点。常用。
    
        BaseServer.server_activate():通过服务器的构造函数来激活服务器。默认的行为只是监听服务器套接字。可重载。
    
        BaseServer.server_bind():通过服务器的构造函数中调用绑定socket到所需的地址。可重载。
    
        BaseServer.verify_request(request, client_address):返回一个布尔值,如果该值为True ,则该请求将被处理,反之请求将被拒绝。此功能可以重写来实现对服务器的访问控制。默认的实现始终返回True。client_address可以限定客户端,比如只处理指定ip区间的请求。 常用。
    View Code

    这个几个服务类都是同步处理请求的:一个请求没处理完不能处理下一个请求。要想支持异步模型,可以利用多继承让server类继承ForkingMixIn 或 ThreadingMixIn mix-in classes。

    ForkingMixIn利用多进程(分叉)实现异步。

    ThreadingMixIn利用多线程实现异步。

    请求处理类:

    要实现一项服务,还必须派生一个handler class请求处理类,并重写父类的handle()方法。handle方法就是用来专门是处理请求的。该模块是通过服务类和请求处理类组合来处理请求的。

    socketserver模块提供的请求处理类有BaseRequestHandler,以及它的派生类StreamRequestHandler和DatagramRequestHandler。从名字看出可以一个处理流式套接字,一个处理数据报套接字。

    请求处理类有三种方法:

    setup()

    Called before the handle() method to perform any initialization actions required. The default implementation does nothing.

    也就是在handle()之前被调用,主要的作用就是执行处理请求之前的初始化相关的各种工作。默认不会做任何事。(如果想要让其做一些事的话,就要程序员在自己的请求处理器中覆盖这个方法(因为一般自定义的请求处理器都要继承python中提供的BaseRequestHandler,ps:下文会提到的),然后往里面添加东西即可)

    handle()

    This function must do all the work required to service a request. The default implementation does nothing. Several instance attributes are available to it; the request is available as self.request; the client address as self.client_address; and the server instance as self.server, in case it needs access to per-server information.

    The type of self.request is different for datagram or stream services. For stream services,self.request is a socket object; for datagram services, self.request is a pair of string and socket.

    handle()的工作就是做那些所有与处理请求相关的工作。默认也不会做任何事。他有数个实例参数:self.request    self.client_address   self.server

    finish()

    Called after the handle() method to perform any clean-up actions required. The default implementation does nothing. If setup() raises an exception, this function will not be called.

    在handle()方法之后会被调用,他的作用就是执行当处理完请求后的清理工作,默认不会做任何事

    class BaseRequestHandler:
    
        """Base class for request handler classes.
    
        This class is instantiated for each request to be handled.  The
        constructor sets the instance variables request, client_address
        and server, and then calls the handle() method.  To implement a
        specific service, all you need to do is to derive a class which
        defines a handle() method.
    
        The handle() method can find the request as self.request, the
        client address as self.client_address, and the server (in case it
        needs access to per-server information) as self.server.  Since a
        separate instance is created for each request, the handle() method
        can define other arbitrary instance variables.
    
        """
    
        def __init__(self, request, client_address, server):
            self.request = request
            self.client_address = client_address
            self.server = server
            self.setup()
            try:
                self.handle()
            finally:
                self.finish()
    
        def setup(self):
            pass
    
        def handle(self):
            pass
    
        def finish(self):
            pass
    
    
    # The following two classes make it possible to use the same service
    # class for stream or datagram servers.
    # Each class sets up these instance variables:
    # - rfile: a file object from which receives the request is read
    # - wfile: a file object to which the reply is written
    # When the handle() method returns, wfile is flushed properly
    
    
    class StreamRequestHandler(BaseRequestHandler):
    
        """Define self.rfile and self.wfile for stream sockets."""
    
        # Default buffer sizes for rfile, wfile.
        # We default rfile to buffered because otherwise it could be
        # really slow for large data (a getc() call per byte); we make
        # wfile unbuffered because (a) often after a write() we want to
        # read and we need to flush the line; (b) big writes to unbuffered
        # files are typically optimized by stdio even when big reads
        # aren't.
        rbufsize = -1
        wbufsize = 0
    
        # A timeout to apply to the request socket, if not None.
        timeout = None
    
        # Disable nagle algorithm for this socket, if True.
        # Use only when wbufsize != 0, to avoid small packets.
        disable_nagle_algorithm = False
    
        def setup(self):
            self.connection = self.request
            if self.timeout is not None:
                self.connection.settimeout(self.timeout)
            if self.disable_nagle_algorithm:
                self.connection.setsockopt(socket.IPPROTO_TCP,
                                           socket.TCP_NODELAY, True)
            self.rfile = self.connection.makefile('rb', self.rbufsize)
            if self.wbufsize == 0:
                self.wfile = _SocketWriter(self.connection)
            else:
                self.wfile = self.connection.makefile('wb', self.wbufsize)
    
        def finish(self):
            if not self.wfile.closed:
                try:
                    self.wfile.flush()
                except socket.error:
                    # A final socket error may have occurred here, such as
                    # the local error ECONNABORTED.
                    pass
            self.wfile.close()
            self.rfile.close()
    
    
    class DatagramRequestHandler(BaseRequestHandler):
    
        """Define self.rfile and self.wfile for datagram sockets."""
    
        def setup(self):
            from io import BytesIO
            self.packet, self.socket = self.request
            self.rfile = BytesIO(self.packet)
            self.wfile = BytesIO()
    
        def finish(self):
            self.socket.sendto(self.wfile.getvalue(), self.client_address)
    Handler源码

    从源码中可以看出,BaseRequestHandler中的setup()/handle()/finish()什么内容都没有定义,而他的两个派生类StreamRequestHandler和DatagramRequestHandler则都重写了setup()/finish()。

    因此当我们需要自己编写socketserver程序时,只需要合理选择StreamRequestHandler和DatagramRequestHandler之中的一个作为父类,然后自定义一个请求处理类,并在其中重写handle()方法即可。

    用socketserver创建一个服务至少分以下几步:

    1. 创建一个request handler class(请求处理类),合理选择StreamRequestHandler和DatagramRequestHandler之中的一个作为父类(当然,使用BaseRequestHandler作为父类也可),并重写它的handle()方法。
    2. 实例化一个server class对象,并将服务的地址和之前创建的request handler class传递给它。
    3. 调用server class对象的handle_request() 或 serve_forever()方法来处理一个或多个请求。
    4. 最后,调用server_close()方法来关闭套接字

    基本socketserver实例

    #!/usr/bin/python3
    #_*_coding:utf-8_*_
    #__author__ = 'Tengbibo'
    
    
    import socketserver
    
    class MyTCPHandler(socketserver.BaseRequestHandler):
        """
        The request handler class for our server.
    
        It is instantiated once per connection to the server, and must
        override the handle() method to implement communication to the
        client.
        """
    
        def handle(self):
            print("{} wrote:".format(self.client_address[0]))
            while True:
                # self.request is the TCP socket connected to the client
                self.data = self.request.recv(1024).strip()
                if not self.data:
                    print('客户端断开了')
                    break
                print(self.data)
                # just send back the same data, but upper-cased
                self.request.sendall(self.data.upper())
    
    if __name__ == "__main__":
        HOST, PORT = "localhost", 9999
    
        # Create the server, binding to localhost on port 9999
        server = socketserver.TCPServer((HOST, PORT), MyTCPHandler)
    
        # Activate the server; this will keep running until you
        # interrupt the program with Ctrl-C
        server.serve_forever()
    服务器端
    #!/usr/bin/python3
    #_*_coding:utf-8_*_
    #__author__ = 'Tengbibo'
    
    import socket
    
    client = socket.socket()
    client.connect(('localhost',9999))
    
    while True:
        msg = input('>>: ').strip()
        if not msg:continue
        client.send(msg.encode('utf-8'))
        data = client.recv(1024)
        print('recv: ',data.decode())
    
    client.close()
    客户端

    有个问题,上面的代码,依然不能同时处理多个连接。这是因为没有用到多进程或多线程。

    要让socketserver并发起来, 必须选择使用以下一个多并发的类

    • class socketserver.ForkingTCPServer
    • class socketserver.ForkingUDPServer
    • class socketserver.ThreadingTCPServer
    • class socketserver.ThreadingUDPServer

    所以只需要把下面这句

    server = socketserver.TCPServer((HOST, PORT), MyTCPHandler)

    换成下面这个,就可以多并发了,这样,客户端每连进一个来,服务器端就会分配一个新的线程来处理这个客户端的请求

    server = socketserver.ThreadingTCPServer((HOST, PORT), MyTCPHandler)
  • 相关阅读:
    【hdu4057】 恨7不成妻
    【hdu3709】 Balanced Number
    【hdu3555】 Bomb
    【hdu2809】 不要62
    【bzoj3992】 SDOI2015—序列统计
    【uoj125】 NOI2013—书法家
    【bzoj1833】 ZJOI2010—count 数字计数
    【bzoj1026】 SCOI2009—windy数
    【bzoj2780】 Sevenk Love Oimaster
    【bzoj3930】 CQOI2015—选数
  • 原文地址:https://www.cnblogs.com/cyfiy/p/9195414.html
Copyright © 2020-2023  润新知