项目实战 - 飞机大战


游戏的坐标系

  • 原点 在 左上角 (0, 0)

    • x 轴 水平方向向 右,逐渐增加
    • y 轴 垂直方向向 下,逐渐增加
    游戏的坐标系
  • pygame.Rect 是一个比较特殊的类,内部只是封装了一些数字计算,用于描述 矩形区域 Rect(x, y, width, height) -> Rect

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import pygame

    # pygame一定要做初始化的操作
    pygame.init()

    hero_rect = pygame.Rect(100, 500, 120, 125)
    print("英雄的原点 %d %d" % (hero_rect.x, hero_rect.y))
    print("英雄的尺寸 %d %d" % (hero_rect.width, hero_rect.height))
    # size 属性会返回矩形区域的 (宽, 高) 元组
    print("%d %d" % hero_rect.size)

    pygame.quit()

创建游戏主窗口

  • pygame 专门提供了一个 模块 pygame.display 用于创建、管理 游戏窗口

    方法 说明
    pygame.display.set_mode() 初始化游戏显示窗口
    pygame.display.update() 刷新屏幕内容显示,稍后使用
1
2
3
4
5
6
7
8
9
10
11
12
13
import time

import pygame

pygame.init()

# 创建游戏的窗口 480 * 700
screen = pygame.display.set_mode((480, 700))

while True:
time.sleep(1)

pygame.quit()

实现图像绘制

  • 图像文件 初始是保存在磁盘上的,如果需要使用,第一步 就需要 被加载到内存

  • 要在屏幕上 看到某一个图像的内容,需要按照三个步骤:

    • 使用 pygame.image.load() 加载图像的数据

    • 使用 游戏屏幕 对象,调用 blit 方法 将图像绘制到指定位置

    • 调用 pygame.display.update() 方法更新整个屏幕的显示

    流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 显示屏幕背景图

import pygame

pygame.init()

# 创建游戏的窗口 480 * 700
screen = pygame.display.set_mode((480, 700))

# 绘制背景图像
# 1> 加载图像数据
bg = pygame.image.load("./images/background.png")
# 2> blit 绘制图像
screen.blit(bg, (0, 0))
# 3> update 更新屏幕显示
pygame.display.update()

while True:
# 这里一般是监听鼠标和键盘的操作
pass

pygame.quit()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 绘制英雄图像

import pygame

pygame.init()

# 创建游戏的窗口 480 * 700
screen = pygame.display.set_mode((480, 700))

# 1> 加载图像
hero = pygame.image.load("./images/me1.png")
# 2> 绘制在屏幕
screen.blit(hero, (200, 500))
# 3> 更新显示
pygame.display.update()

pygame.quit()
  • 上面两个代码可以合并

游戏循环 和 游戏时钟

游戏的组成

游戏循环的作用:

  • 保证游戏 不会直接退出
  • 变化图像位置 —— 动画效果
    • 每隔 1 / 60 秒 移动一下所有图像的位置
    • 调用 pygame.display.update() 更新屏幕显示
  • 检测用户交互 —— 按键、鼠标等…

游戏时钟

  • pygame 专门提供了一个类 pygame.time.Clock 可以非常方便的设置屏幕绘制速度 —— 刷新帧率

  • 要使用 时钟对象 需要两步:

    • 在 游戏初始化 创建一个 时钟对象
    • 在 游戏循环 中让时钟对象调用 tick(帧率) 方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import pygame

    # 3. 创建游戏时钟对象
    clock = pygame.time.Clock()
    i = 0
    # 游戏循环
    while True:
    # 设置屏幕刷新帧率,每秒 60 次
    clock.tick(60)
    print(i)
    i += 1 # 这里就1s钟打印60个数字

英雄的简单动画实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import pygame

# 游戏的初始化
pygame.init()
# 创建游戏的窗口 480 * 700
screen = pygame.display.set_mode((480, 700))
# 绘制背景图像
bg = pygame.image.load("./images/background.png")
screen.blit(bg, (0, 0))
# pygame.display.update()
# 绘制英雄的飞机
hero = pygame.image.load("./images/me1.png")
screen.blit(hero, (150, 300))
# 可以在所有绘制工作完成之后,统一调用 update 方法
pygame.display.update()
# 创建时钟对象
clock = pygame.time.Clock()
# 1. 定义 rect 记录飞机的初始位置
hero_rect = pygame.Rect(150, 300, 102, 126)
# 游戏循环 -> 意味着游戏的正式开始!
while True:
# 可以指定循环体内部的代码执行的频率
clock.tick(60)
# 2. 修改飞机的位置
hero_rect.y -= 1
if hero_rect.y < - hero_rect.height:
hero_rect.y = 700
# 3. 调用 blit 方法绘制图像
screen.blit(bg, (0, 0))
screen.blit(hero, hero_rect)
# 4. 调用 update 方法更新显示
pygame.display.update()
pygame.quit()
# 如果飞机要循环飞行
# 4. 定义英雄的初始位置
hero_rect = pygame.Rect(150, 500, 102, 126)

while True:
# 可以指定循环体内部的代码执行的频率
clock.tick(60)
# 更新英雄位置
hero_rect.y -= 1
# 如果移出屏幕,则将英雄的顶部移动到屏幕底部
if hero_rect.y <= 0:
hero_rect.y = 700
# 绘制背景图片
screen.blit(bg, (0, 0))
# 绘制英雄图像
screen.blit(hero, hero_rect)
# 更新显示
pygame.display.update()


在游戏循环中 监听 事件

  • pygame 中通过 pygame.event.get() 可以获得 用户当前所做动作 的 事件列表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import pygame

# 游戏的初始化
pygame.init()

# 创建游戏的窗口 480 * 700
screen = pygame.display.set_mode((480, 700))
# 绘制背景图像
bg = pygame.image.load("./images/background.png")
screen.blit(bg, (0, 0))
# pygame.display.update()
# 绘制英雄的飞机
hero = pygame.image.load("./images/me1.png")
screen.blit(hero, (150, 300))
# 可以在所有绘制工作完成之后,统一调用 update 方法
pygame.display.update()
# 创建时钟对象
clock = pygame.time.Clock()

# 1. 定义 rect 记录飞机的初始位置
hero_rect = pygame.Rect(150, 300, 102, 126)
# 游戏循环 -> 意味着游戏的正式开始!
while True:
# 可以指定循环体内部的代码执行的频率
clock.tick(60)
# 捕获事件
event_list = pygame.event.get()
if len(event_list) > 0:
print(event_list)

# 针对退出,我们可以在 while 循环中增加如下代码:
# 就是可以按 x 来退出游戏
for event in event_list:
# 判断事件类型是否是退出事件
if event.type == pygame.QUIT:
print("游戏退出...")
# quit 卸载所有的模块
pygame.quit()
# exit() 直接终止当前正在执行的程序
exit()

# 2. 修改飞机的位置
hero_rect.y -= 1
# 判断飞机的位置
if hero_rect.y <= 0:
hero_rect.y = 700
# 3. 调用 blit 方法绘制图像
screen.blit(bg, (0, 0))
screen.blit(hero, hero_rect)
# 4. 调用 update 方法更新显示
pygame.display.update()

pygame.quit()

精灵 和 精灵组

  • 图像加载、位置变化、绘制图像,这些都需要分别处理
  • 为了简化开发步骤,pygame 提供了两个类:
    • pygame.sprite.Sprite —— 存储 图像数据 image 和 位置 rect 的对象
    • pygame.sprite.Group

精灵与精灵组


项目解析

背景图的原理

精灵组及精灵 部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
import random
import pygame

# 屏幕大小的常量对象
SCREEN_RECT = pygame.Rect(0, 0, 480, 700)
# 刷新的帧率
FRAME_PER_SEC = 30
# 创建敌机的定时器常量,为事件定义不同名字的常量,从而能够区分,从24算起
CREATE_ENEMY_EVENT = pygame.USEREVENT
# 英雄发射子弹事件,为事件定义不同名字的常量,从而能够区分
HERO_FIRE_EVENT = pygame.USEREVENT + 1


class GameSprite(pygame.sprite.Sprite):
"""飞机大战游戏精灵"""

def __init__(self, image_name, speed=1):
# 调用父类的初始化方法
super().__init__()

# 定义对象的属性
self.image = pygame.image.load(image_name) # 上传图像
self.rect = self.image.get_rect() # 获取图像的尺寸
self.speed = speed # 初始速度

def update(self):
# 在屏幕的垂直方向上移动
self.rect.y += self.speed


class Background(GameSprite):
"""游戏背景精灵"""

def __init__(self, is_alt=False):

# 1. 调用父类方法实现精灵的创建(image/rect/speed)
super().__init__("./images/background.png")

# 2. 判断是否是交替图像,如果是,需要设置初始位置
if is_alt:
self.rect.y = -self.rect.height

def update(self):

# 1. 调用父类的方法实现
super().update()

# 2. 判断是否移出屏幕,如果移出屏幕,将图像设置到屏幕的上方
if self.rect.y >= SCREEN_RECT.height:
self.rect.y = -self.rect.height


class Enemy(GameSprite):
"""敌机精灵"""

def __init__(self):
# 1. 调用父类方法,创建敌机精灵,同时指定敌机图片
super().__init__("./images/enemy1.png")

# 2. 指定敌机的初始随机速度 1 ~ 3
self.speed = random.randint(1, 3)

