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())