如今咱们开发每每不断使用封装好的web框架, 运行web服务也有至关多的容器, 可是其原理每每都离不开socket. 像是nginx底层就是采用相似python中epoll的异步监听方式加上socket结合来作. 本文采起从最简单的socket通讯实现聊天机器人, 到伪并发实现聊天机器人, 最后采用异步监听方式实现聊天机器人, 逐步推动.python
首先咱们实现一个最简单版的的socket服务端, server_s1.pynginx
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import socket
HOST='127.0.0.1'
PORT=9999
sockaddr=(HOST,PORT)
sk=socket.socket()
sk.bind(sockaddr)
sk.listen(5)
conn,address=sk.accept()
ret_bytes=conn.recv(1024)
print(str(ret_bytes,encoding='utf-8'))
conn.sendall(ret_bytes+bytes(', 已收到!',encoding='utf-8'))
sk.close()复制代码
sk=socket.socket()
这里建立socket对象sk.bind(sockaddr)
传入一个元组对象以此来设置服务端ip和portsk.listen(5)
表示设置最大等待链接数为5个conn,address=sk.accept()
此时阻塞进程, 循环等待被链接, 返回链接对象和包含链接信息的对象ret_bytes=conn.recv(1024)
等待接受1024个字节的信息conn.sendall(ret_bytes+bytes(', 已收到!',encoding='utf-8'))
将接受的信息加上 , 已收到!
从新发送给客户端. 注意, 在python2中能够传递str类型的数据, 可是在python3中只能传递byte类型的数据sk.close()
关闭链接至此简单的服务端已经写好了, 咱们看看客户端, client_c1.pyweb
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import socket
HOST='127.0.0.1'
PORT=9999
sockaddr=(HOST,PORT)
ct=socket.socket()
ct.connect(sockaddr)
ct.sendall(bytes('第一次链接',encoding='utf-8'))
ret_bytes=ct.recv(1024)
print(str(ret_bytes,encoding='utf-8'))
ct.close()复制代码
ct.connect(sockaddr)
来执行到如今为止, 已经把简单聊天机器人已经写好了, 客户端向服务端发送第一次链接
, 服务端接受输出到客户端并回馈给客户端第一次链接, 已收到!
接下来咱们试着让这个服务端更健壮一些, 尝试让它能够不断的返回客户端发送过来的内容编程
这是第二个版本的服务端, server_s2.py服务器
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import socket
HOST='127.0.0.1'
PORT=9999
sockaddr=(HOST,PORT)
sk=socket.socket()
sk.bind(sockaddr)
sk.listen(5)
while True:
conn,address=sk.accept()
while True:
try:
ret_bytes=conn.recv(1024)
except Exception as ex:
print("已从",address,"断开")
break
else:
conn.sendall(ret_bytes+bytes(', 已收到!',encoding='utf-8'))
sk.close()复制代码
接下来看看客户端文件, client_c2.py网络
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import socket
HOST='127.0.0.1'
PORT=9999
sockaddr=(HOST,PORT)
ct=socket.socket()
ct.connect(sockaddr)
while True:
inp=input("请输入要发送的内容: ")
ct.sendall(bytes(inp,encoding='utf-8'))
ret_bytes=ct.recv(1024)
print(str(ret_bytes,encoding='utf-8'))
ct.close()复制代码
如今第二个版本已经能够接二连三的处理同一链接的消息, 即便断开也不会影响服务器的健壮性. 可是, 咱们的服务器功能还很单一, 只能一次处理一个客户端的链接. 接下来将用select模块实现伪并发处理客户端链接并发
这里是第三个版本的服务端文件, server_s3.pyapp
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import socket
import select
HOST = '127.0.0.1'
PORT = 9999
sockaddr = (HOST, PORT)
sk = socket.socket()
sk.bind(sockaddr)
sk.listen(5)
sk_inps = [sk, ]
while True:
change_list, keep_list, error_list = select.select(sk_inps, [], sk_inps, 1)
for sk_tmp in change_list:
if sk_tmp == sk:
conn, address = sk_tmp.accept()
sk_inps.append(conn)
else:
try:
ret_bytes = sk_tmp.recv(1024)
except Exception as ex:
sk_inps.remove(sk_tmp)
print("已从", sk_tmp.getpeername(), "断开")
else:
sk_tmp.sendall(ret_bytes + bytes(', 已收到!', encoding='utf-8'))
for sk_tmp in error_list:
sk_inps.remove(sk_tmp)
sk.close()复制代码
咱们首先来看一下循环的过程框架
change_list, keep_list, error_list = select.select(sk_inps, [], sk_inps, 1)
中, select.select()
会自动监控起参数的内容, 当第一个参数中的对象发生变化时候会将该对象加到change_list中, 该次循环结束时change_list便会自动清空. 第一个参数中的变化对于sk对象, 这里只有客户端链接sk对象或者与sk对象断开两种状况sk_inps.remove(sk_tmp)
这一句中, 一旦客户端断开链接, 则服务端就会捕捉到异常并将该客户端对象从监控列表sk_inps
中移除select.select()
中的第二个参数, 该参数中有什么对象则keep_list
中就会加入什么对象, 该参数对于读写分离的伪并发处理有很大意义, 咱们稍后再作介绍select.select()
的第三个参数是当被监控的对象出现错误或者异常时候就将出错的对象加入到error_list
中, 随后咱们遍历error_list
并根据里边的出错对象将其从sk_inps
中除去该版本的客户端延续上一版本便可, 无需更改. 至此, 咱们就创建一个能并发简单处理多客户端链接的服务器. 可是, 对于change_list
中遍历时候咱们既有读又有写的操做, 这样当后期的处理复杂的时候, 代码维护很难再进行下去. 接下来咱们接着开发咱们的伪并发处理的最终版本异步
这里是服务的文件, server_s4.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import socket
import select
HOST = '127.0.0.1'
PORT = 9997
sockaddr = (HOST, PORT)
sk = socket.socket()
sk.bind(sockaddr)
sk.listen(5)
sk_inps = [sk, ]
sk_outs=[]
message_dic={}
while True:
change_list, keep_list, error_list = select.select(sk_inps, sk_outs, sk_inps, 1)
for sk_tmp in change_list:
if sk_tmp == sk:
conn, address = sk_tmp.accept()
sk_inps.append(conn)
message_dic[conn]=[]
else:
try:
ret_bytes = sk_tmp.recv(1024)
except Exception as ex:
sk_inps.remove(sk_tmp)
print("已从", sk_tmp.getpeername(), "断开")
del message_dic[sk_tmp]
else:
sk_outs.append(sk_tmp)
message_dic[sk_tmp].append(str(ret_bytes,encoding='utf-8'))
for conn in keep_list:
message= message_dic[conn][0]
conn.sendall(bytes(message+", 已收到!",encoding='utf-8'))
del message_dic[conn][0]
sk_outs.remove(conn)
for sk_tmp in error_list:
sk_inps.remove(sk_tmp)
sk.close()复制代码
sk_outs=[]
中保存发送消息的客户端链接对象message_dic={}
中保存消息内容以上就是伪并发处理客户端请求全部内容, 究其本质实际上是IO多路复用原理. 同时python中也提供了真正的并发处理模块socketserver, 下面咱们采用socketserver来实现
首先看咱们的服务端文件, server_s5.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import socketserver
HOST = '127.0.0.1'
PORT = 9997
sockaddr = (HOST, PORT)
class MySocket(socketserver.BaseRequestHandler):
def handle(self):
conn = self.request
while True:
try:
ret_bytes = conn.recv(1024)
except Exception as ex:
print("已从", self.client_address, "断开")
break
else:
conn.sendall(ret_bytes + bytes(', 已收到!', encoding='utf-8'))
if __name__ == "__main__":
server = socketserver.ThreadingTCPServer(sockaddr, MySocket)
server.serve_forever()复制代码
threading
线程处理, 再加上原本的Socket内容造成server = socketserver.ThreadingTCPServer(sockaddr, MySocket)
该句会将Socket服务端设置ip和port等内容封装到对象中, 执行初始化时候须要加入本身写的继承socketserver.BaseRequestHandler
的类server.serve_forever()
此句执行时候会使得对象调用handle(self)
方法, 在该方法中咱们对客户端链接进行处理以上咱们将Socket从基础原理到复杂自定义已经使用封装好的模块使用介绍完毕. 接下来咱们补充一些理论知识和经常使用的Socket参数和方法:
首先咱们来回顾一下OSI模型和TCP/IP协议簇,如图(图片引自网络)
根据 socket 传输数据方式的不一样(其实就是使用协议的不一样), 致使其与不一样层打交道
Stream sockets
, 是一种面向链接
的 socket, 使用 TCP 协议. Datagram sockets
, 无链接
的 socket,使用 UDP 协议. Raw sockets
, 一般用在路由器或其余网络设备中, 这种socket直接由网络层通向应用层. 如下是注意点:
sk=socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)
实际上默认传入了参数, 第一个参数表示ip协议, ocket.AF_INET
表示ipv4协议(默认就是), 第二个参数表示传输数据格式, socket.SOCK_STREAM
表示tcp协议(默认就是), socket.SOCK_DGRAM
表示udp协议ret_bytes=conn.recv(1024)
中表示最多接受1024个字节; 若没有接受到内容则会阻塞进程, 等待接受内容send()
可能会发送部份内容, sendall()
本质就是内部循环调用send()
直到将内容发送完毕, 建议使用sendall()