网络编程
socket
socket又称"套接字",应用程序通常通过"套接字"向网络发出请求或者应答网络请求,使主机间或者一台计算机上的进程间可以通讯。
socket 与 file 的却别:
- file 是对指定文件进行打开,读写,关闭
- socket 是对服务器和客户端的 socket 进行打开,读写,关闭
一、socket 客户端与服务器交互流程
简单实例
服务端将客户端发送的字符串转成大写后再返回给客户端
import socket sk = socket.socket() sk.bind(('127.0.0.1', 8001)) sk.listen(5) connect, address = sk.accept() while True: receive_data = connect.recv(1024) if not receive_data: break connect.sendall(receive_data.upper()) connect.close()
import socket sk = socket.socket() sk.connect(('127.0.0.1', 8001)) while True: msg = input(">>>").strip() if not msg: continue sk.sendall(bytes(msg, encoding='utf8')) receive_data = sk.recv(1024) print(receive_data.decode()) sk.close()
服务器与客户端交互时不能发送空字符串
二、socket 类
sk = socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)
family:
- socket.AF_INET 默认值,IPv4
- socket.AF_INET6 IPv6
type:
- socket.SOCK_STREAM 默认值,TCP
- socket.SOCK_DGRAM UDP
proto: 默认值 0 ,一般不填
三、socket 对象方法
- sk.bind(address) 绑定地址(IP,Port)到套接字,在 AF_INET 下以元组(IP,Port)的形式表示
- sk.listen(backlog) 开始 TCP 监听,backlog 指定在拒绝连接之前,操作系统可以挂起的最大连接数,该值至少为 1,一般情况下设置为 5 即可
- sk.setblocking(bool) 是否阻塞,默认值为 True, 如果设置为 False,一旦 accept 和 recv 没有数据,则报错
- sk.accept() 被动接收客户端的连接(阻塞式),等待客户端连接,返回(sock, addr) , sock 为新的套接字对象,可以用来接收和发送消息,addr 为客户端的地址
- sk.connect(address) 连接到 address 所在的服务器,address 以元组的形式表示(IP,Port) or (HOST,Port)
- sk.connect_ex(address) 和 sk.connect() 类似,只是会有返回值,而不是抛出异常,如果连接成功返回 0
- sk.close() 关闭套接字
- sk.recv(bufsize[,flag]) 接收 socket 数据,数据以 bytes 返回(python2.x为 str), bufsize 指定一次可以接收的最大数据,flag 忽略,默认值为 0
- sk.recvform(bufsize[,flag]) 接收 socket 数据,返回值为 (data, address), data 为 bytes 类型的数据, address 为从哪个客户端接收的数据
- sk.send(bytes[,flag]) 将 bytes 数据发送给 socket,返回值为发送的字节大小
- sk.sendall(bytes[,flag]) 将 bytes 数据发送给 socket,如果成功则返回 None,失败则抛出异常。
- sk.sendto(bytes,address) 将 bytes 数据发送给 socket,通常用于 UDP
- sk.settimeout(value) 设置 blocking 的超时时间,value 为一个浮点数,单位为 s,默认为不超时,通常配置在 socket 建立之初。value = 5,表示客户端最多等待 5 s
- sk.getpeername() 返回套接字远端的地址,(IP, Port)
- sk.getsockname() 返回自身的地址 (IP,Port)
- sk.fileno() 返回 socket 的文件描述符,返回值为一个数字,-1 表示获取失败,通常在 select.select() 中使用
四、使用 socket 实现简单的 ssh 操作
粘包问题:
sk.recv(bufsize) 在接收 socket 数据时,是有大小限制的,如果 send 的数据过大,就会出现一次性接收不完全的情况,导致后面的交互数据混乱
解决方法:
在发送数据之前,先将需要发送的 bytes 大小发送给对方,等待对方准备完成后,在将数据发送。而接收方会根据 bytes 的大小一直接收,并进行数据组合
import socket import subprocess s = socket.socket() s.bind(("127.0.0.1",9999)) s.listen(5) while True: print("Wating New Connection ... ") conn, addr = s.accept() print("\tWating IP: {0[0]} Port: {0[1]}".format(addr)) while True: # 捕获客户端发送过来的数据,最大接收 1024 字节 recv_data = conn.recv(1024) # 如果接收到的自己为空,则跳出循环 if not recv_data:break # 捕获苦短发送的命令 cmd = str(recv_data, encoding='utf8') res = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) output = str(res.stdout.read(), encoding='utf8') error = str(res.stderr.read(), encoding='utf8') if error: if output: send_data = "%s\n%s" % (output, error) else: send_data = error else: send_data = output print(len(send_data)) # 如果需要发送的字节大约 1024,则先发送一个 Ready|{字节大小},作为标记,然后再紧接着发送数据 if len(send_data) > 1024: ready_tag = "Ready|%s" % len(send_data) conn.send(bytes(ready_tag, encoding='utf8')) start_tag = conn.recv(1024) if start_tag.deconde() == 'Start': conn.send(bytes(send_data, encoding='utf8')) else: conn.send(bytes(send_data, encoding='utf8')) conn.close() print("\tIP:{0[0]} Port:{0[1]} Close Connection".format(addr))
import re import socket s = socket.socket() s.connect(('127.0.0.1', 9999)) while True: try: # 捕获输入 send_data = input(">> ").strip() # 如果捕获数据为空,则继续提示输入 if not send_data:continue # 如果输入的数据为 exit,则跳出循环 if send_data == 'exit':break # 发送捕获到的数据,python 3.5 之后发送的数据必须为 bytes 类型 s.send(bytes(send_data, encoding='utf8')) # 接收最大 1024 字节的数据 recv_data = s.recv(1024) # 将数据转换为 str recv_data = str(recv_data, encoding='utf-8') # 如果接收到的数据为匹配正则: Ready\|\d+$, 说明后面会有一个大约 1024的数据进行发送 # 获取到需要接收的字节大小,然后进行多次接收,并进行组合,然后再输出 if re.match('Ready\|\d+$', recv_data): # 获取数据的字节大小 msg_size = recv_data.split("|")[-1] msg_size = int(msg_size) recv_data = b"" # 接受的数据 recv_size = 0 # 已接收数据大小 s.send(bytes("Start", encoding='utf-8')) while recv_size < msg_size: recv_msg = s.recv(1024) # 多次接收 recv_data += recv_msg # 组合数据 recv_size += len(recv_msg) # 修改已接收数据大小 recv_data = str(recv_data, encoding='utf-8') # 打印数据 print(recv_data) except KeyboardInterrupt: break s.close()
IO多路复用
通过一种机制监视多个文件描述符,一但某个描述符就绪(一般是读就绪或写就绪)能够通知程序做相应的读写操作
Linux中的 select,poll,epoll 都是IO多路复用的机制。
Python中有一个select模块,其中提供了:select、poll、epoll三个方法,分别调用系统的 select,poll,epoll 从而实现IO多路复用。
网络操作、文件操作、终端操作等均属于IO操作,对于windows只支持Socket操作,其他系统支持其他IO操作,但是无法检测 普通文件操作 自动上次读取是否已经变化。
select 方法
readlist, writelist, errorlist =select.
select
(rlist, wlist, xlist[, timeout])
rlist 必选参数,监听读事件
wlist 必选参数,监听写事件
xlist 必选参数,监听异常事件
timeout 可选参数,select 阻塞的超时事件,如指定时间内监听的所有时间均无变化,则返回 3 个空list,如果 timeout 未指定则一直阻塞,知道监听到变化
socketserver
通过 socket 创建的 socker 服务器默认只支持一个连接的操作,当已经存在一个连接时,新来的连接会处于等待状态(sk.listen(5),最多支持 5 个客户端等待),当已有连接断开后才能连接和下一个客户端进行连接。
socketserver 模块内部使用 “IO多路复用” 和 “多线程”、“多进程” 方式实现多并发的 socket
import socketserver class Server(socketserver.BaseRequestHandler): # 这里的方法必须是 handle def handle(self): # 定义与客户端的所有交互内容 while True: receive_data = self.request.recv(1024) if not receive_data: break send_data = receive_data.upper() self.request.send(send_data) if __name__ == '__main__': # 创建 TreadingTCPServer 对象,并执行 serve_forever 方法 server = socketserver.ThreadingTCPServer(('127.0.0.1',9998), Server) server.serve_forever()
import socket s = socket.socket() s.connect(('127.0.0.1',9998)) while True: inp = input(">>>") if not inp: continue if inp == 'quit': break s.send(bytes(inp, encoding='utf-8')) receive_data = s.recv(1024) print(receive_data.decode()) s.close()
一、ThreadingTCPSever 源码剖析
二、ThreadingTCPServer 源码精简版
import socket import select import threading def process(request, client_ip): print("HOST[{0[0]}] Port[{0[1]}] Connect".format(client_ip)) request.sendall(bytes('欢迎连接到服务器。。。', encoding='utf-8')) while True: receive_data = request.recv(1024) if not receive_data: break request.sendall(receive_data.upper()) sk = socket.socket() sk.bind(('127.0.0.1',8001)) sk.listen(5) while True: # 监听 socket 是否有 IO 变化 r, w, e = select.select([sk], [], [], 1) if sk in r: request, client_ip = sk.accept() # 为 socket 创建多线程 t = threading.Thread(target=process, args=(request, client_ip)) t.start() sk.close()