网络编程
一、软件开发的架构
1. C/S架构:
Client(客户端)与Server(服务端)架构这种架构也是从用户层面(物理层面)划分的,
客户端泛指客户应用程序EXE,对用户的电脑操作系统环境依赖较大
2. B/S架构:
Browser(浏览器端)与Server端架构,它隶属于C/S架构,Browser也是一种CLient,
但不需要安装什么应用程序,只需要在浏览器上通过HTTP请求服务器端相关的资源(网页资源)
3. B/S架构有点:
统一了应用的入口(是一个趋势)
网络编程基础
二、通信
1. 同一台电脑两个py文件通信:——文件 a.py———t.txt———b.py
2. 早期: 联机(两台电脑用一根网线连接)
3. 以太网:局域网与交换机(几十台电脑通信)
1) 交换机通信:
- 广播: 主机间‘一对所有’的通讯模式(吼一嗓子)
- 单播: 方向固定的向某一个主机发送
- 组播: 向某一部分或某一组主机发送
2) 同一局域网在同一网段(192.168.12.xx)
- 一个网段最多放256台电脑(0--255)
- 局域网是指在某一区域内由多台计算机互联成的计算机组,一般是方圆几千米内
3) ip地址与ip协议:
- 规定网络地址的协议叫IP协议,他定义的地址称之为ip地址,广泛采用ipv4,他规定网络地址由32位2进制表示
- 范围: 0.0.0.0----255.255.255.255
一个IP地址通常写成四位点分十进制数字
4) mac地址:
- Ethernet规定接入Internet的设备都必须具备网卡,发送端和接收端的地址便是值网卡的地址,即mac地址
- 每块网卡出厂时都被烧制上一个全世界唯一的mac地址,(物理地址)长度为48位2进制,通常由12位16进制数表示(前六位是厂商编号,后六位是流水线号)
5) arp协议:
- 地址解析协议,是根据IP地址获取物理地址的一个TCP/IP协议
6) 子网掩码:
- 计算机根据IP地址产生的表示网络特征的一个参数,形式上等同于IP地址,他的网络部分全部为1,主机部分全部为0.(255.255.255.0)
7) 网段:
- IP地址和子网掩码进行 &(与) 计算
ip: 192. 168. 12. 84
11000000. 101010000. 00001100. 01010100
===============> 11000000. 10101000. 00001100. 00000000(与0得0)
===========>网段: 192. 168. 12. 0
子网: 255. 255. 255. 0
11111111. 11111111. 11111111. 00000000
8) 查看地址命令: ipconfig - all
4. 多个局域网之间通信: 广域网: 交换机 + 路由器 + 代理ip
1) 路由器(网关设备):
- 连接因特网中各局域网,广域网的设备,路由器通过路由表解析去判断网络地址和选择ip路径,
只接收源站或其他路由器的信息,一个路由器可以组成一个局域网
- 访问百度: 主机找路由器 ——> 找代理ip ——> 通过代理ip找到百度的路由器
三、TCP协议和UDP协议(发送数据的协议)
- TCP应用: web浏览器、电子邮件、 文件传输
- UDP应用: 域名系统(DNS)、视频流、IP语音
1. 端口:
- 计算机每一个程序启动都有一个随机且唯一的端口号(port)
- 端口号范围(0-65535)
- IP地址 + port ——> 唯一确定某个电脑上的某一程序。允许开发人员使用的端口号范围从8000开始(8000--10000)
2. TCP协议:
- 当应用程序希望通过TCP与另一个应用程序通信是,它会发送一个通信请求,这个请求必须被送到一个确切的地址,在双方“握手”
之后,TCP将在两个应用程序之间建立一个全双工的通信,TCP提供超时重发,丢弃重复数据,检验数据,流量控制等功能,保证数据能从一端传到另一端
— 建立连接三次握手 —— 数据传输 —— 断开连接四次挥手
1) 三次握手:
① 客户端发送SYN(SEQ = x)报文给服务器端,进入SYN-SEND状态
② 服务器端收到SYN报文,回应一个SYN(SEQ=y)ACK(ACK=x+1)报文,进入SYN-RECV状态
③ 客户端收到服务期短的报文,回应一个ACK(ACK=y+1)报文,进入Established状态
2) 四次挥手:(终止连接需四次挥手,这由TCP的半关闭造成)
① 某个应用程序首先调用close,称该端执行‘主动关闭’,该端的TCP于是发送一个FIN分节,表示数据发送完毕
② 接收到这个FIN的对端执行‘被动关闭’,这个FIN由TCP确认
③ 一段时间后,接收到这个文件结束符的应用进程将调用close关闭他的套接字,他的TCP也发送一个FIN
④ 接受这个最终FIN的原发送端TCP(执行主动关闭的那一端)确认这个FIN,
- TCP三次握手 / 四次挥手 socket客户端向服务端发起连接请求:三次握手 client.connect((......)) --------------------------------------- 客户端 服务端 | | 能睡你吗 来啊 好的,我来了 ---------------------------------------- client.send("发送数据") 收发数据 收发数据 客户端和服务端断开连接: 四次挥手 client.close() 或 conn.close() --------------------------------------- 客户端 服务端 | | 我要断开连接 断开就断开,等我处理一些事 。。。。 我处理完了,断开吧 拜拜 补充:断开连接时,反映到代码上: 抛出异常/发送空内容
3. UDP协议:
- 当应用程序希望通过UDP与另一个应用程序通信时,传输数据之前源端和终端不建立连接,当它想传送时就简单的
去抓取来自应用程序的数据,并尽可能快的把它扔到网络上
4. 对比:
- TCP:
① 传输控制协议,提供的是面向连接,可靠的字节流服务
② 建立全双工的通信(谁先通信都可以)
③ 不允许在同一时间点同时和多个客户端连接通信
- UDP:
① 用户数据报协议,是面向数据报的传输层协议,不可靠
② 只能客户端先通信,服务器端才能知道地址找到客户端
③ 允许在同一时间点同时和多个客户端连接通信
④ 没有超时重发等机制,不用建连接,所以传输速度很快
四、互联网协议与osi模型
- 按功能不同分为osi七层模型或tcp/ip五层或tcp/ip四层
- OSI七层模型
7层:
自己写的代码: 自己代码 + 框架
- 应用层:py文件,https,http,使用软件
- 表示层:看到数据,如图片和视频
- 会话层:保持登录或链接状态
socket模块:
- 传输层:TCP/UDP协议,端口
- 网络层:IP协议,IP地址,路由器,三层交换机
- 数据链路层:MAC,arp协议,网卡,二层交换机
- 物理层:网卡,电信号,hub集线器,将数据转换成电信号发送
5层:
- 应用层
- 应用层 - 表示层
- 会话层
- 传输层
- 网络层
- 数据链路层
- 物理层
4层:
- 应用层
- 应用层 - 表示层
- 会话层
- 传输层
- 网络层
- 物理层 - 数据链路层
- 物理层
五、套接字(socket)
- socket 位于应用层和传输层之间的虚拟层的接口,socket是应用层与TCP/IP协议簇通信的中间软件抽象层,
它是一组接口,其实socket就是一个模块
- 分类:
- 基于文件类型的套接字家族,AF_UNIX(unix里一切皆文件)
- 基于网络类型的套接字家族:AF_INET,现只使用网络的socket里的family参数
六、套接字初使用
1)TCP
- server端:
import socket # 创建服务端socket对象 server = socket.socket() # 绑定IP和端口 server.bind(('127.0.0.1',8000)) # 后面可以再等5个人 server.listen() #等待客户端连接,如果没人来就一直死等着 # conn是客户端和服务端连接的对象(伞),服务端以后要通过该对象进行收发数据 # addr是客户端的地址信息 conn,addr = server.accept() # 通过对象去获取(通过伞给我发送的消息) # 1024表示:服务端通过对象(伞)获取数据时,一次性做多拿1024字节 data = conn.recv(1024) print(data) # 服务端通过连接对象(伞)给客户端回复了一条消息 conn.send(b'stop') #与服务端断开连接(断开伞) conn.close() #关闭服务器的服务 server.close()
- client端
import socket # 创建套接字 client = socket.socket() # 向服务端发阿松连接请求(递伞) # 阻塞,去连接,直到连接成功才继续往下走 client.connect(('127.0.0.1',8000)) # 连接上服务端以后,向服务端发送消息 client.send(b'love you') data = client.recv(1024) print(data) # 关闭自己 client.close()
2)UDP
- server端:
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8000))
msg,addr = sk.recvfrom(1024)
print(msg.decode('utf-8'))
sk.sendto(b'hi',addr)
sk.close()
- client端:
import socket ip_port = ('127.0.0.1',8000) sk = socket.socket(type=socket.SOCK_DGRAM) sk.sendto(b'hello',ip_port) msg,addr = sk.recvfrom(1024) print(msg.decode('utf-8'),addr) sk.close()
3)解决UDP协议反复代码问题
from socket import * class My_socket(socket): def __init__(self,coding='utf-8'): self.coding = coding super(My_socket,self).__init__(type=SOCK_DGRAM) def my_recv(self,num): msg,addr = self.recvfrom(num) return msg.decode(self.coding),addr def my_send(self,msg,addr): return self.sendto(msg.encoding(self.coding),addr) sk = My_socket() msg,addr = sk.my_recv(1024)
七、远程执行命令
import subprocess r = subprocess.Popen( 'dir',shell=True, # 告诉系统把我要执行的命令当坐系统命令来执行 stdout = subprocess.PIPE, # 正确的结果 stderr = subprocess.PIPE) # 错误的结果 print(r.stderr.read().decode('gbk')) # 读一次就没了,以后取不到了, print(r.stderr.read().decode('gbk')) # 若想重复使用,需赋值给一个变量
八、黏包(只有TCP有黏包,UDP永远不会黏包)
1. 成因:
1)TCP协议的拆包机制:
- 当发送端缓冲区的长度大雨网卡的MTU时,TCP会将这次发送的数据拆成几个数据包发送出去,MTU是网络上传送的最大数据包,
单位是字节,大部分网络设备MTU是1500,如果本机的MTU比网关的MTU大,大的数据包就会被拆开来传送,这样会产生很多数据
包碎片,增加丢包率,降低网速
2)面向流的通信特点和Nagle算法
- 发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法,(Nagle算法),将多次间隔较小且数据量小的数据,
合并成一个大的数据块,然后进行封包,而接收端必须要有科学的拆包机制才能分辨,否则会黏包。
2.现象:
① client连续发送少量数据,时间间隔短,Nagle算法进行合包,server端不知要接收多少,造成数据混乱
② client直接发送大量数据,但server端接收的长度不够造成数据混乱
3. TCP和UDP区别:
① TCP基于数据流的,收发的消息不能为空,而UDP可以为空,会封装一个(ip+port)的消息头发送过去
② UDP最大发送数据长度为:65535 - IP头(20) - UDP(头)(8):65507字节。若大于此长度,会报错,而TCP不限制大小
4. 黏包的解决方案
- 发送端在发送数据之前,把自己将要发送的字节流总大小让接收端知晓,然后接收端死循环接受完所有数据
- struct模块: 把一个类型,如数字,转成固定长度的bytes。
- 发送时: 先发送struct转换好的数据,长度4字节,再发送数据
- 接收时: 先接收4个字节使用struct.unpack转换成数字来获取要接收的长度,再按照长度接受数据
b1 = struct.pack('i',123456) # 4字节bytes长度 b1 = struct.ubpack('i',b1) # 转换回来
九、socket的常用方法
sk.bind() # 绑定到套接字 sk.listen() # 监听 sk.accept() # 阻塞等待连接 sk.connect() # 主动初始化TCP服务器的连接 sk.connect_ex() # connect()扩展版,出错时返回出错码,不报错 sk.recv() # 接收TCP数据 sk.send() # 发送TCP数据 sk.sendall() # 发送TCP数据 sk.recvfrom() # 接收UDP数据 sk.sendto() # 发送UDP数据 sk.getpeername() # 连接到当前套接字的远端的地址 sk.getsockname() # 当前套接字的地址 sk.getsockopt() # 返回指定套接字的参数 sk.setsockopt() # 设置指定套接字的参数 sk.close() # 关闭套接字 sk.setblocking(True或False) # 设置阻塞或非阻塞模式 sk.settimeout() # 设置阻塞套接字操作的超时时间 sk.gettimeout() # 得到() sk.fileno() # 套接字的文件描述符 sk.makefile() # 创建一个与该套接字有关的文件
十、验证客户端连接的合法性
import hmac # 相当于md5 name = '光头' pwd = b'taibai' md5_v = hmac.new(name.encode('utf-8'),pwd) r = md5_v.digest() # bytes类型
- 将服务器加完盐的密文与客户端的密文进行一致性验证
十一、socketserver
- server端:
import socketserver class Myserver(socketserver.BaseRequestHandler): def handlf(self): pass # 用self.request进行数据的收发 if __name__ == '__main__': HOST,PORT = '127.0.0.1',9999 # 设置allow_reuse_address允许服务器重用地址 socketserver.TCPServer.allow_reuse_address = True server = socketserver.TCPServer((HOST,PORT),Myserver) server.serve_forever() # 让server永远执行,除非强制停止
- client端:
import socket HOST, PORT = "127.0.0.1", 9999 data = "hello" # 创建一个socket链接,SOCK_STREAM代表使用TCP协议 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.connect((HOST, PORT)) # 链接到客户端 sock.sendall(bytes(data + "\n", "utf-8")) # 向服务端发送数据 received = str(sock.recv(1024), "utf-8") # 从服务端接收数据 print("Sent: {}".format(data)) print("Received: {}".format(received))