python网络编程一
Socket 套接字
Socket
套接字。Socket
是一种通用的网络编程接口,和网络层次没有一一对应的关系。
Python中标准库中提供了socket
模块。socket
模块中也提供了socket
类,实现了对底层接口的封装,socket
模块是非常底层的接口库;
socket
类定义为
socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)
协议族
AF表示Address Family, 用于socket()
第一个参数;
名称 | 含义 |
---|---|
AF_INET |
IPV4 |
AF_INET6 |
IPV6 |
AF_UNIX |
Unix Domain Socket, Windows(Windows 下不支持) |
Socket类型
名称 | 含义 |
---|---|
SOCK_STREAM |
面向连接的流套接字。 |
SOCK_DGRAM |
默认值,UDP协议无连接的数据报文套接字。 |
TCP协议是流协议,也就是一大段数据看做字节流,一段段持续发送这些字节。
UDP协议是数据报协议,每一份数据封在一个单独的数据报中,一份一份发送数据。
socket 常用方法
socket
类创建出socket对象,这个对象常用的方法如下
名称 | 含义 |
---|---|
socket.recv(bufsize[, flags]) |
获取数据。默认是阻塞的方式。 |
socket.recvfrom(bufsize[, flags]) |
获取数据,返回一个二元组(bytes, address)。 |
socket.recv_into(buffer[, nbytes[, flags]]) |
获取到nbytes的数据后,存储到buffer中。如果nbytes没有指定或为0,将buffer大小的数据存入buffer中。返回接收的字节数。 |
socket.recvfrom_into(buffer[, nbytes[, flags]]) |
获取数据,返回一个二元组(bytes, address)到buffer中。 |
socket.send(bytes[, flags]) |
TCP发送数据,发送成功返回发送字节数。 |
socket.sendall(bytes[, flags]) |
TCP发送全部数据,成功返回None。 |
socket.sendto(string[, flags], address) |
UDP发送数据。 |
socket.sendfile(file, offset=0, count=None) |
发送一个文件直到EOF,使用高性能的os.sendfile机制,返回发送的字节数。如果在Windows下不支持sendfile,或者不是普通文件,使用send()发送文件。offset告诉起始位置。从Python 3.5版本开始。 |
名称 | 含义 |
---|---|
socket.getpeername() |
返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。 |
socket.getsockname() |
返回套接字自己的地址。通常是一个元组(ipaddr,port)。 |
socket.setblocking(flag) |
如果flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)。非阻塞模式下,如果调用recv() 没有发现任何数据,或send() 调用无法立即发送数据,那么将引起 socket.error 异常。 |
socket.settimeout(value) |
设置套接字操作的超时期,timeout 是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如 connect() )。 |
socket.setsockopt(level,optname,value) |
设置套接字选项的值。比如缓冲区大小。太多了,去看文档。不同系统,不同版本都不尽相同。 |
TCP
C/S编程
Socket 编程是一种完成一端和另一端通信的编程方式,通常这两端分别处在不同的进程中,实现网络通信。在 Socket 编程中,每个 socket
对象表示了通信的一端。
从业务角度来看,通信的两端分别具有不同的角色:
- 客户端(Client):主动发送请求的一端,负责向服务端发起通信请求。
- 服务端(Server):被动接受请求并回应的一端,负责监听客户端的连接并处理请求。
这种编程模式通常被称为 C/S 编程(Client/Server Programming),在网络应用中被广泛应用。
服务器端编程步骤
创建 Socket 对象:
- 创建用于网络通信的 Socket 对象。
绑定 IP 地址和端口:
- 使用
bind()
方法将 IP 地址和端口绑定到 Socket 对象。 - IPv4 地址表示为一个二元组,包含 IP 地址字符串和端口号。
- 使用
开始监听:
- 使用
listen()
方法在指定的 IP 地址和端口上开始监听连接请求。
- 使用
接受客户端连接:
- 使用
accept()
方法阻塞等待客户端建立连接。 accept()
方法返回一个新的 Socket 对象和客户端地址的二元组。- 客户端地址通常表示为远程客户端的 IP 地址和端口号。
- 使用
接收数据:
- 使用新建立的 Socket 对象的
recv(bufsize, flags)
方法接收数据。 - 可以指定缓冲区大小来接收数据。
- 使用新建立的 Socket 对象的
发送数据:
- 使用新建立的 Socket 对象的
send(bytes)
方法发送数据。
- 使用新建立的 Socket 对象的
import socket import sys server = socket.socket() addr = '127.0.0.1', 9999 server.bind(addr) # 没有端口 server.listen() # netstat ss -l # 队列长度 print(server,file=sys.stderr) newsock,raddr = server.accept() # 阻塞方法 print(newsock.getpeername()) print(newsock.getsockname()) newsock.send(b'hello') data = newsock.recv(1024) # 也是一个新的阻塞方法 print(type(data),data) msg = "data message is {}".format(data) newsock.send(msg.encode()) s1, radd1 = server.accept() # 允许一个连接 data = s1.recv(1024) print(data, '~~~~~~~~~~~~~~~') server.close() # 可以使用telnet 连接本地的9999端口进行测试
- socket 示例,实现多人群聊的后端服务
# 实现一个群聊工具server端 import socket import threading import logging import time FORMAT = '%(asctime)s %(threadName)s %(thread)d %(message)s' logging.basicConfig(level=logging.INFO, format=FORMAT) # 暂时实现了一个echo server class ChatServer: def __init__(self,ip='127.0.0.1',port=9999): self.addr = ip, port # 服务器的地址 self.sock = socket.socket() # 创建一个socket对象 self.event = threading.Event() # 事件对象 self.clients = {} # 用于存储客户端的连接 self.lock = threading.Lock() # 锁对象 def start(self): self.sock.bind(self.addr) # 绑定地址 self.sock.listen() # 监听 threading.Thread(target=self.accept, name='accept').start() # 启动一个线程,用于接收客户端的连接 def accept(self): # 接收客户端的连接 count = 1 # 用于记录客户端的数量 while not self.event.is_set(): # 事件对象没有被设置 try: newsock, raddr = self.sock.accept() # 接收客户端的连接 except ConnectionAbortedError: # 客户端连接被中断 logging.error('client is closed') continue self.clients[raddr] = newsock # 存储客户端的连接 logging.info(newsock) # 打印客户端的连接 threading.Thread(target=self.recv, name=f'recv-{count}',args=(newsock, raddr)).start() # 启动一个线程,用于接收客户端的消息 count += 1 # 客户端的数量加1 def recv(self,sock,raddr): while not self.event.is_set(): # 事件对象没有被设置 try: data = sock.recv(1024) # 接收客户端的消息 except Exception as e: logging.error(e) data = b'' if data.strip().rstrip() == b'' or data.strip().rstrip() == b'quit': # 客户端发送了空消息或者quit with self.lock: # 锁住 self.clients.pop(raddr) # 删除客户端的连接 logging.info("{} bye.".format(raddr)) # 打印客户端的地址 sock.close() # 关闭客户端的连接 break msg = "from {}: {}. data = {}".format(*raddr, data) # 消息 logging.info(msg) # 打印消息 with self.lock: # 锁住 for s in self.clients.values(): # 遍历客户端的连接 s.send(msg.encode()) # 发送消息 def stop(self): self.event.set() # 设置事件对象,停止接收新连接 with self.lock: # 锁住,以确保线程的安全 for s in self.clients.values(): # 遍历客户端的连接 s.close() # 关闭连接 self.clients.clear() # 清空客户端连接字典 self.sock.close() # 关闭服务器的连接 if __name__ == '__main__': cs = ChatServer() # 创建ChatServer对象 cs.start() # 启动服务 while True: cmd = input('>>').strip() if cmd == 'quit': cs.stop() threading.Event().wait(3) break logging.info(threading.enumerate()) # 打印线程的信息 logging.info(cs.clients) # 打印客户端的连接 # 使用telnet 可以连接到服务器进行测试
MakeFile
# 实现一个群聊工具server端 import socket import threading import logging import time FORMAT = '%(asctime)s %(threadName)s %(thread)d %(message)s' logging.basicConfig(level=logging.INFO, format=FORMAT) # 暂时实现了一个echo server class ChatServer: def __init__(self,ip='127.0.0.1',port=9999): self.addr = ip, port # 服务器的地址 self.sock = socket.socket() # 创建一个socket对象 self.event = threading.Event() # 事件对象 self.clients = {} # 用于存储客户端的连接 self.lock = threading.Lock() # 锁对象 def start(self): self.sock.bind(self.addr) # 绑定地址 self.sock.listen() # 监听 threading.Thread(target=self.accept, name='accept').start() # 启动一个线程,用于接收客户端的连接 def accept(self): # 接收客户端的连接 count = 1 # 用于记录客户端的数量 while not self.event.is_set(): # 事件对象没有被设置 try: newsock, raddr = self.sock.accept() # 接收客户端的连接 f = newsock.makefile('rw') # 读写都使用文本 except ConnectionAbortedError: # 客户端连接被中断 logging.error('client is closed') continue with self.lock: self.clients[raddr] = f,newsock # 存储客户端的连接 logging.info(newsock) # 打印客户端的连接 threading.Thread(target=self.recv, name=f'recv-{count}',args=(f, raddr)).start() # 启动一个线程,用于接收客户端的消息 count += 1 # 客户端的数量加1 def recv(self,f,raddr): while not self.event.is_set(): # 事件对象没有被设置 try: #data = sock.recv(1024) # 接收客户端的消息 data = f.readline().strip() # 读取一行 \n 带有换行符 except Exception as e: logging.error(e) data = '' if data == '' or data == 'quit': # 客户端发送了空消息或者quit with self.lock: # 锁住 _, sock = self.clients.pop(raddr) # 弹出客户端的连接 sock.close() f.close() break msg = "from {}: {}. data = {}".format(*raddr, data) # 消息 logging.info(msg) # 打印消息 with self.lock: # 锁住 for ff, _ in self.clients.values(): # 遍历客户端的连接 # s.send(msg.encode()) # 发送消息 ff.write(msg) ff.flush() def stop(self): self.event.set() # 设置事件对象,停止接收新连接 with self.lock: # 锁住,以确保线程的安全 for s in self.clients.values(): # 遍历客户端的连接 s.close() # 关闭连接 self.clients.clear() # 清空客户端连接字典 self.sock.close() # 关闭服务器的连接 if __name__ == '__main__': cs = ChatServer() # 创建ChatServer对象 cs.start() # 启动服务 while True: cmd = input('>>').strip() if cmd == 'quit': cs.stop() threading.Event().wait(3) break logging.info(threading.enumerate()) # 打印线程的信息 logging.info(cs.clients) # 打印客户端的连接 # 使用telnet 可以连接到服务器进行测试
客户端的实现
import socket import threading import logging import termcolor FORMAT = '%(asctime)s %(threadName)s %(thread)d %(message)s' logging.basicConfig(level=logging.INFO, format=FORMAT) # 实现一个群聊工具客户端 class ChatClient: def __init__(self, rip='127.0.0.1', rport=9999): # 初始化, 默认连接服务器的地址 self.raddr = rip, rport # 服务器的地址 self.sock = socket.socket() # 创建一个socket对象 self.event = threading.Event() # 事件对象 def start(self): try: self.sock.connect(self.raddr) # 连接服务器 self.sock.send(b'hello\r\n') # 发送消息 logging.info(self.sock) # 打印连接 threading.Thread(target=self.recv, name='recv').start()# 启动一个线程,用于接收服务器的消息 except Exception as e: logging.error(e) # 打印错误 def recv(self): while not self.event.is_set(): try: data = self.sock.recv(1024) # 接收服务器的消息 msg = 'data={}'.format(data) # 消息 self.sock.send(data) # 发送消息 logging.info(termcolor.colored(msg, 'blue')) #打印消息 except Exception as e: logging.error(e) # 打印错误 def send(self, msg:str): data = "{}\n".format(msg).encode() # 发送的消息 print(data) # 打印消息 self.sock.send(data) def stop(self): self.event.set() self.sock.close() def inter(client:ChatClient): while not client.event.is_set(): # 事件对象没有被设置 cmd = input(termcolor.colored('>>>', 'green')).strip() # 输入命令 if cmd == 'quit': client.stop() # 停止客户端 break client.send(cmd) # 发送退出消息q def main(): cs = ChatClient() try: cs.start() threading.Thread(target=inter, name='inter', args=(cs,)).start() except Exception as e: logging.error(e) if __name__ == '__main__': main() # 可以python 在多个控制台启动测试
SocketServer
socket
编程过于底层,编程虽然有套路,但是想要写出健壮的代码还是比较困难的,所以很多语言都对socket
底层API进行封装,Python的封装就是socketserver
模块。它是网络服务编程框架,便于企业级快速开发;
类的继承关系
+------------+ | BaseServer | +------------+ | V +------------+ +-------------------+ | TCPServer | ---------> | UnixStreamServer | +------------+ +-------------------+ | | V V +------------+ +---------------------+ | UDPServer | ---------> | unixDatagramServer | +------------+ +---------------------+
SocketServer简化了网络服务器的编写
SocketServer 提供了简化网络服务器编写的工具,包括以下同步类和 mixin 类:
同步类:
- TCPServer:TCP 协议的服务器类。
- UDPServer:UDP 协议的服务器类。
- UnixStreamServer:基于 Unix 套接字的流式服务器类。
- UnixDatagramServer:基于 Unix 套接字的数据报服务器类。
Mixin 类:
- ForkingMixIn:用于支持基于多进程的并发。
- ThreadingMixIn:用于支持基于多线程的并发。
混合类:
- 通过组合同步类和 mixin 类,可以得到不同类型的服务器:
- ForkingUDPServer(ForkingMixIn, UDPServer)
- ForkingTCPServer(ForkingMixIn, TCPServer)
- ThreadingUDPServer(ThreadingMixIn, UDPServer)
- ThreadingTCPServer(ThreadingMixIn, TCPServer)
说明:
ForkingMixIn
用于创建多进程服务器,而ThreadingMixIn
用于创建多线程服务器。- 需要注意,fork 操作需要操作系统的支持,而 Windows 平台不支持 fork.
import socketserver class MyHandler(socketserver.BaseRequestHandler): def handle(self): print("=" * 30) print(self.request) # 与客户端通信的socket对象 print(self.client_address) print(id(self.server), self.server) print("=" * 30) server = socketserver.TCPServer(('127.0.0.1',9999),MyHandler) print(id(server)) server.handle_request() # 4298556464 # ============================== # <socket.socket fd=4, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 9999), raddr=('127.0.0.1', 56343)> # ('127.0.0.1', 56343) # 4298556464 <socketserver.TCPServer object at 0x10036c430> # ==============================
BaseRequestHandler类
BaseRequestHandler
类是用于处理用户连接和请求的基类,定义如下:
BaseRequesthandler (request, client_address, server)
当服务端 Server
实例接收到用户请求时,会实例化这个类。它被初始化时,会传入三个构造参数:request
、client_address
、server
。
在 BaseRequestHandler
类的实例上,可以使用以下属性:
self.request
:与客户端的连接的 socket 对象。self.server
:TCPServer 实例本身。self.client_address
:客户端地址。
这个类在初始化时,会依次调用三个方法。子类可以覆盖这些方法。
创建服务器的步骤总结
创建服务器的过程通常包括以下几个步骤:
创建请求处理程序类:
- 从
BaseRequestHandler
类派生出子类,并重写其handle()
方法,用于处理传入的请求。
- 从
实例化服务器类:
- 实例化一个服务器类,通常是
TCPServer
、UDPServer
或其衍生类。 - 传入服务器的地址和请求处理程序类作为参数。
- 实例化一个服务器类,通常是
启动服务器:
- 调用服务器实例的
handle_request()
方法,或者使用serve_forever()
方法启动服务器并持续监听请求。 - 服务器会开始接受并处理传入的请求。
- 调用服务器实例的
关闭服务器:
- 在适当的时候,调用
server_close()
方法关闭服务器的套接字,释放相关资源。
- 在适当的时候,调用
通过以上步骤,可以创建一个简单的服务器,用于处理客户端的请求。
总结
在使用 socketserver 模块创建服务器时,通常会遵循以下步骤和原则:
为每个连接提供 RequestHandlerClass 实例:
- 为每个连接创建一个请求处理程序类的实例,该类通常是从
BaseRequestHandler
类派生而来的。 - 该实例会依次调用
setup()
、handle()
、finish()
方法,以处理连接的请求。 - 使用
try..finally
结构确保finish()
方法一定被调用,即使出现异常情况。
- 为每个连接创建一个请求处理程序类的实例,该类通常是从
循环处理连接:
- 如果想要保持与客户端的通信,需要在
handle()
方法中使用循环,持续处理来自客户端的请求。
- 如果想要保持与客户端的通信,需要在
简化编程接口:
- socketserver 模块提供了多种类,如
TCPServer
、UDPServer
以及多进程、多线程的类等。 - 尽管类别不同,但编程接口是一致的,这大大简化了编程的复杂度。
- 程序员只需专注于实现请求处理程序类(Handler 类),而不必担心底层的网络通信细节。
- socketserver 模块提供了多种类,如
通过以上步骤和原则,可以有效地利用 socketserver 模块来创建服务器,并简化网络编程的过程,使程序员能够更专注于业务逻辑的实现。
import socketserver import time import threading class MyHandler(socketserver.BaseRequestHandler): def handle(self): print("=" * 30) print(self.request) # 与客户端通信的socket对象 print(self.client_address) print(id(self.server), self.server) print("=" * 30) while True: data = self.request.recv(1024) msg = "from {}: {}. data = {}".format(*self.client_address, data) self.request.send(msg.encode()) server = socketserver.ThreadingTCPServer(('127.0.0.1',9999),MyHandler) print(id(server)) server.serve_forever() threading.Thread(target=server.serve_forever,name='serve').start() while True: cmd = input('>>') if cmd == 'quit': server.server_close() print(threading.enumerate())
- 使用
socketserver
改写服务端
# 用socketserver 改写ChatServer import socketserver import threading from socketserver import BaseRequestHandler, StreamRequestHandler import termcolor import logging FORMAT = '%(asctime)s %(threadName)s %(thread)d %(message)s' logging.basicConfig(level=logging.INFO, format=FORMAT) class ChatHandler(StreamRequestHandler): # 继承StreamRequestHandler,重写handle方法,用于处理客户端的连接,读写数据 clients = {} # 用于存储客户端的连接 def setup(self) -> None: super().setup() self.event = threading.Event() self.clients[self.client_address] = self.wfile # 存储客户端的连接 self.clock = threading.Lock() # 锁对象 def handle(self) -> None: super().handle() while not self.event.is_set(): data = self.rfile.readline().strip() # 读取一行 \n 带有换行符 if data == b'' or data == b'quit': with self.clock: # 锁住 self.wfile.close() # 关闭文件 self.clients.pop(self.client_address) # 弹出客户端的连接 break msg = "from {}: {}. message = {}".format(*self.client_address, data) logging.info(termcolor.colored(msg, 'blue')) with self.clock: for c in self.clients.values(): c.write(msg.encode()) c.flush() def finish(self) -> None: super().finish() self.event.set() with self.clock: if self.client_address in self.clients: self.clients.pop(self.client_address) self.wfile.close() class ChatServer: def __init__(self, ip='127.0.0.1',port=9999,HanderClass=ChatHandler): self.addr = ip, port self.server = socketserver.ThreadingTCPServer(self.addr, HanderClass) self.stop_event = threading.Event() def start(self): threading.Thread(target=self.server.serve_forever, name='forever').start() def stop(self): if self.server: self.server.server_close() self.server.shutdown() self.stop_event.set() if __name__ == '__main__': cs = ChatServer() cs.start() while True: cmd = input(termcolor.colored('>>', 'green')).strip() if cmd == 'quit': cs.stop() break print(threading.enumerate())