python编程 入门后学习笔记二 ——socket通信(2)
本节内容
1.socket实现文件发送
2.socketserver的使用
3.练习
1.socket实现文件发送
socket能实现文件传输吗?
答案:能。
那么,socket是怎样实现文件发送的呢?
学习了上一篇socket的内容,相信对于socket文件发送的理解就很容易了。
简单理一下思路:
服务器端
1)获取(客户端请求的)需服务器端发送的文件名
2)检测这个文件在服务器上是否存在
3)检测这个文件的大小
4)把这个文件的大小发送给客户端
5)等待客户端的确认(应答)
6)打开这个文件
7)边读边发数据给客户端
8)关闭这个文件
#-*- coding:utf-8 -*- #Author:'Yang' import socket import os server=socket.socket() server.bind(('localhost',9997)) server.listen(3) print("等待新连接...") while True: conn,addr=server.accept() print('新连接:',addr) while True: print("等待指令...") cmd_data=conn.recv(1024) if not cmd_data: print("该客户端已断开") break cmd,filename=cmd_data.decode().split() print(filename) if os.path.isfile(filename): f=open(filename,'rb') file_size=os.stat(filename).st_size conn.send(str(file_size).encode('utf-8')) #发送数据大小 conn.recv(1024) #等待客户端回应 for line in f: conn.send(line) f.close() print("文件发送完毕!") server.close()
客户端
1)发送命令请求服务器端,如get filename
2)等待服务器端响应,发回该文件的大小值
3)应答服务器,获取文件大小值
4)打开这个文件
5)准备接收数据
6)关闭这个文件
#-*- coding:utf-8 -*- #Author:'Yang' import socket client=socket.socket() client.connect(('localhost',9997)) while True: cmd=input('>>:') if not cmd: continue if cmd.startswith('get'): client.send(cmd.encode()) server_response=client.recv(1024) print("服务器端响应:",server_response) #服务器端发来的数据大小值 client.send(b'ready to recv file') file_total_size=int(server_response.decode()) received_size=0 filename=cmd.split()[1] f=open(filename+'.new','wb') while received_size<file_total_size: data=client.recv(1024) received_size +=len(data) f.write(data) print(received_size,file_total_size) #打印已接收数据大小,文件总大小 else: print("文件接收完毕!") f.close()
Ok,可以试试在本机上的文件传输了,如果需要联机网间传输,记得修改IP和端口号,即可。
那么怎么知道客户端接收到的文件和服务端的文件是同一个文件?
可以用md5校验,那么下面我们就添加代码进行改进。
服务器端
1)获取(客户端请求的)需服务器端发送的文件名
2)检测这个文件在服务器上是否存在
3)检测这个文件的大小
4)把这个文件的大小发送给客户端
5)等待客户端的确认(应答)
6)打开这个文件
7)边读边发数据给客户端
8)边读边发的同时可以加md5进行校验,并将md5发送给客户端
9)关闭这个文件
#-*- coding:utf-8 -*- #Author:'Yang' import socket import os import hashlib server=socket.socket() server.bind(('localhost',9997)) server.listen(3) print("等待新连接...") while True: conn,addr=server.accept() print('新连接:',addr) while True: print("等待指令...") cmd_data=conn.recv(1024) if not cmd_data: print("该客户端已断开") break cmd,filename=cmd_data.decode().split() print(filename) if os.path.isfile(filename): f=open(filename,'rb') m=hashlib.md5() #实例化,生成一个md5对象m file_size=os.stat(filename).st_size conn.send(str(file_size).encode('utf-8')) #发送数据大小 conn.recv(1024) #等待客户端回应 for line in f: m.update(line) #更新md5 conn.send(line) print("md5校验",m.hexdigest()) f.close() #注意这里的send可能会粘包 conn.send(m.hexdigest().encode()) #发送md5 print("文件发送完毕!") server.close()
需要注意的是,上一篇socket通信(1)中我们已经了解过”粘包“,再看看上面的代码,你就不难发现,
conn.send(line)
conn.send(m.hexdigest().encode())
这两句是连着send的,很可能发生”粘包“的现象,而导致”卡住“。
客户端
1)发送命令请求服务器端,如get filename
2)等待服务器端响应,发回该文件的大小值
3)应答服务器,获取文件大小值
4)打开这个文件
5)准备接收数据
6)边接收数据边接收,进行md5加密,这里用client_md5 表示
7)关闭这个文件
8)接收服务器端发来的md5,这里用server_md5 表示
9)比较client_md5 和server_md5 是否相同。
#-*- coding:utf-8 -*- #Author:'Yang' import socket import hashlib client=socket.socket() client.connect(('localhost',9997)) while True: cmd=input('>>:') if not cmd: continue if cmd.startswith('get'): client.send(cmd.encode()) server_response=client.recv(1024) print("服务器端响应:",server_response) #服务器端发来的数据大小值 client.send(b'ready to recv file') file_total_size=int(server_response.decode()) received_size=0 filename=cmd.split()[1] f=open(filename+'.new','wb') m=hashlib.md5() #实例化一个md5对象m while received_size<file_total_size: data=client.recv(1024) received_size +=len(data) f.write(data) m.update(data) #更新md5 print(received_size,file_total_size) #打印已接收数据大小,文件总大小 else: new_file_md5=m.hexdigest() #客户端已接收文件的md5值 print("文件接收完毕!") f.close() server_file_md5=client.recv(1024) #接收服务器端发来的md5值 print("服务器的md5:",server_file_md5.decode()) print("客户端的md5:",new_file_md5)
这里我们尝试测试了两个小文件(socket_client_ftp_v2.0.py、hash_test.py)的下载:
#-*- coding:utf-8 -*- #Author:'Yang' import hashlib m=hashlib.md5() m.update(b'test') m.update(b'abc') print(m.hexdigest()) m2=hashlib.md5() m2.update(b'testabc') print(m2.hexdigest())
在客户端输入
>>:get socket_client_v2.0.py
服务器端响应: b'1296'
72 1296
96 1296
186 1296
204 1296
1228 1296
1296 1296
文件接收完毕!
服务器端的md5: 2ad667214fc2ead1679d05b7924c5c79
客户端的md5: 2ad667214fc2ead1679d05b7924c5c79
>>:get hash_test.py
服务器端响应: b'203'
235 203
文件接收完毕!
卡住了,也就是说,在下载socket_client_v2.0.py时没有粘包、而在下载hash_test.py出现了粘包(可能在你的机器上,会出现不一样的情况)。
.
怎么改进这里的粘包问题呢?
现在来理一下思路:
首先我来找到粘包是粘在哪里?
认真看看服务器端socket_server_ftp_v2.0.py 代码,可以知道,是在最后一次conn.send(line)时和conn.send(m.hexdigest())时粘包了,因为客户端socket_client_ftp_v2.0.py代码,在循环接收数据时,设置了client.recv(1024),而实际情况是最后一次循环里,接收的数据可能小于或等于1024,如果是在小于1024时,缓冲区没有满,那么后面conn.send(m.hexdigest())进来的数据,就会粘在一起。
找到了粘包的具体位置,那么就容易改进了,既然最后一次接收的数据导致的粘包,那么只要让最后一次接收的数据实际大小和缓冲区一样大,那么后面conn.send(m.hexdigest())进来的数据,就不会粘住了。
服务器端(代码不变 socket_server_ftp_v2.0.py)
客户端
#-*- coding:utf-8 -*- #Author:'Yang' import socket import hashlib client=socket.socket() client.connect(('localhost',9997)) while True: cmd=input('>>:') if not cmd: continue if cmd.startswith('get'): client.send(cmd.encode()) server_response=client.recv(1024) print("服务器端响应:",server_response) #服务器端发来的数据大小值 client.send(b'ready to recv file') file_total_size=int(server_response.decode()) received_size=0 filename=cmd.split()[1] f=open(filename+'.new','wb') m=hashlib.md5() #md5验证 while received_size<file_total_size: if file_total_size-received_size>1024:#要收不止一次 size=1024 else:#最后一次了,剩多少收多少 size=file_total_size-received_size print('最后一次接收:',size) data=client.recv(size) received_size +=len(data) f.write(data) m.update(data) #更新 print(received_size,file_total_size) #打印已接收数据大小,文件总大小 else: new_file_md5=m.hexdigest() #客户端已接收文件的md5 print("文件接收完毕!") f.close() server_file_md5=client.recv(1024) #接收服务器端发来的md5 print("服务器的md5:",server_file_md5.decode()) print("客户端的md5:",new_file_md5)
现在你可以尝试不同大小的文件下载传输了,不过由于加了md5验证,会影响下载的速度,这也没办法,总得有舍有得。
2.socketserver的使用
socketserver的主要作用是实现并发处理多个客户端请求的socket服务器端。socketserver的内部其实质是IO多路复用、多线程和多进程。
也就是说,每个客户端请求连接到服务器时,socket服务端都会在服务器端创建一个”线程“或”进程“专门处理当前客户端的所有请求。
创建一个socketserver至少分以下几步:
(使用ThreadingTCPServer)
1)第一步,必须创建一个请求处理类。这个请求处理类要继承BaseRequestHandler类,并且要重写BaseRequestHandler类的handle()方法;
2)第二步,必须实例化一个TCPServer(这里仅以常用的TCPServer举例,当然还有其他的,如UDPServer等协议),并且传递 server ip 和 1)中创建的请求处理类 给这个TCPServer,也就是说 server ip 和 1)中创建的请求处理类 以TCPServer参数的形式传进去;
3)第三步,根据 2)中的实例(假如这里的实例设定为server),进行不同处理
server.handle_request() #只处理一个请求,然后就关闭了。这个一般不用
server.serve_forever() #处理多个或一个请求,永远执行中。通常都是用这个
直接看段简单的代码,动手写一下
服务器端
#-*- coding:utf-8 -*- #Author:'Yang' import socketserver class MyTCPServer(socketserver.BaseRequestHandler): #继承BaseRequestHandler类 ''' The request handler class for our server. It is instantiated once per connection to the server,and must override the handel() method to implement communication to the client. ''' def handle(self): #跟客户端的所有交互都是在handle方法里实现的 # print(self.request,self.client_address,self.server) conn = self.request data=conn.recv(1024) print("{} wrote:".format(self.client_address[0])) print(data.decode()) conn.send(data.upper()) #发送 大写 给客户端 if __name__ == '__main__': Host,Port='localhost',9990 print("启动TCPServer[%s:%s]..." %(Host,Port)) server = socketserver.TCPServer((Host,Port),MyTCPServer) server.serve_forever()
客户端(不需要进行改变(但是注意IP和端口号),直接引用上一篇python编程 入门后学习笔记一 ——socket通信(1)中一段简单代码”socket_client_v2.0.py“)
#-*- coding:utf-8 -*- #Author:'Yang' #客户端 import socket client=socket.socket() #默认family address=AF_INET是IPV4协议 client.connect(("localhost",9990)) while True: msg=input('>>:').strip() client.send(msg.encode("utf-8")) data=client.recv(1024) #1024字节,即1K print("收到服务器端的消息:",data.decode()) client.close()
注:socket_tcpclient_v1.0.py 与 socket_client_v2.0.py唯一不同的地方是,为了方便测试,做了端口号的改变。
好了,代码就绪,启动服务器-->启动客户端-->在客户端输入
就等着看执行结果了......
>>:hello
收到服务器端的消息: HELLO
>>:world
Traceback (most recent call last):
File "C:/Users/Yang/PycharmProjects/s14/day8/SocketServer/socket_tcpclient_v1.0.py", line 12, in <module>
data=client.recv(1024) #1024字节,即1K
ConnectionAbortedError: [WinError 10053] 您的主机中的软件中止了一个已建立的连接。
服务器端仍然连接着,
但是,客户端 报错了!!!(第一次输入正常,第二次输入报错,也就是说,这个连接只进行了一次交互,连接就断开了)
为什么? 因为服务器端没有设计循环接收的代码,哈哈哈哈
改进:
#-*- coding:utf-8 -*- #Author:'Yang' import socketserver class MyTCPServer(socketserver.BaseRequestHandler): #继承BaseRequestHandler类 ''' The request handler class for our server. It is instantiated once per connection to the server,and must override the handel() method to implement communication to the client. ''' def handle(self): #跟客户端的所有交互都是在handle方法里实现的 # print(self.request,self.client_address,self.server) while True: conn = self.request data=conn.recv(1024) print("{} wrote:".format(self.client_address[0])) print(data.decode()) conn.send(data.upper()) #发送 大写 给客户端 if __name__ == '__main__': Host,Port='localhost',9990 print("启动TCPServer[%s:%s]..." %(Host,Port)) server = socketserver.TCPServer((Host,Port),MyTCPServer) server.serve_forever()
加上”while True:“循环语句,客户端就可以进行循环输入,同时服务器端循环发数据给客户端...
>>:hello 收到服务器端的消息: HELLO >>:world! 收到服务器端的消息: WORLD! >>:Let's learn。 收到服务器端的消息: LET'S LEARN。 >>:Let's learn. 收到服务器端的消息: LET'S LEARN. >>:
启动TCPServer[localhost:9990]... 127.0.0.1 wrote: hello 127.0.0.1 wrote: world! 127.0.0.1 wrote: Let's learn。 127.0.0.1 wrote: Let's learn.
还记得” socketserver的主要作用是实现并发处理多个客户端请求的socket服务器端。“这句话吗?
让我们试试,多个客户端并发处理。。。。
启动两个客户端试试,what?客户端1在操作时,客户端2仍然是排队等待中,无法工作(卡死在那里!!!),只有客户端1的连接断开(客户端1在强制断开时,有ConnectionResetError的异常抛出),客户端2的连接才能正常操作
这是怎么回事?
先来改进ConnectionResetError的异常抛出问题:既然是异常只要抓住就可以了,这个简单!
#-*- coding:utf-8 -*- #Author:'Yang' import socketserver class MyTCPServer(socketserver.BaseRequestHandler): #继承BaseRequestHandler类 ''' The request handler class for our server. It is instantiated once per connection to the server,and must override the handel() method to implement communication to the client. ''' def handle(self): #跟客户端的所有交互都是在handle方法里实现的 # print(self.request,self.client_address,self.server) while True: try: conn = self.request data=conn.recv(1024) print("{} wrote:".format(self.client_address[0])) print(data.decode()) conn.send(data.upper()) #发送 大写 给客户端 except ConnectionResetError as e: print("客户端断开:",e) break if __name__ == '__main__': Host,Port='localhost',9990 print("启动TCPServer[%s:%s]..." %(Host,Port)) server = socketserver.TCPServer((Host,Port),MyTCPServer) server.serve_forever()
再来,琢磨并发处理多个客户端,这里并没实现并发处理,原来还有个地方需要改进,这就引入了ThreadingTCPServer。
仅仅需要将
server = socketserver.TCPServer((Host,Port),MyTCPServer)
修改为
server = socketserver.ThreadingTCPServer((Host,Port),MyTCPServer)
#-*- coding:utf-8 -*- #Author:'Yang' import socketserver class MyTCPServer(socketserver.BaseRequestHandler): #继承BaseRequestHandler类 ''' The request handler class for our server. It is instantiated once per connection to the server,and must override the handel() method to implement communication to the client. ''' def handle(self): #跟客户端的所有交互都是在handle方法里实现的 # print(self.request,self.client_address,self.server) while True: try: conn = self.request data=conn.recv(1024) print("{} wrote:".format(self.client_address[0])) print(data.decode()) conn.send(data.upper()) #发送 大写 给客户端 except ConnectionResetError as e: print("客户端断开:",e) break if __name__ == '__main__': Host,Port='localhost',9990 print("启动TCPServer[%s:%s]..." %(Host,Port)) server = socketserver.ThreadingTCPServer((Host,Port),MyTCPServer) server.serve_forever()
OK,再来试试吧,怎么样?........................是不是觉得心情很好!
3.练习
开发一个支持多用户在线的FTP程序
要求:
- 用户加密认证
- 允许同时多用户登录
- 每个用户有自己的家目录 ,且只能访问自己的家目录
- 对用户进行磁盘配额,每个用户的可用空间不同
- 允许用户在ftp server上随意切换目录
- 允许用户查看当前目录下文件
- 允许上传和下载文件,保证文件一致性
- 文件传输过程中显示进度条
- 附加功能:支持文件的断点续传
浙公网安备 33010602011771号