Python 提供了两个级别拜访的网络服务。:
- 低级别的网络服务反对根本的 socket,,能够拜访底层操作系统 Socket 接口的办法。
- 高级别的网络服务模块 socketserver,能够简化网络服务器的开发。
socket
查看 socket 类的帮忙如下
import socket # 导入 socket 模块
>>> help(socket.socket)
重点关注初始化函数:
__init__(self, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_STREAM: 1>, proto=0, fileno=None)
- family:网络协议簇,默认值为 AF_INET
- type:套接字的类型,依据是面向连贯的还是非连贯分为
SOCK_STREAM
或SOCK_DGRAM
- proto:套接字协定,个别默认为 0,示意
- fileno:套接字的 int 型的文件描述符
上面实现一个 TCP 聊天室和一个 UDP 聊天室
<!–more–>
TCP 聊天室
概要设计
获取多个连贯的解决
开启 accept 线程,执行 accept 操作开始阻塞,有客户端连贯时,再开启一个线程 recv 进行数据接管的解决。而后 accept 线程持续阻塞,期待后续客户端的连贯。
阻塞的解决
服务端解决客户端的连贯时,有两处存在阻塞,别离是:
- 获取连贯时,socket.accept()会阻塞
- 每一个建设胜利的连贯在获取数据时,socket.recv(1024)
因而这两处都须要开启线程独自解决,否则会阻塞主线程。
客户端被动断开的解决
客户端被动断开时,如果不告诉服务端,那么服务端上保留的客户端连贯不会被清理,这是不合理的。因而客户端被动断开时,咱们在应用层约定,客户端推出前须要发送 /quit
指令到服务端上,而后有服务端敞开 socket。
TCP 聊天室 -server
聊天室的 server 端次要是监听端口,解决来自 client 端的连贯,并且散发数据到所有的 client 端
代码
import socket
import threading
class TcpChatServer:
def __init__(self, ip='192.168.110.13', port=9001):
self.ip = ip
self.port = port
self.clients = {}
self.sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
self.event = threading.Event()
def recv(self, so, ip ,port):
while not self.event.is_set():
data = so.recv(1024).decode() # 将承受到的字节数据 bytes 转化为 utf- 8 格局的字符串
if data.strip() == '/quit': # 客户端被动断开时的解决
so.close()
self.clients.pop((ip, port))
return
for s in self.clients.values(): # 播送发送
s.send('{}:{}\n{}'.format(ip, port, data).encode())
def accept(self):
while not self.event.is_set():
so, (ip, port) = self.sock.accept()
self.clients[(ip, port)] = so
# 因为 so.recv 会产生阻塞,因而独自开一个线程解决数据的承受局部。这样 accept 能够持续承受来自其余客户端的链接
threading.Thread(target=self.recv, args=(so, ip, port), name='client-{}:{}'.format(ip, port)).start()
def start(self):
self.sock.bind((self.ip, self.port))
self.sock.listen()
t = threading.Thread(target=self.accept, daemon=True) # 为了不阻塞主线程,独自开启一个线程解决 accept(accept 会阻塞线程)try:
t.start()
t.join() # 阻塞直到获取到 KeyboardInterrupt
except KeyboardInterrupt:
self.stop()
def stop(self):
for s in self.clients.values():
s.close()
self.sock.close()
self.event.set() # 进行所有的循环
if __name__ == '__main__':
tcp_chat_server = TcpChatServer()
tcp_chat_server.start()
TCP 聊天室 -client
聊天室的 client 端次要是发动连贯,连贯到 server 端,并且要承受来自服务端播送散发的音讯。
代码
import socket
import threading
class TcpChatClient:
def __init__(self):
self.sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
self.event = threading.Event()
def recv(self): # 客户端须要始终接管服务端播送散发的音讯
while not self.event.is_set():
data = self.sock.recv(1024).decode()
data = data.strip()
print(data)
def send(self): # 输出音讯就发送
while not self.event.is_set():
data = input()
self.sock.send(data.encode())
if data.strip() == '/quit': # 发送 /quit 的时候本身敞开
self.stop()
def start(self, ip, port):
self.sock.connect((ip, port))
s = threading.Thread(target=self.send, daemon=False)
r = threading.Thread(target=self.recv, daemon=False)
s.start()
r.start()
def stop(self):
self.sock.close()
self.event.set()
if __name__ == '__main__':
tcp_chat_client = TcpChatClient()
tcp_chat_client.start('192.168.110.13', 9001)
UDP 聊天室
概要设计
阻塞的解决
在 UDP 服务端接管客户端的音讯时,采纳 socket.recvfrom(1024)
这个办法以便保留客户端的地址信息,这个办法会阻塞以后线程,因而须要开启线程独自解决。
客户端被动断开的解决
UDP 客户端被动敞开之后,服务端是无奈检测到客户端曾经敞开的。咱们能够采纳以下两种办法:
- 如果相似于 TCP 采纳约定退出指令的办法,那么客户端发送退出指令后就调用 close 办法,而后服务端依据失去的指令剔除客户端字典中对应的客户端。
- 还能够通过客户端定时发送心跳给服务端,服务端通过心跳来判断客户端过程是否存活。
UDP 聊天室 -server
UDP 服务端程序开启线程期待接管客户端的数据,而后播送给其余的客户端,并且查看所有连贯的心跳是否超时。
代码
import socket
import datetime
import threading
class UdpChatServer:
def __init__(self, ip='192.168.110.13', port=9001):
self.addr = (ip, port)
self.sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
self.clients = {}
self.event = threading.Event()
def recv(self):
while not self.event.is_set():
data, addr = self.sock.recvfrom(1024)
data = data.decode().strip()
now = datetime.datetime.now()
if data == '#ping#': # 判断是否收到心跳
self.clients[addr] = now # 收到心跳则保留客户端地址,并且更新工夫戳
continue
disconnected = set() # 没收到一次数据就判断所有的生效链接
for addr, timestamp in self.clients.items():
if (now - timestamp).total_seconds() > 10: # 生效条件:2 次(即 10s)没收到心跳就判断客户端敞开
disconnected.add(addr)
else:
self.sock.sendto('{}:{}\n{}'.format(addr[0], addr[1], data).encode(), addr)
for addr in disconnected:
self.clients.pop(addr)
def start(self):
self.sock.bind(self.addr) # 绑定端口之后就开启线程始终承受客户端的数据
t = threading.Thread(target=self.recv(), daemon=True)
try:
t.start()
t.join()
except KeyboardInterrupt:
self.stop()
def stop(self):
self.event.set()
self.sock.close()
if __name__ == '__main__':
udp_chat_server = UdpChatServer()
udp_chat_server.start()
UDP 聊天室 -client
UDP 的客户端的主线程始终在期待用户输出数据而后将数据发送到服务端,同时开启了一个心跳过程和一个承受服务端播送数据的线程。
代码
import socket
import threading
import time
class UdpChatClient:
def __init__(self, ip, port):
self.addr = (ip, port)
self.sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
self.event = threading.Event()
def heartbeat(self): # 心跳线程函数:每 5s 发一次心跳
while not self.event.wait(5):
self.sock.sendto(b'#ping#', self.addr)
def recv(self): # 始终期待承受 udp 服务器播送的数据
while not self.event.is_set():
data = self.sock.recv(1024)
print(data.decode())
def start(self):
threading.Thread(target=self.heartbeat, name='heartbeat', daemon=True).start()
threading.Thread(target=self.recv, name='recv', daemon=True).start()
print('请在 5s 后发言')
time.sleep(5) # 因为服务端必须收到一个心跳之后才会保留次客户端,因而须要期待 5s
print('请开始发言')
while not self.event.is_set():
data = input('')
data = data.strip()
if data == '/quit':
self.event.set()
self.sock.close()
return
self.sock.sendto(data.encode(), self.addr)
if __name__ == '__main__':
udp_chat_client = UdpChatClient('192.168.110.13', 9001)
udp_chat_client.start()
SocketServer
TODO(Flowsnow):改写聊天室程序的 TcpChatServer 和 UdpChatServer
附一:TCP 和 UDP 的本质区别
- udp:所有的客户端发来的数据报都沉积在队列上,而后服务端一个一个的解决
- tcp:每一个客户端和服务端都有一个连贯通道,只解决对应客户端的数据流
附二:参考资料
- socketserver — A framework for network servers
记得帮我点赞哦!
精心整顿了计算机各个方向的从入门、进阶、实战的视频课程和电子书,依照目录正当分类,总能找到你须要的学习材料,还在等什么?快去关注下载吧!!!
朝思暮想,必有回响,小伙伴们帮我点个赞吧,非常感谢。
我是职场亮哥,YY 高级软件工程师、四年工作教训,回绝咸鱼争当龙头的斜杠程序员。
听我说,提高多,程序人生一把梭
如果有幸能帮到你,请帮我点个【赞】,给个关注,如果能顺带评论给个激励,将不胜感激。
职场亮哥文章列表:更多文章
自己所有文章、答复都与版权保护平台有单干,著作权归职场亮哥所有,未经受权,转载必究!