concurrent_network

并发网络编程

Tedu Python 教学部
Author:吕泽

1. 网络编程

1.1 网络基础知识

1.1.1 什么是网络
  • 什么是网络 : 计算机网络功能主要包括实现资源共享,实现数据信息的快速传递。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GTl1WonJ-1609849651547)(/home/tarena/month02/lz/Network/img/1.jpg)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3xw0JBMU-1609849651550)(/home/tarena/month02/lz/Network/img/2.png)]

1.1.2 网络通信标准
  • 面临问题

    1. 不同的国家和公司都建立自己的通信标准不利于网络互连
    2. 多种标准并行情况下不利于技术的发展融合

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5nc0gzyS-1609849651551)(/home/tarena/month02/lz/Network/img/4.jpg)]

  • OSI 7层模型

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aEtuUIen-1609849651553)(/home/tarena/month02/lz/Network/img/5.jpg)]

    • 好处

      1. 建立了统一的通信标准

      2. 降低开发难度,每层功能明确,各司其职

      3. 七层模型实际规定了每一层的任务,该完成什么事情

  • TCP/IP模型

    • 七层模型过于理想,结构细节太复杂
    • 在工程中应用实践难度大
    • 实际工作中以TCP/IP模型为工作标准流程

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e9ZA3EZa-1609849651555)(/home/tarena/month02/lz/Network/img/6.jpg)]

  • 网络协议

    • 什么是网络协议:在网络数据传输中,都遵循的执行规则。

    • 网络协议实际上规定了每一层在完成自己的任务时应该遵循什么规范。

  • 需要应用工程师做的工作 : 编写应用工功能,明确对方地址,选择传输服务。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VMllolYJ-1609849651557)(/home/tarena/month02/lz/Network/img/7.jpg)]

1.1.3 通信地址
  • IP地址

    • 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地址

        [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lSWjhi3k-1609849651558)(/home/tarena/month02/lz/Network/img/7.png)]

      • ping [ip]:查看计算机的连通性

        [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O91u4v5V-1609849651560)(/home/tarena/month02/lz/Network/img/8.png)]

    • 公网IP和内网IP

      • 公网IP指的是连接到互联网上的公共IP地址,大家都可以访问。(将来进公司,公司会申请公网IP作为网络项目的被访问地址)
      • 内网IP指的是一个局域网络范围内由网络设备分配的IP地址。
  • 端口号

    • 端口:网络地址的一部分,在一台计算机上,每个网络程序对应一个端口。

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PyXBPEuF-1609849651560)(/home/tarena/month02/lz/Network/img/3.png)]

    • 端口号特点

      • 取值范围: 0 —— 65535 的整数
      • 一台计算机上的网络应用所使用的端口不会重复
      • 通常 0——1023 的端口会被一些有名的程序或者系统服务占用,个人一般使用 > 1024的端口
1.1.4 服务端与客户端
  • 服务端(Server):服务端是为客户端服务的,服务的内容诸如向客户端提供资源,保存客户端数据,处理客户端请求等。

  • 客户端(Client) :也称为用户端,是指与服务端相对应,为客户提供一定应用功能的程序,我们平时使用的手机或者电脑上的程序基本都是客户端程序。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CiAGDrv7-1609849651561)(/home/tarena/month02/lz/Network/img/10.jpg)]

1.2 UDP 传输方法

1.2.1 套接字简介
  • 套接字(Socket) : 实现网络编程进行数据传输的一种技术手段,网络上各种各样的网络服务大多都是基于 Socket 来完成通信的。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KDnvWnzT-1609849651564)(/home/tarena/month02/lz/Network/img/9.jpg)]

  • Python套接字编程模块:import socket

1.2.3 UDP套接字编程
  • 创建套接字
sockfd=socket.socket(socket_family,socket_type,proto=0)
功能:创建套接字
参数:socket_family  网络地址类型 AF_INET表示ipv4
	 socket_type  套接字类型 SOCK_DGRAM 表示udp套接字 (也叫数据报套接字) 
	 proto  通常为0  选择子协议
返回值: 套接字对象
  • 绑定地址
    • 本地地址 : ‘localhost’ , ‘127.0.0.1’
    • 网络地址 : ‘172.40.91.185’ (通过ifconfig查看)
    • 自动获取地址: ‘0.0.0.0’

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8eEAp50A-1609849651564)(/home/tarena/month02/lz/Network/img/address.png)]

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()
功能:关闭套接字
  • 服务端客户端流程

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-idwTB9Mv-1609849651565)(/home/tarena/month02/lz/Network/img/11.png)]

"""
套接字使用基础示例
"""
import socket

# 创建一个UDP套接字
udp_socket = socket.socket(socket.AF_INET,
                           socket.SOCK_DGRAM)

"""
1. 127.0.0.1 或者 localhost 那么 另外一端只能在
同一个计算机上通过127.0.0.1 访问之

2. 绑定自己的网络IP地址,那么另外一端可以在任何位置
通过 该主机IP地址访问之

3. 绑定自己的网络0.0.0.0地址,那么另外一端可以在
同一计算机上使用127.0.0.1访问之 或者 在任何位置
通过IP地址访问之
"""
udp_socket.bind(("172.40.91.108", 8800))

"""
练习: 基于刚才程序的基础上完成
客户端可以循环发送内容,服务端接收
当客户端直接回车什么都不输入时客户端结束
"""
"""
udp 服务端基础功能示例
重点代码 !!!
"""

from socket import *

# 创建udp套接字
udp_socket = socket(AF_INET, SOCK_DGRAM)

# 绑定地址
udp_socket.bind(("0.0.0.0", 8888))

# 接收消息
while True:
    data, addr = udp_socket.recvfrom(5)
    print(addr, "收到:", data.decode())  # data字节串

    # 发送消息 发送字节串
    n = udp_socket.sendto(b"Thanks", addr)
    print("发送了%d bytes" % n)

# 关闭套接字
udp_socket.close()

"""
udp 客户端流程示例
重点代码 !!!
"""
from socket import *

# 服务端地址
ADDR = ("172.40.91.108", 8888)

# 创建udp套接字
udp_socket = socket(AF_INET, SOCK_DGRAM)

# 发送内容
while True:
    msg = input(">>")
    # msg为空 则退出循环
    if not msg:
        break

    udp_socket.sendto(msg.encode(), ADDR)

    # 接收反馈
    data, addr = udp_socket.recvfrom(1024)
    print("From server:", data.decode())

udp_socket.close()

"""
练习: 编写一个服务端和一个客户端
客户端循环输入单词,发送给服务端,从服务端获取
单词解释,打印出来。

* 使用 dict --> words 表完成
* 数据库和服务端一定是在一起的
"""

from socket import *
import pymysql

# 连接数据库的字典
DATABASE = {
    "host": "localhost",
    "port": 3306,
    "user": "root",
    "password": "123456",
    "database": "dict",
    "charset": "utf8"
}

# 服务器地址
ADDR = ("0.0.0.0", 8888)


# 数据处理
class Database:
    def __init__(self):
        self.db = pymysql.connect(**DATABASE)
        self.cur = self.db.cursor()

    def close(self):
        self.cur.close()
        self.db.close()

    # 查找单词
    def query_word(self, word):
        sql = "select mean from words where word=%s;"
        self.cur.execute(sql, [word])
        mean = self.cur.fetchone()  # 得到解释 ()
        # 考虑是否查询到
        if mean:
            return mean[0]
        else:
            return "Not Found"


def main():
    udp_socket = socket(AF_INET, SOCK_DGRAM)
    udp_socket.bind(ADDR)
    db = Database()  # 实例化对象,处理数据库方法

    # 接收单词
    while True:
        word, addr = udp_socket.recvfrom(50)
        # 查找单词解释
        mean = db.query_word(word.decode())
        # 发送单词解释
        udp_socket.sendto(mean.encode(), addr)

    # 关闭套接字
    udp_socket.close()


if __name__ == '__main__':
    main()
    
"""
客户端
"""
from socket import *

# 服务端地址
ADDR = ("172.40.91.108", 8888)

# 创建udp套接字
udp_socket = socket(AF_INET, SOCK_DGRAM)

# 发送内容
while True:
    word = input("Word:")
    # msg为空 则退出循环
    if not word:
        break

    udp_socket.sendto(word.encode(), ADDR)

    # 接收服务器发来的解释
    data, addr = udp_socket.recvfrom(1024)
    print("%s : %s" % (word, data.decode()))

udp_socket.close()
1.2.4 UDP套接字特点
  • 可能会出现数据丢失的情况
  • 传输过程简单,实现容易
  • 数据以数据包形式表达传输
  • 数据传输效率较高

1.3 TCP 传输方法

1.3.1 TCP传输特点
  • 面向连接的传输服务

    • 传输特征 : 提供了可靠的数据传输,可靠性指数据传输过程中无丢失,无失序,无差错,无重复。
    • 可靠性保障机制(都是操作系统网络服务自动帮应用完成的):
      • 在通信前需要建立数据连接
      • 确认应答机制
      • 通信结束要正常断开连接
  • 三次握手(建立连接)

    • 客户端向服务器发送消息报文请求连接
    • 服务器收到请求后,回复报文确定可以连接
    • 客户端收到回复,发送最终报文连接建立

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rs7nzOIa-1609849651566)(/home/tarena/month02/lz/Network/img/1_scws.png)]

  • 四次挥手(断开连接)
    • 主动方发送报文请求断开连接
    • 被动方收到请求后,立即回复,表示准备断开
    • 被动方准备就绪,再次发送报文表示可以断开
    • 主动方收到确定,发送最终报文完成断开

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OMrRiYvz-1609849651567)(/home/tarena/month02/lz/Network/img/1_schs.png)]

1.3.2 TCP服务端

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wNQ65ONh-1609849651569)(/home/tarena/month02/lz/Network/img/1_TCP_Server.png)]

  • 创建套接字
sockfd=socket.socket(socket_family,socket_type,proto=0)
功能:创建套接字
参数:socket_family  网络地址类型 AF_INET表示ipv4
	 socket_type  套接字类型 SOCK_STREAM 表示tcp套接字 (也叫流式套接字) 
	 proto  通常为0  选择子协议
返回值: 套接字对象
  • 绑定地址 (与udp套接字相同)
  • 设置监听
sockfd.listen(n)
功能 : 将套接字设置为监听套接字,确定监听队列大小
参数 : 监听队列大小
n:最多只能为1024

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8S9EOMv2-1609849651570)(/home/tarena/month02/lz/Network/img/11.jpg)]

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

n = connfd.send(data)
功能 : 发送消息
参数 :要发送的内容  bytes格式
返回值: 发送的字节数
  1. 关闭套接字 (与udp套接字相同)
"""
tcp 服务端基础示例
"""

from socket import *

# 创建tcp套接字 (不写参数默认也是tcp)
tcp_socket = socket(AF_INET,SOCK_STREAM)

# 绑定改地址
tcp_socket.bind(("0.0.0.0",8888))

# 设置监听
tcp_socket.listen(5)

# 等待客户端连接
print("等待连接......")
connfd,addr = tcp_socket.accept()
print("连接了:",addr)

# 收发消息
data = connfd.recv(1024)
print("接收到:",data.decode())

connfd.send(b"Thanks")

# 关闭套接字
connfd.close() # 断开连接
tcp_socket.close()



"""
tcp 客户端 基础示例
"""
from  socket import *