# 3. 指定敌机的初始随机位置
self.rect.bottom = 0

max_x = SCREEN_RECT.width - self.rect.width # 屏幕的宽度减去自身宽度才是敌机的长度
self.rect.x = random.randint(0, max_x)

def update(self):
# 1. 调用父类方法,保持垂直方向的飞行
super().update()

# 2. 判断是否飞出屏幕,如果是,需要从精灵组删除敌机
if self.rect.y >= SCREEN_RECT.height:
# print("飞出屏幕,需要从精灵组删除...")
# kill方法可以将精灵从所有精灵组中移出,精灵就会被自动销毁
self.kill()

def __del__(self):
# print("敌机挂了 %s" % self.rect)
pass


class Hero(GameSprite):
"""英雄精灵"""

def __init__(self):

# 1. 调用父类方法,设置image&speed
super().__init__("./images/me1.png", 0)

# 2. 设置英雄的初始位置
self.rect.centerx = SCREEN_RECT.centerx
self.rect.bottom = SCREEN_RECT.bottom - 120

# 3. 创建子弹的精灵组
self.bullets = pygame.sprite.Group()

def update(self):

# 英雄在水平方向移动
self.rect.x += self.speed

# 控制英雄不能离开屏幕
if self.rect.x < 0:
self.rect.x = 0
elif self.rect.right > SCREEN_RECT.right:
self.rect.right = SCREEN_RECT.right

def fire(self):
print("发射子弹...")

for i in (0, 1, 2):
# 1. 创建子弹精灵
bullet = Bullet()

# 2. 设置精灵的位置
bullet.rect.bottom = self.rect.y - i * 20
bullet.rect.centerx = self.rect.centerx

# 3. 将精灵添加到精灵组
self.bullets.add(bullet)


class Bullet(GameSprite):
"""子弹精灵"""

def __init__(self):
# 调用父类方法,设置子弹图片,设置初始速度
super().__init__("./images/bullet1.png", -5)

def update(self):
# 调用父类方法,让子弹沿垂直方向飞行
super().update()

# 判断子弹是否飞出屏幕
if self.rect.bottom < 0:
self.kill()

def __del__(self):
print("子弹被销毁...")

游戏实际代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import pygame
from plane_sprites import *
import time

class PlaneGame(object):
"""飞机大战主游戏"""

def __init__(self):
print("游戏初始化")

# 1. 创建游戏的窗口
self.screen = pygame.display.set_mode(SCREEN_RECT.size)
# 2. 创建游戏的时钟
self.clock = pygame.time.Clock()
# 3. 调用私有方法,精灵和精灵组的创建,也是初始化
self.__create_sprites()

# 4. 设置定时器事件 - 创建敌机 设定敌机的刷新时间为1s,
# 英雄子弹事件的刷新频率为0.5秒
pygame.time.set_timer(CREATE_ENEMY_EVENT, 1000)
pygame.time.set_timer(HERO_FIRE_EVENT, 300)

def __create_sprites(self):

# 创建背景精灵和精灵组
bg1 = Background()
bg2 = Background(True)

self.back_group = pygame.sprite.Group(bg1, bg2)

# 创建敌机的精灵组
self.enemy_group = pygame.sprite.Group()

# 创建英雄的精灵和精灵组
self.hero = Hero()
self.hero_group = pygame.sprite.Group(self.hero)

def start_game(self):
print("游戏开始...")

while True:
# 1. 设置刷新帧率
self.clock.tick(FRAME_PER_SEC)
# 2. 事件监听
self.__event_handler()
# 3. 碰撞检测
self.__check_collide()
# 4. 更新/绘制精灵组
self.__update_sprites()
# 5. 更新显示
pygame.display.update()

def __event_handler(self):

for event in pygame.event.get():

# 判断是否退出游戏
if event.type == pygame.QUIT:
PlaneGame.__game_over()

elif event.type == CREATE_ENEMY_EVENT:
# print("敌机出场...")
# 创建敌机精灵
enemy = Enemy()

# 将敌机精灵添加到敌机精灵组
self.enemy_group.add(enemy)
elif event.type == HERO_FIRE_EVENT:
self.hero.fire()
# elif event.type == pygame.KEYDOWN and event.key == pygame.K_RIGHT:
# print("向右移动...")

# 使用键盘提供的方法获取键盘按键 - 按键元组
keys_pressed = pygame.key.get_pressed()
# 判断元组中对应的按键索引值 1
if keys_pressed[pygame.K_RIGHT]:
self.hero.speed = 8
elif keys_pressed[pygame.K_LEFT]:
self.hero.speed = -8
else:
self.hero.speed = 0

def __check_collide(self):

# 1. 子弹摧毁敌机
# 两个精灵组 中 所有的精灵 的碰撞检测
# pygame自带方法 sprite.groupcollide
pygame.sprite.groupcollide(self.hero.bullets, self.enemy_group, True, True)

# 2. 敌机撞毁英雄
# 判断 某个精灵 和 指定精灵组 中的精灵的碰撞
enemies = pygame.sprite.spritecollide(self.hero, self.enemy_group, True)

# 判断列表时候有内容
if len(enemies) > 0:
# 让英雄牺牲
self.hero.kill()
m = "./sound/use_bomb.wav"
pygame.mixer.music.load(m)
pygame.mixer.music.play()
time.sleep(2)
# 结束游戏
PlaneGame.__game_over()


# 更新组到画布上
def __update_sprites(self):

self.back_group.update()
self.back_group.draw(self.screen)

self.enemy_group.update()
self.enemy_group.draw(self.screen)

self.hero_group.update()
self.hero_group.draw(self.screen)

self.hero.bullets.update()
self.hero.bullets.draw(self.screen)

@staticmethod
def __game_over():
print("游戏结束")

pygame.quit()
exit() #进程结束


if __name__ == '__main__':
# 创建游戏对象
pygame.init()
game = PlaneGame()

# 启动游戏
game.start_game()

UDP 与 TCP 通信

路由查看

1
2
# route 可以查看任何路由
route -n

内核IP路由表

目标 网关 子网掩码
0.0.0.0 192.168.19.2 0.0.0.0
169.254.0.0 0.0.0.0 255.255.0.0
192.168.19.0 0.0.0.0 255.255.255.0
  • 0.0.0.0 代表任意目的地
  • 网关就是转发数据的设备

端口号的范围:端口号只有整数,范围是 0 到 65535

知名端口:

  • 22 端口分配给 ssh
  • 80 端口分配给HTTP

查看端口状态:

1
2
3
netstat -an | grep tcp  # 查看本地tcp端口情况

sudo losf -i tcp:22 # 查看tcp的22号端口被哪个进程使用了

socket

  • 创建 socket
    • 函数 socket.socket 创建一个 socket,该函数带有两个参数:
      • Address Family:可以选择 AF_INET(用于 Internet 进程间通信)或者AF_UNIX(用于同一台机器进程间通信),实际工作中常用 AF_INET
      • Type:套接字类型,可以是SOCK_STREAM(流式套接字,主要用于TCP协议)或者 SOCK_DGRAM(数据报套接字,主要用于 UDP 协议)
        • TCP 与 UDP 都是全双工通信
1
2
3
4
5
6
7
import socket

# 创建一个tcp socket(tcp 套接字)
s1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

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

UDP网络程序 - 发送与接收数据

UDP 通信流程图

  • UDP 服务端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    from socket import *

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

    # 2. 准备接收方的地址
    # 1> 目的ip的地址:192.168.88.129
    # 2> 目的端口:8080,这个要写1024以上端口
    dest_addr = ("100.65.20.204", 8080)

    # 失败直接抛弃异常
    udp_socket.bind(dest_addr)

    temp = udp_socket.recvfrom(100) # 100 代表接的长度
    print(temp[0]) # 对应的字节流
    # print(temp[0].decode("utf8"))
    print(temp[1]) # 对方的ip地址和端口号

    udp_socket.close()


    """
    接收到的数据:

    b'hello'
    ('100.65.20.204', 56881)
    """
  • UDP 客户端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    from socket import *

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

    dest_addr = ("100.65.20.204", 8080)

    # b"hello" 变成字节流
    udp_socket.sendto(b"hello", dest_addr)
    # udp_socket.sendto("你好".encode("utf8"), dest_addr)

    udp_socket.close()
  • 当UDP接收(recvfrom)的字节数少于发送的字节数

    • windows就会报错
    • Linux是不会报错,没有拿到的数据完全丢弃
  • sendto 和 recvfrom ,次数完全对等

云服务器注意事项:

  • 服务端 bind 的时候要绑定内网地址,就是ifconfig
  • 客户端发送的时候的 ip 地址是公网地址
  • 要开启端口

TCP

  1. 面向连接
  2. 可靠传输(超时重传)
  3. 基于字节流

TCP 与UDP的不同点

  • 面向连接(确认有创建三方交握,连接已创建才作为传输)
  • 有序数据传输
  • 重发丢失的数据包
  • 舍弃重复的数据包
  • 无差错的数据传输
  • 阻塞 / 流量控制

