返回顶部

4- 服务器模型

单进程服务器

1. 完成一个简单的TCP服务器

import socket

serv_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 重复使用绑定的信息
serv_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

address = ('', 8000)

serv_sock.bind(address)

serv_sock.listen(128)

while True:

    print('-----主进程,,等待新客户端的到来------')

    client_sock, client_addr = serv_sock.accept()

    print('-----主进程,接下来负责数据处理[%s]-----' % str(client_addr))

    while True:
        recv_data = client_sock.recv(1024)
        if len(recv_data) > 0:
            print('recv[%s]:%s' % (str(client_addr), recv_data.decode()))
        else:
            print('[%s]客户端已经关闭' % str(client_addr))
            break

    client_sock.close()

serv_sock.close()
View Code

2. 总结

  • 同一时刻只能为一个客户进行服务,不能同时为多个客户服务
  • 类似于找一个“明星”签字一样,客户需要耐心等待才可以获取到服务
  • 当服务器为一个客户端服务时,而另外的客户端发起了connect,只要服务器listen的队列有空闲的位置,就会为这个新客户端进行连接,并且客户端可以发送数据,但当服务器为这个新客户端服务时,可能一次性把所有数据接收完毕
  • 当recv接收数据时,返回值为空,即没有返回数据,那么意味着客户端已经调用了close关闭了;因此服务器通过判断recv接收数据是否为空 来判断客户端是否已经下线

 

多进程服务器

1. 多进程服务器

import socket

from multiprocessing import Process


# 处理客户端的请求并为其服务
def handle_client(client_sock, client_addr):
    while True:
        recv_data = client_sock.recv(1024)
        if len(recv_data) > 0:
            print('recv[%s]:%s' % (str(client_addr), recv_data.decode()))
        else:
            print('[%s]客户端已经关闭' % str(client_addr))
            break

    client_sock.close()


def main():

    server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    address = ('', 8000)
    server_sock.bind(address)
    server_sock.listen(128)

    while True:
        print('-----主进程,,等待新客户端的到来------')
        client_sock, client_addr = server_sock.accept()

        print('-----主进程,,接下来创建一个新的进程负责数据处理[%s]-----' % str(client_addr))
        process = Process(target=handle_client, args=(client_sock, client_addr))
        process.start()

        # 因为已经向子进程中copy了一份(引用),并且父进程中这个套接字也没有用处了
        # 所以关闭
        client_sock.close()

    # 当为所有的客户端服务完之后再进行关闭,表示不再接收新的客户端的链接
    server_sock.close()

if __name__ == '__main__':
    main()
View Code

2. 总结

  • 通过为每个客户端创建一个进程的方式,能够同时为多个客户端进行服务
  • 当客户端不是特别多的时候,这种方式还行,如果有几百上千个,就不可取了,因为每次创建进程等过程需要好较大的资源

多线程服务器

import socket

from threading import Thread


# 处理客户端的请求并执行事情
def handle_client(client_sock, client_addr):
    while True:
        recv_data = client_sock.recv(1024)
        if len(recv_data) > 0:
            print('recv[%s]:%s' % (str(client_addr), recv_data.decode()))
        else:
            print('[%s]客户端已经关闭' % str(client_addr))
            break

    client_sock.close()


def main():

    server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    address = ('', 8000)
    server_sock.bind(address)
    server_sock.listen(128)

    while True:
        print('-----主线程,,等待新客户端的到来------')
        client_sock, client_addr = server_sock.accept()

        print('-----主线程,,接下来创建一个新的线程负责数据处理[%s]-----' % str(client_addr))
        thread = Thread(target=handle_client, args=(client_sock, client_addr))
        thread.start()

        # 因为线程中共享这个套接字,如果关闭了会导致这个套接字不可用,
        # 但是此时在线程中这个套接字可能还在收数据,因此不能关闭
        # client_sock.close()

    server_sock.close()

if __name__ == '__main__':
    main()
View Code

单进程服务器-非堵塞模式

import socket

# 用来存储所有的新链接的客户端
client_list = []

