一、SOCKET

  1、socket是什么?
  socket就是一个通信链的句柄,可以实现不同虚拟机或不同计算机之前的通信。也可以说是由[IP地址,协议,端口号]三元组来标识进程的通信机制。这就像是电话系统中,以电话号码加上分机号码来决定通话对象一般。

  2、socket的类型
    socket可以分为以下几种情况:
    (1)、socket.SOCK_STREAM
      此类型socket是面向TCP的连接,即建立可靠连接,然后数据以字节流的形式传输,从而保证数据的可靠,不重复,有序性。此类socket也是网络编程中默认的类型。

    (2)、socket.SOCK_DGRAM

      此类型socket是面向UDP的连接,即建立不可靠连接,然后数据以数据报文的形式传输,不能保证数据的可靠性、有序性。
    (3)、ocket.SOCK_RAW

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

    (4)、socket.SOCK_RDM

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

    由于后两者涉及较少,此处暂不涉及详述;UDP使用场景较少,此处暂不涉详述。除此以外,可以看出,socket类型主要是受到TCP和UDP协议的影响,分为了面向连接和无连接两种。

  3、socket的地址簇

     地址簇存在以下几种情况:

     socket.AF_UNIX      unix本机进程间通信

     socket.AF_INET    IPV4通信所需

     socket.AF_INET6     IPV6通信所需

    socket自身默认的地址组为socket.AF_INET

  4、socket类型详述

    (1)、socket.SOCK_STREAM

    举例如下:

     服务端

# -*- coding:utf-8 -*-

import socket
server =socket.socket()

while True:
    print("开始接听电话")
    server.bind(("127.0.0.1",6666))#服务端绑定指定IP和端口
    server.listen()#开启监听
    #conn是客户端连过来而在服务器端为其生成的一个连接实例
    conn,addr=server.accept()#等待连接进入
    while True:
        data=conn.recv(1024)#指定接受的数据的大小
        print("data:",data.decode("utf-8"))
        conn.send(data.upper())#指定回传的数据

server.close()

    客户端

# -*- coding:utf-8 -*-

import socket

client=socket.socket()#选择传输协议,同时创建传输实例
client.connect(("127.0.0.1",6666)) #告诉client连接此地址的此端口


while True:
    sendmsg=input(">>:")
    if sendmsg=="q":
        break
    elif len(sendmsg)==0:
        continue
    else:
        client.send(sendmsg.encode("utf-8"))#此传输实例需要传输的内容
        data=client.recv(1024)#告诉此传输实例节接收数据的大小
        print("data:",data.decode("utf-8"))#展示对方发过来的数据

client.close()#类似于通道关闭

    

     从以上示例可以看出,在选择传输协议时,并未对传输协议进行定义,而是直接留白使用默认参数。由此可看出socket默认使用TCP协议进行传输,即默认使用socket.STREAM。

    另,也可以通过以上简单示例得到面向连接的socket模型:

      

 

   

  5、socketserver

    socket创建的服务无法进行并发处理,当有大量请求需要处理时,多数请求就会阻塞在队列中,甚至是被丢弃。若使用socket来满足大规模并发处理,则需要大量创建socket会话,资源耗费巨大,且效率低下。

    socketserver简化了网络服务器的编写,弥补了socket的这项短板,可以直接大批量创建对应类型的socket,实现数据的并发处理。

    socketserver能够创建以下苏中类型的socket:

    TCPServer:

    对应代码:server=socketserver.TCPServer((Host,Port),TCPHandler)

    注释: 此为创建TCP类型的socket,使用Host,Port来作为绑定的IP和端口,创建成功后,若有链接过来,则自动调用TCPHandler函数执行对应操作。

    UDPServer:server=socketserver.UDPServer((Host,Port),UDPHandler)

    注释:此为创建UDP类型的socket,使用Host,Port来作为绑定的IP和端口,创建成功后,若有链接过来,则自动调用UDPHandler函数执行对应操作。

    对应代码:

    ThreadingTCPServer

    对应代码:server=socketserver.ThreadingTCPServer((Host,Port),TCPHandler)

    注释:此为创建多进程TCP类型的socket,使用Host,Port作为绑定的IP和端口,创建成功后,若有链接过来,则自动调用TCPHandler函数执行对应操作。

    UnixStreamServer

    对应代码:server=socketserver.UnixStreamServer((Host,Port),TCPHandler)

    注释:此为创建Unix下的TCP类型socket,使用Host,Port来作为绑定的IP和端口,创建成功后,若有链接过来,则自动调用TCPHandler函数执行对应操作。

       UnixDatagramServer

    对应代码:server=socketserver.UnixDatagramServer((Host,Port),UDPHandler)

    注释:此处创建Unix下的UDP类型socket,使用Host,Port来作为绑定的IP和端口,创建成功后,若有链接过来,则自动调用TCPHandler函数执行对应操作。

  

    以上四个类型之间的关系,借用Alex的图表示为:

  

  +------------+
  | BaseServer |
  +------------+
        |
        v
  +-----------+        +------------------+
  | TCPServer |------->| UnixStreamServer |
  +-----------+        +------------------+
        |
        v
  +-----------+        +--------------------+
  | UDPServer |------->| UnixDatagramServer |
  +-----------+        +--------------------+

    关于此处使用到的TCPHandler、UDPHandler,必须要继承自BaseRequestHandler,同时重写类中的handler函数, 此函数使用时可调用setup()、handle()、finish()
    此三类函数分别代表:
    setup()此函数会在链接开始创建时运行
    handle()此函数是BaseRequestHandler的主体,数据链接基本在靠此函数。
    finish()此函数会在连接结束时运行
    关于socketserver的运行,有两种调用方式:
    1、 server.handle_request()此种调用只处理一个请求,处理完成程序退出。
    2、server.server_forever()此种调用会一直监控运行,接受多个请求的连接。

    socketserver示例:
    服务端:
  
