Python socket编程
本章内容
1、Socket简介
2、Socket远程服务器操作
3、SocketServer模块
4、粘包
Socket简介
python内有很多针对常见网络协议的库,
socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过"套接字"向网络发出请求或者应答网络请求。
socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,对于文件用【打开】【读写】【关闭】模式来操作。socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)
socket和file的区别:
- file模块是针对某个指定文件进行【打开】【读写】【关闭】
- socket模块是针对 服务器端 和 客户端Socket 进行【打开】【读写】【关闭】
sample
import socket #第一步相当于买手机, sockect家族 ,tcp phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #买完手机后插入手机卡,绑定ip,确定服务的唯一性 phone.bind(('127.0.0.1',8080)) #手机开机,并开5个进程,来处理问题 phone.listen(5) #接电话,且里面有两个值, conn,addr = phone.accept() #接受消息,大小1024 data = conn.recv(1024) print('from client msg %s'%dats) conn.send(data.upper()) cnn.close() #断链接 phone.close() #关闭socket
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(('127.0.0.1',8080)) client.send('hello'.encode('utf-8')) data = client.recv(1024) client.close()
server = 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 ,则系统就会根据地址格式和套接类别,自动选择一个合适的协议
server.bind(address)
s.bind(address) 将套接字绑定到地址。address地址的格式取决于地址族。在AF_INET下,以元组(host,port)的形式表示地址。
server.listen(backlog)
开始监听传入连接。backlog指定在拒绝连接之前,可以挂起的最大连接数量。
backlog等于5,表示内核已经接到了连接请求,但服务器还没有调用accept进行处理的连接个数最大为5, 这个值不能无限大,因为要在内核中维护连接队列
server.setblocking(bool)
是否阻塞(默认True),如果设置False,那么accept和recv时一旦无数据,则报错。
server.accept()
接受连接并返回(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据。address是连接客户端的地址。
接收TCP 客户的连接(阻塞式)等待连接的到来
server.connect(address)
连接到address处的套接字。一般,address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。
server.connect_ex(address)
同上,只不过会有返回值,连接成功时返回 0 ,连接失败时候返回编码,例如:10061
server.close()
关闭套接字
server.recv(bufsize[,flag])
接受套接字的数据。数据以字符串形式返回,bufsize指定最多可以接收的数量。flag提供有关消息的其他信息,通常可以忽略。
server.recvfrom(bufsize[.flag])
与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。
server.send(string[,flag])
将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。即:可能未将指定内容全部发送。
server.sendall(string[,flag])
将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。
内部通过递归调用send,将所有内容发送出去。
server.sendto(string[,flag],address)
将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。该函数主要用于UDP协议。
server.settimeout(timeout)
设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如 client 连接最多等待5s )
server.getpeername()
返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。
server.getsockname()
返回套接字自己的地址。通常是一个元组(ipaddr,port)
server.fileno()
套接字的文件描述符
升级版:
#可实现不停的收发数据 import socket #第一步相当于买手机, sockect家族 ,tcp phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #买完手机后插入手机卡,绑定ip,确定服务的唯一性 phone.bind(('127.0.0.1',8080)) #手机开机,并开5个进程,来处理问题 phone.listen(5) while True: #接电话,且里面有两个值, conn,addr = phone.accept() while True: try: #接受消息,大小1024 data = conn.recv(1024) if not data: #如果内容为空 break print('from client msg %s'%dats) conn.send(data.upper()) except: break cnn.close() #断链接 phone.close() #关闭socket
import socket ip_port = ('127.0.0.1',9998) client = socket.socket() client.connect(ip_port) while True: send_data = input('whaht do you want to send? =>') client.sendall(send_data.encode('utf-8')) server_reply = client.recv(1024) print('the data from server',server_reply) client.close()
Socket远程服务器操作
import socket import os ip_port = ('127.0.0.1',9983) server = socket.socket() server.bind(ip_port) server.listen(5) while True: print('wait to connect ....') conn,addr = server.accept() while True: print('wait for recv command ....') client_data = conn.recv(1024) print('the command from client...',client_data) res = os.popen(client_data.decode('utf-8')).read() print(res) conn.send(res.encode('utf-8')) #起先这里没有转码为bytes,会有报错 conn.close()
import socket ip_port = ('127.0.0.1',9983) client = socket.socket() client.connect(ip_port) while True: send_data = input('whaht do you want to send? =>') client.sendall(send_data.encode('utf-8')) server_reply = client.recv(1024) print(server_reply) client.close()
运行中的问题,命令接受过来是bytes类型,命令运行的话需要decode,而send发送的时候需要bytes,所以发送的时候需要decode,不然的话会有TypeError:'str' does not support the buffer interface
客户端接受到数据,因为是bytes类型,显示中文需要decode()下
提前告知client端文件解决粘包问题
import socket import os ip_port = ('127.0.0.1',9999) server = socket.socket() server.bind(ip_port) server.listen(5) while True: print('等待链接 ....') conn,addr = server.accept() while True: print('等待执行命令 ....') client_data = conn.recv(1024) print('client_data',client_data) if not client_data: print('客户端已经断开') break print('执行命令...',client_data) res = os.popen(client_data.decode('utf-8')).read() print('发送数据之前...') if len(res) ==0: res = 'there is no this data!' conn.send(str(len(res.encode('utf-8'))).encode("utf-8")) # print('res',res) conn.send(res.encode('utf-8')) print('发送命令done') conn.close() server.close()
import socket ip_port = ('127.0.0.1',9999) client = socket.socket() client.connect(ip_port) while True: send_data = input('what do you want to send? =>') client.sendall(send_data.encode('utf-8')) server_body_size = client.recv(1024) #告知客户端需要接收文件的大小 print('需要接受的文件大小:',server_body_size) receive_size = 0 receive_data = b'' n = 1 while receive_size < int(server_body_size.decode()): print('第%s循环'%n) data = client.recv(1024) receive_size +=len(data) receive_data += data n +=1 print('接收了数据的大小',receive_size) print(receive_data.decode()) print('接受数据done') client.close()
如果把这些代码放到linux上面执行,则会有一个问题,会有个报错,send需要发的包的大小,会和send包的内容粘到一会来发送,这就是所谓的粘包
解决办法:
1、两个send之间sleep(0.5) 会解决这个问题,不过这个很low,不能时时获取数据
2、这个方法就是在server端send完数据后,然后再cnn.recv()等待接受数据,在client端则是,接收到server端发来的文件大小长度后,然后再随便send点东西给server端,server端再执行发送正常数据的命令,这个问题就解决了。
到server,get一个文件下载到本地
import socket import os import hashlib ip_port = ('127.0.0.1',9995) server = socket.socket() server.bind(ip_port) server.listen(5) while True: print('等待链接 ....') conn,addr = server.accept() while True: print('等待执行命令 ....') client_data = conn.recv(1024) print('接收到的命令文件',client_data) client_method,client_file = client_data.split() print(client_method,client_file) if os.path.isfile(client_file): print('条件命令执行通过') file_size = os.stat(client_file).st_size #获取本地文件的大小,这个点新get到的 conn.send(str(file_size).encode('utf-8')) print('发送文件的大小给客户端', file_size) conn.recv(1024) print('收到客户端响应已经收到文件大小') md5 = hashlib.md5() f = open(client_file,'rb') for line in f: conn.send(line) md5.update(line) print('发送数据结束') conn.send(md5.hexdigest().encode('utf-8')) print('md5已发送给客户端') conn.close() server.close()
import socket import hashlib ip_port = ('127.0.0.1',9995) client = socket.socket() client.connect(ip_port) while True: send_data = input('what do you want to send? =>') if send_data.startswith('get'): want_file = send_data.split()[1] client.send(send_data.encode('utf-8')) want_file_size = client.recv(1024) print('接收到的文件大小',want_file_size) want_file_size = int(want_file_size.decode()) client.send(b'Got the file_size') print('回复确认已经收到文件的大小') md5 = hashlib.md5() f = open(want_file + '.new', 'wb') #这个更改文件名字的方式新get到 get_file_size = 0 while get_file_size < want_file_size: if want_file_size - get_file_size >= 1024: size = 1024 else: size = want_file_size - get_file_size data = client.recv(size) get_file_size += len(data) print(want_file_size,get_file_size,size) f.write(data) md5.update(data) else: f.close() print('客户端文件接收完毕') server_file_md5 = client.recv(1024) print('服务端md5',server_file_md5) print('客户端接收MD5',md5.hexdigest()) client.close()
SocketServer模块
SocketServer内部使用 IO多路复用 以及 “多线程” 和 “多进程” ,从而实现并发处理多个客户端请求的Socket服务端。即:每个客户端请求连接到服务器时,Socket服务端都会在服务器是创建一个“线程”或者“进程” 专门负责处理当前客户端的所有请求。
ThreadingTCPServer
ThreadingTCPServer实现的Soket服务器内部会为每个client创建一个 “线程”,该线程用来和客户端进行交互。
1、ThreadingTCPServer基础
使用ThreadingTCPServer:
- 创建一个继承自 SocketServer.BaseRequestHandler 的类
- 类中必须定义一个名称为 handle 的方法
- 启动ThreadingTCPServer
sample:
#!/usr/bin/env python # -*- coding:utf-8 -*- import SocketServer class MyServer(SocketServer.BaseRequestHandler): def handle(self): # print self.request,self.client_address,self.server conn = self.request conn.sendall('欢迎致电 10086,请输入1xxx,0转人工服务.') Flag = True while Flag: data = conn.recv(1024) if data == 'exit': Flag = False elif data == '0': conn.sendall('通过可能会被录音.balabala一大推') else: conn.sendall('请重新输入.') if __name__ == '__main__': server = SocketServer.ThreadingTCPServer(('127.0.0.1',8009),MyServer) server.serve_forever()
2、ThreadingTCPServer源代码(待研究)
ForkingTCPServer
ForkingTCPServer和ThreadingTCPServer的使用和执行流程基本一致,只不过在内部分别为请求者建立 “线程” 和 “进程”。
sample:
复制代码 #!/usr/bin/env python # -*- coding:utf-8 -*- import SocketServer class MyServer(SocketServer.BaseRequestHandler): def handle(self): # print self.request,self.client_address,self.server conn = self.request conn.sendall('欢迎致电 10086,请输入1xxx,0转人工服务.') Flag = True while Flag: data = conn.recv(1024) if data == 'exit': Flag = False elif data == '0': conn.sendall('通过可能会被录音.balabala一大推') else: conn.sendall('请重新输入.') if __name__ == '__main__': server = SocketServer.ForkingTCPServer(('127.0.0.1',8009),MyServer) #与线程只有这里不相同 server.serve_forever()
SocketServer的ThreadingTCPServer之所以可以同时处理请求得益于 select 和 os.fork 两个东西,其实本质上就是在服务器端为每一个客户端创建一个进程,当前新创建的进程用来处理对应客户端的请求,所以,可以支持同时n个客户端链接(长连接)。
上传一个文件到服务器端(面向对象的方式,socketserver)
import socketserver import json import os class MyTcpHandler(socketserver.BaseRequestHandler): def put(self,*args): cmd_dict = args[0] filename = cmd_dict['filename'] filesize = cmd_dict['size'] if os.path.isfile(filename): f = open(filename + '.new','wb') else: f = open(filename,'wb') self.request.send(b'200,ok') received_size = 0 while received_size < filesize: data = self.request.recv(1024) f.write(data) received_size += len(data) print(received_size,filesize) else: print('%s文件接收完毕'%filename) def handle(self): while True: try: self.data = self.request.recv(1024).strip() print("{} wrote:".format(self.client_address[0])) print('在客户端收到接受文件前的信息',self.data) cmd_dict = json.loads(self.data.decode('utf-8')) print('在客户端接收到的信息是:',cmd_dict) action = cmd_dict['action'] if hasattr(self,action): #反射方法 func = getattr(self,action) func(cmd_dict) except ConnectionResetError as e: print('error',e) break if __name__ == '__main__': HOST,PORT = 'localhost',9997 server = socketserver.ThreadingTCPServer((HOST,PORT),MyTcpHandler) print('ftpserver已启动') server.serve_forever()
import json import socket import os class FtpClient(object): def __init__(self): self.client = socket.socket() def help(self): msg = ''' ls pwd cd ../.. get filename put filename ''' print(msg) def connect(self,ip,port): self.client.connect((ip,port)) print('已连接到ftpserver') def interactive(self): while True: cmd = input('input you cmd=>').strip() if len(cmd) == 0:continue cmd_str = cmd.split()[0] if hasattr(self,'cmd_%s'%cmd_str): #反射,判断有没有这个方法 func = getattr(self,"cmd_%s"%cmd_str) #反射,执行这个方法 func(cmd) print('下一步运行put方法') else: self.help() def cmd_put(self,*args): print('开始上传文件......') cmd_method = args[0].split() if len(cmd_method) >1: filename = cmd_method[1] if os.path.isfile(filename): filesize = os.stat(filename).st_size mes_dict = { 'action':'put', 'filename':filename, "size":filesize, "overridden":True } self.client.send(json.dumps(mes_dict).encode('utf-8')) print('发送了文件信息',json.dumps(mes_dict).encode("utf-8")) server_response = self.client.recv(1024) print('收到了ftp端的确认信息:',server_response) f = open(filename,'rb') for line in f: self.client.send(line) else: print('发送文件完毕...') f.close() else: print('需要发送的文件不存在') def cmd_get(self): pass ftp = FtpClient() ftp.connect('localhost',9997) ftp.interactive()
粘包
期间你会遇到粘包的问题,所谓的粘包的问题是指,你这次输入的命令,返回的结果却是上一次的输入命令的结果问题的原因在于,接收方不知道消息的大小,不知道一次要取多少造成的,tcp有这样的问题,udp没有,程序会有个缓存区
如图所示,程序每次发送消息不是直接给客户,而是放到缓冲区,接受消息也是如此,在缓冲区去取。tcp是流式的,会一直发,信息都存在客户短的缓存中,如果第一次取消息设置的不够大,那就取不完,下一次再输入新的命令时,会继续取上次留下的消息,这就是造成粘包的原因
那如何解决呢?
办法是,服务端在发送消息之前告知客户端我这信息的大小,然后客户端用这个大小去接受,这样每次接受的就是完整的信息了,代码实现如下:
客户端的处理
import struct
#struct 这个模块是用来计算数据大小的
data=client。recv(4) #struct 计算后得知消息体的大小,这段占位为4
data_size=struct.unpack('i',data)[0]#获得需要发送消息的大小
res=client.revc(data_size)#接受实际的大小
print (res.decode(‘gbk’))
服务端处理
import struct
conn.send(struct.pack('i',len(back_msg))) #i,打包的模式,len是消息的大小。
conn.send(back_msg) #在发送消息
这里有个问题,如果数据过大,比缓存区还大,直接send的话数据就会没了,这是后需要用
conn.sendall(back_msg)
sendall是循环调用send命令。
客户端收的时候的处理,
recv_size=0
recv_bytes=b''
while recv_size < data_size:
res=client.recv(1024)
recv_bytes+=res
recv_size+=len(res)
print(recv_bytes.decode('gbk'))
如果,再大,大到文件的长度4个比特位都放不下那有如何,这里就引用到了字典-json,先把文件存为字典{’size‘:1234567891231456789123456789}然后再json化传递给客户端,客户端在反json后再读取,如此获得文件的大小
服务端的配置
import json
head_dict={'data_size':len(back_msg)}#将文件大小放到字典中
head_json=json.dumps(head_dict) #json化
head_bytes=head_json.encode('utf-8')
conn.send(struct.pack('i',len(head_bytes)))#先发送这个json的大小
conn.send(head_bytes)#发送json文件
conn.sendall(back_msg)#发送真实的文件
客户端的配置
#收取包头的长度
head=client.recv(4)
head_size=struct.unpack('i',head)[0]
#收取包头
head_bytes=client.recv(head_size)
head_json=head_bytes.decode('utf-8')
head_dict=json.loads(head_json)
data_size=head_dict['data_size']
#收取真实的数据:
recv_size=0
recv_bytes=b''
while recv_size < data_size:
res=client.recv(1024)
recv_bytes+=res
recv_size+=len(res)
print(recv_bytes.decode('gbk'))