# 服务器地址
server_addr = ("127.0.0.1",8888)

# 创建tcp套接字
tcp_socket = socket()

# 连接服务器
tcp_socket.connect(server_addr)

# 收发消息
msg = input(">>")
tcp_socket.send(msg.encode())
data = tcp_socket.recv(1024)
print("从服务器收到:",data.decode())

# 关闭套接字
tcp_socket.close()
1.3.3 TCP客户端

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EWunuKJt-1609849651571)(/home/tarena/month02/lz/Network/img/1_TCP_Client.png)]

  • 创建TCP套接字
  • 请求连接
sockfd.connect(server_addr)
功能:连接服务器
参数:元组  服务器地址
  • 收发消息

注意: 防止两端都阻塞,recv send要配合

  • 关闭套接字

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HA0Rlr1r-1609849651573)(/home/tarena/month02/lz/Network/img/12.png)]

训练1: 将代码修改为客户端可以循环发送消息,接收thanks
      输入 ##  客户端服务端均退出

训练2: 客户端退出后,服务端不退出,继续可以连接
处理下一个客户端。
"""
tcp 服务端循环实例1
重点代码 !!!
"""

from socket import *
from time import sleep

# 创建tcp套接字 (不写参数默认也是tcp)
tcp_socket = socket(AF_INET,SOCK_STREAM)

# 绑定改地址
tcp_socket.bind(("0.0.0.0",8888))

# 设置监听
tcp_socket.listen(5)

# 循环等待客户端连接
while True:
    print("等待连接......")
    connfd,addr = tcp_socket.accept()
    print("连接了:",addr)

    # 循环与某一个客户端收发消息
    while True:
        data = connfd.recv(5)
        # 客户端断开连接,此时recv返回空字节串
        if not data:
            break
        # 收到了## 表示客户端已经退出
        # if data == b"##":
        #     break
        print("接收到:",data.decode())
        connfd.send(b"Thanks#")
        # sleep(0.1)
    connfd.close() # 断开连接

# 关闭套接字
tcp_socket.close()



"""
tcp 客户端 循环实例
重点代码 !!!
"""
from  socket import *

# 服务器地址
server_addr = ("127.0.0.1",8888)


# 创建tcp套接字
tcp_socket = socket()

# 连接服务器
tcp_socket.connect(server_addr)

# 收发消息
while True:
    msg = input(">>")
    # 直接回车msg为空
    if not msg:
        break

    tcp_socket.send(msg.encode())
    # 发送##告知服务端自己退出
    # if msg == "##":
    #     break
    data = tcp_socket.recv(1024)
    print("从服务器收到:",data.decode())

# 关闭套接字
tcp_socket.close()


"""
tcp 服务端 循环模型2
重点代码 !!! 
"""
from socket import *

# 创建tcp套接字
tcp_socket = socket()

# 绑定改地址
tcp_socket.bind(("0.0.0.0",8888))

# 设置监听
tcp_socket.listen(5)

# 循环等待客户端连接
while True:
    print("等待连接......")
    connfd,addr = tcp_socket.accept()
    print("连接了:",addr)

    # 收发消息 : 每次连接只能收发一次消息
    data = connfd.recv(1024)
    print("收到:",data.decode())
    connfd.send(b"Thanks")

    connfd.close()

tcp_socket.close()

"""
tcp 循环模型 2
重点代码 !!!
"""
from  socket import *

# 服务器地址
server_addr = ("127.0.0.1",8888)


# 所有连接操作 和数据操作 全在循环中
while True:
    msg = input(">>")
    if not msg:
        break

    tcp_socket = socket()
    tcp_socket.connect(server_addr)
    tcp_socket.send(msg.encode())
    data = tcp_socket.recv(1024)
    print("从服务器收到:",data.decode())
    tcp_socket.close()
    
"""
练习: 使用tcp完成,将一个图片从客户端上传的服务端
注意,图片有可能比较大,不允许一次性 read()读取
在服务端以当前日期为名字存储

2020-10-16.jpg

思路 : 客户段读取文件内容发送
       服务端接收内容,写入文件
       
补充要求 : 当文件上传完成后,服务端通知一下客户端
“上传成功”  客户端接收到这个消息后 打印出来
"""
from socket import *
from time import localtime

# 创建tcp套接字服务端
tcp_socket = socket()
tcp_socket.bind(("0.0.0.0",8888))
tcp_socket.listen(5)

# 循环接收客户端连接
while True:
    connfd,addr = tcp_socket.accept()
    # 打开一个以当前日期命名的文件
    filename = "%d-%d-%d.jpg"%localtime()[:3]
    file = open(filename,'wb')

    # 接收某一个客户端上传的图片
    while True:
        # 边收边写入
        data = connfd.recv(1024)
        if data == b'##':
            break
        file.write(data)
    file.close()

    # 发送通知
    connfd.send("上传完成".encode())

    connfd.close()

# 关闭套接字
tcp_socket.close()

"""
客户端
"""
from socket import *

# 服务器地址
server_addr = ("127.0.0.1", 8888)

# 创建tcp套接字
tcp_socket = socket()
tcp_socket.connect(server_addr)

file = open("timg.jfif", 'rb')  # 二进制打开

# 边读取边发送
while True:
    data = file.read(1024)
    if not data:
        break
    tcp_socket.send(data)

file.close()
tcp_socket.send(b"##")

# 接收通知
msg = tcp_socket.recv(1024)
print(msg.decode())

# 关闭套接字
tcp_socket.close()

"""
练习2: 编写一个程序,启动服务端后 作为对话机器人
小美。 客户端可以输入问题给小美,小美做出回答
如果没有找到答案则回复 “人家还小不知道”

要求,可以多个客户端一起启动
"""
from socket import *
import re

CHAT_FILE = "./chat.txt"
chat = []  # [(key,answer)]


# 提取文件中的内容,放在列表中
def answer():
    # 打开对话文件
    file = open(CHAT_FILE)
    for line in file:
        # 匹配出关键词和答案
        tup = re.findall(r"(\w+)\s+(.*)", line)
        chat.extend(tup)  # 列表的合并
    file.close()


# 找答案
def find(q):
    for key, value in chat:
        # 如果关键词在问题中
        if key in q:
            return value  # 返回准备的答案
    return "人家还小不知道啦!"


def main():
    answer()  # 生成列表

    # 创建监听套接字
    tcp_socket = socket()
    tcp_socket.bind(("0.0.0.0", 8888))
    tcp_socket.listen(5)

    # 循环等待客户端连接
    while True:
        print("等待问题......")
        connfd, addr = tcp_socket.accept()

        # 接收问题
        data = connfd.recv(1024)
        # 找答案
        value = find(data.decode())
        connfd.send(value.encode())
        connfd.close()

    tcp_socket.close()


if __name__ == '__main__':
    main()

"""
客户端
"""
from socket import *

# 服务器地址
server_addr = ("127.0.0.1", 8888)

# 所有连接操作 和数据操作 全在循环中
while True:
    msg = input("我:")
    if not msg:
        break

    tcp_socket = socket()
    tcp_socket.connect(server_addr)
    tcp_socket.send(msg.encode())
    data = tcp_socket.recv(1024)
    print("小美:", data.decode())
    tcp_socket.close()
1.3.4 TCP套接字细节
  • tcp连接中当一端退出,另一端如果阻塞在recv,此时recv会立即返回一个空字串。

  • tcp连接中如果一端已经不存在,仍然试图通过send向其发送数据则会产生BrokenPipeError

  • 一个服务端可以同时连接多个客户端,也能够重复被连接

  • tcp粘包问题

    • 产生原因

      • 为了解决数据再传输过程中可能产生的速度不协调问题,操作系统设置了缓冲区
      • 实际网络工作过程比较复杂,导致消息收发速度不一致
      • tcp以字节流方式进行数据传输,在接收时不区分消息边界

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j7NoWXMI-1609849651574)(/home/tarena/month02/lz/Network/img/13.jpg)]

    • 带来的影响

      • 如果每次发送内容是一个独立的含义,需要接收端独立解析此时粘包会有影响。
    • 处理方法

      • 人为的添加消息边界,用作消息之间的分割
      • 控制发送的速度
1.3.5 TCP与UDP对比
  • 传输特征

    • TCP提供可靠的数据传输,但是UDP则不保证传输的可靠性
    • TCP传输数据处理为字节流,而UDP处理为数据包形式
    • TCP传输需要建立连接才能进行数据传,效率相对较低,UDP比较自由,无需连接,效率较高
  • 套接字编程区别

    • 创建的套接字类型不同
    • tcp套接字会有粘包,udp套接字有消息边界不会粘包
    • tcp套接字依赖listen accept建立连接才能收发消息,udp套接字则不需要
    • tcp套接字使用send,recv收发消息,udp套接字使用sendto,recvfrom
  • 使用场景

    • tcp更适合对准确性要求高,传输数据较大的场景
      • 文件传输:如下载电影,访问网页,上传照片
      • 邮件收发
      • 点对点数据传输:如点对点聊天,登录请求,远程访问,发红包
    • udp更适合对可靠性要求没有那么高,传输方式比较自由的场景
      • 视频流的传输: 如直播,视频聊天
      • 广播:如网络广播,群发消息
      • 实时传输:如游戏画面
    • 在一个大型的项目中,可能既涉及到TCP网络又有UDP网络

1.4 数据传输过程

1.4.1 传输流程
  • 发送端由应用程序发送消息,逐层添加首部信息,最终在物理层发送消息包。
  • 发送的消息经过多个节点(交换机,路由器)传输,最终到达目标主机。
  • 目标主机由物理层逐层解析首部消息包,最终到应用程序呈现消息。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-txGd6gkg-1609849651575)(/home/tarena/month02/lz/Network/img/14.png)]

1.4.2 TCP协议首部(了解)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9OLHEZFZ-1609849651577)(/home/tarena/month02/lz/Network/img/1_tcpsjb.png)]

  • 源端口和目的端口 各占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. 多任务编程