TCP 通信流程

  • TCP 服务端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    # tcp的服务端
    def tcp_server():
    # SOCK_STREAM代表的是tcp的socket
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    addr = ("192.168.1.107", 2000)
    s.bind(addr) # 绑定,端口没有激活
    s.listen(128) # listen时,端口才激活
    new_client, client_addr = s.accept()
    # print(client_addr) # ('192.168.1.107', 61371)

    # 接下来就可以进行send和recv操作
    new_client.send(b"I am server machine")
    data = new_client.recv(100)
    print(data)
    new_client.close()
    s.close()


    if __name__ == '__main__':
    tcp_server()
  • TCP 客户端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import socket


    def tcp_client():
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    addr = ("192.168.1.107", 2000)
    client.connect(addr)
    # 先接收
    data = client.recv(100)
    print(data)
    client.send(b"Hello World")
    client.close()

    if __name__ == '__main__':
    tcp_client()

文件的下载

  • 服务端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    import sys
    from socket import *


    def get_file_content(file_name):
    """获取文件内容"""
    try:
    with open(file_name, 'rb') as f:
    content = f.read()
    return content
    except:
    print("没有下载的文件 %s" % file_name)


    def main():
    if len(sys.argv) != 2:
    print("请按照如下方式进行:python3 xxx.py 7890")
    return
    else:
    # 运行方式为:python3 xxx.py 7890
    port = int(sys.argv[1])

    # 创建socket
    tcp_server_socket = socket(AF_INET, SOCK_STREAM)
    # 本地信息
    address = ("192.168.1.107", port)
    # 绑定本地信息
    tcp_server_socket.bind(address)
    # 将主动套接字变为被动套接字
    tcp_server_socket.listen(128)

    while True:
    # 等待客户端的连接,即为这个客户端发送文件
    client_socket, clientAddr = tcp_server_socket.accept()
    # 接收对方发过来的数据
    recv_data = client_socket.recv(1024) # 接收1024个字节
    file_name = recv_data.decode('utf-8')
    print("对方请求下载的文件名为:%s" % file_name)
    file_content = get_file_content(file_name)

    # 发送文件的数据给客户端
    # 因为获取打开文件时是以rb方式打开,所以file_content中的数据时以二进制的格式,因此不需要encoding
    if file_content:
    client_socket.send(file_content)
    # 关闭这个套接字
    client_socket.close()

    # 关闭监听套接字
    tcp_server_socket.close()


    if __name__ == '__main__':
    main()


    # 在cmd中执行 python xxx.py 2000
  • 客户端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    from socket import *


    def main():
    # 创建socket
    tcp_client_socket = socket(AF_INET, SOCK_STREAM)

    # 目的信息
    # server_ip = input("请输入服务器ip:")
    server_ip = "192.168.1.107"
    # server_port = int(input("请输入服务器port:"))
    server_port = 2000

    # 链接服务器
    tcp_client_socket.connect((server_ip, server_port))

    # 输入需要下载的文件名
    file_name = input("请输入要下载的文件名:")

    # 发送文件下载请求
    tcp_client_socket.send(file_name.encode('utf-8'))

    # 接收对方发来的数据,最大接收1024个字节(1K)
    recv_data = tcp_client_socket.recv(1024)

    if recv_data:
    with open("[接收]" + file_name, "wb") as f:
    f.write(recv_data)

    # 关闭套接字
    tcp_client_socket.close()

    if __name__ == '__main__':
    main()

TCP 的三次握手

TCP 的三次握手

  • 主机A 是client,主机B 是server
  • 图示中的右边的 ACK=x+1 是 ack=x+1
  • 为什么TCP两次握手不可以?
    • 两次握手会发生死锁,因为如果两次握手,client 关机了,server一直认为与client连接中,就会造成死锁

TCP 的四次握手

TCP 的四次握手

  • 有 4 个缓冲区,需要 4 个缓冲区
  • Time-wait:是用来判断第四次挥手对方有没有收到,就是在2MSL时间内,如果没有收到任何消息,默认对方收到了第四条消息

TCP 的长连接和短连接

TCP 通信

TCP 短连接(无状态)

  • 模拟一种 TCP 短连接的情况:

    1. client 向 server 发起连接请求
    2. server 接到请求,双方建立连接
    3. client 向 server 发送消息
    4. server 回应 client
    5. 一次读写完成,此时双方任何一个都可以发起 close 操作
  • 短连接一般只会在 client/server 间传递一次读写操作

    短连接

TCP 长连接(游戏、即使通信)

  • 模拟一种 TCP 长连接的情况:
    1. client 向 server 发起连接
    2. server 接到请求,双方建立连接
    3. client 向 server 发送消息
    4. server 回应 client
    5. 一次读写完成,连接不关闭
    6. 后续读写操作…
    7. 长时间操作之后 client 发起关闭请求

长连接


TCP/IP 概述

  • OSI 参考模型:(应 - 表 - 会 - 传 - 网 - 数 - 物)
  • tcp/ip 模型 4层:
    • 应用层 { http 超文本传输协议 ftp 文件传输协议 telnet 远程登录 ssh 安全外壳协议 smtp 简单邮件发送 pop3 收邮件 }
    • 传输层 { tcp 传输控制协议,udp用户数据包协议 }
    • 网络层 { ip 网际互联协议,icmp 网络控制消息协议,igmp 网络组管理协议 }
    • 网络接口层 { arp地址转换协议,rarp 反向地址转换协议,mpls多协议标签交换 }

TCP/IP 协议传输示意图

  • 分时复用:数据包进行排队,eg:一根网线能够同时上网

  • ARP(地址转换协议):用于获得同一物理网络中的硬件主机地址。是设备通过自己知道的 IP 地址来获得自己不知道的物理地址的协议。(只能在局域网内进行运行的)

    ARP

  • RARP(Reverse Address Resolution Protocol):反向地址转换协议,允许局域网的物理机器从网关服务器的 ARP 表或者缓存上请求其 IP 地址。(只能在局域网内进行运行的)

    ARP数据报格式

    • op字段为 1 表示ARP请求,op字段为 2 表示ARP应答
  • IP:(网际互联协议)负责在主机和网络之间寻址和路由数据包。

    IP 数据包格式

    • 版本号:长度 4 bit。一般为 0100(IPv4)、0110(IPv6)
    • IP 包头长度:长度为 4 bit。用于描述 IP 包头的长度
    • 16位报文总长度:长度 16 比特。 以字节为单位计算的 IP 包的长度(包括头部和数据),所以IP 包最大长度 65535 字节。
  • ICMP:(网络控制消息协议)用于发送报告有关数据包的传送错误的协议。

    • Ping 命令的过程实际上就是ICMP协议工作的过程
  • TCP:(传输控制协议)为应用程序提供可靠(可靠数据传输表现在:序列号、确认重传、定时器、滑动窗口等)的通信连接。适合于一次传输大批数据的情况。并适用于要求得到响应的应用程序

    TCP 数据报文头部

    • 源端口、目的端口:16 位长。标识出远端和本地的端口号
    • 顺序号:32 位长。标识发送的数据报的顺序
    • 确认号:32 位长。希望收到的下一个数据报的序列号
    • TCP 头长:4 位长。表明 TCP 头中包含多少个 32 位字。就是有多少个字节
    • URG:紧急指针(urgent pointer)有效,紧急指针指出在本报文段中的紧急数据的最后一个字节的序号
    • ACK:ACK 位置 1 表明确认号是合法的。如果 ACK 为 0,那么数据报不包含确认信息,确认字段被省略
    • PSH:表示是带有 PUSH 标志的数据。接收方因此请求数据报一到便可送往应用 程序而不必等到缓冲区装满时才发送。当 PSH=1 时,则报文段会被尽快地交付给 目的方,不会对这样的报文段使用缓存策略
    • RST:用于复位由于主机崩溃或其他原因而出现的错误的连接。还可以用于拒绝 非法的数据报或拒绝连接请求。当 RST 为 1 时,表明 TCP 连接中出现了严重的差 错,必须释放连接,然后再重新建立连接。
    • SYN:用于建立连接。当 SYN=1 时,表示发起一个连接请求。
    • FIN:用于释放连接。当 FIN=1 时,表明此报文段的发送端的数据已发送完成, 并要求释放连接
    • 窗口大小:16 位长。窗口大小字段表示在确认了字节之后还可以发送多少个字节。此字段用来进行流量控制。单位为字节数,这个值是本机期望一次接收的字节数
  • MTU 与 MSS

    • MTU 是网络传输最大报文包,MSS 是网络传输数据最大值
    • MSS 加包头数据就等于 MTU
    • 总之,MTU 就是总的最后发出去的报文大小。MSS 就是你需要发出去的数据大小
  • UDP:(用户数据包协议)提供了无连接通信,且不对传送包进行可靠的保证。适合于一次传输少量数据。

    • 不需要像 TCP 那样通过 三次握手来建立一个连接。同时,一个 UDP 应用可同时作为应用的客户或服务器方
    • 比 TCP 协议更为高效,也能更好地解决实时性的问题
    • 从理论上说,包含报头在内的数据报的最大长度为 65535 字节

    UDP 报文格式

  • TCP 和 UDP,一台机器向另外一台机器发送了 3 个包,对方可能收到几个包?

    • TCP >=3:3 个到任意多个
    • UDP:0 到 3 个
  • C/S 模式 和 B/S 模式