# -*- coding:utf-8 -*-



import socketserver
import sys,os,json


class MyTCPHandler(socketserver.BaseRequestHandler):

    def setup(self):
        print("连接开始".center(50,"·"))

    def handle(self):
        while True:
            try:
                data=self.request.recv(1024)
                Ndata=json.loads(data,encoding="utf-8")
                Bdata=Ndata.upper()
                print(Bdata)
                self.request.send(json.dumps(Bdata).encode())
            except:
                print("链接已断开。")
    def finish(self):
        print("连接结束".center(50,"·"))


if __name__=="__main__":
    try:
        server=socketserver.ThreadingTCPServer(("127.0.0.1",8000),MyTCPHandler)
        print("Server is Running".center(50,"-"))
        print("Server地址:%s,端口号:%s".center(32," ")%("127.0.0.1",8000))
        server.serve_forever()
    except:
        print("当前server所需端口已被占用,请检查!")

    客户端:

    

# -*- coding:utf-8 -*-



import socket,json


client=socket.socket()
client.connect(("127.0.0.1",8000))
while True:
    data=input(">>")
    if len(data)==0:
        break
    client.send(json.dumps(data).encode())
    newdata=client.recv(1024)
    print(json.loads(newdata,encoding="utf-8"))

 

 

  6、socket的粘包问题:

     socket在使用TCP协议进行传输时,存在粘包的情况,而在使用UDP传输的时,不存在粘包的情况。

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

     接收方接收到粘包的除了发送方引起的原因外,还有就是接收方不知道连续收到的各个消息之间的界限,不知道一次性提取多少字节的数据。

     UDP之所以不存在引起粘包问题,是由于使用UDP协议发送数据时,不使用Negal算法优化,不会将多个小包合并一次发送出去。另外,在UDP协议的接收端,采用了链式结构来记录每一个到达的UDP包,这样接收端应用程序一次recv只能从socket接收缓冲区中读出一个数据包。也就是说,发送端send了几次,接收端必须recv几次(无论recv时指定了多大的缓冲区)。

     最简单的解决办法:

     若需要一次发送多条信息,可以在每条信息之后使用time.sleep()来进行接收发送的等待

     稍微靠谱的解决办法:

     两端必须使用一收一发的形式进行交互,不使用同时接收或同时发送一条信息以上的情况。

 

  7、paramiko模块

     paramiko模块提供了ssh及sftp进行远程登录服务器执行命令和上传下载文件的功能。此模块为第三方模块,使用前需要进行安装。
   

     SSH连接服务器的简单模型:使用用户名和密码:

#引入paramiko模块
import paramiko

#建立一个SSHClient对象
client=paramiko.SSHClient()
#允许将信任的主机自动加入到host_allow列表,如果不使用此句,则会因地址不在信任列表导致SSH连接失败
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
#使用connect进行主机连接
client.connect(hostname="192.168.101.171",port=22,username="root",password="session")
#执行相应命令,并返回相应结果。返回的结果分为三种:1、stdin   标准输入 一般此类型不会存在返回。2、stdout 标准输出  3、stderr 标准错误   标准输出和标准错误同时只会出现一种。
stdin,stdout,stderr=client.exec_command('df -h')

#此处可做判断,判断哪个是正常输出的。
print(stdout.read().decode())
#链接关闭
client.close()

  

      使用SFTP模式连接数据库进行文件传输:使用用户名和密码:

  

