day05网络编程
一. 网编三要素
ip
概述
设备(电脑, 手机, IPad, 耳机...)在网络中的唯一标识.
分类
按照 代数 划分:
IPv4: 4字节, 十进制来表示, 例如: 192.168.13.157 IPv6: 8字节, 十六进制来表示, 理论上来讲, 可以让地球上的每一粒沙子都有自己的IP.
Ipv4 常用类别划分:
城域网: 第1段是网络地址 + 后3段是主机地址, 例如: 10.0.0.0 广域网: 前2段是网络地址 + 后2段是主机地址, 例如: 10.21.0.0 局域网: 前3段是网络地址 + 后1段是主机地址, 例如: 192.168.13.*
IP相关命令
查看本机的IP: ipconfig 适用于 windows系统 ifconfig 适用于 linux系统, Mac系统测试网络连接是否通畅: ping 主机地址 或者 域名
端口号
端口:
传输数据的通道, 每个程序都有
端口号:
应用程序在设备中的唯一标识
范围:
所有范围: 0 ~ 65535, 但是0 ~ 1023已经被占用(或预留端口),
自己用1024 ~ 65535
协议
UDP: 类似于群聊
-
面向无连接
-
采用数据包的形式传输数据, 有大小限制(每个包不超过64KB)
-
不安全(不可靠)协议
-
效率相对较低
-
不区分客户端和服务端, 叫: 发送端和接收端
TCP: 类似于打电话
-
面向有连接的
-
采用 流的方式传输数据, 理论上无大小限制
-
安全(可靠)协议
-
效率相对较低
-
区分客户端和服务器端
查看端口号和协议
netstat -ano
面试题
三次握手
自己:
-
客户端请求建立连接
-
服务器处理(校验客户端数据是否合法)并发送是否可以建立连接
-
服务器允许建立连接 后, 客户端重新请求建立连接通道
讲义:
-
客户端向服务端发送请求,等待服务端确认。
-
服务端收到请求后知道客户端请求建立连接,回复给客户端以确认连接请求。
-
客户端收到确认后,再次发送请求确认服务端,服务端收到正确请求后,如果正确则连接建立成功,完成三次握手,随后客户端与服务端之间可以开始传输数据了。
四次挥手
讲义:
-
当主机A(可以是客户端也可以是服务端)完成数据传输后, 提出停止TCP连接的请求
-
第二次挥手: 主机B收到请求后对其作出响应,确认这一方向上的TCP连接将关闭
-
第三次挥手: 主机B 端再提出反方向的连接关闭请求
-
第四次挥手: 主机A对主机B 的请求进行确认,双方向的关闭结束
图解
二. socket套接字
进程之间通信工具
原理
网络通信也叫套接字通信, Socket通信, 即: 通信双方都有自己的Socket对象, 数据在两个Socket之间通过 数据报包 或者 流(字节流) 的方式传输数据
步骤
图解
客户端
步骤
-
创建客户端Socket对象.
-
连接(服务器端的)ip和端口号, 元组形式.
-
发送数据给服务器端.
-
接受服务器端发送的数据. 回执信息.
-
释放资源.
代码
127.0.0.1 本地回路(回环)地址, 即: 在哪台计算机上运行, 就代表哪台电脑. 简单理解: 代表本机.
import socket # 在main中测试. if __name__ == '__main__': # 1. 创建客户端Socket对象, 指定: 地址族, 传输类型. client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 2. 连接(服务器端)地址和端口. # client_socket.connect(("127.0.0.1", 2121)) client_socket.connect(("192.168.13.157", 2121)) # 3. 接受服务器端发送的数据并打印. # 分解版 # recv_data_bys = client_socket.recv(1024) # recv_data = recv_data_bys.decode("utf-8") # 合并版 recv_data = client_socket.recv(1024).decode("utf-8") print(f'客户端收到: {recv_data}') # 4. 给服务器端发送数据(回执信息). client_socket.send('有内鬼, 终止交易! Over'.encode("utf-8")) # 5. 关闭Socket对象. client_socket.close()
服务器端
步骤
-
创建服务器端Socket对象.
-
绑定(服务器端的)ip和端口号, 元组形式.
-
设置监听数量.
-
等待客户端申请建立连接, 如果有客户端申请建立连接, 校验数据合法后, 会返回1个: (负责和该客户端交互的socket对象, 客户端的信息) 元组
-
给客户端发送数据. 字节形式.
-
接收客户端发送的数据. 字节形式.
-
释放资源.
代码
# 导包. import socket # 在main中编写. if __name__ == '__main__': # 1. 创建服务器端Socket对象, 指定: 地址族, 传输类型. # 参1: IPV4, 参2: 字节流 server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 2. 绑定地址和端口. # server_socket.bind(("127.0.0.1", 2121)) server_socket.bind(("192.168.13.157", 2121)) # 3. 设置监听数量. 范围: 1 ~ 128 server_socket.listen(5) # 4. 等待客户端连接, 如有连接, 则返回: (和客户端交互的Socket对象, 客户端地址) print('等待客户端连接中.....') # print('Server 1') accept_socket, client_info = server_socket.accept() # print('Server 2') # 5. 给客户端发送数据. # accept_socket.send('Welcome to study socket!'.encode('utf-8')) accept_socket.send(b'Welcome to study socket!') # 6. 接受客户端发送的数据并打印. recv_data_bys = accept_socket.recv(1024) # receive: 接收, 一次读取1024个字节 # 把字节转成字符串, 并打印. recv_data = recv_data_bys.decode('utf-8') print(f'服务器端收到 {client_info} 发送的信息: {recv_data}') # 7. 关闭Socket对象. accept_socket.close() # 一般只关闭 和客户端交互的socket对象. # server_socket.close() # 服务器端socket对象一般不关闭.
编解码
概述
编解码指的是 字符串 和 二进制数据之间相互转换.编码: 字符串(我们能看懂的) ==> 二进制(计算机能看懂)解码: 二进制(计算机能看懂) ==> 字符串(我们能看懂的)
细节
-
码表 = 字符 + 数字, 即: 码表存储的就是该字符对应的数字.例如(假设): 存数据流程: 中 => 按照码表, 转成123456(十进制) => 0101 1011(二进制, 源码) => 二进制反码 => 二进制补码 => 存储到计算机中. 读数据流程: 读取二进制数据(补码) => 二进制反码 => 二进制源码 => 转成十进制(例如: 123456) => 根据码表, 找到字符 '中'
-
在GBK码表(针对于国内)中, 1个中文占2个字节, 在UTF-8码表(万国码, 统一码)中, 中文占3个字节.
-
英文字母, 数字, 特殊符号无论在什么码表中, 都只占1个字节.
-
如果遇到乱码情况了, 原因只有1个: 编解码不一致.
-
针对于二进制数据, 语法糖写法为: b'内容', 但是该形式只针对于 英文字母, 数字, 特殊符号 有效, 针对于中文无效.
代码
# 需求1: 演示编码. s1 = '黑马aB1!' bys1 = s1.encode() # bytes: 字节数组, 默认码表: utf-8 bys2 = s1.encode('gbk') bys3 = s1.encode('utf-8') print(f'bys1: {bys1}') # b'\xe9\xbb\x91\xe9\xa9\xac aB1!' print(f'bys2: {bys2}') # b'\xba\xda\xc2\xed aB1!' print(f'bys3: {bys3}') # b'\xe9\xbb\x91\xe9\xa9\xac aB1!' print("-" * 21) # 需求2: 解码 bys4 = b'\xe9\xbb\x91\xe9\xa9\xacaB1!' s2 = bys4.decode(encoding='utf-8') s3 = bys4.decode(encoding='gbk') print(f's2: {s2}') # 黑马aB1! print(f's3: {s3}') # 榛戦┈aB1! print("-" * 21) # 需求3: 二进制的特殊写法. bys5 = b'aB1!' print(type(bys5)) # <class 'bytes'>
端口复用
解释
当服务器端关闭的时候, 端口号释放可能需要1 ~ 2分钟的时间, 如果此时再次开启服务器端, 可能会报: 端口号占用的错误. 解决方案如下: 1. 重新手动更换1个端口号. 2. 设置端口号重用(复用).
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
代码
# 导包. import socket # 在main中编写. if __name__ == '__main__': # 1. 创建服务器端Socket对象, 指定: 地址族, 传输类型. # 参1: IPV4, 参2: 字节流 server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 2. 绑定地址和端口. # server_socket.bind(("127.0.0.1", 2121)) server_socket.bind(("192.168.13.157", 2121)) # 3. 设置监听数量. 范围: 1 ~ 128 server_socket.listen(5) # 4. 等待客户端连接, 如有连接, 则返回: (和客户端交互的Socket对象, 客户端地址) print('等待客户端连接中.....') # print('Server 1') accept_socket, client_info = server_socket.accept() # print('Server 2') # 5. 给客户端发送数据. # accept_socket.send('Welcome to study socket!'.encode('utf-8')) accept_socket.send(b'Welcome to study socket!') # 6. 接受客户端发送的数据并打印. recv_data_bys = accept_socket.recv(1024) # receive: 接收, 一次读取1024个字节 # 把字节转成字符串, 并打印. recv_data = recv_data_bys.decode('utf-8') print(f'服务器端收到 {client_info} 发送的信息: {recv_data}') # 7. 关闭Socket对象. accept_socket.close() # 一般只关闭 和客户端交互的socket对象. # server_socket.close() # 服务器端socket对象一般不关闭. # 8. 设置端口号复用. # 参1: 当前的套接字对象. # 参2: 参数名, 即: 设置端口号复用. # 参3: 参数的值, True: 启用 server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
多客户端
代码
import socket def tcp_server(): # 创建套接字对象 tss = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 绑定IP与端口号 tss.bind(('', 2121)) # 监听客户端连接 tss.listen(128) while True: try: # 接收客户端信息 new_s, ip_port = tss.accept() # 发送数据给客户端 new_s.send('back_data'.encode(encoding='utf-8')) # 接收客户端信息 res = new_s.recv(1024) res_data = res.decode('utf-8') print(f'{ip_port}: {res_data}') new_s.close() tss.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) except: print('断开连接') # 释放资源 tcp_server()
长连接
客户端
import socket if __name__ == '__main__': # 套接字对象 cli_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 连接服务器端口号 cli_socket.connect(('127.0.0.1', 6666)) # 发送信息到服务端 while True: data = input('客户端输入信息:') cli_socket.send(data.encode('utf-8')) if data == '886': break cli_socket.close()
服务端
import socket if __name__ == '__main__': # 套接字对象 ser_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 绑定端口号 ser_socket.bind(('127.0.0.1', 6666)) # 设置监听 ser_socket.listen(128) # 启动监听 accept_socket, client_info = ser_socket.accept() # 发送信息到服务端 while True: recv_data = accept_socket.recv(1024).decode('utf-8') print(f'服务端收到信息: {recv_data}') if recv_data == '886': break accept_socket.close()
文件
客户端
import socket if __name__ == '__main__': # 实例化套接字对象 cli_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 连接到服务端 cli_socket.connect(('127.0.0.1', 8848)) # 读取数据源数据, 并发送到服务端 with open('data/data.txt', 'rb') as src_f: # cli_socket.send('hang.txt'.encode('utf-8')) # 分批读取 while True: # 读取数据 data = src_f.read(1024) # 发送到服务器 cli_socket.send(data) # 读取到空结束 if len(data) <= 0: break cli_socket.close()
服务端
import socket if __name__ == '__main__': # 实例化套接字对象 ser_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 连接到服务端 ser_socket.bind(('127.0.0.1', 8848)) # 设置监听 ser_socket.listen(1) # 启动监听, 等待客户端连接 accept_socket, client_info = ser_socket.accept() # 接收客户端数据 # 关联目的地文件, 用于把客户端的数据写到文件中 with open('./data/file.txt', 'wb') as dest_f: while True: # 接收客户端数据 res = accept_socket.recv(1024) if len(res) <= 0: break else: dest_f.write(res) accept_socket.close()
多客户端
服务端
import socket if __name__ == '__main__': # 实例化套接字对象 ser_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 连接到服务端(IP可以不写) ser_socket.bind(('', 8888)) # 设置监听 ser_socket.listen(10) # 启动监听, 等待客户端连接 try: cnt = 0 while True: cnt += 1 filename = './data/文件_'.join(str(cnt) + 'txt') accept_socket, client_info = ser_socket.accept() # 接收客户端数据 # 关联目的地文件, 用于把客户端的数据写到文件中 with open(filename, 'wb') as dest_f: while True: res = accept_socket.recv(1024) if len(res) <= 0: break dest_f.write(res) accept_socket.send('文件上传成功'.encode('utf-8')) print(f'服务器端收到客户端:{client_info}的文件, 接收成功') accept_socket.close() except: pass
三. 进程
每个软件都可以看作是一个进程(数据隔离)
软件内的多个任务可以看作是多个线程(数据共享)
单核CPU: 宏观并行, 微观并发
真正的并行必须有多核CPU
多任务介绍
概述
多任务指的是, 多个任务同时执行
目的
节约资源, 充分利用CPU资源, 提高效率
表现形式
并发
针对于单核CPU来讲, 如果有多个任务同时请求执行, 但是同一瞬间CPU只能执行1个(任务), 于是就安排他们交替执行.
因为时间间隔非常短, 所以宏观上看是并行, 但是微观上还是并发的.
并行
针对多核CPU来讲, 多个任务可以同时执行
进程介绍
概述
进程: 值得是可执行程序, 也是CPU分配资源的最小单位.
线程: 进程的执行路径, 执行单元
解释:
进程: 车
线程: 车道
多进程实现步骤
-
导包(multiprocessing)
-
创建进程对象, 关联 该进程要执行的任务(函数)
-
启动进程执行任务
代码
import multiprocessing, time # 定义代码函数 def coding(): for i in range(1, 21): time.sleep(1) print(f'正在敲代码---- {i}') # 定义音乐函数 def music(): for i in range(1, 21): time.sleep(1) print(f'正在听音乐**** {i}') if __name__ == '__main__': # 创建进程对象 p1 = multiprocessing.Process(target=coding) p2 = multiprocessing.Process(target=music) # 执行进程 p1.start() p2.start()
进程参数
参数
target: 用于关联 进程要执行的任务的.name: 进程名, 默认是: Process-1, Process-2,...., 可以手动修改, 一 般不改.args: 可以通过 元组 的形式传递参数, 实参的个数 及 对应的数据类型 要和 形参的个数及类型 一致.kwargs: 可以通过 字典 的形式传递参数, 实参的个数 要和 形参的个数 一致.
代码演示
import multiprocessing, time def coding(name, num): for i in range(1, num): time.sleep(0.01) print(f'{name}正在敲第{i}行代码-') def music(name, num): for i in range(1, num): time.sleep(0.01) print(f'{name}正在听第{i}首音乐********') if __name__ == '__main__': p1 = multiprocessing.Process(target=coding, args=('小明', 21)) p2 = multiprocessing.Process(target=music, kwargs={'num': 21, 'name': '小明'}) p11 = multiprocessing.Process(target=coding, args=('小明', 21), name='QQ') p22 = multiprocessing.Process(target=music, kwargs={'num': 21, 'name': '小明'}, name='WX') print(f'p1:{p1.name}') print(f'p2:{p2.name}') print(f'p11:{p11.name}') print(f'p22:{p22.name}') p1.start() p2.start()
main进程
解释
main程序入口也相当于一个进程, 在程序执行时遇到自定义进程会发生资源抢占, 上述代码中在输出进程对象后, 进程才启动, 所以上述的自定义进程不会和main进程强制资源, 并且自定义进程的启动需要一定时间, 此时的main进程可能已经完成自己的任务, 执行自定义进程.
图解
代码
import multiprocessing, time def coding(name, num): for i in range(1, num): time.sleep(0.01) print(f'{name}正在敲第{i}行代码-') def music(name, num): for i in range(1, num): time.sleep(0.01) print(f'{name}正在听第{i}首音乐********') if __name__ == '__main__': p1 = multiprocessing.Process(target=coding, args=('小明', 11)) p2 = multiprocessing.Process(target=music, kwargs={'num': 11, 'name': '小明'}) # p11 = multiprocessing.Process(target=coding, args=('小明', 11), name='QQ') # p22 = multiprocessing.Process(target=music, kwargs={'num': 11, 'name': '小明'}, name='WX') p1.start() p2.start() # print(f'p1:{p1.name}') # time.sleep(0.1) # print(f'p2:{p2.name}') # time.sleep(0.1) # print(f'p11:{p11.name}') # time.sleep(0.1) # print(f'p22:{p22.name}') for i in range(1, 21): print(f'main: ------- {i}') time.sleep(0.1)
注: 上述代码中, 若将main中的循环放到自定义进程前, 则自定义进程不会和main抢占资源, 因为执行完main中的循环才执行到自定义进程, 可以把main函数看作栈, 从上到下执行
代码执行结果(每次可能都不一样)