2.1 多任务概述

  • 多任务

    即操作系统中可以同时运行多个任务。比如我们可以同时挂着qq,听音乐,同时上网浏览网页。这是我们看得到的任务,在系统中还有很多系统任务在执行,现在的操作系统基本都是多任务操作系统,具备运行多任务的能力。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IBbj8IHd-1609849651577)(/home/tarena/month02/lz/Network/img/13.png)]

  • 计算机原理

    • CPU:计算机硬件的核心部件,用于对任务进行执行运算。

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BCu3WtYH-1609849651578)(/home/tarena/month02/lz/Network/img/14.jpg)]

    • 操作系统调用CPU执行任务

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b5ZE0ZHl-1609849651579)(/home/tarena/month02/lz/Network/img/15.png)]

    • cpu轮训机制 : cpu都在多个任务之间快速的切换执行,切换速度在微秒级别,其实cpu同时只执行一个任务,但是因为切换太快了,从应用层看好像所有任务同时在执行。

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1ctP8ssz-1609849651582)(/home/tarena/month02/lz/Network/img/16.gif)]

    • 多核CPU:现在的计算机一般都是多核CPU,比如四核,八核,我们可以理解为由多个单核CPU的集合。这时候在执行任务时就有了选择,可以将多个任务分配给某一个cpu核心,也可以将多个任务分配给多个cpu核心,操作系统会自动根据任务的复杂程度选择最优的分配方案。

      • 并发 : 多个任务如果被分配给了一个cpu内核,那么这多个任务之间就是并发关系,并发关系的多个任务之间并不是真正的‘“同时”。
      • 并行 : 多个任务如果被分配给了不同的cpu内核,那么这多个任务之间执行时就是并行关系,并行关系的多个任务时真正的“同时”执行。
  • 什么是多任务编程

    多任务编程即一个程序中编写多个任务,在程序运行时让这多个任务一起运行,而不是一个一个的顺次执行。

    比如微信视频聊天,这时候在微信运行过程中既用到了视频任务也用到了音频任务,甚至同时还能发消息。这就是典型的多任务。而实际的开发过程中这样的情况比比皆是。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cn6WZOgf-1609849651583)(/home/tarena/month02/lz/Network/img/12.jpg)]

    • 实现多任务编程的方法 : 多进程编程,多线程编程
  • 多任务意义

    • 提高了任务之间的配合,可以根据运行情况进行任务创建。

      比如: 你也不知道用户在微信使用中是否会进行视频聊天,总不能提前启动起来吧,这是需要根据用户的行为启动新任务。

    • 充分利用计算机资源,提高了任务的执行效率。

      • 在任务中无阻塞时只有并行状态才能提高效率

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S9iE17MR-1609849651584)(/home/tarena/month02/lz/Network/img/17.jpg)]

      • 在任务中有阻塞时并行并发都能提高效率

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cb7EDWLQ-1609849651585)(/home/tarena/month02/lz/Network/img/18.jpg)]

2.2 进程(Process)

2.2.1 进程概述
  • 定义: 程序在计算机中的一次执行过程。

    • 程序是一个可执行的文件,是静态的占有磁盘。

    • 进程是一个动态的过程描述,占有计算机运行资源,有一定的生命周期。

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9957Xoof-1609849651586)(/home/tarena/month02/lz/Network/img/19.jpg)]

  • 进程状态

    • 三态
      就绪态 : 进程具备执行条件,等待系统调度分配cpu资源

       运行态 : 进程占有cpu正在运行 
      
       等待态 : 进程阻塞等待,此时会让出cpu
      

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-axyl3Jko-1609849651588)(/home/tarena/month02/lz/Network/img/4_3.png)]

    • 五态 (在三态基础上增加新建和终止)

       新建 : 创建一个进程,获取资源的过程
      
       终止 : 进程结束,释放资源的过程
      

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oBfyrMhm-1609849651589)(/home/tarena/month02/lz/Network/img/4_5.png)]

  • 进程命令

    • 查看进程信息

      ps -aux
      

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QdPmkhVU-1609849651590)(/home/tarena/month02/lz/Network/img/20.png)]

      • USER : 进程的创建者
      • PID : 操作系统分配给进程的编号,大于0的整数,系统中每个进程的PID都不重复。PID也是重要的区分进程的标志。
      • %CPU,%MEM : 占有的CPU和内存
      • STAT : 进程状态信息,S I 表示阻塞状态 ,R 表示就绪状态或者运行状态
      • START : 进程启动时间
      • COMMAND : 通过什么程序启动的进程
    • 进程树形结构

      pstree
      
      • 父子进程:在Linux操作系统中,进程形成树形关系,任务上一级进程是下一级的父进程,下一级进程是上一级的子进程。
2.2.2 多进程编程
  • 使用模块 : multiprocessing

  • 创建流程

    【1】 将需要新进程执行的事件封装为函数

    【2】 通过模块的Process类创建进程对象,关联函数

【3】 可以通过进程对象设置进程信息及属性

【4】 通过进程对象调用start启动进程

【5】 通过进程对象调用join回收进程资源

  • 主要类和函数使用
Process()
功能 : 创建进程对象
参数 : target 绑定要执行的目标函数 
	   args 元组,用于给target函数位置传参
	   kwargs 字典,给target函数键值传参
p.start()
功能 : 启动进程

注意 : 启动进程此时target绑定函数开始执行,该函数作为新进程执行内容,此时进程真正被创建

p.join([timeout])
功能:阻塞等待回收进程
参数:超时时间
"""
Process进程基础创建示例:

将需要新进程执行的事件封装为函数
通过模块的Process类创建进程对象,关联函数
通过进程对象调用start启动进程
通过进程对象调用join回收进程资源
"""
import multiprocessing as mp
from time import sleep

a = 1


# 进程的执行函数
def fun():
    print("开始执行一个进程内容")
    global a
    print("a = ", a)
    a = 10000
    sleep(3)
    print("一个任务假装执行了3秒结束")


# 创建进程对象 绑定函数
p = mp.Process(target=fun)

# 启动进程  这时进程产生,进程执行fun函数
p.start()

print("我也要干点事情")
sleep(2)
print("我这件事做了2秒")

# 阻塞等待回收进程 将创建的进程资源释放
p.join()

print("a:", a)  # 1


"""
含有参数的进程函数示例
"""
from multiprocessing import Process
from time import sleep


# 含有参数的进程函数
def worker(sec, name):
    for i in range(3):
        sleep(sec)
        print("I'm %s" % name)
        print("I'm working")


# 位置传参 args = (,)一定为元组
# p = Process(target=worker,args=(2,"Levi"))

# 关键字传参 kwargs={}
p = Process(target=worker,
            args=(2,),
            kwargs={"name": "Tom"})

p.start()
p.join(3)  # 最多等待3秒

print("==================")


"""
同时创建多个子进程
"""
from multiprocessing import Process
from time import sleep
import os, sys


def th1():
    sleep(3)
    print("吃饭")
    print(os.getppid(), "---", os.getpid())


def th2():
    sleep(2)
    print("睡觉")
    print(os.getppid(), "---", os.getpid())


def th3():
    sys.exit("打豆豆进程退出")
    sleep(4)
    print("打豆豆")
    print(os.getppid(), "---", os.getpid())


things = [th1, th2, th3]
jobs = []  # 存储进程对象
# 循环创建进程
for th in things:
    p = Process(target=th)
    jobs.append(p)  # 存储进程对象
    p.start()

for i in jobs:
    i.join()



"""
练习1 : 大文件拆分
将一个文件拆分成2个部分,按照字节数平分
要求使用两个子进程完成这件事,要求上下两个部分
的拆分工作同时进程

思路: 一个进程拷贝上半部分  函数
      一个进程拷贝下半部分  函数
      两个子进程同时执行
      os.path.getsize() 获取文件大小
      文件操作位置: seek(1000,0)
"""
from multiprocessing import Process
import os

# 获取文件大小
size = os.path.getsize("zly.jpg")


# 如果父进程中打开文件,各子进程直接使用fr
# 那么文件偏移量会相互影响,所以这里应该在各自子
# 进程中打开
# fr = open("zly.jpg", 'rb')

# 上半部分
def top():
    fr = open("zly.jpg", 'rb')
    fw = open("top.jpg", 'wb')
    n = size // 2  # 从头开始复制n个字节
    while n >= 1024:
        fw.write(fr.read(1024))
        n -= 1024
    else:
        fw.write(fr.read(n))
    fr.close()
    fw.close()


