python socket编程介绍以及遇到的问题

前言

学习python socket编程主要的参考资料为《socket-programming-in-python-cn》, 英文原版地址在这里, 中文版pdf下载在这里。

一.echo客户端和服务器的介绍以及问题:

服务端代码如下:

#!/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,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)

二. I/O复用select,python中的selector模块的介绍以及问题

服务端代码如下:

#!/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是事件就绪的掩码:
    • mask & selectors.EVENT_WRITE表示write事件
    • mask & selectors.EVENT_READ表示read事件
  • key是SelectKey类型的具名元组,nametuple参考这个吧!:
    • key.fileobj:已经注册的文件对象,也就是socket;
    • key.data:与套接字关联的数据;
  • 如果key.data为空,说明是来自服务端监听的socket,于是我们accept,并且对已经连接的客户端套接字进行注册,关联的数据采用types.SimpleNamespace()生成一个object子类,这个子类有三个属性包括:addr(地址), inb(接收的数据), outb(发送的数据),详见官方文档。
  • 如果key.data非空,说明是新的客户端连接进来了,通过事件就绪掩码mask来判断I/O事件
    • 如果是READ,那么接收数据,调用recv
    • 如果是WRITE,那么发送数据,调用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函数:

  • 当处于READ状态时:使用recv函数接收为recv_data,并计算已经接收的数据的长度data.recv_total,当recv_data为空或者已经收到的数据的长度和待发送消息的总长度msg_total相等时,说明服务端已经发送完毕,关闭客户端套接字。注意:这里我们对接收到的消息进行了处理,即判断是否接收完全。
  • 当处于WRITE状态时:当待发送单个消息outb为空且待发送消息列表messages非空时,将messagespop出第一个消息给outboutb非空的时候,发送消息给服务端,并且将outb中已经发出的数据清空。
  • 后面的过程与服务端类似,sel.get_map()返回的是socket,key这样的Mapping。

你可能感兴趣的:(python,socket)