python - socket模块2

本篇博文参考:
http://www.cnblogs.com/jishuweiwang/p/5660933.html
http://www.cnblogs.com/wupeiqi/articles/5040823.html

概述

默认应用程序:是单进程、单线程的。

  进程是资源分配的最小单位。与程序相比,程序只是一组指令的有序集合,它本身没有任何运行的含义,只是一个静态实体。进程是程序在某个数据集上 的执行,是一个动态实体。它因创建而产生,因调度而运行,因等待资源或事件而被处于等待状态,因完成任务而被撤消,反映了一个程序在一定的数据集上运行的 全部动态过程。每个正在系统上运行的程序都是一个进程。每个进程包含一到多个线程。进程也可能是整个程序或者是部分程序的动态执行。


  线程是轻量级的进程或子进程,是CPU调度的最小单位,所有的线程都存在于相同的进程。所以线程基本上是轻量级的进程,它负责在单个程序里执行 多任务。通常由操作系统负责多个线程的调度和执行。多线程是为了同步完成多项任务,不是为了提高运行效率,而是为了提高资源使用效率来提高系统的效率。


  由于Python中GIL的存在,GIL 会在进程级别加的一个逻辑锁,这个锁粒度很大,把整个系统资源看做一个整体,所以GIL 不管你有多少CPU核心,都看做一个CPU核心来用,虽然单进程多线程的程序拥有多个线程,但是同一时间之会有一个线程利用到CPU资源。因此为了提高 CPU利用率,通常会启用多进程,即启动多个Python进程来提高CPU的利率用,从而提高工作效率。

对比

 

 对比维度

多进程

多线程

总结

数据共享、同步

数据共享复杂,需要用IPC;数据是分开的,同步简单

因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂

各有优势

内存、CPU

占用内存多,切换复杂,CPU利用率低

占用内存少,切换简单,CPU利用率高

线程占优

创建销毁、切换

创建销毁、切换复杂,速度慢

创建销毁、切换简单,速度很快

线程占优

编程、调试

编程简单,调试简单

编程复杂,调试复杂

进程占优

可靠性

进程间不会互相影响

一个线程挂掉将导致整个进程挂掉

进程占优

分布式

适应于多核、多机分布式;如果一台机器不够,扩展到多台机器比较简单

适应于多核分布式

进程占优

选用
单进程,多线程的程序(io操作不占用CPU):如果是CPU密集型,那么则不能提高效率。如果是IO密集型,那么则能提高效率。

 多进程,单线程的程序:CPU密集型的,一般用多进程提高并发效率。


  小结:


  CPU密集型:多进程


  IO密集型:多线程

 

ThreadingTCPServer


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

 

 

 

ThreadingTCPServer实现的Soket服务器内部会为每个client创建一个 “线程”,该线程用来和客户端进行交互。

 

ThreadingTCPServer基础

 

使用ThreadingTCPServer:

 

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

 

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Auther: pangguoping
import socketserver

class MyServer(socketserver.BaseRequestHandler):
    def handle(self):
        #print self.request , self.client_address,self.server
        conn = self.request
        conn.sendall(bytes('欢迎致电10086,请输入1xxx,0转人工服务',encoding='utf-8'))
        Flag = True
        while Flag:
            data = conn.recv(1024)
            print(str(data.decode()))
            if str(data.decode()) == 'exit':
                Flag = False
            elif str(data.decode()) == '0':
                conn.sendall(bytes('通过可能会被录音',encoding='utf-8'))
            else:
                conn.sendall(bytes('请重新输入',encoding='utf-8'))

if __name__ == '__main__':
    server = socketserver.ThreadingTCPServer(('127.0.0.1',8090),MyServer)
    server.serve_forever()
socketserver实现服务端
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Auther: pangguoping

import socket

ip_port = ('127.0.0.1',8090)
sk = socket.socket()
sk.connect(ip_port)
sk.settimeout(5)

while True:
    data = sk.recv(1024)
    print('recive:',data.decode())  #相当于print('recive:',data,encoding='utf-8')
    inp = input('please input:')
    sk.sendall(bytes(inp,encoding='utf-8'))
    if inp == 'exit':
        break

sk.close()
socketserver实现客户端

 例子:

一个简单实现多并发案例:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# auth : pangguoping

import socketserver

class MyClass(socketserver.BaseRequestHandler):
    def handle(self):
        pass
#创建socket对象
#accept
#server_address = ('127.0.0.1',9999)
#RequestHandlerClass = MyClass == ()
#self.RequestHandlerClass() = MyClass() == ()
#1、obj封装了self.RequestHandlerClass = MyClass
#2、创建了socket,bind,listen

