python成长之路9——socket和socketserver
IPC:进程间通信
本地的进程间通信(IPC)有很多种方式,但可以总结为下面4类:
- 消息传递(管道、FIFO、消息队列)
- 同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量)
- 共享内存(匿名的和具名的)
- 远程过程调用(Solaris门和Sun RPC)
现在是网络时代,我们更关心的是网络中进程之间如何通信呢?首先要通信必须进程彼此之间互相认识对方,在本地可以通过进程PID来唯一标识一个进程,在网络上其实也可以用三元组(ip,协议,端口)来标识一个进程,这样就为网络进程间通信提供了可能,具体如何实现呢?
这就要引出我们今天的重点啦!!!!! socket
socket 即“套接字”,是TCP/IP协议的一个封装,一个软件抽象层,它对用户进程来说就是一个接口(可以看作是TCP/IP协议的接口),我们要使用TCP/IP协议来进行数据传输时就不需要直接处理TCP/IP协议,而是使用socket就可以啦,用户进程通过它来和TCP/IP进行数据的发送接收
socket 起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“open-->read/write-->close”模式来操作。socket作为一个特殊的文件,socket里的很多函数都是这种模式的。
下面的流程图详细的描述了socket服务端进程和客户端进程之间的工作关系,我们可以形象的把它类比成一次打电话的过程!
1 #/usr/bin/env python 2 #-*- coding:utf-8 -*- 3 #Authot:Zhang Yan 4 5 import socket,subprocess 6 ip_port=("127.0.0.1",9007) 7 8 #买电话 9 s=socket.socket() 10 11 #绑定电话卡 12 s.bind(ip_port) 13 14 #开机 15 s.listen(5) 16 17 #待机 18 conn,addr=s.accept() 19 20 while True: 21 #接听消息 22 recv_data=str(conn.recv(1024),encoding="utf8") 23 if recv_data == "exit":break 24 print("客户端说:>>",recv_data) 25 #发送消息 26 send_data=bytes("我收到你的消息啦!",encoding="utf8") 27 conn.send(send_data) 28 29 #挂断 30 conn.close()
1 #/usr/bin/env python 2 #-*- coding:utf-8 -*- 3 #Authot:Zhang Yan 4 5 import socket 6 ip_port=("127.0.0.1",9007) 7 8 #找到公用电话 9 s=socket.socket() 10 11 #给服务端打电话 12 s.connect(ip_port) 13 14 while True: 15 #发消息 16 send_data=input(">>").strip() 17 s.send(bytes(send_data,encoding="utf8")) 18 if send_data == "exit":break 19 #收消息 20 resv_data=str(s.recv(1024),encoding="utf8") 21 print("服务端说:>>",resv_data) 22 23 #挂断 24 s.close()
client: >>hello 服务端说:>> 我收到你的消息啦! >>hi 服务端说:>> 我收到你的消息啦! >>你好 服务端说:>> 我收到你的消息啦! >>库尼奇瓦 服务端说:>> 我收到你的消息啦! >>哦哈呦 服务端说:>> 我收到你的消息啦! >>你还会说别的吗 服务端说:>> 我收到你的消息啦! >>exit server: 客户端说:>> hello 客户端说:>> hi 客户端说:>> 你好 客户端说:>> 库尼奇瓦 客户端说:>> 哦哈呦 客户端说:>> 你还会说别的吗
上面是两个最简单的socket server和client的例子,从中我们可以看出,socket的代码很符合我们的那个例子------打电话,现在你应该可以凭自己的理解和记忆写个socket小程序了吧,恭喜你完成了很重要的一小步!
下面我们看下socket的功能:
sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM,0) 参数一:地址簇 socket.AF_INET IPv4(默认) socket.AF_INET6 IPv6 socket.AF_UNIX 只能够用于单一的Unix系统进程间通信 参数二:类型 socket.SOCK_STREAM 流式socket , for TCP (默认) socket.SOCK_DGRAM 数据报式socket , for UDP socket.SOCK_RAW 原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以;其次,SOCK_RAW也可以处理特殊的IPv4报文;此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头。 socket.SOCK_RDM 是一种可靠的UDP形式,即保证交付数据报但不保证顺序。SOCK_RAM用来提供对原始协议的低级访问,在需要执行某些特殊操作时使用,如发送ICMP报文。SOCK_RAM通常仅限于高级用户或管理员运行的程序使用。 socket.SOCK_SEQPACKET 可靠的连续数据包服务 参数三:协议 0 (默认)与特定的地址家族相关的协议,如果是 0 ,则系统就会根据地址格式和套接类别,自动选择一个合适的协议 sk.bind(address) s.bind(address) 将套接字绑定到地址。address地址的格式取决于地址族。在AF_INET下,以元组(host,port)的形式表示地址。 sk.listen(backlog) 开始监听传入连接。backlog指定在拒绝连接之前,可以挂起的最大连接数量。 backlog等于5,表示内核已经接到了连接请求,但服务器还没有调用accept进行处理的连接个数最大为5 这个值不能无限大,因为要在内核中维护连接队列 sk.setblocking(bool) 是否阻塞(默认True),如果设置False,那么accept和recv时一旦无数据,则报错。 sk.accept() 接受连接并返回(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据。address是连接客户端的地址。 接收TCP 客户的连接(阻塞式)等待连接的到来 sk.connect(address) 连接到address处的套接字。一般,address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。 sk.connect_ex(address) 同上,只不过会有返回值,连接成功时返回 0 ,连接失败时候返回编码,例如:10061 sk.close() 关闭套接字 sk.recv(bufsize[,flag]) 接受套接字的数据。数据以字符串形式返回,bufsize指定最多可以接收的数量。flag提供有关消息的其他信息,通常可以忽略。 sk.recvfrom(bufsize[.flag]) 与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。 sk.send(string[,flag]) 将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。即:可能未将指定内容全部发送。 sk.sendall(string[,flag]) 将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。 内部通过递归调用send,将所有内容发送出去。 sk.sendto(string[,flag],address) 将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。该函数主要用于UDP协议。 sk.settimeout(timeout) 设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如 client 连接最多等待5s ) sk.getpeername() 返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。 sk.getsockname() 返回套接字自己的地址。通常是一个元组(ipaddr,port) sk.fileno() 套接字的文件描述符
1 server: 2 #/usr/bin/env python 3 #-*- coding:utf-8 -*- 4 #Authot:Zhang Yan 5 6 import socket 7 8 sk=socket.socket(socket.AF_INET,socket.SOCK_DGRAM,0) 9 sk.bind(("127.0.0.1",9999)) 10 11 while True: 12 data,ip_port=sk.recvfrom(1024) 13 sk.sendto(data,ip_port) 14 15 client: 16 #/usr/bin/env python 17 #-*- coding:utf-8 -*- 18 #Authot:Zhang Yan 19 20 import socket 21 22 sk=socket.socket(socket.AF_INET,socket.SOCK_DGRAM,0) 23 ip_port=("127.0.0.1",9999) 24 25 while True: 26 inp=input(">>:").strip() 27 sk.sendto(bytes(inp,encoding="utf8"),ip_port) 28 data,ip_port=sk.recvfrom(1024) 29 print(str(data,encoding="utf8"))
使用socket的注意事项:
1.基于python3.5版本的socket只能收发字节(pyhton2.7可以收发str)
2.在链接正常的情况下accept()和recv()是阻塞的
3.listen(backlog) backlog代表能挂起的链接数,即:如果backlog=1,则代表可链接一个,挂起一个,剩余的请求将被拒绝。
下面我们有个小的需求,从这个小需求里会慢慢的暴露我们程序的不足并完善它:
需求:
我们实现一个服务端和一个客户端,客户端模拟ssh远程执行命令操作服务端。
1 #/usr/bin/env python 2 #-*- coding:utf-8 -*- 3 #Authot:Zhang Yan 4 5 import socket,subprocess 6 ip_port=("127.0.0.1",9006) 7 8 #买电话 9 s=socket.socket() 10 11 #电话卡 12 s.bind(ip_port) 13 14 #开机 15 s.listen(5) 16 17 while True: 18 try: 19 #待机 20 conn,addr=s.accept() 21 while True: 22 #接听消息 23 recv_data=str(conn.recv(1024),encoding="utf8") 24 if recv_data == "exit" or len(recv_data) == 0: break 25 cmd=recv_data 26 p=subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) 27 out=str(p.stdout.read(),encoding="utf8") 28 err=str(p.stderr.read(),encoding="utf8") 29 #发送消息 30 if len(err) == 0: 31 if len(out) == 0: 32 send_data=bytes("执行无输出",encoding="utf8") 33 conn.send(send_data) 34 continue 35 else: 36 num=len(bytes(out,encoding="utf8")) 37 send_data="out size is %d bytes" % num 38 send_data=bytes(send_data,encoding="utf8") 39 else: 40 send_data=bytes(err,encoding="utf8") 41 conn.send(send_data) 42 continue 43 conn.send(send_data) 44 recv_data = str(conn.recv(1024), encoding="utf8") 45 if recv_data == "y": 46 send_data = bytes(out, encoding="utf8") 47 conn.send(send_data) 48 else: 49 send_data=bytes("客户端选择不接收数据",encoding="utf8") 50 conn.send(send_data) 51 except KeyboardInterrupt: 52 break 53 #挂断 54 conn.close()
1 #/usr/bin/env python 2 #-*- coding:utf-8 -*- 3 #Authot:Zhang Yan 4 5 import socket 6 ip_port=("127.0.0.1",9006) 7 8 #找到公用电话 9 s=socket.socket() 10 11 #打电话 12 s.connect(ip_port) 13 14 while True: 15 try: 16 #发消息 17 send_data=input(">>").strip() 18 if len(send_data) == 0:continue 19 s.send(bytes(send_data,encoding="utf8")) 20 if send_data == "exit":break 21 #收消息 22 resv_data=str(s.recv(1024),encoding="utf8") 23 if resv_data.startswith("out size is"): 24 print(resv_data) 25 num=int(resv_data.split(" ")[3]) 26 st=input("is starting? y/n") 27 s.send(bytes(st,encoding="utf8")) 28 n1,n2=divmod(num,1024) 29 if n2 == 0: 30 n=n1 31 else: 32 n=n1+1 33 for i in range(n): 34 resv_data=str(s.recv(1024),encoding="utf8") 35 if resv_data.startswith("客户端"): 36 print(resv_data) 37 break 38 print(resv_data) 39 else: 40 print(resv_data) 41 except KeyboardInterrupt or BrokenPipeError: 42 break 43 #挂断 44 s.close()
client: >> >>ls out size is 170 bytes is starting? y/ny __init__.py socket_client.py socket_client2.py socket_client_simple.py socket_server.py socket_server_simple.py socket_server_thread.py thread_client.py thread_server.py >>cat __init__.py out size is 60 bytes is starting? y/nn 客户端选择不接收数据 >>cat __init__.py out size is 60 bytes is starting? y/ny #/usr/bin/env python #-*- coding:utf-8 -*- #Authot:Zhang Yan >>exit client断开时server继续等待下一次的链接
在上面的代码中我们遇到的最大的一个问题就是“黏包”的问题,什么是黏包呢?
黏包: socket在传输过程中,客户端第一次请求的数据包没有接收完全,第二次请求时第一次请求的数据包和第二次请求的数据包合到一起发送到了客户端,这个就叫黏包。
怎么解决呢?其实很简单,我们只要确保每次我们发送的数据都能被对方全部接收,就不会和之后传送的包合并到一起,这样就避免了黏包的问题。具体如何操作呢?上面的代码里已经解决了这个问题啦。
解决思路是:
客户端请求数据后,服务端先给客户端发送一个数据长度的数据,客户端拿到这个数据长度的数据之后,循环的取数据,直到取完所有的数据,如何循环取呢?我采用这种方式:假定我们一次接收1024M的数据,我们用总数据长度除以1024得到商和余数,如果余数为0,取数据的次数就是商,如果非0,取数据的次数就是商+1,这样我们就能保证每次传输数据的完整性啦,从而就解决了黏包的问题。
上面我们使用socket实现了一对一的通信,现在大家可能会想,现在是互联网时代,我们必须要考虑高并发的问题,及实现一对多(一个服务端对多个客户端),也可以说是多对多(多个线程或进程对多个请求),python已经为我们提供了一个很好的模块(socketserver)来满足这个需求啦!
python把网络服务抽象成两个主要的类,一个是Sever类,用于处理链接相关的网络操作,另外一个则是RequestHandle类,用于处理数据相关的操作。并且提供两个MixIn类,用于扩展server,实现多进程和多线程。在构建网络服务时,server和requesthandle并不是分开的,requesthander的实例对象在server内配合server工作。
该模块的几个server继承关系如下:
socketserver的源码我们下次分析,这次我们只先关注它的使用:
创建服务器的步骤:首先,你必须创建一个请求处理类(eg:Myserver),它是BaseRequestHandler的子类并重载其hangle()方法。其次,你必须实例化一个服务器类,传人服务器的地址和请求处理程序类。最后,调用handle_request()(一般是调用其他事件循环或者使用select()或serve_forever())
下面是一个socketserver的简单例子:
1 #/usr/bin/env python 2 #-*- coding:utf-8 -*- 3 #Authot:Zhang Yan 4 5 import socketserver 6 7 class MyServer(socketserver.BaseRequestHandler): 8 def handle(self): 9 self.request.sendall(bytes("欢迎致电10086",encoding="utf8")) 10 while True: 11 data=self.request.recv(1024) 12 print("[%s] says:%s" % (self.client_address,str(data,encoding="utf8"))) 13 self.request.sendall(data.upper()) 14 if __name__ == "__main__": 15 ip_port=("127.0.0.1",3397) 16 server=socketserver.ThreadingTCPServer(ip_port,MyServer) 17 server.serve_forever()
1 #/usr/bin/env python 2 #-*- coding:utf-8 -*- 3 #Authot:Zhang Yan 4 5 import socket 6 ip_port=("127.0.0.1",3397) 7 8 #找到公用电话 9 s=socket.socket() 10 11 #打电话 12 s.connect(ip_port) 13 14 welcome_msg=s.recv(1024) 15 print("from server:",str(welcome_msg,encoding="utf8")) 16 17 while True: 18 try: 19 #发消息 20 send_data=input(">>").strip() 21 if len(send_data) == 0:continue 22 s.send(bytes(send_data,encoding="utf8")) 23 #收消息 24 resv_data=str(s.recv(1024),encoding="utf8") 25 print(resv_data) 26 except KeyboardInterrupt or BrokenPipeError: 27 break 28 #挂断 29 s.close()
1 server: 2 [('127.0.0.1', 52136)] says:hi 我是client1 3 [('127.0.0.1', 52137)] says:hi 我是client2 4 5 client1: 6 from server: 欢迎致电10086 7 >>hi 我是client1 8 HI 我是CLIENT1 9 >> 10 11 client2: 12 from server: 欢迎致电10086 13 >>hi 我是client2 14 HI 我是CLIENT2 15 >>
下面我们将之前的那个需求(ssh那个)用socketserver实现下:
1 #/usr/bin/env python 2 #-*- coding:utf-8 -*- 3 #Authot:Zhang Yan 4 5 import socketserver,subprocess 6 ip_port=("127.0.0.1",9007) 7 8 class MyServer(socketserver.BaseRequestHandler): 9 def handle(self): 10 conn=self.request 11 data="[%s]欢迎登录本系统!" % str(self.client_address) 12 conn.sendall(bytes(data,encoding="utf8")) 13 while True: 14 # 接听消息 15 recv_data = str(conn.recv(1024), encoding="utf8") 16 if recv_data == "exit" or len(recv_data) == 0: break 17 cmd = recv_data 18 p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 19 out = str(p.stdout.read(), encoding="utf8") 20 err = str(p.stderr.read(), encoding="utf8") 21 # 发送消息 22 if len(err) == 0: 23 if len(out) == 0: 24 send_data = bytes("执行无输出", encoding="utf8") 25 conn.send(send_data) 26 continue 27 else: 28 num = len(bytes(out, encoding="utf8")) 29 send_data = "out size is %d bytes" % num 30 send_data = bytes(send_data, encoding="utf8") 31 else: 32 send_data = bytes(err, encoding="utf8") 33 conn.send(send_data) 34 continue 35 conn.send(send_data) 36 recv_data = str(conn.recv(1024), encoding="utf8") 37 if recv_data == "y": 38 send_data = bytes(out, encoding="utf8") 39 conn.send(send_data) 40 else: 41 send_data = bytes("客户端选择不接收数据", encoding="utf8") 42 conn.send(send_data) 43 if __name__ == "__main__": 44 server=socketserver.ThreadingTCPServer(ip_port,MyServer) 45 server.serve_forever()
1 #/usr/bin/env python 2 #-*- coding:utf-8 -*- 3 #Authot:Zhang Yan 4 5 import socket 6 ip_port=("127.0.0.1",9007) 7 8 s=socket.socket() 9 10 s.connect(ip_port) 11 12 wel_msg=s.recv(1024) 13 print(str(wel_msg,encoding="utf8")) 14 15 while True: 16 try: 17 send_data=input(">>").strip() 18 if len(send_data) == 0:continue 19 s.send(bytes(send_data,encoding="utf8")) 20 if send_data == "exit":break 21 resv_data=str(s.recv(1024),encoding="utf8") 22 if resv_data.startswith("out size is"): 23 print(resv_data) 24 num=int(resv_data.split(" ")[3]) 25 st=input("is starting? y/n") 26 s.send(bytes(st,encoding="utf8")) 27 n1,n2=divmod(num,1024) 28 if n2 == 0: 29 n=n1 30 else: 31 n=n1+1 32 for i in range(n): 33 resv_data=str(s.recv(1024),encoding="utf8") 34 if resv_data.startswith("客户端"): 35 print(resv_data) 36 break 37 print(resv_data) 38 else: 39 print(resv_data) 40 except KeyboardInterrupt or BrokenPipeError: 41 break 42 s.close()
1 client1: 2 [('127.0.0.1', 52161)]欢迎登录本系统! 3 >>pwd 4 out size is 29 bytes 5 is starting? y/nn 6 客户端选择不接收数据 7 >>pwd 8 out size is 29 bytes 9 is starting? y/ny 10 /Users/admin/Desktop/zy/day9 11 12 >> 13 14 client2: 15 [('127.0.0.1', 52162)]欢迎登录本系统! 16 >>pwd 17 out size is 29 bytes 18 is starting? y/ny 19 /Users/admin/Desktop/zy/day9 20 21 >>pwd 22 out size is 29 bytes 23 is starting? y/nn 24 客户端选择不接收数据 25 >>