这类需要登录方式使用密码验证。
#建立SSH连接
transport =paramiko.Transport(("192.168.101.171",22))
#配置连接参数
transport.connect(username="root",password="session")
#SFTPclient与连接对接
sftp=paramiko.SFTPClient.from_transport(transport)
#选择本地文件和远程存放目录
sftp.put(r"C:\Users\Desktop\1.txt","/test/1.txt")
#连接关闭
transport.close()

 

      使用SSH连接服务器:使用证书:

 

      前期工作:

        1、首先需要生成一个公钥对

        可以使用SecureCRT或者Xshell进行生成。

        生成的密钥使用openssh密钥格式,若使用默认格式,则上传到服务器后需要进行格式转换。

        生成文件会产生两个:Identity和Identity.pub。Identity为私钥 pub为公钥。

        2、将生成的公钥pub上传到服务器上。

        此文件需要放置在服务器的指定位置,即每个用户的家目录的.ssh目录下,若此目录没有的话可直接创建。

        3、将文件放到.ssh下后,若密钥格式不为openssh格式,则需要执行

        ssh-keygen -i -f Identity.pub >> authorized_keys

        将pub公钥下的内容导入到认证文件authorized_keys内

        chmod 600 authorized_keys 为该文件夹赋值权限600

        4、更改SSH配置文件

        SSH配置文件位置是/etc/ssh/sshd_config

        文件需要修改的内容是:

        PermitRootLogin yes 允许root用户登录

        PubkeyAuthentication yes 允许使用认证密钥登录

        AuthorizedKeysFile .ssh/authorized_keys 认证密钥文件所在位置
        RSAAuthentication yes 允许使用RSA密钥

        PasswordAuthentication no 关闭密码认证

        最后重新加载SSHD服务 /etc/init.d/sshd restart

        设置完成以后,可使用CRT进行验证,CRT新建会话时选择使用公钥认证,去掉密码验证。

        至此,使用证书登录的环境已经搭建完成。

      代码部分:

private_key = paramiko.RSAKey._from_private_key_file("Identity.pub")
client=paramiko.SSHClient()

client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(hostname="192.168.3.68",port=22,username="root",key=private_key)

stdin,stdout,stderr=client.exec_command('df -h')
print(stdout.read().decode())

client.close()

 

 

二、进程与线程

 

  1、相关问题  

    什么是进程?

      进程:程序需要以一个整体的形式暴露给操作系统管理,里面包含对各种资源的调用,如内存的管理、网络接口的调用等。对各种资源管理的集合就可以称为进程。

  

    进程和线程谁运行的快?
      没有可比性。进程是资源的结合,线程是真正执行任务的。进程要想执行任务最终也是通过线程,因此比较进程和线程谁快的说法其实就是比较线程和线程谁快,说法不成立。

    进程和线程谁启动的更快
      启动线程快。启动进程相当于修一个屋子,要申请各项权限。启动线程相当于把一个人拉进屋子。

 

    线程和进程的区别
      1)、线程共享内存空间,进程的内存是独立的。

      2)、子进程复制一份父进程创建,两个子进程之间不能互相访问,数据不共享。同一个进程内的线程之间可以直接交流。两个进程想通信,必须通过一个中间代理来实现。

      3)、创建新线程很简单,创建新进程需要对其父进程进行一次克隆。

      4)、一个线程可以控制和操作同一进程里的其他线程,但是进程只能操作子线程。

 

  2、子进程的运行

    启动子线程,并计算所有子线程运行所需要的时间。

# -*- coding:utf-8 -*-

import time
import threading

# 启动子线程,并计算所有子线程运行所需要的时间。
def run(n):
    print("running task-%s"%n)
    time.sleep(2)


start_time=time.time()
t_objs=[]
for i in range(0,50):
    t=threading.Thread(target=run,args=[i])
    t.start()
    t_objs.append(t)

for j in t_objs:
    j.join()

print("%s"%(time.time()-start_time))

     res:

       

     其中threading模块的threading.Thread()为启动一个新线程。     

        threading.current_thread() 可以判断当前输出信息的是哪个进程。

        threading.active_count()可以统计当前活跃的线程数,包括主线程

      *.join()是主线程用来等待子线程运行结束后再行继续

      *.start()是用来启动子线程

  3、守护进程

      将线程更改为守护线程以后,主进程不会再等待守护线程执行完成。只要主线程执行结束,那么进程只会检查普通线程是否都已经执行完成,没有的话会立即结束任务。这期间不会考虑守护线程的执行情况

     ( setDaemon(True)为设置为守护线程,需要在线程启动之前设置。)

# -*- coding:utf-8 -*-

import time
import threading


def run(n):
    print("running task-%s"%n,threading.current_thread(),threading.active_count())
    time.sleep(2)