obj = socketserver.ThreadingTCPServer(('127.0.0.1',9999),MyClass)
obj.serve_forever()

 

例1:使用socketserver 实现多并发执行命令

server端:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# auth : pangguoping

#socketserver  实现多并发命令执行
import socketserver
import subprocess

class MyServer(socketserver.BaseRequestHandler):
    def handle(self):
        #print self.request,self.client_address,self.server
        conn = self.request
        conn.sendall(bytes('欢迎您的到来!',encoding='utf-8'))
        while True:
            data = conn.recv(1024)
            print("--->",len(data))
            if len(data) == 0:break
            print("[%s] says:%s" % (self.client_address,data.decode()))

            cmd = subprocess.Popen(data.decode(),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
            cmd_res = cmd.stdout.read()
            if not cmd_res:
                cmd_res = cmd.stderr.read()
            if len(cmd_res) == 0: # cmd has not output
                cmd_res = bytes('cmd has output',encoding='utf-8')
            #解决粘包问题
            ready_tag = 'Ready|%s' %len(cmd_res)
            conn.send(bytes(ready_tag,encoding='utf-8'))
            feedback = conn.recv(1024)    #收到客户端发送过来的Start
            feedback = str(feedback,encoding='utf-8')  #把收到的feedback转换为str
            if feedback.startswith('Start'):
                conn.send(cmd_res)

if __name__ == '__main__':
    server = socketserver.ThreadingTCPServer(('127.0.0.1',8070),MyServer)
    server.serve_forever()
server端代码

client端:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# auth : pangguoping

import socket

ip_port = ('127.0.0.1',8070)
sk = socket.socket()
sk.connect(ip_port)
welcome_msg = sk.recv(1024)
print("from server:",welcome_msg.decode())
while True:
    send_data = input(">>>:").strip()
    if len(send_data) == 0:continue
    sk.send(bytes(send_data,encoding='utf-8'))
    #解决粘包的问题
    ready_tag = sk.recv(1024)  #收到的格式为  Ready|9999
    ready_tag = str(ready_tag,encoding='utf-8')
    if ready_tag.startswith('Ready'):
        msg_size = int(ready_tag.split('|')[-1])
    start_tag = 'Start'
    sk.send(bytes(start_tag,encoding='utf-8')) #给server发送Start,告诉server可以准备发送数据了
    recv_sise = 0  #初始化数据大小
    recv_msg = b''
    while recv_sise < msg_size: #解决粘包问题
        recv_data = sk.recv(1024)
        recv_msg += recv_data
        recv_sise += len(recv_data)
        print('MSG SIZE %s RECV SIZE %s' %(msg_size,recv_sise))
    print(str(recv_msg,encoding='utf-8'))

sk.close()
client端代码

 

例2:使用socketserver 实现ftp上传文件功能

ftp_server端:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# auth : pangguoping

#ftp server 上传文件
import socketserver
import json

class MyServer(socketserver.BaseRequestHandler):
    def handle(self):
        #print self.request,self.client_address,self.server
        conn = self.request
        conn.sendall(bytes('欢迎您的到来!',encoding='utf-8'))
        while True:
            data = conn.recv(1024)
            if len(data) == 0:break
            print("[%s] says:%s" % (self.client_address,data.decode()))
            #[('127.0.0.1', 51852)]  says:{"file_size": 1670262, "filename": "IMG_20160714_172632.jpg", "action": "put"}
            task_data = json.loads(str(data,encoding='utf-8'))
            task_action = task_data.get("action")
            if hasattr(self,"task_%s" %task_action):
                func = getattr(self,"task_%s" %task_action)
                func(task_data)
            else:
                print("task action is not supported",task_action)


    def task_put(self,*args,**kwargs):
        print("put",args,kwargs)
        #put ({'file_size': 1670262, 'action': 'put', 'filename': 'IMG_20160714_172632.jpg'},) {}
        file_size = args[0].get('file_size')
        file_name = args[0].get('filename')
        server_response = {"status":200} #解决粘包问题
        self.request.send(bytes(json.dumps(server_response),encoding='utf-8'))
        f = open(file_name,'wb')
        recv_size = 0
        while recv_size < file_size:
            data = self.request.recv(4096)
            f.write(data)
            recv_size += len(data)
            print('filesize: %s recvsize:%s' %(file_size,recv_size))
        print('file reve success')
        f.close()





if __name__ == '__main__':
    server = socketserver.ThreadingTCPServer(('127.0.0.1',8010),MyServer)
    server.serve_forever()
ftp_server代码

ftp_client端:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# auth : pangguoping

#ftp 上传
import socket
import os
import json

ip_port = ('127.0.0.1',8010)
sk = socket.socket()
sk.connect(ip_port)
welcome_msg = sk.recv(1024)
print("from server:",welcome_msg.decode())

while True:
    send_data = input(">>>:").strip()
    if len(send_data) == 0:continue
    cmd_list = send_data.split()
    if len(cmd_list) < 2:continue
    task_type = cmd_list[0]
    if task_type == 'put':
        abs_filepath = cmd_list[1]
        if os.path.isfile(abs_filepath):
            file_size = os.stat(abs_filepath).st_size
            filename = abs_filepath.split('/')[-1]
            print('file:%s size:%s' %(abs_filepath,file_size))
            msg_data = {"action":"put",
                        "filename":filename,
                        "file_size":file_size}

            sk.send(bytes(json.dumps(msg_data),encoding='utf-8'))
            server_confirmation_msg = sk.recv(1024) #解决粘包问题
            confirm_data = json.loads(server_confirmation_msg.decode())
            if confirm_data['status'] == 200:

                print('start sending file',filename)
                f = open(abs_filepath,'rb')
                for line in f:
                    sk.send(line)
                print('send file done')

        else:
            print("\033[31;1mfile [%s] is not exist\033[0m" %abs_filepath)
            continue

    else:
        print('do not support task type',task_type)
        continue

    #sk.send(bytes(send_data,encoding='utf-8'))
    #收消息
    recv_data = sk.recv(1024)
    print(str(recv_data,encoding='utf-8'))

sk.close()
ftp_client代码

 

ThreadingTCPServer源码剖析

ThreadingTCPServer的类图关系如下:

1. 在实例化的时候,会调用socketserver.ThreadingTCPServer(...)的__init__方法,加载顺序如下

2. 调用对象的serve_forever方法



3. 最终进入自定义的类MyServer处理环节:从第二部最后一步中,执行了 self.self.RequestHandlerClass(request, client_address, self),即创建了一个MyServer的实例,那么在实例化的时候,就会执行MyServer类的__init__方法。明显地,MyServer中 没有定义__init__方法,那么从父类的__init__方法中继承。

发现在socketserver.BaseRequestHandler类中,__init__包含如下代码

    def __init__(self, request, client_address, server):
        self.request = request
        self.client_address = client_address
        self.server = server
        self.setup()
        try:
            self.handle()
        finally:
            self.finish()

 发现会调用self.handler()方法,最终又回到了MyServer定义的handler方法中,来处理客户端请求。

  至此,socketserver源码简单剖析完毕。

IO多路复用

I/O多路复用指:通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。

  IO多路复用适用如下场合:

  (1)当客户处理多个描述符时(一般是交互式输入和网络套接口),必须使用I/O复用。

  (2)当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。

  (3)如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。

  (4)如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。

  (5)如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。

  :IO多路复用不支持文件操作,支持其他IO操作,监控内部是否发生变化

监听socket对象内部是否变化了?sk = socket.socket() ,这个sk对象变化,表示有新连接来了。

什么时候变化? 连接或收发消息。 conn,address = sk.accept()  , 这个conn变化表示要收发消息了

服务器端有新连接来了,服务器端的socket对象就会发生变化。

IO多路复用  --- 监听socket对象内部是否变化了?

  与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

http://www.zhihu.com/question/32163005 这个链接里深入的介绍了IO多路复用的起源以及核心原理,这里不再赘述。

  其中重要的三项复用技术就是:select,poll,epoll

select,poll,epoll简介

 select

select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:

1 单个进程可监视的fd数量被限制

2 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大

3 对socket进行扫描时是线性扫描

Poll

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入 一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无 谓的遍历。

它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。

poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

Epoll

epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就需态,并且只会通知一次。

在前面说到的复制问题上,epoll使用mmap减少复制开销。

还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知

:水平触发(level-triggered)——只要满足条件,就触发一个事件(只要有数据没有被获取,内核就不断通知你)

  边缘触发(edge-triggered)——每当状态变化时,触发一个事件。

 

 

Select

Poll

Epoll

支持最大连接数

1024

无上限

无上限

IO效率

每次调用进行线性遍历,时间复杂度为O(N)

每次调用进行线性遍历,时间复杂度为O(N)

使用“事件”通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到rdllist里面,这样epoll_wait返回的时候我们就拿到了就绪的fd。时间发复杂度O(1)

fd拷贝

每次select都拷贝

每次poll都拷贝

调用epoll_ctl时拷贝进内核并由内核保存,之后每次epoll_wait不拷贝

python中使用select实现为并发:

简单的为并发

import socket
import select

sk = socket.socket()
sk.bind(('127.0.0.01', 9999))
sk.listen(5)

inputs = [sk, ]
while True:
    rlist, wlist, e, = select.select(inputs, [], [], 1)
    # 监听sk对象,如果sk对象发生变化,表示有客户端连接来了,此时rlist为[sk,]
    # 监听conn对象,如果conn发生变化,表示客户端有新消息发送过来了,此时rlist为【客户端】
    # 1 表示检测间隔
    print(len(inputs), len(rlist))
    # rlist中是socket对象列表
    for r in rlist:
        if r == sk:
            # 新客户端来连接
            conn, address = r.accept()
            # conn是什么? 其实socket对象
            inputs.append(conn)
            conn.sendall(bytes('hello',encoding='utf8'))
        else:
            # 有人给我发消息了
            try: # 捕捉客户端断开连接异常
                ret = r.recv(1024)
                r.sendall(ret)
                # 这里也可以给客户端发送消息,但是不推荐,这样读写混淆到一起了。推荐使用读写分离的写法
                if not ret: # 捕捉不同系统的客户端断开连接
                    raise Exception
            except Exception as e:
                inputs.remove(r)

select伪并发简单案例
select伪并发简单案例
import socket
import select

sk = socket.socket()
sk.bind(('127.0.0.01', 9999))
sk.listen(5)
inputs = [sk, ]
outputs = []  # 包含了所有给我发消息的人

while True:
    # rlist, w, e, = select.select([sk,], [], [],)
    rlist, wlist, e, = select.select(inputs, outputs, [], 1)
    # 监听sk对象,如果sk对象发生变化,表示有客户端连接来了,此时rlist为[sk,]
    # 监听conn对象,如果conn发生变化,表示客户端有新消息发送过来了,此时rlist为【客户端】
    # 1 表示检测间隔
    print(len(inputs), len(rlist), len(wlist), len(outputs))
    # rlist中是socket对象列表
    for r in rlist:
        if r == sk:
            # 新客户端来连接
            conn, address = r.accept()
            # conn是什么? 其实socket对象
            inputs.append(conn)
            conn.sendall(bytes('hello',encoding='utf8'))
        else:
            # 有人给我发消息了
            print("=========")
            try: # 捕捉客户端断开连接异常
                ret = r.recv(1024)
                if not ret: # 捕捉不同系统的客户端断开连接
                    raise Exception("断开连接")
                else:
                    outputs.append(r)
            except Exception as e:
                inputs.remove(r)

    # 循环所有给我发消息的人
    for w in wlist:
        w.sendall(bytes('response', encoding='utf8'))
        outputs.remove(w)

select读写分离式伪并发
select读写分离式伪并发
import socket
import select

sk = socket.socket()
sk.bind(('127.0.0.01', 9999))
sk.listen(5)

inputs = [sk, ]
outputs = []  # 包含了所有给我发消息的人
messages = {} # 消息队列
# messages = { sk1:[],  sk2: [], sk3: [].....}
while True:
    # rlist, w, e, = select.select([sk,], [], [],)
    rlist, wlist, e, = select.select(inputs, outputs, [], 1)
    # 监听sk对象,如果sk对象发生变化,表示有客户端连接来了,此时rlist为[sk,]
    # 监听conn对象,如果conn发生变化,表示客户端有新消息发送过来了,此时rlist为【客户端】
    # 1 表示检测间隔
    print(len(inputs), len(rlist), len(wlist), len(outputs))
    # rlist中是socket对象列表
    for r in rlist:
        if r == sk:
            # 新客户端来连接
            conn, address = r.accept()
            # conn是什么? 其实socket对象
            inputs.append(conn)
            messages[conn] = []
            conn.sendall(bytes('hello',encoding='utf8'))
        else:
            # 有人给我发消息了
            print("=========")
            try: # 捕捉客户端断开连接异常
                ret = r.recv(1024)
                if not ret: # 捕捉不同系统的客户端断开连接
                    raise Exception("断开连接")
                else:
                    outputs.append(r)
                    messages[r].append(ret)
            except Exception as e:
                inputs.remove(r)
                # 记住: 要如果断开,要清空message对应对象的key
                del messages[r]

    # 循环所有给我发消息的人
    for w in wlist:
        msg = messages[w].pop()
        msg += bytes('response', encoding='utf8')
        w.sendall(msg)
        outputs.remove(w)

select读写分离+消息队列伪并发
select读写分离+消息队列伪并发

 

posted @ 2016-07-16 11:42  unixfbi.com  阅读(237)  评论(0编辑  收藏  举报