# 下半部分
def bot():
    fr = open("zly.jpg", 'rb')
    fw = open("bot.jpg", 'wb')
    fr.seek(size // 2, 0)  # 偏移量放在中间
    while True:
        # 从一半开始复制
        data = fr.read(1024)
        if not data:
            break
        fw.write(data)
    fr.close()
    fw.close()


jobs = []  # 存储进程对象
# 循环创建进程
for th in [bot, top]:
    p = Process(target=th)
    jobs.append(p)  # 存储进程对象
    p.start()

[i.join() for i in jobs]
  • 进程执行现象理解 (难点)

    • 新的进程是原有进程的子进程,子进程复制父进程全部内存空间代码段,一个进程可以创建多个子进程。
    • 子进程只执行指定的函数,其余内容均是父进程执行内容,但是子进程也拥有其他父进程资源。
    • 各个进程在执行上互不影响,也没有先后顺序关系。
    • 进程创建后,各个进程空间独立,相互没有影响。
    • multiprocessing 创建的子进程中无法使用标准输入(input)。
  • 进程对象属性

    • p.name 进程名称
    • p.pid 对应子进程的PID号
    • p.is_alive() 查看子进程是否在生命周期
    • p.daemon 设置父子进程的退出关系
      • 如果设置为True则该子进程会随父进程的退出而结束
      • 要求必须在start()前设置
      • 如果daemon设置成True 通常就不会使用 join()
"""
进程属性信息 解释
"""
from multiprocessing import Process
import time


def fun():
    for i in range(3):
        print(time.ctime())
        time.sleep(2)


# 创建进程对象
p = Process(target=fun, name="Aid")

# 该子进程会随父进程而退出 start前设置
p.daemon = True

p.start()  # 进程有了

print("Name:", p.name)  # 进程名
print("PID:", p.pid)  # PID
print("is alive:", p.is_alive())
2.2.3 进程处理细节
  • 进程相关函数
os.getpid()
功能: 获取一个进程的PID值
返回值: 返回当前进程的PID 
os.getppid()
功能: 获取父进程的PID号
返回值: 返回父进程PID
sys.exit(info)
功能:退出进程
参数:字符串 表示退出时打印内容
  • 孤儿和僵尸

    • 孤儿进程 : 父进程先于子进程退出,此时子进程成为孤儿进程。

      • 特点: 孤儿进程会被系统进程收养,此时系统进程就会成为孤儿进程新的父进程,孤儿进程退出该进程会自动处理。
    • 僵尸进程 : 子进程先于父进程退出,父进程又没有处理子进程的退出状态,此时子进程就会成为僵尸进程。

      • 特点: 僵尸进程虽然结束,但是会存留部分进程信息资源在内存中,大量的僵尸进程会浪费系统的内存资源。

      • 如何避免僵尸进程产生

        1. 使用join()回收

        2. 在父进程中使用signal方法处理

        from signal import *
        signal(SIGCHLD,SIG_IGN)
        
"""
僵尸进程的处理
"""
from multiprocessing import Process
from time import sleep
from signal import *


def worker():
    for i in range(3):
        sleep(2)
        print("I'm Levi")
        print("I'm working")


# 忽略子进程退出
signal(SIGCHLD, SIG_IGN)

p = Process(target=worker)
p.start()
print(p.pid)

# p.join() # 处理僵尸

while True:
    pass
2.2.5 创建进程类

进程的基本创建方法将子进程执行的内容封装为函数。如果我们更热衷于面向对象的编程思想,也可以使用类来封装进程内容。

  • 创建步骤

    【1】 继承Process类

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

    【3】 重写run()方法

  • 使用方法

    【1】 实例化对象

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

    【3】 调用join回收进程

"""
自定义进程类
"""

from multiprocessing import Process

class MyProcess(Process):
    def __init__(self,value):
        self.value = value
        # 调用父类init保留父类属性
        super().__init__()

    def fun1(self):
        print("假设这个事情很复杂")

    def fun2(self):
        print("特别复杂 too",self.value)

    # 进程做的事情
    def run(self):
        self.fun1()
        self.fun2()

p = MyProcess(3)
p.start() # 运行run'作为一个进程
p.join()

# 猜想源码怎么写的
# class Process:
#     def __init__(self,target=None):
#         self.target = target
#
#     def run(self):
#         self.target()
#
#     def start(self):
#         self.run()
"""
作业: 1. 进程函数使用熟练,自定义进程类
求100000以内质数之和,写成一个函数
写一个装饰器求一个这个函数运行时间

将100000分成4等份 分别使用4个进程求
每一份的质数之和,四个进程同时执行
记录时间

将100000分成10等份 分别使用10个进程求
每一份的质数之和,10个进程同时执行
记录时间
"""
import time
from multiprocessing import Process


# 装饰器
def timeis(f):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        res = f(*args, **kwargs)
        end_time = time.time()
        print("函数执行时间:", end_time - start_time)
        return res

    return wrapper


# 判断一个数n是否为质数
def isprime(n):
    if n <= 1:
        return False
    for i in range(2, n // 2 + 1):
        if n % i == 0:
            return False
    return True


class Prime(Process):
    def __init__(self, begin, end):
        self.begin = begin
        self.end = end
        super().__init__()

    def run(self):
        prime = []
        for i in range(self.begin, self.end):
            if isprime(i):
                prime.append(i)  # 是质数加到列表中
        print(sum(prime))


@timeis
def multi_process(n):
    jobs = []
    step = 100000 // n
    for i in range(1, 100001, step):
        p = Prime(i, i + step)  # 数值区间
        jobs.append(p)
        p.start()
    [i.join() for i in jobs]


# @timeis
# def prime_sum():
#     prime = []
#     for i in range(1,100001):
#         if isprime(i):
#             prime.append(i) # 是质数加到列表中
#     print(sum(prime))

multi_process(10)

# 函数执行时间: 6.944215297698975
# multi_process(10)

# 函数执行时间: 7.557642936706543
# multi_process(4)

# 函数执行时间: 14.412267923355103
# prime_sum()
2.2.4 进程池
  • 必要性

    【1】 进程的创建和销毁过程消耗的资源较多

    【2】 当任务量众多,每个任务在很短时间内完成时,需要频繁的创建和销毁进程。此时对计算机压力较大

    【3】 进程池技术很好的解决了以上问题。

  • 原理

    创建一定数量的进程来处理事件,事件处理完进程不退出而是继续处理其他事件,直到所有事件全都处理完毕统一销毁。增加进程的重复利用,降低资源消耗。

  • 进程池实现

  1. 创建进程池对象,放入适当的进程
from multiprocessing import Pool

Pool(processes)
功能: 创建进程池对象
参数: 指定进程数量,默认根据系统自动判定
  1. 将事件加入进程池队列执行
pool.apply_async(func,args,kwds)
功能: 使用进程池执行 func事件
参数: func 事件函数
      args 元组  给func按位置传参
      kwds 字典  给func按照键值传参
  1. 关闭进程池
pool.close()
功能: 关闭进程池
  1. 回收进程池中进程
pool.join()
功能: 回收进程池中进程
"""
进程池使用示例

* 父进程退出则进程池会自动销毁
"""
from multiprocessing import Pool
from time import sleep, ctime
import random


# 进程池事件函数
def worker(msg, sec):
    print(ctime(), "---", msg)
    sleep(sec)


# 创建进程池
pool = Pool(4)

# 向进程池队列添加事件
for i in range(10):
    msg = "Tedu-%d" % i
    pool.apply_async(func=worker,
                     args=(msg, random.randint(1, 4)))

# 关闭进程池  不能添加新的事件
pool.close()

# 阻塞回收进程池
pool.join()

"""
练习1 :  使用进程池完成
拷贝一个指定的目录 (文件夹中全是普通文件没有子文件夹)

思路 : 1. 什么事情作为进程池事件  (拷贝文件)
       2. 拷贝文件函数  找共性封装  特性传参

       os.listdir()
       os.mkdir("xxx")
"""

from multiprocessing import Pool, Queue
import os

q = Queue()  # 创建一个消息队列


# 拷贝每一个文件 --》 进程池要做的事情
def copy(filename, old_folder, new_folder):
    fr = open(old_folder + "/" + filename, 'rb')
    fw = open(new_folder + "/" + filename, 'wb')
    while True:
        data = fr.read(1024)
        if not data:
            break
        n = fw.write(data)  # 获取已经拷贝的大小
        q.put(n)  # 将字节数放到消息队列
    fr.close()
    fw.close()


# 获取文件夹总大小
def get_size(dir):
    total_size = 0
    for file in os.listdir(dir):
        total_size += os.path.getsize(dir + '/' + file)
    return total_size  # 返回文件夹大小


# 创建进程池 参数为要拷贝的目录
def main(old_folder):
    # 创建新文件夹
    new_folder = old_folder + "-备份"
    os.mkdir(new_folder)
    total_size = get_size(old_folder)

    pool = Pool(4)
    for file in os.listdir(old_folder):
        pool.apply_async(func=copy,
                         args=(file, old_folder, new_folder))

    pool.close()

    copy_size = 0  # 已经拷贝的大小
    while copy_size < total_size:
        copy_size += q.get()
        print("已拷贝:%.2f%%" % (copy_size / total_size * 100))

    pool.join()


if __name__ == '__main__':
    main("/home/tarena/FTP")
2.2.5 进程通信
  • 必要性: 进程间空间独立,资源不共享,此时在需要进程间数据传输时就需要特定的手段进行数据通信。

  • 常用进程间通信方法:消息队列,套接字等。

  • 消息队列使用

    • 通信原理: 在内存中开辟空间,建立队列模型,进程通过队列将消息存入,或者从队列取出完成进程间通信。

    • 实现方法

      from multiprocessing import Queue
      
      q = Queue(maxsize=0)
      功能: 创建队列对象
      参数:最多存放消息个数
      返回值:队列对象
      
      q.put(data,[block,timeout])
      功能:向队列存入消息
      参数:data  要存入的内容
      	block  设置是否阻塞 False为非阻塞
      	timeout  超时检测
      	block和timeout一般情况下不会写
          
      q.get([block,timeout])
      功能:从队列取出消息
      参数:block  设置是否阻塞 False为非阻塞
      	 timeout  超时检测
      返回值: 返回获取到的内容
      
      q.full()   判断队列是否为满
      q.empty()  判断队列是否为空
      q.qsize()  获取队列中消息个数
      q.close()  关闭队列
      
"""
消息队列进程间通信演示
"""
from multiprocessing import Process,Queue

# 创建消息队列
q = Queue()

def request():
    name = "Levi"
    passwd = "123456"
    # 通过消息队列传送给另外一个进程
    q.put(name)
    q.put(passwd)

def handle():
    # 从消息队列获取内容
    name = q.get()
    passwd = q.get()
    print("用户:",name)
    print("密码:",passwd)

p1 = Process(target=request)
p2 = Process(target=handle)
p1.start()
p2.start()
p1.join()
p2.join()

**群聊聊天室 **

功能 : 类似qq群功能

【1】 有人进入聊天室需要输入姓名,姓名不能重复

【2】 有人进入聊天室时,其他人会收到通知:xxx 进入了聊天室

【3】 一个人发消息,其他人会收到:xxx : xxxxxxxxxxx

【4】 有人退出聊天室,则其他人也会收到通知:xxx退出了聊天室

【5】 扩展功能:服务器可以向所有用户发送公告:管理员消息: xxxxxxxxx

群聊聊天室

需求分析

* 有人进入聊天室需要输入姓名,姓名不能重复
* 有人进入聊天室时,其他人会收到通知:xxx 进入了聊天室
* 一个人发消息,其他人会收到:xxx : xxxxxxxxxxx
* 有人退出聊天室,则其他人也会收到通知:xxx退出了聊天室

技术分析  c/s

存储人员信息: 服务端
   存什么 : 名字  地址
   怎么存 : {name:address}
            [(name,address),...]
            class Person:
                def __init__(self,name,address):
                   self.name = name
                   self.address = address

消息的网络传递 : udp
    消息发送:转发的方法   客户端-》服务端-》客户端
    收发消息: 多进程,一个负责发送,一个负责接收


功能模块划分 封装方法 : 函数封装

    框架模型

    进入聊天室

    聊天

    退出聊天室


网络协议设置

           请求类型     数据参量
    进入     LOGIN      name

    聊天     CHAT    name   说话的内容

    退出     EXIT


功能模块逻辑具体分析

    框架模型
        服务端 : 1. 创建udp网络服务端
                 2. 循环接收各种客户端请求
                 3. 根据请求做出调用

        客户端 : 1. 创建udp网络

    进入聊天室
        客户端 : 1. 输入姓名
                 2. 发送给服务端
                 3. 接收服务端反馈
                 4. Y 进入聊天 N 回到第一步

        服务端 : 1. 接收请求
                 2. 判断是否有这个姓名
                 3. 根据判断发送结果
                     Y -》存储用户 告知其他人
                     N -over

    聊天
       客户端 :1. 创建子进程
               2. 父进程循环发送消息
                  子进程循环接收消息

       服务端 : 1. 接收请求
                2. 将消息转发给其他人


    退出聊天室
       客户端 1. 输入exit 表示退出
             2. 发送请求

       服务端 1. 接收请求
             2. 告知其他人
             3. 删除该用户


优化完善
"""
Author: Levi
Email: lvze@tedu.cn
Time:  2020-10-20
env: python3.6  pycharm
socket and Process exercise
"""
from socket import *
from multiprocessing import Process

# 服务器地址
HOST = "0.0.0.0"
PORT = 8000
ADDR = (HOST, PORT)

# 存储用户信息  {name:address}
user = {}


# 处理进入聊天室
def login(sock, name, addr):
    if name in user or "管理" in name:
        # 反馈结果
        sock.sendto(b"FAIL", addr)
    else:
        sock.sendto(b"OK", addr)
        # 循环通知其他人
        msg = "欢迎 %s 进入聊天室" % name
        for i in user:
            sock.sendto(msg.encode(), user[i])
        user[name] = addr  # 增加该用户
    # print(user)


# 处理聊天
def chat(sock, name, content):
    msg = "%s : %s" % (name, content)
    for i in user:
        # 出去本人
        if i != name:
            sock.sendto(msg.encode(), user[i])


# 处理退出
def exit(sock, name):
    del user[name]  # 删除用户
    msg = "%s 退出了聊天室" % name
    for i in user:
        sock.sendto(msg.encode(), user[i])


# 处理客户端请求
def request(sock):
    # 循环接收各种客户端请求 (总分模式)
    while True:
        # 接收所有客户端所有请求
        data, addr = sock.recvfrom(1024)
        # 对数据结构进行简单解析
        tmp = data.decode().split(' ', 2)
        if tmp[0] == "LOGIN":
            # tmp --> [LOGIN,name]
            login(sock, tmp[1], addr)
        elif tmp[0] == "CHAT":
            # tmp --> [CHAT,name,content]
            chat(sock, tmp[1], tmp[2])
        elif tmp[0] == "EXIT":
            # tmp--> [EXIT,name]
            exit(sock, tmp[1])


# 程序启动函数
def main():
    # UDP套接字
    sock = socket(AF_INET, SOCK_DGRAM)
    sock.bind(ADDR)

    # 创建子进程
    p = Process(target=request, args=(sock,))
    p.daemon = True
    p.start()

    # 发送管理员信息
    while True:
        content = input("管理员消息:")
        if content == 'exit':
            break
        msg = "CHAT 管理员消息 " + content
        sock.sendto(msg.encode(), ADDR)


if __name__ == '__main__':
    main()

"""
chat room 客户端代码
"""

from socket import *
from multiprocessing import Process
import sys

# 服务器地址
ADDR = ('119.3.124.77', 8000)


# 处理登录
def login(sock):
    while True:
        # 进入聊天室
        name = input("Name:")
        # 发送姓名
        msg = "LOGIN " + name
        sock.sendto(msg.encode(), ADDR)
        # 接收结果
        result, addr = sock.recvfrom(128)
        if result.decode() == 'OK':
            print("进入聊天室")
            return name
        else:
            print("该用户已存在")


# 接收消息
def recv_msg(sock):
    while True:
        data, addr = sock.recvfrom(1024 * 10)
        msg = "\n%s\n发言:" % data.decode()
        print(msg, end="")  # 不换行


# 发送消息
def send_msg(sock, name):
    while True:
        try:
            content = input("发言:")
        except KeyboardInterrupt:
            content = "exit"
        # 输入exit表示要退出聊天室
        if content == "exit":
            msg = "EXIT " + name
            sock.sendto(msg.encode(), ADDR)
            sys.exit("您已退出聊天室")

        msg = "CHAT %s %s" % (name, content)
        # 给服务端发送聊天请求
        sock.sendto(msg.encode(), ADDR)



# 网络连接
def main():
    sock = socket(AF_INET, SOCK_DGRAM)
    sock.bind(("0.0.0.0",12345)) # 地址不变化
    name = login(sock)  # 进入聊天室

    # 创建子进程 用于接收消息
    p = Process(target=recv_msg, args=(sock,))
    p.daemon = True  # 父进程退出子进程也退出
    p.start()
    send_msg(sock, name)  # 父进程发送消息


if __name__ == '__main__':
    main()

2.3 线程 (Thread)

2.3.1 线程概述
  • 什么是线程

    【1】 线程被称为轻量级的进程,也是多任务编程方式

​ 【2】 也可以利用计算机的多cpu资源

​ 【3】 线程可以理解为进程中再开辟的分支任务

  • 线程特征

    ​ 【1】 一个进程中可以包含多个线程

    ​ 【2】 线程也是一个运行行为,消耗计算机资源

​ 【3】 一个进程中的所有线程共享这个进程的资源

​ 【4】 多个线程之间的运行同样互不影响各自运行

​ 【5】 线程的创建和销毁消耗资源远小于进程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H7gPpSky-1609849651591)(/home/tarena/month02/lz/Network/img/21.jpg)]

