
01 Python进阶(包括pygame的使用) 笔记
项目实战 - 飞机大战
游戏的坐标系
-
原点 在 左上角 (0, 0)
- x 轴 水平方向向 右,逐渐增加
- y 轴 垂直方向向 下,逐渐增加
-
pygame.Rect是一个比较特殊的类,内部只是封装了一些数字计算,用于描述 矩形区域Rect(x, y, width, height) -> Rect1
2
3
4
5
6
7
8
9
10
11
12import 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 | import time |
实现图像绘制
-
图像文件 初始是保存在磁盘上的,如果需要使用,第一步 就需要 被加载到内存
-
要在屏幕上 看到某一个图像的内容,需要按照三个步骤:
-
使用
pygame.image.load()加载图像的数据 -
使用 游戏屏幕 对象,调用
blit方法 将图像绘制到指定位置 -
调用
pygame.display.update()方法更新整个屏幕的显示

-
1 | # 显示屏幕背景图 |
1 | # 绘制英雄图像 |
- 上面两个代码可以合并
游戏循环 和 游戏时钟

游戏循环的作用:
- 保证游戏 不会直接退出
- 变化图像位置 —— 动画效果
- 每隔 1 / 60 秒 移动一下所有图像的位置
- 调用
pygame.display.update()更新屏幕显示
- 检测用户交互 —— 按键、鼠标等…
游戏时钟
-
pygame 专门提供了一个类
pygame.time.Clock可以非常方便的设置屏幕绘制速度 —— 刷新帧率 -
要使用 时钟对象 需要两步:
- 在 游戏初始化 创建一个 时钟对象
- 在 游戏循环 中让时钟对象调用 tick(帧率) 方法
1
2
3
4
5
6
7
8
9
10
11import pygame
# 3. 创建游戏时钟对象
clock = pygame.time.Clock()
i = 0
# 游戏循环
while True:
# 设置屏幕刷新帧率,每秒 60 次
clock.tick(60)
print(i)
i += 1 # 这里就1s钟打印60个数字
英雄的简单动画实现
1 | import pygame |
在游戏循环中 监听 事件
- pygame 中通过
pygame.event.get()可以获得 用户当前所做动作 的 事件列表
1 | import pygame |
精灵 和 精灵组
- 图像加载、位置变化、绘制图像,这些都需要分别处理
- 为了简化开发步骤,pygame 提供了两个类:
pygame.sprite.Sprite—— 存储 图像数据 image 和 位置 rect 的对象pygame.sprite.Group

项目解析

精灵组及精灵 部分代码:
1 | import random |
游戏实际代码:
1 | import pygame |
UDP 与 TCP 通信
路由查看
1 | route 可以查看任何路由 |
内核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 | netstat -an | grep tcp # 查看本地tcp端口情况 |
socket
- 创建 socket
- 函数
socket.socket创建一个 socket,该函数带有两个参数:- Address Family:可以选择
AF_INET(用于 Internet 进程间通信)或者AF_UNIX(用于同一台机器进程间通信),实际工作中常用 AF_INET - Type:套接字类型,可以是
SOCK_STREAM(流式套接字,主要用于TCP协议)或者SOCK_DGRAM(数据报套接字,主要用于 UDP 协议)- TCP 与 UDP 都是全双工通信
- Address Family:可以选择
- 函数
1 | import socket |
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
27from 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
12from 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
- 面向连接
- 可靠传输(超时重传)
- 基于字节流
TCP 与UDP的不同点
- 面向连接(确认有创建三方交握,连接已创建才作为传输)
- 有序数据传输
- 重发丢失的数据包
- 舍弃重复的数据包
- 无差错的数据传输
- 阻塞 / 流量控制

-
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
15import 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
56import 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
34from 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 的三次握手

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

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

TCP 短连接(无状态)
-
模拟一种 TCP 短连接的情况:
- client 向 server 发起连接请求
- server 接到请求,双方建立连接
- client 向 server 发送消息
- server 回应 client
- 一次读写完成,此时双方任何一个都可以发起 close 操作
-
短连接一般只会在 client/server 间传递一次读写操作

TCP 长连接(游戏、即使通信)
- 模拟一种 TCP 长连接的情况:
- client 向 server 发起连接
- server 接到请求,双方建立连接
- client 向 server 发送消息
- server 回应 client
- 一次读写完成,连接不关闭
- 后续读写操作…
- 长时间操作之后 client 发起关闭请求

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

-
分时复用:数据包进行排队,eg:一根网线能够同时上网
-
ARP(地址转换协议):用于获得同一物理网络中的硬件主机地址。是设备通过自己知道的 IP 地址来获得自己不知道的物理地址的协议。(只能在局域网内进行运行的)

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

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

