TOP

python学习笔记 IO模型

概念

一、事件驱动模型

通常,我们写服务器处理模型的程序时,有以下几种模型:

(1)每收到一个请求,创建一个新的进程,来处理该请求; 
(2)每收到一个请求,创建一个新的线程,来处理该请求; 
(3)每收到一个请求,放入一个事件列表,让主进程通过非阻塞I/O方式来处理请求

第三种就是协程、事件驱动的方式,一般普遍认为第(3)种方式是大多数网络服务器采用的方式 

 

总结:事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理

 

协程:遇到I/O操作就是切换。 
但什么时候切回去呢?怎么确定IO操作完了?

1.要理解事件驱动和程序,就需要与非事件驱动的程序进行比较。实际上,现代的程序大多是事件驱动的,比如多线程的程序,肯定是事件驱动的。早期则存在许多非事件驱动的程序,这样的程序,在需要等待某个条件触发时,会不断地检查这个条件,直到条件满足,这是很浪费cpu时间的。而事件驱动的程序,则有机会释放cpu从而进入睡眠态(注意是有机会,当然程序也可自行决定不释放cpu),当事件触发时被操作系统唤醒,这样就能更加有效地使用cpu.
2.再说什么是事件驱动的程序。一个典型的事件驱动的程序,就是一个死循环,并以一个线程的形式存在,这个死循环包括两个部分,第一个部分是按照一定的条件接收并选择一个要处理的事件,第二个部分就是事件的处理过程。程序的执行过程就是选择事件和处理事件,而当没有任何事件触发时,程序会因查询事件队列失败而进入睡眠状态,从而释放cpu。
3.事件驱动的程序,必定会直接或者间接拥有一个事件队列,用于存储未能及时处理的事件。
4.事件驱动的程序的行为,完全受外部输入的事件控制,所以,事件驱动的系统中,存在大量这种程序,并以事件作为主要的通信方式。
5.事件驱动的程序,还有一个最大的好处,就是可以按照一定的顺序处理队列中的事件,而这个顺序则是由事件的触发顺序决定的,这一特性往往被用于保证某些过程的原子化。
6.目前windows,linux,nucleus,vxworks都是事件驱动的,只有一些单片机可能是非事件驱动的。

注意,事件驱动的监听事件是由操作系统调用的cpu来完成的

在进行解释之前,首先要说明几个概念:

  1. 用户空间和内核空间 :操作系统的核心是内核,为保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间(内核态,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限),另一部分为用户空间(用户态,不能直接调用硬件,普通应用程序就属于用户态),用户态需要调用硬件资源必须先传给内核态,由内核态来完成。
  2. 进程切换 :非常耗资源
  3. 进程的阻塞 :当进程进入阻塞状态,是不占用CPU资源的。
  4. 文件描述符:是一个用于表述指向文件的引用的抽象化概念,但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
  5. 缓存 I/O:数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。用户空间没法直接访问内核空间的,内核态到用户态的数据拷贝,因为用户态没有操作硬件的权限,所以必须经过内核态的缓冲区。 缺点: 数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。

五种IO Model:

  •     blocking IO
  •     nonblocking IO
  •     IO multiplexing
  •     signal driven IO  不常用
  •     asynchronous IO

二、IO模型

对于一个network IO (这里我们以read举例),它会涉及到两个系统对象,一个是调用这个IO的process (or thread),另一个就是系统内核(kernel)。当一个read操作发生时,它会经历两个阶段:
 1 等待数据准备 (Waiting for the data to be ready)
 2 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
记住这两点很重要,因为这些IO Model的区别就是在两个阶段上各有不同的情况。

1、blocking IO 阻塞IO

在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:

当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据。对于network io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。
所以,blocking IO的特点就是在IO执行的两个阶段(前文提到的两个阶段)都被block阻塞了。

import time
import socket
sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sk.bind(('127.0.0.1',6667))
sk.listen(5)

print ('waiting client connection .......')
connection,address = sk.accept() #程序进入阻塞状态,等待客户端连接
print("+++",address)
client_messge = connection.recv(1024) #程序进入阻塞状态,等待客户端发送数据
print(str(client_messge,'utf8'))
connection.close()


#############################client
import socket
sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