start_time=time.time()
t_objs=[]
for i in range(0,50):
    t=threading.Thread(target=run,args=[i])
    t.setDaemon(True)
    t.start()
    t_objs.append(t)

print("%s"%(time.time()-start_time))

  4、全局锁(GIL锁) 

      程序看上去总是并发的,即使是CPU是单核的情况下。单核情况下的并发现象是因为程序执行了上下文的切换,使得看上去是并发。但是单核的情况下肯定是串行的。


      而多核CPU则是真真正正是并发的,如果CPU是四核的,那么就可以同时执行四个任务。


      python中,无论CPU有几个核心,并发都是假象。


      因为python诞生的时候还是单核心时代,在得到物理限制的解除以后,得到了更多的处理器性能,为了更好的利用此性能,也就出现了多线程的编程方式,但python的线程是调用操作系统的原生线程,即为C的线程接口。python不能去指挥C的线程,只能等待接口返回结果,

        无法通过改变线程运行方式来解决此问题,因此添加了GIL全局锁的功能。


      GIL全局锁全程为 Global Interpreter Lock,起到的作用就是一个数据,在全局存在多个线程的情况下,仅有一个线程能够拿到这个数据,仅有一个线程能够处理这个数据。


      全局锁不需要代码操作,此为python自身特性。

  5、线程锁      

      线程锁保证了同一个数据在同一时间仅允许一个处理器进行调用,保证了数据的统一性。


      此说法与GIL锁类似,但是线程锁是在程序将数据进行复制,发送给了不同的处理器的前提下。这样的话数据就不算同一个数据,GIL锁就无法保证数据的统一性。


      程序加上线程锁以后,处理过程就变为串行


      python3没有明确说明线程默认已经上锁,在使用多线程的时候仍旧需要加上。

def run(n):
    lock.acquire()
    global  number
    number+=1
    time.sleep(1)
    lock.release()

lock=threading.Lock()
number=0
t_list=[]
for i in range(0,50):
    t=threading.Thread(target=run,args=[i])
    t.start()
    t_list.append(t)
#主线程不等待子线程完成的话,则无法完成相应的计算。
for item in t_list:
    item.join()
print(number)

      此模式下可观察到,等待时间变为50s,可以判断出程序已经变为串行模式。

  6、递归锁

      递归锁就是一个大锁中间再包含若干小锁,用法与线程锁类似。

      创建递归锁的形式为:

         lock = threading.RLock()

      带入到实际应用中则为以下形式:

import threading,time

def run1():
    print("grab the first part data")
    lock.acquire()
    global num
    num +=1
    lock.release()
    return num
def run2():
    print("grab the second part data")
    lock.acquire()
    global  num2
    num2+=1
    lock.release()
    return num2
def run3():
    lock.acquire()
    res = run1()
    print('--------between run1 and run2-----')
    res2 = run2()
    lock.release()
    print(res,res2)


if __name__ == '__main__':

    num,num2 = 0,0
    lock = threading.RLock()
    for i in range(10):
        t = threading.Thread(target=run3)
        t.start()

while threading.active_count() != 1:
    print(threading.active_count())
else:
    print('----all threads done---')
    print(num,num2)

  7、信号量

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

import time
import threading


def run(n):
    sem.acquire()
    global  number
    number+=1
    print(number)
    time.sleep(1)
    sem.release()




number=0
t_list=[]
sem=threading.BoundedSemaphore(5)
for i in range(0,50):
    t=threading.Thread(target=run,args=[i])
    t.start()
    t_list.append(t)
#主线程不等待子线程完成的话,则无法完成相应的计算。
for item in t_list:
    item.join()

      最终输出的时候可以看到,输出的数量是以5个为一组进行输出,不会以下全部输出。

  8、event

      python线程的事件用于主线程控制其他线程的执行,事件主要提供了四个方法wait、clear、set、is_set

      wait 等待被设置为True

      clear 将flag设置为False

      set 将flag设置为True

      is_set 判断flag是否为True

 

      事件处理的机制:全局定义了一个“Flag”,如果“Flag”值为 False,那么当程序执行 event.wait 方法时就会阻塞,如果“Flag”值为True,那么event.wait 方法时便不再阻塞。

 

      clear:将“Flag”设置为False

      set:将“Flag”设置为True

# -*- coding:utf-8 -*-

import threading
import time
def run(count):
    while True:
        #event.set()
        count+=1
        if count>5 and count<=10:
            event.clear()
            print("1111111111111111111红灯")
        elif count>10:
            #event.set()
            print("22222222222222222222绿灯")
            count=0
        else:
            print("22222222222222222222绿灯")
        time.sleep(1)