- 版本号:长度 4 bit。一般为 0100(IPv4)、0110(IPv6)
- IP 包头长度:长度为 4 bit。用于描述 IP 包头的长度
- 16位报文总长度:长度 16 比特。 以字节为单位计算的 IP 包的长度(包括头部和数据),所以IP 包最大长度 65535 字节。
-
ICMP:(网络控制消息协议)用于发送报告有关数据包的传送错误的协议。
- Ping 命令的过程实际上就是ICMP协议工作的过程
-
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 字节

-
TCP 和 UDP,一台机器向另外一台机器发送了 3 个包,对方可能收到几个包?
- TCP >=3:3 个到任意多个
- UDP:0 到 3 个
-
C/S 模式 和 B/S 模式
DDos(SYN Flooding)攻击
-
全称是 分布式拒绝服务攻击
-
原理:我们构造网络包,源地址随机构建,意味着当 Server 接到 SYN 包时,应答 ack 和 syn 时不会得到回应。在这种情况下, Server 端,内核就会维持一个很大的队列来管理这些半连接。 当半连接足够多的时候,就会导致新来的正常连接请求得不到响应, 也就是所谓的 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
26import 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
19import 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()
上面这段代码每次只能传/收一句话的原因:
input():标准输入缓存区recv():接收缓冲区
epoll 的接口及使用
1 | import select # 导入 select 模块 |
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
52import 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
48import 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
62import 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
48import 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
64import 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 | from socket import * |
如果程序被 ctrl+c 终止,这个时候端口会处于 TIME_WAIT 状态
如果端口会处于 TIME_WAIT 状态,不能被再次绑定,解决办法:
1 | #重用对应地址和端口 |
写(send) 是会阻塞的
-
写是会阻塞的,当写满就不能再写,缓冲区不够就阻塞
-
对方断开后,怎么办?判断返回值,或者加入 try 异常判断,避免程序崩溃
1
send_size = tcp_client_socket.send(temp_str.encode('utf-8'), MSG_DONTWAIT)
Python 发送整型数,浮点数
1 | import struct |
持续发送多个文件
-
粘包:两次发送的报文挨在一起,分不开
- 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
34import 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
28from 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
107import 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
82import 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
4kill -9 进程名
查看系统的所有信号
kill -l -
将挂起的进程放到后台运行
1
bg 作业ID
-
将后台进程拉到前台
1
fg 作业ID
-
看后台任务
1
jobs
-
让进程后台运行
1
python xxx.py &
-
查看物理内存
1
free
-
杀死进程名为 a 的进程,如果有空格,用 \ 转义
1
2
3
4pkill -f a
eg:
pkill -f python\ xxx.py -
查看磁盘及磁盘分区情况
1
fdisk –l
-
设置当前用户定时任务
1
2
3
4
5crontab –e
vim /etc/crontab # 设置定时任务
crontab -l # 查看当前自己设置的定时任务
进程的创建
multiprocessing模块就是跨平台版本的多进程模块,提供了一个Process类来代表一个进程对象,这个对象可以理解为是一个独立的进程,可以执行另外的事情
1 | from multiprocessing import Process |
- 创建子进程时,只需要传入一个执行函数和函数的参数,创建一个 Process 实例,用
start()方法启动 - 孤儿进程 — 父进程退出(kill 杀死父进程),子进程变为孤儿
- 1 号进程一般是接管孤儿进程的,负责代替之前的父进程来回收PCB
- 僵尸进程 — 子进程退出,父进程在忙碌,没有回收它,要避免僵尸
- Python 进程变为僵尸进程后,名字会改变
- 僵尸进程是 kill 不掉的
获取进程的pid
1 | import os |
-
获取父进程的pid
1
os.getppid()
Process 语法结构
1 | Process(group , target , name , args , kwargs) |
- target:如果传递了函数的引用,可以让这个子进程就执行这里的代码
- args:给 target 指定的函数传递的参数,以元组的方式传递
- kwargs:给 target 指定的函数传递命名参数,keyword 参数
Process 创建的实例对象的常用方法:
1 | start() # 启动子进程实例(创建子进程) |
eg:
1 | from multiprocessing import Process |
进程间对于全局变量的共享
1 | # 子进程的创建是父进程的复制品,资源是独立使用的 |
进程间通信 - Queue
- 使用 multiprocessing 模块的 Queue 实现多进程之间的数据传递,Queue 本身是一个消息列队程序
1 | from multiprocessing import Queue |
- 初始化 Queue() 对象时(例如:q=Queue()),若括号中没有指定最大可接收的消息数量,或数量为负值,那么就代表可接受的消息数量没有上限(直到内存的尽头)
Queue.qsize():返回当前队列包含的消息数量Queue.empty():如果队列为空,返回 True,反之 FalseQueue.full():如果队列满了,返回 True,反之 FalseQueue.get([block[, timeout]]):获取队列中的一条消息,然后将其从列队中移除,block 默认值为 True;
eg:
1 | import time |
进程池 - Pool
- 可以用到 multiprocessing 模块提供的 Pool 方法
join():主进程阻塞,等待子进程的退出, 必须在 close 或 terminate 之后使用
1 | from multiprocessing.pool import Pool |
进程池中的 Queue
- 如果要使用 Pool 创建进程,就需要使用
multiprocessing.Manager()中的Queue(),而不是multiprocessing.Queue(),否则会得到一条如下的错误信息:RuntimeError: Queue objects should only be shared between processes through inheritance.
1 | # 修改 import 中的 Queue 为 Manager |
Pycharm 远程开发
-
步骤一:

