一、文件上传/下载
1、文件上传/下载
学习了socket套接字,我们现在可以写一个文件上传/下载的程序,如下示例:
import socket import json server = socket.socket() server.bind(('127.0.0.1',8899)) server.listen(5) while 1: print("server is working....") conn,addr = server.accept() while 1: dic_json = conn.recv(1024).decode('utf8') dic = json.loads(dic_json) print(dic) operate = dic.get('operate') file_name = dic.get('file_name') file_size = dic.get('file_size') conn.send(b'200') with open(file_name,'wb') as f: recv_len = 0 while recv_len < file_size: line = conn.recv(1024) recv_len += len(line) f.write(line) print('接收完成,接收了%s,发送了%s' % (recv_len,file_size)) print('接收完成') conn.close()
import socket import os import json client = socket.socket() client.connect(('127.0.0.1',8899)) while 1: cmd = input('请输入命令>>>') # 'put a.txt' operate,file_name = cmd.strip().split() file_size = os.path.getsize(file_name) print('文件大小是---',file_size) dic = { 'operate':operate, 'file_name':file_name, 'file_size':file_size } dic_json = json.dumps(dic).encode('utf-8') client.send(dic_json) # 确认服务端接收到了文件信息 res = client.recv(1024).decode('utf8') print(res) if res == '200': with open(file_name,'rb') as f: for line in f: client.send(line) else: print('服务器异常')
分析上边代码,我们发现,client发送上传文件相关信息的字典序列化之后,server又给client发送了一个状态码,然后client才开始上传文件数据,思考一下如果不发送状态码,直接发送文件数据且避免黏包现象发生该怎么写?没错!可以用struct模块解决,如下示例:
服务端代码:
import socket import json import struct import hashlib server = socket.socket() server.bind(('127.0.0.1',8899)) server.listen(5) while 1: print("server is working....") conn,addr = server.accept() while 1: dic_json_len_pack = conn.recv(4) # 接收字典字节码长度的pack值 dic_json_len = struct.unpack('i',dic_json_len_pack)[0] dic_json = conn.recv(dic_json_len).decode('utf8') dic = json.loads(dic_json) # b'xxxxxxxx{"":"","":"","":"","":""}xxxxxxxxxxxxxxxxxxxxxxxxxx' operate = dic.get('operate') file_name = dic.get('file_name') file_size = dic.get('file_size') md5 = hashlib.md5() with open(file_name,'wb') as f: recv_len = 0 while recv_len < file_size: line = conn.recv(1024) recv_len += len(line) f.write(line) md5.update(line) print('接收完成,接收了%s,发送了%s' % (recv_len,file_size)) print('接收完成') conn.send(b'ok') print(md5.hexdigest()) client_md5_val = conn.recv(1024).decode('utf8') if client_md5_val == md5.hexdigest(): conn.send(b'200') else: conn.send(b'400') conn.close()
客户端代码:
import socket import os import json import struct import hashlib client = socket.socket() client.connect(('127.0.0.1',8899)) while 1: cmd = input('请输入命令>>>') # 'put a.txt' operate,file_name = cmd.strip().split() file_size = os.path.getsize(file_name) print('文件大小是---',file_size) dic = { 'operate':operate, 'file_name':file_name, 'file_size':file_size } dic_json = json.dumps(dic).encode('utf-8') # 字典序列化后转成字节码 dic_json_len_pack = struct.pack('i',len(dic_json)) # 对字典序列化并转成字节码的长度进行pack client.send(dic_json_len_pack) client.send(dic_json) # 发送字典序列化后的字节码 md5 = hashlib.md5() with open(file_name,'rb') as f: for line in f: client.send(line) md5.update(line) print(md5.hexdigest()) client.recv(1024).decode('utf8') md5_val = md5.hexdigest() client.send(md5_val.encode('utf8')) response = client.recv(1024).decode('utf8') if response == '200': print('文件完整') else: print('文件上传失败!')
分析上面的代码,看看是如何避免黏包现象的?client先给server发送字典的长度的pack包,再发送字典,再发送文件数据,server先接收4字节的pack包,进行unpack后得到字典长度,再根据字典长度接收字典,最后再循环接收文件数据。并且该示例还进行了MD5算法来进行文件一致性校验。
二、socketserver(并发)
通过这两天学习socket套接字,我们发现在写服务端和客户端的时候,在建立连接之前总是要写一些固定的重复代码来,比如socket.socket()(创建套接字对象)、bind()、acecept()等等,学习了socketserver(并发)之后,就可以少写一些代码,并且实现并发,如下示例:
import socketserver class Myserver(socketserver.BaseRequestHandler): def handle(self): """ 到此已经是等待跟客户端连接的状态 所以从这里写代码正常逻辑代码 conn 用self.request替换即可 """ # 1 创建socket对象 2 self.socket.bind() 3 self.socket.listen(5) server = socketserver.ThreadingTCPServer(('127.0.0.1',8800),Myserver) server.serve_forever() # self.accept()
三、解读python中socketserver源码
结合下图中类的继承关系和类中的方法,分析socketserver代码的执行过程:
a、初始化相关过程:server = socketserver.ThreadingTCPServer(('127.0.0.1',8800),Myserver)
(1)TCPServer类中的__init__方法:
TCPServer类中主动执行BaseServer类中的__init__ 方法(把自己创建的Myserver类传参);
创建socket.socket()对象;
server_bind() -- 在TCPServer类中执行了socket.bind(self.server_address)
server_activate() -- 在TCPServer类中执行了socket.listen(5)
(2)BaseServer类中的__init__ 方法:
将参数server_address(ip地址和端口号)赋值给了self.server_address;
将形参RequestHandlerClass(实参是我们自己创建的Myserver类)赋值给了self.RequestHandlerClass;
b、执行serve_forever的相关代码:
(1)执行BaseServer类中的serve_forever()方法:
注意看if ready后边的那句self._handle_request_noblock(),其他先忽略;
(2)执行BaseServer中的_handle_request_noblock(self)方法:
看第一个try中request,client_address = self.get_request(),
TCPServer中有get_request()方法,方法中是return self.socket.accept(),即等待连接;
再看第二个try中的self.process_request(request,client_address)
(3)连接成功之后拿到了request(即conn)和client_address(即addr)再去执行BaseServer类中的.process_request方法;
创建线程,执行方法process_request_thread()
(4)执行ThreadingMixIn 类中的process_request_thread(self, request, client_address)方法:
看try中self.finish_request(request,client_address)
(5)执行BaseServer中的finish_request(request,client_address)方法:
此时还记得RequestHandlerClass这个参数吗?正是我们执行BaseServer中__init__方法时传过来的自己创建的类Myserver,类() 即实例化一个Myserver对象,并且传了两个参数(conn,addr),但是我们自己写的Myserver类中没有__init__方法,所以执行Myserver父类BaseRequestHandler类中的__init__方法,并且带了每个连接的线程的conn和addr:
(6)执行BaseRequestHandler中的__init__方法:
此时self是Myserver类的对象,所以优先去执行Myserver类中handle方法。
附录:以下是各类的继承关系以及类中成员方法: