网络编程
网络架构:
C/S B/S :
C:客户端,B:游览器(rowser),S:服务端
-
C/S:客户端与服务器之间的架构:QQ,微信,游戏,App都属于cs架构
-
优点:安全,响应速度快,个性化功能多
-
缺点:开发和维护成本高,面向客户固定
-
-
B/S:游览器与服务器之间的架构:
- 优点:开发维护成本低,维护成本低,面向的用户广泛
- 缺点:相对不安全,响应速度相对慢,个性化设计单一
互联网通讯原理:
- 打电话示例:
- 互联网通信:
- 1,一堆物理介质将两个电话连接起来
- 2,拨号
- 3,统一的通信标准,一揽子协议,这些互联网协议就是一个个标准,最终可以通信
网络五层:
-
osi五层:
-
OSI(Open System Interconnect)开放式系统互联
- osi是一个标准,是一个规范,五层协议都是操作系统帮我们封装各种head
-
应用层 python (应用层包括会话层,表示层) 会话:保证多次网络传输准确性,表示:保证数据安全
应用层常见协议:HTTP、HTTPS、FTP、SMTP、TELNET、POP3、DNS、DHCP
-
表示层 数据的表示,安全,压缩,格式有JPEG,ASCII 加密格式等
-
会话成 建立、管理、终止会话,对应主机进程,指本地主机与远程主机正在进行的会话
-
传输层 tcp udp 四层路由器 四层交换机
-
网络层 路由器 三层交换机 ipv4 ipv6 (国际协议)
-
数据链路层 二层傻瓜交换机,以太网协议,mac arp 网卡
-
物理层 物理连接介质,网线,光纤,发送的是010101比特数据流
-
-
1.物理层:
- 物理连接介质:网线,光纤:发送的数据就是0101010 1比特数据流
-
2.数据链路层:
-
交换机
-
以太网协议:
-
以太网协议是局域网中的一种协议
-
将比特流数据分组,一组叫做一帧,每一数据帧分成:报头head和数据data两部分
-
-
数据头(head)|data数据:
-
数据头:固定长度18个字节(标准)(源地址,目的地址,端口,数据类型)
-
data数据:最短46字节,<=data <=1500字节
-
以太网问题: 1,数据头为什么要固定? 固定就是一个标准,为了提取源地址以及目标地址 2,以太网协议中源目标地址如何设置唯一? 网线直接接触的硬件就是网卡,网卡上有一个mac地址,确定计算机的唯一性的物理地址 网卡mac地址:12位,16进制组成的一串数字,前六位厂商编号:后六位:流水线号
head(源地址,目的地址,端口,数据类型)|data("俊丽")
-
广播/单播:
-
计算机最原始的通信方式
-
数据经过以太网协议封装后(head)|data,先要从局域网内广播,
-
广播:
- 一对多连接,有了mac地址,两台主机通信,一台主机通过arp协议获取到另一台主机的mac地址
- 广播只存在局域网内,广播通信基本靠吼
-
单播:
-
一对一连接(第一次是广播访问,第二次交换机有学习功能,将mac写入表中,下次进行单播)
-
优点:提升效率
-
-
-
交换机学习功能:
-
交换机对照(网口与MAC地址的)表: 1: 1C-1B-0D-DA-E8-8F 2: FF-FF-FF-FF-FF-FF 3: FF-FF-FF-FF-FF-FF 4: FF-FF-FF-FF-FF-FF 5: 1C-1B-0F-4A-E8-8F 网口1: 出来一条信息: # 第一次广播的形式发出去 网口1:源地址: 1C-1B-0D-DA-E8-8F 目标地址: 1C-1B-0F-4A-E8-8F | 明天放假 2,3,4,5口接收到次消息,查看目标mac地址是否是自己的,5口确定是自己的. 每个网口都广播发送消息一遍之后,对照表就构建好,下次在任意的网口在发消息,就直接以单播的形式发送. 目的: 避免局域网内每一次都广播的形式通信.以后就可以单播,提升效率. mac地址:head报头中的mac地址 ,都会替换,了解
-
-
arp协议:
-
通过ip获取到mac地址
-
ARP协议:就是将对方的ip地址获取到对方的MAC地址 如果两个用户进行第一次通信的时候,你不可能知道对象的mac地址么? 但是你必须要知道对方的IP地址. IP + ARP协议 = 获取对方的MAC地址 源mac 目标mac 源ip 目标ip 数据部分 发送端mac: 1C-1B-0D-DA-E8-8F FF:FF:FF:FF:FF:FF 172.16.10.10/24 172.16.10.11/34 今晚一起吃饭 目标计算机 (源mac:1C-1B-0F-4A-E8-8F 目标mac 1C-1B-0D-DA-E8-8F 源ip: 172.16.10.11/34 目标ip:172.16.10.10/24)
-
-
-
3.网络层:
-
路由器
-
网络层的作用是解决客户端到服务端连接中的各个节点问题
-
IP协议:
-
ipv4 ipv6 国际互联网协议第四版
-
广播,mac地址+IP == 可以找到世界上任意一台计算机
-
IP:
- IP地址:
- ip地址+子网掩码确定计算机所在网段,子网,局域网位置
- ipv4地址:四点分十进制,范围 0-255.0-255.0-255.0-255
- 子网掩码:
- 一般都是c类
- 255.255.255.0
- ip地址+子网掩码可以计算出IP网段,可以分254个ip地址,同一局域网内的ip地址不能重复
-
-
传输层:(tcp,udp)
-
端口协议:传输层建立了点到点的连接
-
端口号:0-65535 系统占用端口:1-1023 软件端口:1024-8000
-
广播,mac地址+IP +端口 == 可以找到世界上任意一台计算机对应的软件
-
-
TCP:
-
tcp是一种面向连接,可靠的,基于IP的传输层协议
-
使用的tcp的应用:Web浏览器,文件传输
-
优点:
- 可靠的面向连接协议,稳定安全 (面向连接就是双方通信)
-
缺点
- 效率相对低,传输速度慢些
-
-
UDP:
-
面向数据报协议,无连接协议
-
使用UDP应用:域名系统,视频流,IP语言,微信,QQ
-
优点:
- 效率高,速度快,发送数据前不需要建立连接
-
缺点:
- 不安全,不可靠,容易丢包
-
应用层:
-
应用层存储的都是自己开发的程序,HTTP
-
软件自己定义的协议:
-
qq:发送数据:“海洋“ ------> [”海洋“]
-
将数据按照自己定义的协议进行封装(http ftp协议等等)
-
-
物理层 --> 数据链路层(以太网协议,mac) --> 网络层 --> 传输层( tcp,udp) --> 应用层
-
mac地址 + 广播形式 +ip地址 + 端口 ==可以找到任意一台机器
握手状态:
- seq 序号 用来表示tcp源端像目标端发送字节流,发送方发送数据对此进行标记 32位
- ACK 确认请求 只有ACK标志位等于1事,确认序号才有效,ack = seq + 1
- SYN 发送请求
- FIN 释放一个连接
- 注意:确认方=发起方req + 1 ,两端配对
三次握手:
-
客户端与服务端第一次需要建立通信联系,需要三次握手:
-
第一次握手:
- 客户端向服务端发送syn=1的请求和一个随机seq等于x的报文,进入发送状态
-
第二次握手:
- 两条请求合并成一条请求
- 服务端收到客户端发送的syn报文,给客户端回应一个ack=x+1(我收到你的报文,进行+1)的确认请求和同样还会返回syn=1的请求一个随机seq等于y的报文 ,进入接收状态
-
第三次握手:
- 客户端收到服务器syn报文和确认请求,在回应服务器一个ack = y+1报文,客户端和服务端进入建立连接状态,完成三次握手
-
三次握手意义:
- 简述:主要防止客户端已经失效的连接请求报文突然传到了服务端,而产生的错误
-
客户端向服务端发送一个连接请求,这个请求因为网络节点问题滞留了,请求在到达服务器已经是一个失效的连接,如果没有三次握手,服务器确认请求,建立连接,但是请求是失效的,客户端不会理会服务器的确认信息,也不会发送消息,服务端一直等待客户端发送数据,这样很多资源就浪费了,这就是三次握手的作用
-
为什么要进行三次握手:
- TCP协议在建立连接时,需要确认通信的双方收发信息的功能都是正常的,因此要进行三次握手
四次挥手:
-
建立的连接不能一直连接着
-
第一次挥手:
- 客户端向服务器发送FIN=1,seq序号=x 客户端停止发送数据
-
第二次挥手:
-
服务端收到FIN,回复确认请求 ack=x+1
客户端到服务端这条路断开
-
-
第三次挥手:
- 服务端向客户端发送FIN=1 ,seq=y
-
第四次挥手:
-
客户端发送ack=1 + y
服务端到客户端这条路断开
-
-
为什么要进行四次挥手:
-
tcp是一个面向连接,全双工模式,两条通道要进行两次确认才断开
-
客户端向服务端发送Fin关闭连接,服务端确认后,服务端确认后就表示服务端没有数据在发送给客户端,
-
客户端到服务端通道关闭
-
但是服务器的数据不一定都完整传给了客户端,服务器不会直接关闭,他可能还会发送数据给客户端,
-
之后,再次发送FIN报文,给客户端,客户端在同意后在关闭连接
-
服务端到客户端通道关闭
-
Socket:
-
socket 套接字就是一个网络通信的工具,建立客户端与服务端的通信 ,他存在于应用层和传输层之间的抽象层,这个通信工具封装好了所有的底层接口,可以直接调用这些接口,提高开发效率
TCP_Socket:
-
#服务端 import socket phone = socket.socket() #创建socket对象,默认tcp协议 phone.bind(("127.0.0.1",666)) #绑定IP地址和端口 phone.listen(5) #监听,开机状态,5同时可以有多少个请求连接 conn,addr = phone.accept() #建立连接,等待客户端,conn两条管道,addr客户端地址 while 1: from_client_data = conn.recv(1024) #接受消息和多少字节 print(f'来自客户端的{addr}消息{from_client_data.decode("utf-8")}') to_server = input(">>>>") conn.send(to_server.encode("utf-8")) #发送 conn.close() #关闭通道连接 phone.close() #关闭socket套接字 #客户端 import socket phone = socket.socket() phone.connect(("127.0.0.1",666)) #连接server服务端信息 while 1: to_client = input(">>>") phone.send(to_client.encode("utf-8")) #send发送 from_server_data = phone.recv(1024) #接受消息和多少字节 print(from_server_data.decode("utf-8")) phone.close()
-
通讯循环-一个对多个客户端,客户端需要排队
-
import socket phone = socket.socket() #创建socket对象,默认tcp协议 phone.bind(("127.0.0.1",666)) #绑定IP地址和端口 phone.listen(5) #监听,开机状态,5同时可以有多少个请求连接 while 1: print("开启") conn,addr = phone.accept() #建立连接,等待客户端,conn连接管道,addr客户端地址 print(conn,addr) while 1: try: #客户端异常退出报错进行异常处理 from_client_data = conn.recv(1024) #recv:接受消息和多少字节,夯住 if from_client_data == "q".encode("utf-8"): break print(f'来自客户端的{addr}消息{from_client_data.decode("utf-8")}') to_server = input(">>>>") conn.send(to_server.encode("utf-8")) #send:发送 except ConnectionResetError: break conn.close() #关闭通道连接 phone.close() #关闭socket套接字 #客户端还引用上面那个
-
执行win端命令:
-
#服务端: import subprocess import socket phone = socket.socket() #创建socket对象,默认tcp协议 phone.bind(("127.0.0.1",1666)) #绑定IP地址和端口 phone.listen(5) #监听,开机状态,5同时可以有多少个请求连接 while 1: print("开启") conn,addr = phone.accept() #建立连接,等待客户端,conn连接管道,addr客户端地址 while 1: try: cmd = conn.recv(1024) #接受消息和多少字节,夯住 obj = subprocess.Popen(cmd.decode("utf-8"), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) result = obj.stdout.read() + obj.stderr.read() conn.send(result) except ConnectionResetError: break conn.close() #关闭通道连接 phone.close() #关闭socket套接字 #客户端: import socket phone = socket.socket() phone.connect(("127.0.0.1",1666)) #连接server服务端信息 while 1: cmd = input(">>>").strip() phone.send(cmd.encode("utf-8")) #将命令转为utf-8发送 result = phone.recv(1024) #接受的消息为windows系统编码转为gbk print(result.decode("gbk")) phone.close()
粘包:
-
缓冲区作用:
-
Socket进行send和recv的时候,都要通过输出缓冲区和输入缓冲区,缓冲区可以暂时解决网络不稳定问题,提高稳定性
-
缓存区一般为8K
-
-
产生粘包的现象(只有TCP):
- 粘包发生在tcp协议中
-
产生粘包的现象有两种recv和send:
-
recv产生粘包:
- 如果你的recv最大接受值为1024字节,当你接受一个1500的字节,recv只会接受1024剩下的476个字节会停留在输入缓存区,当你第二次在发送数据,造成了数据的粘包。
-
send产生粘包:
- 当你连续send发送少量的数据,tcp会根据优化算法将数据合并成一个TCP段发送出去
-
-
解决粘包方式:
-
1、可以扩大recv的上限,但是会对服务器内存占用
-
2、客户端sleep睡眠,这样会非常影响效率
-
-
正确思路:
-
当第二次接受数据前,循环recv将所有的数据取完
-
-
recv的工作原理:
-
send多次,recv一次(不是一发一收制) 当缓存区没有数据可取时,recv会一直处于阻塞状态,直到缓冲区至少有一个字节数据可取,或者远程端关闭,关闭远程端并读取所有数据后,返回空字符
-
low版:
-
问题1:非常大的数据使用struct会报错 问题2:报头信息不可能只包含数据总大小 # 服务端 import subprocess import socket import struct phone = socket.socket() # 创建socket对象,默认tcp协议 phone.bind(("127.0.0.1", 1666)) # 绑定IP地址和端口 phone.listen(5) # 监听,开机状态,5同时可以有多少个请求连接 while 1: print("开启") conn, addr = phone.accept() # 建立连接,等待客户端,conn连接管道,addr客户端地址 while 1: try: cmd = conn.recv(1024) # 接受消息和多少字节,夯住 obj = subprocess.Popen(cmd.decode("utf-8"), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) result = obj.stdout.read() + obj.stderr.read() total_size = len(result) # 制作报头(371) # 将不固定长度的int类型报头,转化成固定4字节(b's\x01\x00\x00') total_size_bytes = struct.pack("i", total_size) conn.send(total_size_bytes) # 发送字节长度和数据信息 conn.send(result) except ConnectionResetError: break conn.close() phone.close() # 客户端: import socket import struct phone = socket.socket() phone.connect(("127.0.0.1", 1666)) # 连接server服务端信息 while 1: cmd = input(">>>").strip() phone.send(cmd.encode("utf-8")) head_bytes = phone.recv(4) # 接受报头四个字节(b's\x01\x00\x00') total_size = struct.unpack("i", head_bytes)[0] # 将报头反解回int类型(371) total_data = b'' # 循环接受原数据 while len(total_data) < total_size: total_data += phone.recv(1024) print(total_data.decode("gbk")) phone.close()
旗舰版-自定义报头版:
-
解决粘包思路:
-
1,当第二次接受数据前,循环recv将所有的数据取完
-
result 3000bytes recv 3次
result 5000bytes recv 5次
result 30000bytes recv ?次 ---> 循环次数相关
-
-
2,接受总数据bytes,进行while循环,当接受数据大于总数据,循环结束
-
3,怎么获取到总bytes个数:数据转成bytes之后len() =总bytes数
- dict报头-->“dict报头”---->json("dict报头") = 字典数据
- len(json("dict报头")) --->len()转成四个字节 = 字典报头长度
- 先发送字典报头长度,在发送字典数据
-
-
#服务端: import socket import subprocess import struct import json phone = socket.socket() phone.bind(('127.0.0.1', 8888)) phone.listen(5) while 1: print('start') conn, addr = phone.accept() while 1: try: cmd = conn.recv(1024) obj = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) result = obj.stdout.read() + obj.stderr.read() result = result.decode('gbk').encode('utf-8') head_dict = { 'MD5': 'fdsaf2345544324dfs', 'file_name': '视频文件', 'file_size': len(result), } #制作报头 head_dict_json = json.dumps(head_dict) #将报头字典转化成json序列化 head_dict_json_bytes = head_dict_json.encode('utf-8') #将json字符串 转化成bytes head_len = len(head_dict_json_bytes) #获取字典的长度(88) head_len_bytes = struct.pack('i',head_len) #将长度转化成固定的4个字节 (b'X\x00\x00\x00') conn.send(head_len_bytes) #字典报头长度--->4个字节(b'X\x00\x00\x00') conn.send(head_dict_json_bytes) # 字典数据bytes conn.send(result) #发送原数据bytes except ConnectionResetError: break conn.close() phone.close() #客户端: import socket import struct import json phone = socket.socket() phone.connect(('127.0.0.1', 8888)) while 1: cmd = input('>>>').strip() phone.send(cmd.encode('utf-8')) jie_bytes = phone.recv(4) total_size = struct.unpack('i', jie_bytes)[0] #接收字典报头长度,struct转化为int head_bytes = phone.recv(total_size) #接受字典数据,total_size字典报头长度 head_dict = json.loads(head_bytes.decode("utf-8")) #将json字典转为字典 total_size = head_dict["file_size"] #通过字典取出要发送过来数据的总长度 recv_data = b'' #循环累加接受原数据 while len(recv_data) < total_size: #不等于,当等于还会进去取值,这时值已经没有了 recv_data += phone.recv(1024) print(recv_data.decode('utf-8')) phone.close()
UDP_Socket:
-
udp协议:不可靠,相对来说不安全的协议,面向数据报无连接协议(可靠不可靠是指数据是否能到达)效率高,速度快
-
基础通信:
-
UDP通信没有tcp连接通道,服务端和客户端每次发送都需要带着IP地址和端口号
-
#服务端: import socket udp_server = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) #创建udp协议对象 udp_server.bind(("127.0.0.1",666)) while 1: from_client_data = udp_server.recvfrom(1024) #(b'niha', ('127.0.0.1', 64180)) print(f'来自{from_client_data[1]}{from_client_data[0].decode("utf-8")}') to_client_data = input("输入").strip() udp_server.sendto(to_client_data.encode("utf-8"),from_client_data[1]) #客户端: import socket udp_client= socket.socket(socket.AF_INET,socket.SOCK_DGRAM) #创建udp协议对象 while 1: to_client_data = input("输入").strip() udp_client.sendto(to_client_data.encode("utf-8"),("127.0.0.1",666)) from_server_data = udp_client.recvfrom(1024) print(f'来自{from_server_data[1]}的消息:{from_server_data[0].decode("utf-8")}')
-
Socket_Server多线程通信:
-
#服务端: import socketserver class MyServer(socketserver.BaseRequestHandler): # 继承的类固定的 def handle(self): # 必须是这个handle名字,类的约束 while 1: from_client_data = self.request.recv(1024).decode('utf-8') #self.request == conn管道 print(from_client_data) to_client_data = input('>>>').strip() self.request.send(to_client_data.encode('utf-8')) if __name__ == '__main__': ip_port = ('127.0.0.1',8848) server = socketserver.ThreadingTCPServer(ip_port,MyServer) # server.allow_reuse_address = True # print(socketserver.ThreadingTCPServer.mro()) # [ThreadingTCPServer, ThreadingMixIn,TCPServer, BaseServer] server.serve_forever() #客户端: import socket client = socket.socket() client.connect(('127.0.0.1', 8848)) while 1: content = input('>>>').strip() client.send(f'海洋:{content}'.encode('utf-8')) from_server_data = client.recv(1024) # 夯住,等待服务端的数据传过来 print(f'来自服务端消息:{from_server_data.decode("utf-8")}') phone.close()