def main():
    server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    address = ('', 8000)
    server_sock.bind(address)
    server_sock.listen(128)

    # 将套接字设置为非堵塞
    # 设置为非堵塞后,如果accept时,恰巧没有客户端connect,那么accept会
    # 产生一个异常,所以需要try来进行处理
    server_sock.setblocking(False)

    while True:

        try:
            client_info = server_sock.accept()
        except BlockingIOError:
            pass
        else:
            print("一个新的客户端到来%s:" % str(client_info))
            # 将套接字设置为非堵塞
            client_info[0].setblocking(False)
            client_list.append(client_info)

        # 用来存储需要删除的客户端信息
        need_del_client_info_list = []

        for client_socket, client_addr in client_list:
            try:
                recv_data = client_socket.recv(1024)
                if len(recv_data) > 0:
                    print('recv[%s]:%s' % (str(client_addr), recv_data.decode()))
                else:
                    print('[%s]客户端已经关闭' % str(client_addr))
                    client_socket.close()
                    need_del_client_info_list.append((client_socket, client_addr))
            except BlockingIOError:
                pass

        for client in need_del_client_info_list:
            client_list.remove(client)

if __name__ == '__main__':
    main()
View Code

select版-TCP服务器

1. select 原理

在多路复用的模型中,比较常用的有select模型和epoll模型。这两个都是系统接口,由操作系统提供。当然,Python的select模块进行了更高级的封装。

将需要判断有数据传来的(可读的)socket、可以向外发送数据的(可写的)socket及发生异常状态的socket交给select,select会帮助我们从中遍历找出有事件发生的socket,并返回给我们,我们可以直接处理这些发生事件的socket。

2. select 回显服务器

import queue
import socket
import select

PORT = 8080

listen_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listen_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

address = ("", PORT)
listen_sock.bind(address)

listen_sock.listen(128)

# 在该列表中保存让select进行判断是否能接收数据的套接字
input_list = [listen_sock]

# 在该列表中保存让select进行判断是否能发送数据的套接字
output_list = []

# # 消息容器队列
# {
#     "client1_sock":queue(msg1, msg2),
#     "clietn2_sock":...
# }
message_queue = {}

while 1:
    # 将input_list 与 output_list交给select进行遍历监视
    # input_list 表示 要select判断是否能够接收数据
    # output_list 表示 要select判断是否能够发送数据
    # 返回的recv_sock_list 中保存了能够立即接收数据的socket
    # 返回的send_sock_list 中保存了能够立即发送数据的socket
    recv_sock_list, send_sock_list, exception_sock_list = select.select(input_list, output_list, [])

    # 遍历处理可以接收数据或者接收请求的套接字列表recv_sock_list
    for sock in recv_sock_list:
        # 如果是监听的socket
        if sock is listen_sock:
            # 接收客户端的连接请求
            client_sock, client_addr = sock.accept()
            print("客户端%s进行了连接" % str(client_addr))
            # 将对接该客户端的socket添加到input_list的任务中,让select判断什么时候有数据出来
            input_list.append(client_sock)
            # 创建跟该客户端对应的队列,用于保存对应该客户端socket可能发送的数据
            message_queue[client_sock] = queue.Queue()
        else:
            # 与客户端对应的socket,接收数据
            recv_data = sock.recv(1024)
            if recv_data:
                print("客户端传来数据%s" % recv_data.decode())
                # 因为send操作默认会阻塞,所以现将socket放到监视的队列中(output_list),由select来监视什么时候能够发送数据
                output_list.append(sock)
                # 将要发送的数据先保存到message_queue中
                message_queue[sock].put(recv_data)
            else:
                # 客户端关闭连接
                # 清除掉与该客户端对应的消息容器
                del message_queue[sock]
                # 从input_list中移除掉该关闭的socket
                input_list.remove(sock)
                # 如果在output_list中也存在该socket,则也将其移除
                if sock in output_list:
                    output_list.remove(sock)
                # 关闭服务端与客户端通信的套接字
                sock.close()
                print("客户端关闭了连接")

    # 遍历处理返回的可以发送数据的套接字列表
    for sock in send_sock_list:
        # 如果与套接字对应的消息队列中的数据不为空,取 消息数据
        if not message_queue[sock].empty():
            msg = message_queue[sock].get()
            # 像客户端发送数据
            sock.send(msg)
        else:
            # 如果消息队列中的数据为空,则表示数据已经发送完成,将sock从需要监视的output_list移除
            output_list.remove(sock)