2.3.2 多线程编程
  • 线程模块: threading

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5IH2yhMv-1609849651592)(/home/tarena/month02/lz/Network/img/22.jpg)]

  • 创建方法

    【1】 创建线程对象

from threading import Thread 

t = Thread()
功能:创建线程对象
参数:target 绑定线程函数
     args   元组 给线程函数位置传参
     kwargs 字典 给线程函数键值传参

​ 【2】 启动线程

 t.start()

​ 【3】 回收线程

 t.join([timeout])
"""
线程基本使用示例
"""

import threading
from time import sleep
import os

a = 1


# 线程的执行函数
def music():
    for i in range(3):
        sleep(2)
        print(os.getpid(), "播放:甜蜜蜜")
    global a
    print("a = ", a)
    a = 10000


# 创建线程对象
t = threading.Thread(target=music)

# 启动线程
t.start()

for i in range(4):
    sleep(1)
    print(os.getpid(), "播放:葫芦娃")

# 回收线程
t.join()

print("a:", a) 

"""
创建线程实例2
"""
from threading import Thread
from time import sleep


# 含有参数的线程函数
def fun(sec, name):
    print("含有参数的线程函数")
    sleep(sec)
    print("%s线程执行完成" % name)


# 创建多个线程
jobs = []
for i in range(5):
    t = Thread(target=fun,
               args=(2,),
               kwargs={"name": "T%d" % i})
    jobs.append(t)
    t.start()  # 启动线程

# 回收线程
[i.join() for i in jobs]

  • 线程对象属性
    • t.setName() 设置线程名称
    • t.getName() 获取线程名称
    • t.is_alive() 查看线程是否在生命周期
    • t.setDaemon() 设置daemon属性值
    • t.isDaemon() 查看daemon属性值
      • daemon为True时主线程退出分支线程也退出。要在start前设置,通常不和join一起使用。
"""
线程属性
"""

from threading import Thread
from time import sleep


def fun():
    sleep(3)
    print("线程属性设置")


t = Thread(target=fun)

# 分之线程会随主线程退出
t.setDaemon(True)

t.start()

print(t.is_alive())
t.setName("Tarena")  # 线程名称
print(t.getName())

"""
练习:模拟一个售票系统程序
一共500张票 ---》T1---T500

编程10个线程模拟10个售票窗口机器 记为 W1-W10
10个窗口同时售票知道所有票都卖出为止

票按照顺序出售
每个窗口卖出一张后   w2----T346
卖出一张需要0.1s
"""

from threading import Thread
from time import sleep

ticket = ["T%d" % x for x in range(1, 501)]


# 买票函数
def sell(w):
    while ticket:
        print("%s --- %s" % (w, ticket.pop(0)))
        sleep(0.1)


jobs = []
for i in range(1, 11):
    t = Thread(target=sell, args=("w%d" % i,))
    jobs.append(t)
    t.start()  # 启动线程

# 回收线程
[i.join() for i in jobs]

2.3.3 创建线程类
  1. 创建步骤

    【1】 继承Thread类

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

    【3】 重写run()方法

  2. 使用方法

    【1】 实例化对象

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

    【3】 调用join回收线程

"""
自定义线程类
"""
from threading import Thread
import time


class MyThread(Thread):
    def __init__(self, song):
        self.song = song
        super().__init__()  # 加载父类方法

    def run(self):
        for i in range(3):
            print("playing %s:%s" % (self.song, time.ctime()))
            time.sleep(2)


t = MyThread("凉凉")
t.start()  # 运行run方法,作为一个线程执行
t.join()
2.3.4 线程同步互斥
  • 线程通信方法: 线程间使用全局变量进行通信

  • 共享资源争夺

    • 共享资源:多个进程或者线程都可以操作的资源称为共享资源。对共享资源的操作代码段称为临界区。
    • 影响 : 对共享资源的无序操作可能会带来数据的混乱,或者操作错误。此时往往需要同步互斥机制协调操作顺序。
  • 同步互斥机制

    • 同步 : 同步是一种协作关系,为完成操作,多进程或者线程间形成一种协调,按照必要的步骤有序执行操作。

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NbgAWljn-1609849651594)(/home/tarena/month02/lz/Network/img/7_tb.png)]

    • 互斥 : 互斥是一种制约关系,当一个进程或者线程占有资源时会进行加锁处理,此时其他进程线程就无法操作该资源,直到解锁后才能操作。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e0g8BtqZ-1609849651595)(/home/tarena/month02/lz/Network/img/7_hc.png)]

  • 线程Event
from threading import Event

e = Event()  创建线程event对象

e.wait([timeout])  阻塞等待e被set

e.set()  设置e,使wait结束阻塞

e.clear() 使e回到未被设置状态

e.is_set()  查看当前e是否被设置
"""
event 同步互斥方法
"""
from threading import Thread, Event

e = Event()  # event对象

msg = None  # 用于线程通信


# 线程函数
def 杨子荣():
    print("杨子荣前来拜山头")
    global msg
    msg = "天王盖地虎"
    e.set()  # 解除阻塞


t = Thread(target=杨子荣)
t.start()

# 主线程验证口令
print("说对口令就是自己人")
e.wait()  # 阻塞等待
if msg == '天王盖地虎':
    print("宝塔镇河妖")
    print("确认过眼神,你是对的人")
else:
    print("打死他...无情啊...")

t.join()
  • 线程锁 Lock
from  threading import Lock

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

with  lock:  上锁
...
...
	 with代码块结束自动解锁
"""
lock 线程锁
"""
from threading import Thread, Lock

lock = Lock()

a = b = 0  # 共享资源


def value():
    while True:
        with lock:
            if a != b:
                print("a = %d,b = %d" % (a, b))
        # with语句块后自动解锁


t = Thread(target=value)
t.start()
while True:
    lock.acquire()  # 上锁
    a += 1
    b += 1
    lock.release()  # 解锁

t.join()

"""
练习2: 创建两个线程同时执行
一个线程负责打印 1---52   52个数字
另一个线程打印 A--Z  26个字母
要求打印结果为 12A34B.....5152Z
"""

from threading import Thread, Lock

lock1 = Lock()
lock2 = Lock()


def print_num():
    for i in range(1, 53, 2):
        lock1.acquire()
        print(i)
        print(i + 1)
        lock2.release()


def print_chr():
    for i in range(65, 91):
        lock2.acquire()
        print(chr(i))
        lock1.release()


t1 = Thread(target=print_num)
t2 = Thread(target=print_chr)

lock2.acquire()  # 让打印数字先执行

t1.start()
t2.start()
t1.join()
t2.join()
2.3.5 死锁
  • 什么是死锁

    死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DMlgWfL6-1609849651597)(/home/tarena/month02/lz/Network/img/ss.jpg)]

  • 死锁产生条件

    • 互斥条件:指线程使用了互斥方法,使用一个资源时其他线程无法使用。

    • 请求和保持条件:指线程已经保持至少一个资源,但又提出了新的资源请求,在获取到新的资源前不会释放自己保持的资源。

    • 不剥夺条件:不会受到线程外部的干扰,如系统强制终止线程等。

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

  • 如何避免死锁

    • 逻辑清晰,不要同时出现上述死锁产生的四个条件
    • 通过测试工程师进行死锁检测
2.3.6 GIL问题
  • 什么是GIL问题 (全局解释器锁)

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

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

  • GIL问题建议

    • 尽量使用进程完成无阻塞的并发行为

    • 不使用c作为解释器 (Java C#)

    Guido的声明:http://www.artima.com/forums/flat.jsp?forum=106&thread=214235

  • 结论

    • GIL问题与Python语言本身并没什么关系,属于解释器设计的历史问题。
    • 在无阻塞状态下,多线程程序程序执行效率并不高,甚至还不如单线程效率。
    • Python多线程只适用于执行有阻塞延迟的任务情形。
2.3.7 进程线程的区别联系
  • 区别联系
  1. 两者都是多任务编程方式,都能使用计算机多核资源
  2. 进程的创建删除消耗的计算机资源比线程多
  3. 进程空间独立,数据互不干扰,有专门通信方法;线程使用全局变量通信
  4. 一个进程可以有多个分支线程,两者有包含关系
  5. 多个线程共享进程资源,在共享资源操作时往往需要同步互斥处理
  6. Python线程存在GIL问题,但是进程没有。
  • 使用场景

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AJtN0P7h-1609849651597)(/home/tarena/month02/lz/Network/img/23.jpg)]

  1. 任务场景:一个大型服务,往往包含多个独立的任务模块,每个任务模块又有多个小独立任务构成,此时整个项目可能有多个进程,每个进程又有多个线程。
  2. 编程语言:Java,C#之类的编程语言在执行多任务时一般都是用线程完成,因为线程资源消耗少;而Python由于GIL问题往往使用多进程。

3. 网络并发模型