DDos(SYN Flooding)攻击

  • 全称是 分布式拒绝服务攻击

  • 原理:我们构造网络包,源地址随机构建,意味着当 Server 接到 SYN 包时,应答 ack 和 syn 时不会得到回应。在这种情况下, Server 端,内核就会维持一个很大的队列来管理这些半连接。 当半连接足够多的时候,就会导致新来的正常连接请求得不到响应, 也就是所谓的 DDos 攻击

    DDos攻击流程

  • SYN Flood 攻击防护手段:

    • tcp_max_syn_backlog: 半连接队列长度
    • tcp_synack_retries: syn+ack 的重传次数
    • tcp_syncookies : syn cookies
  • 一般的防御措施:就是就是减小 SYN+ACK 重传次数,增加半连接队列长度,启用 syn cookie。 不过在高强度攻击面前,调优 tcp_syn_retries 和 tcp_max_syn_backlog 并不能解决根本问题,更有效的防御手段是激活 tcp_syncookies,在连接真正创建起来之前,它并不会立刻给请求分配数据区存储连接状态,而是通过构建一个带签名的序号来屏蔽伪造请求。


网络编程 - epoll 的使用

  • epoll 是对 select、poll 模型的改进,提高了网络编程的性能,广泛应用于大规模并发请求的 C/S 架构中
  • 触发方式:边缘触发/水平触发,只适用于 Unix/Linux 操作系
  • epoll 的官方名字:多路复用

下面的代码一人只能轮流说一句

  • 服务端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    import socket


    # tcp的服务端
    def tcp_server():
    # SOCK_STREAM代表的是tcp的socket
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    addr = ("100.65.1.86", 2000)
    s.bind(addr) # 绑定,端口没有激活
    s.listen(128) # listen时,端口才激活
    new_client, client_addr = s.accept()
    # print(client_addr) # ('192.168.1.107', 61371)

    while True:
    # 服务器端先接收
    data = new_client.recv(128)
    print(data.decode('utf-8'))
    data = input()
    new_client.send(data.encode("utf8"))

    new_client.close()
    s.close()


    if __name__ == '__main__':
    tcp_server()
  • 客户端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import socket


    def tcp_client():
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    addr = ("100.65.1.86", 2000)
    client.connect(addr)
    while True:
    # 先说话
    data = input()
    client.send(data.encode("utf8"))
    data = client.recv(128) # 收对方说的话
    print(data.decode("utf8"))

    client.close()


    if __name__ == '__main__':
    tcp_client()

上面这段代码每次只能传/收一句话的原因:

  1. input():标准输入缓存区
  2. recv():接收缓冲区

epoll 的接口及使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import select  #  导入 select 模块

epoll = select.epoll() # 初始化,创建一个 epoll 对象

epoll.register(文件描述符, 事件类型) # 注册要监控的文件句柄和事件
"""

事件类型:
select.EPOLLIN 可读事件
select.EPOLLOUT 可写事件
select.EPOLLERR 错误事件
select.EPOLLHUP 客户端断开事件

"""

epoll.unregister(文件描述符) # 销毁文件描述符

epoll.poll(timeout, maxevents)
# 当文件句柄发生变化,则会以列表的形式主动报告给用户进程, timeout为超时时间,默认为-1,即一直等待直到文件句柄发生变化,如果指定为 1,那么 epoll 每 1 秒汇报一次当前文件句柄的变化情况,如果无变化则返回空
# maxevents:指监控的个数
# eg:fd, event = epoll.poll(-1) 会返回两个参数

epoll.fileno() # 返回 epoll 的控制文件描述符(Return the epoll control file descriptor)

epoll.modfiy(fineno,event) # fineno 为文件描述符 event 为事件类型 作用是修改文件描述符所对应的事件

epoll.fromfd(fileno) # 从 1 个指定的文件描述符创建 1 个 epoll 对象

epoll.close() # 关闭 epoll 对象的控制文件描述符

epoll 实现即时聊天

  • 服务端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    import socket
    import sys

    import select


    # tcp的服务端
    def tcp_server():
    # SOCK_STREAM代表的是tcp的socket
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    addr = ("", 2000)
    s.bind(addr) # 绑定,端口没有激活
    s.listen(128) # listen时,端口才激活
    new_client, client_addr = s.accept()
    # print(client_addr) # ('192.168.1.107', 61371)

    # 初始化了一个epoll对象
    epoll = select.epoll()

    # 获取文件描述符
    # print(new_client.fileno())

    # 让epoll 监控 new_client
    epoll.register(new_client.fileno(), select.EPOLLIN)
    epoll.register(sys.stdin.fileno(), select.EPOLLIN) # sys.stdin.fileno() 指标准输入

    while True:
    # 谁的缓冲区有数据,就填到写到events里面
    # 这个events里面存储的是元组,为(fd, 事件)
    events = epoll.poll(-1)
    for fd, event in events:
    if fd == new_client.fileno():
    data = new_client.recv(128)
    if data:
    print(data.decode('utf-8'))
    else:
    print("对端断开")
    return
    elif fd == sys.stdin.fileno():
    try:
    data = input() # 服务器端说话,发给对方
    except EOFError: # ctrl + d 后服务器断开
    print("I wanna go")
    return
    new_client.send(data.encode("utf8"))

    new_client.close()
    s.close()


    if __name__ == '__main__':
    tcp_server()
  • 客户端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    import socket
    import sys
    import select


    # tcp的客户端
    def tcp_client():
    if sys.argv == 1:
    return
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    addr = (sys.argv[1], 2000)
    client.connect(addr)
    # 初始化了一个epoll对象
    epoll = select.epoll()

    # 获取文件描述符
    # print(new_client.fileno())

    # 让epoll 监控 new_client
    epoll.register(client.fileno(), select.EPOLLIN)
    epoll.register(sys.stdin.fileno(), select.EPOLLIN) # sys.stdin.fileno() 指标准输入

    while True:
    # 谁的缓冲区有数据,就填到写到events里面
    # 这个events里面存储的是元组,为(fd, 事件)
    events = epoll.poll(-1)

    for fd, event in events:
    if fd == client.fileno():
    data = client.recv(128)
    if data:
    print(data.decode('utf-8'))
    else:
    print("对端断开")
    return
    elif fd == sys.stdin.fileno():
    try:
    data = input() # 客户端端说话,发给对方
    except EOFError:
    print("I wanna go")
    return
    client.send(data.encode("utf8"))

    client.close()


    if __name__ == '__main__':
    tcp_client()

epoll 实现客户端断开后可以再次连接

  • 服务端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    import socket
    import sys

    import select


    # 聊天室的服务器端
    def tcp_server():
    # SOCK_STREAM代表的是tcp的socket
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    addr = ("", 2000)
    s.bind(addr) # 绑定,端口没有激活
    s.listen(128) # listen时,端口才激活
    # new_client, client_addr = s.accept()
    # print(client_addr) # ('192.168.1.107', 61371)

    # 初始化了一个epoll对象
    epoll = select.epoll()

    # 获取文件描述符
    # print(new_client.fileno())

    # 让epoll 监控 new_client
    epoll.register(s.fileno(), select.EPOLLIN)
    epoll.register(sys.stdin.fileno(), select.EPOLLIN) # sys.stdin.fileno() 指标准输入

    while True:
    # 谁的缓冲区有数据,就填到写到events里面
    # 这个events里面存储的是元组,为(fd, 事件)
    events = epoll.poll(-1)
    for fd, event in events:
    if fd == s.fileno():
    # 有客户端连接,就连上
    new_client, client_addr = s.accept()
    print("Connected by", client_addr)
    epoll.register(new_client.fileno(), select.EPOLLIN)

    if fd == new_client.fileno():
    data = new_client.recv(128)
    if data:
    print(data.decode('utf-8'))
    else:
    print("对端断开")
    # 对端断开要解除监控
    # epoll.unregister(new_client.fileno())
    new_client.close()
    break

    elif fd == sys.stdin.fileno():
    try:
    data = input() # 服务器端说话,发给对方
    except EOFError: # ctrl + d 后服务器断开
    print("I wanna go")
    return
    new_client.send(data.encode("utf8"))

    new_client.close()
    s.close()


    if __name__ == '__main__':
    tcp_server()
  • 客户端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    import socket
    import sys
    import select


    # tcp的客户端
    def tcp_client():
    if sys.argv == 1:
    return
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    addr = (sys.argv[1], 2000)
    client.connect(addr)
    # 初始化了一个epoll对象
    epoll = select.epoll()

    # 获取文件描述符
    # print(new_client.fileno())

    # 让epoll 监控 new_client
    epoll.register(client.fileno(), select.EPOLLIN)
    epoll.register(sys.stdin.fileno(), select.EPOLLIN) # sys.stdin.fileno() 指标准输入

    while True:
    # 谁的缓冲区有数据,就填到写到events里面
    # 这个events里面存储的是元组,为(fd, 事件)
    events = epoll.poll(-1)

    for fd, event in events:
    if fd == client.fileno():
    data = client.recv(128)
    if data:
    print(data.decode('utf-8'))
    else:
    print("对端断开")
    return
    elif fd == sys.stdin.fileno():
    try:
    data = input() # 客户端端说话,发给对方
    except EOFError:
    print("I wanna go")
    return
    client.send(data.encode("utf8"))

    client.close()


    if __name__ == '__main__':
    tcp_client()