while True:
    sk.connect(('127.0.0.1',6667))
    print("hello")
    time.sleep(10)
    sk.sendall(bytes("hello","utf8"))
    break

 

2、non-blocking IO(非阻塞IO)

linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:

从图中可以看出,当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。
所以,用户进程其实是需要不断的主动询问kernel数据好了没有。

 注意:

      在网络IO时候,非阻塞IO也会进行recvform系统调用,检查数据是否准备好,与阻塞IO不一样,”非阻塞将大的整片时间的阻塞分成N多的小的阻塞, 所以进程不断地有机会 ‘被’ CPU光顾”。即每次recvform系统调用之间,cpu的权限还在进程手中,这段时间是可以做其他事情的,

      也就是说非阻塞的recvform系统调用调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error。进程在返回之后,可以干点别的事情,然后再发起recvform系统调用。重复上面的过程,循环往复的进行recvform系统调用。这个过程通常被称之为轮询。轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。

import time
import socket
sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sk.bind(('127.0.0.1',6667))
sk.listen(5)
sk.setblocking(False)
while True:
    try:
        print ('waiting client connection .......')
        connection,address = sk.accept()   # 进程主动轮询
        print("+++",address)
        client_messge = connection.recv(1024)
        print(str(client_messge,'utf8'))
        connection.close()
    except Exception as e: #如果客户端数据没有准备好,会返回一个错误被捕捉
        print (e)
        time.sleep(4) #等待4秒后继续while循环,轮询

############################client
import socket
sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

while True:
    sk.connect(('127.0.0.1',6667))
    sk.sendall(bytes("hello","utf8"))
    print("hello")
    break

优点:能够在等待任务完成的时间里干其他活了(包括提交其他任务,也就是 “后台” 可以有多个任务在同时执行)。

缺点:任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成(有可能数据准备刚好到sleep开始,这就不能及时的处理)。这会导致整体数据吞吐量的降低。

 

3、IO multiplexing(IO多路复用)

IO multiplexing这个词可能有点陌生,但是如果我说select,epoll,大概就都能明白了。有些地方也称这种IO方式为event driven IO。我们都知道,select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。它的流程如图:

当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
这个图和blocking IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。(如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)
在IO multiplexing Model中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。

注意1:select函数返回结果中如果有文件可读了,那么进程就可以通过调用accept()或recv()来让kernel将位于内核中准备到的数据copy到用户区。

注意2:select的优势在于可以处理多个连接,不适用于单个连接

import socket #导入socket网络模块
import select #导入多路复用模块

sk = socket.socket()
sk.bind(("127.0.0.1", 9904))
sk.listen(5)

while True:
    # select参数 ([输入, ],[输出, ],[错误, ],超时时间)
    # 输入数据给 r, 输出数据给w, 错误信息给e
    # 目前我们只看输入的数据,也就是我们的socket对象,列表中可以添加多个socket对象
    r, w, e = select.select([sk, ], [], [], 5)
    for i in r:
        conn,add=i.accept()
        data = conn.recv(1024)
        print(conn)
        print('hello')
    print('>>>>>>')


#######################client
import socket

sk = socket.socket()

sk.connect(("127.0.0.1", 9904))

IO多路复用有两个特别的系统调用select、poll、epoll函数。select调用是内核级别的,select轮询相对非阻塞的轮询的区别在于—前者可以等待多个socket,能实现同时对多个IO端口进行监听,当其中任何一个socket的数据准好了,就能返回进行可读,然后进程再进行recvfrom系统调用,将数据由内核拷贝到用户进程,当然这个过程是阻塞的。

补充:IO多路复用的触发方式是水平触发

如果

      conn,add=i.accept()

      data = conn.recv(1024)  

  没有这两条,那么内核的数据就还在,那么下次循环检测的时候就还是会赋给r这样就会重复运行。

水平触发:如果文件描述符已经就绪可以非阻塞的执行IO操作了,此时会触发通知.允许在任意时刻重复检测IO的状态, 没有必要每次描述符就绪后尽可能多的执行IO.select,poll就属于水平触发.

