网络与并发编程
- 1. 网络编程
- 1.1 网络基础知识
- 1.1.1 什么是网络
- 1.2.3 网络功能
- 1.2.3 网络分类
- 1.2.4 网络性能衡量指标
- 1.2.5 网络编程中的几个关键概念
- 1.2.6 网络通信要解决的问题
- 1.2.7 网络通信协议
- 1.1.8 网络通信标准
- 1.1.9 通信地址
- 1.2 UDP 传输方法
- 1.2.1 套接字简介
- 1.2.2 UDP套接字编程
- 1.2.3 UDP套接字特点
- 1.3 TCP 传输方法
- 1.3.1 TCP通信过程
- 1.3.2 TCP服务端
- 1.3.3 TCP客户端
- 1.3.4 TCP套接字细节
- 1.3.5 TCP与UDP对比
- 1.4 数据传输过程
- 1.4.1 传输流程
- 1.4.2 TCP协议首部信息
- 2. 多任务编程
- 2.1 进程(Process)
- 2.1.1 进程概述
- 2.1.2 多进程编程
- 2.1.3 进程相关函数
- 2.1.4 创建进程类
- 2.1.5 进程间通信
- 2.2 线程 (Thread)
- 2.2.1 线程概述
- 2.2.2 多线程编程
- 2.2.3 创建线程类
- 2.2.4 线程互斥锁
- 2.2.5 GIL问题
- 2.2.6 进程线程的区别联系
- 3. 网络并发模型
- 3.1 网络并发模型概述
- 3.2 多进程/线程并发模型
- 4. web服务
- 4.1 HTTP协议
- 4.1.1 协议概述
- 4.1.2 网页访问流程
- 4.1.2 HTTP请求
- 4.1.3 HTTP响应
- 5. 高并发技术探讨
- 5.1 高并发问题
- 5.2 更高并发的实现
1. 网络编程
今天我们处在互联网非常发达的时代,绝大多数程序无法离开网络运行。掌握网络编程技术是程序员必备的专业技能。
1.1 网络基础知识
互联网(又译作因特网)是Internet的中文译名,它的前身是20世纪60年代末美国国防部高级研究计划局(ARPA)主持研制的ARPAnet。1974年,出现了连接分组网络的协议,其中就包括了TCP/IP——著名的网际互联协议IP和传输控制协议TCP。这两个协议相互配合,其中,IP是基本的通信协议,TCP是帮助IP实现可靠传输的协议。1983年,ARPAnet分成两部分:一部分军用,称为MILNET;另一部分仍称ARPAnet,供民用。Internet的发展引起了商家的极大兴趣。1992年,美国IBM、MCI、MERIT三 家公司联合组建了一个高级网络服务公司(ANS),建立了一个新的网络,叫做ANSnet,成为Internet的另一个主干网。它与NSFnet不 同,NSFnet是由国家出资建立的,而ANSnet则是ANS 公司所有,从而使Internet开始走向商业化。
1.1.1 什么是网络
- 网络的定义:将多个节点通过特定的介质联系起来的一种关系,例如:铁路网、交通网、人际关系网
- 计算机网络:以计算设备作为节点,通信线路作为介质的网络
- 英特网:把全世界许多网络连到一起的网络
1.2.3 网络功能
- 数据与信息的传输
- 实现资源共享
- 打破时空限制,优化资源配置
1.2.3 网络分类
按照范围
- 局域网:局域网(Local Area Network,简写做LAN)自然就是局部地区形成的一个区域网络,其特点就是分布地区范围有限,可大可小,大到一栋建筑楼 与相邻建筑之间的连接,小到可以是办公室之间的联系。局域网自身相对其他网络传输速度更快,性能更稳定,框架简易,并且是封闭性。
- 城域网:城域网(Metropolitan Area Network)是在一个城市范围内所建立的计算机通信网,简称MAN,可以理解为一种大型的LAN。
- 广域网:广域网(英语:Wide Area Network,缩写为 WAN),又称外网、公网。是连接不同地区局域网或城域网计算机通信的远程网。通常跨接很大的物理范围,所覆盖的范围从几十公里到几千公里,它能连接多个地区、城市和国家,或横跨几个洲并能提供远距离通信,形成国际性的远程网络。
按照使用者
- 公用网 (public network) :开放性网络,互联互通,比如:互联网,教育网等,挂载公共网络上电脑,容易被侵入
- 专用网 (private network) :封闭性网络,用专线连接各个子网,比如:军队专网、政府专网、公司内部网络,防止外部侵入
1.2.4 网络性能衡量指标
- 带宽:通信信道支持的最高数据频率(Mb/s, kb/s, Gb/s)
- 传输速率:每秒传输多少个bit数据
- 吞吐量:单位时间内通过某个网络的数据量
- 时延
- 传输时延:发送数据时候,到完成发送
- 传播时延:电磁波、电信号传输需花费的时间
- 处理时延:网络数据交换节点存储、转发所必需的处理时间
- 排队时延:网络节点队列分组、排队所经历的时间
1.2.5 网络编程中的几个关键概念
- 客户端:请求服务的一方
- 服务器:提供服务的一方
- 通信:数据传输过程
- 协议:数据组织、编码、传输、校验、解码的规则
1.2.6 网络通信要解决的问题
客户端 | 服务器 |
---|---|
如何找到通信对方 | 如何让对方找到自己 |
如何联系对方 | 如何让对方联系自己 |
如何正确传输数据 | 如何正确传输数据 |
如何让对方理解自己的意思 | 如何让对方理解自己的意思 |
如何结束对话 | 如何结束对话 |
1.2.7 网络通信协议
1)生活中的协议
2)网络通信协议
- 是一组规则,对数据组织、发送、传输、解析、校验纠错的规则
- 由第三方机构事先制定(中间组织、头部企业等),或通信双方约定
- 需要通信各方共同遵守,否则就无法完成正常通信
1.1.8 网络通信标准
1)OSI七层参考模型
在网络技术发展早期,不同硬件、软件、网络厂商都开发了自己的通信标准,导致出现了互不兼容的情况。为了更好地促进互联网络的研究和发展,国际标准化组织ISO制定了网络互连的七层框架的一个参考模型,称为开放系统互连参考模型,简称OSI/RM(Open System Internetwork Reference Model)。 OSI参考模型是一个具有7层协议结构的开放系统互连模型,是由国际标准化组织在20世纪80年代早期制定的一套普遍适用的规范集合,使全球范围的计算机可进行开放式通信。
每一层的功能及数据形态如下表所示:
名称 | 功能 | 数据形态 |
---|---|---|
应用层 | 用户与网络接口,应用功能 | 字节或字符 |
表示层 | 数据编码的表示方式问题,进程间数据标准 | |
会话层 | 进程-进程会话管理 | |
传输层 | 进程-进程通信 | 数据段/报文 |
网络层 | 广域网主机-主机通信 | 数据包/数据分组 |
数据链路 | 局域网主机-主机通信 | 数据帧 |
物理层 | 物理、机械及电气标准 | BIT流 |
OSI模型的优点:
- 建立了统一的通信标准
- 降低开发难度,每层功能明确,各司其职
- 七层模型实际规定了每一层的任务,该完成什么事情
OSI模型的缺点:
- 复杂,分层过细
- 只定义了概念,没有具体实现 (只有图纸,没有完成施工)
2)TCP/IP模型
ISO制定的OSI参考模型是理想化的模型,但是它过于庞大、复杂招致了许多批评。与此对照,由技术人员自己开发的TCP/IP协议栈获得了更为广泛的应用。因此现在TCP/IP协议已经称为Internet事实上的工业标准。并衍生出了TCP/IP模型指导实际的开发工作。
3)数据传输过程
- 发送端由应用层逐层根据协议添加首部信息,最终在物理层实现发送
- 发送的消息经过中间多个节点转发到达目标主机
- 目标主机根据协议逐层解析首部,最终到达应用层获取数据
1.1.9 通信地址
-
IP地址 : 即在网络中标识一台计算机的地址编号
-
IP地址分类
- IPv4 :192.168.1.5
- IPv6 :fe80::80a:76cf:ab11:2d73
-
IPv4 特点
- 分为4个部分,每部分是一个整数,取值分为0-255
-
IPv6 特点(了解)
- 分为8个部分,每部分4个16进制数,如果出现连续的数字 0 则可以用 ::省略中间的0
-
IP地址相关命令
-
ifconfig : 查看Linux系统下计算机的IP地址
-
ping [ip]:查看计算机的连通性
-
-
公网IP和内网IP
- 公网IP指的是连接到互联网上的公共IP地址,大家都可以访问。(将来进公司,公司会申请公网IP作为网络项目的被访问地址)
- 内网IP指的是一个局域网络范围内由网络设备分配的IP地址。
-
端口号
-
什么是端口号:用来区分同一台机器上,不同的服务(或应用程序)
-
端口号的取值范围: 0~65535 的整数,不能重复。通常 0~1023 的端口会被一些有名的程序或者系统服务占用,个人一般使用 大于1024的端口
-
1.2 UDP 传输方法
UDP(User Datagram Protocol)用户数据报协议,是一种快速、高效、可靠性较低的传输数据协议。其特点有:
- 无连接协议:在数据发送前,不需要进行连接,发送方直接将数据发给接收方
- 无确认:发送方发送出数据后,接收方是否正确接受,接收方不应答响应信息
- 无流量控制:吞吐量不受拥挤控制算法的调节,只受应用软件生成数据的速率、传输带宽、源端和终端主机性能的限制
- 传输可靠性较低:由于面向非连接,数据收发没有确认机制,所以可能造成数据丢失
- 传输效率较高:因为面向非连接,可靠性控制策略较少,所传输效率较高
UDP协议适合发送小尺寸数据(如对DNS服务器进行IP地址查询时)在接收到数据,给出应答较困难的网络中使用UDP。(如:无线网络)适合于广播/组播式通信中。MSN/QQ/Skype等即时通讯软件的点对点文本通讯以及音视频通讯通常采用UDP协议。
1.2.1 套接字简介
-
套接字(Socket) : 实现网络编程进行数据传输的一种技术手段,网络上各种各样的网络服务大多都是基于 Socket 来完成通信的
-
Python套接字编程模块:import socket
1.2.2 UDP套接字编程
- 创建套接字
sockfd=socket.socket(family,type)
"""功能:创建套接字参数:family 网络地址类型 AF_INET表示ipv4type 套接字类型 SOCK_DGRAM 表示udp套接字 (也叫数据报套接字) 返回值: 套接字对象
"""
- 绑定地址
- 本地地址 : ‘localhost’ , ‘127.0.0.1’
- 网络地址 : ‘172.40.91.185’ (通过ifconfig查看)
- 自动获取地址: ‘0.0.0.0’
sockfd.bind(addr)
"""功能: 绑定本机网络地址参数: 二元元组 (ip,port) ('0.0.0.0',8888)
"""
- 消息收发
data,addr = sockfd.recvfrom(buffersize)
"""功能: 接收UDP消息参数: 每次最多接收多少字节返回值: data 接收到的内容addr 消息发送方地址n = sockfd.sendto(data,addr)功能: 发送UDP消息参数: data 发送的内容 bytes格式addr 目标地址返回值:发送的字节数
"""
- 关闭套接字
sockfd.close()
"""功能:关闭套接字
"""
"""
udp_server.py
udp服务端实例代码
"""
from socket import *# 创建UDP套接字
udp_socket = socket(AF_INET,SOCK_DGRAM)# 绑定地址
udp_socket.bind(("0.0.0.0",8888))while True:# 接收发送消息 data--> bytesdata,addr = udp_socket.recvfrom(1024)# if data == b"##":# breakprint("从",addr,"收到:",data.decode("utf-8"))# 发送给刚才收到的地址udp_socket.sendto(b"Thanks",addr)# 关闭套接字
udp_socket.close()
"""
udp_client.py
udp 客户端示例
"""
from socket import *# 服务器地址
ADDR = ("127.0.0.1",8888)# 与服务端相同套接字
udp_socket = socket(AF_INET,SOCK_DGRAM)# 发送消息
while True:msg = input(">>")if not msg:breakudp_socket.sendto(msg.encode("utf-8"),ADDR)# 结束发送# if msg == "##":# breakdata,addr = udp_socket.recvfrom(1024)print("从服务端收到:",data.decode("utf-8"))udp_socket.close()
-
服务端客户端流程
示例:
使用udp完成网络单词查询
从客户端输入单词,发送给服务端,得到单词的解释,打印出来
利用 dict 数据库下的 words表来完成
【服务器端代码】
""" 测试SQL
create database dict charset=utf8;use dict;create table words (id int primary key auto_increment,word char(30),mean varchar(512)
);insert into words(word, mean) VALUES
('hello', '你好'),
('student', '学生'),
('apple', '苹果'),
('orange', '橙子'),
('grape', '葡萄'),
('car', '小汽车'),
('banana', '香蕉'),
('dog', '狗');
"""
########################## 服务端 ###############################
from socket import *
import pymysql# 数据处理类
class Dict:def __init__(self):self.kwargs = {"host": "127.0.0.1","port": 3306,"user": "root","password": "root12345678","database": "dict","charset": "utf8"}self.connect()# 完成数据库连接def connect(self):self.db = pymysql.connect(**self.kwargs)self.cur = self.db.cursor()# 关闭def close(self):self.cur.close()self.db.close()def get_mean(self, word):sql = "select mean from words where word=%s;"self.cur.execute(sql, [word])mean = self.cur.fetchone() # (mean,) Noneif mean:return mean[0]else:return "Not Found"# 逻辑处理 网络搭建
class QueryWord:def __init__(self, host="0.0.0.0", port=8888):self.host = hostself.port = portself.dict = Dict()self.sock = self.create_socket()def create_socket(self):sock = socket(AF_INET, SOCK_DGRAM)sock.bind((self.host, self.port))return sockdef close(self):self.sock.close()# 查找单词方法def query_word(self):while True:print("Waiting for client...")word, addr = self.sock.recvfrom(128)# 查询单词mean = self.dict.get_mean(word.decode())self.sock.sendto(mean.encode(), addr)if __name__ == '__main__':query = QueryWord()query.query_word()
【客户端端代码】
############################ 客户端代码#########################
from socket import *# 服务器地址
ADDR = ("127.0.0.1", 8888)class QueryWord:def __init__(self):self.sock = socket(type=SOCK_DGRAM)def close(self):self.sock.close()# 网络传输def recv_mean(self, word):self.sock.sendto(word.encode(), ADDR)mean, addr = self.sock.recvfrom(1024)return mean.decode()# 输入输出def query_word(self):while True:word = input("Word:")if not word:breakmean = self.recv_mean(word)print("%s : %s" % (word, mean))if __name__ == '__main__':query = QueryWord()query.query_word() # 查单词query.close()
1.2.3 UDP套接字特点
1)优点
- 传输过程简单,实现容易
- 数据传输效率较高
- 数据以数据包形式表达传输
- 适合传输少量、可靠性要求较低的数据
2)缺点
- 可能会出现数据丢失的情况
- 不适合传输大量、可靠性要求较高的数据
1.3 TCP 传输方法
传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议,其特点有:
- 面向连接:在通信前需要建立连接(相当于通话之前拨电话)
- 数据分片:在发送端对用户数据进行分片,在接收端进行重组,由TCP确定分片的大小并控制分片和重组
- 可靠传输:提供了可靠的数据传输,可靠性指数据传输过程中无丢失,无失序,无差错,无重复
- 数据确认与应答机制:接收端接收到分片数据时,根据分片数据序号向发送端发送一个确认
- 超时重发:发送方在发送分片时启动超时定时器,如果在定时器超时之后没有收到相应的确认,重发分片
- 流量控制:TCP提供了流量控制机制,使得收发双方处理速度基本一致,既保证发送效率,又保证传输质量
- 数据校验:TCP将保持它首部和数据的检验和,目的是检测数据在传输过程中的任何变化。如果收到分片的检验和有差错,TCP将丢弃这个分片,并不确认收到此报文段导致对端超时并重发
1.3.1 TCP通信过程
- 整体通信过程
-
三次握手(建立连接)
- 客户端向服务器发送消息报文请求连接
- 服务器收到请求后,回复报文确定可以连接
- 客户端收到回复,发送最终报文连接建立
-
四次挥手(断开连接)
- 主动方发送报文请求断开连接
- 被动方收到请求后,立即回复,表示准备断开
- 被动方准备就绪,再次发送报文表示可以断开
- 主动方收到确定,发送最终报文完成断开
1.3.2 TCP服务端
- 创建套接字
sockfd=socket.socket(family,type)
"""功能:创建套接字参数:family 网络地址类型 AF_INET表示ipv4type 套接字类型 SOCK_STREAM 表示TCP套接字 (也叫流式套接字) 返回值: 套接字对象
"""
- 绑定地址 (与udp套接字相同)
- 设置监听
sockfd.listen(n)
"""功能 : 将套接字设置为监听套接字,确定监听队列大小参数 : 监听队列大小
"""
- 处理客户端连接请求
connfd,addr = sockfd.accept()
"""功能: 阻塞等待处理客户端请求返回值: connfd 客户端连接套接字addr 连接的客户端地址
"""
- 消息收发
data = connfd.recv(buffersize)
"""功能 : 接受客户端消息参数 :每次最多接收消息的大小返回值: 接收到的内容
"""n = connfd.send(data)
"""功能 : 发送消息参数 :要发送的内容 bytes格式返回值: 发送的字节数
"""
- 关闭套接字:socket.close()
服务器端代码示例:
"""
TCP服务端函数示例
"""
from socket import *# 创建tcp套接字
tcp_socket = socket(AF_INET,SOCK_STREAM)# 绑定地址
tcp_socket.bind(("0.0.0.0",8888))# 设置为监听套接字
tcp_socket.listen(5)# 等待客户端连接
while True:print("Waiting for connect...")connfd,addr = tcp_socket.accept()print("Connect from",addr)# 循环收发消息 客户端退出 recv立即返回b""while True:data = connfd.recv(5)# data=b""客户端直接关闭 b"##"客户端主动告知关闭if not data or data == b'##':breakprint("收到:",data.decode())connfd.send(b"Thanks/")connfd.close()# 关闭套接字
tcp_socket.close()
1.3.3 TCP客户端
- 创建TCP套接字
- 请求连接
sockfd.connect(server_addr)
"""功能:连接服务器参数:元组 服务器地址
"""
-
收发消息:同服务器端
-
关闭套接字:
tcp_socket.close()
客户端代码示例:
"""
TCP套接字编程 客户端
"""
from socket import *# 服务端地址
ADDR = ("127.0.0.1",8888)tcp_socket = socket() # 默认创建TCP socket# 发起连接
tcp_socket.connect(ADDR)# 循环发送接收消息
while True:msg = input(">>")tcp_socket.send(msg.encode())# 结束发送if msg == "##":breakdata = tcp_socket.recv(1024)print("From server:",data.decode())tcp_socket.close()
【示例:】
在客户端将一张图片上传到服务端,图片自选,上传到服务端后命名为 recv.jpg。思路:
- 客户端 获取文件内容 → 发送出去
- 服务端 接收文件内容 → 写入磁盘
服务器端代码:
from socket import *address = ("0.0.0.0", 9999)server = socket()
server.bind(address)
server.listen(5)
print("服务器已启动:", address)sockfd, addr = server.accept() # 接受请求f = open("recv.png", "wb") # 二进制写模式while True:data = sockfd.recv(1024)if not data:breakelse:f.write(data)f.close() # 关闭文件
sockfd.close() # 关闭通信socket
server.close() # 关闭接收服务器
客户端部分:
# 发送端
from socket import *client = socket()
client.connect(("127.0.0.1", 9999))try:f = open("dog.png", "rb") # 二进制读模式
except:print("读取文件错误")exit()while True:data = f.read(1024)if not data:breakelse:client.send(data) # 发送数据f.close()
client.close()
1.3.4 TCP套接字细节
-
tcp连接中当一端退出,另一端如果阻塞在recv,此时recv会立即返回一个空字串。
-
tcp连接中如果一端已经不存在,仍然试图通过send向其发送数据则会产生BrokenPipeError
-
一个服务端可以同时连接多个客户端,也能够重复被连接
-
tcp粘包问题
-
产生原因
- 为了解决数据再传输过程中可能产生的速度不协调问题,操作系统设置了缓冲区
- 实际网络工作过程比较复杂,导致消息收发速度不一致
- tcp以字节流方式进行数据传输,在接收时不区分消息边界
-
带来的影响
- 如果每次发送内容是一个独立的含义,需要接收端独立解析此时粘包会有影响。
-
处理方法
- 消息格式化处理,如人为的添加消息边界,用作消息之间的分割
-
控制发送的速度
-
- 练习:
服务器端
""" :
在客户端有一些数据
data = ["Jerry 18 177","Tom 19 180","Lily 120 183"
]
从客户端向服务端发送这些数据,在服务端将这些数据分别写入到一个文件中,每个数据占一行
"""######################### 服务端 ######################
from socket import *def recv_data(connfd):f = open("student.txt", 'wt')while True:data = connfd.recv(1024)if not data:breakdata = data.decode()#print("接受到的数据:", data)msg = ""for s in data:if s == "\n":f.write(msg)f.write("\n")msg = ""else:msg += sif msg != "":f.write(msg)f.close()def main():sock = socket()sock.bind(("0.0.0.0",8888))sock.listen(3)print("服务端启动成功")connfd,addr = sock.accept()print("连接:",addr)recv_data(connfd) # 接收数据if __name__ == '__main__':main()
客户端
###################### 客户端 ################################from socket import *
from time import sleepdata = ["Jerry 18 177","Tom 19 180","Lily 120 183"
]# 发送数据 (处理粘包方法1)
# def send_data(sock):
# for item in data:
# sock.send(item.encode())
# sleep(0.1) # 延迟发送
# sock.send(b"##") # 表示发送完成# 发送数据 (处理粘包方法2)
def send_data(sock):info = '\n'.join(data)sock.send(info.encode()) # 一次性发送print("发送完毕, 发送长度:", len(info))
def main():sock = socket()sock.connect(("127.0.0.1",8888))send_data(sock) # 发送数据sock.close()if __name__ == '__main__':main()
1.3.5 TCP与UDP对比
1)传输特征
- TCP提供可靠的数据传输,但是UDP则不保证传输的可靠性
- TCP传输数据处理为字节流,而UDP处理为数据包形式
- TCP传输需要建立连接才能进行数据传,效率相对较低,UDP比较自由,无需连接,效率较高
2)套接字编程区别
- 创建的套接字类型不同
- tcp套接字会有粘包,udp套接字有消息边界不会粘包
- tcp套接字依赖listen accept建立连接才能收发消息,udp套接字则不需要
- tcp套接字使用send,recv收发消息,udp套接字使用sendto,recvfrom
3)使用场景
- tcp更适合对准确性要求高,传输数据较大的场景
- 文件传输:如下载电影,访问网页,上传照片
- 邮件收发
- 点对点数据传输:如点对点聊天,登录请求,远程访问,发红包
- udp更适合对可靠性要求没有那么高,传输方式比较自由的场景
- 视频流的传输: 如部分直播,视频聊天等
- 广播:如网络广播,群发消息
- 实时传输:如游戏画面
- 在一个大型的项目中,可能既涉及到TCP网络又有UDP网络
练习:
完成一个对话小程序,客户端可以发送问题给服务端,服务端接收到问题将对应答案给客户端,客户端打印出来
要求可以同时多个客户端提问,如果问题没有指定答案,则回答 “人家还小,不知道。”注意: 不需要使用数据库文件存储应答内容,在服务端用字典表示关键字和答案之间的对应关系即可
{"key":"value"}
key: 几岁
value : 我2岁啦################ 服务端 ############################
from socket import *# 对话字典
chat = {"你好":"你好啊!","叫什么":"我叫小美","男生女生":"我是机器人啦","你几岁":"我2岁啦"
}def handle(connfd):# q 客户端问题q = connfd.recv(1024).decode()for key,value in chat.items():if key in q:connfd.send(value.encode())breakelse:connfd.send("人家还小不知道啦。".encode())def main():sock = socket()sock.bind(("0.0.0.0",8888))sock.listen(5)# 循环处理对话while True:connfd,addr = sock.accept()handle(connfd) # 接收问题回答问题connfd.close()if __name__ == '__main__':main()####################### 客户端 ###############################
from socket import *# 服务器地址
ADDR = ("127.0.0.1",8888)def chat(msg):sock = socket()sock.connect(ADDR)sock.send(msg.encode())result = sock.recv(1024)sock.close()return result.decode()# 创建套接字
def main():while True:msg = input("我:")if not msg:breakresult = chat(msg)print("小美:",result)if __name__ == '__main__':main()
1.4 数据传输过程
1.4.1 传输流程
- 发送端由应用程序发送消息,逐层添加首部信息,最终在物理层发送消息包
- 发送的消息经过多个节点(交换机,路由器)传输,最终到达目标主机
- 目标主机由物理层逐层解析首部消息包,最终到应用程序呈现消息
1.4.2 TCP协议首部信息
-
源端口和目的端口 各占2个字节,分别写入源端口和目的端口。
-
序号 占4字节。TCP是面向字节流的。在一个TCP连接中传送的字节流中的每一个字节都按顺序编号。例如,一报文段的序号是301,而接待的数据共有100字节。这就表明本报文段的数据的第一个字节的序号是301,最后一个字节的序号是400。
-
确认号 占4字节,是期望收到对方下一个报文段的第一个数据字节的序号。例如,B正确收到了A发送过来的一个报文段,其序号字段值是501,而数据长度是200字节(序号501~700),这表明B正确收到了A发送的到序号700为止的数据。因此,B期望收到A的下一个数据序号是701,于是B在发送给A的确认报文段中把确认号置为701。
-
确认ACK(ACKnowledgment) 仅当ACK = 1时确认号字段才有效,当ACK = 0时确认号无效。TCP规定,在连接建立后所有的传送的报文段都必须把ACK置为1。
-
同步SYN(SYNchronization) 在连接建立时用来同步序号。当SYN=1而ACK=0时,表明这是一个连接请求报文段。对方若同意建立连接,则应在响应的报文段中使SYN=1和ACK=1,因此SYN置为1就表示这是一个连接请求或连接接受报文。
-
终止FIN(FINis,意思是“完”“终”) 用来释放一个连接。当FIN=1时,表明此报文段的发送发的数据已发送完毕,并要求释放运输连接。
2. 多任务编程
现今的操作系统大多为多任务的系统,即一个系统中可以同时运行多个任务,这样才能充分发挥硬件性能,提升计算机处理能力和吞吐量。多任务编程,就是编写一个包含多个任务同时运行(宏观上)的程序,这样就能大幅度提高系统处理能力。
- 串行执行:多个任务先后执行,前面的任务执行完成后后面的任务再执行
- 并行执行:多个任务同时执行(例如多核CPU)
- 并发执行:多个任务宏观上同时执行,微观上分时间片(或时间段)执行,在操作系统中利用多进程、多线程方式实现
2.1 进程(Process)
2.1.1 进程概述
进程(Process)是操作系统中一个极其重要的概念,是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。简单来讲,进程指程序在计算机中的一次执行过程,是一个正则运行的程序。
为了更好对操作系统进行研究、分析、设计、管理,60年代初首先由麻省理工学院的MULTICS系统和IBM公司的CTSS/360系统引入了进程的概念。
1)进程的特点
- 动态性:进程的实质是程序在多道程序系统中的一次执行过程,进程是动态产生,动态消亡的
- 并发性:任何进程都可以同其他进程一起并发执行
- 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位
- 异步性:由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进
- 结构特征:进程由程序、数据和进程控制块三部分组成
多个不同的进程可以包含相同的程序:一个程序在不同的数据集里就构成不同的进程,能得到不同的结果;但是执行过程中,程序不能发生改变
2)进程和程序的区别
- 程序是一个可执行的文件,是静态的占有磁盘
- 进程是一个动态的过程描述,占有计算机运行资源,是一个独立的运行单元,有一定的生命周期
3)进程状态
进程状态是描述一个进程“从生到死”的过程,描述进程生命周期的变化过程。进程状态包括:就绪状态(Ready)、运行状态(Running)、阻塞状态(Blocked)
- 就绪状态:进程已获得除处理器(即CPU)外的所需资源,等待分配处理器资源;只要分配了处理器进程就可执行(可以理解为在医院挂了号,等待医生诊断)
- 运行状态:正在处理器上执行(可以理解为正在接受医生诊断状态)
- 阻塞状态:由于进程等待某种条件(如I/O操作或进程同步),在条件满足之前无法继续执行。该事件发生前即使把处理器资源分配给该进程,也无法运行(可以理解为,等待检查报告,拿到报告后才能进行下一步诊断)
进程状态发生变化,称为“进程状态转换”,进程状态转换是由一些操作系统事件引起的,进程状态转换有以下几种情况:
- 就绪→执行:处于就绪状态的进程,当进程调度程序为之分配了处理机后,该进程便由就绪状态转变成执行状态
- 执行→就绪:处于执行状态的进程在其执行过程中,因分配给它的一个时间片已用完而不得不让出处理机,于是进程从执行状态转变成就绪状态
- 执行→阻塞:正在执行的进程因等待某种事件发生而无法继续执行时,便从执行状态变成阻塞状态。引起进程阻塞的事件可有多种,例如,等待I/O完成、申请缓冲区不能满足、等待信件(信号)等
- 阻塞→就绪:处于阻塞状态的进程,若其等待的事件已经发生,于是进程由阻塞状态转变为就绪状态
注意:阻塞状态不能直接转换到运行状态!
4)进程结构
进程有程序段、数据段、进程控制块(Processing Control Block,简写PCB)三部分组成,如下图所示:
其中,PCB中记录了最重要的进程控制信息,用于操作系统对进程进行管理和调度,它是系统中的一个内存区域,存放操作系统用于描述进程情况及控制程序运行所需的全部信息。如下图所示:
5)进程管理命令
-
查看进程信息
ps 命令""" 参数说明:ps -a 显示现行终端机下的所有程序,包括其他用户的程序。ps -A 显示所有程序。 ps -c 列出程序时,显示每个程序真正的指令名称,而不包含路径,参数或常驻服务的标示。 ps -e 列出程序时,显示每个程序所使用的环境变量。 ps -f 用ASCII字符显示树状结构,表达程序间的相互关系。 ps -H 显示树状结构,表示程序间的相互关系。 ps -N 显示所有的程序,除了执行ps指令终端机下的程序之外。 ps -s 采用程序信号的格式显示程序状况。 ps -S 列出程序时,包括已中断的子程序资料。 ps -u 以用户为主的格式来显示程序状况。 ps -x 显示所有程序,不以终端机来区分 """
- USER : 进程的创建者
- PID : 操作系统分配给进程的编号,大于0的整数,系统中每个进程的PID都不重复。PID也是重要的区分进程的标志。
- %CPU,%MEM : 占有的CPU和内存
- STAT : 进程状态信息,S表示阻塞状态 ,R 表示运行状态,Z表示僵尸进程,s包含子进程,I表示多线程,<表示优先级高,N表示优先级低
- START : 进程启动时间
- COMMAND : 通过什么程序启动的进程
- TIME:用掉的CPU时间
- VSZ:虚拟内存大小,这是linux分配给进程的内存大小,但是这并不一定意味着这个进程使用了所有的内存
- RSS:驻留集大小(Resident Set Size),这是进程当前加载其所有页面的内存大小
- 结束进程
kill -9 pid #用于杀死一个进程
2.1.2 多进程编程
- 使用模块 : multiprocessing
- 创建流程
- 将需要新进程执行的事件封装为函数
- 通过模块的Process类创建进程对象,关联函数
- 通过进程对象调用start启动进程
- 主要类和函数使用
Process()
"""功能 : 创建进程对象参数 : target 绑定要执行的目标函数 args 元组,用于给target函数位置传参kwargs 字典,给target函数键值传参daemon bool值,让子进程随父进程退出
"""
p.start()
"""功能 : 启动进程
"""
注意 : 启动进程此时target绑定函数开始执行,该函数作为新进程执行内容,此时进程真正被创建
p.join([timeout])
"""功能:阻塞等待子进程退出参数:最长等待时间
"""
示例:创建进程
"""
进程创建示例 01
"""
import multiprocessing as mp
from time import sleepa = 1 # 全局变量# 进程目标函数
def fun():print("子进程: 开始运行一个进程")sleep(4) # 模拟事件执行事件global aprint("子进程: a =", a) # Yesa = 10000print("子进程: 进程执行结束")# 实例化进程对象
process = mp.Process(target=fun)# 启动进程 进程产生 执行fun
process.start()print("主进程:我也做点事情")
sleep(3)
print("主进程:我也把事情做完了...")process.join() # 阻塞等待子进程结束
print("主进程 a:", a) # 1 10000
示例:创建进程,并向进程传参
"""
进程创建示例02 : 含有参数的进程函数
"""
from multiprocessing import Process
from time import sleep# 含有参数的进程函数
def worker(sec,name):for i in range(3):print("sec:", sec)sleep(sec)print("I'm %s"%name)print("I'm working....")# 元组位置传参
# p = Process(target=worker,args=(2,"Tom"))# 关键字传参
p = Process(target=worker,args = (2,),kwargs={"name":"Tom"},daemon=True) # 守护进程标志,守护进程指退出时要终止所有子进程
p.start()
p.join()
-
进程执行现象理解
- 新的进程是原有进程的子进程,子进程复制父进程全部内存空间,一个进程可以创建多个子进程。
- 子进程只执行指定的函数,执行完毕子进程生命周期结束,但是子进程也拥有其他父进程资源。
- 各个进程在执行上互不影响,也没有必然的先后顺序关系。
- 进程创建后,各个进程空间独立,相互没有影响。
- multiprocessing 创建的子进程中无法使用标准输入(即无法使用input)。
2.1.3 进程相关函数
- 进程相关函数
os.getpid()
"""功能: 获取一个进程的PID值返回值: 返回当前进程的PID
"""os.getppid()
"""功能: 获取父进程的PID号返回值: 返回父进程PID
"""
示例:创建多个子进程
"""
创建多个子进程
"""
from multiprocessing import Process
from time import sleep
import sys, osdef worker1():sleep(3)print("我是worker1, pid=", os.getppid(), " ppid=", os.getppid())def worker2():sleep(1)print("我是worker2, pid=", os.getppid(), " ppid=", os.getppid())def worker3():sleep(2)print("我是worker3, pid=", os.getppid(), " ppid=", os.getppid())# 循环创建子进程
jobs = [] # 存放每个进程对象
for th in [worker1, worker2, worker3]:p = Process(target=th)jobs.append(p) # 存入jobsp.start()# 确保三件事都结束
for i in jobs:i.join()print("子进程执行完成, 主进程退出.")
2.1.4 创建进程类
进程的基本创建方法将子进程执行的内容封装为函数。如果我们更热衷于面向对象的编程思想,也可以使用类来封装进程内容。
-
创建步骤
【1】 继承Process类
【2】 重写
__init__
方法添加自己的属性,使用super()加载父类属性【3】 重写run()方法
-
使用方法
【1】 实例化对象
【2】 调用start自动执行run方法
""" 自定义进程类 """ from multiprocessing import Process from time import sleepclass MyProcess(Process):def __init__(self, value):self.value = valuesuper().__init__() # 调用父类的init# 重写run 作为进程的执行内容def run(self):for i in range(self.value):sleep(2)print("自定义进程类。。。。")p = MyProcess(3) p.start() # 将 run方法作为进程执行
2.1.5 进程间通信
进程间通信指在不同进程间传递数据。进程的用户空间是互相独立的,一般而言是不能互相访问的,进程间通信需要遵循特定的方式。常用进程间通信方法:消息队列,网络套接字等。此处介绍消息队列进程间通信方式。
消息队列,是在内存中开辟空间,建立队列模型,进程通过队列将消息存入,或者从队列取出完成进程间通信。
消息队列的主要操作有:
from multiprocessing import Queueq = Queue(maxsize=0)
# 功能: 创建队列对象
# 参数:最多存放消息个数
# 返回值:队列对象q.put(data)
# 功能:向队列存入消息
# 参数:data 要存入的内容q.get()
# 功能:从队列取出消息
# 返回值: 返回获取到的内容q.full() # 判断队列是否为满
q.empty() # 判断队列是否为空
q.qsize() # 获取队列中消息个数
消息队列进程间通信示例:
from multiprocessing import *# 生产者(向队列中添加数据)
def producer(queue):for i in range(10):queue.put("Producer:" + str(i))# 消费者(从队列中取数据)
def consumer(queue):while True:item = queue.get()if item is None:breakprint(item)if __name__ == '__main__':queue = Queue()p1 = Process(target=producer, args=(queue,))p2 = Process(target=consumer, args=(queue,))p1.start()p2.start()p1.join()queue.put(None)p2.join()
2.2 线程 (Thread)
2.2.1 线程概述
- 线程被称为轻量级的进程,也是多任务编程方式
- 线程可以理解为进程中再开辟的分支任务
- 线程也是一个运行行为,消耗计算机资源
- 一个进程中的所有线程共享这个进程的资源
- 多个线程之间的运行同样互不影响各自运行
- 线程的创建和销毁过程给计算机带来的压力远小于进程
2.2.2 多线程编程
线程模块的用法几乎和进程一模一样,完全可以仿照完成。
- 线程模块: threading
- 线程主要操作
from threading import Thread t = Thread()
"""
功能:创建线程对象
参数:target 绑定线程函数args 元组 给线程函数位置传参kwargs 字典 给线程函数键值传参daemon bool值,主线程推出时该分支线程也推出
"""t.start()
"""启动线程
"""t.join([timeout])
"""
功能:阻塞等待分支线程退出
参数:最长等待时间
"""
示例:简单多线程示例
# 线程示例01:import threading
from time import sleep
import osa = 1# 线程函数
def music():global aprint("a =",a)a = 10000for i in range(3):sleep(2)print(os.getpid(),"播放:黄河大合唱")# 实例化线程对象
thread = threading.Thread(target=music)
# 启动线程 线程存在
thread.start()for i in range(4):sleep(1)print(os.getpid(),"播放:葫芦娃")# 阻塞等待分支线程结束
thread.join()
print("a:",a)
示例:包含参数传输的多线程示例
# 线程示例02:from threading import Thread
from time import sleep# 带有参数的线程函数
def func(sec,name):print("含有参数的线程来喽")sleep(sec)print("%s 线程执行完毕"%name)# 循环创建线程
for i in range(5):t = Thread(target=func,args=(2,),kwargs={"name":"T-%d"%i},daemon=True)t.start()
2.2.3 创建线程类
-
创建步骤
【1】 继承Thread类
【2】 重写
__init__
方法添加自己的属性,使用super()加载父类属性【3】 重写run()方法
-
使用方法
【1】 实例化对象
【2】 调用start自动执行run方法
from threading import Thread from time import sleepclass MyThread(Thread):def __init__(self,song):self.song = songsuper().__init__() # 得到父类内容# 线程要做的事情def run(self):for i in range(3):sleep(2)print("播放:",self.song)t = MyThread("让我们荡起双桨") t.start() # 运行run
2.2.4 线程互斥锁
-
线程通信方法: 线程间使用全局变量进行通信
-
共享资源争夺
- 共享资源:多线程都可以操作的资源称为共享资源。对共享资源的操作代码段称为临界区。
- 影响 : 对共享资源的无序操作可能会带来数据的混乱,或者操作错误。此时往往需要互斥机制协调。
-
互斥锁机制
当一个线程占有资源时会进行加锁处理,此时其他线程就无法操作该资源,直到解锁后才能操作。
- 线程锁 Lock
from threading import Locklock = Lock() # 创建锁对象
lock.acquire() # 上锁 如果lock已经上锁再调用会阻塞
lock.release() # 解锁
线程锁示例:
# Lock使用示例:from threading import Thread, Locklock = Lock() # 创建锁
a = b = 0def value():while True:lock.acquire() # 上锁if a != b:print("a = %d,b = %d" % (a, b))lock.release() # 解锁t = Thread(target=value)
t.start()while True:lock.acquire()a += 1b += 1lock.release()
- 线程锁的注意问题:死锁问题
死锁产生条件* 互斥条件:即使用了互斥锁。* 请求和保持条件:锁住一定资源不解锁的情况先再请求锁住其他资源。* 不剥夺条件:不会受到线程外部的干扰,终止锁行为。* 环路等待条件:指在发生死锁时,必然存在一个线程——资源的环形链,如 T0正在等待一个T1占用的资源;T1正在等待T2占用的资源,……,Tn正在等待已被T0占用的资源。
2.2.5 GIL问题
-
什么是GIL问题 (全局解释器锁)
由于python解释器设计中加入了解释器锁,导致python解释器同一时刻只能解释执行一个线程,大大降低了线程的执行效率。
-
导致后果
因为遇到阻塞时线程会主动让出解释器,去解释其他线程。所以python多线程在执行多阻塞任务时可以提升程序效率,其他情况并不能对效率有所提升。 -
关于GIL问题的处理
-
来自Guido:http://www.artima.com/forums/flat.jsp?forum=106&thread=214235
-
尽量使用进程完成无阻塞的多任务情况
-
不使用c作为解释器 (可以用Java C#)
-
- 结论
- GIL问题与Python语言本身并没什么关系,属于解释器设计的历史问题。
- 在无阻塞状态下,多线程程序程序执行效率并不高,甚至还不如单线程效率。
- Python多线程只适用于执行有阻塞延迟的任务情形。
2.2.6 进程线程的区别联系
1)区别联系
- 两者都是多任务编程方式
- 进程的创建销毁过程给计算机带来的压力比线程多
- 进程空间独立,数据互不干扰,有专门通信方法;线程使用全局变量通信
- 一个进程可以有多个分支线程,两者有包含关系
- 多个线程共享进程资源,在共享资源操作时往往需要互斥锁处理
- Python线程存在GIL问题,但是进程没有。
2)使用场景
- 任务场景:一个大型服务,往往包含多个独立的任务模块,每个任务模块又有多个小独立任务构成,此时整个项目可能有多个进程,每个进程又有多个线程。
- 编程语言:Java,C#之类的编程语言在执行多任务时一般都是用线程完成,因为线程资源消耗少;而Python由于GIL问题往往使用多进程。
3. 网络并发模型
3.1 网络并发模型概述
-
什么是网络并发
在实际工作中,一个服务端程序往往要应对多个客户端同时发起访问的情况。如果让服务端程序能够更好的同时满足更多客户端网络请求的情形,这就是并发网络模型。
-
循环网络模型问题
循环网络模型只能循环接收客户端请求,处理请求。同一时刻只能处理一个请求,处理完毕后再处理下一个。这样的网络模型虽然简单,资源占用不多,但是无法同时处理多个客户端请求就是其最大的弊端,往往只有在一些低频的小请求任务中才会使用。
3.2 多进程/线程并发模型
多进程/线程并发模中每当一个客户端连接服务器,就创建一个新的进程/线程为该客户端服务,客户端退出时再销毁该进程/线程,多任务并发模型也是实际工作中最为常用的服务端处理模型。
-
模型特点
- 优点:能同时满足多个客户端长期占有服务端需求,可以处理各种请求。
- 缺点: 资源消耗较大
- 适用情况:客户端请求较复杂,需要长时间占有服务器。
-
创建流程
- 创建网络套接字
- 等待客户端连接
- 有客户端连接,则创建新的进程/线程具体处理客户端请求
- 主进程/线程继续等待处理其他客户端连接
- 如果客户端退出,则销毁对应的进程/线程
多进程并发模型示例:
# 多进程并发模型示例:"""
基于多进程的网络并发模型创建tcp套接字
等待客户端连接
有客户端连接,则创建新的进程具体处理客户端请求
父进程继续等待处理其他客户端连接
如果客户端退出,则销毁对应的进程
"""
from socket import *
from multiprocessing import Process
import sys# 地址变量
HOST = "0.0.0.0"
PORT = 8888
ADDR = (HOST, PORT)# 处理客户端具体请求
def handle(connfd):while True:data = connfd.recv(1024)if not data:breakprint(data.decode("utf-8"))resp_str = "收到了你的信息:" + data.decode("utf-8")connfd.send(resp_str.encode("utf-8"))connfd.close()# 服务入口函数
def main():# 创建tcp套接字tcp_socket = socket()tcp_socket.bind(ADDR)tcp_socket.listen(5)print("Listen the port %d" % PORT)# 循环连接客户端while True:try:connfd, addr = tcp_socket.accept()print("Connect from", addr)except KeyboardInterrupt:tcp_socket.close()sys.exit("服务结束")# 创建进程 处理客户端请求p = Process(target=handle, args=(connfd,), daemon=True)p.start()if __name__ == '__main__':main()
以上程序可以同时运行两个TCP_client.py,可以同时对多个客户端完成信息收发。
多线程并发示例:
# 多线程并发模型示例:
"""
基于多线程的网络并发模型
思路: 网络构建 线程搭建 / 具体处理请求
"""
from socket import *
from threading import Thread# 处理客户端具体请求
class Handle:# 具体处理请求函数 (逻辑处理,数据处理)def request(self, data):print(data)# 创建线程得到请求
class ThreadServer(Thread):def __init__(self, connfd):self.connfd = connfdself.handle = Handle()super().__init__(daemon=True)# 接收客户端的请求def run(self):while True:data = self.connfd.recv(1024).decode("utf-8")if not data:breakself.handle.request(data)resp_str = "收到了你的信息:" + dataself.connfd.send(resp_str.encode("utf-8"))self.connfd.close()# 网络搭建
class ConcurrentServer:"""提供网络功能"""def __init__(self, *, host="", port=0):self.host = hostself.port = portself.address = (host, port)self.sock = self.__create_socket()def __create_socket(self):tcp_socket = socket()tcp_socket.bind(self.address)return tcp_socket# 启动服务 --> 准备连接客户端def serve_forever(self):self.sock.listen(5)print("Listen the port %d" % self.port)while True:connfd, addr = self.sock.accept()print("Connect from", addr)# 创建线程t = ThreadServer(connfd)t.start()if __name__ == '__main__':server = ConcurrentServer(host="0.0.0.0", port=8888)server.serve_forever() # 启动服务
以上程序可以同时运行两个TCP_client.py,可以同时对多个客户端完成信息收发。
4. web服务
4.1 HTTP协议
4.1.1 协议概述
- 用途 : 网页获取,数据的传输
- 特点
- 应用层协议,使用tcp进行数据传输
- 简单,灵活,很多语言都有HTTP专门接口
- 有丰富的请求类型
- 可以传输的数据类型众多
4.1.2 网页访问流程
-
客户端(浏览器)通过tcp传输,发送http请求给服务端
-
服务端接收到http请求后进行解析
-
服务端处理请求内容,组织响应内容
-
服务端将响应内容以http响应格式发送给浏览器
-
浏览器接收到响应内容,解析展示
4.1.2 HTTP请求
- 请求行 : 具体的请求类别和请求内容
GET / HTTP/1.1请求类别 请求内容 协议版本
请求类别:每个请求类别表示要做不同的事情
GET : 获取网络资源POST :提交一定的信息,得到反馈HEAD : 只获取网络资源的响应头PUT : 更新服务器资源DELETE : 删除服务器资源
- 请求头:对请求的进一步解释和描述
Accept-Encoding: gzip
- 空行
- 请求体: 请求参数或者提交内容
4.1.3 HTTP响应
- 响应行 : 反馈基本的响应情况
HTTP/1.1 200 OK版本信息 响应码 附加信息
响应码 :
1xx 提示信息,表示请求被接收2xx 响应成功3xx 响应需要进一步操作,重定向4xx 客户端错误5xx 服务器错误
- 响应头:对响应内容的描述
Content-Type: text/html
- 空行
- 响应体:响应的主体内容信息
HTTP服务器示例一:
# HTTP协议示例:
"""
http请求和响应 演示
"""
from socket import *sock = socket()
sock.bind(("0.0.0.0",8000))
sock.listen(5)# 等待浏览器连接
connfd,addr = sock.accept()
print("Connect from",addr)# 接收HTTP请求
request = connfd.recv(1024)
print(request.decode())# 组织响应
response = """HTTP/1.1 200 OK
Content-Type:text/htmlhello world
"""
connfd.send(response.encode())connfd.close()
sock.close()
以上代码在浏览器中输入http://127.0.0.1:8000,返回页面中显示“hello world”
HTTP服务器示例二:
# 随堂练习:将网页 一个图片 通过浏览器访问显示出来
# 提示 : Content-Type:image/jpegfrom socket import *# 处理http请求
def handle(connfd):# 接收http请求request = connfd.recv(1024).decode()if not request:return# 组织响应response = "HTTP/1.1 200 OK\r\n"response += "Content-Type:image/jpeg\r\n"response += "\r\n"with open("abc.jpeg",'rb') as f:response =response.encode() + f.read()connfd.send(response # 发送响应def main():sock = socket()sock.bind(("0.0.0.0", 8000))sock.listen(5)# 等待浏览器连接while True:connfd, addr = sock.accept()print("Connect from", addr)handle(connfd) # 处理请求connfd.close()if __name__ == '__main__':main()
以上代码在浏览器中输入http://127.0.0.1:8000,返回页面中显示一张图片。
5. 高并发技术探讨
5.1 高并发问题
-
衡量高并发的关键指标
-
响应时间(Response Time) : 接收请求后处理的时间
-
同时在线用户数量:同时连接服务器的用户的数量
-
每秒查询率QPS(Query Per Second): 每秒接收请求的次数
-
每秒事务处理量TPS(Transaction Per Second):每秒处理请求的次数(包含接收,处理,响应)
-
吞吐量(Throughput): 响应时间+QPS+同时在线用户数量
-
-
多大的并发量算是高并发
-
没有最高,只要更高
比如在一个小公司可能QPS2000+就不错了,在一个需要频繁访问的门户网站可能要达到QPS5W+
-
C10K问题
早先服务器都是单纯基于进程/线程模型的,新到来一个TCP连接,就需要分配1个进程(或者线程)。而进程占用操作系统资源多,一台机器无法创建很多进程。如果是C10K就要创建1万个进程,那么单机而言操作系统是无法承受的,这就是著名的C10k问题。创建的进程线程多了,数据拷贝频繁, 进程/线程切换消耗大, 导致操作系统崩溃,这就是C10K问题的本质!
-
5.2 更高并发的实现
为了解决C10K问题,现在高并发的实现已经是一个更加综合的架构艺术。涉及到进程线程编程,IO处理,数据库处理,缓存,队列,负载均衡等等,这些我们在后面的阶段还会学习。此外还有硬件的设计,服务器集群的部署,服务器负载,网络流量的处理等。
实际工作中,应对更庞大的任务场景,网络并发模型的使用有时也并不单一。比如多进程网络并发中每个进程再开辟线程,或者在每个进程中也可以使用多路复用的IO处理方法。