View Code

3. 总结

优点

select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。

缺点

select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。

一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.

对socket进行扫描时是依次扫描的,即采用轮询的方法,效率较低。

当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。

epoll版-TCP服务器

1. epoll的优点:

  1. 没有最大并发连接的限制,能打开的FD(指的是文件描述符,通俗的理解就是套接字对应的数字编号)的上限远大于1024
  2. 效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,epoll的效率就会远远高于select。

2. epoll使用参考代码

import socket
import select
import queue

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("", 8080))
s.listen(128)

# 创建一个epoll对象(或者理解为一个epoll容器)
epoll = select.epoll()

# 向epoll中添加需要epoll进行监视管理的socket(通过socket文件编号--fileno()的返回值--进行注册)
# EPOLLIN表示要进行input监控,即监视什么时候能够执行recv或者accept操作
epoll.register(s.fileno(), select.EPOLLIN)

# 用来保存文件编号与socket对应的关系
client_socks = {}
# 用来保存文件编号与客户端地址的对应关系
client_addrs = {}
# 用来保存要发送给客户端的消息数据
msg_queue = {}

while 1:
    # 询问epoll有无可以操作的socket,返回的epoll_list中包含可以进行处理的socket对应的文件编号
    epoll_list = epoll.poll()

    # 遍历能够处理的socket文件编号列表,依次进行处理
    # fd表示文件的编号
    # events表示是可以进行读取recv/accept操作还是可以进行写send操作
    for fd, events in epoll_list:
        # 如果是监听的socket文件
        if fd == s.fileno():
            # 接收客户端的请求
            conn, addr = s.accept()
            # 将连接的客户端的socket与地址保存
            client_socks[conn.fileno()] = conn
            client_addrs[conn.fileno()] = addr
            # 创建对应 该客户端的发送消息的队列
            msg_queue[conn.fileno()] = queue.Queue()
            print("%s已连接" % str(addr))
            # 将新的客户端的socket添加到epoll中进行监视管理
            epoll.register(conn.fileno(), select.EPOLLIN)
        # 如果是可以进行读取数据accept/recv的套接字
        elif events == select.EPOLLIN:
            recv_data = client_socks[fd].recv(1024)
            if recv_data:
                print("%s传来数据%s" % (str(client_addrs[fd]), recv_data.decode()))
                # 将要发送给客户端的数据放到消息队列中
                msg_queue[fd].put(recv_data)
                # 将这个与客户端进行通信的socket在epoll中的监视行为改为监视可否发送数据
                epoll.modify(fd, select.EPOLLOUT)
            else:
                # 客户端关闭了连接
                print("%s已关闭" % str(client_addrs[fd]))
                # 将socket从epoll中删除
                epoll.unregister(fd)
                # 删除与该socket对应的消息队列
                del msg_queue[fd]
                # 关闭该socket
                client_socks[fd].close()
                # 删除socket在cliet_socks和client_addrs中保存的数据
                del client_socks[fd]
                del client_addrs[fd]
        # 如果是可以进行发送数据send的套接字
        elif events == select.EPOLLOUT:
            # 消息队列中不为空,就取出数据发送
            if not msg_queue[fd].empty():
                msg = msg_queue[fd].get()
                client_socks[fd].send(msg)
            # 没有消息数据了,就将对该socket的监视行为改为监视是否有数据从客户端发送过来
            else:
                epoll.modify(fd, select.EPOLLIN)
View Code

2. 说明

  • EPOLLIN (可读)
  • EPOLLOUT (可写)
  • EPOLLET (ET模式)

epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:

LT模式:当epoll检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll时,会再次响应应用程序并通知此事件。

ET模式:当epoll检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll时,不会再次响应应用程序并通知此事件。

协程

