Python 提供了两个级别访问的网络服务。:html
查看socket类的帮助以下python
import socket # 导入socket模块 >>> help(socket.socket)
重点关注初始化函数:程序员
__init__(self, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_STREAM: 1>, proto=0, fileno=None)
SOCK_STREAM
或SOCK_DGRAM
下面实现一个TCP聊天室和一个UDP聊天室segmentfault
<!--more-->服务器
获取多个链接的处理网络
开启accept线程,执行accept操做开始阻塞,有客户端链接时,再开启一个线程recv进行数据接收的处理。而后accept线程继续阻塞,等待后续客户端的链接。socket
阻塞的处理tcp
服务端处理客户端的链接时,有两处存在阻塞,分别是:ide
所以这两处都须要开启线程单独处理,不然会阻塞主线程。函数
客户端主动断开的处理
客户端主动断开时,若是不通知服务端,那么服务端上保存的客户端链接不会被清理,这是不合理的。所以客户端主动断开时,咱们在应用层约定,客户端推出前须要发送/quit
指令到服务端上,而后有服务端关闭socket。
聊天室的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()
聊天室的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服务端接收客户端的消息时,采用socket.recvfrom(1024)
这个方法以便保存客户端的地址信息,这个方法会阻塞当前线程,所以须要开启线程单独处理。
客户端主动断开的处理
UDP客户端主动关闭以后,服务端是没法检测到客户端已经关闭的。咱们能够采用如下两种方法:
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的客户端的主线程一直在等待用户输入数据而后将数据发送到服务端,同时开启了一个心跳进程和一个接受服务端广播数据的线程。
代码
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()
TODO(Flowsnow):改写聊天室程序的TcpChatServer和UdpChatServer
附一:TCP和UDP的本质区别
附二:参考资料
记得帮我点赞哦!
精心整理了计算机各个方向的从入门、进阶、实战的视频课程和电子书,按照目录合理分类,总能找到你须要的学习资料,还在等什么?快去关注下载吧!!!
念念不忘,必有回响,小伙伴们帮我点个赞吧,很是感谢。
我是职场亮哥,YY高级软件工程师、四年工做经验,拒绝咸鱼争当龙头的斜杠程序员。听我说,进步多,程序人生一把梭
若是有幸能帮到你,请帮我点个【赞】,给个关注,若是能顺带评论给个鼓励,将不胜感激。
职场亮哥文章列表:更多文章
本人全部文章、回答都与版权保护平台有合做,著做权归职场亮哥全部,未经受权,转载必究!