聊天室的实现

  • 服务端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    import socket
    import sys

    import select


    # 聊天室的服务器端
    def tcp_server():
    # SOCK_STREAM代表的是tcp的socket
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    addr = ("", 2000)
    s.bind(addr) # 绑定,端口没有激活
    s.listen(128) # listen时,端口才激活
    # new_client, client_addr = s.accept()
    # print(client_addr) # ('192.168.1.107', 61371)

    # 初始化了一个epoll对象
    epoll = select.epoll()

    # 获取文件描述符
    # print(new_client.fileno())

    # 让epoll 监控 new_client
    epoll.register(s.fileno(), select.EPOLLIN)
    epoll.register(sys.stdin.fileno(), select.EPOLLIN) # sys.stdin.fileno() 指标准输入
    client_list = [] # 存储所有的client对象

    while True:
    # 谁的缓冲区有数据,就填到写到events里面
    # 这个events里面存储的是元组,为(fd, 事件)
    events = epoll.poll(-1)
    for fd, event in events:
    if fd == s.fileno():
    # 有客户端连接,就连上,得到客户端对应的new_client,放入列表,并注册它
    new_client, client_addr = s.accept()
    print("Connected by", client_addr)
    client_list.append(new_client)
    epoll.register(new_client.fileno(), select.EPOLLIN)
    else:
    remove_client = None
    for client in client_list: # 遍历所有的客户端对象
    if client.fileno() == fd:
    data = client.recv(128) # 接收对应客户端发过来的数据
    if data: # 拿到数据,群发给其他的客户端
    for other_client in client_list:
    if other_client is client:
    continue
    else:
    other_client.send(data)
    else: # 断开了就记录一下
    remove_client = client
    if remove_client is not None:
    print(" %s Client disconnected" % remove_client.fileno())
    client_list.remove(remove_client)
    epoll.unregister(remove_client.fileno())
    remove_client.close()

    new_client.close()
    s.close()


    if __name__ == '__main__':
    tcp_server()

  • 客户端

    • 同上面的客户端

非阻塞编程

  • 同步阻塞
    • 正常的 recv,recvfrom 读取数据会阻塞
  • 同步非阻塞编程
    • 轮训
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from socket import *
import select
import sys

tcp_server_socket = socket(AF_INET, SOCK_STREAM)
# 重用对应地址和端口
tcp_server_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)

# 本地 IP 地址和端口
address = ('192.168.1.111', 2000)
tcp_server_socket.bind(address)

# 端口激活
tcp_server_socket.listen(100)
tcp_server_socket.setblocking(False)
client_socket = None
temp_client = None

while True:
try:
temp_client, clientAddr = tcp_server_socket.accept()
except Exception as e:
# print(e)
client_socket = temp_client
if client_socket:
client_socket.setblocking(False)
try:
text = client_socket.recv(1024)
# 如果对方断开
if not text:
print('byebye')
client_socket.close()
temp_client = None
continue
print(text.decode('utf-8'))
except Exception as e:
pass

如果程序被 ctrl+c 终止,这个时候端口会处于 TIME_WAIT 状态

如果端口会处于 TIME_WAIT 状态,不能被再次绑定,解决办法:

1
2
#重用对应地址和端口
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

写(send) 是会阻塞的

  • 写是会阻塞的,当写满就不能再写,缓冲区不够就阻塞

  • 对方断开后,怎么办?判断返回值,或者加入 try 异常判断,避免程序崩溃

    1
    send_size = tcp_client_socket.send(temp_str.encode('utf-8'), MSG_DONTWAIT)

Python 发送整型数,浮点数

1
2
3
4
5
6
7
8
9
10
11
12
13
import struct

train_content = '我很帅你很牛'.encode('utf-8')
train_head = len(train_content)

# 转换后的 str 虽然是字符串类型,但相当于其他语言中的字节流(字节数组),可以在网络上传输
temp_str = struct.pack("I", train_head)
# I 表示整型数
print(temp_str)
# b'\x12\x00\x00\x00':这种是小端的内存

train_head_bytes = struct.unpack("I", temp_str)
print(train_head_bytes[0]) # 18

持续发送多个文件

  • 粘包:两次发送的报文挨在一起,分不开

    • eg:文件名1 + 文件内容1 + 文件名2 + 文件内容2 …
  • 服务端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    import socket
    import struct


    def tcp_init():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    addr = ("", 2000)
    s.bind(addr)
    s.listen(128)
    return s

    def send_file():
    file_name = "test"
    s = tcp_init()
    new_client, client_addr = s.accept()

    # 先发文件名(火车头 + 火车体)
    file_name_bytes = file_name.encode("utf-8")
    train_head_bytes = struct.pack("I", len(file_name_bytes))
    new_client.send(train_head_bytes + file_name_bytes)

    # 再发文件内容(火车头 + 火车体)
    f = open(file_name, "rb")
    file_content = f.read()
    train_head_bytes = struct.pack("I", len(file_content))
    new_client.send(train_head_bytes + file_content)

    new_client.close()
    s.close()


    if __name__ == '__main__':
    send_file()
  • 客户端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    from socket import *
    import struct

    tcp_client_socket = socket(AF_INET, SOCK_STREAM)

    # 本地 IP 地址和端口
    address = ('100.65.59.237', 2000)

    # 连接服务器
    tcp_client_socket.connect(address)

    # 每次先读4个字节的火车头
    train_head_bytes = tcp_client_socket.recv(4)

    train_content_len = struct.unpack('I', train_head_bytes)

    # 接收到文件名
    file_name = tcp_client_socket.recv(train_content_len[0])
    f = open(file_name.decode("utf-8"), 'wb')

    # 接收文件内容的长度
    train_head_bytes = tcp_client_socket.recv(4)
    train_content_len = struct.unpack('I', train_head_bytes)
    file_content = tcp_client_socket.recv(train_content_len[0]) # 接收文件内容
    f.write(file_content)

    f.close()
    tcp_client_socket.close()

百度网盘 - 基础功能实现

  • 服务端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    import socket
    import struct
    import os


    class Server:
    def __init__(self, ip, port):
    # 用来listen的socket对象
    self.s_listen = None
    self.ip = ip
    self.port = port

    def tcp_init(self):
    self.s_listen = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    self.s_listen.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    self.s_listen.bind((self.ip, self.port))
    self.s_listen.listen(128)

    def task(self):
    new_client, client_addr = self.s_listen.accept()
    user = User(new_client)
    user.deal_command() # 处理客户端发来的各种命令


    class User:
    """

    每一个User对象对应一个客户端

    """

    def __init__(self, new_client):
    self.new_client = new_client
    self.user_name = None
    self.path = os.getcwd() # 存储连上的用户的路径

    def deal_command(self):
    while True:
    command = self.recv_train().decode('utf-8')
    if command[:2] == "ls":
    self.do_ls()
    elif command[:2] == "cd":
    self.do_cd(command)
    elif command[:3] == "pwd":
    self.do_pwd()
    elif command[:2] == "rm":
    self.do_rm(command)
    elif command[:4] == "gets":
    self.do_gets(command)
    elif command[:4] == "puts":
    self.do_puts(command)
    else:
    print("Invalid command")

    def do_ls(self):
    """
    当前路径下的信息传输给客户端
    :return:
    """
    data = ""
    for file in os.listdir(self.path):
    data += file + " " * 5 + str(os.stat(file).st_size) + "\n"
    self.send_train(data.encode('utf-8'))

    def do_cd(self, command):
    path = command.split()[1]
    os.chdir(path)
    self.path = os.getcwd()
    self.send_train(self.path.encode("utf-8")) # 每次客户端执行cd后,再显示一下路径

    def do_pwd(self):
    self.send_train(self.path.encode('utf-8'))

    def do_rm(self, command):
    path = command.split()[1]
    os.remove(path)
    self.send_train(self.path.encode("utf-8"))

    def do_gets(self, command):
    pass

    def do_puts(self, command):
    pass

    def send_train(self, send_bytes):
    """
    send_train:就是把某个字节流内容以火车形式发过去
    :param send_bytes:
    :return:
    """
    train_head_bytes = struct.pack("I", len(send_bytes))
    self.new_client.send(train_head_bytes + send_bytes)

    def recv_train(self):
    """
    recv_train:就是把火车recv的内容返回出去
    :return:
    """
    train_head_bytes = self.new_client.recv(4)
    train_head = struct.unpack("I", train_head_bytes)
    return self.new_client.recv(train_head[0])


    if __name__ == '__main__':
    server = Server("", 2000)
    server.tcp_init()
    server.task()
  • 客户端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    import socket
    import struct


    class Client:
    def __init__(self, ip, port):
    self.client = None
    self.ip = ip
    self.port = port

    def tcp_connect(self):
    self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    self.client.connect((self.ip, self.port))

    def send_train(self, send_bytes):
    """
    send_train:就是把某个字节流内容以火车形式发过去
    :param send_bytes:
    :return:
    """
    train_head_bytes = struct.pack("I", len(send_bytes))
    self.client.send(train_head_bytes + send_bytes)

    def recv_train(self):
    """
    recv_train:就是把火车recv的内容返回出去
    :return:
    """
    train_head_bytes = self.client.recv(4)
    train_head = struct.unpack("I", train_head_bytes)
    return self.client.recv(train_head[0])

    def send_command(self):
    """
    发送各种命令给服务器
    :return:
    """
    while True:
    command = input()
    self.send_train(command.encode("utf-8"))
    if command[:2] == "ls":
    self.do_ls()
    elif command[:2] == "cd":
    self.do_cd(command)
    elif command[:3] == "pwd":
    self.do_pwd()
    elif command[:2] == "rm":
    self.do_rm(command)
    elif command[:4] == "gets":
    self.do_gets(command)
    elif command[:4] == "puts":
    self.do_puts(command)
    else:
    print("Invalid command")

    def do_ls(self):
    data = self.recv_train().decode("utf-8")
    print(data)


    def do_cd(self, command):
    data = self.recv_train().decode("utf-8")
    print(data)

    def do_pwd(self):
    data = self.recv_train().decode("utf-8")
    print(data)

    def do_rm(self, command):
    data = self.recv_train().decode("utf-8")
    print(data)

    def do_gets(self, command):
    pass

    def do_puts(self, command):
    pass

    if __name__ == '__main__':
    client = Client('100.65.59.237', 2000)
    client.tcp_connect()
    client.send_command()

