学习python socket编程主要的参考资料为《socket-programming-in-python-cn》, 英文原版地址在这里, 中文版pdf下载在这里。
服务端代码如下:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import socket
HOST = '127.0.0.1' #本机地址
PORT = 9190
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind((HOST, PORT))
s.listen()
conn, addr = s.accept()
with conn:
print('Connected by ', addr)
while True:
data = conn.recv(1024)
if not data:
break
conn.sendall(data)
socket.AF_INET
表示ipv4
地址族,socket.SOCK_STREAM
表示TCP
协议;使用with as
语句就可以不用自己再写s.close()
了;bind
:绑定ip
和端口,127.0.0.1
是本机ip
,端口号范围0~65535
,绑定的端口最好大于1024;listen
:服务器接收连接请求,成为正在监听的套接字,参数backlog
表示最大监听的个数,python3.5之后取默认值;accept
:阻塞式,某客户端连接后,返回其套接字和地址;recv
:接受客户端的消息,返回的是byte
类型的数据即b''
,1024
指的是缓冲区的大小,客户端发送的数据可能很大,一次性无法接收完,所以多次接收;sendall
:一次性发送所有的数据给客户端,而send
函数可能无法一次性发送完,每次send
函数都返回已发送的字节长度,也可以用以下循环代替conn.sendall(data)
:len = 0
while True:
len = conn.send(data[len:]) #每次都发送一部分
if not len:
break
客户端代码:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import socket
HOST = '127.0.0.1'
PORT = 9190
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
string = 'hello world!'
s.sendall(string.encode('utf-8'))
data = s.recv(1024)
print('Received data: ', data)
客户端的代码比较简单:
connect
:连接服务端的ip
和端口;sendall(str.encode('utf-8'))
:直接发送byte
数据;recv
:接收服务端发来的数据;先运行服务端,再运行客户端,输出都正常:
服务端输出:Connected by ('127.0.0.1', 53703)
客户端:Received data: b'hello world!'
问题来了,当我把客户端发送的数据变得很大时,例如,将
string = 'hello world!' * 1024
改变端口第一次运行服务端会抛出异常:
Connected by ('127.0.0.1', 54049)
Traceback (most recent call last):
File "echo_server.py", line 16, in
data = conn.recv(1024)
ConnectionResetError: [WinError 10054] 远程主机强迫关闭了一个现有的
连接。
再重复运行服务端则抛出异常:
Connected by ('127.0.0.1', 54049)
Traceback (most recent call last):
File "echo_server.py", line 16, in
data = conn.recv(1024)
ConnectionAbortedError: [WinError 10053] 你的主机中的软件中止了一个
已建立的连接。
客户端收到一些数据,但是并没有1024个'hello world!'
。
经过排查,发现原来是客户端接收数据时,并没有像服务器那样循环接收,因此将客户端的data = s.recv(1024)
改成下面:
while True:
data = s.recv(1024)
if not data:
break
这样就没有抛出异常,但是客户端和服务端却同时都阻塞了。使用setblocking
函数设置为非阻塞,下面是修改后的完整代码:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import socket
HOST = '127.0.0.1' #本机地址
PORT = 65438
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind((HOST, PORT))
s.listen()
conn, addr = s.accept()
with conn:
conn.setblocking(0)
print('Connected by ', addr)
while True:
try:
data = conn.recv(1024)
if not data:
break
conn.sendall(data)
except BlockingIOError as e:
print("套接字阻塞")
break
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import socket
HOST = '127.0.0.1'
PORT = 65438
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
string = 'hello world!' * 1024
s.sendall(string.encode('utf-8'))
totalData = ''
while True:
data = s.recv(1024)
totalData += data.decode('utf-8')
if not data:
break
print('Received data: ', totalData)
服务端代码如下:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import socket
import selectors
import types
HOST = '127.0.0.1' #本机地址
PORT = 9190
sel = selectors.DefaultSelector()
lsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
lsock.bind((HOST, PORT))
lsock.listen()
print('listening on', (HOST, PORT))
lsock.setblocking(False)
sel.register(lsock, selectors.EVENT_READ, data=None)
while True:
events = sel.select(timeout=None)
for key, mask in events:
if key.data is None:
accept_wrapper(key.fileobj)
else:
service_connection(key, mask)
def accept_wrapper(sock):
conn, addr = sock.accept()
print('accept connection from ', addr)
conn.setblocking(False)
data = types.SimpleNamespace(addr=addr, inb=b'', outb=b'')
events = selectors.EVENT_READ | selectors.EVENT_WRITE
sel.register(conn, events, data=data)
def service_connection(key, mask):
sock = key.fileobj
data = key.data
if mask & selectors.EVENT_READ:
recv_data = sock.recv(1024)
if recv_data:
data.outb += recv_data
else:
print('closing connection to', data.addr)
sel.unregister(sock)
sock.close()
if mask & selectors.EVENT_WRITE:
if data.outb:
print('echoing ', repr(data.outb), 'to ', data.addr)
sent = sock.send(data.outb)
data.outb = data.outb[sent:]
selector
是一个python的高级I/O复用库,在文件Lib/selectors.py
中BaseSelector
+-- SelectSelector
+-- PollSelector
+-- EpollSelector
+-- DevpollSelector
+-- KqueueSelector
BaseSelector
是抽象基类,剩余的是以各种I/O复用方式区别的子类,通常selectors.DefaultSelector()
就是SelectSelector
类型。
sel.register(lsock, selectors.EVENT_READ, data=None)
:注册一个套接字并且监听其I/O事件,EVENT_READ,EVENT_WRITE
分别是读写事件。sel.select(timeout=None)
:等待已经注册的套接字就绪或者超时,并返回一个(key, mask)元组的列表:mask & selectors.EVENT_WRITE
表示write事件mask & selectors.EVENT_READ
表示read事件key.fileobj
:已经注册的文件对象,也就是socket;key.data
:与套接字关联的数据;key.data
为空,说明是来自服务端监听的socket,于是我们accept
,并且对已经连接的客户端套接字进行注册,关联的数据采用types.SimpleNamespace()
生成一个object子类,这个子类有三个属性包括:addr
(地址), inb
(接收的数据), outb
(发送的数据),详见官方文档。key.data
非空,说明是新的客户端连接进来了,通过事件就绪掩码mask来判断I/O事件recv
;send
;客户端的代码如下:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import socket
import selectors
import types
sel = selectors.DefaultSelector()
def start_connections(host, port, num_conns, messages):
server_addr = (host, port)
for i in range(num_conns):
connid = i + 1
print('starting connection ', connid, 'to ', server_addr)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setblocking(False)
sock.connect_ex(server_addr)
events = selectors.EVENT_READ | selectors.EVENT_WRITE
data = types.SimpleNamespace(connid=connid,
msg_total=sum(len(m) for m in messages),
recv_total=0,
messages=list(messages),
outb=b'')
sel.register(sock, events, data=data)
def server_connection(key, mask):
sock = key.fileobj
data = key.data
if mask & selectors.EVENT_READ:
recv_data = sock.recv(1024)
if recv_data:
print('recv_data: ', repr(recv_data), 'from connection', data.connid)
data.recv_total += len(recv_data)
if not recv_data or data.recv_total == data.msg_total:
print('closing connection', data.connid)
sel.unregister(sock)
sock.close()
if mask & selectors.EVENT_WRITE:
if not data.outb and data.messages:
data.outb = data.messages.pop(0)
if data.outb:
print('sending', repr(data.outb), 'to connection', data.connid)
sent = sock.send(data.outb)
data.outb = data.outb[sent:]
messages = [b'Hello world!', b'Nice to meet you!']
host = '127.0.0.1'
port = 9190
start_connections(host, port, 2, messages)
while True:
events = sel.select(timeout=1)
if events:
for key, mask in events:
server_connection(key, mask)
if not sel.get_map():
break
sel.close()
客户端的代码与之前比较类似,先从函数start_connections
介绍:
numm_conns
是准备要连接的客户端的个数,messages
是待发送的消息。函数connect_ex()
返回错误码,而不是函数connect()
在进程中抛出BlockingIOError异常。data
。connid: 连接的id
msg_total: 待发送消息的总长度
recv_total: 客户端已经收到消息的长度
messages: 待发送消息列表
outb: 待发送单个消息
再看server_connection
函数:
recv
函数接收为recv_data
,并计算已经接收的数据的长度data.recv_total
,当recv_data
为空或者已经收到的数据的长度和待发送消息的总长度msg_total
相等时,说明服务端已经发送完毕,关闭客户端套接字。注意:这里我们对接收到的消息进行了处理,即判断是否接收完全。outb
为空且待发送消息列表messages
非空时,将messages
pop出第一个消息给outb
,outb
非空的时候,发送消息给服务端,并且将outb
中已经发出的数据清空。sel.get_map()
返回的是socket,key这样的Mapping。