【网络与并发编程】

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表示ipv4
         type  套接字类型 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--> bytes
    data,addr = udp_socket.recvfrom(1024)
    # if data == b"##":
    #     break
    print("从",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:
        break

    udp_socket.sendto(msg.encode("utf-8"),ADDR)
    # 结束发送
    # if msg == "##":
    #     break

    data,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,) None
        if mean:
            return mean[0]
        else:
            return "Not Found"


# 逻辑处理 网络搭建
class QueryWord:
    def __init__(self, host="0.0.0.0", port=8888):
        self.host = host
        self.port = port
        self.dict = Dict()
        self.sock = self.create_socket()

    def create_socket(self):
        sock = socket(AF_INET, SOCK_DGRAM)
        sock.bind((self.host, self.port))
        return sock

    def 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:
                break
            mean = 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表示ipv4
         type  套接字类型 SOCK_STREAM 表示TCP套接字 (也叫流式套接字) 
    返回值: 套接字对象
"""
  • 绑定地址 (与udp套接字相同)
  • 设置监听
sockfd.listen(n)
"""
    功能 : 将套接字设置为监听套接字,确定监听队列大小
    参数 : 监听队列大小
"""

在这里插入图片描述

  • 处理客户端连接请求
connfd,addr = sockfd.accept()
"""
    功能: 阻塞等待处理客户端请求
    返回值: connfd  客户端连接套接字
            addr  连接的客户端地址
"""
  • 消息收发
data = connfd.recv(buffersize)
"""
    功能 : 接受客户端消息
    参数 :每次最多接收消息的大小
    返回值: 接收到的内容
"""

n = connfd.send(data)
"""
    功能 : 发送消息
    参数 :要发送的内容  bytes格式
    返回值: 发送的字节数
"""
  1. 关闭套接字: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'##':
            break
        print("收到:",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 == "##":
        break
    data = 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:
        break
    else:
        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:
        break
    else:
        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:
            break

        data = data.decode()
        #print("接受到的数据:", data)
        msg = ""
        for s in data:
            if s == "\n":
                f.write(msg)
                f.write("\n")
                msg = ""
            else:
                msg += s

        if 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 sleep

data = [
  "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())
            break
    else:
        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:
            break
        result = 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 sleep

a = 1  # 全局变量


# 进程目标函数
def fun():
    print("子进程: 开始运行一个进程")
    sleep(4)  # 模拟事件执行事件
    global a
    print("子进程: a =", a)  # Yes
    a = 10000
    print("子进程: 进程执行结束")


# 实例化进程对象
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, os


def 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)  # 存入jobs
    p.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 sleep
    
    
    class MyProcess(Process):
        def __init__(self, value):
            self.value = value
            super().__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 Queue

q = 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:
            break
        print(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 os

a = 1

#  线程函数
def music():
    global a
    print("a =",a)
    a = 10000
    for 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. 创建步骤

    【1】 继承Thread类

    【2】 重写__init__方法添加自己的属性,使用super()加载父类属性

    【3】 重写run()方法

  2. 使用方法

    【1】 实例化对象

    【2】 调用start自动执行run方法

    from threading import Thread
    from time import sleep
    
    class MyThread(Thread):
        def __init__(self,song):
            self.song = song
            super().__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 Lock

lock = Lock()  # 创建锁对象
lock.acquire() # 上锁  如果lock已经上锁再调用会阻塞
lock.release() # 解锁

线程锁示例:

# Lock使用示例:

from threading import Thread, Lock

lock = Lock() # 创建锁
a = b = 0

def 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 += 1
    b += 1
    lock.release()
  • 线程锁的注意问题:死锁问题
    在这里插入图片描述
死锁产生条件

* 互斥条件:即使用了互斥锁。

* 请求和保持条件:锁住一定资源不解锁的情况先再请求锁住其他资源。

* 不剥夺条件:不会受到线程外部的干扰,终止锁行为。

* 环路等待条件:指在发生死锁时,必然存在一个线程——资源的环形链,如 T0正在等待一个T1占用的资源;T1正在等待T2占用的资源,……,Tn正在等待已被T0占用的资源。

2.2.5 GIL问题

  • 什么是GIL问题 (全局解释器锁)

    由于python解释器设计中加入了解释器锁,导致python解释器同一时刻只能解释执行一个线程,大大降低了线程的执行效率。

  • 导致后果
    因为遇到阻塞时线程会主动让出解释器,去解释其他线程。所以python多线程在执行多阻塞任务时可以提升程序效率,其他情况并不能对效率有所提升。

  • 关于GIL问题的处理

  • 结论
    • 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:
            break
        print(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 = connfd
        self.handle = Handle()
        super().__init__(daemon=True)

    # 接收客户端的请求
    def run(self):
        while True:
            data = self.connfd.recv(1024).decode("utf-8")
            if not data:
                break
            self.handle.request(data)
            resp_str = "收到了你的信息:" + data
            self.connfd.send(resp_str.encode("utf-8"))
        self.connfd.close()


# 网络搭建
class ConcurrentServer:
    """
    提供网络功能
    """
    def __init__(self, *, host="", port=0):
        self.host = host
        self.port = port
        self.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 网页访问流程

  1. 客户端(浏览器)通过tcp传输,发送http请求给服务端

  2. 服务端接收到http请求后进行解析

  3. 服务端处理请求内容,组织响应内容

  4. 服务端将响应内容以http响应格式发送给浏览器

  5. 浏览器接收到响应内容,解析展示

    在这里插入图片描述

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/html

hello world
"""
connfd.send(response.encode())

connfd.close()
sock.close()

以上代码在浏览器中输入http://127.0.0.1:8000,返回页面中显示“hello world”

HTTP服务器示例二:

# 随堂练习:将网页 一个图片 通过浏览器访问显示出来
# 提示 : Content-Type:image/jpeg

from 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处理方法。

  • 12
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值