边缘触发:如果文件描述符自上次状态改变后有新的IO活动到来,此时会触发通知.在收到一个IO事件通知后要尽可能多的执行IO操作,因为如果在一次通知中没有执行完IO那么就需要等到下一次新的IO活动到来才能获取到就绪的描述符.信号驱动式IO就属于边缘触发.

epoll既可以采用水平触发,也可以采用边缘触发.

举例说明:一个管道收到了1kb的数据,epoll会立即返回,此时 读了512字节数据,然后再次调用epoll.这时如果是水平触发的,epoll会立即返回,因为有数据准备好了.如果是边缘触发的不会立即返回,因为此时虽然有数据可读但是已经触发了一次通知,在这次通知到现在还没有新的数据到来, 直到有新的数据到来epoll才会返回,此时老的数据和新的数据都可以读取到(当然是需要这次你尽可能的多读取).

物理底层理解:水平触发的底层原理就是当处于某个电平的时候触发,比如高电平触发。边缘触发的底层原理就是高低电平交替时候触发,比如高电平到低电平瞬间。

select进阶,不用多线程实现多客户端同时连接

import socket  #导入soket模块
import select
sk=socket.socket()
sk.bind(("127.0.0.1",8801))
sk.listen(5)
inputs=[sk,]  #建立一个文件描述符列表
while True:
    r,w,e=select.select(inputs,[],[],5) #select轮询检查列表内文件描述符有无变化,有变化就赋给r
    print(len(r))

    for obj in r: #循环从r中取出文件描述符
        if obj==sk: #判断obj是不是一个新的连接,如果是就走下面的流程
            conn,add=obj.accept()  #接受TCP客户端的连接
            print(conn)
            inputs.append(conn) #将连接加入到列表中,循环监听是否有新的数据到来
        else: #如果是一个已经建立好的连接走下面的流程
            data_byte=obj.recv(1024) 
            print(str(data_byte,'utf8'))
            inp=input('回答%s号客户>>>'%inputs.index(obj))
            obj.sendall(bytes(inp,'utf8'))

    print('>>',r)

#####################client
import socket
sk=socket.socket()
sk.connect(('127.0.0.1',8801))

while True:
    inp=input(">>>>")
    sk.sendall(bytes(inp,"utf8"))
    data=sk.recv(1024)
    print(str(data,'utf8'))

文件描述符其实就是咱们平时说的句柄,只不过文件描述符是linux中的概念。注意,我们的accept或recv调用时即向系统发出recvfrom请求

    (1)  如果内核缓冲区没有数据--->等待--->数据到了内核缓冲区,转到用户进程缓冲区;

    (2)  如果先用select监听到某个文件描述符对应的内核缓冲区有了数据,当我们再调用accept或recv时,直接将数据转到用户缓冲区。

 4、Asynchronous I/O(异步IO)

linux下的asynchronous IO其实用得很少。先看一下它的流程:

用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。

 

四种IO的区别:blocking IO和non-blocking IO区别在于,blocking IO会一直阻塞住对应的进程直到操作完成,而non-blocking IO在kernel还没准备数据的情况下会立刻返回。

同步IO和异步IO:同步I / O操作导致请求过程被阻塞,直到该I / O操作完成为止。异步I / O操作不会导致请求进程被阻塞;总结:但凡有阻塞的就是同步IO

按照这个定义,之前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO。

 

注意:由于咱们接下来要讲的select,poll,epoll都属于IO多路复用,而IO多路复用又属于同步的范畴,故,epoll只是一个伪异步而已。

各个IO Model的比较如图所示:

 

经过上面的介绍,会发现non-blocking IO和asynchronous IO的区别还是很明显的。在non-blocking IO中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。而asynchronous IO则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。

五种IO模型比较:

 

5、select poll epoll IO多路复用介绍

  • select 
    select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。 
    select目前几乎在所有的平台上支持 
      
    select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。 
      
    另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。
  • poll 
    它和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制。 
    一般也不用它,相当于过渡阶段

  • epoll 
    直到Linux2.6才出现了由内核直接支持的实现方法,那就是epoll。被公认为Linux2.6下性能最好的多路I/O就绪通知方法。windows不支持 

    没有最大文件描述符数量的限制。 
    比如100个连接,有两个活跃了,epoll会告诉用户这两个两个活跃了,直接取就ok了,而select是循环一遍。 

    (了解)epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。 
    另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。 

    所以市面上上见到的所谓的异步IO,比如nginx、Tornado、等,我们叫它异步IO,实际上是IO多路复用。

 实例1

