第九章:Python の 网络编程基础(一)
本課主題
- 何为TCP/IP协议
- 初认识什么是网络编程
- 网络编程中的 "粘包"
- 自定义 MySocket 类
- 本周作业
何为TCP/IP 协议
TCP/IP协议是主机接入互网以及接入互联网的两台机器通信的标准,是一個通信合同,比如有两台机器,A 主机 和 B 主机,它们两者之间只是根据合同上的标准来工作就可以啦。TCP/IP 有4层架构:
- 应用层
- 运输层
- 网络层
- 链路层
OSI 七层
- 物理层:
- 链路层:mac 地址,ethernet
- 网络层:是用 IP协议,意思是通过 Ip 地址把信息发出去跟别的主器沟通
- 传输层:是 tcp、udp、port:具体表示一个应用程序。
- 会话层
- 表示层
- 应用层
原地址、目标地址和数据类型,扩播只能在一个子网络中。
初认识什么是网络编程
socket 是 TCP/IP 的一个封裝,对用戶来说它只是一堆接口,socket 是一个软件抽象层,它不负责发送数据,它只帮你做数据处理,Socket有分BS和CS架构,它們本质上都是一个客户端和服务端之間的数据通信,下图说明当客户端和服务端之间进行沟通时所需要调用的方法。创建 socket.socket( ) 对象,它的基本语法是
s = socket.socket(socket_family, socket_type, protocol=0) # tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
服务端
- Socket( ):第一步创建一个 socket 对象,这是用来封装 TCP/IP的过程,之后就可以利用它来发送 TCP 或者是 UDP. e.g. s = socket.socket( )
- bind( ):第二步是绑定 IP 和端口,它接受一个元组类型的数据。e.g. s.bind(('127.0.0.1',8088,))
- listen( ):第三步是定义最多能挂起的数目,e.g. s.listen(2),意思说你当前允许一个客户端在连接,两个客户端在等待发送消息(挂起)。
- accept( ):第四步是创建客户端和服务端之间的那条连接 conn,程序在连接前会处于挂起的状态。 e.g. conn, addr = s.accept( )
#!/usr/bin/env python # -*- coding: utf-8 -*- # Author: Janice Cheng import socket ip_port = ('127.0.0.1',9999) # 这是一个元组 # 买手机 s = socket.socket() # 利用创建出来的对象来绑定: 买手机卡 # 因为这个对象是已经封装好 TCP 协议的 s.bind(ip_port) # 开机 s.listen(5) # 等待电话 # conn 服务端跟客户端连接的通讯 conn, addr = s.accept() # 每次听电话只能跟一个人通信中、然后另外条线会挂着 # 收消息 recv_data = conn.recv(1024) print("--------",type(recv_data)) # 发消息 send_data = recv_data.upper() conn.send(send_data) # 挂电话 conn.close()
#!/usr/bin/env python # -*- coding: utf-8 -*- # Author: Janice Cheng import socket ip_port = ('127.0.0.1',9999) # 这是一个元组 # 买手机 s = socket.socket() # 利用创建出来的对象来绑定: 买手机卡 # 因为这个对象是已经封装好 TCP 协议的 s.bind(ip_port) # 开机 s.listen(5) # 最大接受挂线的数目 # 等待电话 # conn 服务端跟客户端连接的通讯 conn, addr = s.accept() # 每次听电话只能跟一个人通信中、然后另外条线会挂着 while True: # 收消息 recv_data = conn.recv(1024) print("--------",type(recv_data)) if str(recv_data, encoding='utf8') == 'exit': break # 发消息 send_data = recv_data.upper() print(send_data) conn.send(send_data) # 挂电话 conn.close()
#!/usr/bin/env python # -*- coding: utf-8 -*- # Author: Janice Cheng import socket ip_port = ('127.0.0.1',9999) s = socket.socket() s.bind(ip_port) s.listen(5) while True: conn, addr = s.accept() while True: try: recv_data = conn.recv(1024) if len(recv_data) == 0: break send_data = recv_data.upper() print(send_data) conn.send(send_data) except Exception: break conn.close()
客戶端
- Socket( ):第一步创建一个 socket 对象,这是用来封装 TCP/IP的过程,之后就可以利用它来发送 TCP 或者是 UDP. e.g. s = socket.socket( )
- connect( ):第二步客户端用自己的对象来连接服务端,它接受一个元组类型的数据。e.g. s.connect(('127.0.0.1',8088,))
#!/usr/bin/env python # -*- coding: utf-8 -*- # Author: Janice Cheng import socket ip_port = ('127.0.0.1',9999) # 这是一个元组 # 找一个手机 s = socket.socket() # 拨号 s.connect(ip_port) # 发消息 send_data = input(">>: ").strip() s.send(bytes(send_data,encoding='utf-8')) # 收消息 recv_data = s.recv(1024) print(str(recv_data, encoding='utf-8')) # 挂电话 s.close()
#!/usr/bin/env python # -*- coding: utf-8 -*- # Author: Janice Cheng import socket ip_port = ('127.0.0.1',9999) s = socket.socket() s.connect(ip_port) while True: send_data = input(">>: ").strip() if len(send_data) == 0: continue s.send(bytes(send_data, encoding='utf8')) if send_data == 'exit': break recv_data = s.recv(1024) print(str(recv_data, encoding='utf8')) s.close()
運行結果
#socket_client >>: hello python HELLO PYTHON >>: hello spark HELLO SPARK >>: hello kafka HELLO KAFKA >>: #socket_server -------- <class 'bytes'> b'HELLO PYTHON' -------- <class 'bytes'> b'HELLO SPARK' -------- <class 'bytes'> b'HELLO KAFKA'
我想用几句话总结一下思路,你如果要用socket编程,需要有 Server-side 和 Client-side 的代码,它们俩用的代码都差不多,Server-side 比 Client-side 只多了一个准备的过程。
- 第一点:在写Serverside 的程序时,首先要创建s=socket.socket( )对象,然后给它绑定一个ip和端口s.bind(ip),然后定义自己最大可接收挂起的连接数目s.listen(5),这好比一个老师在讲课之前首先要准备课室和自己最大能同时教多少个学生,然后开通课室入口 conn,addr = s.accept( ),学生们登记认证好了以后,就可以跟据老师开通的连接进入课室上课了 s.connect(ip_port)。客戶端的 s.connect 就相当于服务端 conn.connect,如果其中一方断了,就会报错。
- 第二点:当成功建立连接后,下一步要考虑的就是消息的发送和接收的流程,Server-side (老师) 和 Client-side (学生) 双方都有发送和接收的功能,只不過 Server-side 是用了 conn 來完成,即 conn.send( ) 和 conn.recv(1024),但 client-side 就用它自己,即 s.send( ) 和 s.recv(1024)。
问题:s.recv(1024) 是什么意思,试说明? - 第三点:在整个 send( ) 和 recv( ) 的过程中,Server-side (老师) 和 Client-side (学生) 双方都有共通点,就是大家接受消息时都有一定的上限和原來老師是一個不慬中文的法国人,要么你直接跟他讲法语,要么你就用百度翻译一下,此刻,我选择了后者:
# client-side send_data = "hello python socket programming" s.send(bytes(send_data, encoding='utf8')) #翻译的过程 #server-side recv_data = s.recv(1024) #翻译的过程 recv_data = str(recv_data,encoding='utf8')
- 第四点:学生上课的过程中会不断的提问题和回答问题,老师也有可能提问题,这需要考虑你的情景, 想由谁开始先发消息 conn.send( )/ s.send( )
问题:send( ) 和 sendall( ) 方法有什么区别,试说明? - 第五点:问与答在一个课堂中是一个不断的循环过程 while True:
- 问题:为什么按一下回车之后程序就会卡住,试说明?
因为把 send_data调用了一个strip( )函数,所以空格没有了,变成了空值,然后下一步把空的东西发到服务端,从客户端发空消息是不会阻塞的,但服务端就卡在 conn.recv(1024) 这一步,socket 编程当中,除了accept( )会阻塞,recv( )也是会阻塞,客户端发空消息对服务端来说这相当于没有接收消息,conn.recv(1024) 变成一个在等待着接受消息的状态,形成阻塞的现象。 - 问题:为什么客户端意外地终止了程序之后,服务器端会不断地收到空消息值?
首先要知道 s.accept( ) 和s.recv( ) 是会阻塞的,意思说它们等待消息,但这是基于连接正常的情况下,因为客户端意外地终止了程序,它们之间的连接崩了,连接经崩了 s.recv( ) 就不能阻塞,在没有阻塞的情况下进入了死循环,所以就不断的打印空值。
网络编程中的 "粘包"
socket根本不负责数据真实的传输,数据真实的传输还是靠协义去做的,每个程序在接收的时候都设置了一个接收上限,e.g. conn.recv(1024),这里的 1024 就是每次與服务端沟通时接收消息的上限,客户端每次与服务端的循环过程中只接收1024字节的数据,导致进入下一个循环的时候,依旧在打印之前接收了但还没有打印成功的数据,这就是粘包问题。
粘包的解决办法是什么
有人会说增加了接收消息的字节上限,不就是简单地把问题解决了吗?有些现象可能会让你觉得问题已经解决,但实际上是没有的,因为协议在传送数据时总有一个上限。为什么程序在接收数据完毕后出现卡住的情况? 这是因为客户端不知道服务器端需要发送多少数据,阻塞在一个接受消息的阶段。粘包的解决方法是:
- 服务器端在真正发送数据之前,把这次需要发送数据的长度,先发给客户端 e.g. Ready|4096;
#服务器端 ready_tag = 'Ready|%s' %len(send_data) #Ready|4096 conn.send(bytes(ready_tag, encoding='utf8')) # 发送数据长度 #客户端 ready_tag = s.recv(1024) # 获取数据长度的字节 Ready|4096 ready_tag = str(ready_tag,encoding='utf8') if ready_tag.startswith('Ready'): #Ready|4096 msg_size=int(ready_tag.split('|')[-1]) # 获取待接收数据
- 此时,客户端可以回复服务器端:你现在可以发消息啦,我已经准备好接收消息 - Started
- 当服务器端發送了一個大於1024的消息時,客户端就可以循环接收由服务端发送的消息,如果接收到的数据和服务器端发送的数据长度是一样的话,表示已经收完了,在这个逻辑的前提下,接收多少数据也不会出现粘包问题。
自己动手写一个 ssh 交互,优雅地解决了粘包问题。
#!/usr/bin/env python # -*- coding: utf-8 -*- # Author: Janice Cheng import socket import subprocess ip_port = ('127.0.0.1',9998) # 定义元组 s = socket.socket() # 绑定协义,生成套接字 s.bind(ip_port) # 绑定 IP 端口,用来唯一标视一个进程, ip_port 必需是元组格式 s.listen(5) # 定义最大可以挂起的连接数 while True: # 用来重复接收新的连接 conn, addr = s.accept() # 接受客户端的连接请求,返还 conn (相当于一个特定的连接), addr 是客户端的 ip + port while True: # 用来基于一个连接重复收发消息 try: # 捕捉客户端的异常关闭 recv_data = conn.recv(1024) # 收消息,阻塞 if len(recv_data) == 0: break # 客户端如果退出了,服务端将收到空消息,退出 p = subprocess.Popen(str(recv_data, encoding='utf8'), shell=True, stdout=subprocess.PIPE) # 执行系统命令 res = p.stdout.read() if len(res) == '0': # 执行错误命令,标准输出为空 send_data = 'cmd err' else: send_data = str(res, encoding='gbk') # 命令执行 ok, 字节 gbk --> str --> 字节 uft8 send_data=bytes(send_data,encoding='utf8') # 为了解决粘包问题 ready_tag = 'Ready|%s' %len(send_data) # 生成 conn.send(bytes(ready_tag, encoding='utf8')) # 发送数据长度 feedback = conn.recv(1024) # Started 接收确认信息 feedback = str(feedback,encoding='utf8') if feedback.startswith('Started'): conn.send(send_data) # 发送命令的执行结果 except Exception: break conn.close()
#!/usr/bin/env python # -*- coding: utf-8 -*- # Author: Janice Cheng import socket import subprocess #执行命令模块 ip_port = ('127.0.0.1',9998) #定义元组 s = socket.socket() # 绑定协义,生成套接字 s.connect(ip_port) # 连接服务端,如果服务端已经有一个连接的话,就立即挂起 while True: #基于 s.connect() 建立的连接来循环发消息 send_data = input(">>: ").strip() if send_data == 'exit': break if len(send_data) == 0: continue s.send(bytes(send_data, encoding='utf8')) #为了解决粘包问题 ready_tag = s.recv(1024) # 获取数据长度的字节 Ready|9998 ready_tag = str(ready_tag,encoding='utf8') if ready_tag.startswith('Ready'): #Ready|9998 msg_size=int(ready_tag.split('|')[-1]) # 获取待接收数据 start_tag = 'Started' # 发送确认信息 s.send(bytes(start_tag,encoding='utf8')) # 基于已经收到的待接收数据长度,循环接收消息 recv_size = 0 recv_msg=b'' while recv_size < msg_size: recv_data = s.recv(1024) recv_msg += recv_data recv_size += len(recv_data) print("Msg Size %s Recv Size %s" % (msg_size, recv_size)) print(str(recv_msg, encoding='utf8')) s.close()
自己动手写一个 ftp 文件传送
#!/usr/bin/env python # -*- coding: utf-8 -*- # Author: Janice Cheng import socketserver import json class MySocket(socketserver.BaseRequestHandler): def handle(self): print(self.request,self.client_address,self.server) conn = self.request send_data = "Hello...May I help you?" conn.sendall(bytes(send_data,encoding='utf-8')) while True: # 用来基于一个连接重复收发消息 try: # 捕捉客户端的异常关闭 recv_data = conn.recv(1024) # 收消息,阻塞 if len(recv_data) == 0: break # 客户端如果退出了,服务端将收到空消息,退出 print('{} says: {}'.format(self.client_address, recv_data.decode())) task_data = json.loads(recv_data.decode()) task_action = task_data.get('action') if hasattr(self, "task_%s" %task_action): func = getattr(self,"task_%s" %task_action) func(task_data) else: print("task action is not supported", task_action) except Exception: break def task_put(self,*args, **kwargs): print('put',args,kwargs) file_name = args[0].get('filename') file_size = args[0].get('filesize') # response to the client side server_response = {'status':200} self.request.send(bytes(json.dumps(server_response),encoding='utf-8')) f = open(file_name,'wb') recv_size = 0 while recv_size < file_size: recv_data = self.request.recv(4096) f.write(recv_data) recv_size += len(recv_data) print("filesize: %s recv_size: %s" %(file_size,recv_size)) print("file recv success") if __name__=='__main__': ip_port = ('127.0.0.1',8088) server = socketserver.ThreadingTCPServer(ip_port,MySocket) server.serve_forever()
#!/usr/bin/env python # -*- coding: utf-8 -*- # Author: Janice Cheng import socket import os import json ip_port = ('127.0.0.1',8088) s = socket.socket() s.connect(ip_port) welcome_msg = s.recv(1024) print("from server:",welcome_msg.decode()) while True: send_data = input(">>: ").strip() if len(send_data) == 0: continue cmd_list = send_data.split() if len(cmd_list) < 2: continue task_type = cmd_list[0] if task_type == 'put': abs_filepath = cmd_list[1] if os.path.isfile(abs_filepath): file_size = os.stat(abs_filepath).st_size file_name = abs_filepath.split("/")[-1] print("file: %s size: %s" %(abs_filepath,file_size)) msg_data = {"action":"put","filename":file_name,"filesize":file_size,"md5":'1'} s.send(bytes(json.dumps(msg_data),encoding='utf-8')) # send a Ready msg to the server server_confirmation_msg = s.recv(1024) confirm_data = json.loads(server_confirmation_msg.decode()) if confirm_data['status'] == 200: print("Started sending filename:", file_name) f = open(abs_filepath, 'rb') for line in f: s.send(line) print("Send file done") else: print("File does not exists") continue else: print("doesn't support taskt type",task_type) continue # use getattr method
自定义 MySocket 类
- 自己创建一个 Socket类
- 继承一个 socketserver.BaseRequestHandler的类
- 覆盖 handle 方法
- 创建一个 socketserver 的对象,这是创建 socketserver.ThreadingTCPServer 的对象
- 把自己定义的 Socket 类传入 socketserver.ThreadingTCPServer 类中
每当服务器收到一个请求(来自客户端的连接时),就会实例化一个请求处理程序,并在实例化时调用了它的__init__(self)方法,这个方法会调用self.setup( )、 self.hanlde( )和self.finally( )。所以当我们在自定义类中只有覆盖 handle 方法,它就会自动在socketserver.ThreadingTCPServer 创建时执行它。
class BaseRequestHandler: """Base class for request handler classes. This class is instantiated for each request to be handled. The constructor sets the instance variables request, client_address and server, and then calls the handle() method. To implement a specific service, all you need to do is to derive a class which defines a handle() method. The handle() method can find the request as self.request, the client address as self.client_address, and the server (in case it needs access to per-server information) as self.server. Since a separate instance is created for each request, the handle() method can define arbitrary other instance variariables. """ def __init__(self, request, client_address, server): self.request = request self.client_address = client_address self.server = server self.setup() try: self.handle() finally: self.finish() def setup(self): pass def handle(self): pass def finish(self): pass
本周作业
作业:开发一个支持多用户在线的FTP程序
要求:
- 用户加密认证
- 允许同时多用户登录
- 每个用户有自己的家目录 ,且只能访问自己的家目录
- 对用户进行磁盘配额,每个用户的可用空间不同
- 允许用户在ftp server上随意切换目录
- 允许用户查看当前目录下文件
- 允许上传和下载文件,保证文件一致性
- 文件传输过程中显示进度条
- 附加功能:支持文件的断点续传
运行的知识点:
- socket 发送字符串
- socket 发送文件
- 客户端:1) 文件大小;2) 发消息
- 服务端:1) 接收消息 (文件大小)
- 客户端:
- 发消息
- 服务端:把数据存储成 Json 然后从客户端发消息到服务端
- 用户验证;
- 接受subprocess (默应 win>gbk 编码);
- 接收消息 (文件大小);
- 发消息;
- 断点输传:
- 5.1) a 追加、w 清空写;
- 5.2) 文件指針;
服务端
客户端
參考資料
银角大王:Python之路【第六篇】:socket
金角大王:Python之路,Day8 - Socket网络编程