count=0
event=threading.Event()
light=threading.Thread(target=run,args=(count,))
light.start()

   9、多进程

     1、多进程的引入

       import multiprocessing

       示例:

import multiprocessing
import time

def run(name):
    time.sleep(1)
    print("Hello,",name)

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

  10、进程间通信

    1.使用queue()

      1)、线程queue:

        引入  import queue

        调用: q=queue.Queue()

        示例:

# -*- coding:utf-8 -*-
import queue
import threading

def run():
    a.put({1:2})

if __name__=="__main__":
    a=queue.Queue()
    p=threading.Thread(target=run)
    p.start()
    print(a.get().get(1))

        结果显示为    2

        线程queue不能传给进程queue。

      3)、进程queue 

        引入:from multiprocessing import Queue

        调用:q=Queue()

        示例:

from multiprocessing import Process
from multiprocessing import Queue

def run(a):
    a.put({1:2})

if __name__=="__main__":
    a=Queue()
    p=Process(target=run,args=(a,))
    p.start()
    print(a.get().get(1))

    2、使用管道的方式 Pipe()

      引入:    from multiprocessing import Pipe

      调用 :    father_conn,child_conn=Pipe()

      示例:

from multiprocessing import Pipe
from multiprocessing import Process

def run(a):
    a.send("this is a child speaking")
    f=a.recv()
    a.send(f)

if __name__=="__main__":
    father_conn,child_conn=Pipe()
    p=Process(target=run,args=(child_conn,))
    p.start()
    print(father_conn.recv())
    father_conn.send("this is a father speaking")
    print(father_conn.recv())

    3、数据共享 manager()

      引入:from multiprocessing import Manager

      调用:d=manager.dict()

      示例:

# -*- coding:utf-8 -*-

import multiprocessing
import time
import os
from multiprocessing import Manager

def run(d,l):
    d[1]=123
    d[2]=234
    d[3]=345
    l.append(os.getpid())
    time.sleep(1)

if __name__=="__main__":
    list=[]
    with Manager() as manager:
        d=manager.dict()
        l=manager.list()
        for i in range(0,10):
            p=multiprocessing.Process(target=run,args=(d,l))
            p.start()
            list.append(p)
        for item in list:
            item.join()
            print(d,l)

  11、进程锁

      示例:

from multiprocessing import Lock,Process

def f(l,i):
    l.acquire()
    print("hello",i)
    l.release()

if __name__=="__main__":
    lock=Lock()
    for item in range(10):
        Process(target=f,args=(lock,item,)).start()

      虽然相对于每个进程而言数据是独立的,看上去并不需要对应的锁。但是对于进程而言,他们之间共享的是屏幕的输出,若不使用锁对屏幕的输出进行控制,那么可能存在一行输出还未打完,新来的一行自动将前面的输出顶掉,出现输出上的问题。

  12、进程池

     示例:

# -*- coding:utf-8 -*-



from  multiprocessing import Process,Pool
import time,os

def Foo(i):
    time.sleep(2)
    print("Process:",os.getpid())
    return i+100

def Bar(i):
    print('-->exec done:',os.getpid())



if  __name__=="__main__":
    pool = Pool(5)
    for i in range(10):
        pool.apply_async(func=Foo, args=(i,),callback=Bar)#callbock=回调, 即为执行完成Foo以后,再次执行Bar

    print('end')
    pool.close()
    pool.join()#进程池中进程执行完毕后再关闭,如果注释,那么程序直接关闭。

    示例中:

    pool=Pool(5)  即为 pool=Poll(process=5)

    pool.apply_async 表示串行

    pool.apply 表示并行 

 

三、协程

    协程,又称微线程,一句话说明什么是协程:协程是一种用户态的轻量级线程。CPU不认识协程,最小只认识线程。

    协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:

    协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置    

    协程的优点:    

      无需线程上下文切换的开销

      无需原子操作锁定及同步的开销,(协程是单线程,本身就是串行)

      方便切换控制流,简化编程模型

      支持高并发,高扩展,低成本,一个CPU可支持上万个协程

    缺点:

·      无法利用多核资源,协程需要和进程配合才能运行在多CPU上。我们日常所编写的绝大部分应用都没有这个必要,除非是CPU密集型应用。

       进行阻塞(blocking)操作时,如IO操作,会阻塞掉整个程序。

 

    yield实现的效果即为单线程下并发运算的效果。

    yield的效果是保留当前结果并暂停

    示例:

# -*- coding:utf-8 -*-

import time
def chibaozi(name):
    print("%s准备开始吃包子啦!"%name)
    while True:
        zuobaozi=yield
        print("%s吃了包子"%name)