多进程


  • 查看 Linux 的 cpu 的核数

    1
    cat /proc/cpuinfo 
  • 看到 R 状态是两次采样之间的一个时间片变化分析计算

    • R 状态 运行
    • S 状态 睡眠
  • 可以显示父子进程关系

    1
    ps –elf 

    举例

  • 看 %cpu(cpu 使用量) %mem(内存使用量) stat 状态{S 睡眠 T 暂停 R 运行 Z僵尸}

    1
    ps -aux
  • 显示前 20 条进程,动态的改变,按 q 退出

    1
    top

    举例

    • %us 用户空间占用 cpu 百分比
    • %sy 内核空间占用 cpu 百分比
    • %id 空闲 cpu 百分比,反映一个系统 cpu 的闲忙程度。越大越空闲
  • 杀死进程

    1
    2
    3
    4
    kill -9 进程名

    # 查看系统的所有信号
    kill -l
  • 将挂起的进程放到后台运行

    1
    bg 作业ID
  • 将后台进程拉到前台

    1
    fg 作业ID
  • 看后台任务

    1
    jobs
  • 让进程后台运行

    1
    python xxx.py &
  • 查看物理内存

    1
    free
  • 杀死进程名为 a 的进程,如果有空格,用 \ 转义

    1
    2
    3
    4
    pkill -f a

    eg:
    pkill -f python\ xxx.py
  • 查看磁盘及磁盘分区情况

    1
    fdisk –l
  • 设置当前用户定时任务

    1
    2
    3
    4
    5
    crontab –e


    vim /etc/crontab # 设置定时任务
    crontab -l # 查看当前自己设置的定时任务

进程的创建


  • multiprocessing 模块就是跨平台版本的多进程模块,提供了一个 Process 类来代表一个进程对象,这个对象可以理解为是一个独立的进程,可以执行另外的事情
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from multiprocessing import Process
import time


def run_proc():
"""
子进程要执行的代码
"""
while True:
print("----2----")
time.sleep(1)


if __name__ == '__main__':
# 创建一个进程
p = Process(target=run_proc) # run_proc 传递时不能加括号
p.start()
while True:
print("----1----")
time.sleep(1)
  • 创建子进程时,只需要传入一个执行函数和函数的参数,创建一个 Process 实例,用 start() 方法启动
  • 孤儿进程 — 父进程退出(kill 杀死父进程),子进程变为孤儿
    • 1 号进程一般是接管孤儿进程的,负责代替之前的父进程来回收PCB
  • 僵尸进程 — 子进程退出,父进程在忙碌,没有回收它,要避免僵尸
    • Python 进程变为僵尸进程后,名字会改变
    • 僵尸进程是 kill 不掉的

获取进程的pid


1
2
3
4
5
6
7
8
9
10
11
12
import os
from multiprocessing import Process

def run_process():
print("I am son process, pid is {}, ppid is {}".format(os.getpid(), os.getppid()))
# I am son process, pid is 16412, ppid is 15008

if __name__ == '__main__':
child = Process(target=run_process) # target代表子进程启动时运行哪个函数
child.start()
print("I am father process, pid is {}, ppid is {}".format(os.getpid(), os.getppid()))
# I am father process, pid is 15008, ppid is 4860
  • 获取父进程的pid

    1
    os.getppid()

Process 语法结构


1
Process(group , target , name , args , kwargs)
  • target:如果传递了函数的引用,可以让这个子进程就执行这里的代码
  • args:给 target 指定的函数传递的参数,以元组的方式传递
  • kwargs:给 target 指定的函数传递命名参数,keyword 参数

Process 创建的实例对象的常用方法:

1
2
3
4
5
6
7
8
9
10
start()  # 启动子进程实例(创建子进程)


is_alive() # 判断进程子进程是否还在活着


join([timeout]) # 是否等待子进程执行结束,或等待多少秒--回收子进程尸体


terminate() # 不管任务是否完成,立即终止子进程

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from multiprocessing import Process
import os
import time


def run_proc(name, age, **kwargs):
for i in range(10):
print("son process {} {}, {}".format(name, age, kwargs))
print("son process pid is {}".format(os.getpid()))
time.sleep(0.5)


if __name__ == '__main__':
p = Process(target=run_proc, args=("zhaosan", 6), kwargs={"math": 130})
p.start()
time.sleep(1)
p.terminate() # 给子进程发信号杀掉它
p.join() # 会等待子进程先执行
print("father process pid is {}".format(os.getpid()))

"""
son process zhaosan 6, {'math': 130}
son process pid is 17088
son process zhaosan 6, {'math': 130}
son process pid is 17088
father process pid is 29940

"""

进程间对于全局变量的共享


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 子进程的创建是父进程的复制品,资源是独立使用的
# 每个进程都是独立的

from multiprocessing import Process
import os
import time

nums = [11, 22]


def work1():
print("I am work1, {}".format(os.getpid()))
nums.append(33)
print("work1 {}".format(nums))


def work2():
print("I am work2, {}".format(os.getpid()))
print(nums)


if __name__ == '__main__':
p = Process(target=work1)
p.start()
p.join()
p = Process(target=work2)
p.start()
p.join()

"""
I am work1, 13152
work1 [11, 22, 33]
I am work2, 25648
[11, 22]

"""

进程间通信 - Queue


  • 使用 multiprocessing 模块的 Queue 实现多进程之间的数据传递,Queue 本身是一个消息列队程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from multiprocessing import Queue

q = Queue(3) # 初始化一个 Queue 对象,最多可接收三条 put 消息
q.put(1)
q.put(2)
print(q.full()) # False
q.put(3)
print(q.full()) # True

# 队列满了之后,如果再放就会阻塞

# print(q.get()) # 1
# print(q.get()) # 2
# print(q.get()) # 3

try:
print(q.put(4, True, 2))
except:
print("队列长度:{}".format(q.qsize())) # 队列长度:3
  • 初始化 Queue() 对象时(例如:q=Queue()),若括号中没有指定最大可接收的消息数量,或数量为负值,那么就代表可接受的消息数量没有上限(直到内存的尽头)
    • Queue.qsize():返回当前队列包含的消息数量
    • Queue.empty():如果队列为空,返回 True,反之 False
    • Queue.full():如果队列满了,返回 True,反之 False
    • Queue.get([block[, timeout]]):获取队列中的一条消息,然后将其从列队中移除,block 默认值为 True;

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import time
from multiprocessing import Queue, Process


def write(q: Queue):
for value in ["A", "B", "C"]:
print("Puts {} to queue".format(value))
q.put(value)
time.sleep(1)


def reader(q: Queue):
while True:
if not q.empty():
value = q.get(True)
print("Reads {} to queue".format(value))
time.sleep(2)
else:
break


if __name__ == '__main__':
q = Queue(10)
pw = Process(target=write, args=(q,)) # 一个元素必须加逗号,才是元组
pr = Process(target=reader, args=(q,))
pw.start()
pr.start()
pw.join()
pr.join()

"""
Puts A to queue
Reads A to queue
Puts B to queue
Puts C to queue
Reads B to queue
Reads C to queue

"""

进程池 - Pool


  • 可以用到 multiprocessing 模块提供的 Pool 方法
  • join():主进程阻塞,等待子进程的退出, 必须在 close 或 terminate 之后使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
from multiprocessing.pool import Pool

import os
import time
import random


def worker(msg):
t_start = time.time()

print("%s 开始执行,进程号为%d" % (msg, os.getpid())) # 目的只有3个进程(服务员)
# random.random()随机生成 0~1 之间的浮点数
time.sleep(random.random() * 2)

t_stop = time.time()
print(msg, "执行完毕,耗时%0.2f" % (t_stop - t_start))


if __name__ == '__main__':
pool = Pool(processes=3)
for i in range(10):
# Pool().apply_async(要调用的目标,(传递给目标的参数元祖,))
# 异步执行,每次循环将会用空闲出来的子进程去调用目标
pool.apply_async(worker, (i,))

print("----start----")
pool.close() # 关闭进程池,关闭后 po 不再接收新的请求
pool.join() # 等待 pool 中所有子进程执行完成,必须放在 close 语句之后
print("-----end-----")

"""
----start----
0 开始执行,进程号为20148
1 开始执行,进程号为16328
2 开始执行,进程号为26488
2 执行完毕,耗时0.16
3 开始执行,进程号为26488
1 执行完毕,耗时0.45
4 开始执行,进程号为16328
4 执行完毕,耗时0.42
5 开始执行,进程号为16328
3 执行完毕,耗时0.76
6 开始执行,进程号为26488
0 执行完毕,耗时1.03
7 开始执行,进程号为20148
6 执行完毕,耗时1.11
8 开始执行,进程号为26488
5 执行完毕,耗时1.61
9 开始执行,进程号为16328
9 执行完毕,耗时0.36
7 执行完毕,耗时1.86
8 执行完毕,耗时1.01
-----end-----

"""

