1.什么是软件开发架构
编写项目之前需要遵循的代码层面上的规范。
架构总体发展趋势一致为统一接口原则。(与三层架构其实一样。)
2.软件开发架构的分类
进行软件开发时候,通常会在两种基本架构中进行选择,即C/S架构和B/S架构。
C/S架构:C/S架构是客户端服务端交互模式,是Client与Server的简称。它是早期常用的一种软件架构,这种架构的软件需要在用户的电脑上安装客户端程序,如图:
B/S架构:B/S架构是浏览器/服务器交互模式,是Browser/Server的简称。它是目前最常用的一种软件架构,这总架构的软件不需要在用户的电脑上安装任何客户端程序,只需要在用户的电脑上安装浏览器即可。用户可以使用浏览器通过web服务器和数据库做交互,交互的结果将会以网页的形式显示在浏览器端。如图:
3.三种架构的优缺点
- cs架构:
优势:下载对应的客户端,可以在客户端软件内高度定制相关服务。
劣势:使用必须先下载客户端,比较繁琐。- bs架构:
优势:不需要下载客户端,能够快速体验服务。
劣势:定制花里胡哨的功能较为繁琐。
1.网络编程简介
(1)理解:基于互联网编写代码,程序可以实现远程数据交互。
(2)目的:网络编程的本质是为了解决计算机之间远程数据交互。
(3)意义:学习网络编程之后,可以编写一个C/S架构的软件。
(4)要求:计算机之间要想实现远程数据交互,首要条件就是要有物理连接介质。
OSI是一个开放性的通信系统互连参考模型,它是一个定义的非常好的协议规范。
各层功能
- 应用层
与其他计算机进行通讯的一个应用,它是对应应用程序的通信服务的。- 表示层
这一层的主要功能是定义数据格式及加密。- 会话层
它定义了如何开始,控制和结束一个会话,包括对多个双向消息的控制和管理,以便在只完成连续消息的一部分时可以通知应用,从而表示层看到的数据是连续的,在这种情况下,如果表示层收到了所有的数据,则用数据代表表示层。示例:RPC,SQL等。- 传输层
这层的功能包括是选择差错恢复协议还是无差错恢复协议,及在同一主机上对不同应用的数据流的输入进行复用,还包括对收到的顺序不对的数据包的重新排序功能。示例:TCP,UDP,SPX,PORT。PORT协议(端口协议) 用于标识一台计算机上面正在运行的应用程序(端口号类似于手牌号) 端口号范围是:0~65535 0~1024:系统经常使用的端口号; 1024~8000:常用软件的端口号
- 1
- 2
- 3
- 4
- 5
针对端口号我们要注意:写项目的话推荐使用8000之后的端口号。 1.同一时间同一台计算机上面端口号不能冲突; 2.端口号是动态分配的;
- 1
- 2
- 3
IP地址:用于标识全世界独一无二的一台接入互联网的计算机。 PORT号:用于标识一台计算机上面的某一个应用程序。 IP+PORT:能够标识全世界独一无二的一台计算机上面的某一个应用程序。
- 1
- 2
- 3
- 网络层
这层对端到端的包传输进行定义,它定义了能够标识所有结点的逻辑地址,还定义了路由实现的方式和学习的方式。为了适应最大传输单元长度小于包长度的传输介质,网络层还定义了如何将一个包分解成更小的包的分段方法、示例:IP,IPX等。IP协议: 规定了每台接入互联网的计算机都必须要有一个IP地址(取决于网线) IP地址目前有两个版本: (1)IPV4:点分十进制 最小:0.0.0.0 最大:255.255.255.255 (2)IPV6:十六进制 ```IPV6可以标识地球上每一粒啥子```
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 数据链路层
它定义了在单个链路上如何传输数据。这些协议与被讨论的各种介质有关。1.规定了二进制数据的分组方式; 2.规定了‘以太网协议’; 但凡能够接入互联网的计算机都必须有一块网卡; 每块网卡在出厂的时候都会被烧制一个全世界独一无二的编号; 该编号是由12位16进制数组成; 前六位是产商编号; 后六位是流水线号; 这个独一无二的编号也有一个称呼>>>:'mac地址(类似于身份证号)' 基于mac地址就可以实现计算机之间的数据通信了!!!
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 物理连接层
为数据端设备提供传送数据的通路,数据通路可以是一个物理媒体,也可以是多个物理媒体连接而成。一次完整的数据传输,包括激活物理连接,传送数据,终止物理连接.所谓激活,就是不管有多少物理媒体参与,都要在通信的两个数据终端设备间连接起来,形成一条通路。
1.交换机
能够让接入交换机的多台计算机实现彼此互联。
实际上,交换机有时被称为多 端口网桥。网络交换机,是一个扩大网络的器材,能为子网络中提供更多的连接端口,以便连接更多的计算机。
2.以太网通信(mac通信)
原理
有了交换机之后 根据电脑的mac地址就可以实现数据交互。
广播:先在交换机中吼 所有接入交换机的设备都能收到;
单播:只有被查找设备 才会回复相应信息。
缺陷
1.mac地址通信仅限于局域网
2.接入交换机的设备过多 可能会造成广播风暴
广播风暴:类似于所有人同时吼!!!
3,局域网
有某个固定区域组成的网络;
局域网,LAN(Local Area Network)是指在某一区域内(如一个学校、工厂和机关内)由多台计算机互联成的计算机组。一般是方圆几千米以内。将各种计算机,外部设备和数据库等互相联接起来组成的计算机通信网。
4,广域网
广域网可以看成是更大区域的局域网;
广域网,WAN(Wide Area Network)也称远程网。通常跨接很大的物理范围,所覆盖的范围从几十公里到几千公里,它能连接多个城市或国家,或横跨几个洲并能提供远距离通信,形成国际性的远程网络。
5.路由器
将多个局域网连接到一起的设备。
路由器又可以称之为网关设备。路由器就是在OSI/RM中完成的网络层中继以及第三层中继任务,对不同的网络之间的数据包进行存储、分组转发处理,其主要就是在不同的逻辑分开网络。
1.TCP协议:
(1)三次握手:
建立双向通道
洪水攻击——同时让大量的客户朝服务端发送建立TCP连接的请求、
(2)四次挥手
断开双向通道
特别注意:中间两步不可以合并,需要有检查的时间。
(3)基于TCP传输数据非常安全,因为有双向通道这句话是否正确?
答:不是因为有双向通道传输数据才安全,基于TCP传输数据,数据不易丢失,其原因在于二次确认机制。
每次发送数据都需要返回确认消息,否则在一定的时间会反复发送。
2.UDP协议
基于UDP协议发送数据,没有任何的通道也没有任何的机制;
UDP发送数据没有TCP安全(没有二次确认机制)
1.套接字
基于文件类型的套接字家族。
套接字家族的名字:AF_UNIX
基于网络类型的套接字家族。
套接字家族的名字:AF_INEF
2.代码实现
(1)服务端import socket # 创建一个socket对象 server = socket.socket() # 括号内什么都不写,默认就是基于网络的TCP套接字 # 绑定一个固定的地址(ip\port) server.bind(('127.0.0.1', 8088)) # 127.0.0.1本地回环地址(只允许自己的机器访问) # 半连接池 server.listen(5) sock,address = server.accept() print(sock,address) # sock是双向通道,address是客户端地址 # 数据交互 sock.send(b'hello word') # 朝客户发送数据 data = sock.recv(1024) # 接收客户端发送的数据 1024bytes print(data) # 断开连接 sock.close() # 断链接 server.close() # 关机
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
(2)客户端import socket # 产生一个socker对象 client = socket.socket() # 连接服务器(拼接服务端的ip和port) client.connect(('127.0.0.1',8088)) # 数据交互 data = client.recv(1024) # 接收服务端发送的数据 print(data) client.send(b'hello python') # 朝服务端发送数据 # 关闭 client.close()
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
3.实例展示之代码优化处理
(1)send与recv
客户端与服务端不能同时执行同一个
有一个接收另外一个就是发送;
有一个发送另外一个就是接收。
不能实现同时接收或者同时发送。
(2)消息自定义
input获取用户数据即可。
(3)循环通信
给数据交互环节添加循环即可。
(4)服务端能够持续提供服务
要不会因为客户端断开而报错:
解决方法:异常捕获 一旦客户端断开连接 服务端结束通信循环 调到连接处等待
(5)消息不可以为空
判断是否为空 如果是则重新输入(主要针对客户端)
针对mac本:
(6)服务端频繁重启可能会报端口被占用的错(主要针对mac电脑)
from socket import SOL_SOCKET,SO_REUSEADDR
server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
(7)客户端异常退出会发送空消息(针对mac linux)
针对接收的消息加判断处理即可
(8)代码展示
# 服务端:
import socket
# 创建一个socket对象
server = socket.socket() # 括号内什么都不写,默认就是基于网络的TCP套接字
# 绑定一个固定的地址(ip\port)
server.bind(('127.0.0.1', 8088)) # 127.0.0.1本地回环地址(只允许自己的机器访问)
# 半连接池(暂且忽略)
server.listen(5)
# 开业,等待接客
while True:
sock, address = server.accept()
print(sock, address) # sock是双向通道,address是客户端地址
# 数据交互
while True:
try:
msg = input('请输入发送给客户端的消息>>>:').strip()
if len(msg) == 0:
continue
sock.send(msg.encode('utf8')) # 朝客户端发送数据
data = sock.recv(1024) # 接收客户端发送的数据 1024bytes
if len(data) == 0:
break
print(data.decode('utf8'))
except ConnectionResetError:
sock.close()
break
# 客户端
import socket
# 产生一个socket对象
client = socket.socket()
# 连接服务端(拼接服务端的ip和port)
client.connect(('127.0.0.1',8088))
# 数据交互
while True:
data = client.recv(1024) # 接收服务端发送的数据
print(data.decode('utf8'))
msg = input('请输入发送给客户端的消息>>>:').strip()
if len(msg) == 0:
msg = '手抖了,暂无消息'
client.send(msg.encode('utf8')) # 朝服务端发送数据
server.listen(5)
主要是为了缓冲,避免太多的无效等待!!!
1.问题:
在前面我们知道tcp容易产生黏包的问题,而udp不会产生黏包的问题,但是会产生丢包的问题,tcp应用的场景很多所以黏包问题必须要解决。
(1)TCP协议特性:
流程协议:所有的数据类似于水流,连接在一起;
会将数据量比较小并且时间间隔比较短的数据整合到一起发送
并且还会受制于recv括号内的数字大小。
(2)recv
我们不知道即将要接收的数据量多大 如果知道的话不会产生也不会产生黏包
2.解决方法一:
1.解决黏包问题第一种方法,我们知道黏包问题是由于tcp的优化算法将两个不太大的数据包合并了一起发送的,这种情况一般出现在连续使用几个send()出现的,所以我们如果知道要发送的数据有多大我们就可以设置接收的大小,这样就可以刚好能把所有的数据接收完。下面是具体的步骤细节见代码
server端代码import struct import socket sk = socket.socket() sk.bind(('127.0.0.1',8080)) sk.listen() conn,addr = sk.accept() while True: cmd = input('>>>') conn.send(bytes(cmd,encoding='utf-8')) num = conn.recv(1024).decode('utf-8') #接收client端计算好的数据长度 conn.send(bytes('ok',encoding='utf-8')) #发送一个确认防止发送num的时候跟后面的send内容合并了 ret = conn.recv(num) print(ret.decode('gbk')) conn.close() sk.close()
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
client端代码import socket import struct import subprocess sk = socket.socket() sk.connect(('127.0.0.1',8080)) while True: cmd = sk.recv(1024).decode('utf-8') ret = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) std_out = ret.stdout.read() std_err = ret.stderr.read() sk.send(bytes(str(len(std_err)+len(std_out)),encoding='utf-8')) #上面计算字符串的长度发送给server端在接收的时候刚好接收那么长的数据 sk.recv(1024) #ok 这一步主要的目的是为了将num的发送跟后面的send分割开防止黏包现象 sk.send(std_out) sk.send(std_err) sk.close()
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
3.解决方法二:
用struct模块解决黏包现象
server端代码#tcp黏包现象的解决 struct import struct import socket sk = socket.socket() sk.bind(('127.0.0.1',8080)) sk.listen() conn,addr = sk.accept() while True: cmd = input('>>>') conn.send(bytes(cmd,encoding='utf-8')) num = conn.recv(1024) #接收数据 num = struct.unpack('i',num)[0]#进行解包,解包的结果是一个元组类型取第一个数据 ret = conn.recv(num) print(ret.decode('gbk')) conn.close() sk.close()
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
client端代码import socket import struct import subprocess sk = socket.socket() sk.connect(('127.0.0.1',8080)) while True: cmd = sk.recv(1024).decode('utf-8') ret = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) std_out = ret.stdout.read() std_err = ret.stderr.read() num = len(std_err) + len(std_out) num = struct.pack('i',num) #利用struct模块将一个数据转换成bytes类型 i代表int型 sk.send(num) sk.send(std_out) sk.send(std_err) sk.close()
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
4.思路与总结
(1)思路
服务端:
1.先将真实数据的长度制作成固定长度
2.先发送固定长度的报头
3.再发送真实数据
客户端:
1.先接收固定长度的报头
2.再根据报头解压出真实长度
3.根据真实长度接收即可
(2)解决黏包问题的终极方案
服务端
1.先构造一个数据的详细字典
2.对字典数据进行打包处理 得到一个固定长度的数据
3.将上述打包之后的数据发送给客户端
4.将字典数据发送给客户端
5.将真实数据发送给客户端
客户端
1.先接收固定长度的数据
2.根据固定长度解析出即将要接收的字典真实长度
3.接收字典数据
4.根据字典数据 获取出真实数据的长度
5.接收真实数据长度
(3)总结
struct模块无论数据长度是多少 都可以帮你打包成固定长度
然后基于该固定长度 还可以反向解析出真实长度
struct模块针对数据量特别大的数字没有办法打包!!!