3.1 网络并发模型概述

  • 什么是网络并发

    在实际工作中,一个服务端程序往往要应对多个客户端同时发起访问的情况。如果让服务端程序能够更好的同时满足更多客户端网络请求的情形,这就是并发网络模型。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iDk2GGRX-1609849651598)(/home/tarena/month02/lz/Network/img/24.jpg)]

  • 循环网络模型问题

    循环网络模型只能循环接收客户端请求,处理请求。同一时刻只能处理一个请求,处理完毕后再处理下一个。这样的网络模型虽然简单,资源占用不多,但是无法同时处理多个客户端请求就是其最大的弊端,往往只有在一些低频的小请求任务中才会使用。

3.2 多任务并发模型

多任务并发模型具体指多进程多线程网络并发模型,即每当一个客户端连接服务器,就创建一个新的进程/线程为该客户端服务,客户端退出时再销毁该进程/线程,多任务并发模型也是实际工作中最为常用的服务端处理模型。

  • 优点:能同时满足多个客户端长期占有服务端需求,可以处理各种请求。
  • 缺点: 资源消耗较大
  • 适用情况:客户端请求较复杂,需要长时间占有服务器。
3.1.1 多进程并发模型
  • 创建网络套接字用于接收客户端请求
  • 等待客户端连接
  • 客户端连接,则创建新的进程具体处理客户端请求
  • 主进程继续等待其他客户端连接
  • 如果客户端退出,则销毁对应的进程
"""
多进程网络并发模型
重点代码 !!!

创建网络套接字用于接收客户端请求
等待客户端连接
客户端连接,则创建新的进程具体处理客户端请求
主进程继续等待其他客户端连接
如果客户端退出,则销毁对应的进程
"""
from socket import *
from multiprocessing import Process
from signal import *

# 服务器地址
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())
        connfd.send(b"ok")
    connfd.close()


# 函数中编写并发服务
def main():
    sock = socket()  # tcp套接字
    sock.bind(ADDR)
    sock.listen(5)

    print("Listen the port %d" % PORT)
    signal(SIGCHLD, SIG_IGN)  # 处理僵尸进程

    while True:
        # 循环接收客户端连接
        try:
            connfd, addr = sock.accept()
            print("Connect from", addr)
        except KeyboardInterrupt:
            # 退出服务
            sock.close()
            break
        # 为连接的客户端创建新进程
        p = Process(target=handle, args=(connfd,))
        p.daemon = True  # 客户端随服务端退出
        p.start()


if __name__ == '__main__':
    main()
3.1.2 多线程并发模型
  • 创建网络套接字用于接收客户端请求
  • 等待客户端连接
  • 客户端连接,则创建新的线程具体处理客户端请求
  • 主线程继续等待其他客户端连接
  • 如果客户端退出,则销毁对应的线程
"""
多线程网络并发模型
重点代码 !!!
"""
from socket import *
from threading import Thread

# 服务器地址
HOST = "0.0.0.0"
PORT = 8888
ADDR = (HOST, PORT)


# 实现具体的业务功能,客户端请求都在这里处理
class MyThread(Thread):
    def __init__(self, connfd):
        self.connfd = connfd
        super().__init__()

    def run(self):
        # 与客户端配合测试
        while True:
            data = self.connfd.recv(1024)
            if not data:
                break
            print(data.decode())
            self.connfd.send(b"ok")
        self.connfd.close()


# 函数中编写并发服务
def main():
    sock = socket()  # tcp套接字
    sock.bind(ADDR)
    sock.listen(5)

    print("Listen the port %d" % PORT)

    while True:
        # 循环接收客户端连接
        try:
            connfd, addr = sock.accept()
            print("Connect from", addr)
        except KeyboardInterrupt:
            # 退出服务
            sock.close()
            break
        # 使用自定义线程类为连接的客户端创建新线程
        t = MyThread(connfd)
        t.setDaemon(True)  # 客户端随服务端退出
        t.start()


if __name__ == '__main__':
    main()

ftp 文件服务器

【1】 分为服务端和客户端,要求可以有多个客户端同时操作。

【2】 客户端可以查看服务器文件库中有什么文件。

【3】 客户端可以从文件库中下载文件到本地。

【4】 客户端可以上传一个本地文件到文件库。

【5】 使用print在客户端打印命令输入提示,引导操作

"""
ftp 文件服务 客户端
c / s  连接   发请求   获取结果
"""
from socket import *
import sys
from time import sleep

# 服务端地址
ADDR = ("127.0.0.1", 8888)


# 发起请求的所有功能
class FTPClient:
    def __init__(self, sock):
        self.sock = sock

    # 请求文件列表
    def do_list(self):
        self.sock.send(b"LIST")  # 发送请求
        result = self.sock.recv(128).decode()  # 等回复
        # 根据不同结果分情况处理
        if result == "OK":
            # 接收文件列表
            while True:
                file = self.sock.recv(1024).decode()
                if file == '##':
                    break
                print(file)
        else:
            print("文件库为空")

    # 退出
    def do_exit(self):
        self.sock.send(b"EXIT")
        self.sock.close()
        sys.exit("谢谢使用")

    # 处理上传
    def do_put(self, file):
        # 测一下这个文件是否存在
        try:
            f = open(file, 'rb')
        except:
            print("文件不存在")
            return
        # 防止file带有文件路径,提取文件名
        filename = file.split("/")[-1]
        data = "STOR " + filename
        self.sock.send(data.encode())  # 发请求
        result = self.sock.recv(128).decode()  # 等待回复
        if result == 'OK':
            # 上传文件 读--》发送
            while True:
                data = f.read(1024)
                if not data:
                    break
                self.sock.send(data)
            sleep(0.1)
            self.sock.send(b"##")
            f.close()
        else:
            print("该文件已存在")

    # 处理下载
    def do_get(self, file):
        data = "RETR " + file
        self.sock.send(data.encode())  # 发请求
        result = self.sock.recv(128).decode()  # 等待回复
        if result == 'OK':
            # 接收文件
            f = open(file, 'wb')
            while True:
                data = self.sock.recv(1024)
                if data == b"##":
                    break
                f.write(data)
            f.close()
        else:
            print("该文件不存在")


# 启动函数
def main():
    sock = socket()
    sock.connect(ADDR)

    # 实例化对象用于调用方法
    ftp = FTPClient(sock)

    while True:
        print("""
        ============ 命令选项 =============
                      list 
                    get  file
                    put  file
                      exit
        ==================================
        """)
        cmd = input("请输入命令:")

        if cmd == "list":
            ftp.do_list()
        elif cmd == 'exit':
            ftp.do_exit()
        elif cmd[:3] == 'put':
            file = cmd.split(' ')[-1]
            ftp.do_put(file)
        elif cmd[:3] == 'get':
            file = cmd.split(' ')[-1]
            ftp.do_get(file)
        else:
            print("请输入正确命令")


if __name__ == '__main__':
    main()

    
"""
ftp文件管理服务端
多线程 tcp 并发
"""
from socket import *
from threading import Thread
import os
from time import sleep

# 服务器地址
HOST = "0.0.0.0"
PORT = 8888
ADDR = (HOST, PORT)

# 文件库位置
FTP = "/home/tarena/FTP/"


# 实现具体的业务功能,客户端请求都在这里处理
class FTPServer(Thread):
    def __init__(self, connfd):
        self.connfd = connfd
        super().__init__()

    # 处理请求文件列表
    def do_list(self):
        # 判断文件库是否为空
        files = os.listdir(FTP)
        if not files:
            self.connfd.send(b"FAIL")  # 失败
        else:
            self.connfd.send(b"OK")
            sleep(0.1)
            # 一次发送所有文件名
            data = "\n".join(files)
            self.connfd.send(data.encode())
            sleep(0.1)
            self.connfd.send(b"##")

    # 处理上传
    def do_put(self, filename):
        # 判断文件是否已存在
        if os.path.exists(FTP + filename):
            self.connfd.send(b"FAIL")
            return
        else:
            self.connfd.send(b"OK")
            # 接收文件
            f = open(FTP + filename, 'wb')
            while True:
                data = self.connfd.recv(1024)
                if data == b"##":
                    break
                f.write(data)
            f.close()

    # 处理下载
    def do_get(self, filename):
        try:
            f = open(FTP + filename, 'rb')
        except:
            # 文件不存在
            self.connfd.send(b"FAIL")
            return
        else:
            self.connfd.send(b"OK")
            sleep(0.1)
            # 发送文件
            while True:
                data = f.read(1024)
                if not data:
                    break
                self.connfd.send(data)
            sleep(0.1)
            self.connfd.send(b"##")
            f.close()

    # 线程启动方法
    def run(self):
        while True:
            # 接收某一个各类请求
            data = self.connfd.recv(1024).decode()
            # print(data)
            if not data or data == "EXIT":
                break
            elif data == "LIST":
                self.do_list()
            elif data[:4] == "STOR":
                filename = data.split(' ')[-1]
                self.do_put(filename)
            elif data[:4] == "RETR":
                filename = data.split(' ')[-1]
                self.do_get(filename)
        self.connfd.close()


# 函数中搭建并发结构
def main():
    sock = socket()  # tcp套接字
    sock.bind(ADDR)
    sock.listen(5)

    print("Listen the port %d" % PORT)

    while True:
        # 循环接收客户端连接
        try:
            connfd, addr = sock.accept()
            print("Connect from", addr)
        except KeyboardInterrupt:
            # 退出服务
            sock.close()
            break
        # 使用自定义线程类为连接的客户端创建新线程
        t = FTPServer(connfd)
        t.setDaemon(True)  # 客户端随服务端退出
        t.start()


if __name__ == '__main__':
    main()

3.3 IO并发模型

3.2.1 IO概述
  • 什么是IO

    在程序中存在读写数据操作行为的事件均是IO行为,比如终端输入输出 ,文件读写,数据库修改和网络消息收发等。

  • 程序分类

    • IO密集型程序:在程序执行中有大量IO操作,而运算操作较少。消耗cpu较少,耗时长。
    • 计算密集型程序:程序运行中运算较多,IO操作相对较少。cpu消耗多,执行速度快,几乎没有阻塞。
  • IO分类:阻塞IO ,非阻塞IO,IO多路复用,异步IO等。

3.2.2 阻塞IO
  • 定义:在执行IO操作时如果执行条件不满足则阻塞。阻塞IO是IO的默认形态。
  • 效率:阻塞IO效率很低。但是由于逻辑简单所以是默认IO行为。
  • 阻塞情况
    • 因为某种执行条件没有满足造成的函数阻塞
      e.g. accept input recv
    • 处理IO的时间较长产生的阻塞状态
      e.g. 网络传输,大文件读写
3.2.3 非阻塞IO
  • 定义 :通过修改IO属性行为,使原本阻塞的IO变为非阻塞的状态。
  • 设置套接字为非阻塞IO

    sockfd.setblocking(bool)
    功能:设置套接字为非阻塞IO
    参数:默认为True,表示套接字IO阻塞;设置为False则套接字IO变为非阻塞
    
"""
非阻塞IO实例
"""

from socket import *

s = socket()
s.bind(("0.0.0.0", 8888))
s.listen(5)

# 设置为非阻塞
s.setblocking(False)

while True:
    try:
        c, addr = s.accept()
        print("Connect from ", addr)
    except BlockingIOError as e:
        print("干点别的")
    else:
        data = c.recv(1024)
        print(data.decode())
  • 超时检测 :设置一个最长阻塞时间,超过该时间后则不再阻塞等待。

    sockfd.settimeout(sec)
    功能:设置套接字的超时时间
    参数:设置的时间
    
 """
非阻塞IO实例2
"""

