一、Socket介绍
- Socket套接字,python中提供socket.py标准库,非常底层的接口库
- Socket是一种通用的网络编程接口,和网络层次没有一一对应的关系
1、Socket类型
- SOCK_STREAM :面向连接的流套接字,默认值,TCP协议
- SOCK_DGRAM : 五连接的数据报文套接字,UDP协议
二、TCP编程
- Socket编程,需要两端,一般来说需要一个服务端,一个客户端,服务端称为Sever,客户端称为lient
1、TCP服务端
服务器端编程步骤:
- 创建Socket对象
- 绑定IP地址Address和端口Port,bind()方法,IPv4地址为一个二元组('ip地址字符串',Port)
- 开始监听,将在指定的IP的端口上监听,listen()方法
- 获取用于传送数据的Socket对象:socket.accept() ->(socket object, address info)
- accept方法阻塞等待客户端建立连接,返回一个新的Socket对象和客户端地址的二元组
- 地址是远程客户端的地址,IPv4中它是一个二元组(clientaddr,port)
s = socket.socket() #创建socket对象 s.bind(('127.0.0.1',8888)) # 一个二元组 s.listen() #开始监听 #开启一个连接 s1, info = s.accept() #阻塞直到客户端成功建立连接,返回一个socket对象和客户端地址 #使用缓冲区获取数据 data = s1.recv(1024) print(data, info) s1.send(b'magedu.com ack')
2、 写一个群聊程序
聊天工具是CS程序,C是每一个客户端,S是服务器端,服务器端应该具有的功能:
- 启动服务,包括绑定地址和端口,监听
- 建立连接,能和多个客户端建立连接,接收不同用户的信息
- 分发,将接收的某个用户的信息转发到已连接的所有客户端
- 停止服务,记录连接的客户端,服务器端应该对应一个类
举例 import logging import socket import threading import datetime FORAMT = '%(asctime)s %(thread)d %(message)s' logging.basicConfig(level=logging.INFO, format=FORAMT) class ChatSever: def __init__(self, ip='127.0.0.1', port=8888): # 启动服务 self.sock = socket.socket() self.addr = (ip, port) self.clients = {} #客户端 self.event = threading.Event() def start(self): #启动监听 self.sock.bind(self.addr) self.sock.listen() # 监听 #accept会阻塞主线程,所以要开一个新线程 print('------------') threading.Thread(target=self.accept).start() def accept(self): # 多人连接 sock, client = self.sock.accept() # 阻塞 print(111,sock, client) self.clients[client] = sock # 添加到客户端字典 #准备接收数据,recv是阻塞的,开启新的线程 threading.Thread(target=self.recv, args=(sock, client)).start() def recv(self, sock:socket, client): # 接收客户端数据 while not self.event.is_set(): data = sock.recv(1024) #阻塞到数据到来 msg = "{:%Y/%m/%d %H:%M:%S} {} : {}\n{}\n".format(datetime.datetime.now(), *client, data.decode()) print('msg') logging.info(msg) for s in self.clients.values(): s.send(msg.encode()) def stop(self): #停止服务 for s in self.clients.values(): s.close() self.sock.close() self.event.set() cs = ChatSever() cs.start() e = threading.Event() while not e.wait(1): cmd = input('>>>').strip() if cmd == "quit": cs.stop() e.wait(3) break 客户端主动断开带来的问题 服务端知道自己何时断开,如果客户端断开,服务器不知道 所以,好的做法是,客户端开发出特殊消息通知服务器端断开连接 但是,如果客户端主动断开,服务端主动发送一个空消息,超时返回异常,捕获异常并清理连接 即使为客户端提供了断开命令,也不能保证客户端会使用它断开连接,但是还是要增加这个退出功能
为下面代码增加功能 增加客户端退出命令 增加多客户端支持 import logging import socket import threading import datetime FORAMT = '%(asctime)s %(thread)d %(message)s' logging.basicConfig(level=logging.INFO, format=FORAMT) class ChatSever: def __init__(self, ip='127.0.0.1', port=8888): # 启动服务 self.sock = socket.socket() self.addr = (ip, port) self.clients = {} #客户端 self.event = threading.Event() def start(self): #启动监听 self.sock.bind(self.addr) self.sock.listen() # 监听 #accept会阻塞主线程,所以要开一个新线程 print('------------') threading.Thread(target=self.accept).start() def accept(self): # 多人连接 sock, client = self.sock.accept() # 阻塞 print(111,sock, client) self.clients[client] = sock # 添加到客户端字典 #准备接收数据,recv是阻塞的,开启新的线程 threading.Thread(target=self.recv, args=(sock, client)).start() def recv(self, sock:socket, client): # 接收客户端数据 while not self.event.is_set(): data = sock.recv(1024) #阻塞到数据到来 msg = data.decode().strip() #客户端退出命令 if msg == "quit": self.clients.pop(client) sock.close() logging.info("{} quits".format(client)) break msg = "{:%Y/%m/%d %H:%M:%S} {} : {}\n{}\n".format(datetime.datetime.now(), *client, data.decode()) print('msg') logging.info(msg) for s in self.clients.values(): s.send(msg.encode()) def stop(self): #停止服务 for s in self.clients.values(): s.close() self.sock.close() self.event.set() cs = ChatSever() cs.start() e = threading.Event() while not e.wait(1): cmd = input('>>>').strip() if cmd == "quit": cs.stop() e.wait(3) break
3、其他方法
- socket.recv() : 获取数据,默认是阻塞的方式
- socket.recvfrom() : 获取数据,返回一个二元组
- socket.recv_info() : 获取到nbytes的数据后,存储到buffer中
创建一个与该套接字相关连的文件对象 使用makefile改写群聊类 import logging import socket import threading import datetime FORAMT = '%(asctime)s %(thread)d %(message)s' logging.basicConfig(level=logging.INFO, format=FORAMT) class ChatSever: def __init__(self, ip='127.0.0.1', port=8888): # 启动服务 self.sock = socket.socket() self.addr = (ip, port) self.clients = {} #客户端 self.event = threading.Event() def start(self): #启动监听 self.sock.bind(self.addr) self.sock.listen() # 监听 #accept会阻塞主线程,所以要开一个新线程 print('------------') threading.Thread(target=self.accept).start() def accept(self): # 多人连接 while not self.event.is_set(): sock, client = self.sock.accept() # 阻塞 print(111,sock, client) f = sock.makefile(mode='rw') self.clients[client] = f # 添加到客户端字典 #准备接收数据,recv是阻塞的,开启新的线程 threading.Thread(target=self.recv, args=(f, client), name='recv').start() def recv(self, f, client): # 接收客户端数据 while not self.event.is_set(): try: data = f.readline() # 阻塞到换行符 except Exception as e: logging.error(e) #有任何异常保证退出 data = 'quit' msg = data.strip() #客户端退出命令 if msg == "quit": self.clients.pop(client) f.close() logging.info("{} quits".format(client)) break msg = "{:%Y/%m/%d %H:%M:%S} {} : {}\n{}\n".format(datetime.datetime.now(), *client, data) print('msg') logging.info(msg) for s in self.clients.values(): s.wtitelines(msg) s.flush() def stop(self): #停止服务 for s in self.clients.values(): s.close() self.sock.close() self.event.wait(3) self.event.set() def show_thread(e:threading.Event): while not e.wait(3): logging.info(threading.enumerate()) def main(): e = threading.Event() cs = ChatSever() cs.start() threading.Thread(target=show_thread, args=(e,), name='showthread').start() while not e.wait(1): cmd = input('>>>').strip() if cmd == "quit": cs.stop() e.wait(3) break if __name__ == "__main__": main() Socket太底层了,实际开发中很少使用这么底层的接口
三、TCP客户端
- 客户端编程步骤
- 创建Socket对象
- 连接到远程服务端的ip和port,connect()
- 传输数据:使用send,recv方法,接收数据
- 关闭连接,释放资源
import logging import socket import threading import datetime FORAMT = '%(asctime)s %(thread)d %(message)s' logging.basicConfig(level=logging.INFO, format=FORAMT) class ChatClient: def __init__(self, ip='127.0.0.1', port=8888): self.sock = socket.socket() self.addr = (ip, port) self.event = threading.Event() self.start() def start(self): #启动对远端的连接 self.sock.connect(self.addr) #准备接收数据,recv是阻塞的,开启新的线程 threading.Thread(target=self._recv, name="recv").start() def _recv(self): #接收客户端数据 while not self.event.is_set(): try: data = self.sock.recv(1024) #阻塞 except Exception as e: logging.error(e) break msg = "{:%Y/%m/%d %H:%M:%S} {} : {}\n{}\n".format(datetime.datetime.now(), *client, data) logging.info(msg) def send(self, msg:str): data = "{}\n".format(msg.strip()).encode() self.sock.send(data) def stop(self): logging.info("{} broken".format(self.addr)) self.sock.close() self.event.wait(3) self.event.set() logging.info("client stops") def show_thread(e:threading.Event): while not e.wait(3): logging.info(threading.enumerate()) def main(): e = threading.Event() cc = ChatClient() while True: msg = input('>>>') if msg.strip() == "quit": cs.stop() break cc.send(msg) if __name__ == '__main__': main()
四、Socketserver
- socket编程过于底层,编程虽然有套路,但是想要写出健壮的代码还是比较困难的,所以很多语言都对socket底层API进行封装,Python的封装就是socketserver模块
- 网络服务编程框架,便于企业级快速开发
1、类的继承关系
- SocketSever简化了网络服务器的编写
- 它有4个同步类:TCPServer,UDPServer,UnixStreamServer,UnixDatagramServer
- 2个Mixin类ForkingMixln和ThreadMixln类来支持异步
2、编程接口
- socketserver.BaseServer(sever_address, RequestHandleClass)
- 需要提供服务器绑定的地址信息,和用于处理请求的RequestHandlerClass类,RequestHandleClass类必须是BaseRequestHandler类的子类
3、BaseRequestHandler类
- 和用户连接的用户请求处理类,Server实例接收用户请求后,最后会实例化这个类;它被初始化时,送入3个构造参数:request,client_address,server
- 以后可以在BaseRequestHandler类的实例上通过
- self.request是客户端的连接的socket对象
- self.server是TCPserver本身
- self.client_address是客户端地址
- 它会一次调用3个函数,子类可以覆盖
import threading import socketserver class MyHandler(socketserver.BaseRequestHandler): def handle(self): print(self.server, self.request, self.client_address) print('{} handler'.format(self.__class__)) print(self.__class__) print(type(self).__dict__) print(self.__class__.__bases__[0].__dict__) print(threading.enumerate(), threading.current_thread()) addr = ('127.0.0.1', 8888) server = socketserver.ThreadingTCPServer(addr, MyHandler) server.serve_forever() 测试结果说明,handler方法和socket的accept对应,用户连接请求过来后,建立连接并生成一个 socket对象保存在self.request中,客户端地址保存在self.client_address中,
4、创建服务器需要几个步骤
- 必须通过BaseRequestHandler类进行子类化并覆盖其他handle()方法来创建请求处理程序类,此方法将处理传入请求
- 必须实例化一个服务器类,将它传递给服务器的地址和请求处理程序类
- 然后调用服务器对象的handle_request()或server_forever()方法
- 调用server_close()关闭套接字,shutdown()方法,等待停止server_forever()
5、实现EchoServer
- 顾名思义,Echo来什么消息回显什么消息,客户端发来什么信息,返回什么信息
import threading import socketserver class EchoHandler(socketserver.BaseRequestHandler): def finish(self): super().finish() self.event.set() # 清理工作 def setup(self): super().setup() self.event = threading.Event() # 初始化工作 def handle(self): super().handle() while not self.event.is_set(): data = self.request.recv(1024).decode() msg = "{} {}".format(self.client_address, data).encode() self.request.send(msg) print('End') addr = ('127.0.0.1', 8888) server = socketserver.ThreadingTCPServer(addr, EchoHandler) server_thread = threading.Thread(target=server.serve_forever, daemon=True) server_thread.start() try: while True: cmd = input('>>>') if cmd.strip() == 'quit': break except Exception as e: print(e) except KeyboardInterrupt: print('Exit') finally: server.shutdown() server.server_close()