Python网络编程
6.1、网编先导知识
6.1.1、网络应用开发架构
-
C / S即client(客户端) / server(服务端)
飞秋、Git、百度云、输入法....
-
B / S即browser(浏览器)/ server(服务器)
淘宝、邮箱、百度、知乎....
-
B / S是特殊的C / S架构
6.1.2、网卡:每个实际存在在计算机硬件里面的
6.1.3、mac地址:每块网卡上都有一个全球唯一的mac地址
6.1.4、交换机:是连接多台机器并帮助通讯的物理设备,只可以识别mac地址
6.1.5、协议:两台物理设备之间对于要发送的内容,长度和顺序做的一些规范
6.1.6、ip地址(规格)
-
ipv4协议:韦德点分十进制,32位的二进制
范围:0.0.0.0~255.255.255.255
-
ipv6协议:8位的冒分十六制,128位十进制来表示 (点分十进制不足以表示)
范围:0:0:0:0:0:0:0:0 ~ FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF
6.1.7、ip地址的使用:
-
如果一个ip地址向北所有人都看到,这个ip地址是必须要申请的,即公网ip
-
提供内网ip,供局域网使用:
192.168.0.0 - 192.168.255.255
172.16.0.0 - 172.31.255.255
10.0.0.0 - 10.255.255.255
6.1.8、交换机实现的ARP协议(地址解析协议)
- 通过ip地址获取一台机器的mac地址
- 交换机工作原理:收到一个请求,通知所有连接他的ip地址,获取对应的ip地址的mac地址并返回给请求的ip地址
6.1.9、网关ip:一个局域网的网络出口,访问局域网以外的区域都需要经过路由器的网关
6.1.10、网段:一般是一个末尾为0的地址段
6.1.11、子网掩码:判断两个机器是否在同一个网段
屏蔽一个IP地址的网络部分的“全1”比特模式。对于A类地址来说,默认的子网掩码是255.0.0.0;对于B类地址来说默认的子网掩码是255.255.0.0;对于C类地址来说默认的子网掩码是255.255.255.0
# 示例:通过将请求的计算的子网掩码和两个要匹配的计算机的二进制按位与运算
ip = 255.255.255.255
11111111.11111111.11111111.11111111
192.168.12.87
11000000.10101000.00001100.00000111
192.168.12.7
6.1.12、ip地址可以确认一台机器,port端口可以确认一台机器的一个应用,端口的范围:0~65535
6.1.13、一般实现互联,使用127.0.0.1,是自己的地址,不过减缓及ip地址是可以过交换机的。
6.2、ISO模型(五层结构)
6.2.1、物理层
- 物理层是ISO模型的最底层,负责网络设备在各种物理介质上传输比特流,并规定各种各种物理传输介质、接口的机械特性和电气特性。一般用位表示。
6.2.2、数据链路层
- mac地址,ARP协议 物理设备:网卡,交换机。
6.2.3、网络层
- IPV4/IPV6协议,物理设备:路由器,三层交换机(交换机具有路由功能)ip通过DNS解析获取(DNS域名和ip互相映射的数据库)
6.2.4、传输层
- tcp协议和udp协议,物理设备:端口,四层路由器,四层交换机。
6.2.5、应用层
- 应用层 :https/http/ftp/smtp协议 所有的应用层协议是基于tcp或者是udp协议
6.3、传输层的两种协议(tcp/udp)
6.3.1、tcp协议
-
特点
- 面向连接的,可靠,但是慢,可以实现全双工通信,即双方都是实时的,区别于半双工(传呼机)
- 无边界,流式传输(导致粘包问题)
- 长连接:会一直占用双方的端口
- 能够传输的数据长度几乎没有限制
-
三次握手和四次挥手
-
具体的三次握手(连接方式):
# accept接受过程中等待客户端的连接 # connect客户端发起了一个syn连接请求(附带了一个随机数) # 如果得到了server端的响应ack的同时还会再收到一个由server端发来的syn链接请求 # client端进行回复ack之后,就建立起了一个tcp协议的链接 # 备注:三次握手的过程再代码中是由accept和connect共同完成的,具体的细节再socket中没有体现出来
-
具体的四次挥手(断开连接方式):
# server和client端对应的在代码中都有close方法 # 每一端发起的close操作都是一次fin的断开请求,得到'断开确认ack'之后,就可以结束一端的数据发送 # 如果两端都发起close,那么就是两次请求和两次回复,一共是四次操作 # 可以结束两端的数据发送,表示链接断开了
-
-
应用场景
- QQ和微信等上面的传输压缩文件,缓存下载的电影等等
6.3.2、udp协议
-
特点
- 面向数据报的,无连接,速度很快,类似于发短信,能实现一对一,多对一,一对多的高效通讯
- 由于没有回执,对于发送信息和接受信息的双方来说,可能存在丢失消息的情况
- 能够传递的长度有限,是根据数据传递设备的位置有关系
-
应用场景
- 通信类的 如QQ 微信,发短信, 在线观看小视频等
6.4、套接字
6.4.1、基于tcp协议实现套接字
-
简单小示例
-
服务端
import socket # 1.买电话 phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 流式协议 --->tcp协议 # 2.绑定电话卡 phone.bind(('127.0.0.1', 8080)) # 端口范围0-65535,1024前的被操作系统保留 # 3.开机 phone.listen(5) # 5指的是半连接池接受的最大连接请求 print('服务端开始启动了!') # 4.等待电话连接请求:拿到电话连接conn conn, client_addr = phone.accept() print(conn) print('获取到的电话连接是:',client_addr) # 5.通信:收\发消息 data = conn.recv(1024) # 最大接收的数据量是1024Bytes,接收到的是Bytes类型 print(data.decode('utf-8')) # 解码操作 conn.send(data.upper()) # 6.关闭电话连接conn(必选的回收资源操作) conn.close() # # 7.关机(可选操作) phone.close()
-
客户端
import socket # 1.买手机 phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 2.拨通服务端电话 phone.connect(('127.0.0.1',8080)) # 流式协议 --->tcp协议 # 3.通信 import time time.sleep(10) phone.send('hello world'.encode('utf-8')) data = phone.recv(1024) print(data.decode('utf-8')) # 解码 # 4.关闭连接(必选的回收资源的操作) phone.close()
-
-
循环通信+修复bug
-
服务端
# 服务端的特点 # 1.一直提供服务 # 2.并发地提供服务 import socket # 1.买电话 phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 流式协议 --->tcp协议 # 2.绑定电话卡 phone.bind(('127.0.0.1', 8081)) # 端口范围0-65535,1024前的被操作系统保留 # 3.开机 phone.listen(5) # 5指的是半连接池接受的最大连接请求 print('服务端开始启动了!') print('连接请求是:{},端口是:{}'.format('127.0.0.1', 8080)) # 4.等待电话连接请求:拿到电话连接conn while True: # 循环连接 conn, client_addr = phone.accept() while True: # 5.通信:收\发消息 try: data = conn.recv(1024) # 最大接收的数据量是1024Bytes,接收到的是Bytes类型 print(data.decode('utf-8')) # 解码操作 conn.send(data.upper()) except Exception as e: break # 6.关闭电话连接conn(必选的回收资源操作) conn.close() # # 7.关机(可选操作) phone.close()
-
客户端
import socket # 1.买手机 phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 2.拨通服务端电话 phone.connect(('127.0.0.1',8081)) # 流式协议 --->tcp协议 # 3.通信 while True: content = input('请输入>>>>:').strip() if len(content) == 0: continue phone.send(content.encode('utf-8')) data = phone.recv(1024) print(data.decode('utf-8')) # 解码 # 4.关闭连接(必选的回收资源的操作) # phone.close()
-
6.4.2、基于udp协议实现套接字
-
简单小示例
-
服务端
import socket # 1.实例化一个数据报对象,基于udp协议(SOCK_DGRAM) server = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) # 2.绑定固定的ip和端口号,与客户端的一致 server.bind(('127.0.0.1',8080)) print('服务开始了:') while True: # 3.接受客户端发送过来的数据,得到的是数据和客户端的ip和端口号 data,client_addr = server.recvfrom(1024) # 4.解码打印 print(data.decode('utf-8').upper()) print(client_addr) # 5.将客户端需要的数据传输回去,加上客户端的ip和端口号(重点) server.sendto(data.upper(),client_addr) server.close()
-
客户端
import socket # 1.实例化一个数据报的对象,基于udp协议(SOCK_DGRAM) client = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) while True: # 2.输入发送的数据 content = input('>>>>>:').strip() # 3.发送数据,发送内容+服务器ip和端口号 client.sendto(content.encode('utf-8'),('127.0.0.1',8080)) # 4.接受服务端发送过来的数据,接受的数据(Bytes类型)+客户端的ip和端口号 data,server_addr = client.recvfrom(1024) # 解码打印 print(data.decode('utf-8')) print(server_addr) client.close()
-
6.5、tcp的粘包问题
6.5.1、原因概述
- 出现原因:dcp协议本身的工作原理导致,发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。
- 传输数据过小,当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间,看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去。即合包机制
- 传输数据过大,当我们提交一段数据给TCP发送时,TCP选择进行拆包(不然无法发送,最大发送长度1500),然后逐个发送,即拆包机制
- dcp协议本身的特性:无边界和流式传输,导致合包机制和拆包机制
- 流式传输:传递过程中通过交换机,dcp协议本身无边界,但是在内部发送有拆解,拆解后逐步发送,导致接受有滞后,即接收方已经读取数据,还有数据在传输过程无法被读取,表象是接受到的大文件 会比发送方的文件略小。
- 有固定的准确的接受长度时,不会出现粘包问题,如发送4字节,收4字节。
6.5.2、解决方法
-
发送长度:发送每条数据的时候,将数据的长度一并发送,比如可以选择每条数据的前4位是数据的长度,应用层处理时可以根据长度来判断每条数据的开始和结束。方法就是通过struct方法
- 即自定义协议
- 首先发送报头,报头固定是4个字节
- 报头内容是即将发送的报文的字节长度
- 之后接收端按照字节长度接受报文
-
第二种方法就是格式化数据:每条数据有固定的格式(开始符、结束符),这种方法简单易行,但选择开始符和结束符的时候一定要注意每条数据的内部一定不能出现开始符或结束符;(老师没讲)
6.5.3、示例
-
一般方法解决粘包问题
-
服务端
import socket import struct import subprocess server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) server.bind(('127.0.0.1',8080)) server.listen(5) # 服务端应该做两件事: # 1.循环地从半连接池中取出连接请求与其立马建立双向连接,拿到连接对象 while True: conn,client_addr = server.accept() # 2.拿到连接对象,与其进行循环通信 while True: try: cmd = conn.recv(1024) if len(cmd) == 0:break obj = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) stdout_res = obj.stdout.read() stderr_res = obj.stderr.read() total_size = len(stdout_res)+len(stderr_res) # 先发头信息(固定能够长度的Bytes):对数据描述信息 # int-->固定长度的Bytes header = struct.pack('i',total_size) conn.send(header) # 再发真实数据 conn.send(stdout_res) conn.send(stderr_res) except Exception: break conn.close()
-
客户端
import socket import struct ''' 1.先拿到服务端传输过来的数据大小 2.做一个循环接收数据 ''' client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(('127.0.0.1',8080)) while True: cmd = input('请输入命令>>>>:').strip() if len(cmd) == 0:continue client.send(cmd.encode('utf-8')) # 解决粘包问题思路 # 一、先固定长度的头:解析出数据的描述信息,包括数据的总大小total header = client.recv(4) total_size = struct.unpack('i',header)[0] # 二、根据解析出的描述信息,接受真实的数据 # recv_size=0,循环接收,每接受一次,recv_size+=接收长度 # 直到recv_size=total_size,结束循环 recv_size = 0 while recv_size < total_size: recv_data = client.recv(1024) recv_size += len(recv_data) print(recv_data.decode('gbk'),end='') else: print()
-
-
终极方法解决粘包问题
-
服务端
import socket import struct import json import subprocess server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind(('127.0.0.1', 8080)) server.listen(5) # 服务端应该做两件事: # 1.循环地从半连接池中取出连接请求与其立马建立双向连接,拿到连接对象 while True: conn, client_addr = server.accept() # 2.拿到连接对象,与其进行循环通信 while True: try: cmd = conn.recv(1024) if len(cmd) == 0: break obj = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) stdout_res = obj.stdout.read() stderr_res = obj.stderr.read() total_size = len(stdout_res) + len(stderr_res) # 1.制作头部信息 header_dic = { 'filename': 'a.txt', 'total_size': total_size, 'md5': '12387459560yhsdjsncvnchfuuiokwjdndb' } json_str = json.dumps(header_dic) json_str_bytes = json_str.encode('utf-8') # 2.先把头信息的长度发出去 x = struct.pack('i', len(json_str_bytes)) conn.send(x) # 3.发头信息 conn.send(json_str_bytes) # 4.再发真实数据 conn.send(stdout_res) conn.send(stderr_res) except Exception: break conn.close()
-
客户端
import json import socket import struct ''' 1.先拿到服务端传输过来的数据大小 2.做一个循环接收数据 ''' client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect(('127.0.0.1', 8080)) while True: cmd = input('请输入命令>>>>:').strip() if len(cmd) == 0: continue client.send(cmd.encode('utf-8')) # 接收端 # 1.先收4个字节,从中提取接下来要收的头的长度 x = client.recv(4) header_len = struct.unpack('i', x)[0] # 2.接收头,并解析 json_str_bytes = client.recv(header_len) # json_str_bytes = b'{"filename": "a.txt", "total_size": 401, "md5": "12387459560yhsdjsncvnchfuuiokwjdndb"}' # print(json_str_bytes.decode(encoding="utf-8", errors="strict")) json_str = json_str_bytes.decode('utf-8') header_dic = json.loads(json_str) print(header_dic) total_size = header_dic.get('total_size') # 3.接收真实数据 recv_size = 0 while recv_size < total_size: recv_data = client.recv(1024) recv_size += len(recv_data) print(recv_data.decode('gbk'), end='') else: print() # 粘包问题出现的原因 # 1、tcp是流式协议,数据像水流一样粘在一起,没有任何边界区分 # 2、收数据没收干净,有残留,就会下一次结果混淆在一起 # 解决的核心法门就是:每次都收干净,不要任何残留
-
6.6、socketserver实现多线程
6.6.1、基于tcp协议实现
-
服务端
import socketserver class MyRequestHandle(socketserver.BaseRequestHandler): def handle(self): # 如果tcp协议,self.request=>conn print(self.request) # 这相当于conn print(self.client_address) # 这是客户端的ip和端口 while True: try: msg = self.request.recv(1024) if len(msg) == 0: break self.request.send(msg.upper()) except Exception: break self.request.close() # 服务端应该做两件事 # 第一件事:循环地从半连接池中取出链接请求与其建立双向链接,拿到链接对象 s=socketserver.ThreadingTCPServer(('127.0.0.1',8889),MyRequestHandle) s.serve_forever() # 等同于 # while True: # conn,client_addr=server.accept() # 启动一个线程(conn,client_addr) # 第二件事:拿到链接对象,与其进行通信循环===>handle
-
客户端
from socket import * client=socket(AF_INET,SOCK_STREAM) client.connect(('127.0.0.1',8889)) while True: msg=input('请输入命令>>:').strip() if len(msg) == 0:continue client.send(msg.encode('utf-8')) res=client.recv(1024) print(res.decode('utf-8'))
6.6.2、基于udp协议实现
-
服务端
import socketserver class MyRequestHanlde(socketserver.BaseRequestHandler): def handle(self): client_data=self.request[0] # 这是客户端传来的数据 server=self.request[1] # 这是服务端的发送接收接口 client_address=self.client_address # 这是客户端的IP地址和端口 print('客户端发来的数据%s' %client_data) server.sendto(client_data.upper(),client_address) s=socketserver.ThreadingUDPServer(("127.0.0.1",8888),MyRequestHanlde) s.serve_forever() # 相当于:只负责循环地收 # while True: # data,client_addr=server.recvfrom(1024) # 启动一个线程处理后续的事情(data,client_addr)
-
客户端
import socket client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) # 流式协议=》tcp协议 while True: msg=input('>>>: ').strip() client.sendto(msg.encode('utf-8'),('115.29.65.16',8888)) res=client.recvfrom(1024) print(res) client.close()