from socket import *
from time import sleep,ctime

log = open("my.log",'a') # 打开一个日志

s = socket()
s.bind(("0.0.0.0",8888))
s.listen(5)

# 设置为非阻塞
# s.setblocking(False)

# 设置超时检测
s.settimeout(3)

while True:
    try:
        c,addr = s.accept()
        print("Connect from ",addr)
    except timeout as e:
        print("发生超时")
        msg = "%s : %s\n" % (ctime(), e)
        log.write(msg)
        log.flush()
    except BlockingIOError as e:
        print("非阻塞日志")
        sleep(2)
        msg = "%s : %s\n"%(ctime(),e)
        log.write(msg)
        log.flush()
    else:
        data = c.recv(1024)
        print(data.decode())
3.2.4 IO多路复用
  • 定义

    同时监控多个IO事件,当哪个IO事件准备就绪就执行哪个IO事件。以此形成可以同时处理多个IO的行为,避免一个IO阻塞造成其他IO均无法执行,提高了IO执行效率。

  • 具体方案

    • select方法 : windows linux unix   监控IO数量1024 效率一般
    • poll方法: linux unix         监控IO数量无限制  效率一般
    • epoll方法: linux           监控IO数量无限制 优秀
  • select 方法

rs, ws, xs=select(rlist, wlist, xlist[, timeout])
功能: 监控IO事件,阻塞等待IO发生
参数:rlist  列表  读IO列表,添加等待发生的或者可读的IO事件
      wlist  列表  写IO列表,存放要可以主动处理的或者可写的IO事件
      xlist  列表 异常IO列表,存放出现异常要处理的IO事件
      timeout  超时时间

返回值: rs 列表  rlist中准备就绪的IO
        ws 列表  wlist中准备就绪的IO
	   xs 列表  xlist中准备就绪的IO
"""
select IO 多路复用方法
"""
from socket import *
from select import select

# 制作一些IO对象
file = open("my.log",'a+')

sock_tcp = socket()
sock_tcp.bind(("0.0.0.0",8888))
sock_tcp.listen(5)

sock_udp = socket(AF_INET,SOCK_DGRAM)

# 监控IO
print("开始监控IO")
rs,ws,xs = select([],[sock_udp,file],[])
print("rlist:",rs)
print("wlist:",ws)
print("xlist:",xs)

"""
基于select的 IO网络并发模型

IO 多路复用与非阻塞搭配

重点代码 !!!
"""
from socket import *
from select import select

# 网络地址
HOST = "0.0.0.0"
PORT = 8888
ADDR = (HOST,PORT)

# 创建监听套接字
sockfd = socket()
sockfd.bind(ADDR)
sockfd.listen(5)

# 设置非阻塞
sockfd.setblocking(False)

# 初始只有监听套接字,先关注他
rlist = [sockfd]  # 客户端连接
wlist = []
xlist = []

# 循环监控IO对象
while True:
    rs,ws,xs = select(rlist,wlist,xlist)
    # 处理就绪的IO
    for r in rs:
        # 有客户端连接
        if r is sockfd:
            connfd, addr = r.accept()
            print("Connect from", addr)
            # 将客户端连接套接字也监控起来
            connfd.setblocking(False)
            rlist.append(connfd)
        else:
            # 某个客户端发消息 connfd 就绪
            data = r.recv(1024).decode()
            # 客户端退出处理
            if not data:
                rlist.remove(r)
                r.close()
                continue
            print(data)
            # r.send(b"OK")
            wlist.append(r) # 存入写列表

    for w in ws:
        w.send(b"OK")
        wlist.remove(w) # 写完要移除要不一直写

    for x in xs:
        pass
  • poll方法
p = select.poll()
功能 : 创建poll对象
返回值: poll对象
p.register(fd,event)   
功能: 注册关注的IO事件
参数:fd  要关注的IO
      event  要关注的IO事件类型
  	     常用类型:POLLIN  读IO事件(rlist)
		      POLLOUT 写IO事件 (wlist)
		      POLLERR 异常IO  (xlist)
		      POLLHUP 断开连接 
		  e.g. p.register(sockfd,POLLIN|POLLERR)

p.unregister(fd)
功能:取消对IO的关注
参数:IO对象或者IO对象的fileno(文件描述符)
events = p.poll()
功能: 阻塞等待监控的IO事件发生
返回值: 返回发生的IO
        events格式  [(fileno,event),()....]
        每个元组为一个就绪IO,元组第一项是该IO的fileno,第二项为该IO就绪的事件类型
"""
poll IO 多路复用方法
"""
from socket import *
from select import *

# 制作一些IO对象
file = open("my.log", 'a+')

sock_tcp = socket()
sock_tcp.bind(("0.0.0.0", 8888))
sock_tcp.listen(5)

sock_udp = socket(AF_INET, SOCK_DGRAM)

#  查找字典 需要与register的IO保持一直
map = {sock_tcp.fileno(): sock_tcp,
       sock_udp.fileno(): sock_udp,
       file.fileno(): file}

# 准备poll方法
p = poll()  # 生成poll对象
p.register(sock_tcp, POLLIN | POLLERR)
p.register(sock_udp, POLLOUT)
p.register(file, POLLOUT)

print("开始监控IO")
events = p.poll()

# events --> [(fileno,event),()]
# 必须获取到IO对象才能调用方法处理IO
print(events)

"""
基于poll的 IO网络并发模型

IO 多路复用与非阻塞搭配
"""
from socket import *
from select import *

# 网络地址
HOST = "0.0.0.0"
PORT = 8888
ADDR = (HOST, PORT)

# 创建监听套接字
sockfd = socket()
sockfd.bind(ADDR)
sockfd.listen(5)

# 设置非阻塞
sockfd.setblocking(False)

# 创建poll对象
p = poll()

# 查找字典  fileno--> io object
map = {sockfd.fileno(): sockfd}

# 初始监听套接字,先关注他
p.register(sockfd, POLLIN)

# 循环监控IO对象
while True:
    # events --> [(fileno,event),()]
    events = p.poll()
    # 遍历events 处理就绪的IO
    for fd, event in events:
        # 分类讨论
        if fd == sockfd.fileno():
            connfd, addr = map[fd].accept()
            print("Connect from", addr)
            # 将客户端连接套接字也监控起来
            connfd.setblocking(False)
            p.register(connfd, POLLIN | POLLERR)
            map[connfd.fileno()] = connfd  # 维护字典
        elif event == POLLIN:
            # 某个客户端发消息 connfd 就绪
            data = map[fd].recv(1024).decode()
            # 客户端退出处理
            if not data:
                p.unregister(fd)  # 不再关注
                map[fd].close()
                del map[fd]  # 从字典删除
                continue
            print(data)
            # map[fd].send(b"OK")
            p.register(fd, POLLOUT)
        elif event == POLLOUT:
            map[fd].send(b"OK")
            p.register(fd, POLLIN)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HWD9fnT7-1609849651600)(/home/tarena/month02/lz/day16/select_poll.png)]

  • epoll方法

    使用方法 : 基本与poll相同

    生成对象改为 epoll()

    将所有事件类型改为EPOLL类型

    • epoll特点
      • epoll 效率比select 、poll要高
      • epoll 监控IO数量比select要多
      • epoll 的触发方式比poll要多 (EPOLLET边缘触发)
"""
基于epoll的 IO网络并发模型

IO 多路复用与非阻塞搭配

重点代码 !!!
"""
from socket import *
from select import *

# 网络地址
HOST = "0.0.0.0"
PORT = 8888
ADDR = (HOST, PORT)

# 创建监听套接字
sockfd = socket()
sockfd.bind(ADDR)
sockfd.listen(5)

# 设置非阻塞
sockfd.setblocking(False)

# 创建epoll对象
ep = epoll()

# 查找字典  fileno--> io object
map = {sockfd.fileno(): sockfd}

# 初始监听套接字,先关注他
ep.register(sockfd, EPOLLIN)

# 循环监控IO对象
while True:
    # events --> [(fileno,event),()]
    events = ep.poll()
    print("你有新的IO需要处理哦", events)
    # 遍历events 处理就绪的IO
    for fd, event in events:
        # 分类讨论
        if fd == sockfd.fileno():
            connfd, addr = map[fd].accept()
            print("Connect from", addr)
            # 将客户端连接套接字也监控起来
            connfd.setblocking(False)
            ep.register(connfd, EPOLLIN | EPOLLET)  # 设置边缘触发
            map[connfd.fileno()] = connfd  # 维护字典
        elif event == EPOLLIN:
            # 某个客户端发消息 connfd 就绪
            data = map[fd].recv(1024).decode()
            # 客户端退出处理
            if not data:
                ep.unregister(fd)  # 不再关注
                map[fd].close()
                del map[fd]  # 从字典删除
                continue
            print(data)
            # map[fd].send(b"OK")
            ep.unregister(fd)
            ep.register(fd, EPOLLOUT)
        elif event == EPOLLOUT:
            map[fd].send(b"OK")
            ep.unregister(fd)
            ep.register(fd, EPOLLIN)
3.2.5 IO并发模型

利用IO多路复用等技术,同时处理多个客户端IO请求。

  • 优点 : 资源消耗少,能同时高效处理多个IO行为

  • 缺点 : 只针对处理并发产生的IO事件

  • 适用情况:HTTP请求,网络传输等都是IO行为,可以通过IO多路复用监控多个客户端的IO请求。

  • 并发服务实现过程

    【1】将关注的IO准备好,通常设置为非阻塞状态。

    【2】通过IO多路复用方法提交,进行IO监控。

    【3】阻塞等待,当监控的IO有发生时,结束阻塞。

    【4】遍历返回值列表,确定就绪IO事件。

    【5】处理发生的IO事件。

    【6】继续循环监控IO发生。

4. web服务

4.1 HTTP协议

4.1.1 协议概述
  • 用途 : 网页获取,数据的传输
  • 特点
    • 应用层协议,使用tcp进行数据传输
    • 简单,灵活,很多语言都有HTTP专门接口
    • 无状态,数据传输过程中不记录传输内容
    • 有丰富了请求类型
    • 可以传输的数据类型众多
4.1.2 网页访问流程
  1. 客户端(浏览器)通过tcp传输,发送http请求给服务端
  2. 服务端接收到http请求后进行解析
  3. 服务端处理请求内容,组织响应内容
  4. 服务端将响应内容以http响应格式发送给浏览器
  5. 浏览器接收到响应内容,解析展示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9A6Uiy9J-1609849651601)(/home/tarena/month02/lz/Network/img/2_wzfw.png)]

4.1.2 HTTP请求
  • 请求行 : 具体的请求类别和请求内容
	GET                      /                 HTTP/1.1
请求类别        请求内容          协议版本

请求类别:每个请求类别表示要做不同的事情

		GET : 获取网络资源
		POST :提交一定的信息,得到反馈
		HEAD : 只获取网络资源的响应头
		PUT : 更新服务器资源
		DELETE : 删除服务器资源
  • 请求头:对请求的进一步解释和描述
Accept-Encoding: gzip
  • 空行(必须要有)
  • 请求体: 请求参数或者提交内容

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NisHFrrI-1609849651602)(./img/request.jpg)]

4.1.3 HTTP响应
  • 响应行 : 反馈基本的响应情况
HTTP/1.1      200            OK
版本信息     响应码   附加信息

​ 响应码 :