进程池中的 Queue

  • 如果要使用 Pool 创建进程,就需要使用 multiprocessing.Manager() 中的 Queue(),而不是 multiprocessing.Queue(),否则会得到一条如下的错误信息:
    • RuntimeError: Queue objects should only be shared between processes through inheritance.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 修改 import 中的 Queue 为 Manager
from multiprocessing import Manager, Pool
import os, time, random


def reader(q):
print("reader 启动(%s),父进程为(%s)" % (os.getpid(), os.getppid()))
for i in range(q.qsize()):
print("reader 从 Queue 获取到消息:%s" % q.get())


def writer(q):
print("writer 启动(%s),父进程为(%s)" % (os.getpid(), os.getppid()))
for i in "zhangsan":
q.put(i)


if __name__ == "__main__":
print("(%s) start" % os.getpid())
q = Manager().Queue() # 使用 Manager 中的 Queue
po = Pool()
po.apply_async(writer, (q,))
time.sleep(1) # 先让上面的任务向 Queue 存入数据,然后再让下面的任务开始从中取数据
po.apply_async(reader, (q,))
po.close()
po.join()
print("(%s) End" % os.getpid())


"""
(23412) start
writer 启动(14096),父进程为(23412)
reader 启动(25924),父进程为(23412)
reader 从 Queue 获取到消息:z
reader 从 Queue 获取到消息:h
reader 从 Queue 获取到消息:a
reader 从 Queue 获取到消息:n
reader 从 Queue 获取到消息:g
reader 从 Queue 获取到消息:s
reader 从 Queue 获取到消息:a
reader 从 Queue 获取到消息:n
(23412) End

"""

Pycharm 远程开发


  • 步骤一:

    1

  • 步骤二:

    2

  • 步骤三:

    3

    • 这里的同步文件夹需要修改,修改Linux下的目录

多线程


线程

python 的 thread 模块是比较底层的模块,python 的 threading 模块是对 thread 做了一些包装的,可以更加方便的被使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import threading
import time


def saySorry():
print("I am sorry, I didn't get")
time.sleep(1)


if __name__ == "__main__":
for i in range(5):
t = threading.Thread(target=saySorry) # 创建线程
t.start() # 启动线程,即让线程开始执行

"""
I am sorry, I didn't get
I am sorry, I didn't get
I am sorry, I didn't get
I am sorry, I didn't get
I am sorry, I didn't get

"""
  • 查看线程的方法

    1
    ps -elLf|grep 线程名
  • 进程会等待所有的线程结束后,它才结束

  • 查看线程的数量

    1
    length = len(threading.enumerate())
  • 一个Python进程中的多个线程是不可以迁移到多个核的,只能在一个核上

    Python 弊端


面向对象创建线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import threading
import time


class MyThread(threading.Thread):
# 执行线程
def run(self) -> None:
for i in range(3):
time.sleep(1)
msg = "I am " + self.name + " @ " + str(i) # name属性中保存的是当前线程的名字
print(msg)


# 创建5个线程
def test():
for i in range(5):
t = MyThread()
t.start()


if __name__ == '__main__':
test()


"""
I am Thread-4 @ 0
I am Thread-5 @ 0
I am Thread-1 @ 0
I am Thread-2 @ 0
I am Thread-3 @ 0
I am Thread-3 @ 1
I am Thread-1 @ 1
I am Thread-2 @ 1
I am Thread-5 @ 1
I am Thread-4 @ 1
I am Thread-4 @ 2
I am Thread-5 @ 2
I am Thread-3 @ 2
I am Thread-1 @ 2
I am Thread-2 @ 2

打印的不同是因为存在全局解释器锁的缘故
"""

总结:

  • 每个线程默认有一个名字,尽管上面的例子中没有指定线程对象的 name,但是 python 会自动为线程指定一个名字
  • 当线程的 run() 方法结束时该线程完成
  • 无法控制线程调度程序,但可以通过别的方式来影响线程调度的方式

多线程共享全局变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import threading
import time

g_num = 0


def work1(num):
global g_num
for i in range(num):
g_num += 1
print("----in work1, g_num is %d---" % g_num)


def work2(num):
global g_num
for i in range(num):
g_num += 1
print("----in work2, g_num is %d---" % g_num)


print("---线程创建之前 g_num is %d---" % g_num) # 0
t1 = threading.Thread(target=work1, args=(100,))
t1.start()
t2 = threading.Thread(target=work2, args=(100,))
t2.start()
t1.join() # 等待t1结束
t2.join() # 等待t2结束
print("2 个线程对同一个全局变量操作之后的最终结果是:%s" % g_num) # 200

总结:

  • 在一个进程内的所有线程共享全局变量,很方便在多个线程间共享数据
  • 缺点就是,线程是对全局变量随意遂改可能造成多线程之间对全局变量的混乱**(即线程非安全)**
  • 如果多个线程同时对同一个全局变量操作,会出现资源竞争问题,从而数据结果会不正确
    • +1 操作不是原子操作

互斥锁

  • threading 模块中定义了 Lock 类,可以方便的处理锁定:

    • 创建锁

      1
      mutex = threading.Lock()
    • 锁定

      1
      mutex.acquire()
    • 释放

      1
      mutex.release()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import threading
import time

g_num = 0


def work1(num):
global g_num
for i in range(num):
mutex.acquire() # 加锁
g_num += 1
mutex.release() # 解锁
print("----in work1, g_num is %d---" % g_num)


def work2(num):
global g_num
for i in range(num):
mutex.acquire() # 加锁
g_num += 1
mutex.release() # 解锁
print("----in work2, g_num is %d---" % g_num)


if __name__ == '__main__':
print("---线程创建之前 g_num is %d---" % g_num)
# mutex是全局变量,初始化
# 创建一个互斥锁
# 默认是未上锁的状态
mutex = threading.Lock()
t1 = threading.Thread(target=work1, args=(10000,))
t1.start()
t2 = threading.Thread(target=work2, args=(10000,))
t2.start()
t1.join() # 等待t1结束
t2.join() # 等待t2结束
print("2 个线程对同一个全局变量操作之后的最终结果是:%s" % g_num) # 200


"""
---线程创建之前 g_num is 0---
----in work1, g_num is 10000---
----in work2, g_num is 20000---
2 个线程对同一个全局变量操作之后的最终结果是:20000

"""

总结:

  • 锁的好处:

    • 确保了某段关键代码只能由一个线程从头到尾完整地执行
  • 锁的坏处:

    • 阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
    • 由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁

死锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class MyThread1(threading.Thread):
def run(self):
# 对 mutexA 上锁
mutexA.acquire()
# mutexA 上锁后,延时 1 秒,等待另外那个线程 把 mutexB 上锁
# 这里延时 1秒是确保另外的线程能加上锁
print(self.name + '----do1---up----')
time.sleep(1)
# 此时会堵塞,因为这个 mutexB 已经被另外的线程抢先上锁了
mutexB.acquire()
print(self.name + '----do1---down----')
mutexB.release()
# 对 mutexA 解锁
mutexA.release()


class MyThread2(threading.Thread):
def run(self):
# 对 mutexB 上锁
mutexB.acquire()
# mutexB 上锁后,延时 1 秒,等待另外那个线程 把 mutexA 上锁
print(self.name + '----do2---up----')
time.sleep(1)
# 此时会堵塞,因为这个 mutexA 已经被另外的线程抢先上锁了
mutexA.acquire()
print(self.name + '----do2---down----')
mutexA.release()
# 对 mutexB 解锁
mutexB.release()


mutexA = threading.Lock()
mutexB = threading.Lock()
if __name__ == '__main__':
t1 = MyThread1()
t2 = MyThread2()
t1.start()
t2.start()


"""
Thread-1----do1---up----
Thread-2----do2---up----

"""
  • 避免死锁:
    • 程序设计时要尽量避免(银行家算法)
    • 添加超时时间等