def zuobaozi():
    c1=chibaozi("ss")
    c2=chibaozi("aa")
    c1.__next__()
    c2.__next__()

    for i in range(0,3):
        print("做了一个大包子")
        time.sleep(1)
        c1.send(i)
        c2.send(i)

zuobaozi()

    其中: next调用暂停的yield,send是调用yield并同时给yield传值

    1、greenlet: gevent封装了greenlet,gevent相当于自动挡,greenlet相当于手动挡。因此若需使用greenlet,安装gevent即可。

       引入: from greenlet import greenlet

       调用:gr1=greenlet(对应函数)

       示例:

# -*- coding:utf-8 -*-


from greenlet import greenlet

def f1():
    print(12)
    gr2.switch()
    print(34)
    gr2.switch()
def f2():
    print(56)
    gr1.switch()
    print(78)


gr1=greenlet(f1)
gr2=greenlet(f2)
gr1.switch()

    2、gevent  自动挡切换

     引入: import gevent

     调用: gevent.joinall

     示例:

import gevent

def  func():
    print("Running into func")
    gevent.sleep(2)
    print("Running into func again")


def func2():
    print("Running into func2")
    gevent.sleep(1)
    print("Running into func2 again")

def func3():
    print("Running into func3")
    gevent.sleep(1)
    print("Running into func3 again")

gevent.joinall([
    gevent.spawn(func),
    gevent.spawn(func2),
    gevent.spawn(func3),
])import gevent

def  func():
    print("Running into func")
    gevent.sleep(2)
    print("Running into func again")


def func2():
    print("Running into func2")
    gevent.sleep(1)
    print("Running into func2 again")

def func3():
    print("Running into func3")
    gevent.sleep(1)
    print("Running into func3 again")

gevent.joinall([
    gevent.spawn(func),
    gevent.spawn(func2),
    gevent.spawn(func3),
])

      其中,在此测试代码中,使用gevent.sleep()使gevent交出控制权,来模拟此处正在进行IO操作。

      以上代码在执行的时候,会在:

      Running into func

      Running into func2

      在此之后存在停顿1s,之后再次输出以下内容

      Running into func2 again

      Running into func again

      原因:首先执行func()的函数,在执行完成第一个print后,遇到IO输出(此处使用gevent.sleep模拟),需要2s时间,之后借此跳转到了func2()。

      跳到func2之后,进行了第一个print,同样遇到了一个1s的IO输出,借此再次跳转至func。

      结果func还需要1s才能执行完成,因此重新回到了func2,间隔1s后输出了func2的第二个print,再1s后输出了func第二个print

  3、urllib  下载网页

      借用此模块来说明gevent遇到真正的IO输出时需要注意的情况。

      先使用串行的方式来看下载网页需要多长时间

      串行示例:

import gevent
from urllib import request
import time
from gevent import monkey

def down(url2):
    print("Get : %s"%url2)
    res=request.urlopen(url2)
    data=res.read()
    # with open("url.html","wb") as f_wr:
    #     f_wr.write(data)
    print("lenth:%s"%len(data))

a=time.time()
url_list=["http://www.h3c.com/","http://www.taez.com.cn/","https://www.python.org/"]
for item in url_list:
    down(item)
b=time.time()
print(b-a)

      之后使用并行的方式来验证是否时间变快

import gevent
from urllib import request
import time


def down(url2):
    print("Get : %s"%url2)
    res=request.urlopen(url2)
    data=res.read()
    # with open("url.html","wb") as f_wr:
    #     f_wr.write(data)
    print("lenth:%s"%len(data))


a=time.time()
gevent.joinall([
    gevent.spawn(down,"http://www.h3c.com/"),
    gevent.spawn(down,"http://www.taez.com.cn/"),
    gevent.spawn(down,"https://www.python.org/")
])
b=time.time()
print(b-a)

    通过以上比较会发现,并行和串行的时间相差并不大,多次测试后可能还会在同一水平。

    之所以会引起以上原因,是因为gevent不能检测到外部urllib的IO输出,因此也就不能进行IO切换,从而造成并行跟串行其实都是使用串行的方式完成对应输出。

    若解决以上问题,需要使用打个补丁:

import gevent
from urllib import request
import time
from gevent import monkey

monkey.patch_all()

def down(url2):
    print("Get : %s"%url2)
    res=request.urlopen(url2)
    data=res.read()
    # with open("url.html","wb") as f_wr:
    #     f_wr.write(data)
    print("lenth:%s"%len(data))


a=time.time()
gevent.joinall([
    gevent.spawn(down,"http://www.h3c.com/"),
    gevent.spawn(down,"http://www.taez.com.cn/"),
    gevent.spawn(down,"https://www.python.org/")
])
b=time.time()
print(b-a)

      再次测试即可发现并行速度要比串行速度快。

 

