• python---基础知识回顾(六)网络编程


    python---基础知识回顾(十)进程和线程(进程)

    python---基础知识回顾(十)进程和线程(多线程)

    python---基础知识回顾(十)进程和线程(自定义线程池)

    一:Socket

    (一)套接字了解

    网络套接字,用于描述IP地址和端口,是一个通信链句柄,一般,应用程序通过对这个句柄进行读写操作,可以对网络发出或者应答网络请求。也是一种特殊的文件(起源Unix,一切皆是文件)

    socket主要支持两种类型的套接字,流套接字和数据报套接字。(其实还有其他的)

    SOCK_STREAM :流套接字(TCP)提供了双向有序且不重复的数据服务。

    SOCK_DGRAM:数据报套接字(UDP)虽然支持双向的数据流,但是对报文的有序性和可靠性不保证。

    套接字家族也主要使用两种,基于文件和基于网络类型的套接字家族

    基于文件类型的套接字家族:AF_UNIX

    调用的就是底层的文件系统来取数据,两个套接字进程运行在统一机器,可以通过访问同一个文件系统间接完成通信

    基于网络类型的套接字家族:AF_INET

    所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET

    (二)socket工作方式(TCP)

    套接字在工作时将连接的双方分为服务器端和客户端,C/S模式的由来。

    import socket
    
    ip_port = ('127.0.0.1',8080)    #用于绑定本机地址和端口
    
    sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)   #里面的参数是默认的,可以不写
    sk.bind(ip_port)    #绑定连接地址和端口
    
    sk.listen(5)    #设置监听数量
    
    while True:
        print("Server waiting...")
        conn,addr = sk.accept() #返回客户端套接字和地址
        print("Connect from",addr)
    
        client_data = conn.recv(1024)   #从客户端套接字中读取1024字节数据
        print("Recv:%s"%client_data.decode("utf-8"))
        conn.sendall("接收到你的数据".encode("utf-8"))
    
        conn.close()    #关闭客户端套接字的连接
    sk.close()
    socket server(注意网络中数据传递是字节型传递,记得解码和编码
    import socket
    
    ip_port = ("127.0.0.1",8080)    #用于绑定服务器端的地址和端口
    
    sk = socket.socket()
    sk.connect(ip_port) #与服务器连接
    
    sk.sendall("我开始发送数据了".encode("utf-8"))  #向服务器端发送数据
    
    ser_data = sk.recv(1024)    #从服务器端获取数据
    
    print(ser_data.decode("utf-8"))
    
    sk.close()
    socket client实现
    sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)   #里面的参数是默认的,可以不写
    sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)  #设置地址复用
    sk.bind(ip_port)    #绑定连接地址和端口
    使用setsockopt设置地址复用,防止在与客户端断开连接时,地址依旧被占用

    相关套接字函数:

        _accept() -- accept connection, returning new socket fd and client address
        bind(addr) -- bind the socket to a local address
        close() -- close the socket
        connect(addr) -- connect the socket to a remote address
        connect_ex(addr) -- connect, return an error code instead of an exception
        dup() -- return a new socket fd duplicated from fileno()
        fileno() -- return underlying file descriptor
        getpeername() -- return remote address [*]
        getsockname() -- return local address
        getsockopt(level, optname[, buflen]) -- get socket options
        gettimeout() -- return timeout or None
        listen([n]) -- start listening for incoming connections
        recv(buflen[, flags]) -- receive data
        recv_into(buffer[, nbytes[, flags]]) -- receive data (into a buffer)
        recvfrom(buflen[, flags]) -- receive data and sender's address
        recvfrom_into(buffer[, nbytes, [, flags])
          -- receive data and sender's address (into a buffer)
        sendall(data[, flags]) -- send all data
        send(data[, flags]) -- send data, may not send all of it
        sendto(data[, flags], addr) -- send data to a given address
        setblocking(0 | 1) -- set or clear the blocking I/O flag
        setsockopt(level, optname, value) -- set socket options
        settimeout(None | float) -- set or clear the timeout
        shutdown(how) -- shut down traffic in one or both directions
        if_nameindex() -- return all network interface indices and names
        if_nametoindex(name) -- return the corresponding interface index
        if_indextoname(index) -- return the corresponding interface name
        
    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()        创建一个与该套接字相关的文件
    面向文件的套接字的函数

    (三)socket工作方式(UDP)

     udp是无连接的,先启动哪一端都不会报错(TCP必须先启动服务端)

     udp使用的socket方法:recvfrom,sendto

    与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。
    sk.recvfrom(bufsize[.flag])
    将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。该函数主要用于UDP协议。
    sk.sendto(string[,flag],address)
    import socket
    
    ip_port = ("127.0.0.1",8080)
    
    sk = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)    #数据包类型的套接字UDP
    
    sk.bind(ip_port)
    
    while True:
        msg, addr = sk.recvfrom(1024)   #返回客户端地址和接收的信息
        print(msg.decode("utf-8"),"from",addr)
        sk.sendto("hahahhaha".encode("utf-8"),addr) #回复给客户端,方法sendto带有客户端地址
    
    sk.close()
    UDP服务端
    import socket
    
    ip_port = ("127.0.0.1",8080)    #用于绑定服务器端的地址和端口
    
    sk = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
    
    while True:
        msg = input(">>>:").strip()
        if not msg:
            continue
        sk.sendto(msg.encode("utf-8"),ip_port)
    
        back_msg, addr = sk.recvfrom(1024)
        print(back_msg.decode("utf-8"),"form",addr)
    
    sk.close()
    UDP客户端

    时间服务器:

    import socket,time
    
    ip_port = ("127.0.0.1",8080)
    
    sk = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)    #数据包类型的套接字UDP
    
    sk.bind(ip_port)
    
    while True:
        msg, addr = sk.recvfrom(1024)   #返回客户端地址<是一个元组,包括ip和port>和接收的信息
        print(msg.decode("utf-8"),"from",addr)
    
        if not msg:
            time_fmt = "%Y-%m-%d %X"
        else:
            time_fmt = msg.decode("utf-8")
    
        time_data = time.strftime(time_fmt)
    
        sk.sendto(time_data.encode("utf-8"),addr) #回复给客户端,方法sendto带有客户端地址
    
    sk.close()
    时间服务器
    import socket
    
    ip_port = ("127.0.0.1",8080)    #用于绑定服务器端的地址和端口
    
    sk = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
    
    while True:
        msg = input("请输入时间格式(例如%Y-%m-%d)>>>:").strip()
    
        sk.sendto(msg.encode("utf-8"),ip_port)
    
        back_msg, addr = sk.recvfrom(1024)
        print(back_msg.decode("utf-8"),"form",addr)
    
    sk.close()
    客户端

    二:异步通信方式 

    上面的实例中所有的服务器实现都是同步的,也就是说,服务器只有在处理完一个连接后,才能处理另外一个连接。如果要是服务器应用程序能够同时处理多个连接,则需要使用异步通信方式。这里介绍3中方式Fork方式,线程方式,和异步I/O方式。

    上面的3中方式前两种基于SocketServer模块。

    (零)SocketServer了解

    SocketServer内部使用 IO多路复用 以及 “多线程” 和 “多进程” ,从而实现并发处理多个客户端请求的Socket服务端。即:每个客户端请求连接到服务器时,Socket服务端都会在服务器是创建一个“线程”或者“进程” 专门负责处理当前客户端的所有请求。

    SocketServer模块主要分为两大类:server类(解决连接问题)和request类(解决通信问题)

    Server类(解决连接问题)

    class BaseServer
    
        class TCPServer(BaseServer)
    
            class UnixStreamServer(TCPServer)
    
        class UDPServer(TCPServer)
    
            class UnixDatagramServer(UDPServer)
    与连接有关的类

    class ForkingMixIn
    
        class ForkingUDPServer(ForkingMixIn, UDPServer)
    
        class ForkingTCPServer(ForkingMixIn, TCPServer)
    多进程实现并发

    class ThreadingMixIn
    
        class ThreadingUDPServer(ThreadingMixIn, UDPServer)
    
        class ThreadingTCPServer(ThreadingMixIn, TCPServer)
    多线程实现并发

    request类(解决通信问题)

    class BaseRequestHandler
    
        class StreamRequestHandler(BaseRequestHandler)
    
        class DatagramRequestHandler(BaseRequestHandler)
    与通信有关的类

    (一)使用Fork方式(linux下)

    对于接受到的每一个连接,主进程都生成一个子进程去专门的原来处理此连接,而主进程任然保持在侦听状态。这样对于每个连接,都有一个对应的子进程来处理。由于生成的子进程和主进程是同时运行的,所以不会阻塞新的连接。这种方式的好处是处理比较简单有效。但是由于生成进程消耗的资源比较大,在处理连接和是哪个可能会带来性能问题。(这种处理方式在SocketServer模块中已经实现了)

     服务器端(linux下):

    import socketserver
    import time
    
    
    class MyHandler(socketserver.StreamRequestHandler):
        def handle(self):   #重载handle函数
            conn = self.request
            conn.sendall("欢迎查询时间,请输入时间格式:(例如%Y-%m-%d)".encode("utf-8"))
            while True:
                tm_fmt = conn.recv(1024)
                if not tm_fmt:
                    tm_fmt = "%Y-%m-%d"
                else:
                    tm_fmt = tm_fmt.decode("utf-8")
                time_data = time.strftime(tm_fmt)
                conn.sendall(time_data.encode("utf-8"))
            conn.close()
    
    if __name__ == "__main__":
        server = socketserver.ForkingTCPServer(("0.0.0.0",8080),MyHandler)
        server.serve_forever()

    客户端(windows下)

    import socket
    
    ip_port = ("192.168.218.128",8080)    #用于绑定服务器端的地址和端口
    
    sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    
    sk.connect(ip_port)
    
    back_msg = sk.recv(1024)
    print(back_msg.decode("utf-8"))
    
    while True:
        msg = input("请输入时间格式(例如%Y-%m-%d)>>>:").strip()
    
        sk.sendall(msg.encode("utf-8"))
    
        back_msg = sk.recv(1024)
    
        print(back_msg.decode("utf-8"),"form")
    
    sk.close()

     本质

    基于os.fork和select其实本质上就是在服务器端为每一个客户端创建一个进程,当前新创建的进程用来处理对应客户端的请求,所以,可以支持同时n个客户端链接(长连接)。

    补充:

    server = socketserver.ForkingTCPServer(("0.0.0.0",8080),MyHandler)
    
    0.0.0.0和127.0.0.1
    此处我们若是使用127.0.0.1会错:
    ConnectionRefusedError: [WinError 10061] 由于目标计算机积极拒绝,无法连接。
    一、0.0.0.0
    
    严格说来,0.0.0.0已经不是一个真正意义上的IP地址了。它表示的是这样一个集合:所有不清楚的主机和目的网络。这里的“不清楚”是指在本机的路由表里没有特定条目指明如何到达。对本机来说,它就是一个“收容所”,所有不认识的“三无”人员,一律送进去。如果你在网络设置中设置了缺省网关,那么Windows系统会自动产生一个目的地址为0.0.0.0的缺省路由。
    
    二、255.255.255.255
    
    限制广播地址。对本机来说,这个地址指本网段内(同一广播域)的所有主机。如果翻译成人类的语言,应该是这样:“这个房间里的所有人都注意了!”这个地址不能被路由器转发。
    
    三、127.0.0.1
    
    本机地址,主要用于测试。用汉语表示,就是“我自己”。在Windows系统中,这个地址有一个别名“Localhost”。寻址这样一个地址,是不能把它发到网络接口的。除非出错,否则在传输介质上永远不应该出现目的地址为“127.0.0.1”的数据包。
    0.0.0.0和255.255.255.255和127.0.0.1
    0.0.0.0是本机所有的IP集合。
    127.0.0.1是本机环回地址,在两个pc之间,根本没有用
    我们可以使用
    netstat -a -n查看监听状态,发现所有127.0.0.1:端口是我们无法去在其他主机上进行telnet连接,但是以0.0.0.0是可以的

    (二)使用线程方式

    import socketserver
    import time
    
    
    class MyHandler(socketserver.StreamRequestHandler):
        def handle(self):   #重载handle函数
            conn = self.request
            conn.sendall("欢迎查询时间,请输入时间格式:(例如%Y-%m-%d)".encode("utf-8"))
            while True:
                tm_fmt = conn.recv(1024)
                if not tm_fmt:
                    tm_fmt = "%Y-%m-%d"
                else:
                    tm_fmt = tm_fmt.decode("utf-8")
                time_data = time.strftime(tm_fmt)
                conn.sendall(time_data.encode("utf-8"))
            conn.close()
    
    if __name__ == "__main__":
        server = socketserver.ThreadingTCPServer(("0.0.0.0",8080),MyHandler)
        server.serve_forever()
    ThreadingTCPServer的使用(服务器端)
    import socket
    
    ip_port = ("127.0.0.1",8080)    #用于绑定服务器端的地址和端口
    
    sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    
    sk.connect(ip_port)
    
    back_msg = sk.recv(1024)
    print(back_msg.decode("utf-8"))
    
    while True:
        msg = input("请输入时间格式(例如%Y-%m-%d)>>>:").strip()
    
        sk.sendall(msg.encode("utf-8"))
    
        back_msg = sk.recv(1024)
    
        print(back_msg.decode("utf-8"),"form")
    
    sk.close()
    客户端
    基于select 和 Threading 两个东西,其实本质上就是在服务器端为每一个客户端创建一个线程,当前线程用来处理对应客户端的请求,所以,可以支持同时n个客户端链接(长连接)。

    (三)IO多路复用

    Linux中的 select,poll,epoll 都是IO多路复用的机制。

    select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。
    select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,事实上从现在看来,这也是它所剩不多的优点之一。
    select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。
    另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。
    select(跨平台,但是现在文件描述符1024个)
    poll在1986年诞生于System V Release 3,它和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制。
    poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。
    另外,select()和poll()将就绪的文件描述符告诉进程后,如果进程没有对其进行IO操作,那么下次调用select()和poll()的时候将再次报告这些文件描述符,所以它们一般不会丢失就绪的消息,这种方式称为水平触发(Level Triggered)。
    poll(没有最大文件描述符限制,缺点:在linux下可用,文件描述符会整体复制,开销增大<select也有这个缺点>)
    直到Linux2.6才出现了由内核直接支持的实现方法,那就是epoll,它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。
    epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。
    epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。
    另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。
    epoll缺点是只在linux下可用,其优点很多(不限制最大文件描述符,epoll采用基于事件的就绪通知方式)

    1.非阻塞IO的使用(setblocking)<了解即可>:

    import socket,time
    
    ip_port = ('127.0.0.1',8080)    #用于绑定本机地址和端口
    
    sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)   #里面的参数是默认的,可以不写
    sk.setblocking(False)
    sk.bind(ip_port)    #绑定连接地址和端口
    
    sk.listen(5)    #设置监听数量
    
    while True:
        print("Server waiting...")
        try:
            time.sleep(5)
            conn, addr = sk.accept()  # 返回客户端套接字和地址
            conn.setblocking(False)
            print("Connect from", addr)
            while True:
                time.sleep(5)
                client_data = conn.recv(1024)  # 从客户端套接字中读取1024字节数据
                print("Recv:%s" % client_data.decode("utf-8"))
                conn.sendall("接收到你的数据".encode("utf-8"))
            conn.close()  # 关闭客户端套接字的连接
        except Exception as e:
            pass
    服务端setblocking(False)
    import socket
    
    ip_port = ("127.0.0.1",8080)    #用于绑定服务器端的地址和端口
    
    sk = socket.socket()
    sk.connect(ip_port) #与服务器连接
    
    while True:
        data = input(">>>:").strip()
    
        sk.sendall(data.encode("utf-8"))  # 向服务器端发送数据
    
        ser_data = sk.recv(1024)  # 从服务器端获取数据
    
        print(ser_data.decode("utf-8"))
    
    sk.close()
    客户端

    设置后套接字变为非阻塞(服务端客户端都变为非阻塞)

    即:accept,recv方法由原来阻塞状态变为非阻塞

    但是程序处于死循环,还有sleep影响,效若连接的客户端较多,则效率很低。是短连接。

    主要功能:是将原来的阻塞函数设置为了非阻塞。

    2.select的使用(重点)

    select服务端实现:

    import socket,select
    
    ip_port = ('0.0.0.0',8080)    #用于绑定本机地址和端口
    
    sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)   #里面的参数是默认的,可以不写
    sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
    
    sk.bind(ip_port)    #绑定连接地址和端口
    sk.listen(5)    #设置监听数量
    sk.setblocking(False)
    
    inputs = [sk,]  #最开始时是去监听自己是否有连接到来,后面还会去监听新来的连接
    
    while True:
        r_list, w_list,e_list = select.select(inputs,[],inputs,1)
    
        for r in r_list:
            if sk == r: #有新的连接到来,使用accept去接收
                print("New Connection")
                conn, addr = r.accept()
                conn.setblocking(False)
                inputs.append(conn) #放入到可读列表
            else:   #客户端正常的可读到来
                conn_addr,conn_port = r.getpeername()
                client_data = r.recv(1024)  # 从客户端套接字中读取1024字节数据
                if client_data: #有数据,代表正常传输数据
                    print("Recv:%s from %s:%s" % (client_data.decode("utf-8"),conn_addr,conn_port))
                    r.sendall("接收到你的数据".encode("utf-8"))
                else:   #无数据,代表客户端连接断开
                    conn.close()
                    inputs.remove(r)
    
    sk.close()
    import socket
    
    ip_port = ("192.168.218.128",8080)    #用于绑定服务器端的地址和端口
    
    sk = socket.socket()
    sk.connect(ip_port) #与服务器连接
    
    while True:
        data = input(">>>:").strip()
        if not data:
           continue
        sk.sendall(data.encode("utf-8"))  # 向服务器端发送数据
    
        ser_data = sk.recv(1024)  # 从服务器端获取数据
    
        print(ser_data.decode("utf-8"))
    
    sk.close()
    select客户端
    此处的Socket服务端相比与原生的Socket,他支持当某一个请求不再发送数据时,服务器端不会等待而是可以去处理其他请求的数据。但是,如果每个请求的耗时比较长时,select版本的服务器端也无法完成同时操作。

     select实现的socket服务端:

    import socket
    import select
    import queue
    
    ip_port = ("0.0.0.0",8080)
    
    sk = socket.socket()
    sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
    sk.setblocking(0)
    sk.bind(ip_port)
    sk.listen(5)
    inputs = [sk,]  #读
    outputs = []    #写
    
    message_queue = {} #用于存放每个到来的消息
    
    while True:
        r_list, w_list,e_list = select.select(inputs,outputs,inputs)
    
        for r in r_list:
            if r == sk:
                conn,addr = r.accept()
                print("New Connection from %s:%s"%(addr[0],addr[1]))
                conn.setblocking(0)
                inputs.append(conn)
                message_queue[conn] = queue.Queue() #用于存放每个到来的消息
            else:
                data = r.recv(1024)
                ip_addr = r.getpeername()[0]
                port_num = r.getpeername()[1]
                if data:
                    print("%s(%s): %s"%(ip_addr, port_num,data.decode("utf-8")))
                    message_queue[r].put("我收到你发送的消息:%s"%data.decode("utf-8"))
                    if r not in outputs:
                        outputs.append(r)
                else:
                    print("%s closing"%ip_addr)
                    if r in outputs:
                        outputs.remove(r)
                    r.close()
                    del message_queue[r]
    
        for w in w_list:
            try:
                rep_msg = message_queue[w].get_nowait()  #若是其中有数据,会一直去里面获取发送(因为可写套接字没有被移除,会一直遍历他,不同于可写(处理后自动去下一个),可写套接字若是不主动移除,会一直去处理)
            except queue.Empty:
                print("%s queue empty"%w.getpeername()[0])
                outputs.remove(w)   #注意处理完一个可写套接字后,必须将其从outputs中移除,不然会一直去获取这个套接字
            else:
                print("Sending %s to %s"%(rep_msg,w.getpeername()[0]))
                w.sendall(rep_msg.encode("utf-8"))
    
        for e in e_list:
            print("%s exception error"%e.getperrname()[0])
            inputs.remove(e)
            if e in outputs:
                outputs.remove(e)
            e.close()
            del message_queue[e]

    3.poll的使用(重点)

    Select升级版,无可监控事件数量限制,在linux下使用,还是要轮询所有事件:

    import socket
    import select
    import queue
    
    ip_port = ("0.0.0.0",8080)
    
    sk = socket.socket()
    sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
    sk.setblocking(0)
    sk.bind(ip_port)
    
    sk.listen(5)
    message_queue = {}
    
    #超时时间毫秒
    timeout = 1000
    #监听那些事件
    READ_ONLY = (select.POLLIN|select.POLLPRI|select.POLLHUP)
    #POLLIN读消息,POLLPRI紧急可读消息,POLLHUPsocket被挂起,关闭
    READ_WRITE = (READ_ONLY|select.POLLOUT)
    #POLLOUTsocket写消息
    
    #新建轮询事件对象
    poller = select.poll()
    
    #注册本机监听socket到等待可读事件事件集合
    poller.register(sk,READ_ONLY)   #本机的socket一般只有可读操作,所以我们将其注册到可读事件集合
    #文件描述符到socket映射
    fd_to_socket = {sk.fileno():sk}  #注意linux下对于socket都是操作器文件描述符,下面的epoll也是一样
    
    while True:
        print("等待活动连接...")
        #轮询注册的事件集合
        events = poller.poll(timeout)
        if not events:
            print("poll超时,无活动连接,重新轮询")
            continue
        print("有%s个新事件,开始处理"%len(events))
        for fd,flag in events:
            s = fd_to_socket[fd]
            #可读事件
            if flag & (select.POLLIN|select.POLLPRI):
                if s is sk: #有新的连接到达
                    conn,addr = s.accept()
                    print("新连接到达:%s(%s)"%(addr[0],addr[1]))
                    conn.setblocking(0)
                    fd_to_socket[conn.fileno()] = conn
                    #加入到等待可读事件集合,进行读监听(有socket到来,我们可以进行读取数据)
                    poller.register(conn,READ_ONLY)
                    message_queue[conn] = queue.Queue()
                else:   #有消息到达
                    try:
                        data = s.recv(1024)# 从客户端套接字中读取1024字节数据  #当对方强制关闭时,读取信息会报错
                    except ConnectionResetError as e:  #或者Exception
                        data = None
                    if data:
                        print("接收到数据:%s from %s(%s)"%(data.decode("utf-8"),s.getpeername()[0],s.getpeername()[1]))
                        message_queue[s].put("我接收到你发送的数据:%s"%data.decode("utf-8"))
                        #将该套接字设置到可写集合中
                        poller.modify(s,READ_WRITE)
                    else:   #客户端打开连接
                #注意:当对方强制关闭管道后,我们对于一些方法是不能使用的,像是getpeername(),fileno()等这些方法还是会使用管道获取数据,但是当前管道已断开,所有会报错 print(
    "Closing") poller.unregister(s) #解除该socket的所有事件 s.close() del message_queue[s] #连接关闭事件 elif flag & select.POLLHUP: print("%s(%s) Closing HUP"%(s.getpeername()[0],s.getpeername()[1])) poller.unregister(s) # 解除该socket的所有事件 s.close() #可写事件 elif flag & select.POLLOUT: try: msg = message_queue[s].get_nowait() except queue.Empty: print(s.getpeername(),"queue empty") poller.modify(s,READ_ONLY) #无消息可写,设置为可读 else: print("发送数据:%s 到 %s(%s)"%(msg,s.getpeername()[0],s.getpeername()[1])) s.sendall(msg.encode("utf-8")) #异常事件 elif flag & select.POLLERR: print("exception on %s(%s)"%(s.getpeername()[0],s.getpeername()[1])) poller.unregister(s) s.close() del message_queue[s]
    import socket
    
    ip_port = ("192.168.218.128",8080)    #用于绑定服务器端的地址和端口
    
    sk = socket.socket()
    sk.connect(ip_port) #与服务器连接
    
    while True:
        data = input(">>>:").strip()    #若为空,对方是无法接收到数据的,所以不会处理,而自己却阻塞在recv处
        if not data:
            continue
        sk.sendall(data.encode("utf-8"))  # 向服务器端发送数据
    
        ser_data = sk.recv(1024)  # 从服务器端获取数据
    
        print(ser_data.decode("utf-8"))
    
    sk.close()
    客户端代码

    补充(重点):

    1.当我们尝试去发送空字符串到服务器端时
    客户端 send空数据,应该不会实际发包,也就是服务端不会知道,不会去做相关处理(因为服务端根本没有接受到数据)。
    虽然客户端系统有时会发送空包( ack 等)但是服务端 read 不会返回空的(阻塞模式)。
    所以我们应该对客户端的数据进行处理,若是为空,我们需要跳过,或者其他处理
    2.当客户端强制关闭时,服务端可能会退出:
    原因:TCP是全双工的信道, 可以看作两条单工信道, TCP连接两端的两个端点各负责一条. 当对端调用close时, 虽然本意是关闭整个两条信道, 但本端只是收到FIN包. 
    按照TCP协议的语义, 表示对端只是关闭了其所负责的那一条单工信道, 仍然可以继续接收数据. 也就是说, 因为TCP协议的限制, 一个端点无法获知对端的socket是调用了close还是shutdown. 若是我们没有关闭本端连接,我们可以对这个socket进行传输数据,但是当我们使用recv等方法去读取数据时,则会保错,因为对方的传输通道已经关闭。
    在3.5以上会报错
    ConnectionResetError,我们可以对其进行捕获,在进行处理。在2.7会出现Error,所以我们可以去捕获所有,然后在进一步处理。
    注意:2.7中当对方强制关闭管道后,我们对于一些方法是不能使用的,像是getpeername(),fileno()等这些方法还是会使用管道获取数据,但是当前管道已断开,所有会报错socket.error: [Errno 107] Transport endpoint is not connected
    在bind以后就可以调用getsockname来获取本地地址和端口,虽然这没有什么太多的意义。getpeername只有在链接建立以后才调用,否则不能正确获得对方地址和端口,所以他的参数描述字一般是链接描述字而非监听套接口描述字
    getsockname和getpeername调用时机

    4.epoll的使用(重点)

     基于回调的事件通知模式,轻松管理大量连接,linux下使用

    import socket
    import select
    import queue
    
    ip_port = ("0.0.0.0",8080)
    
    sk = socket.socket()    #使用默认TCP
    sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
    sk.setblocking(0)
    sk.bind(ip_port)
    sk.listen(5)
    
    timeout = 10
    #新建epoll事件对象,后续要监控的事件添加到其中
    epoll = select.epoll()
    #添加到服务器监听fd到可读事件集合
    epoll.register(sk.fileno(),select.EPOLLIN)  #EPOLLIN读操作
    message_queue = {}
    
    fd_to_socket = {sk.fileno():sk}
    
    while True:
        print("等待活动连接...")
        #轮询注册的事件集合
        events = epoll.poll(timeout)
        if not events:
            print("epoll超时,无活动连接,重新轮询")
            continue
        print("有%s个新事件,开始处理" % len(events))
        for fd,flag in events:
            s = fd_to_socket[fd]
            #可读事件
            if flag & select.EPOLLIN:
                if s is sk: #有新的连接到达
                    conn,addr = s.accept()
                    print("新连接到达:%s(%s)"%(addr[0],addr[1]))
                    conn.setblocking(0)
                    fd_to_socket[conn.fileno()] = conn
                    #加入到等待可读事件集合,进行读监听(有socket到来,我们可以进行读取数据)
                    epoll.register(conn.fileno(),select.EPOLLIN)
                    message_queue[conn] = queue.Queue()
                else:   #有消息到达
                    try:
                        data = s.recv(1024)# 从客户端套接字中读取1024字节数据
                    except ConnectionResetError as e:
                        data = None
                    if data:
                        print("接收到数据:%s from %s(%s)"%(data.decode("utf-8"),s.getpeername()[0],s.getpeername()[1]))
                        message_queue[s].put("我接收到你发送的数据:%s"%data.decode("utf-8"))
                        #将该套接字设置到可写集合中
                        epoll.modify(fd,select.EPOLLOUT)
                    else:   #客户端打开连接
                        print("Closing")
                        epoll.unregister(fd)    #解除该socket的所有事件
                        s.close()
                        del message_queue[s]
            #连接关闭事件
            elif flag & select.EPOLLHUP:
                print("%s(%s) Closing HUP"%(s.getpeername()[0],s.getpeername()[1]))
                epoll.unregister(fd)  # 解除该socket的所有事件
                del fd_to_socket[fd]
                s.close()
            #可写事件
            elif flag & select.EPOLLOUT:
                try:
                    msg = message_queue[s].get_nowait()
                except queue.Empty:
                    print(s.getpeername(),"queue empty")
                    epoll.modify(fd,select.EPOLLIN)  #无消息可写,设置为可读
                else:
                    print("发送数据:%s 到 %s(%s)"%(msg,s.getpeername()[0],s.getpeername()[1]))
                    s.sendall(msg.encode("utf-8"))
            #异常事件
            elif flag & select.POLLERR:
                print("exception on %s(%s)"%(s.getpeername()[0],s.getpeername()[1]))
                epoll.unregister(fd)
                s.close()
                del message_queue[s]
    客户端和上面poll一致
    epoll实现方法和上面poll基本一致,
    注意:上面epoll对套接字的注册使用的是文件描述符,而poll可以直接使用socket套接字。其实两个都是可以使用的,但是在代码中需要统一
    要使用文件描述符注册,就都使用,要使用套接字,就都使用套接字

    select,poll,epoll都属于IO多路复用,而IO多路复用又属于同步的范畴,故,epoll只是一个伪异步而已

    (5)异步asyncore模块的使用(了解,为下面异步框架做准备)

    推文:Python asyncore异步socket封装模块用法总结

    模块中提供了原来构建异步通信方式的客户端和服务器端的基础构架。特别适用于聊天了的服务器端和协议的实现。
    其基本思想是创建一个或者多个网络信道,而实际上网络信道是socket对象的一个封装。
    当信道创建后,通过调用loop方法来激活网络信道的服务,直到最后一个网络信道关闭。
    主要用于网络时间循环检测的loop方法是其核心。
    在loop方法中会将通过select方法来检测特定的网络信道。
    当select方法返回有事件的socket对象后,loop方法检查此事件和套接字状态并创建一个高层次的事件信息。
    然后针对此事件信息调用相关的方法。asyncore提供了底层的API原来创建服务器
    同时在模块中还有一个dispatcher类,这是一个对socket对象的轻量级封装,用于处理网络交互事件。其中的方法是在异步loop方法中调用的,或者可以直接当做一个普通的非阻塞socket对象。
    
    在dispatcher类中,当特定的事件或连接状态的条件下,会触发一些高层次的事件。在继承类中可以通过重载这些方法来处理特定的事件

    在进行异步处理的过程,通过

    def readable(self)
    def writable(self) 

    方法对事件进行控制。通过这些方法能够判断是否需要使用IO多路复用方法来读取事件。可以通过重载这些方法来判断需要检查的连接

    1.asyncore.loop() - 用于循环监听网络事件。loop()函数负责检测一个字典。字典中保存dispatcher的实例。
    2.asyncore.dispatcher类 - 一个底层套接字对象的简单封装。这个类有少数由异步循环调用的,用来事件处理的函数。
    dispatcher类中的writable()和readable()在检测到一个socket能够写入或者数据到达的时候被调用,并返回一个bool值,决定是否调用handle_read或者handle_write
    3.asyncore.dispatcher_with_send类 - 一个 dispatcher的子类,加入了简单的缓冲输出能力。对简单的client非常实用。
    import asyncore, socket
    
    
    class HttpClient(asyncore.dispatcher):
        def __init__(self, host, path):
            asyncore.dispatcher.__init__(self)
            self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
            self.connect((host, 80))
            self.buffer = 'GET %s HTTP/1.1
    
    ' % path
            self.buffer = self.buffer.encode("utf-8")
    
        def handle_connect(self):
            pass
    
        def handle_close(self):
            self.close()
    
        def handle_read(self):
            print(self.recv(8192).decode("utf-8"))
    
    
        def writable(self):
            return (len(self.buffer) > 0)
    
        def handle_write(self):
            sent = self.send(self.buffer)
            self.buffer = self.buffer[sent:]    #意思是当我们发送的数据过长,只能发送一部分的话,将我们小于的那部分赋值给buffer,接着发送
    
    
    if __name__ == '__main__':
        c = HttpClient('www.baidu.com', '/')
        asyncore.loop()
        print('Program exit')
    18
    b''
    HTTP/1.1 200 OK
    Accept-Ranges: bytes
    Cache-Control: no-cache
    Connection: Keep-Alive
    Content-Length: 14615
    Content-Type: text/html
    Date: Tue, 15 May 2018 07:17:16 GMT
    Last-Modified: Fri, 11 May 2018 09:27:00 GMT
    P3p: CP=" OTI DSP COR IVA OUR IND COM "
    Pragma: no-cache
    Server: BWS/1.1
    Set-Cookie: BAIDUID=65F3A0063454557EBBC1A2B0FD5F03D3:FG=1; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
    Set-Cookie: BIDUPSID=65F3A0063454557EBBC1A2B0FD5F03D3; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
    Set-Cookie: PSTM=1526368636; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
    Vary: Accept-Encoding
    X-Ua-Compatible: IE=Edge,chrome=1
    
    <!DOCTYPE html><!--STATUS OK-->
    <html>
    <head>
        <meta http-equiv="content-type" content="text/html;charset=utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=Edge">
        <link rel="dns-prefetch" href="//s1.bdstatic.com"/>
        <link rel="dns-prefetch" href="//t1.baidu.com"/>
        <link rel="dns-prefetch" href="//t2.baidu.com"/>
        <link rel="dns-prefetch" href="//t3.baidu.com"/>
        <link rel="dns-prefetch" href="//t10.baidu.com"/>
        <link rel="dns-prefetch" href="//t11.baidu.com"/>
        <link rel="dns-prefetch" href="//t12.baidu.com"/>
        <link rel="dns-prefetch" href="//b1.bdstatic.com"/>
        <title>百度一下,你就知道</title>
        <link href="http://s1.bdstatic.com/r/www/cache/static/home/css/in
    dex.css" rel="stylesheet" type="text/css" />
        <!--[if lte IE 8]><style index="index" >#content{height:480px9}#m{top:260px9}</style><![endif]-->
        <!--[if IE 8]><style index="index" >#u1 a.mnav,#u1 a.mnav:visited{font-family:simsun}</style><![endif]-->
        <script>var hashMatch = document.location.href.match(/#+(.*wd=[^&].+)/);if (hashMatch && hashMatch[0] && hashMatch[1]) {document.location.replace("http://"+location.host+"/s?"+hashMatch[1]);}var ns_c = function(){};</script>
        <script>function h(obj){obj.style.behavior='url(#default#homepage)';var a = obj.setHomePage('//www.baidu.com/');}</script>
        <noscript><meta http-equiv="refresh" content="0; url=/baidu.html?from=noscript"/></noscript>
        <script>window._ASYNC_START=new Date().getTime();</script>
    </head>
    <body link="#0000cc"><div id="wrapper" style="display:none;"><div id="u"><a href="//www.baidu.com/gaoji/preferences.html"  onmousedown="return user_c({'fm':'set','tab':'setting','login':'0'})">搜索设置</a>|<a id="btop" href="/"  onmousedown="return user_c({'fm':'set','tab':'index','login':'0'})">百度首页</a>|<a id="lb" href="https://passport.baidu.com/v2/?login&tpl=mn&u=http%3A%2F%2Fwww.baidu.com%2F" onclick="return false;"  onmousedown="return user_c({'fm':'set','tab':'login'})">登录</a><a href="https://passport.baidu.com/v2/?reg&regType=1&tpl=mn&u=http%3A%2F%2Fwww.baidu.com%2F"  onmousedown="return user_c({'fm':'set','tab':'reg'})" target="_blank" class="reg">注册</a></div><div id="head"><div class="s_nav"><a href="/" class="s_logo" onmousedown="return c({'fm':'tab','tab':'logo'})"><img src="//www.baidu.com/img/baidu_jgylogo3.gif" width="117" height="38" border="0" alt="到百度首页" title="到百度首页"></a><div class="s_tab" id="s_tab"><a href="http://news.baidu.com/ns?cl=2&rn=20&tn=news&word=" wdfield="word"  onmousedown="return c({'fm':'tab','tab':'news'})">新闻</a>&#12288;<b>网页</b>&#12288;<a href="http://tieba.baidu.com/f?kw=&fr=wwwt" wdfield="kw"  onmousedown="return c({'fm':'tab','tab':'tieba'})">贴吧</a>&#12288;<a href="http://zhidao.baidu.com/q?ct=17&pn=0&tn=ikaslist&rn=10&word=&fr=wwwt" wdfield="word"  onmousedown="return c({'fm':'tab','tab':'zhidao'})">知道</a>&#12288;<a href="http://music.baidu.com/search?fr=ps&key=" wdfield="key"  onmousedown="return c({'fm':'tab','tab':'music'})">音乐</a>&#12288;<a href="http://image.baidu.com/i?tn=baiduimage&ps=1&ct=201326592&lm=-1&cl=2&nc=1&word=" wdfield="word"  onmousedown="return c({'fm':'tab','tab':'pic'})">图片</a>&#12288;<a href="http://v.baidu.com/v?ct=301989888&rn=20&pn=0&db=0&s=25&word=" wdfield="word"   onmousedown="return c({'fm':'tab','tab':'vid
    eo'})">视频</a>&#12288;<a href="http://map.baidu.com/m?word=&fr=ps01000" wdfield="word"  onmousedown="return c({'fm':'tab','tab':'map'})">地图</a>&#12288;<a href="http://wenku.baidu.com/search?word=&lm=0&od=0" wdfield="word"  onmousedown="return c({'fm':'tab','tab':'wenku'})">文库</a>&#12288;<a href="//www.baidu.com/more/"  onmousedown="return c({'fm':'tab','tab':'more'})">更多»</a></div></div><form id="form" name="f" action="/s" class="fm" ><input type="hidden" name="ie" value="utf-8"><input type="hidden" name="f" value="8"><input type="hidden" name="rsv_bp" value="1"><span class="bg s_ipt_wr"><input name="wd" id="kw" class="s_ipt" value="" maxlength="100"></span><span class="bg s_btn_wr"><input type="submit" id="su" value="百度一下" class="bg s_btn" onmousedown="this.className='bg s_btn s_btn_h'" onmouseout="this.className='bg s_btn'"></span><span class="tools"><span id="mHolder"><div id="mCon"><span>输入法</span></div><ul id="mMenu"><li><a href="javascript:;" name="ime_hw">手写</a></li><li><a href="javascript:;" name="ime_py">拼音</a></li><li class="ln"></li><li><a href="javascript:;" name="ime_cl">关闭</a></li></ul></span><span class="shouji"><strong>推荐&nbsp;:&nbsp;</strong><a href="http://w.x.baidu.com/go/mini/8/10000020" onmousedown="return ns_c({'fm':'behs','tab':'bdbrowser'})">百度浏览器,打开网页快2秒!</a></span></span></form></div><div id="content"><div id="u1"><a href="http://news.baidu.com" name="tj_trnews" class="mnav">新闻</a><a href="http://www.hao123.com" name="tj_trhao123" class="mnav">hao123</a><a href="http://map.baidu.com" name="tj_trmap" class="mnav">地图</a><a href="http://v.baidu.com" name="tj_trvideo" class="mnav">视频</a><a href="http://tieba.baidu.com" name="tj_trtieba" class="mnav">贴吧</a><a href="https://passport.baidu.com/v2/?login&tpl=mn&u=http%3A%2F%2Fwww.baidu.com%2F" name="tj_login" id="lb" onclick="return false;">登录</a><a href="//www.baidu.com/gaoji/preferences.html" name="tj_settingicon" id="pf">设置</a><a href="//www.baidu.com/more/" name="tj_briicon" id="bri">更多产品</a></div><div id="m"><p id="lg"><img src="//www.baidu.com/img/bd_logo.png" width="270" height="129"></p><p id="nv"><a href="http://news.baidu.com">新&nbsp;闻</a> <b>网&nbsp;页</b> <a href="http://tieba.baidu.com">贴&nbsp;吧</a> <a href="http://zhidao.baidu.com">知&nbsp;道</a> <a href="http://music.baidu.com">音&nbsp;乐</a> <a href="http://image.baidu.com">图&nbsp;片</a> <a href="http://v.baidu.com">视&nbsp;频</a> <a href="http://map.baidu.com">地&nbsp;图</a></p><div id="fm"><form id="form1" name="f1" action="/s" class="fm"><span class="bg s_ipt_wr"><input type="text" name="wd" id="kw1" maxlength="100" class="s_ipt"></span><input type="hidden" name="rsv_bp" value="0"><input type=hidden name=ch value=""><input type=hidden name=tn value="baidu"><input type=hidden name=bar value=""><input type="hidden" name="rsv_spt" value="3"><input type="hidden" name="ie" value="utf-8"><span class="bg s_btn_wr"><input type="submit" value="百度一下" id="su1" class="bg s_btn" onmousedown="this.className='bg s_btn s_btn_h'" onmouseout="this.className='bg s_btn'"></span></form><span class="tools"><span id="mHolder1"><div id="mCon1"><span>输入法</span></div></span></span><ul id="mMenu1"><div class="mMenu1-tip-arrow"><em></em><ins></ins></div><li><a href="javascript:;" name="ime_hw">手写</a></li><li><a href="javascript:;" name="ime_py">拼音</a></li><li class="ln"></li><li><a href="javascript:;" name="ime_cl">关闭</a></li></ul></div><p id="lk"><a href="http://baike.baidu.com">百科</a> <a href="http://wenku.baidu.com">文库</a> <a href="http://www.hao123.com">hao123</a><span>&nbsp;|&nbsp;<a href="//www.baidu.com/more/">更多&gt;&gt;</a></span></p><p id="lm"></p></div></div><div id="ftCon"><div id="ftConw"><p id="lh"><a id="seth" onClick="h(this)" href="/" onmousedown="return ns_c({'fm':'behs','tab':'homepage','pos':0})">把百度设为主页</a><a id="setf" href="//www.baidu.com/cache/sethelp/index.html" onmousedown="return ns_c({'fm':'behs','tab':'favorites','pos':0})" target="_blank">把百度设为主页</a><a onmousedown="return ns_c({'fm':'behs','tab':'tj_about'})" href="http://home.baidu.com">关于百度</a><a onmousedown="return ns_c({'fm':'behs','tab':'tj_about_en'})" href="http://ir.baidu.com">About Baidu</a></p><p id="cp">&copy;2018&nbsp;Baidu&nbsp;<a href="/duty/" name="tj_duty">使用百度前必读</a>&nbsp;京ICP证030173号&nbsp;<img src="http://s1.bdstatic.com/r/www/cache/static/global/img/gs_237f015b.gif"></p></div></div><div id="wrapper_wrapper"></div></div><div class="c-tips-container" id="c-tips-container"></div>
    <script>window.__async_strategy=2;</script>
    <script>var bds={se:{},su:{urdata:[],urSendClick:function(){}},util:{},use:{},comm : {domain:"http://www.baidu.com",ubsurl : "http://sclick.baidu.com/w.gif",tn:"baidu",queryEnc:"",queryId:"",inter:"",templateName:"baidu",sugHost : "http://suggestion.baidu.com/su",query : "",qid : "",cid : "",sid : "",indexSid : "",stoken : "",serverTime : "",user : "",username : "",loginAction : [],useFavo : "",pinyin : "",favoOn : "",curResultNum:"",rightResultExist:false,protectNum:0,zxlNum:0,pageNum:1,pageSize:10,newindex:0,async:1,maxPreloadThread:5,maxPreloadTimes:10,preloadMouseMoveDistance:5,switchAddMask:false,isDebug:false,ishome : 1},_base64:{domain : "http://b1.bdstatic.com/",b64Exp : -1,pdc : 0}};var name,navigate,al_arr=[];var selfOpen = window.open;eval("var open = selfOpen;");var isIE=navigator.userAgent.indexOf("MSIE")!=-1&&!window.opera;var E = bds.ecom= {};bds.se.mon = {'loadedItems':[],'load':function(){},'srvt':-1};try {bds.se.mon.srvt = parseInt(document.cookie.match(new RegExp("(^| )BDSVRTM=([^;]*)(;|$)"))[2]);document.cookie="BDSVRTM=;expires=Sat, 01 Jan 2000 00:00:00 GMT"; }catch(e){}</script>
    <script>if(!location.hash.match(/[^a-zA-Z0-9]wd=/)){document.getElementById("ftCon").style.display='block';document.getElementById("u1").style.display='block';document.getElementById("content").style.display='block';document.getElementById("wrapper").style.display='block';setTimeout(function(){try{document.getElementById("kw1").focus();document.getElementById("kw1").parentNode.className += ' iptfocus';}catch(e){}},0);}</script>
    <script type="text/javascript" src="http://s1.bdstatic.com/r/www/cache/static/jquery/jquery-1.10.2.min_f2fb5194.js"></script>
    <script>(function(){var index_content = $('#content');var index_foot= $('#ftCon');var index_css= $('head [index]');var index_u= $('#u1');var result_u= $('#u');var wrapper=$("#wrapper");window.index_on=function(){index_css.insertAfter("meta:eq(0)");result_common_css.remove();result_aladdin_css.remove();result_sug_css.remove();index_content.show();index_foot.show();index_u.show();result_u.hide();wrapper.show();if(bds.su&&bds.su.U&&bds.su.U.homeInit){bds.su.U.homeInit();}setTimeout(function(){try{$('#kw1').get(0).focus();window.sugIndex.start();}catch(e){}},0);if(typeof initIndex=='function'){initIndex();}};window.index_off=function(){index_css.remove();index_content.hide();index_foot.hide();index_u.hide();result_u.show();result_aladdin_css.insertAfter("meta:eq(0)");result_common_css.insertAfter("meta:eq(0)");result_sug_css.insertAfter("meta:eq(0)");wrapper.show();};})();</script>
    <script>window.__switch_add_mask=1;</script>
    <script type="text/javascript" src="http://s1.bdstatic.com/r/www/cache/static/global/js/instant_search_newi_redirect1_20bf4036.js"></script>
    <script>initPreload();$("#u,#u1").delegate("#lb",'click',function(){try{bds.se.login.open();}catch(e){}});if(navigator.cookieEnabled){document.cookie="NOJS=;expires=Sat, 01 Jan 2000 00:00:00 GMT";}</script>
    <script>$(function(){for(i=0;i<3;i++){u($($('.s_ipt_wr')[i]),$($('.s_ipt')[i]),$($('.s_btn_wr')[i]),$($('.s_btn')[i]));}function u(iptwr,ipt,btnwr,btn){if(iptwr && ipt){iptwr.on('mouseover',function(){iptwr.addClass('ipthover');}).on('mouseout',function(){iptwr.removeClass('ipthover');}).on('click',function(){ipt.focus();});ipt.on('focus',function(){iptwr.addClass('iptfocus');}).on('blur',function(){iptwr.removeClass('iptfocus');}).on('render',function(e){var $s = iptwr.p
    arent().find('.bdsug');var l = $s.find('li').length;if(l>=5){$s.addClass('bdsugbg');}else{$s.removeClass('bdsugbg');}});}if(btnwr && btn){btnwr.on('mouseover',function(){btn.addClass('btnhover');}).on('mouseout',function(){btn.removeClass('btnhover');});}}});</script>
    <script type="text/javascript" src="http://s1.bdstatic.com/r/www/cache/static/home/js/bri_7f1fa703.js"></script>
    <script>(function(){var _init=false;window.initIndex=function(){if(_init){return;}_init=true;var w=window,d=document,n=navigator,k=d.f1.wd,a=d.getElementById("nv").getElementsByTagName("a"),isIE=n.userAgent.indexOf("MSIE")!=-1&&!window.opera;(function(){if(/q=([^&]+)/.test(location.search)){k.value=decodeURIComponent(RegExp["x241"])}})();(function(){var u = G("u1").getElementsByTagName("a"), nv = G("nv").getElementsByTagName("a"), lk = G("lk").getElementsByTagName("a"), un = "";var tj_nv = ["news","tieba","zhidao","mp3","img","video","map"];var tj_lk = ["baike","wenku","hao123","more"];un = bds.comm.user == "" ? "" : bds.comm.user;function _addTJ(obj){addEV(obj, "mousedown", function(e){var e = e || window.event;var target = e.target || e.srcElement;if(target.name){ns_c({'fm':'behs','tab':target.name,'un':encodeURIComponent(un)});}});}for(var i = 0; i < u.length; i++){_addTJ(u[i]);}for(var i = 0; i < nv.length; i++){nv[i].name = 'tj_' + tj_nv[i];}for(var i = 0; i < lk.length; i++){lk[i].name = 'tj_' + tj_lk[i];}})();(function() {var links = {'tj_news': ['word', 'http://news.baidu.com/ns?tn=news&cl=2&rn=20&ct=1&ie=utf-8'],'tj_tieba': ['kw', 'http://tieba.baidu.com/f?ie=utf-8'],'tj_zhidao': ['word', 'http://zhidao.baidu.com/search?pn=0&rn=10&lm=0'],'tj_mp3': ['key', 'http://music.baidu.com/search?fr=ps&ie=utf-8'],'tj_img': ['word', 'http://image.baidu.com/i?ct=201326592&cl=2&nc=1&lm=-1&st=-1&tn=baiduimage&istype=2&fm=&pv=&z=0&ie=utf-8'],'tj_video': ['word', 'http://video.baidu.com/v?ct=301989888&s=25&ie=utf-8'],'tj_map': ['wd', 'http://map.baidu.com/?newmap=1&ie=utf-8&s=s'],'tj_baike': ['word', 'http://baike.baidu.com/search/word?pic=1&sug=1&enc=utf8'],'tj_wenku': ['word', 'http://wenku.baidu.com/search?ie=utf-8']};var domArr = [G('nv'), G('lk'),G('cp')],kw = G('kw1');for (var i = 0, l = domArr.length; i < l; i++) {domArr[i].onmousedown = function(e) {e = e || window.event;var target = e.target || e.srcElement,name = target.getAttribute('name'),items = links[name],reg = new RegExp('^\s+|\s+x24'),key = kw.value.replace(reg, '');if (items) {if (key.length > 0) {var wd = items[0], url = items[1],url = url + ( name === 'tj_map' ? encodeURIComponent('&' + wd + '=' + key) : ( ( url.indexOf('?') > 0 ? '&' : '?' ) + wd + '=' + encodeURIComponent(key) ) );target.href = url;} else {target.href = target.href.match(new RegExp('^http://.+.baidu.com'))[0];}}name && ns_c({'fm': 'behs','tab': name,'query': encodeURIComponent(key),'un': encodeURIComponent(bds.comm.user || '') });};}})();};if(window.pageState==0){initIndex();}})();document.cookie = 'IS_STATIC=1;expires=' + new Date(new Date().getTime() + 10*60*1000).toGMTString();</script>
    </body></html>
    
    
    Program exit
    输出结果
    1.create_socket方法
    print(self._map)  #由dispatcher类进行维护,是一个字典,{socket套接字文件描述符:当前对象}
    print(self._fileno)#字典中key
    print(self)     #字典中value
    {228: <__main__.HTTPClient www.baidu.com:80 at 0xc1fbe0>}
    228
    <__main__.HTTPClient www.baidu.com:80 at 0xc1fbe0>
    输出结果
    2.connect((host, 80))方法连接服务器80端口,这是http协议的默认端口
    3.类变量buffer中存放了我们的请求头,将在handle_write方法中被发送
    handle_connect:在connect连接的时候被调用
    handle_read:调用recv方法类获取http返回的数据。在后驱数据的时候调用,缓冲区大小最好是2的乘幂
    writable:用来判断在什么时候发送数据,在这里只需要判断buffer数据是否发送完毕
    handle_write:调用send发送数据,有雨异步通信,不一定每次都发送完全,所以我们需要对返回的sent(发送数据长度)进行处理,用来改变buffer
    handle_close:用来关闭连接,在Http关闭的时候调用
    其中没有对readable重载,所以将会轮询去看是否需要接受数据
    最后在
    c = HTTPClient('www.baidu.com', '/')
    asyncore.loop()
    之间没有关联,如何调用?
    def loop(timeout=30.0, use_poll=False, map=None, count=None):
        if map is None:
            map = socket_map#(这里和HTTPClient中的self._map是一个变量)  
           
    try:
        socket_map
    except NameError:
        socket_map = {}
    
    class dispatcher:
        def __init__(self, sock=None, map=None):
            if map is None:
                self._map = socket_map
    socket_map 是全局变量
    if use_poll and hasattr(select, 'poll'):
            poll_fun = poll2
        else:
            poll_fun = poll  #调用poll方法去处理socket
    
        if count is None:
            while map:
                poll_fun(timeout, map)  #调用poll方法去处理
        else:
            while map and count > 0:
                poll_fun(timeout, map)
                count = count - 1

    异步实现服务器端和客户端

    import time
    import asyncore
    import socket
    import threading
    
    #服务端数据响应类,接收数据并将其原样返回
    class EchoHandler(asyncore.dispatcher_with_send):
        def handle_read(self):
            data = self.recv(1024).decode("utf-8")
            if data:
                self.send(data.encode("utf-8"))
    
    #服务端程序,负责监听一个端口,并响应客户端发送的数据,其中handle_accept方法定义当一个连接到来的时候
    #要执行的操作,这里指定了使用一个self.handler<任意变量>来处理发送来的数据
    class EchoServer(asyncore.dispatcher):
        def __init__(self,host,port):
            asyncore.dispatcher.__init__(self)
            self.create_socket(socket.AF_INET,socket.SOCK_STREAM)
            self.set_reuse_addr()
            self.bind((host,port))
            self.listen(5)
    
        def handle_accept(self):
            conn,addr = self.accept()
            print("Incomming connection from %s"%repr(addr))
            EchoHandler(conn)
    
    #客户端,负责连接服务器
    class EchoClient(asyncore.dispatcher):
        def __init__(self,host,port):
            asyncore.dispatcher.__init__(self)
            self.messages = ["1","2","3","4","5","6",'7', '8', '9', '10']
            self.create_socket(socket.AF_INET,socket.SOCK_STREAM)
            self.connect((host,port))
    
        def handle_connect(self):
            pass
    
        def handle_read(self):
            print(self.recv(1024))
    
        def writable(self):
            return (len(self.messages) > 0)
    
        def handle_write(self):
            time.sleep(1)
            data = self.messages.pop(0)
            print(data)
            self.send(data.encode("utf-8"))
    
        def handle_close(self):
            self.close()
    
    #启动服务器的线程
    class EchoServerThread(threading.Thread):
        def __init__(self):
            threading.Thread.__init__(self)
    
        def run(self):
            server = EchoServer("localhost",8080)
            asyncore.loop()  #loop方法只需要调用一次,不然会存在线程冲突(所有的套接字都是在其中维护),若是我们将客户端和服务器端分开的话,需要两个loop方法去维护各自的字典
    
    #启动客户端的线程
    class EchoClientThread(threading.Thread):
        def __init__(self):
            threading.Thread.__init__(self)
    
        def run(self):
            client = EchoClient("localhost", 8080)
    
    EchoServerThread().start()
    time.sleep(2)
    EchoClientThread().start()

    注意:

    EchoServer:用来维护服务器端(其中含有服务端的{socket:自己对象}进行维护),主要作用是接收客户端连接,获取客户端套接字
    EchoHandler:也是一个数据响应类,主要是维护服务器发送过来的客户端socket,(含有客户端socket:自己对象),对每一个客户端对生成一个对象去处理数据
    EchoClient:用来维护客户端
    上面三个是同一级对象,维护的字典都是在全局变量socket_map中,loop方法循环时会调用write(obj)或者其他read(obj)全局函数,去调用对应的对象去维护相关套接字
    Incomming connection from ('127.0.0.1', 6309)
    1
    2
    b'12'
    3
    4
    b'34'
    5
    6
    b'56'
    7
    8
    b'78'
    9
    10
    b'910'
    其中b''中是接收的数据,其他的是发送的数据
    输出结果,存在粘包现象(后面解决)

    三:网络框架Twisted

    Twisted是一个事件驱动的网络框架,其中包含了诸多功能,例如:网络协议(SSH2,FTP,POP3和SMTP等)、线程、数据库管理、网络操作、电子邮件等。

    安装方法:Python环境安装

    在上面的多进程,多线程以及异步通信传输都是使用了一种轮询的方式来处理连接。

    而对于Twisted框架,采用的是一种事件驱动的方式。也就是说,只需要在事件发生的点构建相关的代码即可。包括新连接到来,新数据到来,详解关闭的时候。Twisted对这些还有更加详细的划分。

    Twisted框架是由模块化的组件组成的,包括协议,工厂,反应器(reactor)和Deferred对象等。
    工厂原来产生一个新的实例,一种实例可以产生一个类型的协议。这些协议定义了如何和服务器交换数据。在运行的时候,每个连接都会产生一个协议实例。
    其中,用来管理事件信息循环的反应器,是整个Twisted应用服务器的核心。
    另外,Deferred对象则是一种处理延时调度器处理的方法。使用这种方法,可以不必等待运行结果而返回。

    (一)事件驱动

    在上面的异步 asyncore模块中也是使用了事件驱动模型,这里的twisted也是一个事件驱动的网络框架。

    事件驱动分为两个部分:
    第一:注册事件
    第二:触发事件

    异步模块:asyncore中

    注册事件:将所有socket套接字和操作对象放在全局字典中进行维护socket_map

    try:
        socket_map
    except NameError:
        socket_map = {}

    触发事件:在loop中去触发事件

    def loop(timeout=30.0, use_poll=False, map=None, count=None):
        if map is None:
            map = socket_map
            while map:
                poll_fun(timeout, map)

    自定义事件驱动框架:

    event_list = []
    
    def run():
        for event in event_list:
            if issubclass(event,BaseHandler):
                obj = event()
                obj.execute()
    
    class BaseHandler(object):
        '''
        用户需要继承该类,从而规范所有类的方法
        '''
        def execute(self):
            raise("你必须重载该方法")
    事件驱动框架
    import event_drive
    
    class MyHandler(event_drive.BaseHandler):
        def execute(self):
            print("我被执行了")
    
    class MyHandler2(event_drive.BaseHandler):
        def execute(self):
            print("俺也被执行了")
    
    event_drive.event_list.append(MyHandler)
    event_drive.event_list.append(MyHandler2)
    event_drive.run()
    事件触发
    我被执行了
    俺也被执行了
    输出结果
    在使用框架的时候就是向模块中维护的数据类型中添加数据,在框架执行的时候触发函数去处理这些数据,完成这些被注册的数据。

    (二)Twisted框架下服务器的实现

    from twisted.internet import reactor
    from twisted.internet.protocol import Protocol,Factory
    
    class SimpleServer(Protocol):
        def connectionMade(self):   #建立连接的时候
            print("Get connect from",self.transport.client)
    
        def connectionLost(self, reason):   #连接断开的时候
            print(self.transport.client,"disconnect")
    
        def dataReceived(self, data):   #接收数据的时候
            print(data)
    
    factory = Factory()
    factory.protocol = SimpleServer
    
    port = 8080
    reactor.listenTCP(port,factory)
    reactor.run()   #进入循环  核心
    Get connect from ('127.0.0.1', 8340)
    b'fwaf'
    ('127.0.0.1', 8340) disconnect
    输出结果
    1.上面的功能是将收到的数据打印出来,同时在连接开始和断开的时候给出显示信息。事件驱动编程方式,只是重载了特定事件的代码实现。
    2.代码中导入了reactor,Protocol和Factory类,其中reactor是核心
    The reactor is the Twisted event loop within Twisted, the loop which drives
    applications using Twisted. The reactor provides APIs for networking,
    threading, dispatching events, and more.
    reactor实现事件循环
        This is the base class for streaming connection-oriented protocols.
    
        If you are going to write a new connection-oriented protocol for Twisted,
        start here.  Any protocol implementation, either client or server, should
        be a subclass of this class.
    Protocol是字节型(TCP)连接的基类,模块中实现了该类的子类供我们使用,我们也可以自定义,但是需要去继承。补充DatagramProtocol是UDP连接
        This is a factory which produces protocols.
    
        By default, buildProtocol will create a protocol of the class given in
        self.protocol.
    Factory根据一种协议产生一种实例。每次有客户端连接都会产生一个协议实例
    3.定义了一个SimpleServer类,此类是从Protocol类中继承的。此类中实现了通信协议的基本框架。并定义了相关的通信接口。这些接口将在通信时的特定事件中被触发。
    当连接建立的时候,也就是通信协议最开始协商的时候,将会调用connectionMade方法。这里的实现仅仅只是输出了连接信息。其中self.transport中保存了当前的连接对象,可以通过其中的client获取连接的客户端属性
    
    当连接断开会去调用connectionLost方法。
    
    当收到消息会去调用dataReceived方法。
    4.在定义完SimpleServer类后,调用了Factory类的构造函数并创建了一个工厂方法实例,并设置其协议为自定义协议类
    5.最后,调用了反应器的listenTCP方法,设置监听的端口号和上面生成的工厂对象。然后调用run方法进入循环,在此期间,程序监听端口,当接收到请求,将会调用自定义协议类中的处理函数去处理事件。
    class PosixReactorBase(_SignalReactorMixin, _DisconnectSelectableMixin,
                           ReactorBase):
        def listenTCP(self, port, factory, backlog=50, interface=''):
            p = tcp.Port(port, factory, backlog, interface, self)
            p.startListening()
            return p
    listenTCP
    class Port(base.BasePort, _SocketCloser):
        socketType = socket.SOCK_STREAM
        transport = Server
        _type = 'TCP'
        def __init__(self, port, factory, backlog=50, interface='', reactor=None):
            """Initialize with a numeric port to listen on.
            """
            base.BasePort.__init__(self, reactor=reactor)
            self.port = port
            self.factory = factory
            self.backlog = backlog
            if abstract.isIPv6Address(interface):
                self.addressFamily = socket.AF_INET6
                self._addressType = address.IPv6Address
            self.interface = interface
    Port类
    class _SignalReactorMixin(object):
        def run(self, installSignalHandlers=True):
            self.startRunning(installSignalHandlers=installSignalHandlers)
            self.mainLoop()
    
        def startRunning(self, installSignalHandlers=True):
            """
            Extend the base implementation in order to remember whether signal
            handlers should be installed later.
    
            @type installSignalHandlers: C{bool}
            @param installSignalHandlers: A flag which, if set, indicates that
                handlers for a number of (implementation-defined) signals should be
                installed during startup.
            """
            self._installSignalHandlers = installSignalHandlers
            ReactorBase.startRunning(self)
    run方法
        def mainLoop(self):
            while self._started:
                try:
                    while self._started:
                        # Advance simulation time in delayed event
                        # processors.
                        self.runUntilCurrent()
                        t2 = self.timeout()
                        t = self.running and t2
                        self.doIteration(t)
                except:
                    log.msg("Unexpected error in main loop.")
                    log.err()
                else:
                    log.msg('Main loop terminated.')
    mainLoop方法
        def runUntilCurrent(self):
            """
            Run all pending timed calls.
            """
            if self.threadCallQueue:
                # Keep track of how many calls we actually make, as we're
                # making them, in case another call is added to the queue
                # while we're in this loop.
                count = 0
                total = len(self.threadCallQueue)
                for (f, a, kw) in self.threadCallQueue:
                    try:
                        f(*a, **kw)
                    except:
                        log.err()
                    count += 1
                    if count == total:
                        break
                del self.threadCallQueue[:count]
                if self.threadCallQueue:
                    self.wakeUp()
    runUntilCurrent方法
        def wakeUp(self):
            """
            Wake up the event loop.
            """
            if self.waker:
                self.waker.wakeUp()
            # if the waker isn't installed, the reactor isn't running, and
            # therefore doesn't need to be woken up
    wakeUp方法
    self.waker中包含了我们维护的socket套接字

    LineReceiver的使用:(需要在缓冲去中接收到换行符才会获取数据,这是防止粘包的一种好办法)

    from twisted.internet import reactor
    from twisted.internet.protocol import Protocol,Factory
    from twisted.protocols.basic import LineReceiver
    
    class SimpleServer(LineReceiver):
        def connectionMade(self):   #建立连接的时候
            print("Get connect from",self.transport.client)
    
        def connectionLost(self, reason):   #连接断开的时候
            print(self.transport.client,"disconnect")
    
        def lineReceived(self,line):    #当收到一行数据的时候
            print("reve a line :%s"%line.decode("utf-8"))
    
    factory = Factory()
    factory.protocol = SimpleServer
    
    port = 8080
    reactor.listenTCP(port,factory)
    reactor.run()   #进入循环

    注意:

    class LineReceiver(protocol.Protocol, _PauseableMixin):内部已经继承了协议类
    '''
    By default this is
                         C{b'\r\n'}.
    '''
    且默认换行符为
    
    import socket
    
    ip_port = ("127.0.0.1",8080)    #用于绑定服务器端的地址和端口
    
    sk = socket.socket()
    sk.connect(ip_port) #与服务器连接
    
    while True:
        data = input(">>>:")
        if not data:
            continue
        if data == "hh":
            data += "
    "
            print(data)
        sk.sendall(data.encode("utf-8"))  # 向服务器端发送数据
    
    sk.close()
    客户端实现

    上面只实现了接收数据,下面实现响应数据:将数据返回

    from twisted.internet import reactor
    from twisted.internet.protocol import Protocol,Factory
    from twisted.protocols.basic import LineReceiver
    
    class EchoServer(LineReceiver):
        def connectionMade(self):   #建立连接的时候
            print("Get connect from",self.transport.client)
    
        def connectionLost(self, reason):   #连接断开的时候
            print(self.transport.client,"disconnect")
    
        def lineReceived(self,line):    #当收到一行数据的时候
            data = "reve a line :%s"%line.decode("utf-8")
            print(data)
            self.transport.write(data.encode("utf-8"))
    
    factory = Factory()
    factory.protocol = EchoServer
    
    port = 8080
    reactor.listenTCP(port,factory)
    reactor.run()   #进入循环

    只需要使用self.transport(其中保存了当前连接对象)中的write方法,就可以发送数据。

    import socket
    
    ip_port = ("127.0.0.1",8080)    #用于绑定服务器端的地址和端口
    
    sk = socket.socket()
    sk.connect(ip_port) #与服务器连接
    
    while True:
        data = input(">>>:")
        if not data:
            continue
        data += "
    "
        sk.sendall(data.encode("utf-8"))  # 向服务器端发送数据
        data = sk.recv(1024)
        print(data.decode("utf-8"))
    
    sk.close()
    客户端实现

    实现限制客户端连接数量:

    from twisted.internet import reactor
    from twisted.internet.protocol import Protocol,Factory
    from twisted.protocols.basic import LineReceiver
    
    class EchoServer(LineReceiver):
        def connectionMade(self):   #建立连接的时候
            print("Get connect from",self.transport.client)
            self.factory.numPorts = self.factory.numPorts + 1
            if self.factory.numPorts > 3:
                self.transport.write("Too many connections , try later".encode("utf-8"))
                self.transport.loseConnection()
                self.factory.numPorts = self.factory.numPorts - 1
            else:
                self.transport.write("Successful connections".encode("utf-8"))
    
        def connectionLost(self, reason):   #连接断开的时候
            print(self.transport.client,"disconnect")
            self.factory.numPorts = self.factory.numPorts - 1
    
        def lineReceived(self,line):    #当收到一行数据的时候
            data = "reve a line :%s"%line.decode("utf-8")
            print(data)
            self.transport.write(data.encode("utf-8"))
    
    factory = Factory()
    factory.protocol = EchoServer
    
    port = 8080
    reactor.listenTCP(port,factory)
    reactor.run()   #进入循环
    import socket
    
    ip_port = ("127.0.0.1",8080)    #用于绑定服务器端的地址和端口
    
    sk = socket.socket()
    sk.connect(ip_port) #与服务器连接
    
    data = sk.recv(1024)
    print(data.decode("utf-8"))
    
    while True:
        data = input(">>>:")
        if not data:
            continue
        data += "
    "
        try:
            sk.sendall(data.encode("utf-8"))  # 向服务器端发送数据
            data = sk.recv(1024)
            print(data.decode("utf-8"))
        except ConnectionAbortedError:
            break
    
    sk.close()
    客户端实现

    补充:我在看资料的时候发现使用的都是numProtocols来代表工厂中协议实例化个数(每次有客户端连接,就会实例化一个协议类)。但是并没有这个属性,会报错,所有使用了一个numPorts变量(其在工厂实例化后始终为一),具体咋用,以后再探究。

    图片来源:https://blog.csdn.net/nginxs/article/details/77197505

  • 相关阅读:
    2822 爱在心中
    P1707 刷题比赛
    1269 匈牙利游戏
    1482 路线统计
    Codevs 1287 矩阵乘法&&Noi.cn 09:矩阵乘法(矩阵乘法练手题)
    P2022 有趣的数
    1087 麦森数
    P1111 修复公路
    python为在线漫画站点自制非官方API(未完待续)
    逻辑运算0==x和x==0具体解释
  • 原文地址:https://www.cnblogs.com/ssyfj/p/8903723.html
Copyright © 2020-2023  润新知