import select
import socket
import sys
import queue

# Create a TCP/IP socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setblocking(False)
# 设置成非阻塞模式,accept和recv都非阻塞
# 如果没有连接会报错,有数据才调他们
# BlockIOError:[WinError 10035] 无法立即完成一个非阻塞性套接字操作。


# Bind the socket to the port
server_address = ('localhost', 10000)
print(sys.stderr, 'starting up on %s port %s' % server_address)
server.bind(server_address)

# Listen for incoming connections
server.listen(5)

# Sockets from which we expect to read
inputs = [server]
# 交给内核、select检测的列表。
# 必须有一个值,让select检测,否则报错提供无效参数。
# 没有其他连接之前,自己就是个socket,自己就是个连接,检测自己。活动了说明有链接

# Sockets to which we expect to write
outputs = []  # 你往里面放什么,下一次就出来了

message_queues = {} #存放链接接收到客户端的数据
while inputs:

    # Wait for at least one of the sockets to be ready for processing
    print('\nwaiting for the next event')
    readable, writable, exceptional = select.select(inputs, outputs, inputs) #定义检测
    #新来连接                                       检测列表        异常(断开)列表
    # Handle inputs
    # 异常的也是inputs是: 检测那些连接的存在异常
    for s in readable:

        if s is server: # 有数据,代表来了一个新连接
            # A "readable" server socket is ready to accept a connection
            connection, client_address = s.accept()
            print('new connection from', client_address)
            inputs.append(connection)  # 把连接加到检测列表里,如果这个连接活动了,就说明数据来了
            # 如果server活动,则来了新连接,conn活动则来数据
            # Give the connection a queue for data we want to send
            message_queues[connection] = queue.Queue() # 初始化一个队列,后面存要返回给这个客户端的数据
        else:
            try:
                data = s.recv(1024)  # 注意这里是s,而不是conn,多个连接的情况
                if not data:raise Exception
                # A readable client socket has data
                print(sys.stderr, 'received "%s" from %s' % (data, s.getpeername()))
                message_queues[s].put(data)   # 往里面放数据
                # Add output channel for response
                if s not in outputs:
                    outputs.append(s)  # 放入返回的连接队列里
            except Exception:
                # Interpret empty result as closed connection
                print('closing', client_address, 'after reading no data')
                # Stop listening for input on the connection
                if s in outputs:
                    outputs.remove(s)  # 既然客户端都断开了,我就不用再给它返回数据了,所以这时候如果这个客户端的连接对象还在outputs列表中,就把它删掉
                inputs.remove(s)  # inputs中也删除掉
                s.close()  # 把这个连接关闭掉
                # Remove message queue
                del message_queues[s] #清理已断开的连接
    # Handle outputs
    for s in writable: # 要返回给客户端的连接列表
        try:
            next_msg = message_queues[s].get_nowait()   # 在字典里取数据
        except queue.Empty:
            # No messages waiting so stop checking for writability.
            print('output queue for', s.getpeername(), 'is empty')
            outputs.remove(s)  # 删除这个数据,确保下次循环的时候不返回这个已经处理完的连接了。
        else:
            print('sending "%s" to %s' % (next_msg, s.getpeername()))
            s.send(next_msg) # 返回给客户端
            outputs.remove(s)  # 删除这个数据,确保下次循环的时候不返回这个已经处理完的连接了。
    # Handle "exceptional conditions"
    for s in exceptional: # 如果连接断开,删除连接相关数据
        print('handling exceptional condition for', s.getpeername())
        # Stop listening for input on the connection
        inputs.remove(s)
        if s in outputs:
            outputs.remove(s)
        s.close()

        # Remove message queue
        del message_queues[s]

###################client
import socket
sk=socket.socket()
sk.connect(('127.0.0.1',1234))

while True:
    inp=input(">>>>")
    sk.sendall(bytes(inp,"utf8"))
    data=sk.recv(1024)
    print(str(data,'utf8'))
selectors模块
import selectors
import socket