四:事件驱动

  1、概念

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

    (1)每收到一个新的请求,就创建一个新的进程,来处理该请求

    (2)每收到一个新的请求,就创建一个新的线程,来处理该请求

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

   三种方法个各有利弊:

    (1)由于创建的是进程,开销比较大,所以会导致服务器性能比较差,但是实现简单。

    (2)由于操作涉及线程,可能会面临锁死的等问题。

    (3)在写应用程序代码时,逻辑相对于前两种方法较为复杂。

    综合考虑各方面因素,一般普遍认为第三种办法是大多数网络服务器采用的方式。

    

    事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。它的特点是包含一个事件循环,当外部时间发生时,使用回调机制来出发相应的处理。另外两种常见的编程范式是(单线程)同步以及多线程编程。

 

  2、多路复用

    (1)概念:

      用户空间与内核空间

        操作系统采用虚拟存储器,对于32位操作系统而言,他的寻址空间为4G(2的32次方),操作系统的核心是内核,独立于普通的应用程序,可以方位受保护的内存空间,也有访问底层硬件设备的所有权限。

        为乐保证用户进程不能直接操作内核,保证内核的安全,操作系统将虚拟空间划分为了两部分,一部分为内核空间,一部分为用户空间。

        对于linux系统而言,将最高的1G字节供内核使用,称为内核空间,而将较低的3G字节供各个进程使用,称为用户空间。

      进程的阻塞

        正在执行的进程,由于期待的事件未发生,如请求系统资源失败,等待某种操作的完成,新数据尚未到达或无新工作等,则由系统自动执行阻塞原语,使自己的运行状态变为阻塞状态。进入阻塞状态后,进程不占用CPU资源。

      文件描述符

        是一个用于表述指向文件的引用的抽象化概念的记录表。这一概念只适用于Unix和linux

      缓存I/O

        又被称为标准I/O,大多数文件系统的默认I/O操作都是缓存I/O。在linux的缓存I/O机制中,操作系统会将I/O的数据缓存在文件系统的页缓存中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲器拷贝到应用程序的地址空间。

        缓存IO有一个缺点:数据在传输过程中需要在应用程序空间和内核之间进行多次数据拷贝操作,这些数据拷贝操作所带来的CPU以及内存开销是非常大的。

     (2)IO模式

        IO存在两种情况:

          1)、等待数据准备

          2)、将数据从内核拷贝到进程中

          以上两点比较重要的一个过程是内核态的数据到用户态。

          注意:多路复用情况下,不论是select、poll、epoll,数据最终都是在内核态,

        因为这两种情况,linux系统产生了五种网络模式方案:

          1)、阻塞的IO (blocking IO)

              等待数据,此时状态为阻塞状态

              将数据从内核拷贝到进程中 此时阻塞

              特点: blocking IO的两个阶段都是被阻塞的(等数据的阶段和将数据从内核拷贝到用户进程的阶段)。

          2、非阻塞的I/O (nonblocking IO)

              进程不停询问内核数据是否准备完成,若内核返回error,则证明尚未准备完成,进程会继续发送问询信息,若内核数据准备完成,则会返回锁请求的数据。

              特点:进程需要不断的主动询问内核数据是否准备好了。
             

          3、I/O多路复用(IO multiplexing)

            IO多路复用就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO(即为事件驱动).

            select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO.他的基本原理就是select,poll,epoll这个函数会不断的轮训所负责的所有socket,当某个socket有数据到达,就通知用户进程。

            当用户进程调用了select,那么整个进程会被block,而同时,内核会监视所有select负责的socket,当任何一个socket中的数据准备好了以后,select就会返回,这时用户进程再调用read操作,将数据从内核读取到用户进程。

            I/O多路复用的一个特点是一个进程能够同时监控多个文件描述符,这些文件描述符中任意一个进入数据就绪状态,select就可以返回。

          4、信号驱动 

             由于signal driven IO在实际中并不常用,所有此处暂不涉及。

          5、异步IO(asynchronous I/O)

            用户发起read操作以后,内核会立刻返回相应的值,用户进程方不会因此产生任何阻塞,然后内核等待数据准备完成后,会将数据拷贝到用户内存,等这一切都完成以后,内核会向用户进程发送信号,告诉用户进程操作完成。

    (3)、总结

      blocking 和non-blocking的区别

        调用blocking IO 会一直block对应的进程,直到操作完成。而non-blocking IO在内核还在准备数据时会立刻返回。

 

      同步IO和异步IO的区别

        同步IO导致这个进程的请求一直处于阻塞状态,直到这个IO操作完成。

        异步IO不会导致请求的进程阻塞。 

      对应说明:

        non-blocking IO(非阻塞模式)的IO也存在阻塞的说法。定义中所指的““IO操作”是真实的IO操作,非阻塞的IO操作如果在前期数据没有准备好的时候,确实不存在阻塞情况。但是在数据准备好后,开始从内核态往用户态拷贝时,就会产生阻塞的情况。

        而异步IO则不一样,当进程发起IO操作之后,就直接返回再也不理睬,知道内核态发送一个信号,告知进程IO操作已经完成,在这个过程中,进程完全没有被阻塞。

    (4)select

      select 最早于1983年出现于4.2的BSD中。

        目前几乎在所有的平台上支持,具有良好的跨平台支持

      缺点

        1、与单个进程你能够监视的文件描述符数量存在最大限制,在linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制。

        2、select所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select会对所有socket进行一次线性扫描,这也浪费了一定的开销。

    (5)poll

      poll在1986年诞生于System V Release 3,它和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制。

      poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。

      另外,select()和poll()将就绪的文件描述符告诉进程后,如果进程没有对其进行IO操作,那么下次调用select()和poll()的时候将再次报告这些文件描述符,所以它们一般不会丢失就绪的消息,这种方式称为水平触发(Level Triggered)。

      (边缘触发:将就绪的文件描述符告诉给进程后,如果进程没有对其进行IO操作,那么下次不会再次报告这些文件描述符)

    (6)epoll

      具备select和poll的一切有点,被公认为linux2.6下性能最好的多路I/O就绪通知方法。

      epoll可以同时支持水平触发和边缘出发,理论上边缘出发的性能要更高一些,但是在代码实现上相当复杂。

      epoll童谣知告知哪些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,

      你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。

      另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描。

      而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。

    (7)select示例:

        

