Linux系统中一切皆文件,socket在操作系统层面看,也是一个文件,每个建立链接的socket内核都会为其创建发送缓冲区和接收缓冲区。
socket 四元组 确定一个链接的过程
socket在内核中维护了两个队列,分别是发送缓冲区和接收缓冲区
机器A与机器B已经建立了socket链接
当机器A给机器B发送数据时
至此一个发送数据的流程就走完了。
IO多路复用机制是在一个线程中处理多个IO流,内核负责监听套接字上面的可读事件、可写事件和错误事件,触发相应事件的回调函数。
可读事件、可写事件
当socket的接收缓冲区中有数据时,在用户态的角度看就是可读的
当socket的发送缓冲区没有数据或者数据没有满时,在用户态的角度看就是可写的
socket调用listen方法会将socket变成监听套接字,监听套接字等待客户端的链接
当有客户端想要与监听套接字建立连接时,accept()函数会被触发,返回一个已连接套接字和一个远程客户端地址。
客户端发送握手报文给服务端,报文会发送到服务端内核的接收缓冲区中,对于服务端用户态代码来讲,是可读的,可以把数据从接收缓冲区拷贝到用户态中。
发送数据到发送缓冲区,发送缓冲区没有满,对于用户态来说就是可写的。
从内核的接收缓冲区获取数据,接收缓冲区有数据,就是可读事件
accept 可读事件
recv 可读事件
send 可写事件
事件的可读还是可写是站在用户态的角度说的,如果内核的接收缓冲区有数据,那么就是可读的,如果内核的发送缓冲区没有满,那么就是可写的。
服务端
import socket
# 创建一个套接字对象
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置套接字对象端口释放后直接可以使用,默认是False
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 绑定地址和端口,
server_socket.bind(('127.0.0.1', 9001))
# 开启监听
server_socket.listen(5)
print(f"server running .... at 9001")
# 阻塞等待客户端链接
client_socket, remote_addr = server_socket.accept()
print(f"欢迎{remote_addr[0]}:{remote_addr[1]}访问服务端~~~")
# 接收客户端发送过来的数据
recv_msg = client_socket.recv(8096).decode("utf-8")
print(f"从客户端{remote_addr[0]}接收数据: {recv_msg}")
# 给客户端返回数据
send_msg = recv_msg + "呵呵哈哈哈"
client_socket.send(send_msg.encode("utf-8"))
# 服务端主动断开链接
client_socket.close()
客户端
import socket
# 创建一个socket客户端
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 发起远程链接
client_socket.connect(("127.0.0.1", 9001))
# 发送数据
client_socket.send("我来了".encode('utf-8'))
# 接收数据
recv_msg = client_socket.recv(8096).decode('utf-8')
print(f"从服务端接收数据:{recv_msg}")
服务端
import socket
import threading
def handle_request(client_socket, remote_addr):
""" 开启一个线程,处理请求 """
print(f"欢迎客户端访问:{remote_addr[0]}at{remote_addr[1]}")
recv_msg = client_socket.recv(8096).decode("utf-8")
print(f"从客户端接收到信息:{recv_msg}")
send_msg = recv_msg + "哈哈哈哈哈哈!!!"
client_socket.send(send_msg.encode('utf-8'))
client_socket.close()
# 创建socket_server服务端
socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置socket属性
socket_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 绑定地址
socket_server.bind(("127.0.0.1", 9002))
# 开启监听
socket_server.listen(128)
print(f"server listening at 9002......")
while True:
client_socket, remote_addr = socket_server.accept()
threading.Thread(target=handle_request, args=(client_socket, remote_addr)).start()
客户端
import socket
# 创建一个socket客户端
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 发起远程链接
client_socket.connect(("127.0.0.1", 9002))
# 发送数据
client_socket.send("我来了".encode('utf-8'))
# 接收数据
recv_msg = client_socket.recv(8096).decode('utf-8')
print(f"从服务端接收数据:{recv_msg}")
client_socket.close()
大家都知道,多线程的切换会导致一些系统的开销,线程的切换,导致系统的开销比较大,使用单线程IO多路复用技术实现多任务处理,是一种很好的方式。
前面介绍的socket都是阻塞的socket,开启监听之后,服务端阻塞等待客户端链接,
服务端
import socket
import threading
def handle_request(client_socket, remote_addr):
""" 开启一个线程,处理请求 """
print(f"欢迎客户端访问:{remote_addr[0]}at{remote_addr[1]}")
recv_msg = client_socket.recv(8096).decode("utf-8")
print(f"从客户端接收到信息:{recv_msg}")
send_msg = recv_msg + "哈哈哈哈哈哈!!!"
client_socket.send(send_msg.encode('utf-8'))
client_socket.close()
# 创建socket_server服务端
socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置socket属性
socket_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 绑定地址
socket_server.bind(("127.0.0.1", 9002))
# 开启监听
socket_server.listen(128)
print(f"server listening at 9002......")
while True:
client_socket, remote_addr = socket_server.accept()
threading.Thread(target=handle_request, args=(client_socket, remote_addr)).start()
客户端
import socket
# 创建一个socket客户端
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 发起远程链接
client_socket.connect(("127.0.0.1", 9003))
# 发送数据
client_socket.send("我来了".encode('utf-8'))
# 接收数据
recv_msg = client_socket.recv(8096).decode('utf-8')
print(f"从服务端接收数据:{recv_msg}")
client_socket.close()
服务端
import socket
import select
from queue import Queue
from typing import Dict
# 创建server_socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定IP和port
server_socket.bind(("127.0.0.1", 9005))
# 设置重用地址
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 必须设置成非阻塞,IO多路复用必须用非阻塞套接字
server_socket.setblocking(False)
server_socket.listen(128)
# 以上创建监听套接字,负责监听是否有客户端链接
# 当事件发生时,代表客户端过来了,属于可读事件
# 所以把server_socket放到rlist里面
rlist = [server_socket]
wlist = []
xlist = rlist
# 因为可以监听多个套接字,所以rlist,wlist,xlist,都是列表
# 但在初始状态下,select只监听一个监听套接字(server_socket)即可
message_queues: Dict[socket.socket, Queue] = {}
while True:
# 开启select返回三个元素
# readable: rlist中发生可读事件的socket对象
# writeable: wlist中发生可写事件的socket对象
# exceptional: xlist中发生异常的socket
readable, writeable, exceptional = select.select(rlist, wlist, xlist)
# 遍历readable
for r in readable:
# 如果r是server_socket, 则代表监听套接字有事件发生
# 显然是客户端链接来了
if r is server_socket:
# 监听套接字是非阻塞的,那么已链接套接字默认也是非阻塞的
client_socket, remote_addr = server_socket.accept()
print(f"欢迎客户端:{remote_addr[0]}:{remote_addr[1]}到来!")
# 客户端将数据发送到接收缓冲区,接收缓冲区的数据对于用户来说是可读的
# 将客户端socket放到可读文件列表中,当客户端给服务端发消息时
# 遍历的时候会处理这个已连接的套接字
rlist.append(client_socket)
# 负责保存客户端发送过来的消息,以socket对象为键,以Queue()对象为值
message_queues[client_socket] = Queue()
else:
# 如果不是server_socket 则表示已链接套接字有事件发生
data = r.recv(1024)
if data:
# 这里的r就是活跃的已连接的socket
addr = r.getpeername()
# 数据没有接收完,继续接收
print(f"接收到{addr}发来的数据{data.decode('utf-8')}")
# message_queues保存了所有已连接的字符串
# 每一个套接字都对应一个队列,找到该活跃套接字对应的队列
message_queues[r].put(data) # 将消息放进去
# 消息放进去之后,服务器需要回复客户端消息,属于可写事件
# 还要将r放到wlist中接受select检查
if r not in wlist:
wlist.append(r)
else:
# 走到这里,说明data为假
# 客户端断开链接了,给服务器发送一个b''
addr = r.getpeername()
print(f"客户端{addr}断开链接了!")
# 我们要将套接字从rlist, wlist中移除
# 客户端都断开链接了,那么select也就不需要再监听了
rlist.remove(r)
if r in wlist:
wlist.remove(r)
# 把message_queues里面的已连接套接字移除
message_queues.pop(r)
# 关闭服务端套接字链接
r.close()
# 以上是遍历可读事件,可读事件可以发生在监听套接字上面(和客户端建立连接)
# 也可以发生在已连接套接字上面(客户端发信息了)
# 如果没有事件发生或者处理完毕,那么接下来就要遍历可写事件
# 而可写事件一定发生在已连接套接字上面(要回消息给客户端)
for w in writeable:
message_queue = message_queues[w]
# 一开始队列里面肯定是有消息的,因为我们手动往里面放了一条
# 但如果队列为空,说明服务端已经回复过了,那么要将该套接字从 wlist 里面移除
# 让它变为非活跃状态,不再满足可写
# 等到下一次客户端发消息时,再将它添加到 wlist 中,让它变得可写
if message_queue.empty():
wlist.remove(w)
continue
# 获取消息
data = message_queue.get()
# 发送给客户端
w.send("服务端收到,你发的消息是: ".encode("utf-8") + data)
for x in exceptional:
addr = x.getpeername()
print(f"和客户端 {addr[0]}:{addr[1]} 通信出现错误")
rlist.remove(x)
if x in wlist:
wlist.remove(x)
message_queues.pop(x)
x.close()