Wiesler

导航

Day 9~10 文档归类,基于Socket的网络编程,线程,进程,协程

文档归类

lib文件夹:对于一个项目,可以创建一个lib文件夹,里面放上.py文件作为模块,专门用来存储类库信息,导入这个模块,需要哪个类就从这个模块导入

 

db文件夹:该文件夹下存放所有的数据库或保存数据的文件,用户登录信息等,从这个文件夹内的文件中去取.


config文件夹: 创建一个settings.py保存所有的配置信息。在我们将所有的数据库文件挪到db文件夹后,我们的系统中需要调用这些文件的时候,就需要很多路径信息,这些都是常量。可以在settings.py中保存这些路径信息   

import os
BASE_DIR = os.path.dirname(os.path.dirname(__file__))  # 能看到bin, db, lib, config的根目录
User = os.path.join(BASE_DIR, “db”, “user.db”)   # os.path.join会连接文件夹层次,自动转为路径字符串

这样是非常高效和保险的,因为路径信息都保存在这里,如果有变动,只修改这一个就可以了,不需要再到文件中去查找修改


log文件夹:日志目录。日志可以在文件中,或者另外建一个log文件夹


bin文件夹:放可执行文件


当我们对文件夹进行分类之后,在执行文件中,除了导入相应的包之外,我们还要将上级目录添加到sys.path

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

这是将应用的根目录添加进了sys.path, (也就是我们能看到bin, lib, log, 等的外层文件夹),这样就可以在可执行文件如main.py中用from lib import ... 了,否则是找不到lib的

 

在2.7中用pycharm创建的普通的文件夹和创建的package文件夹是不同的。package文件夹会默认创建一个__init__.py文件,方便导入模块。

导入模块的方法再汇总:

# import法
import module  # 引入一个特定模块, 后续程序用module.f1()来调用module模块中内容
import lib.test.com.commons  # 从lib下的test下的com下的目录引入了commons.py模块,后续调用必须写全 lib.test.com.commons.f1()
import lib.test.com.commons as X # 论重命名的魅力:和上述引入的一样,但是重命名为X,后续只要X.f1()调用即可
from module.aa.bb import cc  # 从module目录到下属的aa目录,再下属的bb目录中引入cc.py模块, 后续只要cc.f1()调用即可
from module.aa.bb import cc as X   # 引入cc模块后命名为X, 后续程序可以用X.f1()来调用这个模块内的方法
from module.aa.bb import *  # 从module目录下的aa目录下的bb目录中引入所有该目录的模块

# 反射__import__法
X = __import__("cc")  # 引入cc.py模块, 重命名为module,后续就可以用X.f1()来调用cc模块内的方法
X = __import__("lib.test.com.commons", fromlist=True)  # 多级目录导入:从lib下的test下的com下的commons.py模块,
# 注意这个反射法导入多级目录一定要加 fromlist=True

# 检测模块内方法存在否hasattr(obj, attr)
print(hasattr(__import__("commons"), "f5"))  # 在commons.py模块中是否有f5这个方法
# 获取模块内的方法getattr(obj, attr)
dd = __import__("commons")
func = getattr(dd, "f5", "Not Found")  # 在commons模块中找名为f5的内容,找不到就返回Not Found

导入模块时我们的路径从哪开始写呢?

我们的路径都是以import sys   中sys.path里为主的,在这个路径列表下的目录都可以直接找到。如果这个路径列表中任意一个路径里直接看不到你第一个写的文件夹,

那么就需要将路径添加进入sys.path使用sys.path.append()   os.path.join(path1, path2)

__file__为当前文件的路径 E:\python\demo1.py 默认情况下,当前文件的父母类在执行时已经被添加到sys.path中了

print(__file__)  # E:/python/demo3.py
print(sys.path)  # ['E:\\python', ......]

如果文件在E:/python/day01/bin/demo1.py  那么day01就是根目录,根目录下可见其他文件夹如config,lib等, 此时E:/python/day01/bin已经存在sys.path了,要方便引入其他文件夹中的模块,需要将根目录添加进sys.path中

root = os.path.dirname(os.path.dirname(__file__)) 就是E:/python/day01目录,此时引入别的文件夹中的模块就可以用 import lib.module as X

 

网络编程

基于Socket的网络编程

socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过"套接字"向网络发出请求或者应答网络请求

socket和file的区别:

  • file模块是针对某个文件【打开】【读写】【关闭】
  • socket模块是针对服务器和客户端的【打开】【读写】【关闭】

Python支持,TCP连接(连接后发送, 通信时有收发确认的reliable机制),UDP连接(不需连接,直接发送,无收发确认,unreliable,类似电话),Unix的进程间的连接

socket的网络连接,在没有select支持下,每次只能有一个用户连接,其余用户需要等待,类似只有一个客服人员

服务端 (两个while,外部的维持server运行,随时接受客户连接,内部持续接收数据)

import socket

sk = socket.socket()
sk.bind(("127.0.0.1", 9999, ))  # 接受元祖,放一个ip地址和端口号
sk.listen(5)  # 监听,后边最多排5个等待的用户

while True:  # 服务端永远等待,客户端每连接一次就打印一次
    conn, address = sk.accept()  # 接收客户端的请求,这.accept()的方法会阻塞,如果没有人连接就永远等在这不往下执行
    # 获得的是个两元素的数组,conn获取了这个建立起来的连接
    # address是客户端的地址信息
    conn.sendall(bytes("Welcome connect our FTP", encoding="utf-8"))  # 发送一个欢迎语
    file_size = str(conn.recv(1024), encoding="utf-8")  # 接受客户端发来的文件大小
    conn.sendall(bytes("ack", encoding="utf-8"))  # 回复客户端一个ack信号,表示我收到了文件大小,此时服务端处于recv等待状态,收到了ack后再传文件,有效解决了粘包问题
    total_size = int(file_size)
    received_size = 0
    f = open('new.pdf', "wb")
    while True:
        if total_size == received_size:  # 接收文件内容,直到获取完毕
            break
        data = conn.recv(1024)  # 服务端接收客户端的数据
        f.write(data)
        received_size += len(data)

    f.close()

客户端

import socket
import os
obj = socket.socket()
obj.connect(("127.0.0.1", 9999))  # 与服务端建立了连接
result = obj.recv(1024)  # 接收消息,表示最多接收1024字节,这是接收刚开始的欢迎语
# 这个recv也是阻塞的,如果服务端没有conn.sendall()没有内容接收就会永远等待

# 先发送当前文件字节大小
length = os.stat("old.pdf").st_size
obj.sendall(bytes(str(length), encoding="utf-8"))
ret_str = str(result, encoding="utf-8")
sent_size = 0
print(ret_str)
obj.recv(1024)  # 接收ack信号,并不做处理,只是等待,防止粘包
with open("old.pdf", "rb") as f:
    for line in f:
        obj.sendall(line)
        sent_size += len(line)
        progress_bar(sent_size, length, 0.003)

obj.close()

注意:

1 客户端和服务端都需要import socket, server = socket.socket()     client = socket.socket()

2服务端绑定server.bind(("127.0.0.1", 9999)) 绑定了IP, 网络上的本机IP为127.0.0.1和端口号9999,用元组绑定。并且之后要server.listen(k) 表示最多可以有k个用户等待

客户端client.connect(("127.0.0.1", 9999)) 连接相同的IP和端口才能建立连接

3服务端 while True: conn, address = server.accept() 这个accept()方法会阻塞在这里,相当于服务端的接线员等在电话旁,有客户端连接了,才会通,程序继续向下执行

客户端的client.recv(1024)或者服务端的conn.recv(1024)表示每次最多接收1024个字节. 这个.recv(1024)也是阻塞的

相当于两个人打电话:一个人客户端或服务端client/conn.sendall("hello"),对方就要接收.recv(1024), 如果听不到对方的声音或发送,就持机等待,一直到对方说话了,接受到了内容,程序再向下

4 上述可知,客户端服务端类似于人与人的交流,你一言我一语,所以任何一方.sendall() 另外一方一定要有相应的.recv(1024)来接收内容

写客服双方时,.sendall()和.recv(1024)的数量是一样的,一一对应的,听完了说,说完了听...... 

5 在py2.7中客服双方的收发.sendall()   .recv(1024)都是字符串类型,可以直接收发,而在py3.5中.sendall()和.recv(1024)都只能收发字节类型bytes型

原因:其实py2.7中也要字节来收发,只是py2.7中没有bytes类型,所以字节也被当成了字符串,所以py3.5中更规范而已

使用:

client或conn.sendall(bytes(字符串, encoding="utf-8"))    
str(client或conn.recv(1024), encoding="utf-8")

6 粘包问题的由来和解决方案:

