socket编程
网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。
Socket的英文原义是“孔”或“插座”。作为BSD UNIX的进程通信机制,取后一种意思。通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,可以用来实现不同虚拟机或不同计算机之间的通信。在Internet上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。Socket正如其英文原意那样,像一个多孔插座。一台主机犹如布满各种插座的房间,每个插座有一个编号,有的插座提供220伏交流电, 有的提供110伏交流电,有的则提供有线电视节目。 客户软件将插头插到不同编号的插座,就可以得到不同的服务。
socket是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)
既然是文件那么socket模块 和 file 模块之间有哪些区别呢?
- file模块是针对某个指定文件进行【打开】【读写】【关闭】
- socket模块是针对 服务器端 和 客户端Socket 进行【打开】【读写】【关闭】
如下为socket流程图
1.OSI 7层模型回顾
七层模型,实际上是一个体系,亦称OSI(Open System Interconnection)参考模型,该参考模型是国际标准化组织(ISO)制定的一个用于计算机或通信系统间互联的标准体系
那么具体的每层如下:
- 应用层 (Application):网络服务于最终用户的一个接口 常用协议有:HTTP FTP TFTP SMTP SNMP DNS
- 表示层(Presentation Layer):数据的表示、安全、压缩。(在五层模型里面已经合并到了应用层) 格式有:JPEG、ASCll、DECOIC、加密格式等
- 会话层(Session Layer):建立、管理、终止会话。(在五层模型里面已经合并到了应用层) 对应主机进程,指本地主机与远程主机正在进行的会话
- 传输层 (Transport):定义传输数据的协议端口号,以及流控和差错校验。 协议有:TCP UDP,数据包一旦离开网卡即进入网络传输层
- 网络层 (Network):进行逻辑地址寻址,实现不同网络之间的路径选择。 协议有:ICMP IGMP IP(IPV4 IPV6) ARP RARP
- 数据链路层 (Link):建立逻辑连接、进行硬件地址寻址、差错校验等功能。(由底层网络定义协议) 将比特组合成字节进而组合成帧,用MAC地址访问介质,错误发现但不能纠正。
- 物理层(Physical Layer):建立、维护、断开物理连接。(由底层网络定义协议)
实际上两个(多个)主机在进行通话时正是遵循如上的模型,如下过程达到两个主机之间的数据交流。
更加详细的过程介绍如下:
那么我门今天所要学习的socket在数据交换过程中充当的是什么角色呢?请看下一张图:
2.python中socket模块
熟悉了以上socket在整个通信过程中的地位,下面就利用socket模块写个简单的server
一开始已经展示了socket的流程图,如果将这连个主机之间通过socket通信的过程比作为两个人打电话的过程,那么那幅图可以这样理解:
就针对如上的过程我们来创建一个简单的server:
server端代码:
# -*- coding:utf-8 -*- #Author:wencheng.zhao # import socket ip_port = (‘127.0.0.1‘,9999) #买手机 s = socket.socket() #买手机卡 s.bind(ip_port) #开机 s.listen(5) #等待电话 conn,addr = s.accept() #接听电话 res_data = conn.recv(1024) #接受客户端一开始发送的问候语。 print(‘----->>>>>>>>‘,res_data.decode()) #发消息 sen_data = res_data.upper() #将客户端传入的数据,全部处理成答谢然后在发回客户端去-(必须为字节的方式) print(sen_data) conn.send(sen_data) #将要传的数据传给客户端 #挂掉电话 conn.close()
client端代码:
# -*- coding:utf-8 -*- #Author:wencheng.zhao # import socket ip_port = (‘127.0.0.1‘,9999) #买手机 s = socket.socket() #拨号 s.connect(ip_port) #发消息 s.send(bytes("hello my name is zhaowencheng",encoding=‘utf-8‘)) #一开始就向server端发送一段问候语 - (必须一字节的方式) #接受消息 res_data = s.recv(1024) #再次接收,服务端处理完后的数据。 print(res_data.decode()) #挂掉电话: s.close()
下面我们看下这个执行过程:
1.首先执行server端,使server端处于socket监听模式。(这时候server会阻塞,等待客户端传数据过来才能继续)
2.再执行client端代码后出现如下:
#client 显示如下: HELLO MY NAME IS ZHAOWENCHENG Process finished with exit code 0 #server端显示如下: ----->>>>>>>> hello my name is zhaowencheng b‘HELLO MY NAME IS ZHAOWENCHENG‘ Process finished with exit code 0
根据上面的内容可以实现一个client端与server端的交互,但是还有改进之处,如本程序当客户端结束之后服务端也会随之结束,这点不符合常理,下面针对此再完善。并且加入判断exit的功能。
###############server端代码############## # -*- coding:utf-8 -*- #Author:wencheng.zhao ########简单server ############################### import socket ip_port=(‘127.0.0.1‘,9999) s = socket.socket() s.bind(ip_port) s.listen(5) #等待电话 while True: #将整个创建链接的过程写到循环里,当客户端中断的时候,这边在重新创建。 conn,addr = s.accept() while True: try: recv_data = conn.recv(1024) if len(recv_data) == 0:break print(recv_data,type(recv_data)) send_data = recv_data.upper() conn.send(send_data) except Exception: break conn.close() ########client端代码############### # -*- coding:utf-8 -*- #Author:wencheng.zhao #普通客户端 import socket ip_port = (‘127.0.0.1‘,9999) s = socket.socket() s.connect(ip_port) while True: send_data = input(">>:").strip() if len(send_data) == 0 :continue s.send(bytes(send_data,encoding=‘utf8‘)) if send_data == ‘exit‘:break recv_data = s.recv(1024) print(str(recv_data,encoding=‘utf8‘)) s.close() ########### ############################# #####执行代码后如下: #client >>:hh HH >>:exit Process finished with exit code 0 #server端 /Library/Frameworks/Python.framework/Versions/3.5/bin/python3.5 /Users/wenchengzhao/PycharmProjects/s13/day9/server.py b‘hh‘ <class ‘bytes‘> b‘exit‘ <class ‘bytes‘> #当client退出的时候 server并没有推出。
实例,利用上面的知识实现一个类似ssh功能(能够执行某些简单的命令)
########server端代码############ while True: conn,addr = s.accept() while True: try: recv_data = conn.recv(1024) if len(recv_data) == 0:break #print(recv_data,type(recv_data)) p = subprocess.Popen(str(recv_data,encoding=‘utf8‘),shell=True,stdout=subprocess.PIPE) #执行系统命令 res = p.stdout.read() print(res) if len(res) == 0: send_data = ‘cmd -- err‘ else: send_data = str(res,encoding=‘gbk‘) #send_data = ‘ok‘ conn.send(bytes(send_data,encoding=‘utf8‘)) except Exception as e: print(e) break conn.close() ######client端代码如下 # -*- coding:utf-8 -*- #Author:wencheng.zhao #普通客户端 import socket ip_port = (‘127.0.0.1‘,9999) s = socket.socket() s.connect(ip_port) while True: send_data = input(">>:").strip() if len(send_data) == 0 :continue s.send(bytes(send_data,encoding=‘utf8‘)) if send_data == ‘exit‘:break recv_data = s.recv(1024) print(str(recv_data,encoding=‘utf8‘)) s.close() ###################################### #执行结果如下: #client >>:ls client.py client2.py server.py temp9.py tttclient.py ttttblog.py >>:ls -a . .. client.py client2.py server.py temp9.py tttclient.py ttttblog.py >>: ###########server端显示如下: b‘client.py\nclient2.py\nserver.py\ntemp9.py\ntttclient.py\nttttblog.py\n‘ b‘.\n..\nclient.py\nclient2.py\nserver.py\ntemp9.py\ntttclient.py\nttttblog.py\n‘
如上实现了一个简单的远程执行命令的小程序。
3.socket粘包问题
粘包问题的由来: 由上面我们在接收数据时是用的是:
conn.recv(1024) --这个1024指的是一次接受的大小。
那么这样就存在一个问题,当发送端将大于1024时,会出现什么问题呢?当大于1024时这里最大也只能接受到1024(这个值可以调整,但是永远是一个定值),当再次接受时会首先接受上次未能接受完的内容,这样造成的结果肯定不是我们想要的。下面提供一种解决办法就是,当发送内容之前,先发送一段内容的长度大小,我在接收的时候会判断接受的内容是否已经全部接受完,如果没有接受完就应该一直循环来接收,这样就能确保会包所有的内容都能接收完了。
如下代码实例:
server端代码:
1 # -*- coding:utf-8 -*- 2 #Author:wencheng.zhao 3 4 import socket 5 import subprocess #导入执行命令模块 6 ip_port=(‘127.0.0.1‘,9999) #定义元祖 7 #买手机 8 s=socket.socket() #绑定协议,生成套接字 9 s.bind(ip_port) #绑定ip+协议+端口:用来唯一标识一个进程,ip_port必须是元组格式 10 s.listen(5) #定义最大可以挂起胡链接数 11 #等待电话 12 while True: #用来重复接收新的链接 13 conn,addr=s.accept() #接收客户端胡链接请求,返回conn(相当于一个特定胡链接),addr是客户端ip+port 14 #收消息 15 while True: #用来基于一个链接重复收发消息 16 try: #捕捉客户端异常关闭(ctrl+c) 17 recv_data=conn.recv(1024) #收消息,阻塞 18 if len(recv_data) == 0:break #客户端如果退出,服务端将收到空消息,退出 19 20 #发消息 21 p=subprocess.Popen(str(recv_data,encoding=‘utf8‘),shell=True,stdout=subprocess.PIPE) #执行系统命令,windows平 22 # 台命令的标准输出是gbk编码,需要转换 23 res=p.stdout.read() #获取标准输出 24 if len(res) == 0: #执行错误命令,标准输出为空, 25 send_data=‘cmd err‘ 26 else: 27 send_data=str(res,encoding=‘gbk‘) #命令执行ok,字节gbk---->str---->字节utf-8 28 29 send_data=bytes(send_data,encoding=‘utf8‘) 30 31 32 #解决粘包问题 33 ready_tag=‘Ready|%s‘ %len(send_data) 34 conn.send(bytes(ready_tag,encoding=‘utf8‘)) #发送数据长度 35 feedback=conn.recv(1024) #接收确认信息 36 feedback=str(feedback,encoding=‘utf8‘) 37 38 if feedback.startswith(‘Start‘): 39 conn.send(send_data) #发送命令的执行结果 40 except Exception: 41 break 42 #挂电话 43 conn.close()
client端代码:
1 # -*- coding:utf-8 -*- 2 #Author:wencheng.zhao 3 import socket 4 ip_port=(‘127.0.0.1‘,9999) 5 #买手机 6 s=socket.socket() 7 #拨号 8 s.connect(ip_port) #链接服务端,如果服务已经存在一个好的连接,那么挂起 9 10 while True: #基于connect建立的连接来循环发送消息 11 send_data=input(">>: ").strip() 12 if send_data == ‘exit‘:break 13 if len(send_data) == 0:continue 14 s.send(bytes(send_data,encoding=‘utf8‘)) 15 16 #解决粘包问题 17 ready_tag=s.recv(1024) #收取带数据长度的字节:Ready|9998 18 ready_tag=str(ready_tag,encoding=‘utf8‘) 19 if ready_tag.startswith(‘Ready‘): #Ready|9998 20 msg_size=int(ready_tag.split(‘|‘)[-1]) #获取待接收数据长度 21 start_tag=‘Start‘ 22 s.send(bytes(start_tag,encoding=‘utf8‘)) #发送确认信息 23 24 #基于已经收到的待接收数据长度,循环接收数据 25 recv_size=0 26 recv_msg=b‘‘ 27 while recv_size < msg_size: #如果接收到的总数比应该接收到的小就继续接收。(等于时结束,不会有大的情况) 28 recv_data=s.recv(1024) 29 recv_msg+=recv_data #将每一次接收到内容都追加到接收总内容中,最后统一展示 30 recv_size+=len(recv_data) #每次汇总一次接收大小 31 print(‘MSG SIZE %s RECE SIZE %s‘ %(msg_size,recv_size)) 32 33 print(str(recv_msg,encoding=‘utf8‘)) 34 #挂电话 35 s.close()
执行结果展示:
#依次执行 server端,client端。 #在client输入命令(此命令的返回结果应该大于1024,才有测试粘包问题的效果,例如下面的命令) ####client端输入命令 >>: ls /usr/lib MSG SIZE 4448 RECE SIZE 1024 #这里可以显示客户端在接收的时候的过程。。。 直到接收完才能停止。 MSG SIZE 4448 RECE SIZE 2048 MSG SIZE 4448 RECE SIZE 3072 MSG SIZE 4448 RECE SIZE 4096 MSG SIZE 4448 RECE SIZE 4448 charset.alias cron dtrace dyld groff libATCommandStudioDynamic.dylib libAVFAudio.dylib libAccountPolicyTranslation.dylib libBSDPClient.A.dylib libBSDPClient.dylib libCRFSuite.dylib libCRFSuite0.12.dylib libChineseTokenizer.dylib ...等等... .. .. ..
4.使用功能详解以及功能补充。
s = 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 ,则系统就会根据地址格式和套接类别,自动选择一个合适的协议
s.bind(address)
s.bind(address) 将套接字绑定到地址。address地址的格式取决于地址族。在AF_INET下,以元组(host,port)的形式表示地址。
s.listen(backlog)
开始监听传入连接。backlog指定在拒绝连接之前,可以挂起的最大连接数量。
backlog等于5,表示内核已经接到了连接请求,但服务器还没有调用accept进行处理的连接个数最大为5
这个值不能无限大,因为要在内核中维护连接队列
s.setblocking(bool)
是否阻塞(默认True),如果设置False,那么accept和recv时一旦无数据,则报错。
s.accept()
接受连接并返回(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据。address是连接客户端的地址。
接收TCP 客户的连接(阻塞式)等待连接的到来
s.connect(address)
连接到address处的套接字。一般,address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。
s.connect_ex(address)
同上,只不过会有返回值,连接成功时返回 0 ,连接失败时候返回编码,例如:10061
s.close()
关闭套接字
s.recv(bufsize[,flag])
接受套接字的数据。bufsize指定最多可以接收的数量。flag提供有关消息的其他信息,通常可以忽略。
s.recvfrom(bufsize[.flag])
与recv()类似,但返回值是(data,address)。其中data是包含接收数,address是发送数据的套接字地址。
s.send(string[,flag])
将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。即:可能未将指定内容全部发送。
s.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()
套接字的文件描述符
5.socketserver 模块
socketserver内部使用 IO多路复用 以及 “多线程” 和 “多进程” ,从而实现并发处理多个客户端请求的Socket服务端。即:每个客户端请求连接到服务器时,Socket服务端都会在服务器是创建一个“线程”或者“进程” 专门负责处理当前客户端的所有请求。 python3中的socketserver模块与python2中的不同,在python2中对应为 SocketServer 模块。
如下:对于每一个请求都有一个对应的线程或者进程去处理。
ThreadingTCPServer
1、ThreadingTCPServer基础
使用ThreadingTCPServer:
- 创建一个继承自 SocketServer.BaseRequestHandler 的类
- 类中必须定义一个名称为 handle 的方法
- 启动ThreadingTCPServer
下面通过一段代码来实现一个简单的例子
server端代码:
####socketserver import socketserver class MyServer(socketserver.BaseRequestHandler): #创建一个类 继承socketserver.BaseRequestHandler def handle(self): #必须定义一个handel的方法。 self.request.sendall(bytes(‘欢迎 ---‘,encoding=‘utf8‘)) # while True: data = self.request.recv(1024) if len(data) == 0:break print("%s sysa:%s" % (self.client_address,data.decode())) self.request.sendall(data.upper()) if __name__ == ‘__main__‘: server = socketserver.ThreadingTCPServer((‘127.0.0.1‘,8009),MyServer) server.serve_forever()
client:没有太大区别
############# 多线程 client #普通客户端 import socket ip_port = (‘127.0.0.1‘,8009) s = socket.socket() s.connect(ip_port) recv_data = s.recv(1024) print(recv_data.decode()) while True: send_data = input(">>:").strip() if len(send_data) == 0 :continue s.send(bytes(send_data,encoding=‘utf8‘)) if send_data == ‘exit‘:break recv_data = s.recv(1024) print(str(recv_data,encoding=‘utf8‘)) s.close()
验证-执行结果:
#先运行server端代码 #再运行client端代码(多打开几个) #执行如下: #client 欢迎 --- >>:ls LS >>:llll LLLL >>: #server ##分别对应多个客户端。 (‘127.0.0.1‘, 62021) sysa:ls (‘127.0.0.1‘, 62021) sysa:llll (‘127.0.0.1‘, 62044) sysa:ls (‘127.0.0.1‘, 62021) sysa:ls (‘127.0.0.1‘, 62037) sysa:ls (‘127.0.0.1‘, 62037) sysa:llll
客户端打开情况如下:
实例:多线程实现类ssh功能(远程执行简单命令):
server代码如下:(客户端不变)
import socketserver import subprocess class MyServer(socketserver.BaseRequestHandler): def handle(self): # print self.request,self.client_address,self.server self.request.sendall(bytes(‘欢迎-----‘,encoding="utf-8")) while True: data = self.request.recv(1024) if len(data) == 0:break print("[%s] says:%s" % (self.client_address,data.decode() )) #self.request.sendall(data.upper()) cmd = subprocess.Popen(data.decode(),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) cmd_res = cmd.stdout.read() if not cmd_res: cmd_res = cmd.stderr.read() if len(cmd_res) == 0: #cmd has not output cmd_res = bytes("cmd has output",encoding="utf-8") self.request.send(cmd_res ) if __name__ == ‘__main__‘: server = socketserver.ThreadingTCPServer((‘0.0.0.0‘, 8009), MyServer) server.serve_forever()