-
步骤二:

-
步骤三:

- 这里的同步文件夹需要修改,修改Linux下的目录
多线程
线程
python 的 thread 模块是比较底层的模块,python 的 threading 模块是对 thread 做了一些包装的,可以更加方便的被使用
1 | import threading |
-
查看线程的方法
1
ps -elLf|grep 线程名
-
进程会等待所有的线程结束后,它才结束
-
查看线程的数量
1
length = len(threading.enumerate())
-
一个Python进程中的多个线程是不可以迁移到多个核的,只能在一个核上

面向对象创建线程
1 | import threading |
总结:
- 每个线程默认有一个名字,尽管上面的例子中没有指定线程对象的 name,但是 python 会自动为线程指定一个名字
- 当线程的 run() 方法结束时该线程完成
- 无法控制线程调度程序,但可以通过别的方式来影响线程调度的方式
多线程共享全局变量
1 | import threading |
总结:
- 在一个进程内的所有线程共享全局变量,很方便在多个线程间共享数据
- 缺点就是,线程是对全局变量随意遂改可能造成多线程之间对全局变量的混乱**(即线程非安全)**
- 如果多个线程同时对同一个全局变量操作,会出现资源竞争问题,从而数据结果会不正确
- +1 操作不是原子操作
互斥锁
-
threading 模块中定义了 Lock 类,可以方便的处理锁定:
-
创建锁
1
mutex = threading.Lock()
-
锁定
1
mutex.acquire()
-
释放
1
mutex.release()
-
1 | import threading |
总结:
-
锁的好处:
- 确保了某段关键代码只能由一个线程从头到尾完整地执行
-
锁的坏处:
- 阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
- 由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁
死锁
1 | class MyThread1(threading.Thread): |
- 避免死锁:
- 程序设计时要尽量避免(银行家算法)
- 添加超时时间等
迭代器
-
迭代是访问集合元素的一种方式。迭代器是一个可以记住遍历的位置的对象。迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退
-
可迭代对象
-
把一种数据类型都可以放到 for…in…的语句中,然后让for…in…每次从中取出一条数据供我们使用,即供我们迭代
1
2
3for 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
18In [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
9In [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
46class 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
18def 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 | def gen(): |
协程
- 协程是 python 个中另外一种实现多任务的方式,只不过比线程更小占用更小执行单元(理解为需要的资源)
- 把一个协程 切换到另一个协程。 只要这个过程中保存或恢复 CPU 上下文,那么程序还是可以运行的
- 原理:通过 yield 在用户态保存上下文,不是操作系统所支持的
- 与 进程、线程 相比,协程切换的效率是最低的
1 | import time |
greenlet
-
为了更好使用协程来完成多任务,python 中的 greenlet 模块对其封装,从而使得切换任务变的更加简单
1
2
3
4
5windows
pip install greenlet
Linux
pip3 install greenlet
1 | from greenlet import greenlet |
gevent
-
greenlet 已经实现了协程,但是这个还的人工切换,能够自动切换任务的模块 gevent
-
原理:当一个 greenlet 遇到 IO(指的是 input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的 greenlet,等到 IO 操作完成,再在适当的时候切换回来继续执行
-
gevent 的使用方法:
1
2# gevent.spawn 接口使用方法
gevent.spawn(函数名,传参)
1 | import gevent |
给程序打补丁
1 | # 有耗时操作时需要 |
eg:
1 | from gevent import monkey |
进程、线程与协程的对比:
- 进程是资源分配的单位
- 线程是操作系统内核调度的基本单位
- 进程切换需要的资源很最大,效率很低
- 线程切换需要的资源一般,效率一般(当然了在不考虑 GIL 的情况下)
- 协程切换任务资源很小,效率高
asyncio
并行下载网页
1 | from gevent import monkey |






