1. socket 版本
此程序案列仅供参考学习,学习其中的思维
1.1 无法退出
在Linux系统中运行案列代码时存在如下问题:
- 当服务器端quit时,此时self.socket关闭,但是已经进入self.accept,而且
accept处于阻塞状态
,若不进行捕获则错误退出,若进行捕获则处于阻塞状态无法进行,因此导致start线程无法退出
- 综合考虑start线程无法退出,
主线程都已经退出,因此就不等待start线程是否完成任务,因此start线程采用deamon=True
。 - 当服务器异常终端时,客户端会处于不停的循环状态,因此需要提供
心态机制
1.2 服务器端案列程序
服务器端程序
import socket
import threading
import time
import sys
import logging
from datetime import datetime
FORMAT="%(asctime)s %(threadName)s %(thread)d %(message)s"
logging.basicConfig(format=FORMAT,level=logging.INFO)
class Chatserver:
def __init__(self):
self.sock = socket.socket()
self.event = threading.Event()
self.clients = {}
def start(self):
self.sock.bind(('0.0.0.0',8880))
self.sock.listen()
# threading.Thread(target=self.accept,name="start",deamon=True).start()
#若设置未deamon线程时,主线程quit时,start线程自然退出
threading.Thread(target=self.accept, name="start").start()
def accept(self):
print('0000good')
while not self.event.is_set():
print('111goodgoodgood')
try:
s,radd = self.sock.accept()
print('222goodgoodgood')
threading.Thread(target=self.reve,args=(s,),name="accept").start()
print('333goodgoodgood')
except OSError:
print('sssssssssssssssssssssssss')
break
def reve(self,s):
self.clients[s] = datetime.now()
print('444goodgoodgood')
while not self.event.is_set():
try:
data = s.recv(1024)
print('555goodgoodgood')
if data == b'quit':
self.clients.pop(s)
print('666goodgoodgood')
s.close()
print('7777goodgoodgood')
logging.info("{} {} -___- quit".format(s,data))
print('888goodgoodgood')
break
logging.info('{}'.format(data))
for sock in self.clients.keys():
if sock == s:
continue
sock.send(data)
print('999goodgoodgood')
except Exception:
return
def stop(self):
for so in self.clients.keys():
so.close()
self.event.set()
self.sock.close()
chat = Chatserver()
chat.start()
while True:
cmd = input(">>>")
if cmd.strip() == "quit":
chat.stop()
threading.Event().wait(3)
break
print(threading.enumerate())
1.3 客户端案列程序
import threading
import socket
import logging
import datetime
FORMAT="%(asctime)s %(threadName)s %(thread)d %(message)s"
logging.basicConfig(format=FORMAT,level=logging.INFO)
class Client:
def __init__(self, ipport=('172.16.102.100',8880)):
self.ipport = ipport
self.socket = socket.socket()
self.event = threading.Event()
def start(self):
"""开启对客户端"""
self.connect = self.socket.connect(self.ipport)
self.send("I'am ready")
threading.Thread(target=self.recv).start()
def recv(self):
while not self.event.is_set():
try:
data = self.socket.recv(1024)
except Exception as e:
logging.info(e)
break
msg = "{}{}{}".format(datetime.datetime.now(),self.ipport,data.strip())
logging.info(msg)
def send(self,mesg):
data = "{}".format(mesg).encode()
self.socket.send(data)
def stop(self):
self.socket.close()
self.event.set()
logging.info('client stops')
def main():
cc = Client()
cc.start()
while True:
cmd = input('>>>')
if cmd.strip() == 'quit':
cc.stop()
break
cc.send(cmd)
if __name__ == '__main__':
main()
2. socketserver 版本
在此版本中,客户端主动断开时,发现服务器端抛出异常,经过日志发现当客户端主动断开时会向服务器端发送b''空byte,因此服务器端除了quit而且还需添加b''
.
另外要注意如下:
- 在socketsever版本中,
serve_forever
重新启用也daemon线程,方便主程序退出时,serve_forever也会自动关闭 - socketerver版本中一个类实例处理一个请求,处理请求顺序未setup,handle,finish。
- 若使用ThreadingTCPServer,一个线程处理一个类实例,一个类实例处理一个请求,再加上GIL大锁,因此在多并发中会严重影响效率。
2.1 程序案列
import socketserver
import threading
import datetime
import logging
import sys
FORMAT="%(asctime)s %(threadName)s %(thread)d %(message)s"
logging.basicConfig(format=FORMAT,level=logging.INFO)
class Chatserver(socketserver.BaseRequestHandler):
client = {}
clientpop = set()
def setup(self):
super().setup()
print('OK')
self.event = threading.Event()
self.client[self.request] = datetime.datetime.now()
def handle(self):
"""doc"""
while not self.event.is_set():
data = self.request.recv(1024)
if data == b'quit':
self.client.pop(self.request)
break
for sock in self.client.keys():
if sock == self.request:
continue
sock.send(data)
def finish(self):
super().finish()
self.event.set()
server = socketserver.ThreadingTCPServer(('0.0.0.0',9998),Chatserver)
threading.Thread(target=server.serve_forever,daemon=True).start()
try:
while True:
cmd = input('>>>')
if cmd.strip() == 'quit':
break
except Exception as e:
logging.info(e)
except KeyboardInterrupt:
pass
finally:
print('exit')
3. select 模型版本
在学习之前首先看一下select模型图:
3.1 select模型基础知识
在学习select模型时,首先要学习python标准库中得案列,学习标准库中得最主要得如下两个方法。
***********在调用select方法的前提时:必须要至少注册一个后才能调用select方法***************
3.1.1 register注册方法
在注册中通过源码可知:
def register(self, fileobj, events, data=None):
key = super().register(fileobj, events, data)
if events & EVENT_READ:
self._readers.add(key.fd)
if events & EVENT_WRITE:
self._writers.add(key.fd)
return key
在注册是时需要传入参数:文件对象
,触发回调data指标
,data
,意思就是当某个文件对象达到要监控指标后,就触发data,data可以是一个数据,也可以是一个函数,通常情况下。
为了在回调函数和fileobj管理起来,通常回调函数要处理触发回调函数得fileobj,那么必须要将fileobj传递给回调函数,否则无法明确指出处理那个fileobj
。
3.1.2 select代理监控方法
一旦某个fileobj注册到select模型中,select通过循环一遍又一遍查看每个fileobj,看那个fileobj达到监控指标,若监控的fileobj数量非常庞大时,select也会出现效率问题,但是基于epoll等模型时就会避免这个方法,因为会有水平触发和垂直触发,select方法返回值又是什么呢?注册了什么就返回什么事件,注册的时候fileobj,监控指标mask和data,因此我们要做的就是处理返回值,处理返回值的方法通常是:提取出fileobj和data,将fileobj送给data函数去处理
另外:select返回一个列表,列表里包含注册时fielobj和mask,但是fileobj包含了fileobj,fd,mask和打他。
具体案例如下:
import selectors
import socket
sel = selectors.DefaultSelector()
def accept(sock, mask):
conn, addr = sock.accept() # Should be ready
print('accepted', conn, 'from', addr)
conn.setblocking(False)
sel.register(conn, selectors.EVENT_READ, read)
def read(conn, mask):
data = conn.recv(1000) # Should be ready
if data:
print('echoing', repr(data), 'to', conn)
conn.send(data) # Hope it won't block
else:
print('closing', conn)
sel.unregister(conn)
conn.close()
sock = socket.socket()
sock.bind(('localhost', 1234))
sock.listen(100)
sock.setblocking(False)
sel.register(sock, selectors.EVENT_READ, accept)
while True:
events = sel.select()
print(events)
for key, mask in events:
print(key)
callback = key.data
callback(key.fileobj, mask)
触发一个read事件:
得结果如下:
#print(events)的结果:列表包含SelectorKey 和mask
[(SelectorKey(fileobj=<socket.socket fd=272, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 1234)>,
fd=272, events=1, data=<function accept at 0x0000019533F23C80>), 1)]
#提取selectorKey又是一个命名元组,访问其元素可以像访问类方法一样
SelectorKey(fileobj=<socket.socket fd=272, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 1234)>,
fd=272, events=1,data=<function accept at 0x0000019533F23C80>)
3.1.3 get_map方法
在循环select时,通常是一遍又一遍的循环select中的监控对象,那么这些监控对象肯定要存储在某一个容器中,换句话说就是一旦用户将某个fileobj对象注册到selectors时,就要将fileobj存储在某个容器之中,这个容器就是get_map方法,要注意这个字典的key:value是fd:slectorkey组成的,因此所有的都在此记录
将上述代码进行修改:
def select():
while True:
events = sel.select()
# print(sel.get_map())
# print(events)
for key, mask in events:
# print(key)
callback = key.data
callback(key.fileobj, mask)
threading.Thread(target=select).start()
while True:
for fd,key in sel.get_map().items():
print(fd,key)
time.sleep(10)
通过观察fd和key的情况如下:
#验证可知确实是存储在select的对象上的get_map之中
320 SelectorKey(fileobj=<socket.socket fd=320, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 1234)>,
fd=320, events=1, data=<function accept at 0x0000011B7A3838C8>)
3.2 select版本聊天室
3.2.1 select 仅监听读的操作
存在极其大的隐患:当一个用户收到信息,然后循环出其他sock,利用其他sock.send(data),但是如果其中一个sock正常忙着发送其他数据,如正在忙着发送一个电影,因此正个遍历sock的循环一下子就处于阻塞状态。
当用户异常中断时,怎么处理,
import selectors
import socket
import threading
import logging
import time
FORMAT="%(asctime)s %(threadName)s %(thread)d %(message)s"
logging.basicConfig(format=FORMAT,level=logging.INFO)
# sel = selectors.DefaultSelector()
class Chatserver:
def __init__(self,addr=('0.0.0.0',1234)):
self.addr = addr
self.sock = socket.socket()
self.sel = selectors.DefaultSelector()
# self.clients = {} 因为fileobj对象在self.sel.get_map的字典中,因此无须此操作
def start(self):
self.sock.bind(self.addr)
self.sock.listen()
self.sock.setblocking(False)
self.sel.register(self.sock,selectors.EVENT_READ,self.accept)
# threading.Thread(target=self.select,name="select").start()
threading.Thread(target=self.select, name="select", daemon=True).start()
def select(self):
while True:
events = chat.sel.select() # 阻塞状态,直到某一个IO准备好,立马拿到一个events
# print(events)
# print(type(events))
for key, mask in events:
# print(key)
callback = key.data
callback(key.fileobj)
def accept(self,sock):
conn,raddr = sock.accept()
# self.clients[raddr] = conn
logging.info('this is accept:{}'.format(conn))
conn.setblocking(False)
self.sel.register(conn,selectors.EVENT_READ,self.recv)
def recv(self,conn):
data = conn.recv(1024)
if data:
if data == b'quit':
self.sel.unregister(conn)
conn.close()
logging.info(data)
for key in self.sel.get_map().values():
# print(key) #因为get_map中又accept和recv的监控IO,因此需要排除accept的fileobj
if key.data == self.recv:
key.fileobj.send(data)
else :
self.sel.unregister(conn)
def stop(self):
fileobjlist = []
for fd,key in self.sel.get_map().items():
# self.sel.unregister(key.fileobj) #因为是遍历字典的同时不能对字典进行增删改
fileobjlist.append(key.fileobj)
for fileobj in fileobjlist:
self.sel.unregister(fileobj)
fileobj.close()
self.sel.close()
chat = Chatserver()
chat.start()
logging.info('Chatserver running...')
while True:
print('zhunbie')
cmd = input('>>>')
for fd,key in chat.sel.get_map().items():
print('OK')
print('{}::::::{}'.format(fd,key))
# threading.Event.wait()
if cmd.strip() == 'quit':
chat.stop()
logging.info('Chatserver sotped...')
break
3.2.1 select 监听读写
就是socket同时满足收和发时如何处理,案例如下:
会发现发一直处于可就绪发的状态,因此会处于不停的循环阶段
import selectors
import socket
import threading
import logging
import time
import queue
FORMAT="%(asctime)s %(threadName)s %(thread)d %(message)s"
logging.basicConfig(format=FORMAT,level=logging.INFO)
# sel = selectors.DefaultSelector()
class Chatserver:
def __init__(self,addr=('0.0.0.0',1234)):
self.addr = addr
self.sock = socket.socket()
self.sel = selectors.DefaultSelector()
# self.clients = {} 因为fileobj对象在self.sel.get_map的字典中,因此无须此操作
self.clients = {} #用来保存每个fileobj对象的Queue
def start(self):
self.sock.bind(self.addr)
self.sock.listen()
self.sock.setblocking(False)
self.sel.register(self.sock,selectors.EVENT_READ,self.accept)
# threading.Thread(target=self.select,name="select").start()
threading.Thread(target=self.select, name="select", daemon=True).start()
def select(self):
while True:
events = chat.sel.select() # 阻塞状态,直到某一个IO准备好,立马拿到一个events
# print(events)
# print(type(events))
for key, mask in events:
# print(key)
callback = key.data
callback(key.fileobj,mask)
def accept(self,sock,mask):
conn,raddr = sock.accept()
# self.clients[raddr] = conn
logging.info('this is accept:{}'.format(conn))
conn.setblocking(False)
self.clients[raddr] = queue.Queue()
self.sel.register(conn,selectors.EVENT_READ,self.handle)
def handle(self,conn,mask):
if mask @ selectors.EVENT_READ:
data = conn.recv(1024)
if data:
if data == b'quit':
self.sel.unregister(conn)
conn.close()
logging.info(data)
# for key in self.sel.get_map().values():
# # print(key) #因为get_map中又accept和recv的监控IO,因此需要排除accept的fileobj
# if key.data == self.recv:
# # key.fileobj.send(data)
# self.clients[key.fileobj.getpeername()].put()#现在就发到对应的队列之中了
for c in self.clients.values():
c.put(data) #同样就将数据放到对应的Q中
else :
self.sel.unregister(conn)
if mask @ selectors.EVENT_WRITE:
raddr = conn.getpeername()
q = self.clients[raddr]
if not q.empty: #只有q不空时才去取东西
conn.send(q.get())
def stop(self):
fileobjlist = []
for fd,key in self.sel.get_map().items():
# self.sel.unregister(key.fileobj) #因为是遍历字典的同时不能对字典进行增删改
fileobjlist.append(key.fileobj)
for fileobj in fileobjlist:
self.sel.unregister(fileobj)
fileobj.close()
self.sel.close()
chat = Chatserver()
chat.start()
logging.info('Chatserver running...')
while True:
print('zhunbie')
cmd = input('>>>')
for fd,key in chat.sel.get_map().items():
print('OK')
print('{}::::::{}'.format(fd,key))
# threading.Event.wait()
if cmd.strip() == 'quit':
chat.stop()
logging.info('Chatserver sotped...')
break