网络编程
软件开发架构
C/S架构
Client (客户端)<==========> Server(服务端)
- C/S数据传输过程
"C" "S" | 客户端软件 服务端 | 操作系统 操作系统 ↓ 计算机硬件 <==========> 计算机硬件
B/S架构
Browser (浏览器)<==========> Server(服务器)
-
其实B/S架构的底层还是用到了C/S架构,浏览器充当了各个服务器的客户端;
-
如果使用这种架构开发,不像C/S架构开发的标准是自己定制的,而必须按照浏览器的标准来开发;
-
B/S数据传输过程
浏览器向服务器发送请求,等待服务器的响应; 服务器处理请求,接收并处理请求,并产生响应; 服务器发送响应,把用户请求的数据返回给浏览器; 浏览器解释执行HTML文件; 用户看到想要的界面;
服务端三大特征
- 24小时不间断提供服务(24小时监听)
- 固定的地址(IP地址)
- 能够服务多个用户(高并发)
网络理论
-
远程数据传输发展史
设备 通信介质 有线电话 电话线互联 无线电话 信号发射器 台式电脑 网线 笔记本电脑 网卡 """所以要想实现远程数据交互的前提是必须要有物理的连接介质,以及保障数据互通的安全协议等;""" 通信定义:网络存在的意义就是跨区域数据传输,即为通信; 协议:规定怎么做的规章制度; 网络 = 物理链接介质 + 互联网通信协议
-
交换机
能够使接入该机器的所有计算机之间彼此互联
-
局域网
有交换机组成的网络
-
互联网
可以简单的理解为是多个局域网之间彼此互联
-
路由器
能够连接多个局域网并实现局域网之间的数据传输,(上网的核心)
路由器(Router)是连接两个或多个网络的硬件设备,在网络间起网关的作用,是读取每一个数据包中的地址然后决定如何传送的专用智能性的网络设备。它能够理解不同的协议,例如某个局域网使用的以太网协议,因特网使用的TCP/IP协议。这样,路由器可以分析各种不同类型网络传来的数据包的目的地址,把非TCP/IP网络的地址转换成TCP/IP地址,或者反之;再根据选定的路由算法把各数据包按最佳路线传送到指定位置。所以路由器可以把非TCP/IP网络连接到因特网上。
OSI七层协议(五层)
-
OSI七层协议
‘应’-‘表’-‘会’-‘传’-‘网’-‘数’-‘物’
-
物理连接层
- 物理层确保原始的数据可在各种物理媒体上传输。局域网与广域网皆属第1、2层。
- 物理层是OSI的第一层,它虽然处于最底层,却是整个开放系统的基础。物理层为设备之间的数据通信提供传输媒体及互连设备,为数据传输提供可靠的环境。如果您想要用尽量少的词来记住这个第一层,那就是“信号和介质”。
- 其实就是提供一个物理连接接口(网线口 无线网卡)
-
数据链路层
-
功能
功能一:规定了电信号的分组方式
- 规定内容:
规定1:一组数据称之为一个数据帧 规定2:数据帧分成两部分 =》头+数据 head包含(18个字节):源地址与目标地址,该地址是mac地址 data包含(最短46个字节,最长1500字节 ):包含的是网络层整体的内容 规定3:规定但凡接入互联网的主机必须有一块网卡,每块网卡在出厂的时候都烧制好一个全世界独一无二的地址,该地址称为mac地址
功能二:以太网协议:规定了计算机必须有一块网卡 并且网卡上面要有一个固定的一串数字12位16进制数(Mac地址);
-
Mac地址
每块网卡出厂时都被烧制上一个世界唯一的mac地址,长度为48位2进制,通常由12位16进制数表示;前六位:厂商编号;后六位: 流水线号;
Mac地址只能在局域网中传播,不能跨局域网
- 规定内容:
-
计算机通信基本靠“吼”,就是以太网的工作方式是广播
- 广播:广播是去“寻找”主机;
- 单播:回应广播;
- !!!广播风暴!!!
-
-
网络层
-
网络层功能
- 划分广播域
- 每一个广播域要接通外部,需要网关 帮内部的计算机转发到公网,网关发送数据包
- 网关与外界通信,走路由协议传输
-
规定
数据帧 = 头+数据
- 头包含:源地址与目标地址,该地址是IP地址
- 数据包含:传输层发过来的整体内容
-
IP协议
规定了接入互联网的计算机都必须有一个IP地址用于唯一标识;
- IPV4
最小0.0.0.0 === 最大255.255.255.2555 # 最多也就42亿 - IPV6
能够表示出地球上每一粒沙子
- IPV4
-
公网IP与私网IP
- 公网IP需要申请
- 私网IP自带的,但是无法直接基于互联网访问
-
IPV4子网掩码
- 32位2进制数组成:8bit.8bit.8bit.8bit
- eg:255.255.255.0 = 11111111.11111111.11111111.00000000
- 合法的IPV4地址组成的部分 = ip地址/子网掩码地址
eg:172.16.10.1 / 255.255.255.0
# 不加子网掩码默认为24(172.16.10.1/24)
-
局域网的计算:
将ip地址的二进制和子网掩码的二进制与运算(同1为1,同0为0,1和0得1),然后转十进制
计算机在同一个局域网,可以互联: # 案例 计算机1-》172.16.10.1 / 255.255.255.0 局域网 = 172.16.10.0 计算机2-》172.16.10.2 / 255.255.255.0 局域网 = 172.16.10.0 # 如果两台计算机不在同一局域网,那么走网关传输
- ARP协议 - 地址解析协议(用在数据链路层)
将ip地址解析成mac地址
地址解析协议,即ARP(Address Resolution Protocol),是根据IP地址获取物理地址的一个TCP/IP协议。
主机发送信息时将包含目标IP地址的ARP请求广播到网络上的所有主机,并接收返回消息,以此确定目标的物理地址(MAC地址)。
收到返回消息后将该IP地址和物理地址存入本机ARP缓存中并保留一定时间,下次请求时直接查询ARP缓存以节约资源。
地址解析协议是建立在网络中各个主机互相信任的基础上的,网络上的主机可以自主发送ARP应答消息,其他主机收到应答报文时不会检测该报文的真实性就会将其记入本机ARP缓存;由此攻击者就可以向某一主机发送伪ARP应答报文,使其发送的信息无法到达预期的主机或到达错误的主机,这就构成了一个ARP欺骗。ARP命令可用于查询本机ARP缓存中IP地址和MAC地址的对应关系、添加或删除静态对应关系等。相关协议有RARP、代理ARP。NDP用于在IPv6中代替地址解析协议。# 两台计算机在同一个局域网内 计算机1 计算机2 ARP: 1、计算二者网络地址,如果一样,拿到计算机2的mac地址就可以了 2、发送广播包 发送端mac # 两台计算机不在同一个局域网内 计算机1 | 网关 | 计算机2 ARP:计算机二者网络地址,如果不一样,应该拿到网关的mac地址
IP地址+Mac地址可以标识全世界范围内独一无二的一台计算机;如果在同一个局域网内,IP地址只是用于回去mac地址;不在同一局域网,ARP协议获取的是网关的mac地址
-
-
传输层
-
端口协议
端口就是队,端口其实就是为每个应用程序(进程)分了不同的队,数据包按照目的端口被推入相应的队中,等待被进程取用,在极特殊的情况下,这个队也是有可能溢出的,不过操作系统允许各进程指定和调整自己的队的大小。
不光接受数据包的进程需要开启它自己的端口,发送数据包的进程也需要开启端口,这样,数据包中将会标识有源端口,以便接受方能顺利地回传数据包到这个端口。
就好比知道了ip和端口号,能够唯一标识世界上某一台接入互联网的计算机上面的某一个正在运行的应用程序;
大白话:房间的钥匙是ip,那么想使用房间内的电器(应用层序)的钥匙就是端口;范围0-65535
、特性:动态分配
、使用原则:0-1024 系统默认端口号、1024-8000常见软件端口号
端口冲突:端口号在同一台计算机同一时刻不能重复
"""IP+port:唯一标识世界上接入互联网的唯一一台计算机上正在运行的一款程序""" 127.0.0.1:8080
-
TCP协议
TCP协议是流式协议,可靠协议,在连接到不同但互连的计算机通信网络的主计算机中的成对进程之间依靠TCP提供可靠的通信服务,用此协议传输比较可靠,TCP数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常TCP数据包的长度不会超过IP数据包的长度,以确保单个TCP数据包不必再分割。
- 三次握手,四次挥手
“三次握手,四次挥手”怎么完成? 其实3次握手的目的并不只是让通信双方都了解到一个连接正在建立,还在于利用数据包的选项来传输特殊的信息,交换初始序列号ISN。 3次握手是指发送了3个报文段,4次挥手是指发送了4个报文段。
-
LISTENING状态:FTP服务启动后首先处于侦听(LISTENING)状态;
-
SYN_SENT状态:忙于恢复确认建立请求;洪水攻击!!
-
ESTABLISHED状态:ESTABLISHED状态是表示两台机器正在传输数据,观察这个状态最主要的就是看哪个程序正在处于ESTABLISHED状态;
-
CLOSE_WAIT状态:对方主动关闭连接或者网络异常导致连接中断,这时客户端状态会变成CLOSE_WAIT 此时客户端要调用close()来使得连接正确关闭;
-
TIME_WAIT状态:真空期,如果客户端主动断开连接,收到服务端的确认状态,但是连接不会瞬间断开,为了防止连接中的包没有传输完成,或者还有想要传输的包等,影响了连接,会处于一段时间TIME_WAIT态,几次重连后,最后断开释放资源;
-
三次握手建链接过程
- 第一次握手:客户端给服务器发送一个 SYN 报文;
- 第二次握手:服务器收到 SYN 报文之后,会应答一个 SYN+ACK 报文;
- 第三次握手:客户端收到 SYN+ACK 报文之后,会回应一个 ACK 报文;
- 服务器收到 ACK 报文之后,三次握手建立完成;
TCP可靠的原因并不是因为有双向通道,是因为接收数据的时候会有反馈机制;
发送数据必须等到对方确认后才算完成,才会将自己内存中的数据清除
效率低,可靠,如果需要效率的话用UDP协议(不需要链接,不需要对方确认)
当服务端大量处于TIME_WAT状态,表示正在经历高并发 -
四次挥手断链接过程
- 第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于FIN_WAIT1状态。
- 第二次握手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 + 1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT状态。
- 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。在这一步之前要确认自己是否还有数据没有发送,处于真空期;
- 第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 + 1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态
- 服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。
-
TCP报文
-
UDP协议
无连接的传输协议、不可靠协议,该协议称为用户数据包协议(UDP,User Datagram Protocol)。UDP 为应用程序提供了一种无需建立连接就可以发送封装的 IP 数据包的方法。相比TCP而言速度快,但是不安全,无需确认,不管对方是否就收到,发送不管通道的状态,发出去就“摊牌了”;
-
-
应用层
应用层对于开发来说主要是规定了程序的数据格式,都是人为自定义的协议标准;
该层为用于通信的应用程序和用于消息传输的底层网络提供接口。- 可以定义协议 =》 头部+数据部分
自定义协议需要注意的问题: 1. 两大组成部分:头部+数据部分 头部:放对数据的描述信息(比如想发送给谁,数据的类型,数据的长度) 数据部分:想要发送的数据 2. 头部的长度必须固定 因为接收端要通过头部获取所接收数据的详细信息
- 客户端和服务端逻辑上使用的是
HTTP协议
- ABC类网络
- 可以定义协议 =》 头部+数据部分
-
七层协议封包解包
-
其他协议补充
-
DHCP协议
- 动态获取ip地址,基于udp协议
DHCP(Dynamic Host Configuration Protocol,动态主机配置协议),前身是BOOTP协议,是一个局域网的网络协议,使用UDP协议工作,统一使用两个IANA分配的端口:67(服务器端),68(客户端)。DHCP通常被用于局域网环境,主要作用是集中的管理、分配IP地址,使client动态的获得IP地址、Gateway地址、DNS服务器地址等信息,并能够提升地址的使用率。简单来说,DHCP就是一个不需要账号密码登录的、自动给内网机器分配IP地址等信息的协议。
- 获取过程
以太网 ip头 udp头 dhcp数据包 1 2 3 4
- 动态获取ip地址,基于udp协议
-
DNS域名解析
域名系统(Domain Name System,缩写:DNS)是互联网的一项服务。它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。DNS使用TCP和UDP端口53。当前,对于每一级域名长度的限制是63个字符,域名总长度则不能超过253个字符。DNS协议是用来将域名转换为IP地址(也可以将IP地址转换为相应的域名地址)。
- 根域名服务器
www.163.com.,com后面的点,就是根! - 查找顺序
本地DNS服务器 --》根DNS服务器 --》 13台顶级域名服务器
- 根域名服务器
-
URL组成
- 应用层协议:http/s协议
封包,七层协议 - 域名加端口:www.baidu.com:80
DNS域名解析,搞到ip - 虚拟路径:/AlearnMeta/p/16559127.html
- 应用层协议:http/s协议
-
socket套接字编程
只要涉及到远程数据交互必须要操作OSI七层模型,那么每层都需要相应的程序去操作,现在就需要一个模块去操作,直接实现;
Socket是处于应用层和传输层之间的抽象层,Socket并不是一个单独的层,在我们设计程序软件的时候,它会让编程变的更简单,我们大量用的都是通过socket实现的;
Socket的作用显而易见,TCP和UDP比喻成小弟,socket是大哥,那么下面的协议(TCP/UDP)不需要我们去管,这样暴露出来的只有Socket接口,Socket自动的去组织数据,来符合指定的协议标准;
Socket 通信流程图
Socket基于TCP/IP协议的面向连接的通信,分为客户端和服务端,必须先启动服务端,然后再启动客户端去链接服务端;
Socket模块
socket()方法
客户端和服务端的入口,默认就是基于网络的TCP协议传输;
套接字家族
AF_UNIX:本机通信
AF_INET:TCP/IP协议,使用IPV4,基于网络传输
AF_INET6:TCP/IP协议,使用IPV6,基于网络传输
bind()方法
绑定函数的作用就是为调用socket()函数产生的套接字分配一个本地协议地址,建立地址与套接字的对于关系;
# 源码
def bind(self, address: Union[_Address, bytes]) -> None: ...
# 参数
address: Union[_Address, bytes]
address:要接收的数据类型集合
->:返回什么(返回值)
# 示例bind((ip,端口))
server.bind(('127.0.0.1', 8080)) # 绑定ip和端口
listen()方法
监听函数,作用是建立半连接池,规定最大连接数,在windows系统下如果客户端数量超过半连接池规定的数量会报错;
server.listen(5) # 半连接池
# 如果服务端正在和一个客户端做交互,那么半连接池就规定了,还可以服务几个客服端;
# 类似于,餐厅门口可以让顾客坐的凳子,满了就不能坐了
accept()方法
作用就是使服务器接受客户端的连接请求;
运行服务端,会在此监听,等待请求;
def accept(self) -> Tuple[socket, _RetAddress]: ...
# 返回一个元组,元组包括socket对象和地址信息
"""
accept() -> (socket object, address info)
Wait for an incoming connection. Return a new socket
representing the connection, and the address of the client.
For IP sockets, the address info is a pair (hostaddr, port).
"""
- accept()函数返回值:sock、addr
- sock:用于操作服务端和客户端连接的双向通道的媒介
- addr:客户端的地址
- sock.recv():接收消息,返回bytes类型数据
def recv(self, bufsize: int, flags: int = ...) -> bytes: ... # 示例 sock.recv(1024) # 接收客户端发送的消息,一次接收1024bytes
- sock.send():发送消息,返回int类型数据
def send(self, data: bytes, flags: int = ...) -> int: ... # 示例 sock.send(b'alan')
connect() 方法
作用是TCP客户端连接服务器
def connect(self, address: Union[_Address, bytes]) -> None: ...
# 示例
# 格式:connect((ip,port)),里面是tuple类型
client.connect(('127.0.0.1', 8080))
close()方法
关闭套接字,并立即返回到进程;
sock.close()
server.close()
服务端客户端对比
服务端 | 客户端 | 对比 |
---|---|---|
server=socket.socket() | client=socket.socket() | 开头必须 |
server.bind((ip,port)) | client.connect((ip,port)) | 连接必须 |
server.listen(n) # 半连接池 | 服务端 | |
sock,addr = server.accept() # 获取客户端请求,返回sock,addr(客户端地址) |
服务端 | |
sock.recv(1024) # 服务端接收内容,1024为size |
client.send(bytes) # 客户端发送bytes类型数据 |
服务端和客户端相对 |
sock.send(bytes) # 服务端发送bytes类型数据 |
client.recv(1024) # 客户端接收内容,1024为size |
服务端和客户端相对 |
sock.close() # 关闭会话通道,断开连接 |
服务端 | |
server.close() # 关闭套接字 |
client.close() # 关闭套接字 |
服务端和客户端 |
注意:服务端和客户端不可同时发数据(send),也不可同时收数据(recv)
简单案例
- 服务端
'''server.py''' import socket server = socket.socket() # 建立连接 server.bind(('127.0.0.1', 8080)) # 半连接池 server.listen(5) # 获取客户端请求 sock, address = server.accept() print(address) # 接收数据 data = sock.recv(1024) print(data) # 发送数据 sock.send(b"Hi,This is server-side!") # 关闭通话 sock.close() # 关闭套接字(服务器) server.close()
- 客户端
'''client.py''' import socket client = socket.socket() # 建立连接 client.connect(('127.0.0.1', 8080)) # 发送数据 client.send(b"Hello guys,I'am Alan!") # 接收数据 data = client.recv(1024) print(data) # 关闭套接字 client.close()
通信循环与优化
-
优化内容:
解决,发空消息会停住,导致双方接收和发送混乱 解决Address already in use 报错(端口占用错误) 解决服务端启用,客户端主动重启报错,错误内容:远程主机强迫关闭了一个现有的连接,原因是没有走三次握手和四次挥手主动断开;
-
服务端
import socket from socket import SOL_SOCKET,SO_REUSEADDR server = socket.socket() # 解决断开占用报错 server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) # 解决端口被占用问题 # 建立链接 server.bind(('127.1.1.1', 8086)) # 半连接池 server.listen(5) # 监听 while True: sock,address = server.accept() # 三次握手的listen态,避免下面这个while在客户端突然中断,持续报错 while True: try: # 接收 data = sock.recv(1024) if len(data) ==0:continue print(data.decode('utf8')) to_cmsg = data+ b'server' sock.send(to_cmsg) except Exception: # 解决客户端突然关闭导致服务端报错关闭 break
-
客户端
import socket client = socket.socket() # 获取ip和端口 client.connect(('127.1.1.1', 8086)) # 发送数据 while True: to_smsg = input('请输入发给服务端的内容>>>:').strip() if not to_smsg: print('不能输入空消息,请重新输入') continue client.send(to_smsg.encode('utf8')) # 接收数据 data = client.recv(1024) print(data.decode('utf8'))
TCP黏包现象(流式协议)
数据管道的数据没有被完全取出;TCP特性导致黏包,当数据量比较小 且时间间隔比较短,交互多次数据,那么TCP会自动打包成一个数据包发送;
情景一:如果交互的数据比规定接收的字节大,那么只会接收规定的字节大小,那么下次通信,继续传输上次没有传完的数据(互通管道,先进先出,TCP流式协议);
情景二:如果交互的数据太小,时间间隔比较短,比如想交互三次发三次hello,那么TCP会一次发完;TCP协议自带协议
-
解决办法
- 调整规定接收size,调大或调小(不推荐)
- 使用Struct规定固定报头(推荐)
-
Struct 模块
使用Struct模块规定了报头的长度,通过服务端定制报头和客户端解析报头来获取真实数据的长度,从而接收真实的数据内容,解决黏包问题;
-
规定报头
import struct import json info_dic = { 'name':'alan', 'age':18, } # 序列化 json_info_dic = json.dumps(info_dic) # 真实长度 print(len(json_info_dic)) # 27 # 定制报头 herder = struct.pack('i',len(json_info_dic)) print(len(herder)) # 4 # 解析报头 parse_herder = struct.unpack('i',herder)[0] print(parse_herder) # 29
-
解决黏包问题
直接使用报头+真实数据,会导致struct.pack的第二个长度参数超过最大范围
===》采用报头+字典详细信息+真实数据,在字典中传入真实数据的长度信息,避免了长度过长的情景
- 客户端
import json import os import socket import struct client = socket.socket() client.connect(('127.0.0.1', 8080)) while True: data_path = r'D:\PYTHON\python题目' # print(os.listdir(data_path)) # ['python练习题.md', 'Python阶段面试题.docx'] file_list = os.listdir(data_path) for index, value in enumerate(file_list, 1): print(index, value) # 1 python练习题.md 2 Python阶段面试题.docx file_choice = input('请输入想要上传的文件编号>>>>').strip() if not file_choice: print('上传文件编号不能为空') continue if not file_choice.isdigit(): print('文件编号必须是纯数字') continue file_choice = int(file_choice) if file_choice not in range(1, len(file_list) + 1): print('没有该文件') continue # 获取文件名称 file_name = file_list[file_choice - 1] print(file_name) # 拼接路径 real_file_path = os.path.join(data_path, file_name) # print(real_file_path) # D:\PYTHON\python题目\python练习题.md # 优化操作 data_dict = { 'file_name': file_name, 'desc': 'python面试题', 'size': os.path.getsize(real_file_path), 'info': '通关面试必备!' } data_json = json.dumps(data_dict) # 制作字典报头 data_first = struct.pack('i', len(data_json)) # 发送字典报头 client.send(data_first) # 发送字典 client.send(data_json.encode('utf8')) # 发送真实数据 with open(real_file_path, 'rb') as f: for line in f: client.send(line)
- 服务端
import json import socket import struct server = socket.socket() server.bind(('127.0.0.1',8080)) server.listen(5) while True: sock,address = server.accept() while True: # 接收报头 recv_first = sock.recv(4) # 解析字典报头 dict_length = struct.unpack('i',recv_first)[0] # 接收字典数据 real_data = sock.recv(dict_length) # 解析字典,返序列化 real_dict = json.loads(real_data) print(real_dict) # JOSN格式的bytes数据 loads方法可以先解码,再反序列化 # 获取字典中的内容 data_size = real_dict.get('size') file_name = real_dict.get('file_name') # 接收 recv_size = 0 with open(file_name,'wb') as f: while recv_size<data_size: data = sock.recv(1024) recv_size += len(data) f.write(data)
- 客户端
UDP通信
- 示例
import socket udp_sk = socket.socket(type=socket.SOCK_DGRAM) # UDP协议 udp_sk.bind(('127.0.0.1',9000)) # 绑定地址 msg,addr = udp_sk.recvfrom(1024) udp_sk.sendto(b'hi',addr) udp_sk.close() import socket ip_port=('127.0.0.1',9000) udp_sk=socket.socket(type=socket.SOCK_DGRAM) udp_sk.sendto(b'hello',ip_port) back_msg,addr=udp_sk.recvfrom(1024) print(back_msg.decode('utf-8'),addr)