前言
习惯性的使用requests等上层包去完成http请求交互,没用python socket写过通信。偶然间发现周围同事,上手裸写socket行云流水,看了一会发现,平时忽略了很多下一层的东西,因此想着去尝试熟悉python socket编程。
socket
简述
socket–Berkeley Socket,俗称套接字,实质是一套应用程序API(编程接口,对TCP/IP协议的封装,传输层/网络层协议,http是应用层协议)。主要功能是实现进程间通信,在网络通信中广泛应用。在c,python等语言中都有支持socket编程。
socket API 函数
https://gist.github.com/kevinkindom/108ffd675cb9253f8f71该作者给出python socket通信编程中非常详尽的API调用解析,包括方法,参数详述。此处仅罗列主要方法功能。
- socket():用来创建socket对象
- bind():出现在服务端编码,用来绑定服务器地址,启用端口号
- listen():出现在服务端编码,用来监听绑定的端口,使socket进入监听状态
- accept:接收建立TCP连接的请求,并返回新套接字对象conn和客户端address
- connect():出现在客户端编程,用来连接目标服务器
- recv():接受套接字中数据,以字符串类型返回给主调函数
- send():发送数据到套接字中
socket编程
服务端代码
接受客户端发送信息并显示接收到的数据,server.py内容:
import socket
import re
host = ('localhost', 8080)
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(host)
server.listen(5)
print('server is running...\n')
while True:
conn, address = server.accept() #conn是一个新的socket对象
conn.send('Welcome to test server! \n')
while True: #循环从当前conn连接中拿数据
data = conn.recv(128)
if not data or 'exit' in data:
print('client {} disconnect.'.format(re.findall(
r'client--(.*)', data)[0]))
conn.close()
break
else:
print(data)
客户端代码:
功能连接服务端,并从终端接受用户输入信息,发送给服务端。client.py内容如下:
import socket
import sys
client_name = sys.argv[1]
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('localhost', 8080))
print('client is started......')
hello = 'hello, I am {0}.'.format(client_name) if len(sys.argv) > 1 else ''
s.send(hello)
data = s.recv(128)
print(data)
while True:
content = raw_input('client {} input: '.format(client_name))
s.send(content)
if content == 'exit':
break
s.close()
运行结果:
左侧服务器信息显示,两个终端分别为c1和c2两个客户端。
可见c1与服务器建立TCP连接,并开始通信,其向服务器发送的数据均已被服务器接受。
但是:上述服务器代码是个阻塞式的,通过演示结果可以看出,client2实际已经发送连接请求给服务器了,但是由于服务端是在内层while处循环拿取当前连接上的conn中内容,进而必须待到c1.close()跳出后,c2才建立自己的conn。也就是说一次只能有一个连接在。
如果想改变上述情况,允许多客户连接则需要借助select。
select
python中的select是对linux系统中select的封装。在linux系统中一切皆文件,无论任何程序运行,最终都是归结为对文件进行读写。So,我们的socket通信,最后也是文件读写。linux提供了select工具以监控多个文件操作符,python在此上封装了select函数,用来调用linux select工具。
官网select函数解释
select.select(rlist, wlist, xlist[, timeout])
This is a straightforward interface to the Unix select() system call. The first three arguments are sequences of ‘waitable objects’: either integers representing file descriptors or objects with a parameterless method named fileno() returning such an integer:
rlist: wait until ready for reading
wlist: wait until ready for writing
xlist: wait for an “exceptional condition” (see the manual page for what your system considers such a condition)
理解起来就是python select函数接受三个由文件描述符组成的列表作参数,第一个是所有需要监控读操作的描述符,第二个所有需要监控写操作的描述符,第三个所有需要监控异常的描述符。可选参数是超时设定。返回时这些操作符列表中已经就绪可以进行读操作的操作符列表,可写操作符列表,可以进行异常处理的操作符列表。
利用select函数对之前的服务器改写成非阻塞式服务器 non_blocking_server.py
import socket
import select
import re
host = ('localhost', 8080)
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setblocking(False) #设置为非阻塞
server.bind(host)
server.listen(5)
print('non-blocking server is running...\n')
rlist = [server]
wlist, xlist = [], []
while True:
readable, writeable, exceptional = select.select(rlist, wlist, xlist)
for sock in readable:
if sock == server:
conn, address = server.accept()
conn.send('Welcome to test server! \n')
rlist.append(conn)
else:
data = sock.recv(128)
if not data or 'exit' in data:
print('client {} disconnect.'.format(re.findall(
r'client--(.*)', data)[0]))
rlist.remove(sock)
sock.close()
else:
print(data)
运行结果如下图所示:
可以看出server不再是等待c1连接结束后才接受新建其他连接,并且在读取数据时,也是有数据来就读出来。
代码解析:
1,select作用?
select是将需要进行读操作的所有socket进行检查后生成ready to read操作符列表供读循环。
2,新连接是如何加入的?
读操作符列表循环到原始server socket时,开始接受新的建立连接请求accept(),如果有新请求,那么就将新生成的连接conn添加到需要读监控的rlist中,待下次循环检测读数据。
3,断开连接处理?
当客户端输入退出,server接收到信息后,将此socket从需要监控的rlist中移除,并关闭该socket连接。
4,能否在非阻塞服务server中,不用select.select()方法,转而去手动维护一个readable list,就是上文的rlist?
从select的功能注释中,不推荐自己直接去裸监听操作符列表,因为并不知道哪些操作符(就socket)已经就绪,ready to read。select函数,帮我做了这一步。
上述代码是在python 2.7.8环境下调试通过,其中没有进行异常处理,只是实现自己预想的简单功能。