我们传输文件的时候,接收方一定要知道文件的大小,这样在接收完毕的时候能够及时关闭接收,保证文件的完整性,多接收或者少接收都会导致打不开文件。

当从客户端上传至服务端(upload), 或者由服务端下载到客户端(download)时,文件大小的获取需要  length = os.stat(file_path).st_size 返回字节数

问题:我们发了文件大小,被另一方收到后,接着就立即发文件,有可能会让收到的文件大小和文件内容混在一起。(在发送时都是发到一个缓冲区,再由缓冲区发送,缓冲区的发送频率未知,虽然我们分开写了.sendall发送,但也很可能将文件大小和文件的一部分一并发送)

解决:当其中一方发送了文件大小后,进行.recv(1024)等待对方的回应,同时也保证缓冲区清空, 对方收到了文件大小,再回复.sendall(bytes("ack", encoding="utf-8"))表示收到了,可以开始发文件了

7 转码问题

Python27里发送.sendall, 接收.recv都是字符串,Python35里发送接收都是字节类型

因为27里没有严格意义上的字节类型,其字节类型本质就是字符串,而在35里就有了bytes字节类型

str(字节类型,encoding=”utf-8”)字节转字符串

字节.decode(encoding=”utf-8”)字节转字符串

 

字符串.encode(encoding=”utf-8”)字符串转字节

bytes(字符串,encoding=”utf-8”)字符串转字节


在py35中.sendall   .recv操作的都是字节,但是依然可以用gbk字节发送,接收时也要用gbk字节

转换字符串和编码时就要用encoding=”gbk”

8 客户端用for循环,一行一行地读文件并.sendall,服务端再开一个while循环,来比对接收到的文件的长度len(received_file)是否与文件大小一致,一致的话就关闭接收

9 最后双方都要.close(), 而服务端就可以为下一个用户服务了,服务端的.close()是在第一个while内的

Web服务应用:Web框架也基于下列代码实现

#!/usr/bin/env python
#coding:utf-8
import socket
 
def handle_request(client):
    buf = client.recv(1024)
    client.send("HTTP/1.1 200 OK\r\n\r\n")
    client.send("Hello, World")
 
def main():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # IPv4形式的TCP通信,实际都是默认值,可以sock = socket.socket()
    sock.bind(('localhost',8080))
    sock.listen(5)
 
    while True:
        connection, address = sock.accept()  # 有人连接之后就执行handle_request(connection)方法
        handle_request(connection)
        connection.close()  # 通信结束后关闭通信,等待下一个用户连接
 
if __name__ == '__main__':
  main()

 

更多功能:

socket.socket()还可以接收不同的参数,产生不同的结果

sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)

参数一:地址簇:

socket.AF_INET   表示IPv4 默认

socket.AF_INET6  表示IPv6

socket.AF_UNIX   只能够用于单一的UNIX进程间的通信

参数二:类型

socket.SOCK_STREAM    流式socket,专门用于TCP通信,默认

socket.SOCK_DGRAM     数据报事socket,专门用于UDP

socket.SOCK_RAW   原始套接字,普通的套接字无法处理ICMP,IGMP网络报文,而SOCK_RAW可以;其次,SOCK_RAW也可以处理特殊的IPv4报文;此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头。

socket.SOCK_RDM 是一种可靠的UDP形式,即保证交付数据报但不保证顺序。SOCK_RAM用来提供对原始协议的低级访问,在需要执行某些特殊操作时使用,如发送ICMP报文。SOCK_RAM通常仅限于高级用户或管理员运行的程序使用。

socket.SOCK_SEQPACKET 可靠的连续数据包服务

参数三:协议

0  (默认)与特定的地址家族相关的协议,如果是 0 ,则系统就会根据地址格式和套接类别,自动选择一个合适的协议

import socket
ip_port = ('127.0.0.1',9999)
sk = socket.socket(socket.AF_INET,socket.SOCK_DGRAM,0)
sk.bind(ip_port)

while True:
    data = sk.recv(1024)
    print data




import socket
ip_port = ('127.0.0.1',9999)

sk = socket.socket(socket.AF_INET,socket.SOCK_DGRAM,0)
while True:
    inp = raw_input('数据:').strip()
    if inp == 'exit':
        break
    sk.sendto(inp,ip_port)

sk.close()
UDP 通信Demo

socket.socket()的方法

sk.bind(address)

  s.bind(address) 将套接字绑定到地址。address地址的格式取决于地址族。在AF_INET下,以元组(host,port)的形式表示地址。在网络通信这里,使用元组和列表的时候,最好能够使用(1, 2, 3, )   [1, 2, 3, ]最后多一个逗号的形式,该形式一般都可适用,但是线程进程那里必须使用这种形式

sk.listen(backlog)

  开始监听传入连接。backlog指定在拒绝连接之前,可以挂起的最大连接数量。相当于最多允许排队等待的用户数

      backlog等于5,表示内核已经接到了连接请求,但服务器还没有调用accept进行处理的连接个数最大为5
      这个值不能无限大,因为要在内核中维护连接队列

sk.setblocking(bool)

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

sk.accept()

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

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

sk.connect((hostname, port, ))

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

sk.connect_ex(address)

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

sk.close() 关闭套接字

sk.recv(bufsize[,flag])

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

sk.recvfrom(bufsize[.flag])

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

sk.send(string[,flag])

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

sk.sendall(string[,flag])

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

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

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

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

sk.settimeout(timeout)

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

sk.getpeername()

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

sk.getsockname()

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

sk.fileno()

  套接字的文件描述符

#服务端
import socket
ip_port = ('127.0.0.1',9999)
sk = socket.socket(socket.AF_INET,socket.SOCK_DGRAM,0)
sk.bind(ip_port)

while True:
    data,(host,port) = sk.recvfrom(1024)
    print(data,host,port)
    sk.sendto(bytes('ok', encoding='utf-8'), (host,port))


#客户端
import socket
ip_port = ('127.0.0.1',9999)

sk = socket.socket(socket.AF_INET,socket.SOCK_DGRAM,0)
while True:
    inp = input('数据:').strip()
    if inp == 'exit':
        break
    sk.sendto(bytes(inp, encoding='utf-8'),ip_port)
    data = sk.recvfrom(1024)
    print(data)

sk.close()
UDP

 

重要的应用:

1 文件传输(断点续传)

FTP断点续传

文件的大小os.stat(file_path).st_size

上传

1 获取本地文件大小os.stat(file_path).st_size 字节大小

发过去之后,服务端接收了之后,一旦之后接收的文件大小相等了,就关闭文件,不再接收

2 一旦中断之后

客户端:向服务端发送我要上传文件,文件名

服务端:判断本地有没有这个文件,如果没有

客户端:从头开始发

服务端:以w模式打开文件,源源不断地接收

 

客户端:向服务端发送我要上传文件,文件名

服务端:判断本地有没有这个文件,如果本地有文件

os.stat(file_path).st_size获取本地文件的字节,已经上传了多少

发给客户端已经上传了多少

客户端:接收到之后,打开文件,seek到下一个位置,将剩余的发送

比如接收到之前的12345,打开文件seek(12345), read(1024)...

服务端:以a模式打开文件,源源不断地接收

这里仍然是从已存在文件的长度的位置开始上传

文件的内容长度的情况和数组列表差不多,文件大小是os.stat(file_path).st_size,但是索引是从0~os.stat(fiel_path).st_size - 1, 所以下次开始的位置应该是os.stat(file_path).st_size,这和列表一样,列表的长度数值也同时表示列表要添加的下一个元素的索引值

简易的断点续传

服务端

import socketserver, os, sys


class MyServer(socketserver.BaseRequestHandler):

    def handle(self):
        # while True:
        conn = self.request
        conn.sendall(bytes("welcome", encoding="utf-8"))
        total_size = str(conn.recv(1024), encoding="utf-8")
        total_size = int(total_size)
        recv_size = 0
        conn.sendall(bytes("ACK", encoding="utf-8"))
        # print(total_size)
        try:
            os.stat("new1.pdf")
        except Exception as e:
            # 找不到文件,从头开始传
            print("从头开始传")
            conn.sendall(bytes("Not Found", encoding="utf-8"))
            f = open("new1.pdf", "wb")  # w方式
            pass
        else:  # 找到文件了,断点续传
            print("断点续传")
            recv_size = os.stat("new1.pdf").st_size
            conn.sendall(bytes(str(recv_size), encoding="utf-8"))
            f = open("new1.pdf", "ab")  # a方式
        # 因为代码差不多,传输过程就合并了
        while True:
            line = conn.recv(1024)
            recv_size += len(line)
            f.write(line)
            if recv_size == total_size:
                f.close()
                break


if __name__ == "__main__":
    server = socketserver.ThreadingTCPServer(("127.0.0.1", 9999), MyServer)
    server.serve_forever()
服务端

 

客户端 