协程,又称微线程,纤程。英文名Coroutine。

协程是啥

首先我们得知道协程是啥?协程其实可以认为是比线程更小的执行单元。 为啥说他是一个执行单元,因为他自带CPU上下文。这样只要在合适的时机, 我们可以把一个协程 切换到另一个协程。 只要这个过程中保存或恢复 CPU上下文那么程序还是可以运行的。

通俗的理解:在一个线程中的某个函数,可以在任何地方保存当前函数的一些临时变量等信息,然后切换到另外一个函数中执行,注意不是通过调用函数的方式做到的,并且切换的次数以及什么时候再切换到原来的函数都由开发者自己确定

协程和线程差异

那么这个过程看起来比线程差不多。其实不然, 线程切换从系统层面远不止保存和恢复 CPU上下文这么简单。 操作系统为了程序运行的高效性每个线程都有自己缓存Cache等等数据,操作系统还会帮你做这些数据的恢复操作。 所以线程的切换非常耗性能。但是协程的切换只是单纯的操作CPU的上下文,所以一秒钟切换个上百万次系统都抗的住。

协程的问题

但是协程有一个问题,就是系统并不感知,所以操作系统不会帮你做切换。 那么谁来帮你做切换?让需要执行的协程更多的获得CPU时间才是问题的关键。

例子

目前的协程框架一般都是设计成 1:N 模式。所谓 1:N 就是一个线程作为一个容器里面放置多个协程。 那么谁来适时的切换这些协程?答案是有协程自己主动让出CPU,也就是每个协程池里面有一个调度器, 这个调度器是被动调度的。意思就是他不会主动调度。而且当一个协程发现自己执行不下去了(比如异步等待网络的数据回来,但是当前还没有数据到), 这个时候就可以由这个协程通知调度器,这个时候执行到调度器的代码,调度器根据事先设计好的调度算法找到当前最需要CPU的协程。 切换这个协程的CPU上下文把CPU的运行权交个这个协程,直到这个协程出现执行不下去需要等等的情况,或者它调用主动让出CPU的API之类,触发下一次调度。

那么这个实现有没有问题?

其实是有问题的,假设这个线程中有一个协程是CPU密集型的他没有IO操作, 也就是自己不会主动触发调度器调度的过程,那么就会出现其他协程得不到执行的情况, 所以这种情况下需要程序员自己避免。这是一个问题,假设业务开发的人员并不懂这个原理的话就可能会出现问题。

协程的好处

在IO密集型的程序中由于IO操作远远慢于CPU的操作,所以往往需要CPU去等IO操作。 同步IO下系统需要切换线程,让操作系统可以在IO过程中执行其他的东西。 这样虽然代码是符合人类的思维习惯但是由于大量的线程切换带来了大量的性能的浪费,尤其是IO密集型的程序。

所以人们发明了异步IO。就是当数据到达的时候触发我的回调。来减少线程切换带来性能损失。 但是这样的坏处也是很大的,主要的坏处就是操作被 “分片” 了,代码写的不是 “一气呵成” 这种。 而是每次来段数据就要判断 数据够不够处理哇,够处理就处理吧,不够处理就在等等吧。这样代码的可读性很低,其实也不符合人类的习惯。

但是协程可以很好解决这个问题。比如 把一个IO操作 写成一个协程。当触发IO操作的时候就自动让出CPU给其他协程。要知道协程的切换很轻的。 协程通过这种对异步IO的封装 既保留了性能也保证了代码的容易编写和可读性。在高IO密集型的程序下很好。但是高CPU密集型的程序下没啥好处。

协程一个简单实现

import time

def A():
    while True:
        print("----A---")
        yield
        time.sleep(0.5)

def B(c):
    while True:
        print("----B---")
        c.next()
        time.sleep(0.5)

if __name__=='__main__':
    a = A()
    B(a)
View Code

运行结果:

--B--
--A--
--B--
--A--
--B--
--A--
--B--
--A--
--B--
--A--
--B--
--A--
...省略...
View Code

协程-greenlet版

为了更好使用协程来完成多任务,python中的greenlet模块对其封装,从而使得切换任务变的更加简单

