Python之socket编程进阶版
1.socket之简单的ssh功能
2.socket之简单的ftp服务器
3.socketserver的用法
4.socketserver的多并发的实现
1.socket实现ssh服务
1.1我们现在Windows环境下试一下ssh的功能
1 import socket,os 2 server = socket.socket() 3 server.bind(('localhost',6969)) 4 server.listen() 5 conn, addr = server.accept() 6 while True: 7 data = conn.recv(1024) 8 if not data: 9 print("client lost!") 10 break 11 print("cmd:",data) 12 res = os.popen(data.decode()).read() 13 if len(res) == 0: 14 res = 'cmd has no output' 15 print(res) 16 conn.send(res.encode('utf-8')) 17 server.close()
1 import socket 2 client = socket.socket() 3 client.connect(('localhost',6969)) 4 while True: 5 msg = input(">>>").strip() 6 if len(msg) == 0: 7 continue 8 client.send(msg.encode("utf-8")) 9 data = client.recv(1024) 10 print(data.decode()) 11 client.close()
运行一下dir,没问题,运行一下ipconfig,貌似和跟cmd下运行的结果不一样
注意看结尾的部分,是不是少了一点,重新运行一下dir
我擦,怎么全乱了???想一想,我们在client端收的数据大小是1024,也就是说每次只能接受1024的数据。超过的部分会在一个类似buff的缓存内被阻塞等待下一次的接收。这种情况叫做“半包”。那怎么改一改?
思路:。
1.服务端在发送数据前先获得数据大小,并发送给客户端。
2.客户端先接受数据大小,再对根据数据大小接收数据。
更改后的代码
1 import socket,os 2 server = socket.socket() 3 server.bind(('localhost',6969)) 4 server.listen() 5 conn, addr = server.accept() 6 while True: 7 data = conn.recv(1024) 8 if not data: 9 print("client lost!") 10 break 11 print("cmd:",data) 12 res = os.popen(data.decode()).read() 13 data_size = len(res) 14 if data_size == 0: 15 res = 'cmd has no output' 16 conn.send(str(data_size).encode()) 17 conn.send(res.encode('GBK')) #这里在调试时先用的时UTF-8,但出现unicodeDecodeError, 18 # 改成GBK就好了,问题原因未解 19 server.close()
1 import socket 2 client = socket.socket() 3 client.connect(('localhost',6969)) 4 while True: 5 msg = input(">>>").strip() 6 if len(msg) == 0: 7 continue 8 client.send(msg.encode("utf-8")) 9 data_size = int(client.recv(1024).decode())#先获得传输的数据大小 10 received_size = 0 11 data = '' 12 while received_size <data_size: 13 data += client.recv(1024).decode('GBK') 14 received_size = len(data) 15 print(data) 16 client.close()
这里有个问题还待解:原先数据在传输的时候用的转码格式时UTF-8,但在超出1024的时候会出现UnicodeDecodeError,改用GBK就好了,不知道为什么。
现在我们把他们放在linux里试一下,报错了!
看一下服务器端,在接受数据时候891后面跟着ifconfig的内容,891是ifconfig得到的数据的size。
(这种情况在linux环境下很容易复现,3.0以上版本在windows下不太容易出现,2.7是很容易出现的。所以我把它放在linux中演示一下。)
这里就需要了解一个概念:粘包
我们看一下服务端的代码
#服务端 conn.send(str(data_size).encode()) conn.send(res.encode('GBK'))
在数据的传输中,如果有两个数据包是先后紧挨着发送时,并不是按顺序一次次的接收,很有可能在接收端第一次接受时把两个包粘在一起接收了。这就是“粘包”。
在码代码的时候,一定要防止出现这种情况。
最简单粗暴的方法,就是在两个send过程中加一个time.sleep,让缓冲区超时,在这个时间里客户端会先把第一个包取出来,第二个包再过来等着客户端的读取。可是这种思路也太low了,在对时间要求比较高的环境中也是个找骂的方法(做个交易软件,你要是敢sleep个0.5秒那可是几千万上下啊!!!)
所以改一下思路,我们让服务器端发送第一个包的时候等客户端返回个响应,再发数据包。只要两个包不先后一起发送就OK!再发个最终的代码:
1 import socket,os 2 server = socket.socket() 3 server.bind(('localhost',6969)) 4 server.listen() 5 conn, addr = server.accept() 6 while True: 7 data = conn.recv(1024) 8 if not data: 9 print("client lost!") 10 break 11 print("cmd:",data) 12 res = os.popen(data.decode()).read() 13 data_size = len(res) 14 if data_size == 0: 15 res = 'cmd has no output' 16 print(str(data_size)) 17 conn.send(str(data_size).encode()) 18 client_ack = conn.recv(1024) #等待服务器响应,服务器无响应的话就阻塞在这里 19 conn.send(res.encode('GBK')) #这里在调试时先用的时UTF-8,但出现unicodeDecodeError, 20 # 改成GBK就好了,问题原因未解 21 server.close()
1 import socket 2 client = socket.socket() 3 client.connect(('localhost',6969)) 4 while True: 5 msg = input(">>>").strip() 6 if len(msg) == 0: 7 continue 8 client.send(msg.encode("utf-8")) 9 data_size = int(client.recv(1024).decode())#先获得传输的数据大小 10 client.send('准备接收数据'.encode())#在这里给服务器发一个状态,服务器接收后开始发数据 11 received_size = 0 12 data = '' 13 while received_size <data_size: 14 data += client.recv(1024).decode('GBK') 15 received_size = len(data) 16 print(data) 17 client.close()
服务器在发送第一条指令后,用一个接收指令的代码让他阻塞住,客户端在接收到第一条指令后给服务器发送一条信息,服务器接收后再发送后面的数据。
2.下面,我们把ssh服务的代码稍微改一下,就可以做一个ftp的服务器了!!
首先,在用ftp传输文件时,有个功能要了解一下:md5,就是一个文件的数字签名,我们在传输前获得文件的md5编码,在传输后再获取编码,把两个编码对比一下可获得文件的一致性。作为文件校验的功能使用。所以这里我们要回顾一下hashlib模块里md5的用法
1 import hashlib 2 data1 = 'test1' 3 data2 = 'test2' 4 data ='test1test2' 5 m1 = hashlib.md5() 6 m2 = hashlib.md5() 7 m1.update(data1.encode())#先对data1进行编码,获得第一个字符串的md5文件 8 m1.update(data2.encode())#在第一个文件的MD5上直接对第二个字符串编码 9 m2.update(data.encode())#直接字符串进行编码 10 print('m1:',m1.hexdigest())
m1: beff3fcba56f29677c5d52b843df365e
m2: beff3fcba56f29677c5d52b843df365e
我们在对文件进行MD5编码时候,直接把文件用readlines读出来是不大现实的(数据是先读在内存中,小文件没问题,超过几个G的文件就把内存撑爆了)所以需要用readline把数据一行行读出来,然后不停用update把MD5值更新出来。
其次,我们来捋一下ftp server端的工作顺序
1.读取文件名
2.检测文件是否存在
3.打开文件
4.检测文件大小并发送给客户端
5.发送文件
6.等待客户端确认
7.发送md5给客户端。
客户端的工作流程
1.输入指令
2.对指令解耦并发送给服务器
3.接收文件大小
4.建立新的空白文件
5.逐行接收文件并生成相应的MD5
6.接收原文件的MD5,和新文件进行校验
7.关闭文件
下面是代码
1 import socket 2 client = socket.socket() 3 client.connect(('localhost',6969)) 4 while True: 5 msg = input(">>>").strip() 6 if len(msg) == 0: 7 continue 8 client.send(msg.encode("utf-8")) 9 data_size = int(client.recv(1024).decode())#先获得传输的数据大小 10 received_size = 0 11 data = '' 12 while received_size <data_size: 13 data += client.recv(1024).decode('GBK') 14 received_size = len(data) 15 print(data) 16 client.close()
1 #Author__Aaron 2 import socket,hashlib 3 client = socket.socket() 4 client.connect(("localhost",9696)) 5 while True: 6 cmd = input('cmd:') 7 if len(cmd) == 0: 8 continue 9 if cmd.startswith("get"): 10 m = hashlib.md5() 11 client.send(cmd.encode()) 12 totle_size = client.recv(1024).decode() 13 totle_size = int(totle_size) 14 print("文件大小:",totle_size) 15 client.send(b"ready to recv file") 16 reveived_size = 0 17 cmd = cmd.split()[1] 18 filename = cmd.split(".")[0]+"_new."+cmd.split(".")[1]#新文件名是原文件名+‘new’ 19 f = open(filename,"wb") 20 while reveived_size < totle_size: 21 if totle_size - reveived_size > 1024: 22 get_size = 1024 #防止粘包 23 else: 24 get_size = totle_size-reveived_size 25 data = client.recv(get_size) 26 f.write(data) 27 m.update(data) 28 reveived_size += len(data) 29 else: 30 print("received done") 31 f.close() 32 file_md5 = client.recv(1024).decode() 33 new_file_md5 = m.hexdigest() 34 if file_md5 == new_file_md5: 35 print("文件校验正确,md5=%s,文件接受完毕!"%file_md5) 36 else: 37 print("文件校验失败") 38 print("源文件校验码:",file_md5) 39 print("新文件校验码:",new_file_md5)
客户端在接收数据防止粘包的地方做了些小改进:接收数据量用变量来处理,最后一个循环直接剩下的数据量,前期变量值都是1024。
这里还存在一些问题待改进:
1.在服务器端只判定了有没有文件的存在,如果有能正常工作,如果没有的话就会卡死。
2.客户端接收MD5后只进行校验,如果错误只提示错误并无相应的处理。
3.只做了数据的单项传输(get),其实还应该有从客户端上传数据给服务器(put)。
留在后面解决把!
3.socketserver模块的用法
https://docs.python.org/2/library/socketserver.html#SocketServer.BaseServer.handle_request
socketserver模块有四个类
1.TCPServer
class socketserver.TCPServer(server_address,RequestHanddlerClass,bind_and_active=True)
2UDPServer
class socketserver.UDPServer(server_address,RequestHanddlerClass,bind_and_active=True)
3. UnixStreamServer,类似于TCPServer提供面向数据流的套接字连接,但是旨在UNIX平台上可用;
4. UnixDatagramServer,类似于UDPServer提供面向数据报的套接字连接,但是旨在UNIX平台上可用;
其中常用的是前两个。
+------------+
| BaseServer |
+------------+
|
v
+-----------+ +------------------+
| TCPServer |------->| UnixStreamServer |
+-----------+ +------------------+
|
v
+-----------+ +--------------------+
| UDPServer |------->| UnixDatagramServer |
+-----------+ +--------------------+
这里引用一下python官网上对SocketServer使用方法的描述
Creating a server requires several steps.
Frist,you must creat a request handler class by subclassing the BaseRequestHandler class and overriding its Handle()
method;this method will process incoming requests.
Second,you must instantiate one of the server classes,passing it the server's address and the request handler class.
Then call the handle_request() or serv_forever() method of the server object to process ond or many requests.
Finally,call server_close() to close the socket
首先,必须创建一个请求处理类,并且这个类要继承BaseRequestHandler,并且还要重构父类里的handle()
handle里处理所有和客户端的交互
其次,要实例化一个server class(四个中的一个,举例TCPServer),并且把server的ip和第一步创建的请求处理类当作参数传递给TCPServer。
接下来可以调用server.handle_request()(只处理一个请求)或server.server_forever()(处理多个请求,永远执行)
最终,调用server_close()关闭。
现在我们写一个最简单的socket server
1 import socketserver 2 class MyTCPHandler(socketserver.BaseRequestHandler):#继承BaseRequestHandler类。 3 def handle(self): #重构父类里的handle() 4 self.data = self.request.recv(1024).strip() 5 print('{}wrote:'.format(self.client_address[0])) 6 print(self.data) 7 self.request.send(self.data.upper()) #把接收的数据upper后返送 8 9 if __name__ == '__main__': 10 HOST,PORT = 'localhost',9999 #定义IP和端口 11 server = socketserver.TCPServer((HOST,PORT),MyTCPHandler) #对建立的类实例化,并传递IP和请求的类 12 server.serve_forever()
客户端用最基础的就可以
1 import socket 2 client = socket.socket() 3 client.connect(('localhost',9999)) 4 while True: 5 msg = input('>>>') 6 if len(msg) == 0: 7 continue 8 client.send(msg.encode()) 9 data = client.recv(1024).decode() 10 print(data)
执行,发现第一次发送,正常,第二次就报错了
原因就是handle里处理完一次就结束了,客户端就断开了。所以要在handle里加while。
1 import socketserver 2 class MyTCPHandler(socketserver.BaseRequestHandler):#继承BaseRequestHandler类。 3 def handle(self): #重构父类里的handle() 4 while True: 5 try: 6 self.data = self.request.recv(1024).strip() 7 print('{}wrote:'.format(self.client_address[0])) 8 print(self.data) 9 self.request.send(self.data.upper()) #把接收的数据upper后返送 10 except ConnectionResetError as e:#在这里处理了客户端掉线 11 print('err',e) 12 break 13 14 if __name__ == '__main__': 15 HOST,PORT = 'localhost',9999 #定义IP和端口 16 server = socketserver.TCPServer((HOST,PORT),MyTCPHandler) #对建立的类实例化,并传递IP和请求的类 17 server.serve_forever()
最后,我们试一下socketserver的多并发
1 import socketserver 2 class MyTCPHandler(socketserver.BaseRequestHandler):#继承BaseRequestHandler类。 3 def handle(self): #重构父类里的handle() 4 while True: 5 try: 6 self.data = self.request.recv(1024).strip() 7 print('{}wrote:'.format(self.client_address[0])) 8 print(self.data) 9 self.request.send(self.data.upper()) #把接收的数据upper后返送 10 except ConnectionResetError as e:#在这里处理了客户端掉线 11 print('err',e) 12 break 13 14 if __name__ == '__main__': 15 HOST,PORT = 'localhost',9999 #定义IP和端口 16 server = socketserver.ThreadingTCPServer((HOST,PORT),MyTCPHandler) #对建立的类实例化,并传递IP和请求的类 17 server.serve_forever()
把TCPServer改成ThreadingTCPServer,就实现了多并发。可以多开几个客户端试一下
可以看到服务器端可以同时接收多个客户端的信息。