本节主要讲解socket编程的有关知识点,顺便也会讲解一些其它的关联性知识:
一、概述(socket、socketserver):
python对于socket编程,提供了两个模块,分别是socket和socketserver,它们之间最大的区别在于,socketserver能轻松的实现并发访问,关于这个后面都会介绍的。
先说socket的编程思路:
1 #客户端创建socket并连接服务器 2 s = socket.socket() 3 ip_port = ('127.0.0.1',8089) 4 s.connect(ip_port) 5 6 ---------------------------------- 7 #服务端创建socket并等待客户端的连接 8 s = socket.socket() 9 ip_port = ('127.0.0.1',8089) 10 s.bind(ip_port) 11 s.listen(5) 12 conn,client_address = s.accept()
下面根据上面的代码做一个简单的介绍:
i. 首先我们先看客户端代码的意思(前提是先在第一行的上面导入socket模块,import socket),第一行创建一个socket对象,对于客户端来说,只要知道服务端的IP和 端口,就可以直接连接了,也就是调用connect()方法就可以了。
ii. 那么我们再来看服务端的代码,第一行也是先生成一个socket对象(在这里插入一句以前讲过的知识点,那就是方法只能通过对象去调用),然后服务端需要绑定一个IP和端口(调用bind方法实现),供客户端去连接,然后调用listen
方法,listen方法的作用是定义坚挺客户端的数量;调用accept方法才是允许客户端连接的额最后一步,它会返回一个连接的对象conn,还会返回客户端的IP地址。
iii. 这样我们就建立了客户端到服务端的初步连接,下面我们要谈的,就是连接是建立起来了,那怎么样传输数据呢?我们通过下面这个实例来进一步的讲解:
1 #客户端发送字符串的代码 2 str = 'hello' 3 s.send(bytes(str,encoding='utf8')) 4 5 ---------------------------------------- 6 7 #服务端接收字符串的代码 8 conn,client_address = s.accept() 9 recv_data = conn.recv(1024) 10 print(str(recv_data,encoding='utf8')) 11 print(recv_data.decode()) 12 13 ----------------------------------------- 14 15 #输出结果 16 hello 17 hello
解释说明:
1,客户端使用send方法发送一个字符串hello,服务端使用recv方法接受字符串。
2,重点说明的是python2.7版本可以直接发送字符串,但是在python3.0以上的版本都已经不支持这种功能了,改成了只发送和接受字节的形式,所以在你使用python3.0以上版本的时候,要先把字符串转换成字节,然后再发送。
3,不管是把字节转换成字符串,还是把字符串转换成字节,使用的编码都是utf8。
这就是整个发送和接收的流程,其实并不难,因为这就是一个发送和一个接受的过程,如果多的话,就显得有点乱了。
二,并发的实现:
如果你的代码跑在真正的生产环境中,那么就不可能只有一个用户去连接,其它用户都在等待的状态,显然,这是不现实的,那么怎么样才能做到并发处理呢?其实就是用我们上面提到的socketserver模块来实现并发处理的,下面看具体的事例:
client端:
1 import socket 2 3 s = socket.socket() 4 ip_port = ('127.0.0.1',8089) 5 s.connect(ip_port) 6 str = 'hello' 7 s.send(bytes(str,encoding='utf8'))
server端:
1 import socketserver 2 3 class Myclass(socketserver.BaseRequestHandler): 4 def handle(self): 5 recv_data = self.request.recv(1024) 6 print(recv_data.decode()) 7 8 s = socketserver.ThreadingTCPServer(('127.0.0.1',8089),Myclass) 9 s.serve_forever() 10 11 #输出结果hello
解释说明:
客户端上的代码是一样的,我们只需要看服务端的代码就可以了;首先导入socketserver模块,然后定义一个类叫Myclass,后面括号里面的意思就是继承BaseRequestHandler这个父类,其实在这里定义类的时候,应该注意两个点:
i. 必须继承上面说的那个父类(没有为啥,只是规定)。
ii. 在类里面定义一个handle方法,这个方法是python内部定义好的,在接受客户端发送的字符串的时候,也是调用的这个方法。
然后是调用这个socketserver模块的另一个类来定义调用的自己的类,和要连接的ip和端口;最后执行s.serve_forver()这个方法(其实这个模块中的类和方法所实现的原理就是判断accept客户端的时候,是否有新的链接连接进来;
如果有新的链接进来的话,socket内部会发生变化,python就是根据这种变化,来建立一个新的连接;使用for循环的方式去接收多用户的连接。)
iii. 其实并发处理用到了两个知识点,一个是IO多路复用,一个是多线程或者多线程。
在这里额外的说一下多进程和多线程,通俗点讲就是:(应用程序---》进程---》线程),其实就是这么的一层关系,但是Python中针对不同的程序用多进程或者多线程是不一样的,比如一个程序不需要大量的运算,那么就不需要
消耗太多的CPU,那么用多线程就比较合适了;如果是计算量比较大,那么在Python中应该用多进程。
三、通过搭建一个简单的ftp服务器来深入得了解socket和其它的一些小知识:
废话不多说,直接上代码,简单明了:
client端:
1 while True: 2 ftp_user = input('Input Your ftp user:') 3 ftp_pass = input('Input Your ftp pass:') 4 s.send(bytes(ftp_user,encoding='utf8')) 5 status_code = s.recv(1024) 6 status_code = str(status_code,encoding='utf8') 7 if status_code == '200': 8 s.send(bytes(ftp_pass,encoding='utf8')) 9 status_code = s.recv(2014) 10 status_code = str(status_code,encoding='utf8') 11 if status_code == '200': 12 pass 13 else: 14 print(status_code) 15 continue 16 else: 17 print(status_code) 18 continue 19 while True: 20 ftp_action = input('请输入你要执行的ftp操作>>>:').strip() 21 com_dict = {'help/?':'使用帮助','ls':'查看当前用户下的文件或目录','cd':'切换目录', 22 'put':'上传文件','get':'下载文件','mkdir':'创建目录'} 23 if ftp_action == 'q': 24 sys.exit() 25 if len(ftp_action) == 0:continue 26 cmd_list = ftp_action.split() 27 #如果输入put 文件名,就执行put 28 if cmd_list[0] == 'put': 29 abs_filepath = cmd_list[1] 30 if os.path.isfile(abs_filepath): 31 file_size = os.stat(abs_filepath).st_size 32 filename = abs_filepath.split('\')[-1] 33 print("file:%s,size:%s" %(abs_filepath,file_size)) 34 msg_data = {"action":"put","filename":filename,"file_size":file_size,"ftp_user":ftp_user} 35 s.send(bytes(json.dumps(msg_data),encoding='utf8')) 36 recv_data = s.recv(1024) 37 status_code = json.loads(str(recv_data,encoding='utf8')) 38 print(status_code) 39 if status_code['status'] == 200: 40 print('start send file:',filename) 41 f = open(abs_filepath,'rb') 42 for line in f: 43 s.send(line) 44 print('send file done') 45 else: 46 print('The file is not exist!!')
server端:
1 user_dict = json.load(open(os.path.join(settings.user_dir,'user_info'))) 2 class MyServer(socketserver.BaseRequestHandler): 3 4 def handle(self): 5 #服务端验证客户端输入的用户名和密码 6 while True: 7 ftp_user = self.request.recv(1024) 8 if ftp_user.decode() in user_dict: 9 self.request.send(bytes('200',encoding='utf8')) 10 ftp_pass = self.request.recv(1024) 11 str_ftp_pass = ftp_pass.decode() 12 md = hashlib.md5(str_ftp_pass.encode()).hexdigest() 13 if md == user_dict[ftp_user.decode()].get('passwd'): 14 self.request.send(bytes('200',encoding='utf8')) 15 current_dir = {'current_dir':'',} 16 break 17 else: 18 self.request.sendall(bytes('你输入的密码不正确!!',encoding='utf8')) 19 continue 20 else: 21 self.request.sendall(bytes('你输入的用户名不存在!!',encoding='utf8')) 22 continue 23 #循环的目的是接收客户端发送的字典信息,然后判断调用不同的方法 24 while True: 25 data = self.request.recv(1024) 26 if len(data) == 0:break 27 28 task_data = json.loads(data.decode()) 29 task_action = task_data.get('action') 30 if hasattr(self,"task_%s"%task_action): 31 func = getattr(self,"task_%s"%task_action) 32 func(task_data,current_dir) 33 #如果用户输入的是put命令,就调用此方法 34 def task_put(self,*args,**kwargs): 35 filename = args[0].get('filename') 36 file_size = args[0].get('file_size') 37 user_dic = args[0].get('ftp_user') 38 server_response = {"status":200} 39 self.request.send(bytes(json.dumps(server_response),encoding='utf8')) 40 f = open(user_dic+'\'+filename,'wb') 41 recv_size = 0 42 while recv_size < file_size: 43 data = self.request.recv(1024) 44 f.write(data) 45 recv_size += len(data) 46 print('file recv sucess') 47 f.close()
我想当大家看这段代码的时候,感觉有点乱,那我就跟大家梳理一下思路,以及传输的流程:
1、首先我先给你们说下整个程序大概的一个流程,首先客户端在执行ftp命令之前,先输入用户密码,然后把用户和加密过的密码发送到服务端,让服务端的程序去判断,客户输入的用户名或密码是否正确,然后给出相应的输出;
现在客户端可以执行ftp命令了(在这里只能执行put命令),如果客户端输入的格式正确,就把文件名、文件大小和用户名传到服务端(重点说明:把文件名传过去是想要服务器知道要传的文件,文件大小的作用是为了防止文件
粘包,粘包的结果就是传输不完整(在本实例中,为了粘包是根据客户端提供的文件大小,跟接收的文件大小做判断,只要接收的文件大小小于实际大小,就会一直传输),下面我再详细的讲解粘包的概念;传输用户名的目的是让
该用户只能访问它自己的家目录)
2、我感觉你不明白的可能有两个地方,一个是在server端定义的current_dir字典,在我所列出的代码中,没有体现出来这个字典的真正用图,其实定义它的目的是为了存放每个用户的进入子目录所存的一个标示,存在字典里,然后
用户再执行ls或者mkdir命令的时候,就会从这个字典里取出用户进入的子目录做一列的操作。
3、另外一个可能是hasattr和getattr这两个内置的函数了,hasattr(self,name):这个的意思是说,self对象里面是否包含name方法,如果有,则返回True,如果没有就返回False;
func = get(self,name): 这个和上面那个相结合,就是如果存在就把这个方法或者函数赋值给func,然后由它其传参等操作。
四、附加小知识:
1、在socket客户端和服务端互传数据的时候,如果传输一串字符串很长,那么如果还是用send的话,就可能会出现传输不完全的情况,结果这一个问题的方法是把send换成sendall就OK了!!