# -*- coding:utf-8 -*-

import socket
import select
import sys
import queue


msg_dic={}
server =socket.socket()
server.bind(("localhost",9999))
server.listen(100)

server.setblocking(0)#不阻塞状态
#不阻塞状态主要表现是 recv后不阻塞, accept也不阻塞。
#原来是server.accept()可以将程序阻塞。

inputs=[server,]#监控哪些链接
outputs=[]#
while True:
    reable,writeable,exceptional=select.select(inputs,outputs,inputs)#第一个是告诉系统监控那些链接,第三个为告诉系统监控哪些链接,如果这些
    #链接出现断开或者错误,就报错。

    for item in reable:
        #此处添加判断是为了区分inputs列表内过来的是server类的还是conn类的,若是server类的,则证明新过来一个连接
        #若是conn类的,则证明是原有连接发送过来的数据。
        if item is server:
            conn,addr=server.accept()
            inputs.append(conn)#连接已经建立,但是客户端尚未发送数据,若想接受数据,则需再次对此链接进行监测。
            msg_dic[conn]=queue.Queue()
        #此处为若是conn类,则执行数据的发送或接收。
        else:
            try:
                data=item.recv(1024)
                print(data.decode())
                newdata="服务器接收到的信息:"+data.decode()
                msg_dic[item].put(newdata.encode())
                outputs.append(item)
            except:
                print("连接断开")
                if  item in inputs:
                    inputs.remove(item)
                if item in outputs:
                    outputs.remove(item)
                break
    for w in  writeable:
        w.send(msg_dic[w].get())
        outputs.remove(w)

    for e in exceptional:
        if e in outputs:
            outputs.remove(e)
        if e in inputs:
            inputs.remove(e)
        if e in msg_dic:
            del msg_dic[e]
# -*- coding:utf-8 -*-

import socket

client=socket.socket()
client.connect(("localhost",9999))


while True:
    sendmsg=input(">>:")
    if sendmsg=="q":
        break
    elif len(sendmsg)==0:
        continue
    else:
        client.send(sendmsg.encode("utf-8"))#此传输实例需要传输的内容
        data=client.recv(1024)#告诉此传输实例节接收数据的大小
        print(data.decode("utf-8"))#展示对方发过来的数据

client.close()#类似于通道关闭    

      

五、扩展      

  os.getppid() 获取当前进程的父进程的ID号

  os.getpid()获取当前进程的ID号

 

  程序中的 if __name__=="__main__":

  此句话的意思是为了区分是否为主动执行这个脚本的,如果是主动执行这个脚本的,那么就会调用这个判断下的代码,如果是从其他地方import此模块,那么就不会调用这个判断下的代码。

 

  socketserver 中,socketserver.ForkingTCPServer((Host,Port),TCPHandler)代表启用进程。

   

posted on 2018-01-02 21:21  苏默守  阅读(307)  评论(0编辑  收藏  举报