import socket, os


client = socket.socket()
client.connect(("127.0.0.1", 9999))
# while True:
print(str(client.recv(1024), encoding="utf-8"))
file_size = os.stat("old.pdf").st_size
print(file_size)
sent_size = 0
client.sendall(bytes(str(file_size), encoding="utf-8"))
if str(client.recv(1024), encoding="utf-8") == "ACK":
    response = str(client.recv(1024), encoding="utf-8")
    if response == "Not Found":
        with open("old.pdf", "rb") as f:
            for line in f:
                client.sendall(line)
                sent_size += len(line)
                print(sent_size)
                if sent_size > 120000:  # 制造一个断点,第一次运行,文件传输大于120000bytes就停止,第二次发现文件存在,那么就接着往下传
                    break
    else:
        start = int(response)
        with open("old.pdf", "rb") as f:
            f.seek(start)
            for line in f:
                client.sendall(line)
                sent_size += len(line)
                print(sent_size + start)
        pass

client.close()
客户端

 

2 控制台模拟(客户端发来系统命令windows的DOS命令或者Linux的Shell命令,传回相应的结果)

ret = subprocess.getoutput(“ipconfig”)  # 可以得到系统命令运行的结果. 可以直接运行windows的系统命令
print(ret)

 

或者使用.Popen()