安装方式

使用如下命令安装greenlet模块:

sudo pip install greenlet

 

#coding=utf-8

from greenlet import greenlet
import time

def test1():
    while True:
        print "---A--"
        gr2.switch()
        time.sleep(0.5)

def test2():
    while True:
        print "---B--"
        gr1.switch()
        time.sleep(0.5)

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

#切换到gr1中运行
gr1.switch()
View Code

运行效果

---A--
---B--
---A--
---B--
---A--
---B--
---A--
---B--
...省略...

gevent

greenlet已经实现了协程,但是这个还的人工切换,是不是觉得太麻烦了,不要捉急,python还有一个比greenlet更强大的并且能够自动切换任务的模块gevent

其原理是当一个greenlet遇到IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。

由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO

1. gevent的使用

#coding=utf-8

#请使用python 2 来执行此程序

import gevent

def f(n):
    for i in range(n):
        print gevent.getcurrent(), i

g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()
g2.join()
g3.join()
View Code

运行结果

<Greenlet at 0x10e49f550: f(5)> 0
<Greenlet at 0x10e49f550: f(5)> 1
<Greenlet at 0x10e49f550: f(5)> 2
<Greenlet at 0x10e49f550: f(5)> 3
<Greenlet at 0x10e49f550: f(5)> 4
<Greenlet at 0x10e49f910: f(5)> 0
<Greenlet at 0x10e49f910: f(5)> 1
<Greenlet at 0x10e49f910: f(5)> 2
<Greenlet at 0x10e49f910: f(5)> 3
<Greenlet at 0x10e49f910: f(5)> 4
<Greenlet at 0x10e49f4b0: f(5)> 0
<Greenlet at 0x10e49f4b0: f(5)> 1
<Greenlet at 0x10e49f4b0: f(5)> 2
<Greenlet at 0x10e49f4b0: f(5)> 3
<Greenlet at 0x10e49f4b0: f(5)> 4
View Code

可以看到,3个greenlet是依次运行而不是交替运行

2. gevent切换执行

 

import gevent

def f(n):
    for i in range(n):
        print gevent.getcurrent(), i
        #用来模拟一个耗时操作,注意不是time模块中的sleep
        gevent.sleep(1)

g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()
g2.join()
g3.join()
View Code

运行结果

<Greenlet at 0x7fa70ffa1c30: f(5)> 0
<Greenlet at 0x7fa70ffa1870: f(5)> 0
<Greenlet at 0x7fa70ffa1eb0: f(5)> 0
<Greenlet at 0x7fa70ffa1c30: f(5)> 1
<Greenlet at 0x7fa70ffa1870: f(5)> 1
<Greenlet at 0x7fa70ffa1eb0: f(5)> 1
<Greenlet at 0x7fa70ffa1c30: f(5)> 2
<Greenlet at 0x7fa70ffa1870: f(5)> 2
<Greenlet at 0x7fa70ffa1eb0: f(5)> 2
<Greenlet at 0x7fa70ffa1c30: f(5)> 3
<Greenlet at 0x7fa70ffa1870: f(5)> 3
<Greenlet at 0x7fa70ffa1eb0: f(5)> 3
<Greenlet at 0x7fa70ffa1c30: f(5)> 4
<Greenlet at 0x7fa70ffa1870: f(5)> 4
<Greenlet at 0x7fa70ffa1eb0: f(5)> 4
View Code

3个greenlet交替运行

gevent版-TCP服务器

import sys
import time
import gevent

from gevent import socket,monkey
monkey.patch_all()

def handle_request(conn):
    while True:
        data = conn.recv(1024)
        if not data:
            conn.close()
            break
        print("recv:", data)
        conn.send(data)


def server(port):
    s = socket.socket()
    s.bind(('', port))
    s.listen(5)
    while True:
        cli, addr = s.accept()
        gevent.spawn(handle_request, cli)

if __name__ == '__main__':
    server(7788)
View Code

 

 

 

  

 

posted @ 2017-12-08 19:09  Crazymagic  阅读(211)  评论(0编辑  收藏  举报