sel = selectors.DefaultSelector() #根据操作系统选一个最好的IO多路复用方式
#windows只能用select, linux可以用epoll

def accept(sock, mask):
    conn, addr = sock.accept()  # Should be ready
    print('accepted', conn, 'from', addr)
    conn.setblocking(False)
    # 将conn加入监听列表,同时注册绑定read方法
    sel.register(conn, selectors.EVENT_READ, read)

def read(conn, mask):
    try:
        data = conn.recv(1000)  # Should be ready
        if not data: raise ConnectionResetError
        print('echoing', repr(data), 'to', conn)
        conn.send(data)  # Hope it won't block
    except ConnectionResetError:
        print('closing', conn)
        sel.unregister(conn)
        conn.close()

sock = socket.socket()
sock.bind(('localhost', 1234))
sock.listen(100)
sock.setblocking(False)
sel.register(sock, selectors.EVENT_READ, accept) #注册
#sock就是监听对象,accept是绑定的执行的方法

while True:
    # events 是一个列表有两个元素
    events = sel.select() #定义检测
    # key是一个元组,里面包含了key.data(监听对象绑定的方法),key.fileobj 就是sock套接字对象
    for key, mask in events:
        print(key)
        # 首次新连接这个data就是accept
        callback = key.data
        # 首次新连接fileobj是sock对象。如果监听对象conn活动了,那fileobj就是conn
        callback(key.fileobj, mask)
        #首次新连接callback()等于执行accept()

#################client
import socket
sk=socket.socket()
sk.connect(('127.0.0.1',1234))

while True:
    inp=input(">>>>")
    sk.sendall(bytes(inp,"utf8"))
    data=sk.recv(1024)
    print(str(data,'utf8'))

 不用多线程,利用selector完成多线路文件上传下载

import socket
import selectors
import struct
import json
import os,sys

BASE_DIR = os.path.dirname(__file__)
sys.path.append(BASE_DIR)

class SelectFtpServer:
    def __init__(self):
        self.sock = socket.socket()
        self.sel = selectors.DefaultSelector()
        self.dic = {}
        # self.hasfilesize = 0
        # self.sendfilesize = 0
        self.create_socket()
        self.handle()

    def accept(self,sock,mask):
        conn, addr = sock.accept()
        print('from %s %s conneted' % addr)
        conn.setblocking(False)
        self.sel.register(conn,selectors.EVENT_READ,self.read)
        self.dic[conn] = {}

    def create_socket(self):
        self.sock.bind(('127.0.0.1', 8081))
        self.sock.listen(5)
        self.sock.setblocking(False)
        self.sel.register(self.sock, selectors.EVENT_READ, self.accept)
        print('服务器已经准备就绪,等待连接。。。。')

    def handle(self):
        while True:
            events = self.sel.select()
            for key, mask in events:
                callback = key.data
                callback(key.fileobj, mask)


    def read(self, conn, mask):
        if not self.dic[conn]:
            print('wait-----')
            head_dic = conn.recv(1024)
            cmd, filesize, filename = head_dic.decode('utf8').split('|')
            self.dic[conn] = {'cmd':cmd, 'filesize':filesize, 'filename':filename}
            if cmd == 'put':
                conn.send('ok'.encode('utf8'))
                self.dic[conn]['hasfilesize'] = 0
            if cmd == 'get':
                filepath = os.path.normpath(os.path.join(BASE_DIR, filename))
                self.dic[conn]['filepath'] = filepath
                if not os.path.isfile(filepath):
                    conn.send('804'.encode('utf8'))
                    self.dic[conn] = {}
                else:
                    filesize = os.path.getsize(filepath)
                    self.dic[conn]['filesize'] = filesize
                    self.dic[conn]['sendfilesize'] = 0
                    conn.send(str(filesize).encode('utf8'))
                    print('开始下载')

        else:
            if self.dic[conn].get('cmd',None):
                cmd = self.dic[conn].get('cmd')
                if hasattr(self, cmd):
                    func = getattr(self, cmd)
                    func(conn)
                else:
                    print('error cmd!')
                    conn.close()
            else:
                print('error cmd')
                conn.close()

    def put(self, conn):
        filename = self.dic[conn]['filename']
        filesize = int(self.dic[conn]['filesize'])
        path = os.path.normpath(os.path.join(BASE_DIR,filename))
        recv_data = conn.recv(1024)
        self.dic[conn]['hasfilesize'] += len(recv_data)
        with open(path, 'ab') as f:
            f.write(recv_data)

        if filesize == self.dic[conn]['hasfilesize']:
            if conn in self.dic.keys():
                self.dic[conn] = {}
                conn.send('ok'.encode('utf8'))
            print('%s上传成功'%filename)



    def get(self,conn):
        conn.recv(1024)
        filepath = self.dic[conn]['filepath']
        with open(filepath,'rb') as r:
            r.seek(self.dic[conn]['sendfilesize'])
            data = r.readline()
            self.dic[conn]['sendfilesize'] += len(data)
            conn.send(data)
        if self.dic[conn]['sendfilesize'] == self.dic[conn]['filesize']:
            if conn in self.dic.keys():
                self.dic[conn] = {}
            print('file download sucessful')