obj = subprocess.Popen(["ipconfig"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
out_error_list = obj.communicate()for line in out_error_list:
    print(line)

 

subprocess.Popen(["ipconfig"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)

注意这里的["ipconfig"] 括号内就是直接用来输入控制台的命令,这是输入可以立即得到结果的命令

对于需要进入某种环境再输入命令的,比如要进入python环境,就需要先["python"] 进入python环境,之后再用obj.stdin.write(系统命令)

obj = subprocess.Popen(["python35"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
obj.stdin.write("print('hello')")
out_error_list = obj.communicate()
print(out_error_list)  # 只能在一行内显示
# for line in out_error_list:  # 可以分多行显示
#     print(line)

 

IO多路复用

原生的socket只能同时处理一个用户连接请求,其他的用户需要等待

IO多路复用可以监听多个描述符(socket对象)(文件句柄),一旦文件句柄出现变化就可以感知到

如何同时处理多个用户的请求呢?

1 sk1 = socket.socket()  sk2 = socket.socket() 定义两个socket对象。不可以,因为sk1运行的时候会阻塞

2 使用select模块

select.select([发生变化的句柄], [等待句柄], [错误句柄], 等待时间)

r_list, w_list, e_list = select.select(inputs, [sk1, sk2], [], 1)

第三个参数,如果哪个连接出错,就可以用e_list来接收

第二个参数列表,不管传什么列表元素,都可以用w_list来接收到,应该是wait_list,不管这个连接现在可不可用,但是它始终位于waitlist里 

1表示在这里等1秒,没有人来连接,也就是说这些sk对象没有变化,那么就想下运行

import socket
import select

sk1 = socket.socket()
sk1.bind(("127.0.0.1", 8001))
sk1.listen(5)

sk2 = socket.socket()
sk2.bind(("172.0.0.1", 8002))
sk2.listen(5)

inputs = [sk1, sk2, ]
while True:
   # select在内部就会监听sk1和sk2两个对象,一旦某个句柄发生变化就会感知到
   # 如果有人连sk1,那么r_list = [sk1], r_list就可以拿到发生变化的句柄

   r_list, w_list, e_list = select.select(inputs, [], [], 1)
   # 1表示本次最多等待时间,等1秒,1秒结束后就会向下执行,执行下一次循环

   for sk in r_list:
       conn, address = sk.accept()  # 获取每个连接
       conn.sendall()  # 对每个连接发送一条内容

 

r_list里只会放这次循环时,监听到的发生变化的量

只要有人连接,sk就会变化,如果只有一个sk1,连接这个端口可能有不同的人,也可能有多个端口被连接。伪造的多人处理,本质上还是一个人一个人处理的

将sk和新的连接都放入select.select()来判断是新用户连接还是老用户的通话请求

# ......从上个例子中inputs那里开始
inputs = [sk1, ]
while True:
   # select在内部就会监听sk1和sk2两个对象,一旦某个句柄发生变化就会感知到
   # 如果有人连sk1,那么r_list = [sk1], r_list就可以拿到发生变化的句柄

   r_list, w_list, e_list = select.select(inputs, [], [], 1)
   # 1表示本次最多等待时间,等1秒,1秒结束后就会向下执行,执行下一次循环

   for sk1_or_conn in r_list:  # r_list里面只放每次循环,inputs里变化的那个
       if sk1_or_conn == sk1:  # 当有新用户连接时,sk1会变化,就会把sk1捕捉到
           # 是sk1,表示有新用户来连接了
           conn, address = sk1_or_conn.accept()
           inputs.append(conn)  # 把连接也放入inputs方便监听,一旦有内容也会改变,触发监听
       else:
           # 是conn, 表示有老用户发消息
           try:
               data_bytes = sk1_or_conn.recv(1024)
               # if data_bytes:  # 如果是有真的变化,有内容的变化,那么就接收内容,并返回内容
               data_str = str(data_bytes, encoding="utf-8")
               sk1_or_conn.sendall(bytes(data_str + "", encoding="utf-8"))
           except Exception as ex:
               # 如果接收到的是空,那么表示断开连接
               inputs.remove(sk1_or_conn)

 

读写分离

inputs = [sk1, ]
outputs = []
message_dict = {}
while True:
   # select在内部就会监听sk1和sk2两个对象,一旦某个句柄发生变化就会感知到
   # 如果有人连sk1,那么r_list = [sk1], r_list就可以拿到发生变化的句柄

   r_list, w_list, e_list = select.select(inputs, outputs, [], 1)
   # 1表示本次最多等待时间,等1秒,1秒结束后就会向下执行,执行下一次循环

   for sk1_or_conn in r_list:
       if sk1_or_conn == sk1:
           # 是sk1,表示有新用户来连接了
           conn, address = sk1_or_conn.accept()
           inputs.append(conn)  # 把连接也放入inputs方便监听,一旦有内容也会改变,触发监听
           message_dict[conn] = []
       else:
           # 是conn, 表示有老用户发消息
           try:
               data_bytes = sk1_or_conn.recv(1024)
               # if data_bytes:  # 如果是有真的变化,有内容的变化,那么就接收内容,并返回内容
               # outputs.append(sk1_or_conn)  # 老用户给我发消息之后,我就把该用户添加到wait list
               # data_str = str(data_bytes, encoding="utf-8")
               # message_dict[]
               # sk1_or_conn.sendall(bytes(data_str + "好", encoding="utf-8"))
           except Exception as ex:
               # 如果接收到的是空,那么表示断开连接
               inputs.remove(sk1_or_conn)
           else:
               data_str = str(data_bytes, encoding="utf-8")
               message_dict[sk1_or_conn].append(data_str)
   for conn in w_list:  # 统一处理w_list里保存的用户
       # 对w_list里的用户,回复信息
       recv_str = message_dict[conn][0]
       del message_dict[conn][0]
       conn.sendall(bytes(recv_str + "hello", encoding="utf-8"))
       outputs.remove(conn)  # 回复结束后,就删除这个等待回复的用户
读写分离

 

这样就实现了通过select伪造了并发

select模块,其中提供了:select、poll、epoll三个方法,分别调用系统的 select,poll,epoll 从而实现IO多路复用。

IO多路复用与python没有关系,是系统底层来调用的

刚开始都是用select,有1024个连接的限制,poll出现后,没有1024的限制,epoll在底层就不是for循环实现了,而是用异步的方式实现的。windows只能用select。

第一阶段:

  Socket,服务端只能处理一个请求

第二阶段:

  基于select和socket实现的伪并发

    1 在r_list里既读由写

    2 通过r_list和w_list读写分离

第三阶段:

  Socketserver里真正实现了并发。基于select或者epoll+socket+多线程。再注意一下类的继承

socketserver模块

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

使用ThreadingTCPServer的基础:

  • 创建一个继承自 SocketServer.BaseRequestHandler 的类
  • 类中必须定义一个名为handle的方法
  • 启动ThreadingTCPServer

服务端

 

import socketserver


class MyServer(socketserver.BaseRequestHandler):

    def handle(self):
        while True:  # 接收用户的不同请求
            conn = self.request  # 这就相当于conn, address = server.accept. 
            conn.sendall(bytes("Welcome connect our FTP", encoding="utf-8"))
            file_size = str(conn.recv(1024), encoding="utf-8")
            conn.sendall(bytes("ack", encoding="utf-8"))
            total_size = int(file_size)
            received_size = 0
            f = open('new.pdf', "wb")
            while True:
                if total_size == received_size:  # 接收文件内容,直到获取完毕
                    break
                data = conn.recv(1024)  # 服务端接收客户端的数据
                if not data:  # 如果传过来的是空内容,就退出本次循环
                    break
                f.write(data)
                received_size += len(data)
if __name__ == '__main__':
    server = socketserver.ThreadingTCPServer(('127.0.0.1', 9999), MyServer)
    server.serve_forever()

 

客户端(带进度条)

import socket
import os
import sys
import time


def progress_bar(sent_length, total_length, interval=0.01):
    sys.stdout.write("\r")
    if sent_length / total_length == 1:
        sys.stdout.write(
            "%s%% | %s" % (int((sent_length / total_length) * 100), int((sent_length / total_length) * 100) * ">"))
    else:
        sys.stdout.write(
            "%.2f%% | %s" % ((sent_length / total_length) * 100, int((sent_length / total_length) * 100) * ">"))
    sys.stdout.flush()  # 强制刷新屏幕
    time.sleep(interval)  # 控制间隔时间,方便观察进度条,也方便控制传输速度


obj = socket.socket()
obj.connect(("127.0.0.1", 9999))  # 与服务端建立了连接
result = obj.recv(1024)  # 接收消息,表示最多接收1024字节
# 这个recv也是阻塞的,如果服务端没有conn.sendall()没有内容接收就会永远等待

# 先发送当前文件字节大小
length = os.stat("old.pdf").st_size
obj.sendall(bytes(str(length), encoding="utf-8"))
ret_str = str(result, encoding="utf-8")
sent_size = 0
print(ret_str)
obj.recv(1024)
with open("old.pdf", "rb") as f:
    for line in f:
        obj.sendall(line)
        sent_size += len(line)
        progress_bar(sent_size, length, 0.003)

obj.close()
客户端(进度条)

查看socketserver的源码,ThreadingTCPServer的源码

ThreadingTCPServer源码(武sir)

内部调用流程为:

  • 启动服务端程序
  • 执行 TCPServer.__init__ 方法,创建服务端Socket对象并绑定 IP 和 端口
  • 执行 BaseServer.__init__ 方法,将自定义的继承自SocketServer.BaseRequestHandler 的类 MyRequestHandle赋值给self.RequestHandlerClass
  • 执行 BaseServer.server_forever 方法,While 循环一直监听是否有客户端请求到达 ...
  • 当客户端连接到达服务器
  • 执行 ThreadingMixIn.process_request 方法,创建一个 “线程” 用来处理请求
  • 执行 ThreadingMixIn.process_request_thread 方法
  • 执行 BaseServer.finish_request 方法,执行 self.RequestHandlerClass()  即:执行 自定义 MyRequestHandler 的构造方法(自动调用基类BaseRequestHandler的构造方法,在该构造方法中又会调用 MyRequestHandler的handle方法)

ThreadingTCP相关源码

class BaseServer:

    """Base class for server classes.

    Methods for the caller:

    - __init__(server_address, RequestHandlerClass)
    - serve_forever(poll_interval=0.5)
    - shutdown()
    - handle_request()  # if you do not use serve_forever()
    - fileno() -> int   # for select()

    Methods that may be overridden:

    - server_bind()
    - server_activate()
    - get_request() -> request, client_address
    - handle_timeout()
    - verify_request(request, client_address)
    - server_close()
    - process_request(request, client_address)
    - shutdown_request(request)
    - close_request(request)
    - handle_error()

    Methods for derived classes:

    - finish_request(request, client_address)

    Class variables that may be overridden by derived classes or
    instances:

    - timeout
    - address_family
    - socket_type
    - allow_reuse_address

    Instance variables:

    - RequestHandlerClass
    - socket

    """

    timeout = None

    def __init__(self, server_address, RequestHandlerClass):
        """Constructor.  May be extended, do not override."""
        self.server_address = server_address
        self.RequestHandlerClass = RequestHandlerClass
        self.__is_shut_down = threading.Event()
        self.__shutdown_request = False

    def server_activate(self):
        """Called by constructor to activate the server.

        May be overridden.

        """
        pass

    def serve_forever(self, poll_interval=0.5):
        """Handle one request at a time until shutdown.

        Polls for shutdown every poll_interval seconds. Ignores
        self.timeout. If you need to do periodic tasks, do them in
        another thread.
        """
        self.__is_shut_down.clear()
        try:
            while not self.__shutdown_request:
                # XXX: Consider using another file descriptor or
                # connecting to the socket to wake this up instead of
                # polling. Polling reduces our responsiveness to a
                # shutdown request and wastes cpu at all other times.
                r, w, e = _eintr_retry(select.select, [self], [], [],
                                       poll_interval)
                if self in r:
                    self._handle_request_noblock()
        finally:
            self.__shutdown_request = False
            self.__is_shut_down.set()

    def shutdown(self):
        """Stops the serve_forever loop.

        Blocks until the loop has finished. This must be called while
        serve_forever() is running in another thread, or it will
        deadlock.
        """
        self.__shutdown_request = True
        self.__is_shut_down.wait()

    # The distinction between handling, getting, processing and
    # finishing a request is fairly arbitrary.  Remember:
    #
    # - handle_request() is the top-level call.  It calls
    #   select, get_request(), verify_request() and process_request()
    # - get_request() is different for stream or datagram sockets
    # - process_request() is the place that may fork a new process
    #   or create a new thread to finish the request
    # - finish_request() instantiates the request handler class;
    #   this constructor will handle the request all by itself

    def handle_request(self):
        """Handle one request, possibly blocking.

        Respects self.timeout.
        """
        # Support people who used socket.settimeout() to escape
        # handle_request before self.timeout was available.
        timeout = self.socket.gettimeout()
        if timeout is None:
            timeout = self.timeout
        elif self.timeout is not None:
            timeout = min(timeout, self.timeout)
        fd_sets = _eintr_retry(select.select, [self], [], [], timeout)
        if not fd_sets[0]:
            self.handle_timeout()
            return
        self._handle_request_noblock()

    def _handle_request_noblock(self):
        """Handle one request, without blocking.

        I assume that select.select has returned that the socket is
        readable before this function was called, so there should be
        no risk of blocking in get_request().
        """
        try:
            request, client_address = self.get_request()
        except socket.error:
            return
        if self.verify_request(request, client_address):
            try:
                self.process_request(request, client_address)
            except:
                self.handle_error(request, client_address)
                self.shutdown_request(request)

    def handle_timeout(self):
        """Called if no new request arrives within self.timeout.

        Overridden by ForkingMixIn.
        """
        pass

    def verify_request(self, request, client_address):
        """Verify the request.  May be overridden.

        Return True if we should proceed with this request.

        """
        return True

    def process_request(self, request, client_address):
        """Call finish_request.

        Overridden by ForkingMixIn and ThreadingMixIn.

        """
        self.finish_request(request, client_address)
        self.shutdown_request(request)

    def server_close(self):
        """Called to clean-up the server.

        May be overridden.

        """
        pass

    def finish_request(self, request, client_address):
        """Finish one request by instantiating RequestHandlerClass."""
        self.RequestHandlerClass(request, client_address, self)

    def shutdown_request(self, request):
        """Called to shutdown and close an individual request."""
        self.close_request(request)

    def close_request(self, request):
        """Called to clean up an individual request."""
        pass

    def handle_error(self, request, client_address):
        """Handle an error gracefully.  May be overridden.

        The default is to print a traceback and continue.

        """
        print '-'*40
        print 'Exception happened during processing of request from',
        print client_address
        import traceback
        traceback.print_exc() # XXX But this goes to stderr!
        print '-'*40
BaseServer
class TCPServer(BaseServer):

    """Base class for various socket-based server classes.

    Defaults to synchronous IP stream (i.e., TCP).

    Methods for the caller:

    - __init__(server_address, RequestHandlerClass, bind_and_activate=True)
    - serve_forever(poll_interval=0.5)
    - shutdown()
    - handle_request()  # if you don't use serve_forever()
    - fileno() -> int   # for select()

    Methods that may be overridden:

    - server_bind()
    - server_activate()
    - get_request() -> request, client_address
    - handle_timeout()
    - verify_request(request, client_address)
    - process_request(request, client_address)
    - shutdown_request(request)
    - close_request(request)
    - handle_error()

    Methods for derived classes:

    - finish_request(request, client_address)

    Class variables that may be overridden by derived classes or
    instances:

    - timeout
    - address_family
    - socket_type
    - request_queue_size (only for stream sockets)
    - allow_reuse_address

    Instance variables:

    - server_address
    - RequestHandlerClass
    - socket

    """

    address_family = socket.AF_INET

    socket_type = socket.SOCK_STREAM

    request_queue_size = 5

    allow_reuse_address = False

    def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True):
        """Constructor.  May be extended, do not override."""
        BaseServer.__init__(self, server_address, RequestHandlerClass)
        self.socket = socket.socket(self.address_family,
                                    self.socket_type)
        if bind_and_activate:
            try:
                self.server_bind()
                self.server_activate()
            except:
                self.server_close()
                raise

    def server_bind(self):
        """Called by constructor to bind the socket.

        May be overridden.

        """
        if self.allow_reuse_address:
            self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.socket.bind(self.server_address)
        self.server_address = self.socket.getsockname()

    def server_activate(self):
        """Called by constructor to activate the server.

        May be overridden.

        """
        self.socket.listen(self.request_queue_size)

    def server_close(self):
        """Called to clean-up the server.

        May be overridden.

        """
        self.socket.close()

    def fileno(self):
        """Return socket file number.

        Interface required by select().

        """
        return self.socket.fileno()

    def get_request(self):
        """Get the request and client address from the socket.

        May be overridden.

        """
        return self.socket.accept()

    def shutdown_request(self, request):
        """Called to shutdown and close an individual request."""
        try:
            #explicitly shutdown.  socket.close() merely releases
            #the socket and waits for GC to perform the actual close.
            request.shutdown(socket.SHUT_WR)
        except socket.error:
            pass #some platforms may raise ENOTCONN here
        self.close_request(request)

    def close_request(self, request):
        """Called to clean up an individual request."""
        request.close()
TCPServer

........读后内容

........

SocketServer的ThreadingTCPServer之所以可以同时处理请求得益于 select 和 Threading 两个东西,其实本质上就是在服务器端为每一个客户端创建一个线程,当前线程用来处理对应客户端的请求,所以,可以支持同时n个客户端链接(长连接)。

ForKingTCPServer

ForkingTCPServer和ThreadingTCPServer的使用和执行流程基本一致,只不过在内部分别为请求者建立 “线程”  和 “进程”。

ForkingTCPServer只是将 ThreadingTCPServer 实例中的代码:

server = SocketServer.ThreadingTCPServer(('127.0.0.1',8009),MyRequestHandler)
变更为:
server = SocketServer.ForkingTCPServer(('127.0.0.1',8009),MyRequestHandler)

 

工作中的新任务:1 理清需求 2查询是否有相应功能的模块

FTP讲解内容

线程,进程,协程,线程池,拓展:生产者消费者模型,队列

笔试题常有:简述线程,进程,协程间的关系

程序是运行在系统上的具有某种功能的软件,比如说浏览器,音乐播放器等。 每次执行程序的时候,都会完成一定的功能,比如说浏览器帮我们打开网页,为了保证其独立性,就需要一个专门的管理和控制执行程序的数据结构——进程控制块。 进程就是一个程序在一个数据集上的一次动态执行过程。

Each process (进程) provides the resources needed to execute a program. A process has a virtual address space, executable code, open handles to system objects, a security context, a unique process identifier, environment variables, a priority class, minimum and maximum working set sizes, and at least one thread of execution. Each process is started with a single thread, often called the primary thread, but can create additional threads from any of its threads. 

A thread (线程) is the entity within a process that can be scheduled for execution. All threads of a process share its virtual address space and system resources. In addition, each thread maintains exception handlers, a scheduling priority, thread local storage, a unique thread identifier, and a set of structures the system will use to save the thread context until it is scheduled. The thread context includes the thread's set of machine registers, the kernel stack, a thread environment block, and a user stack in the address space of the thread's process. Threads can also have their own security context, which can be used for impersonating clients.

Differences between threads and process:

1. Threads are easier to create than processes since they 
don't require a separate address space.
						
2. Multithreading requires careful programming since threads 
share data strucures that should only be modified by one thread
at a time.  Unlike threads, processes don't share the same 
address space. Java和C#中的线程并没有GIL锁,所以可以同时在一个进程中操作多个线程,可能会出问题,Python中每个进程每次只能操作一个线程
						
3.  Threads are considered lightweight because they use far 
less resources than processes.
						
4.  Processes are independent of each other.  Threads, since they 
share the same address space are interdependent, so caution 
must be taken so that different threads don't step on each other.  
This is really another way of stating #2 above.
						
5.  A process can consist of multiple threads. 

Python线程

Threading用于提供线程相关的操作,线程是应用程序中工作的最小单元。 

有几核CPU就相当于能够同时服务几个进程,每核CPU服务一个进程,每个进程只能出一个线程被服务。创建有好几个进程的目的,利用CPU的多核,让CPU同时执行多个任务. 这是python内的情况,java和C#没有每个进程只能出一个线程被服务的这个限制

线程(thread),

优点:共享内存,做IO操作的时候创造出并发操作

缺点:每个进程内的多个线程都公用资源,如果每个线程都要修改,就容易抢占资源

 

进程(process),

优点:利用CPU多核来执行同时执行多个操作

缺点:耗费资源,重新开辟内存空间

 

虽然表面上看,Python这种限制会降低效率,但是计算机中很多任务不需要CPU,比如IO操作,计算需要CPU。那么做IO操作的线程就可以避开CPU,就可以让几个线程同时进行

笼统来说,进程数和CPU数一致是最好的,线程也不是越多越好。(CPU还要记录请求上下文,并且请求上下文的切换,这个比较耗费时间)(其他语言中据说线程数也和CPU数一致最好)

线程数具体案例具体分析

 

总结:

计算机有进程和线程的目的:提高效率

  1. 目前为止,除了socketserver之外都是单进程单线程(进程用于保存资源,线程运行的时候从进程里拿资源)主进程和主线程

  2. 自定义线程:

    1. 主进程 :主线程,子线程

    2. 计算机中执行任务的最小单元就是线程

IO操作不利用CPU,对于IO密集型适合用多线程,对于计算密集型适合用多进程

Python中的这个特殊的限制,是通过GIL全局解释器锁,给每个进程上锁,每次出一个线程来执行

线程使用基础:

1 选择进程和线程

  进程就类似于房间,线程就类似于房间里的人。计算密集型需要多进程,IO密集型需要多线程

  不论进程线程都是为了实现并发操作

  python的线程里有个GIL锁,每个进程里一次只能出一个线程被CPU操作。C#和java没有这个锁

2 使用线程

创建线程:

  1 现在创建的也是属于子线程 

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import threading
import time
  
def show(arg):
    time.sleep(1)
    print 'thread'+str(arg)
  
for i in range(10):
    t = threading.Thread(target=show, args=(i,))
    t.start()
  
print 'main thread stop'

上述代码创建了10个“前台”线程,然后控制器就交给了CPU,CPU根据指定算法进行调度,分片执行指令。

import threading

新创建的线程t = threading.Thread(target=方法名, args=([参数][, name=...]))

注意args传参用元组,要采用最后一个元素后边要再加一个逗号的形式 ,线程名字name=...可以不写

import time
import threading
print("start")


def f1():
   pass


def f0(x, y, z):
   f1()
   time.sleep(3)


t = threading.Thread(target=f0, args=(123, 1, 2, ))  # 参数后边要再写一个逗号
t.start()
t = threading.Thread(target=f0, args=(123, 1, 2, ))
t.start()
t = threading.Thread(target=f0, args=(123, 1, 2, ))
t.start()

print("end")
# 程序瞬间打印了start和end, 然后并未结束,等待三个线程运行完毕后再结束

 

thread方法说明

t.start() : 激活线程,

t.getName() : 获取线程的名称

t.setName() : 设置线程的名称 

t.name : 获取或设置线程的名称

t.is_alive() : 判断线程是否为激活状态

t.isAlive() :判断线程是否为激活状态

t.setDaemon() 是否设置为后台线程(默认:False 前台线程);通过一个布尔值设置线程是否为后台线程,必须在执行start()方法之后才可以使用。如果是后台线程(True),主线程执行过程中,后台线程也在进行,主线程执行完毕后,后台线程不论成功与否,均停止;如果是前台线程(默认False或不设),主线程执行过程中,前台线程也在进行,主线程执行完毕后,等待前台线程也执行完成后,程序停止

t.isDaemon() : 判断是否为守护线程

t.ident :获取线程的标识符。线程标识符是一个非零整数,只有在调用了start()方法之后该属性才有效,否则它只返回None。

t.join() :制止并发,线程排队执行。逐个执行每个线程,执行完毕后继续往下执行,该方法使得多线程变得无意义。

t.run() :线程被cpu调度后自动执行线程对象的run方法

import threading
import time
 
 
class MyThread(threading.Thread):
    def __init__(self,num):
        threading.Thread.__init__(self)
        self.num = num
 
    def run(self):#定义每个线程要运行的函数
 
        print("running on number:%s" %self.num)
 
        time.sleep(3)
 
if __name__ == '__main__':
 
    t1 = MyThread(1)
    t2 = MyThread(2)
    t1.start()
    t2.start()
自定义线程类

  2 主线程是否等待子线程t.setDaemon(True/False)设为后台线程,主线程不等待 

t = threading.Thread(target=f0, args=(123, 1, 2, ))
t.setDaemon(True)
t.start()
t = threading.Thread(target=f0, args=(123, 1, 2, ))
t.setDaemon(True)
t.start()
t = threading.Thread(target=f0, args=(123, 1, 2, ))
t.setDaemon(True)
t.start()

print("end")
# setDaemon(True) 将线程设为后台线程
# 程序瞬间打印了start和end, 然后结束,并没有等待三个线程运行完毕后再结束

 

3 主线程等待, 子线程的执行 t.join([seconds]) 制止并发

如果不想线程并发的时候可以直接运行三个f0()或者用t.join()

t.start()

t.join() 执行之后,就不往下执行了,等t执行完了之后再向下执行

t.join(2) 表示最多等2秒,就执行下边的线程,如果任务2秒没执行完,就不等了;如果设置了t.join(2) 但是1秒执行完了,就等1秒

t1 = threading.Thread(target=f0, args=(123, 1, 2, ))
t1.start()
t1.join()
t2 = threading.Thread(target=f0, args=(123, 1, 2, ))
t2.start()
t2.join()
t3 = threading.Thread(target=f0, args=(123, 1, 2, ))
t3.start()
t3.join()

print("end")
# t.join()线程排队执行,不再并发了,
# 程序瞬间打印了start, 三个线程依次执行完毕后,再打印出end

 

线程锁Threading.Rlock 和 Threading.Lock

同一个进程内的多个线程是共享数据资源的,所以当多线程并发,同时修改同一条数据时,就可能产生脏数据

为了保证数据的概念就引入了锁。使得这个线程之外的线程都被锁住,当前线程操作数据之后,再释放

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import threading
import time

gl_num = 0

def show(arg):
    global gl_num
    time.sleep(1)
    gl_num +=1
    print gl_num

for i in range(10):
    t = threading.Thread(target=show, args=(i,))
    t.start()

print 'main thread stop'
未使用锁

 

未使用锁的情况下,多线程并发,同时修改数据,结果是同时打印了从1~10,虽然都是加1的操作,但是多个线程都在操作同一个全局变量

在该线程指定的方法内,头部加锁,尾部释放,当执行该程序时,其他线程被锁住,该线程操作数据之后,再释放锁让其他线程运行

lock = threading.RLock()    lock.acquire()   lock.release()    

#!/usr/bin/env python
#coding:utf-8
   
import threading
import time
   
gl_num = 0
   
lock = threading.RLock()
   
def Func():
    lock.acquire()
    global gl_num
    gl_num +=1
    time.sleep(1)
    print gl_num
    lock.release()
       
for i in range(10):
    t = threading.Thread(target=Func)
    t.start()

 

有了锁之后,每次只打印一个数字,表示每次只有一个线程在操作,其他线程被锁住

threading.RLock和threading.Lock 的区别

RLock允许在同一线程中被多次acquire。而Lock却不允许这种情况。 如果使用RLock,那么acquire和release必须成对出现,即调用了n次acquire,必须调用n次的release才能真正释放所占用的琐。

import threading
lock = threading.Lock()    #Lock对象
lock.acquire()
lock.acquire()  #产生了死琐。
lock.release()
lock.release()
import threading
rLock = threading.RLock()  #RLock对象
rLock.acquire()
rLock.acquire()    #在同一线程内,程序不会堵塞。
rLock.release()
rLock.release()

推荐使用RLock

Threading.Event()

Event是线程间通信的机制之一:一个线程发送一个event信号,其他的线程则等待这个信号。用于主线程控制其他线程的执行。 Events 管理一个flag,这个flag可以使用set()设置成True或者使用clear()重置为False,wait()则用于阻塞,在flag为True之前。flag默认为False。

  • Event.wait([timeout]) : 堵塞线程,直到Event对象内部标识位被设为True或超时(如果提供了参数timeout)。
  • Event.set() :将标识位设为Ture
  • Event.clear() : 将标识位设为False。
  • Event.isSet() :判断标识位是否为Ture。
import threading
 
def do(event):
    print('start')
    event.wait()
    print('execute')
 
event_obj = threading.Event()
for i in range(10):
    t = threading.Thread(target=do, args=(event_obj,))
    t.start()
 
event_obj.clear()
inp = input('input:')
if inp == 'true':
    event_obj.set()

 

有10个线程,执行的时候,同时打印了start,遇到了wait(),大家都在等待标识位被设为True,当用户输入True的时候,标识位设为真,10个线程同时放开执行,同时打印10个execute 

有点类似于所有线程都被锁住,而threading.RLock()是锁别人不锁自己

信号量(Semaphore) 

互斥锁 同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据 ,比如厕所有3个坑,那最多只允许3个人上厕所,后面的人只能等里面有人出来了才能再进去。

import threading,time
 
def run(n):
    semaphore.acquire()
    time.sleep(1)
    print("run the thread: %s" %n)
    semaphore.release()
 
if __name__ == '__main__':
 
    num= 0
    semaphore  = threading.BoundedSemaphore(5) #最多允许5个线程同时运行
    for i in range(20):
        t = threading.Thread(target=run,args=(i,))
        t.start()

 

条件(Condition)

满足一定的条件,锁才会被释放

lock = threading.Condition([lock])创建的时候可以传一个lock对象,如果不传,默认是RLock对象,而且.acquire 和 .release()都与lock对象一致

import threading
import time


def consumer1(cond):
    with cond:
        cond.acquire()
        print("consumer1 before wait")
        cond.wait()
        print("consumer1 after wait")
        cond.release()


def consumer2(cond):
    with cond:
        print("consumer2 before wait")
        cond.wait()
        print("consumer2 after wait")


def producer(cond):
    with cond:
        print("producer before notifyAll")
        # cond.notifyAll()
        print("producer after notifyAll")


condition = threading.Condition()
c1 = threading.Thread(name="c1", target=consumer1, args=(condition,))
c2 = threading.Thread(name="c2", target=consumer2, args=(condition,))

p = threading.Thread(name="p", target=producer, args=(condition,))

c1.start()
time.sleep(2)
c2.start()
time.sleep(2)
p.start()

 

con.wait([timeout])会让当前的线程停止,并且记录当前的线程,当我们用con.notifyAll()就会同时放开所有con.wait()的线程,而con.notify()则会放开一个,再用一次con.notify()再放开一个线程 

里面可以传一个timeout秒数,表示等待多久后放开

使用的时候con.acquire()相当于加锁,锁住线程,con.wait([timeout])表示等几秒后向下执行,或者不写秒数,只能等到con.notify()一个一个放开等待或者con.notifyAll()一起放开等待,con.release()是释放锁

注意:con.wait()并不是释放锁,而是等待,暂停。程序可以不用acquire和release只用.wait()和.notify()  .notifyAll()

定时器Timer

定时器,指定n秒后执行某操作

from threading import Timer
 
 
def hello():
    print("hello, world")
 
t = Timer(1, hello)
t.start()  # after 1 seconds, "hello, world" will be printed

 

生产者消费者模型,队列queue

队列两端开口,队尾进,队头出,先进先出,就和排队一样

import queue

q = queue.Queue(maxsize=0)  # 构造一个先进显出队列,maxsize指定队列长度,为0 时,表示队列长度无限制。

q.join()    # 等到队列为空的时候,在执行别的操作
q.qsize()   # 返回队列的大小 (不可靠)
q.empty()   # 当队列为空的时候,返回True 否则返回False (不可靠)
q.full()    # 当队列满的时候,返回True,否则返回False (不可靠)
q.put(item, block=True, timeout=None) #  将item放入Queue尾部,item必须存在,可以参数block默认为True,表示当队列满时,会等待队列给出可用位置,
                         为False时为非阻塞,此时如果队列已满,会引发queue.Full 异常。 可选参数timeout,表示 会阻塞设置的时间,过后,
                          如果队列无法给出放入item的位置,则引发 queue.Full 异常
q.get(block=True, timeout=None) #   移除并返回队列头部的一个值,可选参数block默认为True,表示获取值的时候,如果队列为空,则阻塞,为False时,不阻塞,
                      若此时队列为空,则引发 queue.Empty异常。 可选参数timeout,表示会阻塞设置的时候,过后,如果队列为空,则引发Empty异常。
q.put_nowait(item) #   等效于 put(item,block=False)
q.get_nowait() #    等效于 get(item,block=False)

 

get方法:队列为空的时候就等,阻塞。get_nowait方法,队列为空的时候就不等

队列可以预先准备好数据,等待程序来取用,并且安排好取用顺序

rabbitMQ 消息队列

Queue队列是单向的,只能在一边取另一边拿,python中也有双向队列,可以两边取用

线程池:python中没有提供线程池,相当于创建了一个容器,里面有创建好的线程,需要的时候从里边拿线程来用

更多的时候要基于线程池来做,很少自己再去定义单独的线程来调用

一定要记住Queue的特性,Queue的最大个数,放值put,取值get,取值get_nowait

数据的准备方将数据添加进入队列,数据的取用放从队列的头部取出数据

#!/usr/bin/env python
import Queue
import threading


message = Queue.Queue(10)


def producer(i):
    while True:
        message.put(i)


def consumer(i):
    while True:
        msg = message.get()


for i in range(12):
    t = threading.Thread(target=producer, args=(i,))
    t.start()

for i in range(10):
    t = threading.Thread(target=consumer, args=(i,))
    t.start()
生产者消费者模型

 

进程

进程就是类似于盖个房子,要耗费资源的。而且一般情况下,进程间的资源是不共享的

1 创建进程(在windows里必须要写在if __name__ == “__main__”才行,Linux下调试比较好

import multiprocessing
import time


def f1(arg):
   time.sleep(2)
   print(arg)

if __name__ == "__main__":
   p0 = multiprocessing.Process(target=f1, args=(123, ))
   p0.start()

   p1 = multiprocessing.Process(target=f1, args=(123,))
   p1.start()
   print("end")

 

定义方法类似于线程的定义,multiprocessing.Process(target=方法名,args=([参数, ]), name=进程名)

代码从上到下解释是主进程里的主线程,现在创建了两个子进程,是由子进程里的线程执行的

后台进程p.daemon = True

也可以在p.start()前边用 p.daemon = True 来让进程后台运行,主进程一终止,子进程不管是不是在进行都要终止

 

进程里边p.join([seconds])也是适用的

进程间的数据不是共享的,每创建一个进程都会有自己的数据

import multiprocessing
li = []


def foo(i):
   li.append(i)
   print('say hi', li)

if __name__ == "__main__":
   for i in range(10):
       p = multiprocessing.Process(target=foo, args=(i,))
       p.start()

创建了10个进程,我们希望能够每个进程都能往li列表内部添加一个数字,最终变成[0~9], 运行结果是产生了10个li列表,每个列表内都只有一个元素,就是该进程拿到的那个数。

说明了进程的资源是不共享的

线程的资源是可以共享的

import threading
li = []


def foo(i):
   li.append(i)
   print('say hi', li)

if __name__ == "__main__":
   for i in range(10):
       p = threading.Thread(target=foo, args=(i,))
       p.start()
创建了10个线程

最终10个线程操作同一个列表li,每一个线程运行都为这个列表添加元素,充分说明了线程资源共享

如果需要进程共同操作一个数据,需要借助特殊的物质,Array数组,或者manager.dict() 优先选这个

方法1:数组Array

数组创建的时候,

  • 必须在开始指定长度或元素(这也是为了使用连续的内存地址,这样刚开始定义好了,元素的内存地址就互相挨着)

  • 数组里必须是统一的数据类型(这也是为了分配确定大小的连续的内存空间)

限制比较多,所以一般不用这个方法

#方法一,Array
from multiprocessing import Process,Array
temp = Array('i', [11,22,33,44])
 
def Foo(i):
    temp[i] = 100+i
    for item in temp:
        print i,'----->',item
 
for i in range(2):
    p = Process(target=Foo,args=(i,))
    p.start()

 

Array数组的第一个参数表示数组内元素类型,对照表

    'c': ctypes.c_char,  'u': ctypes.c_wchar,
    'b': ctypes.c_byte,  'B': ctypes.c_ubyte,
    'h': ctypes.c_short, 'H': ctypes.c_ushort,
    'i': ctypes.c_int,   'I': ctypes.c_uint,
    'l': ctypes.c_long,  'L': ctypes.c_ulong,
    'f': ctypes.c_float, 'd': ctypes.c_double
类型对照表

 

方法2:manager.dict() 

 

from multiprocessing import Process, Manager


def Foo(i, dic):
   dic[i] = 100 + i
   print(dic.values())

if __name__ == "__main__":
   manage = Manager()
   dic = manage.dict()
   for i in range(2):
       p = Process(target=Foo, args=(i, dic,))
       p.start()
       p.join()
# [100]
# [100, 101]

 

两个进程都会共用这个dic,这样dic就会最终变成两个键值对的字典

如果是普通的字典,结果就是第一个进程的字典里有100,第二个进程的字典里有101 

注意要把定义manager对象和Array数组放在if __name__里

另外注意有个p.join()需要加上,阻止两个进程的并发动作,一个一个发生,就可以修改这个公用的对象了

进程池 

进程池Python提供了,线程池Python没有提供

进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进程,那么程序就会等待,直到进程池中有可用进程为止。

Pool = multiprocessing.Pool(5) 定义了有5个进程的进程池,但是并没有生成5个进程,需要的时候生成一个来拿

进程池的方法

  • apply(func[, args[, kwds]]) :使用arg和kwds参数调用func函数,结果返回前会一直阻塞,由于这个原因,apply_async()更适合并发执行,另外,func函数仅被pool中的一个进程运行。

  • apply_async(func[, args[, kwds[, callback[, error_callback]]]]) : apply()方法的一个变体,会返回一个结果对象。如果callback被指定,那么callback可以接收一个参数然后被调用,当结果准备好回调时会调用callback,调用失败时,则用error_callback替换callback。 Callbacks应被立即完成,否则处理结果的线程会被阻塞。

  • close() : 阻止更多的任务提交到pool,待任务完成后,工作进程会退出。

  • terminate() : 不管任务是否完成,立即停止工作进程。在对pool对象进程垃圾回收的时候,会立即调用terminate()。

  • join() : wait工作线程的退出,在调用join()前,必须调用close() or terminate()。这样是因为被终止的进程需要被父进程调用wait(join等价与wait),否则进程会成为僵尸进程。

  • map(func, iterable[, chunksize])

  • map_async(func, iterable[, chunksize[, callback[, error_callback]]])

  • imap(func, iterable[, chunksize])

  • imap_unordered(func, iterable[, chunksize])

  • starmap(func, iterable[, chunksize])

  • starmap_async(func, iterable[, chunksize[, callback[, error_back]]])

#!/usr/bin/env python
# -*- coding:utf-8 -*-
from  multiprocessing import Process,Pool
import time
  
def Foo(i):
    time.sleep(2)
    return i+100
  
def Bar(arg):
    print arg
  
pool = Pool(5)
#print pool.apply(Foo,(1,))  # 申请一个进程,用完了再还给进程池
#print pool.apply_async(func =Foo, args=(1,)).get()  # 异步申请
  
for i in range(10):
    pool.apply_async(func=Foo, args=(i,),callback=Bar)  # 申请一个进程去执行Foo函数,执行结束狗再调用Bar方法,并吧Foo方法的返回值交个Bar方法来执行
  
print 'end'
pool.close()
pool.join()#进程池中进程执行完毕后再关闭,如果注释,那么程序直接关闭。

pool = Pool(5)

#print pool.apply(Foo,(1,))  申请一个进程,用完了再还给进程池

#print pool.apply_async(func =Foo, args=(1,)).get()  异步申请

pool.apply_async(func=Foo, args=(i,),callback=Bar)  申请一个进程去执行Foo函数,执行结束后再调用Bar方法,并且把Foo方法的返回值交给Bar方法来执行

进程池apply和apply_async方法:

Apply申请时,每个进程是排队进行的          有进程.join()     daemon = False前台进程

而apply_async都是并发进行的,并且可以设置回调函数       没有join()     daemon = True 后台进程,主进程并不等子进程完成,

如果需要主进程等待子进程完成了再结束:一定要写pool.close()或pool.terminate()句

pool.apply_async()...

pool.close()或pool.terminate()

pool.join()  也是类似于制止并发,排队执行,所以要等进程池中的进程都执行完毕才能结束

线程池

Python并没有给出,需要自己写

初级线程池:

import queue
import threading
import time


class ThreadPool:
  
   def __init__(self, max_num=20):
       self.Queue = queue.Queue(max_num)  # 通过队列,建立一个最大有20个线程的线程池
       for i in range(max_num):
           self.Queue.put(threading.Thread)  # 将类名放入队列,仅仅是类名

   def get_thread(self):
       return self.Queue.get()  # 从队列中取出一个类名,之后使用的时候要再加()来运行

   def put_thread(self):
       self.Queue.put(threading.Thread)  # 使用之后需要将线程还给线程池,也就是再加一个线程名进去


def func(pool, arg):
   time.sleep(1)
   print(arg)
   pool.put_thread()  # 使用之后将该线程还给线程池,实际就是再加一个线程到队列

threadpool = ThreadPool(10)  # 使用队列的好处就是,我们使用了get方法,如果队列中没有空余的线程,那么就需要等待别人用完了
# 还回去,你再使用
for i in range(100):

   thread = threadpool.get_thread()  # 得到threading.Thread类名
   t = thread(target=func, args=(threadpool, i, ))  # 创建一个线程对象
   t.start()

上述的初级线程池虽然可以实现到线程池的功能,但是却耗费了太多资源

问题:1 需要取得线程的时候再创建,不需要维护那么长的队列 2 没有像进程池中那样的回调函数  3 用完的线程其实是被销毁,然后再队列中加入一个新线程名,这样浪费资源

线程池思路:

我们将需要运行的任务放入队列,然后让线程在队列一端取事件,执行之后再来取事件

1 队列放任务

2 20个线程的线程池,如果有30个任务,也不一定都能用到20个,如果执行的快,可能只用一两个线程就可以

有空闲线程就不创建了,没有空闲线程再创建的线程

if … is not None: 判断某个数是否存在

is 判断a和b是否是同一个对象,用id来判断, a is b;a == b 判断a,b值是否相等,用值来判断

在JS中可以通过  if(typeof ... != undefined){}

想要终止:

1 让正在从队列中取任务的线程终止

2 主线程也随之终止

import queue
import threading
import contextlib

StopEvent = object()


class ThreadPool(object):
   def __init__(self, max_num):
       self.q = queue.Queue(max_num)  # 队列放任务,任务数量不限
       self.max_num = max_num  # 最多创建的线程数
       self.cancel = False
       self.generate_list = []  # 真实创建的线程列表
       self.free_list = []  # 空闲线程列表

   def run(self, func, args, callback=None):
       """
       线程池执行一个任务
       :param func: 任务函数
       :param args: 任务函数所需参数
       :param callback: 任务执行失败或成功后执行的回调函数,回调函数有两个参数1、任务函数执行状态;2、任务函数返回值(默认为None,即:不执行回调函数)
       :return: 如果线程池已经终止,则返回True否则None
       """
       if self.cancel:
           return True
       if len(self.free_list) == 0 and len(self.generate_list) < self.max_num:
           # 空线程列表里没有线程,并且已经创建的线程数要少于限制数
           self.generate_thread()  # 创建线程
       w = (func, args, callback,)  # 将任务包装到元祖中
       self.q.put(w)  # 放入队列中

   def generate_thread(self):
       """
       创建一个线程
       """
       t = threading.Thread(target=self.call)  # 执行call方法,判断是否生成线程
       t.start()

   def call(self):
       """
       循环去获取任务函数并执行任务函数
       """
       current_thread = threading.currentThread  # 获取当前的线程
       self.generate_list.append(current_thread)  # 将该线程添加到已创建的列表中

       event = self.q.get()  # 从队列中拿出任务
       while event != StopEvent:  # 如果拿出的是元祖,那么就是任务,如果拿出的是StopEvent那么就不是任务,表示队列中没有任务了
           func, arguments, callback = event  # 解析任务包
           try:
               result = func(*arguments)  # 任务包中的执行的任务有可能有出错的情况,这个和线程池无关
               success = True
           except Exception as e:
               success = False
               result = None

           if callback is not None:  # 如果回调函数存在,也就是说写了回调函数
               try:
                   callback(success, result)
               except Exception as e:
                   pass

           with self.worker_state(self.free_list, current_thread):
                if self.terminal:
                    event = StopEvent
                else:
                    event = self.q.get()
       else:  # 如果任务都取完了,那么就清除了当前线程

           self.generate_list.remove(current_thread)

   def terminal(self):
       """
       终止线程池中的所有线程
       """
       self.cancel = True
       full_size = len(self.generate_list)
       while full_size:
           self.q.put(StopEvent)
           full_size -= 1

   @contextlib.contextmanager
   def worker_state(self, state_list, worker_thread):
       """
       用于记录线程中正在等待的线程数
       """
       state_list.append(worker_thread)
       try:
           yield
       finally:
           state_list.remove(worker_thread)

 

 

线程池的使用

pool = ThreadPool(5)

def callback(status, result):
    # status, execute action status
    # result, execute action return value
    pass


def action(i):
    print(i)

for i in range(30):
    ret = pool.run(action, (i,), callback)

time.sleep(5)
print(len(pool.generate_list), len(pool.free_list))
print(len(pool.generate_list), len(pool.free_list))
# pool.close()
# pool.terminate()

 

上下文管理与with语句

if self.terminal:  # False
    event = StopEvent
else:
    self.free_list.append(current_thread)
    event = self.q.get()
    self.free_list.remove(current_thread)

 

红色代码片段中:将当前线程添加到空闲列表,再去取任务,之后再从这个空闲列表中删除这个线程

可以用with和contextlib模块实现

Python中打开文件的方法有 with open(file, "w") as f: ...   这个方法在操作后会自动f.close()关闭文件

import contextlib
import queue


@contextlib.contextmanager
def work_state(xxx, val):
    xxx.append(val)
    try:
        yield 123
    finally:
        xxx.remove(val)
q = queue.Queue()
q.put("alex")
li = []
with work_state(li, 1) as f:  # yield的123就是f
    print(f)  # 123
    q.get()

 核心是 try: yield [f]  finally: [action]             

使用的时候with ... as 得到的就是这个[f]   而with句里执行完毕后会自动执行[action]部分, 有点像装饰器

我们也实现一个类似的MyOpen方法

import contextlib


@contextlib.contextmanager
def MyOpen(file_path, mode):
    f = open(file_path, mode, encoding="utf-8")
    try:
        yield f
    finally:
        f.close()
with MyOpen("aaa1.json", "r") as file_handler:
    print(file_handler.readline())

Python协程

一个线程的执行需要很长的时间,这个时间段内还可以干一些别的事

创建进程消耗的资源不少。用一个线程实现多个线程能做的事。经常用一些不需要CPU介入的操作,如IO密集型的操作

“协程就是你可以暂停执行的函数”。如果你把它理解成“就像生成器一样”,那么你就想对了。 线程和进程的操作是由程序触发系统接口,最后的执行者是系统;协程的操作则是程序员。

协程存在的意义:对于多线程应用,CPU通过切片的方式来切换线程间的执行,线程切换时需要耗时(保存状态,下次继续)。协程,则只使用一个线程,在一个线程中规定某个代码块执行顺序

协程也叫微线程

可以实现协程的模块,import greenlet

from greenlet import greenlet


def test1():
   print(12)
   gr2.switch()  # 调到test2执行
   print(34)
   gr2.switch()


def test2():
   print(56) 
   gr1.switch()  # 跳回到test1执行,并且从上次test1的位置继续往后执行
   print(78)


gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch()

 

结果是12  56  34  78 

import gevent


def f1():
   print("1")
   gevent.sleep(0)
   print("2")


def f2():
   print("3")
   gevent.sleep(0)
   print("4")

gevent.joinall([
   gevent.spawn(f1),
   gevent.spawn(f2),
])

 

执行结果是 1 3 2 4

当前线程会在gevent.sleep(0) 表示不要在这个协程停顿了,到下一个协程去

比如点开网页上的5张图片,每张图片都需要等待,这时候可以用协程。比如IO操作费时的时候,遇到某个费时的操作先执行后边的操作,你执行完了再告诉我结果

 

gevent模块内部调用的其实也是greenlet模块的方法

以上的greenlet和gevent都是手动切换

自动切换:

程序自上而下执行,joinall就是等待, 等待这里的程序运行结束后退出

from gevent import monkey; monkey.patch_all()
import gevent
import requests

def f(url):
   print('GET: %s' % url)
   resp = requests.get(url)
   data = resp.text
   print('%d bytes received from %s.' % (len(data), url))

print("before")
gevent.joinall([
       gevent.spawn(f, 'https://www.python.org/'),
       gevent.spawn(f, 'https://www.yahoo.com/'),
       gevent.spawn(f, 'https://github.com/'),
       gevent.spawn(print("keep on"))
])
print("after")

结果是

程序先运行了before然后进入运行gevent, 执行了三个HTTP请求并继续向下运行,打印了keep on,最后再打印after

一个线程同时发多个IO请求,相当于开启了多线程。在gevent里的事件会并发,gevent外的事件顺序执行

基于socket模拟网站请求流程

对于server端返回的信息,如果是返回了字符串,我们可以在浏览器中用127.0.0.1:端口号, 打开页面即可以看到这个信息。而访问网站也是一样,我们输入了IP,而网页的server端返回给我们了一长串的html字符串,再由浏览器解释成为网页页面

当然我们可以把html写在html文件中,边读边发

 

 

 

 

 

 

。。。。。。。本节结束。。。。。。。

 

posted on 2016-07-16 07:14  Wiesler  阅读(171)  评论(0)    收藏  举报