1xx  提示信息,表示请求被接收
2xx  响应成功
3xx  响应需要进一步操作,重定向
4xx  客户端错误
5xx  服务器错误
  • 响应头:对响应内容的描述
Content-Type: text/html
Content-Length:109\r\n
  • 空行
  • 响应体:响应的主体内容信息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wnyPaZjr-1609849651603)(/home/tarena/month02/lz/Network/img/response.png)]

"""
http 请求响应示例
"""
from socket import *

s = socket()
s.bind(("0.0.0.0",8888))
s.listen(5)

c,addr = s.accept() # 浏览器连接
print("Connect from",addr)

# 接收浏览器发送的HTTP请求
data = c.recv(1024 * 10)
print(data.decode())

# 发送http响应给浏览器
response = "HTTP/1.1 404 Not Found\r\n"
response += "Content-Type:text/html\r\n"
response += "\r\n"
response += "Sorry...."

c.send(response.encode())

c.close()
s.close()


"""
练习: 编写一个程序完成,如果浏览器访问
127.0.0.1:8888/python的时候可以访问到
Python.html网页,否则则访问不到任何内容,得到404
响应

提示 : 提取请求内容 --》 分情况讨论
       读取网页内容  作为响应体发送
"""
from socket import *

s = socket()
s.bind(("0.0.0.0", 8880))
s.listen(5)

c, addr = s.accept()  # 浏览器连接
print("Connect from", addr)

# 接收浏览器发送的HTTP请求
data = c.recv(1024 * 10).decode()
tmp = data.split(" ")
# 判断请求内容
if tmp[1] == "/python":
    with open("Python.html") as f:
        content = f.read()
    response = "HTTP/1.1 200 OK\r\n"
    response += "Content-Type:text/html\r\n"
    response += "\r\n"
    response += content
else:
    response = "HTTP/1.1 404 Not Found\r\n"
    response += "Content-Type:text/html\r\n"
    response += "\r\n"
    response += "Sorry..."

# 发送响应
c.send(response.encode())

c.close()
s.close()

4.2 web 服务程序实现

  1. 主要功能 :
【1】 接收客户端(浏览器)请求

【2】 解析客户端发送的请求

【3】 根据请求组织数据内容

【4】 将数据内容形成http响应格式返回给浏览器
  1. 特点 :

    【1】 采用IO并发,可以满足多个客户端同时发起请求情况

    【2】 通过类接口形式进行功能封装

    【3】 做基本的请求解析,根据具体请求返回具体内容,同时可以满足客户端的网页效果加载

"""
web server 程序

完成一个类,提供给使用者
可以通过这个类快速搭建服务
完成网页展示
"""
from socket import *
from select import select
import re


# 封装所有web后端功能
class WebServer:
    def __init__(self, host="0.0.0.0", port=80, html=None):
        self.host = host
        self.port = port
        self.html = html
        self.rlist = []
        self.wlist = []
        self.xlist = []
        self.create_socket()
        self.bind()

    # 创建设置套接字
    def create_socket(self):
        self.sock = socket()
        self.sock.setblocking(False)

    # 绑定地址
    def bind(self):
        self.address = (self.host, self.port)
        self.sock.bind(self.address)

    # 启动整个服务
    def start(self):
        self.sock.listen(5)
        print("Listen the port %d" % self.port)
        # 先监控监听套接字
        self.rlist.append(self.sock)
        # 循环监控IO对象
        while True:
            rs, ws, xs = select(self.rlist, self.wlist, self.xlist)
            # 处理就绪的IO
            for r in rs:
                # 有客户端连接
                if r is self.sock:
                    connfd, addr = r.accept()
                    print("Connect from", addr)
                    # 将客户端连接套接字也监控起来
                    connfd.setblocking(False)
                    self.rlist.append(connfd)
                else:
                    # 处理浏览器端发的请求
                    try:
                        self.handle(r)
                    except:
                        pass
                    self.rlist.remove(r)
                    r.close()

    # 处理客户端请求
    def handle(self, connfd):
        # http请求
        request = connfd.recv(1024 * 10).decode()
        # 使用正则匹配请求内容
        pattern = r"[A-Z]+\s+(?P<info>/\S*)"
        result = re.match(pattern, request)
        if request:
            # 提取请求内容
            info = result.group("info")
            print("请求内容:", info)
            self.send_html(connfd, info)

    # 发送响应
    def send_html(self, connfd, info):
        # 对info分情况
        if info == "/":
            filename = self.html + "/index.html"
        else:
            filename = self.html + info

        # 打开判断文件是否存在
        try:
            file = open(filename, "rb")
        except:
            #  请求的网页不存在
            response = "HTTP/1.1 404 Not Found\r\n"
            response += "Content-Type:text/html\r\n"
            response += "\r\n"
            with open(self.html + "/404.html") as file:
                response += file.read()
            response = response.encode()
        else:
            # 请求的网页存在
            data = file.read()  # 字节串
            response = "HTTP/1.1 200 OK\r\n"
            response += "Content-Type:text/html\r\n"
            response += "Content-Length:%d\r\n" % len(data)
            response += "\r\n"
            response = response.encode() + data
            file.close()
        finally:
            connfd.send(response)


if __name__ == '__main__':
    # 需要用户决定: 地址  网页
    httpd = WebServer(host="0.0.0.0",
                      port=8000,
                      html="./static")
    # 启动服务
    httpd.start()

并发技术探讨(扩展)

高并发问题

  • 衡量高并发的关键指标

    • 响应时间(Response Time) : 接收请求后处理的时间

    • 吞吐量(Throughput): 响应时间+QPS+同时在线用户数量

    • 每秒查询率QPS(Query Per Second): 每秒接收请求的次数

    • 每秒事务处理量TPS(Transaction Per Second):每秒处理请求的次数(包含接收,处理,响应)

    • 同时在线用户数量:同时连接服务器的用户的数量

  • 多大的并发量算是高并发

    • 没有最高,只要更高

      比如在一个小公司可能QPS2000+就不错了,在一个需要频繁访问的门户网站可能要达到QPS5W+

    • C10K问题

      早先服务器都是单纯基于进程/线程模型的,新到来一个TCP连接,就需要分配1个进程(或者线程)。而进程占用操作系统资源多,一台机器无法创建很多进程。如果是C10K就要创建1万个进程,那么单机而言操作系统是无法承受的,这就是著名的C10k问题。创建的进程线程多了,数据拷贝频繁, 进程/线程切换消耗大, 导致操作系统崩溃,这就是C10K问题的本质!

更高并发的实现

​ 为了解决C10K问题,现在高并发的实现已经是一个更加综合的架构艺术。涉及到进程线程编程,IO处理,数据库处理,缓存,队列,负载均衡等等,这些我们在后面的阶段还会学习。此外还有硬件的设计,服务器集群的部署,服务器负载,网络流量的处理等。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KkQ6rz9m-1609849651604)(./img/25.jpg)]

实际工作中,应对更庞大的任务场景,网络并发模型的使用有时也并不单一。比如多进程网络并发中每个进程再开辟线程,或者在每个进程中也可以使用多路复用的IO处理方法。

def __init__(self, host="0.0.0.0", port=80, html=None):
    self.host = host
    self.port = port
    self.html = html
    self.rlist = []
    self.wlist = []
    self.xlist = []
    self.create_socket()
    self.bind()

# 创建设置套接字
def create_socket(self):
    self.sock = socket()
    self.sock.setblocking(False)

# 绑定地址
def bind(self):
    self.address = (self.host, self.port)
    self.sock.bind(self.address)

# 启动整个服务
def start(self):
    self.sock.listen(5)
    print("Listen the port %d" % self.port)
    # 先监控监听套接字
    self.rlist.append(self.sock)
    # 循环监控IO对象
    while True:
        rs, ws, xs = select(self.rlist, self.wlist, self.xlist)
        # 处理就绪的IO
        for r in rs:
            # 有客户端连接
            if r is self.sock:
                connfd, addr = r.accept()
                print("Connect from", addr)
                # 将客户端连接套接字也监控起来
                connfd.setblocking(False)
                self.rlist.append(connfd)
            else:
                # 处理浏览器端发的请求
                try:
                    self.handle(r)
                except:
                    pass
                self.rlist.remove(r)
                r.close()

# 处理客户端请求
def handle(self, connfd):
    # http请求
    request = connfd.recv(1024 * 10).decode()
    # 使用正则匹配请求内容
    pattern = r"[A-Z]+\s+(?P<info>/\S*)"
    result = re.match(pattern, request)
    if request:
        # 提取请求内容
        info = result.group("info")
        print("请求内容:", info)
        self.send_html(connfd, info)

# 发送响应
def send_html(self, connfd, info):
    # 对info分情况
    if info == "/":
        filename = self.html + "/index.html"
    else:
        filename = self.html + info

    # 打开判断文件是否存在
    try:
        file = open(filename, "rb")
    except:
        #  请求的网页不存在
        response = "HTTP/1.1 404 Not Found\r\n"
        response += "Content-Type:text/html\r\n"
        response += "\r\n"
        with open(self.html + "/404.html") as file:
            response += file.read()
        response = response.encode()
    else:
        # 请求的网页存在
        data = file.read()  # 字节串
        response = "HTTP/1.1 200 OK\r\n"
        response += "Content-Type:text/html\r\n"
        response += "Content-Length:%d\r\n" % len(data)
        response += "\r\n"
        response = response.encode() + data
        file.close()
    finally:
        connfd.send(response)

if name == ‘main’:
# 需要用户决定: 地址 网页
httpd = WebServer(host=“0.0.0.0”,
port=8000,
html="./static")
# 启动服务
httpd.start()




## 并发技术探讨(扩展)

### 高并发问题

* 衡量高并发的关键指标

  - 响应时间(Response Time) : 接收请求后处理的时间

  - 吞吐量(Throughput): 响应时间+QPS+同时在线用户数量

  - 每秒查询率QPS(Query Per Second): 每秒接收请求的次数

  - 每秒事务处理量TPS(Transaction Per Second):每秒处理请求的次数(包含接收,处理,响应)

  - 同时在线用户数量:同时连接服务器的用户的数量

* 多大的并发量算是高并发

  * 没有最高,只要更高

    比如在一个小公司可能QPS2000+就不错了,在一个需要频繁访问的门户网站可能要达到QPS5W+

  * C10K问题

    早先服务器都是单纯基于进程/线程模型的,新到来一个TCP连接,就需要分配1个进程(或者线程)。而进程占用操作系统资源多,一台机器无法创建很多进程。如果是C10K就要创建1万个进程,那么单机而言操作系统是无法承受的,这就是著名的C10k问题。创建的进程线程多了,数据拷贝频繁, 进程/线程切换消耗大, 导致操作系统崩溃,这就是C10K问题的本质!

### 更高并发的实现

​		为了解决C10K问题,现在高并发的实现已经是一个更加综合的架构艺术。涉及到进程线程编程,IO处理,数据库处理,缓存,队列,负载均衡等等,这些我们在后面的阶段还会学习。此外还有硬件的设计,服务器集群的部署,服务器负载,网络流量的处理等。

[外链图片转存中...(img-KkQ6rz9m-1609849651604)]



实际工作中,应对更庞大的任务场景,网络并发模型的使用有时也并不单一。比如多进程网络并发中每个进程再开辟线程,或者在每个进程中也可以使用多路复用的IO处理方法。

​	
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值