if __name__ == '__main__':
    SelectFtpServer()
import socket
import math
import os
import sys




meau = {1: 'put', 2: 'get'}
meau_str = '''
**************FTP**************
          1.FTP上传
          2.FTP下载
*******************************
'''


class SelectFtpClient:
    def __init__(self):
        self.hostport = ('127.0.0.1', 8081)
        self.create_sock()
        self.command_fanout()


    def create_sock(self):
        try:
            self.sock = socket.socket()
            self.sock.connect(self.hostport)
            print('connect server successful')
        except Exception as e:
            print(e)

    def command_fanout(self):
        while True:
            try:
                print(meau_str)
                strnum = int(input('请输入选项:'))
                if strnum in meau:
                    if hasattr(self, meau[strnum]):
                        fuc = getattr(self, meau[strnum])
                        fuc(meau[strnum])
                else:
                    raise Exception
            except Exception as e:
                print(e)
                print('请重新输入')

    def put(self,args):
        cmd = args
        while True:
            filename = input('请输入上传的文件名(xx.txt)>>').strip()
            if not os.path.isfile(filename):
                print('{}文件不存在'.format(filename))
                continue
            else:
                filesize = os.path.getsize(filename)
                head_dic = '%s|%s|%s'%(cmd,filesize,filename)
                self.sock.send(bytes(head_dic,encoding='utf-8'))
                recv_staut = self.sock.recv(1024).decode('utf8')
                if recv_staut == 'ok':
                    send_size = 0
                    with open(filename, 'rb') as f:
                        for line in f:
                            self.sock.send(line)
                            send_size += len(line)
                            part = filesize / 50  # 1%数据的大小
                            count = math.ceil(send_size / part)
                            print('\r')
                            sys.stdout.write(('[%-50s]%.2f%%' % (('>' * count), send_size / filesize * 100)))
                    recv_sucss = self.sock.recv(1024).decode('utf8')
                    if recv_sucss == 'ok':
                        print('\n文件上传成功', filename)
                    else:
                        print('\n文件上传失败', filename)
                    return
    def get(self,args):
        cmd = args
        while True:
            filename = input('[输入要下载的文件名]')
            head_dic = '%s|%s|%s' % (cmd, '', filename)
            self.sock.sendall(head_dic.encode('utf8'))
            res = self.sock.recv(1024).decode('utf-8')
            if res == '804':
                print('文件不存在,请输入准确的文件名')
                continue
            filesize = int(res)
            recv_size = 0
            with open(filename, 'wb') as f:
                while recv_size < filesize:
                    self.sock.send('1'.encode('utf8'))
                    data = self.sock.recv(8192)
                    f.write(data)
                    recv_size += len(data)
                    part = filesize / 50  # 1%数据的大小
                    count = math.ceil(recv_size / part)
                    sys.stdout.write('\r')
                    sys.stdout.write(('[%-50s]%.2f%%' % (('>' * count), recv_size / filesize * 100)))
            print('\n文件接收成功', filename)
            return


if __name__ == '__main__':
    SelectFtpClient()

 

 

 

posted @ 2020-05-04 15:36  liqinsan  阅读(188)  评论(0)    收藏  举报