迭代器


  • 迭代是访问集合元素的一种方式。迭代器是一个可以记住遍历的位置的对象。迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退

  • 可迭代对象

    • 把一种数据类型都可以放到 for…in…的语句中,然后让for…in…每次从中取出一条数据供我们使用,即供我们迭代

      1
      2
      3
      for i in 100

      # int 整型不是 iterable,即 int 整型不是可以迭代的
    • 判断一个对象是否是可迭代的

      • 使用 isinstance() 判断一个对象是否是 Iterable 对象(迭代器)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      In [1]: from collections import Iterable
      <ipython-input-1-c0513a1e6784>:1: DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated since Python 3.3, and in 3.10 it will stop working
      from collections import Iterable

      In [2]: isinstance([], Iterable)
      Out[2]: True

      In [3]: isinstance(int, Iterable)
      Out[3]: False

      In [4]: isinstance((), Iterable)
      Out[4]: True

      In [5]: isinstance({}, Iterable)
      Out[5]: True

      In [6]: isinstance("abc", Iterable)
      Out[6]: True
    • 可迭代对象的本质

      • 可迭代对象通过__iter__方法向我们提供一个迭代器,我们在迭代一个可迭代对象的时候,实际上就是先获取该对象提供的一个迭代器,然后通过这个迭代器来依次获取对象中的每一个数据

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        # DeprecationWarning:弃用警告
        from collections.abc import Iterable
        class MyList:
        def __init__(self):
        self.container = []

        def add(self, item):
        self.container.append(item)

        # 只要重写了iter方法,就会变为可迭代的
        def __iter__(self):
        pass

        if __name__ == '__main__':
        my_list = MyList()
        my_list.add(1)
        my_list.add(2)
        my_list.add(3)
        print(isinstance(my_list, Iterable)) # True

        # for item in my_list:
        # print(item)
    • iter() 函数 与 next() 函数

      • list、tuple 等都是可迭代对象,我们可以通过 iter()函数获取这些可迭代对象的迭代器。然后我们可以对获取到的迭代器不断使用 next()函数来获取下一条数据。

      • iter() 函数实际上就是调用了可迭代对象的__iter__方法

        1
        2
        3
        4
        5
        6
        7
        8
        9
        In [1]: list_test = [1, 2, 3, 4, 5]

        In [2]: list_test = iter(list_test)

        In [3]: next(list_test)
        Out[3]: 1

        In [4]: next(list_test)
        Out[4]: 2
      • 注意,当我们已经迭代完最后一个数据之后,再次调用 next()函数会抛出StopIteration 的异常,来告诉我们所有数据都已迭代完成,不用再执行 next()函数了

    • 迭代器 Iterator

      • 类中重写了__iter__方法和__next__方法,就是迭代器**
      • 可迭代的则是类中只重写了__iter__内置方法,没有重写__next__方法
        • 要求这个方法返回的是一个迭代器
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    class MyList:
    def __init__(self):
    self.container = []

    def add(self, item):
    self.container.append(item)

    def __iter__(self):
    myIterator = MyIterator(self)
    return myIterator


    class MyIterator:
    def __init__(self, mylist):
    self.mylist = mylist
    # current 用来记录当前访问的位置
    self.current = 0

    def __iter__(self): # iter 方法必须返回一个迭代器
    return self

    def __next__(self):
    current = self.current
    self.current += 1
    if current < len(self.mylist.container):
    return self.mylist.container[current]
    else:
    raise StopIteration


    if __name__ == '__main__':
    my_list = MyList()
    my_list.add(1)
    my_list.add(2)
    my_list.add(3)

    # for是先对 i = iter(mylist)进行操作,不断的 next(myiter)
    for i in my_list:
    print(i)

    """
    1
    2
    3

    """
    • for … in … 循环的本质:
      • for item in Iterable 循环的本质就是先通过 iter()函数获取可迭代对象 Iterable 的迭代器,然后对获取到的迭代器不断调用 next() 方法来获取下一个值并将其赋值给item,当遇到 StopIteration 的异常后循环结束

生成器


  • 为了达到记录当前状态,并配合 next() 函数进行迭代使用,我们可以采用更简便的语法,即生成器(generator)

  • 生成器是一类特殊的迭代器

  • yield 的作用:把当前的上下文(寄存器的状态)保存起来,返回一个值,就是 yield 后面的值,下次执行next操作,会从上次yield保存的线程位置继续执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    def fib(n):
    current = 0
    num1, num2 = 0, 1
    while current < n:
    num = num1
    num1, num2 = num2, num1 + num2
    current += 1
    yield num
    return 'done'

    # F 是一个生成器,支持next
    F = fib(10)
    print(F) # <generator object fib at 0x00000248029F0BA0>
    print(next(F)) # 0
    print(next(F)) # 1

    for i in F:
    print(i, end=' ') # 1 2 3 5 8 13 21 34

总结:

  • 使用了 yield 关键字的函数不再是函数,而是生成器。(使用了 yield 的函数就是生成器)
  • yield 关键字有两点作用:
    • 保存当前运行状态(断点),然后暂停执行,即将生成器(函数)挂起
    • 将 yield 关键字后面表达式的值作为返回值返回,此时可以理解为起到了 return 的作用
  • 可以使用 next() 函数让生成器从断点处继续执行,即唤醒生成器(函数)
  • Python3 中的生成器可以使用 return 返回最终运行的返回值

使用 send 唤醒


  • 除了可以使用 next() 函数来唤醒生成器继续执行外,还可以使用 send()函数来唤醒执行
  • 使用 send() 函数的一个好处是可以在唤醒的同时向断点处传入一个附加数据
    • c.next() 等价 c.send(None)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def gen():
i = 0
while i < 5:
temp = yield i
print(temp)
i += 1

g = gen()

print(next(g)) # 0
print(g.send("hello")) # g.send()有返回值
print(next(g))

"""
0
hello
1
None
2

"""

协程


  • 协程是 python 个中另外一种实现多任务的方式,只不过比线程更小占用更小执行单元(理解为需要的资源)
  • 把一个协程 切换到另一个协程。 只要这个过程中保存或恢复 CPU 上下文,那么程序还是可以运行的
    • 原理:通过 yield 在用户态保存上下文,不是操作系统所支持的
  • 与 进程、线程 相比,协程切换的效率是最低的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import time


def work1():
while True:
print("----work1---")
yield
time.sleep(0.5)


def work2():
while True:
print("----work2---")
yield
time.sleep(0.5)


def main():
w1 = work1()
w2 = work2()
while True:
next(w1)
next(w2)


if __name__ == "__main__":
main()

"""
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
...

"""

greenlet

  • 为了更好使用协程来完成多任务,python 中的 greenlet 模块对其封装,从而使得切换任务变的更加简单

    1
    2
    3
    4
    5
    # windows
    pip install greenlet

    # Linux
    pip3 install greenlet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from greenlet import greenlet
import time


def test1():
while True:
print('test1')
gr2.switch()
time.sleep(1)


def test2():
while True:
print('test2')
gr1.switch()
time.sleep(1)



gr1 = greenlet(test1)
gr2 = greenlet(test2)

# 切换到 gr1 中运行
gr1.switch()

"""
test1
test2
test1
test2
test1
test2
...

"""

gevent


  • greenlet 已经实现了协程,但是这个还的人工切换,能够自动切换任务的模块 gevent

  • 原理:当一个 greenlet 遇到 IO(指的是 input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的 greenlet,等到 IO 操作完成,再在适当的时候切换回来继续执行

  • gevent 的使用方法:

    1
    2
    # gevent.spawn 接口使用方法
    gevent.spawn(函数名,传参)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import gevent
import time


def f(n):
for i in range(n):
print(gevent.getcurrent(), i)
# 用来模拟一个耗时操作,注意不是 time 模块中的 sleep
# gevent.sleep(1)主要是把控制权让出去
gevent.sleep(1)


g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()
g2.join()
g3.join()


"""
<Greenlet at 0x198efe56c20: f(5)> 0
<Greenlet at 0x198f1994ae0: f(5)> 0
<Greenlet at 0x198f1994cc0: f(5)> 0
<Greenlet at 0x198efe56c20: f(5)> 1
<Greenlet at 0x198f1994ae0: f(5)> 1
<Greenlet at 0x198f1994cc0: f(5)> 1
<Greenlet at 0x198efe56c20: f(5)> 2
<Greenlet at 0x198f1994ae0: f(5)> 2
<Greenlet at 0x198f1994cc0: f(5)> 2
<Greenlet at 0x198efe56c20: f(5)> 3
<Greenlet at 0x198f1994ae0: f(5)> 3
<Greenlet at 0x198f1994cc0: f(5)> 3
<Greenlet at 0x198efe56c20: f(5)> 4
<Greenlet at 0x198f1994ae0: f(5)> 4
<Greenlet at 0x198f1994cc0: f(5)> 4

"""

给程序打补丁

1
2
3
# 有耗时操作时需要
# 将程序中用到的耗时操作的代码,换为 gevent 中自己实现的模块
from gevent import monkey

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from gevent import monkey
import gevent
import random
import time

# 使用Monkey补丁

# 一定写在最前面,会把耗时的操作替换掉
monkey.patch_all()


def coroutine_work(coroutine_name):
for i in range(10):
print(coroutine_name, i)
time.sleep(random.random())


gevent.joinall([
gevent.spawn(coroutine_work, "work1"),
gevent.spawn(coroutine_work, "work2")
])

"""
work1 0
work2 0
work1 1
work2 1
work1 2
work2 2
work2 3
work1 3
work2 4
work2 5
work1 4
work2 6
work1 5
work2 7
work1 6
work1 7
work2 8
work1 8
work1 9
work2 9

"""

进程、线程与协程的对比:

  1. 进程是资源分配的单位
  2. 线程是操作系统内核调度的基本单位
  3. 进程切换需要的资源很最大,效率很低
  4. 线程切换需要的资源一般,效率一般(当然了在不考虑 GIL 的情况下)
  5. 协程切换任务资源很小,效率高

asyncio


asyncio说明


并行下载网页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from gevent import monkey
import gevent
import urllib.request

# 有耗时操作时需要
monkey.patch_all()


def my_downLoad(url):
print('GET: %s' % url)
resp = urllib.request.urlopen(url)
data = resp.read()
print('%d bytes received from %s.' % (len(data), url))


gevent.joinall([
gevent.spawn(my_downLoad, 'http://www.baidu.com/'),
gevent.spawn(my_downLoad, 'http://www.qq.com/'),
gevent.spawn(my_downLoad, 'http://www.taobao.com/'),
])

"""
GET: http://www.baidu.com/
GET: http://www.qq.com/
GET: http://www.taobao.com/
403527 bytes received from http://www.baidu.com/.
118077 bytes received from http://www.qq.com/.
773511 bytes received from http://www.taobao.com/.

"""