UDP
创建Socket
创建一个udp socket
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
发送数据
from socket import *
udp_socket = socket(AF_INET, SOCK_DGRAM)
udp_socket.sendto("你好".encode(),("172.16.1.19",8080))
- 在python3中发送的数据必须是字节
- 在每一次发送的UDP数据包 使用的端口都不一样
绑定信息
一般情况下,在一台电脑上运行的网络程序有很多,而各自用的端口号很多情况下不知道,为了不与其他的网络程序占用同一个端口号,往往在编程中,udp的端口一般不绑定
但是如果需要做成一个服务器端的程序的话(想要接收数据),是需要绑定的
from socket import *
# 1. 创建socket
udpSocket = socket(AF_INET, SOCK_DGRAM)
# 2. 绑定本地的相关信息,如果一个网络程序不绑定,则系统会随机分配
bindAddr = ('', 7788) # ip地址和端口号,ip一般不写,表示本机的任何一个ip(当多网卡的时候,无论是哪个网卡的数据都能接收到)
udpSocket.bind(bindAddr)
# 3. 等待接收对方发送的数据
recvData = udpSocket.recvfrom(1024) # 1024表示本次接收的最大字节数
# 4. 显示接收到的数据
print(recvData)
# 5. 关闭socket
udpSocket.close()
- 单工 : 只能接收/发送数据
- 半双工 : 可以接收/发送数据,但是接收的时候不能发送,发送的时候不能接收
- 全双工 : 可以同时接收/发送数据
socket是全双工的
UDP应用: TFTP客户端
TFTP协议介绍
TFTP(Trivial File Transfer Protocol,简单文件传输协议),是TCP/IP协议族中的一个用来在客户端与服务器之间进行简单文件传输的协议
特点:
- 简单
- 占用资源小
- 适合传递小文件
- 适合在局域网进行传递
- 端口号为69
- 基于UDP实现
TFTP下载过程
TFTP服务器默认监听69号端口,当客户端发送“下载”请求(即读请求)时,需要向服务器的69端口发送,服务器若批准此请求,则使用一个新的、临时的 端口进行数据传输
当服务器找到需要现在的文件后,会立刻打开文件,把文件中的数据通过TFTP协议发送给客户端
如果文件的总大小较大(比如3M),那么服务器分多次发送,每次会从文件中读取512个字节的数据发送过来
因为发送的次数有可能会很多,所以为了让客户端对接收到的数据进行排序,所以在服务器发送那512个字节数据的时候,会多发2个字节的数据,用来存放序号,并且放在512个字节数据的前面,序号是从1开始的
因为需要从服务器上下载文件时,文件可能不存在,那么此时服务器就会发送一个错误的信息过来,为了区分服务发送的是文件内容还是错误的提示信息,所以又用了2个字节 来表示这个数据包的功能(称为操作码),并且在序号的前面
操作码 | 功能 |
---|---|
1 | 读请求,即 下载 |
2 | 写请求,即 上传 |
3 | 表示数据包,即 DATA |
4 | 确认码,即 ACK |
5 | 错误 |
因为udp的数据包不安全,即发送方发送是否成功不能确定,所以TFTP协议中规定,为了让服务器知道客户端已经接收到了刚刚发送的那个数据包,所以当客户端接收到一个数据包的时候需要向服务器进行发送确认信息,即发送收到了,这样的包成为ACK(应答包)
为了标记数据已经发送完毕,所以规定,当客户端接收到的数据小于516(2字节操作码+2个字节的序号+512字节数据)时,就意味着服务器发送完毕了,如果数据恰好是512个字节,那么还会多发送一个数据大小为0字节的数据包
TFTP数据包的格式如下:
struct模块
由于我们需要将数据组装成数据包,这就需要用到struct模块,该模块可以将数据按照指定格式进行组装/解析
常用方法
struct.pack(fmt,v1,v2....)
# 返回byte类型,按照fmt数据格式组合而成
struct.unpack(fmt,bytes)
# 按照给定数据格式解开(通常都是由struct.pack进行打包)数据,返回值是一个tuple
格式符
struct中最重要的就是格式符,首先是符号
Character | Byte order |
---|---|
@ | native |
= | native |
< | little-endian |
> | big-endian |
! | network(=big-endian) |
大端对齐/小端对齐
字节顺序,又称端序或尾序(英语:Endianness)。在计算机科学领域中,是跨越多字节的程序对象的存储规则
在几乎所有的机器上,多字节对象都被存储为连续的字节序列。例如在C语言中,一个类型为int的变量x
地址为0x100,那么其对应地址表达式&x
的值为0x100。且x
的四个字节将被存储在存储器的0x100, 0x101, 0x102, 0x103位置.
而存储地址内的排列则有两个通用规则。一个多位的整数将按照其存储地址的最低或最高字节排列。如果最低有效位在最高有效位的前面,则称小端序;反之则称大端序。在网络应用中,字节序是一个必须被考虑的因素,因为不同机器类型可能采用不同标准的字节序,所以均按照网络标准转化。
大端序
大端序(big-endian)或称大尾序,以8bit位单位:
示例中 最高位字节是0x0A 存储在最低的内存地址处。下一个字节0x0B存在后面的地址处。正类似于十六进制字节从左到右的阅读顺序
小端序
小端序(little-endian)或称小尾序,以8bit位为单位:
最低位字节是0x0D 存储在最低的内存地址处。后面字节依次存在后面的地址处。
在网络中,统一规定使用大端序来进行传输数据
格式码
格式码中使用字母来对应指定的数据类型
Format | C Type | Python Type |
---|---|---|
x | pad byte | no value |
c | char | string of length 1 |
b | signed char | integer |
B | unsigned char | integer |
? | _Bool | bool |
h | short | integer |
H | unsigned short | integer |
i | int | integer |
I | unsigned int | integer |
l | long | integer |
L | unsigned long | integer |
q | long long | integer |
Q | unsigned long long | integer |
f | float | float |
d | double | float |
s | char[] | string |
p(小写) | char[] | string |
P(大写) | void * | integer |
示例代码
'''
数据格式为
姓名 年龄 性别 职业
lily 18 female teacher
'''
import os
import struct
fp = open('test.bin','wb')
# 按照上面的格式将数据写入文件中
# 这里如果string类型的话,在pack函数中就需要encode('utf-8')
name = b'lily'
age = 18
sex = b'female'
job = b'teacher'
# int类型占4个字节
fp.write(struct.pack('4si6s7s', name,age,sex,job))
fp.flush()
fp.close()
# 将文件中写入的数据按照格式读取出来
fd = open('test.bin','rb')
# 21 = 4 + 4 + 6 + 7
print(struct.unpack('4si6s7s',fd.read(21)))
fd.close()
运行结果:
TFTP示例代码
#coding=utf-8
from socket import *
import struct
import sys
if len(sys.argv) != 2:
print('-'*30)
print("tips:")
print("python xxxx.py 192.168.1.1")
print('-'*30)
exit()
else:
ip = sys.argv[1]
# 创建udp套接字
udpSocket = socket(AF_INET, SOCK_DGRAM)
#构造下载请求数据
cmd_buf = struct.pack("!H8sb5sb",1,"test.jpg",0,"octet",0)
#发送下载文件请求数据到指定服务器
sendAddr = (ip, 69)
udpSocket.sendto(cmd_buf, sendAddr)
p_num = 0
recvFile = ''
while True:
recvData,recvAddr = udpSocket.recvfrom(1024)
recvDataLen = len(recvData)
# print recvAddr # for test
# print len(recvData) # for test
cmdTuple = struct.unpack("!HH", recvData[:4])
# print cmdTuple # for test
cmd = cmdTuple[0]
currentPackNum = cmdTuple[1]
if cmd == 3: #是否为数据包
# 如果是第一次接收到数据,那么就创建文件
if currentPackNum == 1:
recvFile = open("test.jpg", "a")
# 包编号是否和上次相等
if p_num+1 == currentPackNum:
recvFile.write(recvData[4:]);
p_num +=1
print '(%d)次接收到的数据'%(p_num)
ackBuf = struct.pack("!HH",4,p_num)
udpSocket.sendto(ackBuf, recvAddr)
# 如果收到的数据小于516则认为完成
if recvDataLen<516:
recvFile.close()
print '已经成功下载!!!'
break
elif cmd == 5: #是否为错误应答
print "error num:%d"%currentPackNum
break
udpSocket.close()
udp广播
示例代码:
#coding=utf-8
import socket, sys
dest = ('<broadcast>', 7788)
# 创建udp套接字
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 对这个需要发送广播数据的套接字进行修改设置,否则不能发送广播数据
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST,1)
# 以广播的形式发送数据到本网络的所有电脑中
s.sendto(b"Hi", dest)
print ("等待对方回复(按ctrl+c退出)")
while True:
(buf, address) = s.recvfrom(2048)
print("Received from %s: %s" % (address, buf.decode()))
TCP
TCP服务器
如果想要完成一个tcp服务器的功能,需要的流程如下:
- socket创建一个套接字
- bind绑定ip和port
- listen使套接字变为可以被动链接
- accept等待客户端的链接
- recv/send接受发送数据
代码示例:
#coding=utf-8
from socket import *
# 创建socket
tcpSerSocket = socket(AF_INET, SOCK_STREAM)
# 绑定本地信息
address = ('', 7788)
tcpSerSocket.bind(address)
# 使用socket创建的套接字默认的属性是主动的,使用listen将其变为被动的,这样就可以接收别人的链接了
tcpSerSocket.listen(5)
# 如果有新的客户端来链接服务器,那么就产生一个新的套接字专门为这个客户端服务器
# newSocket用来为这个客户端服务
# tcpSerSocket就可以省下来专门等待其他新客户端的链接
newSocket, clientAddr = tcpSerSocket.accept()
# 接收对方发送过来的数据,最大接收1024个字节
recvData = newSocket.recv(1024)
print '接收到的数据为:',recvData
# 发送一些数据到客户端
newSocket.send("thank you !")
# 关闭为这个客户端服务的套接字,只要关闭了,就意味着为不能再为这个客户端服务了,如果还需要服务,只能再次重新连接
newSocket.close()
# 关闭监听套接字,只要这个套接字关闭了,就意味着整个程序不能再接收任何新的客户端的连接
tcpSerSocket.close()
- listen: 参数的意义是连接的总个数,再Linux中这个值没有意义,是由操作系统决定的
- 如果当前已建立链接数和半链接数以达到设定值,那么新客户端就不会connect成功,而是等待服务器
TCP客户端
示例代码:
#coding=utf-8
from socket import *
# 创建socket
tcpClientSocket = socket(AF_INET, SOCK_STREAM)
# 链接服务器
serAddr = ('192.168.1.102', 7788)
tcpClientSocket.connect(serAddr)
# 提示用户输入数据
sendData = input("请输入要发送的数据:")
# tcp客户端已经链接好了服务器,所以在以后的数据发送中,不需要填写对方的ip和端口
tcpClientSocket.send(sendData)
# 接收对方发送过来的数据,最大接收1024个字节
recvData = tcpClientSocket.recv(1024)
print ('接收到的数据为:',recvData.encode())
# 关闭套接字
tcpClientSocket.close()
并发服务器
单进程服务器
1. 简单的TCP服务器
from socket import *
serSocket = socket(AF_INET, SOCK_STREAM)
# 重复使用绑定的信息
serSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR , 1)
localAddr = ('', 7788)
serSocket.bind(localAddr)
serSocket.listen(5)
while True:
print('-----主进程,,等待新客户端的到来------')
newSocket,destAddr = serSocket.accept()
print('-----主进程,,接下来负责数据处理[%s]-----'%str(destAddr))
try:
while True:
recvData = newSocket.recv(1024)
if len(recvData)>0:
print('recv[%s]:%s'%(str(destAddr), recvData))
else:
print('[%s]客户端已经关闭'%str(destAddr))
break
finally:
newSocket.close()
serSocket.close()
2. 总结
- 同一时刻只能为一个客户进行服务,不能同时为多个客户服务
- 类似于找一个“明星”签字一样,客户需要耐心等待才可以获取到服务
- 当服务器为一个客户端服务时,而另外的客户端发起了connect,只要服务器listen的队列有空闲的位置,就会为这个新客户端进行连接,并且客户端可以发送数据,但当服务器为这个新客户端服务时,可能一次性把所有数据接收完毕
- 当recv接收数据时,返回值为空,即没有返回数据,那么意味着客户端已经调用了close关闭了;因此服务器通过判断recv接收数据是否为空 来判断客户端是否已经下线
多进程服务
1. 多进程服务器
from socket import *
from multiprocessing import *
from time import sleep
# 处理客户端的请求并为其服务
def dealWithClient(newSocket,destAddr):
while True:
recvData = newSocket.recv(1024)
if len(recvData)>0:
print('recv[%s]:%s'%(str(destAddr), recvData))
else:
print('[%s]客户端已经关闭'%str(destAddr))
break
newSocket.close()
def main():
serSocket = socket(AF_INET, SOCK_STREAM)
serSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR , 1)
localAddr = ('', 7788)
serSocket.bind(localAddr)
serSocket.listen(5)
try:
while True:
print('-----主进程,,等待新客户端的到来------')
newSocket,destAddr = serSocket.accept()
print('-----主进程,,接下来创建一个新的进程负责数据处理[%s]-----'%str(destAddr))
client = Process(target=dealWithClient, args=(newSocket,destAddr))
client.start()
#因为已经向子进程中copy了一份(引用),并且父进程中这个套接字也没有用处了
#所以关闭
newSocket.close()
finally:
#当为所有的客户端服务完之后再进行关闭,表示不再接收新的客户端的链接
serSocket.close()
if __name__ == '__main__':
main()
2. 说明
- 通过为每个客户端创建一个进程的方式,能够同时为多个客户端进行服务
- 当客户端不是特别多的时候,这种方式还行,如果有几百上千个,就不可取了,因为每次创建进程等过程需要好较大的资源
多线程服务器
#coding=utf-8
from socket import *
from threading import Thread
from time import sleep
# 处理客户端的请求并执行事情
def dealWithClient(newSocket,destAddr):
while True:
recvData = newSocket.recv(1024)
if len(recvData)>0:
print('recv[%s]:%s'%(str(destAddr), recvData))
else:
print('[%s]客户端已经关闭'%str(destAddr))
break
newSocket.close()
def main():
serSocket = socket(AF_INET, SOCK_STREAM)
serSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR , 1)
localAddr = ('', 7788)
serSocket.bind(localAddr)
serSocket.listen(5)
try:
while True:
print('-----主进程,,等待新客户端的到来------')
newSocket,destAddr = serSocket.accept()
print('-----主进程,,接下来创建一个新的进程负责数据处理[%s]-----'%str(destAddr))
client = Thread(target=dealWithClient, args=(newSocket,destAddr))
client.start()
#因为线程中共享这个套接字,如果关闭了会导致这个套接字不可用,
#但是此时在线程中这个套接字可能还在收数据,因此不能关闭
#newSocket.close()
finally:
serSocket.close()
if __name__ == '__main__':
main()
单进程服务器-非阻塞模式
服务器
#coding=utf-8
from socket import *
import time
# 用来存储所有的新链接的socket
g_socketList = []
def main():
serSocket = socket(AF_INET, SOCK_STREAM)
serSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR , 1)
localAddr = ('', 7788)
serSocket.bind(localAddr)
#可以适当修改listen中的值来看看不同的现象
serSocket.listen(1000)
#将套接字设置为非堵塞
#设置为非堵塞后,如果accept时,恰巧没有客户端connect,那么accept会
#产生一个异常,所以需要try来进行处理
serSocket.setblocking(False)
while True:
#用来测试
#time.sleep(0.5)
try:
newClientInfo = serSocket.accept()
except Exception as result:
pass
else:
print("一个新的客户端到来:%s"%str(newClientInfo))
newClientInfo[0].setblocking(False)
g_socketList.append(newClientInfo)
# 用来存储需要删除的客户端信息
needDelClientInfoList = []
for clientSocket,clientAddr in g_socketList:
try:
recvData = clientSocket.recv(1024)
if len(recvData)>0:
print('recv[%s]:%s'%(str(clientAddr), recvData))
else:
print('[%s]客户端已经关闭'%str(clientAddr))
clientSocket.close()
g_needDelClientInfoList.append((clientSocket,clientAddr))
except Exception as result:
pass
for needDelClientInfo in needDelClientInfoList:
g_socketList.remove(needDelClientInfo)
if __name__ == '__main__':
main()
客户端
#coding=utf-8
from socket import *
import random
import time
serverIp = raw_input("请输入服务器的ip:")
connNum = raw_input("请输入要链接服务器的次数(例如1000):")
g_socketList = []
for i in range(int(connNum)):
s = socket(AF_INET, SOCK_STREAM)
s.connect((serverIp, 7788))
g_socketList.append(s)
print(i)
while True:
for s in g_socketList:
s.send(str(random.randint(0,100)))
# 用来测试用
#time.sleep(1)
单进程服务器-select版
1. select原理
在多路复用的模型中,比较常用的有select模型和epoll模型。这两个都是系统接口,由操作系统提供。当然,Python的select模块进行了更高级的封装。
网络通信被Unix系统抽象为文件的读写,通常是一个设备,由设备驱动程序提供,驱动可以知道自身的数据是否可用。支持阻塞操作的设备驱动通常会实现一组自身的等待队列,如读/写等待队列用于支持上层(用户层)所需的block或non-block操作。设备的文件的资源如果可用(可读或者可写)则会通知进程,反之则会让进程睡眠,等到数据到来可用的时候,再唤醒进程。
这些设备的文件描述符被放在一个数组中,然后select调用的时候遍历这个数组,如果对于的文件描述符可读则会返回改文件描述符。当遍历结束之后,如果仍然没有一个可用设备文件描述符,select让用户进程则会睡眠,直到等待资源可用的时候在唤醒,遍历之前那个监视的数组。每次遍历都是依次进行判断的。
2. select回显服务器
import select
import socket
import sys
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('', 7788))
server.listen(5)
# sys.stdin在windows平台上是不能用在select的
inputs = [server, sys.stdin]
running = True
while True:
# 调用 select 函数,阻塞等待
# select 接受3个参数
# 1. 监测这个列表中的套接字是否可以收数据
# 2. 监测这个列表中的套接字是否可以发数据
# 3. 监测这个列表中的套接字是否产生了一场
readable, writeable, exceptional = select.select(inputs, [], [])
# 数据抵达,循环
for sock in readable:
# 监听到有新的连接
if sock == server:
conn, addr = server.accept()
# select 监听的socket
inputs.append(conn)
# 监听到键盘有输入
elif sock == sys.stdin:
cmd = sys.stdin.readline()
running = False
break
# 有数据到达
else:
# 读取客户端连接发送的数据
data = sock.recv(1024)
if data:
sock.send(data)
else:
# 移除select监听的socket
inputs.remove(sock)
sock.close()
# 如果检测到用户输入敲击键盘,那么就退出
if not running:
break
server.close()
3. 包含writeList的服务器
#coding=utf-8
import socket
import queue
from select import select
SERVER_IP = ('', 9999)
# 保存客户端发送过来的消息,将消息放入队列中
message_queue = {}
input_list = []
output_list = []
if __name__ == "__main__":
server = socket.socket()
server.bind(SERVER_IP)
server.listen(10)
# 设置为非阻塞
server.setblocking(False)
# 初始化将服务端加入监听列表
input_list.append(server)
while True:
# 开始 select 监听,对input_list中的服务端server进行监听
stdinput, stdoutput, stderr = select(input_list, output_list, input_list)
# 循环判断是否有客户端连接进来,当有客户端连接进来时select将触发
for obj in stdinput:
# 判断当前触发的是不是服务端对象, 当触发的对象是服务端对象时,说明有新客户端连接进来了
if obj == server:
# 接收客户端的连接, 获取客户端对象和客户端地址信息
conn, addr = server.accept()
print("Client %s connected! "%str(addr))
# 将客户端对象也加入到监听的列表中, 当客户端发送消息时 select 将触发
input_list.append(conn)
# 为连接的客户端单独创建一个消息队列,用来保存客户端发送的消息
message_queue[conn] = queue.Queue()
else:
# 由于客户端连接进来时服务端接收客户端连接请求,将客户端加入到了监听列表中(input_list),客户端发送消息将触发
# 所以判断是否是客户端对象触发
try:
recv_data = obj.recv(1024)
# 客户端未断开
if recv_data:
print("received %s from client %s"%(recv_data, str(addr)))
# 将收到的消息放入到各客户端的消息队列中
message_queue[obj].put(recv_data)
# 将回复操作放到output列表中,让select监听
if obj not in output_list:
output_list.append(obj)
except ConnectionResetError:
# 客户端断开连接了,将客户端的监听从input列表中移除
input_list.remove(obj)
# 移除客户端对象的消息队列
del message_queue[obj]
print("\n[input] Client %s disconnected"%str(addr))
# 如果现在没有客户端请求,也没有客户端发送消息时,开始对发送消息列表进行处理,是否需要发送消息
for sendobj in output_list:
try:
# 如果消息队列中有消息,从消息队列中获取要发送的消息
if not message_queue[sendobj].empty():
# 从该客户端对象的消息队列中获取要发送的消息
send_data = message_queue[sendobj].get()
sendobj.send(send_data)
else:
# 将监听移除等待下一次客户端发送消息
output_list.remove(sendobj)
except ConnectionResetError:
# 客户端连接断开了
del message_queue[sendobj]
output_list.remove(sendobj)
print("\n[output] Client %s disconnected"%str(addr))
4. 总结
优点
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的优点
- 没有最大并发连接的限制,能打开的FD(指的是文件描述符,通俗的理解就是是套接字对应的数字编号)的上限远大于1024
- 效率提升,不是轮询的方式,不会随着FD数目的增加,效率下降,只有活跃可用的FD才会调用callback函数;即epoll最大的优点就在于它只管你”活跃”的链接,而跟连接总数无关,因此在实际的网络环境中,epoll的效率就会源源高于select和poll(与select没有区别,解决了select有上限的问题)
2. epoll使用参考代码
import socket
import select
# 创建套接字
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# 设置可以重复使用绑定的信息
s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
# 绑定本机信息
s.bind(("",7788))
# 变为被动
s.listen(10)
# 创建一个epoll对象
epoll=select.epoll()
# 测试,用来打印套接字对应的文件描述符
# print s.fileno()
# print select.EPOLLIN|select.EPOLLET
# 注册事件到epoll中
# epoll.register(fd[, eventmask])
# 注意,如果fd已经注册过,则会发生异常
# 将创建的套接字添加到epoll的事件监听中
# EPOLLIN 注册接收数据事件
epoll.register(s.fileno(),select.EPOLLIN|select.EPOLLET)
connections = {}
addresses = {}
# 等待客户端的到来或者对方发送数据
while True:
# epoll 进行 fd 扫描的地方 -- 未指定超时时间则为阻塞等待
epoll_list=epoll.poll()
# 对事件进行判断
for fd,events in epoll_list:
# print fd
# print events
# 如果是socket创建的套接字被激活
if fd == s.fileno():
conn,addr=s.accept()
print('有新的客户端到来%s'%str(addr))
# 将 conn 和 addr 信息分别保存起来
connections[conn.fileno()] = conn
addresses[conn.fileno()] = addr
# 向 epoll 中注册 连接 socket 的 可读 事件
epoll.register(conn.fileno(), select.EPOLLIN | select.EPOLLET)
elif events == select.EPOLLIN:
# 从激活 fd 上接收
# 根据描述符找到套接字
recvData = connections[fd].recv(1024)
if len(recvData)>0:
print('recv:%s'%recvData)
else:
# 从 epoll 中移除该 连接 fd
epoll.unregister(fd)
# server 侧主动关闭该 连接 fd
connections[fd].close()
print("%s---offline---"%str(addresses[fd]))
3. 说明
- EPOLLIN : 可读
- EPOLLOUT : 可写
- EPOLLET : ET模式
epoll对文件描述符的操作有两种模式: LT(level trigger) 和 ET (edge trigger),LT模式是默认模式,LT模式与ET模式的区别:
- LT模式: 当epoll监测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件,下次调用epoll时,会再次响应应用程序并通知此事件
- ET模式: 当epoll检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件,如果不处理,下次调用epoll时,不会再次响应应用程序并通知此事件,ET要比LT效率高
协程
协程,又称微线程,纤程。英文名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密集型的程序下没啥好处。*
协程的一个简单实现
python2:
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)
python3:
import time
def A():
while True:
print("----A---")
yield
time.sleep(0.5)
def B(c):
while True:
print("----B---")
next(c)
time.sleep(0.5)
if __name__=='__main__':
a = A()
B(a)
运行结果
greenlet版本
为了更好使用协程来完成多任务,python中的greenlet模块对其封装,从而使得切换任务变的更加简单
安装
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()
运行结果:
协程-gevent
greenlet已经实现了协程,但是这个还的人工切换,是不是觉得太麻烦了,不要捉急,python还有一个比greenlet更强大的并且能够自动切换任务的模块gevent
其原理是当一个greenlet遇到IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。
由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO
单进程服务器-gevent版
import sys
import time
import gevent
# 要想使用gevent来管理协程,所有的耗时操作必须使用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)