第十九章:网络编程
软件开发架构
规定了程序的请求逻辑、功能分块
C/S架构
Client:客户端,即将要去消费的客人
Server:服务端,给客人提供服务的店
这种结构的软件要求我们必须安装一个客户端才可以使用,比如微信、网易云音乐、爱奇艺等软件
一般情况下客户端与服务端交互需要互联网,但是有些不需要(因为客户端和服务端都在一台计算机上)
1、C/S架构的优点:
1) C/S架构的界面和操作可以很丰富。(客户端操作界面可以随意排列,满足客户的需要)
2) 安全性能可以很容易保证。(因为只有两层的传输,而不是中间有很多层。
3) 由于只有一层交互,因此响应速度较快。(直接相连,中间没有什么阻隔或岔路,比如QQ,每天那么多人在线,也不觉得慢)
2、C/S架构的缺点:
可以将QQ作为类比:
1) 适用面窄,通常用于局域网中。
2) 用户群固定。由于程序需要安装才可使用,因此不适合面向一些不可知的用户。
3) 维护成本高,发生一次升级,则所有客户端的程序都需要改变。
B/S架构
Browser:浏览器
Server:服务器/端
浏览器可以充当所有服务端的客户端,B/S 架构本质还是 C/S 架构,
1、B/S架构的优点:
1) 客户端无需安装,有Web浏览器即可。
2) BS架构可以直接放在广域网上,通过一定的权限控制实现多客户访问的目的,交互性较强。
3) BS架构无需升级多个客户端,升级服务器即可。可以随时更新版本,而无需用户重新下载啊什么的。
2、B/S架构的缺点:
1) 在跨浏览器上,BS架构不尽如人意。
2) 表现要达到CS程序的程度需要花费不少精力。
3) 在速度和安全性上需要花费巨大的设计成本,这是BS架构的最大问题。
4) 客户端服务器端的交互是请求-响应模式,通常需要刷新页面,这并不是客户乐意看到的。(在Ajax风行后此问题得到了一定程度的缓解)
C/S架构 与 B/S架构的区别
1、操作不同,相对于B/S架构来说,C/S架构界面与操作更丰富。
2、安全性不同,C/S架构安全性要高于B/S架构。
3、相应速度不同,由于C/S架构只有一层交互的原因,去相应速度明显高于B/S架构。
4、适用不同,C/S常用于局域网,B/S常用于广域网。
5、使用条件不同,C/S需要程序安装,B/S只需要有web浏览器即可。
6、成本构成不同,虽然两者的成本都不低,但C/S主要源于日常升级维护,B/S主要源于程序设计花费。
7、交互模式不同,B/S的交互模式是请求-响应模式,通常需要刷新页面,C/S则是集权式的,只有一层交互。
网络相关专业名词
计算机之间要想实现数据交互必须要'连接'到一起
1.交换机
将网络分成若干小段,数据可以在这些段间一段一段地传输,提高通信效率,以解决网络拥堵,降低出错,提高传输效率。
2.广播
主机之间一对所有的通讯模式。有线电视网就是典型的广播型网络,我们的电视机实际上是接受到所有频道的信号,但只将一个频道的信号还原成画面。
3.单播
主机之间一对一的通讯模式。首次被查找的计算机回应查找它的计算机,并附带自己的mac地址
4.广播风暴
当广播数据充斥网络无法处理,并占用大量网络带宽,导致正常业务不能运行,甚至彻底瘫痪,这就发生了“广播风暴”。
5.局域网
是指在某一区域内由多台计算机互联成的计算机组。一般是方圆几千米以内。局域网是封闭型的,可以由办公室内的两台计算机组成,也可以由一个公司内的上千台计算机组成。
可以简单的理解为有单个交换机组成的网络,在局域网内可以直接使用 mac 地址通信。
6.广域网
缩写为 WAN,又称广域网。指的是连接不同地区局域网或城域网计算机通信的远程网。它能连接多个地区、城市和国家,或横跨几个洲并能提供远距离通信,形成国际性的远程网络。一般所指的互联网是属于一种公共型的广域网。
可以简单的理解为范围更大的局域网。
7.互联网
由所有的局域网、广域网连接到一起形成的网络
8.路由器
不同的局域网计算机之间是无法直接实现数据交互的,需要路由器连接。
路由器的基本功能是,把数据(IP 报文)传送到正确的网络
1、IP 数据报的转发,包括数据报的寻径和传送
2、子网隔离,抑制广播风暴;
3、维护路由表,并与其它路由器交换路由信息,这是 IP 报文转发的基础;
OSI七层协议
简介
"""
OSI七层协议:规定了所有的计算机在远程数据交互的时候必须经过相同的处理流程、在制造过程中必须拥有相同的功能硬件
"""
应用层:HTTP,FTP,NFS
表示层:Telnet,SNMP
会话层:SMTP,DNS
传输层:TCP,UDP
网络层:IP,ICMP,ARP
数据链路层:Ethernet,PPP,PDN,SLIP,FDDI
物理连接层:IEEE 802.1A,IEEE 802.11
ps:应、表、会、传、网、数、物
'''常见的是整合之后五层或者四层'''
应用层、传输层、网络层、数据链路层、物理连接层
应用层、传输层、网络层、网络接口层
"""
接收网络消息 数据由下往上传递
发送网络消息 数据由上往下传递
"""
物理连接层
物理层实际上就是布线、光纤、网卡和其它用来把两台网络通信设备连接在一起的东西。甚至一个信鸽也可以被认为是一个第 1 层设备。网络故障的排除经常涉及到第 1 层问题。
主要用于确保计算机之间的物理连接介质,接收数据(bytes类型、二进制)
数据链路层
运行以太网等协议。网桥都在2层工作,仅关注以太网上的MAC地址。
有关MAC地址、交换机或者网卡和驱动程序,就是在第 2 层的范畴。集线器属于第 1 层的领域,因为它们只是电子设备,没有 2 层的知识。
1.规定了电信号的分组方式
2.以太网协议
规定了计算机在出厂的时候都必须有一块网卡,网卡上有一串数字
该数字相当于是计算机的身份证号码是独一无二的
该数字的特征:12 位 16 进制数据
前 6 位产商编号,后 6 位流水线号
该数字也称为:以太网地址/MAC地址
网络层
网络层的任务就是选择合适的网间路由和交换结点, 确保数据及时传送。如果你在谈论一个IP地址,那么你是在处理第 3 层的问题,这是“数据包”问题,而不是第 2 层的“帧”。
IP是第 3 层问题的一部分,此外还有一些路由协议和地址解析协议(ARP)。有关路由的一切事情都在第 3 层处理。地址解析和路由是第 3层的重要目的。
IP协议:规定了所有接入互联网的计算机都必须有一个IP地址 类似于身份证号
mac地址是物理地址可以看成永远无法修改
IP地址是动态分配的 不同的场所IP是不同的
IP地址特征:
IPV4:点分十进制
0.0.0.0
255.255.255.255
IPV6:能够给地球上每一粒沙分一个IP地址
IP地址可以跨局域网传输
ps:IP地址可以用来标识全世界独一无二的一台计算机
传输层
第 4 层的数据单元也称作数据包(packets)。这个层负责获取全部信息,因此,它必须跟踪数据单元碎片、乱序到达的数据包和其它在传输过程中可能发生的危险。
理解第 4 层的另一种方法是,第 4 层提供端对端的通信管理。像TCP等一些协议非常善于保证通信的可靠性。有些协议并不在乎一些数据包是否丢失,UDP协议就是一个主要例子。
PORT协议(端口协议)
用来标识一台计算机上面的某一个应用程序
范围:0-65535
特征:动态分配(洗浴中心号码牌)
建议:
0-1024 系统默认需要使用
1024-8000 常见软件的端口号
8000之后的
URL:统一资源定位符(网址)
网址本质是有IP和PORT组成的!!!
IP+PORT:能够定位全世界独一无二的一台计算机上面的某一个应用程序
域名解析:将网址解析成IP+PORT
我们之所以不直接使用IP+PORT的原因是太难记 所以发明了域名(网址)
IP:PORT 实际使用冒号连接
114.55.205.139:80
会话层
这一层也可以称为会晤层或对话层,在会话层及以上的高层次中,数据传送的单位不再另外命名,统称为报文。
会话层不参与具体的传输,它提供包括访问验证和会话管理在内的建立和维护应用之间通信的机制。如服务器验证用户登录便是由会话层完成的。
表示层
这一层主要解决用户信息的语法表示问题。
它将欲交换的数据从适合于某一用户的抽象语法,转换为适合于OSI系统内部使用的传送语法。即提供格式化的表示和转换数据服务。数据的压缩和解压缩, 加密和解密等工作都由表示层负责。
应用层
是专门用于应用程序的。
应用层确定进程之间通信的性质以满足用户需要,以及提供网络与用户应用软件之间的接口服务。SMTP、DNS和FTP都是第7层协议。
应用层相当于是程序员自己写的应用程序 里面的协议非常的多
常见的有:HTTP、HTTPS、FTP
ps:后续框架部分再做介绍
传输层之TCP与UDP协议
TCP与UDP都是用来规定通信方式的
ps:不遵循上述协议也可以通信 只不过遵循了更合规合法合理!!!
TCP协议
三次握手建链接
1.TCP协议也称为可靠协议(数据不容易丢失)
造成数据不容易丢失的原因不是因为有双向通道,而是因为有反馈机制
给对方发消息之后会保留一个副本,直到对方回应消息收到了才会删除
否则会在一定的时间内反复发送
2.洪水攻击
同一时间有大量的客户端请求建立链接,会导致服务端一直处于SYN_RCVD状态
3.服务端如何区分客户端建立链接的请求
可以对请求做唯一标识
四次挥手断链接
1.四次不能合并为三次
因为中间需要确认消息是否发完(TIME_WAIT)
UDP协议
无连接:只知道对端的IP和端口号就可以发送,不需要实现建立连接。(就像寄信)。
不可靠:没有确认机制, 没有重传机制。如果因为网络故障该段无法发到对方, UDP协议层也不会给应用层返回任何错误信息。
面向数据报: 应用层交给UDP多长的报文, UDP原样发送既不会拆分,也不会合并。所以UDP不能够灵活的控制读写数据的次数和数量。
UDP存在接收缓冲区,但不存在发送缓冲区。UDP没有发送缓冲区,在调用 send to 时会直接将数据交给内核,由内核将数据传给网络层协议进行后续的传输动作。UDP具有接收缓冲区,但是这个接收缓冲区不能保证收到的UDP报文的顺序和发送UDP报的顺序一致,如果缓冲区满了再到达的UDP数据报就会被丢弃。
socket模块
如果我们需要编写基于网络进行数据交互的程序 意味着我们需要自己通过代码来控制我们之前所学习的OSI七层(很繁琐 很复杂 类似于我们自己编写操作系统)
socket 类似于操作系统 封装了丑陋复杂的接口提供简单快捷的接口
socket也叫套接字
基于文件类型的套接字家族(单机)
AF_UNIX
基于网络类型的套接字家族(联网)
AF_INET
基于类的 socket TCP 连接
服务端
# -*- coding: utf-8 -*-
# @File : server.py
# @Date : 2022/11/16 17:15
# @Author : ysg
# @Describe:
import socket
class Server:
address_family = socket.AF_INET
socket_type = socket.SOCK_STREAM
max_size = 1024
codeing = 'utf-8'
request_queue_size = 5
def __init__(self, server_address, bind_and_activate=True):
self.server_address = server_address
self.sock = socket.socket(self.address_family, self.socket_type)
if bind_and_activate:
print('等待连接...')
self.server_bind()
self.server_listen()
else:
self.server_close()
def server_bind(self):
self.sock.bind(self.server_address)
def server_listen(self):
self.sock.listen(self.request_queue_size)
def server_close(self):
self.sock.close()
def run(self):
# 连接循环
while 1:
self.conn, self.addr = self.sock.accept()
print(f'连接的地址为:{self.addr}')
# 通信循环
while 1:
try:
info = self.conn.recv(self.max_size).decode(self.codeing)
if not info: break
print(f'来自客户端{self.addr}:{info}')
self.conn.send(info.encode(self.codeing))
except Exception as e:
break
if __name__ == '__main__':
s = Server(('127.0.0.1', 10560))
s.run()
客户端
# -*- coding: utf-8 -*-
# @File : client.py
# @Date : 2022/11/16 17:15
# @Author : ysg
# @Describe:
import socket
class Client:
address_family = socket.AF_INET
socket_type = socket.SOCK_STREAM
max_size = 1024
codeing = 'utf-8'
def __init__(self, server_address, bind_and_activate=True):
self.server_address = server_address
self.sock = socket.socket(self.address_family, self.socket_type)
if bind_and_activate:
self.client_connect()
else:
self.client_close()
def client_connect(self):
self.sock.connect(self.server_address)
def client_close(self):
self.sock.close()
def run(self):
# put = input('请输入消息内容>>>').strip().encode()
put = b'hello'
while 1:
self.sock.send(put)
ret = self.sock.recv(1024).decode()
print(f'输入内容为:{ret}')
if __name__ == '__main__':
c = Client(('127.0.0.1', 10560))
c.run()
黏包现象
1、连续的小包可能会被优化算法给组合到一起进行发送
2、第一次如果发送的数据大小 2000B接收端一次性接受大小为 1024,这就导致剩下的内容会被下一次 recv 接收到,导致结果错乱
黏包现象产生的原因
1.不知道每次的数据到底多大
2.TCP也称为流式协议:数据像水流一样绵绵不绝没有间隔(TCP会针对数据量较小且发送间隔较短的多条数据一次性合并打包发送)
避免黏包现象的核心思路\关键点
如何明确即将接收的数据具体有多大
ps:如何将长度变化的数据全部制作成固定长度的数据
struct模块
struct 模块的作用是将数据长度转换成固定长度的内容
需要注意的是,struct 模块是有缺点的,就是 struct 的 int 类型或别的类型不是无限制的,当整数大于一定值后,会失败,即 int 或相关类型是有大小限制的
struct 的格式顺序
字节顺序、大小和对齐方式
默认情况下,C类型以机器的本机格式和字节顺序表示,并在必要时通过跳过 pad 字节(根据C编译器使用的规则)正确对齐。
Character | Byte order | Size | Alignment |
---|---|---|---|
@ | native | native | native |
= | native | standard | none |
< | little-endian | standard | none |
> | big-endian | standard | none |
! | network (= big-endian) | standard | none |
struct 支持的类型
Format | C Type | Python type | Standard size | Notes |
---|---|---|---|---|
x | pad byte | no value | (7) | |
c | char | bytes of length 1 | 1 | |
b | signed char | integer | 1 | (1), (2) |
B | unsigned char | integer | 1 | (2) |
? | _Bool | bool | 1 | (1) |
h | short | integer | 2 | (2) |
H | unsigned short | integer | 2 | (2) |
i | int | integer | 4 | (2) |
I | unsigned int | integer | 4 | (2) |
l | long | integer | 4 | (2) |
L | unsigned long | integer | 4 | (2) |
q | long long | integer | 8 | (2) |
Q | unsigned long long | integer | 8 | (2) |
n | ssize_t | integer | (3) | |
N | size_t | integer | (3) | |
e | (6) | float | 2 | (4) |
f | float | float | 4 | (4) |
d | double | float | 8 | (4) |
s | char[] | bytes | ||
p | char[] | bytes | ||
P | void* | integer | (5) |
struct 的应用
from struct import *
ret = pack('hhl', 1, 2, 3)
print(ret) # b'\x00\x01\x00\x02\x00\x00\x00\x03'
unret = unpack('hhl', b'\x00\x01\x00\x02\x00\x00\x00\x03')
print(unret) # (1, 2, 3)
print(calcsize('hhl')) # 8, 计算大小
规避黏包问题
服务端
# -*- coding: utf-8 -*-
# @File : server.py
# @Date : 2022/11/17 18:52
# @Author : ysg
# @Describe:
import os
import json
import socket
import struct
class Server:
def __init__(self, address):
self.address = address
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.bind(self.address)
self.sock.listen(5)
self.upload = r'H:\py\out_of_labor\socket_file\处理粘包问题\upload'
self.download_path = r'C:\Users\Administrator\Desktop\书籍\阅读\基因'
self.file_lit_path = os.listdir(self.download_path)
self.head = {
'size': None,
'fileName': None
}
def run(self):
while 1: # 连接循环
print('建立连接...')
self.conn, self.addr = self.sock.accept()
print(f'连接的地址为:{self.addr}')
while 1: # 通信循环
try:
info = '''
语法:upload file_path,例子:upload H:\zzgit\A50Futures\config.ini
download file_path,例子:upload DNA 生命的秘密.pdf
'''
info += f'可下载文件:{self.file_lit_path}'
head_bytes = info.encode()
# 构建消息头
self.head['size'] = len(head_bytes)
# 字典序列化
head_json = json.dumps(self.head)
# 发送消息头及内容
self.conn.send(struct.pack('i', len(head_json)) + head_json.encode() + head_bytes)
self.get_recv_lit = self.conn.recv(1024).decode().split(' ')
print(self.get_recv_lit)
if hasattr(self, self.get_recv_lit[0]):
func = getattr(self, self.get_recv_lit[0])
func()
# self.sock.recv(1024)
except Exception as e:
print(e)
break
def upload(self):
pass
def download(self):
file_name = self.get_recv_lit[1:]
path = os.path.join(self.download_path, ' '.join(file_name))
file_size = os.path.getsize(path)
print('书的大小', file_size)
# 构建消息头
self.head['size'] = file_size
self.head['fileName'] = ' '.join(file_name)
head_len = struct.pack('=i', len(json.dumps(self.head).encode()))
head_json = json.dumps(self.head)
with open(path, 'rb') as fr:
info = fr.read()
self.conn.send(head_len + head_json.encode() + info)
if __name__ == '__main__':
s = Server(('192.168.1.38', 9090))
s.run()
客户端
# -*- coding: utf-8 -*-
# @File : client.py
# @Date : 2022/11/17 18:52
# @Author : ysg
# @Describe:
import os
import time
import json
import socket
import struct
class Client:
def __init__(self, address):
self.address = address
self.sock = socket.socket()
self.sock.connect(self.address)
self.upload = r'H:\py\out_of_labor\socket_file\处理粘包问题\upload'
def run(self):
while 1:
# 接收报头长度数据
ret = self.sock.recv(4)
head_len = struct.unpack('=i', ret)[0]
# 接收字典数据
ret = self.sock.recv(head_len).decode()
# 字典反序列化
ret_info = json.loads(ret)
print(f'文件信息:{ret_info}')
if ret_info['fileName']:
num = 0
path = os.path.join(self.upload, ret_info['fileName'])
with open(path, 'wb') as fw:
while num < ret_info['size']:
date = self.sock.recv(1024)
num += len(date)
fw.write(date)
# 接收文件数据
ret = self.sock.recv(ret_info['size']).decode()
print('ret_info', ret)
put = input('输入语法>>>').strip()
self.sock.send(put.encode())
if __name__ == '__main__':
c = Client(('192.168.1.38', 9090))
c.run()