python 学习笔记九 队列,异步IO
queue (队列)
队列是为线程安全使用的。
1.先入先出
- import queue
- #测试定义类传入队列
- class Foo(object):
- def __init__(self,n):
- self.n = n
- new = queue.Queue(maxsize=)
- new.put()
- new.put(Foo(),timeout=) # 超时时间后,抛出队列full异常
- new.put([, , ],timeout=)
- print(new.full()) #判断队列是否满 True
- #new.put("abc",timeout=) #队列已满,再放报错
- print(new.qsize()) # 查看当前队列长度
- print(new.get())
- print(new.get())
- print(new.get())
- print(new.empty()) #判断队列是否为空 True
- #print(new.get_nowait()) #队列已空,取不到数据报异常
2.后进先出
- q = queue.LifoQueue() #指定使用LifoQueue
- q.put()
- q.put()
- print(q.get_nowait())
- print(q.get_nowait())
- “""
- """
3.优先级队列
存入一个元组,第一个为优先级,第二个为数据,第三个默认超时时间
- import queue
- new = queue.PriorityQueue(maxsize=)
- new.put((,[,,]))
- new.put((,"strings"))
- new.put((,"strings"))
- print(new.get_nowait())
- print(new.get_nowait())
- print(new.get_nowait())
- “”“
- (, [, , ])
- (, 'strings')
- (, 'strings')
- “”“
生成者消费者模型
通过queue.task_done 和 queue.join 实现
一对一
- import threading, queue, time
- #生产者消费者模型为了程序松耦合,
#生产者生产消息- def consumer(n):
- while True:
- print("\033[32;1m consumer [%s] \033[0m get task: %s" % (n, q.get()))
- time.sleep() # 每秒吃一个
- q.task_done() # q.get()1次通知队列减少1
#消费者消费消息- def producter(n):
- count =
- while True:
- print("producter [%s] produced a new task : %s" % (n, count))
- q.put(count)
- count +=
- q.join() #消息阻塞 队列为空,重新触发
- print("all task has been cosumed by consumers ...")
- q = queue.Queue()
c1 = threading.Thread(target=consumer, args=[1, ])
p1 = threading.Thread(target=producter, args=["p1", ])
c1.start()
p1.start()- #result:
producter [p1] produced a new task : 1 #生产一个消息- consumer [] get task: 1 #消费一个消息,q.task_done() 通知队列减少1个消息
- all task has been cosumed by consumers ... #q.join() 收到队列为空,开始生产消息
- producter [p1] produced a new task :
- consumer [] get task:
- all task has been cosumed by consumers ...
- producter [p1] produced a new task :
- consumer [] get task:
- all task has been cosumed by consumers ...
- producter [p1] produced a new task :
- consumer [] get task:
- all task has been cosumed by consumers ...
- producter [p1] produced a new task :
- consumer [] get task:
一对多
- def consumer(n):
while True:
print("\033[32;1m consumer [%s] \033[0m get task: %s" % (n, q.get()))
time.sleep(1) # 每秒吃一个
q.task_done() # get()1次通知队列减少1- def producter(n):
count = 1
while True:
print("producter [%s] produced a new task : %s" % (n, count))
q.put(count)
count += 1
q.join() #消息阻塞 队列为空重新触发
print("all task has been cosumed by consumers ...")- q = queue.Queue()
c1 = threading.Thread(target=consumer, args=[1, ])
c2 = threading.Thread(target=consumer, args=[2, ])
c3 = threading.Thread(target=consumer, args=[3, ])
p1 = threading.Thread(target=producter, args=["p1", ])
- c1.start()
c2.start()
c3.start()
p1.start()- result:
- producter [p1] produced a new task : 1
consumer [1] get task: 1
all task has been cosumed by consumers ...
producter [p1] produced a new task : 2
consumer [2] get task: 2
all task has been cosumed by consumers ...
producter [p1] produced a new task : 3
consumer [3] get task: 3
all task has been cosumed by consumers ...
producter [p1] produced a new task : 4
consumer [1] get task: 4
all task has been cosumed by consumers ...
producter [p1] produced a new task : 5
consumer [2] get task: 5
all task has been cosumed by consumers ...
producter [p1] produced a new task : 6
consumer [3] get task: 6
all task has been cosumed by consumers ...
producter [p1] produced a new task : 7
consumer [1] get task: 7
all task has been cosumed by consumers ...
producter [p1] produced a new task : 8
consumer [2] get task: 8
all task has been cosumed by consumers ...
producter [p1] produced a new task : 9
consumer [3] get task: 9
all task has been cosumed by consumers ...
producter [p1] produced a new task : 10
consumer [1] get task: 10
多对多
- def consumer(n):
- while True:
- print("\033[32;1m consumer [%s] \033[0m get task: %s" % (n, q.get()))
- time.sleep() # 每秒吃一个
- q.task_done() # get()1次通知队列减少1
- def producter(n):
- count =
- while True:
- print("producter [%s] produced a new task : %s" % (n, count))
- q.put(count)
- count +=
- q.join() #消息阻塞 队列为空重新触发
- print("all task has been cosumed by consumers ...")
- q = queue.Queue()
- c1 = threading.Thread(target=consumer, args=[, ])
- c2 = threading.Thread(target=consumer, args=[, ])
- c3 = threading.Thread(target=consumer, args=[, ])
- p1 = threading.Thread(target=producter, args=["p1", ])
- p2 = threading.Thread(target=producter, args=["p2", ])
c1.start()
c2.start()
c3.start()
p1.start()
p2.start()
- result:
- producter [p1] produced a new task : 1
producter [p2] produced a new task : 1
consumer [1] get task: 1
consumer [2] get task: 1
all task has been cosumed by consumers ...
all task has been cosumed by consumers ...
producter [p1] produced a new task : 2
producter [p2] produced a new task : 2
consumer [3] get task: 2
consumer [2] get task: 2
all task has been cosumed by consumers ...
producter [p1] produced a new task : 3
consumer [1] get task: 3
all task has been cosumed by consumers ...
producter [p2] produced a new task : 3
consumer [2] get task: 3
all task has been cosumed by consumers ...
producter [p1] produced a new task : 4
consumer [3] get task: 4
all task has been cosumed by consumers ...
all task has been cosumed by consumers ...
producter [p1] produced a new task : 5
producter [p2] produced a new task : 4
consumer [1] get task: 5
consumer [2] get task: 4
all task has been cosumed by consumers ...
all task has been cosumed by consumers ...
协程
协程,又称微线程,纤程。英文名Coroutine。一句话说明什么是线程:协程是一种用户态的轻量级线程。
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:
协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。
协程的好处:
- 无需线程上下文切换的开销
- 无需原子操作锁定及同步的开销
- 方便切换控制流,简化编程模型
- 高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。
缺点:
- 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
- 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序(可以通过生产者消费者模型解决)
使用yield模拟协程
- #使用yield实现在单线程的情况下实现并发运算的效果
- import time
- def consumer(name):
- print("%s 准备吃包子!"%name)
- while True:
- baozi = yield #yield接收返回值
- print("包子[%s]来了,被[%s]吃了!" %(baozi,name)
- def producer(name):
- c.__next__()
- d.__next__()
- print("开始生产包子!")
- for i in range():
- time.sleep()
- print("做了2个包子!")
- c.send(i) #发送给yield
- d.send(i)
- if __name__ == '__main__':
- c = consumer("c1")
- d = consumer("c2")
- p = producer()
- result:
- c1 准备吃包子! #实例化消费者,遇到yield函数冻结接收yield返回值
c2 准备吃包子!
开始生产包子!
做了2个包子!
包子[0]来了,被[c1]吃了! #函数继续执行
包子[0]来了,被[c2]吃了!
做了2个包子!
包子[1]来了,被[c1]吃了!
包子[1]来了,被[c2]吃了!
做了2个包子!
包子[2]来了,被[c1]吃了!
包子[2]来了,被[c2]吃了!
做了2个包子!
包子[3]来了,被[c1]吃了!
包子[3]来了,被[c2]吃了!
做了2个包子!
包子[4]来了,被[c1]吃了!
包子[4]来了,被[c2]吃了!
grentlet
- from greenlet import greenlet
- def test1():
- print()
- gr2.switch() #手动切换
- print()
- gr2.switch()
- def test2():
- print()
- gr1.switch()
- print()
- gr1 = greenlet(test1) #加入协程
- gr2 = greenlet(test2)
- gr1.switch()
- """
- """
Gevent
- import gevent
- def foo():
- print('\033[32;1m Running in foo\033[0m')
- gevent.sleep()#阻塞1秒
- print('\033[32;1m Explicit context switch to foo again\033[0m')
- def bar():
- print('\033[31;1m Explicit context to bar\033[0m')
- gevent.sleep()
- print('\033[31;1m Implicit context switch back to bar\033[0m')
- def boom():
- print('\033[33;1m just boom \033[0m')
- gevent.sleep()
- print('\033[33;1m boom shakashaka \033[0m')
- gevent.joinall(
- [ #将foo加入协程,协程间切换不会按照顺序而是随机切换
- gevent.spawn(foo),
- gevent.spawn(bar),
- gevent.spawn(boom)
- ]
- )
- result:
- Running in foo
Explicit context to bar
just boom
Explicit context switch to foo again
boom shakashaka
Implicit context switch back to bar
"""
gevent实现遇到io阻塞自动切换
- from gevent import monkey; monkey.patch_all()
- import gevent
- from urllib.request import urlopen
- def f(url):
- print('GET: %s' % url)
- resp = urlopen(url)
- data = resp.read()
- print('%d bytes received from %s.' % (len(data), url))
- gevent.joinall([
- gevent.spawn(f, 'https://www.python.org/'),
- gevent.spawn(f, 'https://www.yahoo.com/'),
- gevent.spawn(f, 'https://github.com/'),
- ])
- result:
"""
GET: https://www.python.org/
GET: https://www.yahoo.com/
GET: https://github.com/
46958 bytes received from https://www.python.org/.
24121 bytes received from https://github.com/.
413706 bytes received from https://www.yahoo.com/.
"""
gevent 实现多线程socketserver
- import sys,time,gevent,socket
- from gevent import socket, monkey
- #将所有遇到的阻塞变为非阻塞
- monkey.patch_all()
- def server(port):
- s = socket.socket()
- s.bind(("0.0.0.0", port))
- s.listen()
- while True:
- cli, addr = s.accept()
- #派生协程 执行handle_request函数 将客户端socket连接传参
- gevent.spawn(handle_request, cli)
- def handle_request(s):
- try:
- while True:
- data = s.recv()
- print("recv:", data.decode("utf8"))
- s.send(data)
- if not data:
- #如果客户端断开连接此处没有效果,如果服务端断开连接,通知服务端去掉该连接
- s.shutdown(socket.SHUT_WR)
- except Exception as e:
- print(e)
- finally:
- s.close()
- if __name__ == "__main__":
- server(8001)
- import socket
- HOST = 'localhost' # The remote host
- PORT = # The same port as used by the server
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.connect((HOST, PORT))
- while True:
- msg = bytes(input(">>:"),encoding="utf8")
- s.sendall(msg)
- data = s.recv()
- #print(data)
- print('Received', repr(data))
- s.close()
select、poll、epoll
select
select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,
使得进程可以获得这些文件描述符从而进行后续的读写操作。
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,事实上从现在看来,这也是它所剩不多的优点之一。
select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。
另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。(解读:在select/poll时代,服务器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),
让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll一般只能处理几千的并发连接。)
同时,由于网络响应时间的延迟 使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。
poll
poll在1986年诞生于System V Release 3,它和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制。
poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,
它的开销随着文件描述符数量的增加而线性增大。另外,select()和poll()将就绪的文件描述符告诉进程后,如果进程没有对其进行IO操作,那么下次调用select()和poll()
的时候将再次报告这些文件描述符,所以它们一般不会丢失就绪的消息,这种方式称为水平触发(Level Triggered)。
epoll
直到Linux2.6才出现了由内核直接支持的实现方法,那就是epoll,它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。
epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,
这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。
epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表 就绪描述符数量的值,
你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了 这些文件描述符在系统调用时复制的开销。
另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描 述符进行扫描,而epoll事先通过epoll_ctl()
来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调 机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。
select实现多线程socketserver实例:
- #server端
- import select
- import queue
- import socket
- import sys
- #生成服务端socket实例
- server = socket.socket()
- #设置非阻塞 传入bool类型
- server.setblocking(0)
- #设置绑定的ip地址和端口
- server_address = ('localhost', 10000)
- print(sys.stderr, 'starting up on %s port %s' % server_address)
- server.bind(server_address)
- # 监听客户端最大连接数
- server.listen(5)
- #初始化读取数据的监听列表,最开始时希望从server这个套接字上读取数据
- inputs = [server, ]
- #初始化写入数据的监听列表,最开始并没有客户端连接进来,所以列表为空
- outputs = []
- #消息队列用字典表示 键为客户端socket对象,值为发送内容 可能有多个客户端连接,发送多条信息,将消息先存入队列而不是直接发送
- message_queues = {}
- #inputs列表默认存放server 即服务端的socket对象用于等待客户端接入
- while inputs:
- print(sys.stderr, '\nwaiting for the next event')
- # 注:select能够监控 f=open(),obj=socket(),sys.stdin,sys.stdout终端输入输出(所有带fileno()方法的文件句柄)
- #文件操作是python无法检测的,windows也不支持终端输入输出的文件句柄(OSError:应用程序没有调用 WSAStartup,或者 WSAStartup 失败。)
- readable, writeable, exceptional = select.select(inputs, outputs, inputs)
- #一旦客户端连接,server的内容将改变,select检测到server的变化,将其返回给readable
- for s in readable:
- #默认只有server,等待客户端连接,但是有了client的socket对象后,等待的可能是客户端发送的消息,这里需要判断是socket还是消息
- if s is server:
- #创建客户端socket连接 connection 服务端为客户端生成的socket对象,client_address 客户端地址
- connection, client_address = s.accept()
- print(sys.stderr, 'new connection from', client_address)
- #客户端socket设置非阻塞
- connection.setblocking(0)
- # 因为有读操作发生,所以将此连接加入inputs
- inputs.append(connection)
- # 为每个连接创建一个queue队列,数据并不是立即发送需要放入队列,等待outputs队列有数据才发送,同时确保每个连接接收到正确的数据。
- message_queues[connection] = queue.Queue()
- #等待的将是客户端发送的数据
- else:
- #接收客户端数据
- data = s.recv(1024)
- if data:
- print(sys.stderr, 'received "%s" from %s' % (data, s.getpeername()))
- # 将收到的数据放入队列中
- message_queues[s].put(data)
- if s not in outputs:
- # 将socket客户端的连接加入outputs中,并且用来给客户端返回数据。
- outputs.append(s)
- else:#连接已经断开
- print(sys.stderr, 'closing', client_address, 'after reading no data')
- if s in outputs:
- #因为连接已经断开,无需再返回消息,这时候如果这个客户端的连接对象还在outputs列表中,就把它删掉。
- outputs.remove(s)
- #连接已经断开,在inputs中select也不用感知
- inputs.remove(s)
- #关闭会话
- s.close()
- #从字典中删除服务端为客户端建立连接的socket对象
- del message_queues[s]
- #一旦有参数,将一直为客户端返回数据
- for s in writeable:
- try:
- #读取客户端请求信息,采用非阻塞的方式get_nowait() 没有读取到数据抛出异常
- next_msg = message_queues[s].get_nowait()
- except queue.Empty:#引发队列空异常
- print(sys.stderr, 'output queue for', s.getpeername(), 'is empty')
- #没有读取到数据,无需为客户端返回消息,将其从outputs中删除,否则 select将一直感知,并传给writeable
- outputs.remove(s)
- else:#没有任何异常
- print(sys.stderr, 'sending "%s" to %s' % (next_msg, s.getpeername()))
- #此处是服务端原样返回接收到的信息
- s.send(next_msg)
- #如果服务端或客户端连接发生错误,exceptional将会有内容
- for s in exceptional:
- print(sys.stderr, 'handling exceptional condition for', s.getpeername())
- #将客户端连接删除
- inputs.remove(s)
- #如果还有数据未发完
- if s in outputs:
- #但是连接已经断开,只好从outputs删除
- outputs.remove(s)
- #关闭会话
- s.close()
- #删除该客户端连接队列,无须在发送数据了。
- del message_queues[s]
- #client
- import socket
- import sys
- #消息列表
- messages = ['this is the message. ',
- 'It will be sent ',
- 'in parts.',
- ]
- #ip_port
- server_address = ('localhost', 10000)
- #socket对象
- socks = [socket.socket(socket.AF_INET, socket.SOCK_STREAM),
- socket.socket(socket.AF_INET,socket.SOCK_STREAM),
- ]
- print(sys.stderr, 'connecting to %s port %s' % server_address)
- #发起连接
- for s in socks:
- s.connect(server_address)
- #发送消息
- for message in messages:
- for s in socks:
- print(sys.stderr, '%s: sending "%s"' % (s.getsockname(), message))
- #发送请求
- s.send(bytes(message, "utf8"))
- for s in socks:
- try:
- #接收信息
- data = s.recv(1024)
- print(sys.stderr, '%s: received "%s"' % (s.getsockname(), data))
- except Exception as e:
- print(e, 'closing socket', s.getsockname())
- #未收到回应,连接终止
- s.close()
更多详细内容:http://www.cnblogs.com/wupeiqi/articles/5040823.html
python 学习笔记九 队列,异步IO的更多相关文章
- Python学习笔记九
Python学习笔记之九 为什么要有操作系统 管理硬件,提供接口. 管理调度进程,并且将多个进程对硬件的竞争变得有序. 操作系统发展史 第一代计算机:真空管和穿孔卡片 没有操作系统,所有的程序设计直接 ...
- python学习笔记10--协程、IO、IO多路复用
本节内容 一.协程 1.1.协程概念 1.2.greenlet 1.3.Gevent 1.4.协程之爬虫 1.5.协程之socket 二.论事件驱动与异步IO 三.IO 3.1.概念说明 3.2.IO ...
- Python学习笔记 - day14 - Celery异步任务
Celery概述 关于celery的定义,首先来看官方网站: Celery(芹菜) 是一个简单.灵活且可靠的,处理大量消息的分布式系统,并且提供维护这样一个系统的必需工具. 简单来看,是一个基于pyt ...
- python学习笔记(九)、模块
1 模块 使用import 语句从外部导入模块信息,python提供了很大内置模块.当你导入模块时,你会发现其所在目录中,除源代码文件外,还新建了一个名为__pycache__的子目录(在较旧的Pyt ...
- Java NIO学习笔记九 NIO与IO对比
Java NIO与IO Java nio 和io 到底有什么区别,以及什么时候使用nio和io,本文做一个比较. Java NIO和IO之间的主要区别 下表总结了Java NIO和IO之间的主要区别, ...
- python学习笔记九——序列
4.4 序列 序列是具有索引和切片能力的集合.元组.列表和字符串具有通过索引访问某个具体的值,或通过切片返回一段切片的能力,因此元组.列表和字符串都属于序列.序列索引功能演示: tuple=(&quo ...
- Python学习笔记九:装饰器,生成器,迭代器
装饰器 本质是函数,装饰其他函数,为其他函数添加附加功能 原则: 1不修改原函数的源代码 2不修改原函数的调用方式 知识储备: 1函数即变量 使用门牌号的例子说明函数,调用方式与变量一致 2高阶函数 ...
- Python学习笔记九-文件读写
1,读取文件: f=open('目录','读写模式',encoding='gbk,error='egiong') 后三项可以不写但是默认是' r'读模式:open函数打开的文件对象会自动加上read( ...
- Python学习笔记九:正则表达式
一:正则表达式的符号与方法 常用符号: .:匹配任何一个字符,换行符除外(所以,多行字符串中的匹配要特殊处理,见下面实例) *:匹配前一个字符0次或多次 +:匹配前一个字符1次或多次 ?:匹配前一个字 ...
随机推荐
- ssh框架开发问题
Struts + spring MVC + hibernate 6.1 从职责上分为表示层.业务逻辑层.数据持久层和域模块层四层. 其中使用Struts作为系统的整体基础架构,负责MVC的分离 ...
- Unity3D着色器Shader编程入门(一)
自学Unity3D也有大半年了,对Shader一直不敢入坑,最近看了些资料,以及通过自己的实践,对Shader还是有一点了解了,分享下仅作入门参考. 因Shader是对图像图像渲染的,学习前可以去了解 ...
- js 查找关键字
查找:4种: 1. 查找固定关键字,仅返回位置,可指定开始位置: var i=str.indexOf("kword"[,starti]); str.lastIndexOf(&quo ...
- MUI - 侧滑菜单
各大APP必备的侧滑菜单栏,支持手势滑动.包含QQ式.美团式等 结构模板 这里是示例Html, 必须使用Mui框架才能使用. 主容器 <div class="mui-off-canva ...
- Android课程---关于ListView列表视图的学习
activity_ui3.xml <?xml version="1.0" encoding="utf-8"?> <ListView xmlns ...
- Windows2003中IIS的安全设置技巧
在Windows Server 2003中对于IIS的安全设置具有十分重要的意义,所以掌握IIS安全设置的六大技巧是一个网管员必备的基本技能.下面就是对IIS的安全设置的六大技巧. 技巧1.安装系统补 ...
- vba 工作案例-sheet间拷贝内容
核心代码就是Copy Destination. Sub copy_data() ' ' copy_data 宏 ' ' Dim fzjgs() As Variant Dim cities As Var ...
- [转]JNIEnv解析
1.关于JNIEnv和JavaVM JNIEnv是一个与线程相关的变量,不同线程的JNIEnv彼此独立.JavaVM是虚拟机在JNI层的代表,在一个虚拟机进程中只有一个JavaVM,因此该进程的所有线 ...
- Json与类对象转换
Json在js,jquery中可以直接使用,比如下串: { "from":"en" ,"to":"zh" ," ...
- Windows下mock环境搭建-加速项目Api开发
本文来自http://blog.csdn.net/liuxian13183/ ,引用必须注明出处! 公司进行技术部拆分,以项目制作为新的开发模式,前端+移动端+后端,于是加速Api开发变得很有必要,准 ...