0%

抽了半天时间学了一下fastapi,为了方便,代码没分结构。

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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
import sys
import jwt
from pydantic import BaseModel
import uvicorn,asyncio,signal,os
from fastapi import FastAPI, HTTPException, Depends
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from tortoise import fields
from tortoise.models import Model
from tortoise.contrib.pydantic import pydantic_model_creator
from tortoise.contrib.fastapi import register_tortoise
from fastapi import FastAPI, Request
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()
async def startup_event():
print("Application has started.")

async def shutdown_event():
print("Application is shutting down.")

app.add_event_handler("startup", startup_event)
app.add_event_handler("shutdown", shutdown_event)

db_config = {
'connections': {
'default': {
'engine': 'tortoise.backends.mysql',
'credentials': {
'host': 'localhost',
'port': '3306',
'user': 'root',
'password': 'root',
'database': 'fiber'
}
},
},
'apps': {
'models': {
'models': ['main'], # model所在得包位置,
'default_connection': 'default', # 更新为数据库连接名称
}
}
}

register_tortoise(
app,
config=db_config,
generate_schemas=False
)

# JWT验证中间件
async def jwt_middleware(request: Request, call_next):
# 检查是否是登录接口
if request.url.path == "/token":
response = await call_next(request)
return response
http_bearer = HTTPBearer()
credentials: HTTPAuthorizationCredentials = await http_bearer(request)
token = credentials.credentials

# 检查令牌是否存在
if token is None:
raise HTTPException(status_code=401, detail="Token missing")
try:
payload = jwt.decode(token, JWT_SECRET, algorithms=['HS256'])
user = await User.get(id=payload.get('id'))
except Exception:
raise HTTPException(status_code=401, detail="Token invalid")
# 将解析后的payload存储在请求的状态中
request.state.payload = payload
response = await call_next(request)
return response

# 添加JWT验证中间件到应用程序
app.middleware("http")(jwt_middleware)
# 全局中间件,用于处理程序中的异常
@app.exception_handler(Exception)
async def handle_exceptions(request: Request, exc: Exception):
# 在这里可以根据需要进行相应的异常处理逻辑
# 例如记录日志、返回自定义的错误响应等
return {
"code": 500,
"message": "Internal Server Error",
}

# 添加CORS中间件
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)

class User(Model):
id = fields.IntField(pk=True,generated=True)
username = fields.CharField(max_length=50, unique=True)
password = fields.CharField(max_length=128)
def __str__(self):
return self.username
class Meta:
table = 'users'
@classmethod
def get_user(cls, username):
return cls.get(username=username)

# 这里没有真正校验pass
def verify_password(self, password):
return password == self.password

UserPydantic = pydantic_model_creator(User, name='User')
UserInPydantic = pydantic_model_creator(User, name='UserIn',exclude_readonly=True)
# 其实下面的jsonbody体更常用
#定义读取类
class UserInPydantic2(BaseModel):
username: str
password: str

oauth2_sceheme = OAuth2PasswordBearer(tokenUrl="token")
JWT_SECRET = 'myeasypeasyjwtsecret'
# async def get_current_user(token: str = Depends(oauth2_sceheme)):
# try:
# payload = jwt.decode(token, JWT_SECRET, algorithms=['HS256'])
# user = await User.get(id=payload.get('id'))
# except:
# raise HTTPException(
# status_code=401,
# detail='Invalid username or password'
# )
# return await UserPydantic.from_tortoise_orm(user)
async def authenticate_user(username: str, password: str):
user = await User.get(username=username)
if not user:
return False
if not user.verify_password(password):
return False
return user

# @app.post('/token')
# async def generate_token(form_data: OAuth2PasswordRequestForm = Depends()):
# user = await authenticate_user(form_data.username, form_data.password)
# if not user:
# return {"Error": "Invalid username or password"}
# user_obj = await UserPydantic.from_tortoise_orm(user)
# token = jwt.encode(user_obj.dict(), JWT_SECRET)
# return {
# "access_token": token,
# "token_type": "Bearer",
# }

@app.post('/token')
async def generate_token(form_data: UserInPydantic):
user = await authenticate_user(form_data.username, form_data.password)
if not user:
return {"Error": "Invalid username or password"}
user_obj = await UserPydantic.from_tortoise_orm(user)
token = jwt.encode(user_obj.dict(), JWT_SECRET)
return {
"access_token": token,
"token_type": "Bearer",
}

@app.post('/users')
async def create_user(user: UserInPydantic):
try:
user_obj = User(username=user.username, password=user.password) # 使用哈希密码
await user_obj.save()
return await UserPydantic.from_tortoise_orm(user_obj)
except Exception as e:
print(f"An error occurred while saving the user: {e}")
raise HTTPException(status_code=500, detail="Internal server error")

# @app.get('/users/me')
# async def get_user(current_user: UserPydantic = Depends(get_current_user)):
# return current_user
@app.get('/users/me')
async def get_user(request: Request):
return request.state.payload


if __name__ == "__main__":
try:
# 程序主逻辑
uvicorn.run('main:app', host="127.0.0.1", port=8000, reload=True)
except KeyboardInterrupt:
sys.exit(0)
# 执行必要的清理工作
else:
# 程序正常退出
print("Program exited normally")


如果要把路由提出来,常见得方法,新建文件夹api,并添加user.py

1
2
3
4
5
6
from fastapi import APIRouter
api_user = APIRouter()

@api_user.get('/get')
async def get_user(request: Request):
return {"route":"get_user"}

app.py引用

1
2
3
4
....省略
from api.user import api_user
app = FastAPI()
app.include_router(api_user, prefix="/user", tags=["用户接口"])

slice 扩容机制

1
2
3
4
5
6
7
8
9
10
GO1.17版本及之前
当新切片需要的容量cap大于两倍扩容的容量,则直接按照新切片需要的容量扩容;
当原 slice 容量 < 1024 的时候,新 slice 容量变成原来的 2 倍;
当原 slice 容量 > 1024,进入一个循环,每次容量变成原来的1.25倍,直到大于期望容量。

GO1.18之后
当新切片需要的容量cap大于两倍扩容的容量,则直接按照新切片需要的容量扩容;
threshold 为 256
当原 slice 容量 < threshold 的时候,新 slice 容量变成原来的 2 倍;
当原 slice 容量 > threshold,进入一个循环,每次容量增加(旧容量+3*threshold)/4。

map 底层原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
map 是一个指针 占用8个字节(64位计算机),指向hmap结构体,hmap包含多个bmap数组(桶) 
type hmap struct {
count int //元素个数,调用len(map)时直接返回
flags uint8 //标志map当前状态,正在删除元素、添加元素.....
B uint8 //单元(buckets)的对数 B=5表示能容纳32个元素 B随着map容量增大而变大
noverflow uint16 //单元(buckets)溢出数量,如果一个单元能存8个key,此时存储了9个,溢出了,就需要再增加一个单元
hash0 uint32 //哈希种子
buckets unsafe.Pointer //指向单元(buckets)数组,大小为2^B,可以为nil
oldbuckets unsafe.Pointer //扩容的时候,buckets长度会是oldbuckets的两倍
nevacute uintptr //指示扩容进度,小于此buckets迁移完成
extra *mapextra //与gc相关 可选字段
}

type bmap struct {
tophash [bucketCnt]uint8
}
//实际上编译期间会生成一个新的数据结构
type bmap struct {
topbits [8]uint8 //key hash值前8位 用于快速定位keys的位置
keys [8]keytype //键
values [8]valuetype //值
pad uintptr
overflow uintptr //指向溢出桶 无符号整形 优化GC
}

map 扩容机制

1
2
3
4
5
6
7
8
9
10
11
扩容时机:向 map 插入新 key 的时候,会进行条件检测,符合下面这 2 个条件,就会触发扩容
扩容条件:
1.超过负载 map元素个数 > 6.5(负载因子) * 桶个数
2.溢出桶太多
当桶总数<2^15时,如果溢出桶总数>=桶总数,则认为溢出桶过多
当桶总数>2^15时,如果溢出桶总数>=2^15,则认为溢出桶过多
扩容机制:
双倍扩容:针对条件1,新建一个buckets数组,新的buckets大小是原来的2倍,然后旧buckets数据搬迁到新的buckets。
等量扩容:针对条件2,并不扩大容量,buckets数量维持不变,重新做一遍类似双倍扩容的搬迁动作,把松散的键值对重新排列一次,使得同一个 bucket 中的 key 排列地更紧密,节省空间,提高 bucket 利用率,进而保证更快的存取。
渐进式扩容:
插入修改删除key的时候,都会尝试进行搬迁桶的工作,每次都会检查oldbucket是否nil,如果不是nil则每次搬迁2个桶,蚂蚁搬家一样渐进式扩容

map 遍历为什么无序

1
map每次遍历,都会从一个随机值序号的桶,再从其中随机的cell开始遍历,并且扩容后,原来桶中的key会落到其他桶中,本身就会造成失序.

map 如何查找

1
2
3
4
5
6
7
8
9
10
11
1.写保护机制
先查hmap的标志位flags,如果flags写标志位此时是1,说明其他协程正在写操作,直接panic
2.计算hash值
key经过哈希函数计算后,得到64bit(64位CPU)
10010111 | 101011101010110101010101101010101010 | 10010
3.找到hash对应的桶
上面64位后5(hmap的B值)位定位所存放的桶
如果当前正在扩容中,并且定位到旧桶数据还未完成迁移,则使用旧的桶
4.遍历桶查找
上面64位前8位用来在tophash数组查找快速判断key是否在当前的桶中,如果不在需要去溢出桶查找
5.返回key对应的指针

map 冲突解决方式

1
GO采用链地址法解决冲突,具体就是插入key到map中时,当key定位的桶填满8个元素后,将会创建一个溢出桶,并且将溢出桶插入当前桶的所在链表尾部

map 负载因子为什么是 6.5

1
2
3
4
5
负载因子 = 哈希表存储的元素个数 / 桶个数
Go 官方发现:装载因子越大,填入的元素越多,空间利用率就越高,但发生哈希冲突的几率就变大。
装载因子越小,填入的元素越少,冲突发生的几率减小,但空间浪费也会变得更多,而且还会提高扩容操作的次数
Go 官方取了一个相对适中的值,把 Go 中的 map 的负载因子硬编码为 6.5,这就是 6.5 的选择缘由。
这意味着在 Go 语言中,当 map存储的元素个数大于或等于 6.5 * 桶个数 时,就会触发扩容行为。

Map 和 Sync.Map 哪个性能好

1
2
3
4
5
6
7
8
9
10
11
12
type Map struct {
mu Mutex
read atomic.Value
dirty map[interface()]*entry
misses int
}
对比原始map:
和原始map+RWLock的实现并发的方式相比,减少了加锁对性能的影响。它做了一些优化:可以无锁访问read map,而且会优先操作read map,倘若只操作read map就可以满足要求,那就不用去操作write map(dirty),所以在某些特定场景中它发生锁竞争的频率会远远小于map+RWLock的实现方式
优点:
适合读多写少的场景
缺点:
写多的场景,会导致 read map 缓存失效,需要加锁,冲突变多,性能急剧下降

Channel 底层实现原理

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
通过var声明或者make函数创建的channel变量是一个存储在函数栈帧上的指针,占用8个字节,指向堆上的hchan结构体
type hchan struct {
closed uint32 // channel是否关闭的标志
elemtype *_type // channel中的元素类型
// channel分为无缓冲和有缓冲两种。
// 对于有缓冲的channel存储数据,使用了 ring buffer(环形缓冲区) 来缓存写入的数据,本质是循环数组
// 为啥是循环数组?普通数组不行吗,普通数组容量固定更适合指定的空间,弹出元素时,普通数组需要全部都前移
// 当下标超过数组容量后会回到第一个位置,所以需要有两个字段记录当前读和写的下标位置
buf unsafe.Pointer // 指向底层循环数组的指针(环形缓冲区)
qcount uint // 循环数组中的元素数量
dataqsiz uint // 循环数组的长度
elemsize uint16 // 元素的大小
sendx uint // 下一次写下标的位置
recvx uint // 下一次读下标的位置
// 尝试读取channel或向channel写入数据而被阻塞的goroutine
recvq waitq // 读等待队列
sendq waitq // 写等待队列
lock mutex //互斥锁,保证读写channel时不存在并发竞争问题
}
等待队列:
双向链表,包含一个头结点和一个尾结点
每个节点是一个sudog结构体变量,记录哪个协程在等待,等待的是哪个channel,等待发送/接收的数据在哪里
type waitq struct {
first *sudog
last *sudog
}

type sudog struct {
g *g
next *sudog
prev *sudog
elem unsafe.Pointer
c *hchan
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
创建时:
创建时会做一些检查:
- 元素大小不能超过 64K
- 元素的对齐大小不能超过 maxAlign 也就是 8 字节
- 计算出来的内存是否超过限制

创建时的策略:
- 如果是无缓冲的 channel,会直接给 hchan 分配内存
- 如果是有缓冲的 channel,并且元素不包含指针,那么会为 hchan 和底层数组分配一段连续的地址
- 如果是有缓冲的 channel,并且元素包含指针,那么会为 hchan 和底层数组分别分配地址
发送时:
- 如果 channel 的读等待队列存在接收者goroutine
- 将数据**直接发送**给第一个等待的 goroutine, **唤醒接收的 goroutine**
- 如果 channel 的读等待队列不存在接收者goroutine
- 如果循环数组buf未满,那么将会把数据发送到循环数组buf的队尾
- 如果循环数组buf已满,这个时候就会走阻塞发送的流程,将当前 goroutine 加入写等待队列,并**挂起等待唤醒**
接收时:
- 如果 channel 的写等待队列存在发送者goroutine
- 如果是无缓冲 channel,**直接**从第一个发送者goroutine那里把数据拷贝给接收变量,**唤醒发送的 goroutine**
- 如果是有缓冲 channel(已满),将循环数组buf的队首元素拷贝给接收变量,将第一个发送者goroutine的数据拷贝到 buf循环数组队尾,**唤醒发送的 goroutine**
- 如果 channel 的写等待队列不存在发送者goroutine
- 如果循环数组buf非空,将循环数组buf的队首元素拷贝给接收变量
- 如果循环数组buf为空,这个时候就会走阻塞接收的流程,将当前 goroutine 加入读等待队列,并**挂起等待唤醒**

Channel 有什么特点

1
2
3
4
5
channel有2种类型:无缓冲、有缓冲
channel有3种模式:写操作模式(单向通道)、读操作模式(单向通道)、读写操作模式(双向通道)
写操作模式 make(chan<- int)
读操作模式 make(<-chan int)
读写操作模式 make(chan int)
1
2
3
4
5
6
7
8
9
10
channel 有 3 种状态:未初始化、正常、关闭

操作 未初始化 关闭 正常
关闭 panic panic 正常
发送 永远阻塞导致死锁 panic 阻塞或者成功发送
接收 永远阻塞导致死锁 缓冲区为空则为零值,否则可以继续读 阻塞或者成功接收
注意点:
一个 channel不能多次关闭,会导致painc
如果多个 goroutine 都监听同一个 channel,那么 channel 上的数据都可能随机被某一个 goroutine 取走进行消费
如果多个 goroutine 监听同一个 channel,如果这个 channel 被关闭,则所有 goroutine 都能收到退出信号

Channel 为什么是线程安全的

1
2
不同协程通过channel进行通信,本身的使用场景就是多线程,为了保证数据的一致性,必须实现线程安全
channel的底层实现中,hchan结构体中采用Mutex锁来保证数据读写安全。在对循环数组buf中的数据进行入队和出队操作时,必须先获取互斥锁,才能操作channel数据

Channel 发送和接收什么情况下会死锁

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
func deadlock1() {    //无缓冲channel只写不读
ch := make(chan int)
ch <- 3 // 这里会发生一直阻塞的情况,执行不到下面一句
}
func deadlock2() { //无缓冲channel读在写后面
ch := make(chan int)
ch <- 3 // 这里会发生一直阻塞的情况,执行不到下面一句
num := <-ch
fmt.Println("num=", num)
}
func deadlock3() { //无缓冲channel读在写后面
ch := make(chan int)
ch <- 100 // 这里会发生一直阻塞的情况,执行不到下面一句
go func() {
num := <-ch
fmt.Println("num=", num)
}()
time.Sleep(time.Second)
}
func deadlock3() { //有缓冲channel写入超过缓冲区数量
ch := make(chan int, 3)
ch <- 3
ch <- 4
ch <- 5
ch <- 6 // 这里会发生一直阻塞的情况
}
func deadlock4() { //空读
ch := make(chan int)
// ch := make(chan int, 1)
fmt.Println(<-ch) // 这里会发生一直阻塞的情况
}
func deadlock5() { //互相等对方造成死锁
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
for {
select {
case num := <-ch1:
fmt.Println("num=", num)
ch2 <- 100
}
}
}()
for {
select {
case num := <-ch2:
fmt.Println("num=", num)
ch1 <- 300
}
}
}

互斥锁实现原理

1
2
3
4
5
Go sync包提供了两种锁类型:互斥锁sync.Mutex 和 读写互斥锁sync.RWMutex,都属于悲观锁。
锁的实现一般会依赖于原子操作、信号量,通过atomic 包中的一些原子操作来实现锁的锁定,通过信号量来实现线程的阻塞与唤醒

在正常模式下,锁的等待者会按照先进先出的顺序获取锁。但是刚被唤起的 Goroutine 与新创建的 Goroutine 竞争时,大概率会获取不到锁,在这种情况下,这个被唤醒的 Goroutine 会加入到等待队列的前面。 如果一个等待的 Goroutine 超过1ms 没有获取锁,那么它将会把锁转变为饥饿模式。
Go在1.9中引入优化,目的保证互斥锁的公平性。在饥饿模式中,互斥锁会直接交给等待队列最前面的 Goroutine。新的 Goroutine 在该状态下不能获取锁、也不会进入自旋状态,它们只会在队列的末尾等待。如果一个 Goroutine 获得了互斥锁并且它在队列的末尾或者它等待的时间少于 1ms,那么当前的互斥锁就会切换回正常模式。

互斥锁允许自旋的条件?

1
2
3
4
5
6
7
8
9
10
线程没有获取到锁时常见有2种处理方式:
- 一种是没有获取到锁的线程就一直循环等待判断该资源是否已经释放锁,这种锁也叫做自旋锁,它不用将线程阻塞起来, 适用于并发低且程序执行时间短的场景,缺点是cpu占用较高
- 另外一种处理方式就是把自己阻塞起来,会释放CPU给其他线程,内核会将线程置为「睡眠」状态,等到锁被释放后,内核会在合适的时机唤醒该线程,适用于高并发场景,缺点是有线程上下文切换的开销
Go语言中的Mutex实现了自旋与阻塞两种场景,当满足不了自旋条件时,就会进入阻塞
**允许自旋的条件:**
1. 锁已被占用,并且锁不处于饥饿模式。
2. 积累的自旋次数小于最大自旋次数(active_spin=4)。
3. cpu 核数大于 1。
4. 有空闲的 P。
5. 当前 goroutine 所挂载的 P 下,本地待运行队列为空。

读写锁实现原理

1
2
3
4
5
读写锁的底层是基于互斥锁实现的。
写锁需要阻塞写锁:一个协程拥有写锁时,其他协程写锁定需要阻塞;
写锁需要阻塞读锁:一个协程拥有写锁时,其他协程读锁定需要阻塞;
读锁需要阻塞写锁:一个协程拥有读锁时,其他协程写锁定需要阻塞;
读锁不能阻塞读锁:一个协程拥有读锁时,其他协程也可以拥有读锁。

原子操作有哪些

1
2
3
4
5
6
7
8
9
Go atomic包是最轻量级的锁(也称无锁结构),可以在不形成临界区和创建互斥量的情况下完成并发安全的值替换操作,不过这个包只支持int32/int64/uint32/uint64/uintptr这几种数据类型的一些基础操作(增减、交换、载入、存储等)
当我们想要对**某个变量**并发安全的修改,除了使用官方提供的 `mutex`,还可以使用 sync/atomic 包的原子操作,它能够保证对变量的读取或修改期间不被其他的协程所影响。
atomic 包提供的原子操作能够确保任一时刻只有一个goroutine对变量进行操作,善用 atomic 能够避免程序中出现大量的锁操作。
**常见操作:**
- 增减Add AddInt32 AddInt64 AddUint32 AddUint64 AddUintptr
- 载入Load LoadInt32 LoadInt64 LoadPointer LoadUint32 LoadUint64 LoadUintptr
- 比较并交换CompareAndSwap CompareAndSwapInt32...
- 交换Swap SwapInt32...
- 存储Store StoreInt32...

原子操作和锁的区别

1
2
3
4
5
原子操作由底层硬件支持,而锁是基于原子操作+信号量完成的。若实现相同的功能,前者通常会更有效率
原子操作是单个指令的互斥操作;互斥锁/读写锁是一种数据结构,可以完成临界区(多个指令)的互斥操作,扩大原子操作的范围
原子操作是无锁操作,属于乐观锁;说起锁的时候,一般属于悲观锁
原子操作存在于各个指令/语言层级,比如“机器指令层级的原子操作”,“汇编指令层级的原子操作”,“Go语言层级的原子操作”等。
锁也存在于各个指令/语言层级中,比如“机器指令层级的锁”,“汇编指令层级的锁”,“Go语言层级的锁”等

goroutine 的底层实现原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
g本质是一个数据结构,真正让 goroutine 运行起来的是调度器
type g struct {
goid int64 // 唯一的goroutine的ID
sched gobuf // goroutine切换时,用于保存g的上下文
stack stack // 栈
gopc // pc of go statement that created this goroutine
startpc uintptr // pc of goroutine function ...
}
type gobuf struct { //运行时寄存器
sp uintptr // 栈指针位置
pc uintptr // 运行到的程序位置
g guintptr // 指向 goroutine
ret uintptr // 保存系统调用的返回值 ...
}
type stack struct { //运行时栈
lo uintptr // 栈的下界内存地址
hi uintptr // 栈的上界内存地址
}

goroutine 和线程的区别

1
2
3
4
5
6
内存占用:
创建一个 goroutine 的栈内存消耗为 2 KB,实际运行过程中,如果栈空间不够用,会自动进行扩容。创建一个 thread 则需要消耗 1 MB 栈内存。
创建和销毀:
Thread 创建和销毀需要陷入内核,系统调用。而 goroutine 因为是由 Go runtime 负责管理的,创建和销毁的消耗非常小,是用户级。
切换:
当 threads 切换时,需要保存各种寄存器,而 goroutines 切换只需保存三个寄存器:Program Counter, Stack Pointer and BP。一般而言,线程切换会消耗 1000-1500 ns,Goroutine 的切换约为 200 ns,因此,goroutines 切换成本比 threads 要小得多。

goroutine 泄露场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
泄露原因
Goroutine 内进行channel/mutex 等读写操作被一直阻塞。
Goroutine 内的业务逻辑进入死循环,资源一直无法释放。
Goroutine 内的业务逻辑进入长时间等待,有不断新增的 Goroutine 进入等待

泄露场景
channel 如果忘记初始化,那么无论你是读,还是写操作,都会造成阻塞。
channel 发送数量 超过 channel接收数量,就会造成阻塞
channel 接收数量 超过 channel发送数量,也会造成阻塞
http request body未关闭,goroutine不会退出
互斥锁忘记解锁
sync.WaitGroup使用不当

如何排查
单个函数:调用 `runtime.NumGoroutine` 方法来打印 执行代码前后Goroutine 的运行数量,进行前后比较,就能知道有没有泄露了。
生产/测试环境:使用`PProf`实时监测Goroutine的数量

如何查看正在运行的 goroutine 数量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"net/http"
_ "net/http/pprof"
)

func main() {
for i := 0; i < 100; i++ {
go func() {
select {}
}()
}
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
select {}
}
执行程序之后,命令运行以下命令,会自动打开浏览器显示一系列目前还看不懂的图,提示Could not execute dot; may need to install graphviz.则需要安装graphviz,需要python环境
go tool pprof -http=:1248 http://127.0.0.1:6060/debug/pprof/goroutine

如何控制并发的 goroutine 数量?

1
2
3
4
在开发过程中,如果不对goroutine加以控制而进行滥用的话,可能会导致服务整体崩溃。比如耗尽系统资源导致程序崩溃,或者CPU使用率过高导致系统忙不过来。
解决方案:
有缓冲channel:利用缓冲满时发送阻塞的特性
无缓冲channel:任务发送和执行分离,指定消费者并发协程数

GMP 和 GM 模型

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
G:Goroutine
M: 线程
P: Processor 本地队列
GMP模型:
P的数量:
由启动时环境变量`$GOMAXPROCS`或者是由`runtime`的方法`GOMAXPROCS()`决定
M的数量:
go语言本身的限制:go程序启动时,会设置M的最大数量,默认10000.但是内核很难支持这么多的线程数
runtime/proc中的sched.maxmcount,设置M的最大数量
一个M阻塞了,会创建新的M。

P何时创建:在确定了P的最大数量n后,运行时系统会根据这个数量创建n个P。
M何时创建:没有足够的M来关联P并运行其中的可运行的G。比如所有的M此时都阻塞住了,而P中还有很多就绪任务,就会去寻找空闲的M,而没有空闲的,就会去创建新的M。

全场景解析:
1.P拥有G1,M1获取P后开始运行G1,G1创建了G2,为了局部性G2优先加入到P1的本地队列。
2.G1运行完成后,M上运行的goroutine切换为G0,G0负责调度时协程的切换。从P的本地队列取G2,从G0切换到G2,并开始运行G2。实现了线程M1的复用。
3.假设每个P的本地队列只能存4个G。G2要创建了6个G,前4个G(G3, G4, G5, G6)已经加入p1的本地队列,p1本地队列满了。
4.G2在创建G7的时候,发现P1的本地队列已满,需要执行负载均衡(把P1中本地队列中前一半的G,还有新创建G转移到全局队列),这些G被转移到全局队列时,会被打乱顺序
5.G2创建G8时,P1的本地队列未满,所以G8会被加入到P1的本地队列。
6.在创建G时,运行的G会尝试唤醒其他空闲的P和M组合去执行。假定G2唤醒了M2,M2绑定了P2,并运行G0,但P2本地队列没有G,M2此时为自旋线程
7.M2尝试从全局队列取一批G放到P2的本地队列,至少从全局队列取1个g,但每次不要从全局队列移动太多的g到p本地队列,给其他p留点。
8.假设G2一直在M1上运行,经过2轮后,M2已经把G7、G4从全局队列获取到了P2的本地队列并完成运行,全局队列和P2的本地队列都空了,那m就要执行work stealing(偷取):从其他有G的P哪里偷取一半G过来,放到自己的P本地队列。P2从P1的本地队列尾部取一半的G
9.G1本地队列G5、G6已经被其他M偷走并运行完成,当前M1和M2分别在运行G2和G8,M3和M4没有goroutine可以运行,M3和M4处于自旋状态,它们不断寻找goroutine。系统中最多有GOMAXPROCS个自旋的线程,多余的没事做线程会让他们休眠。
10.假定当前除了M3和M4为自旋线程,还有M5和M6为空闲的线程,G8创建了G9,G8进行了阻塞的系统调用,M2和P2立即解绑,P2会执行以下判断:如果P2本地队列有G、全局队列有G或有空闲的M,P2都会立马唤醒1个M和它绑定,否则P2则会加入到空闲P列表,等待M来获取可用的p。
11.G8创建了G9,假如G8进行了非阻塞系统调用。M2和P2会解绑,但M2会记住P2,然后G8和M2进入系统调用状态。当G8和M2退出系统调用时,会尝试获取P2,如果无法获取,则获取空闲的P,如果依然没有,G8会被记为可运行状态,并加入到全局队列,M2因为没有P的绑定而变成休眠状态

work stealing 机制?

1
2
3
4
5
当线程M⽆可运⾏的G时,尝试从其他M绑定的P偷取G,减少空转,提高了线程利用率(避免闲着不干活)。
当从本线程绑定 P 本地 队列、全局G队列、netpoller都找不到可执行的 g,会从别的 P 里窃取G并放到当前P上面。
从netpoller 中拿到的G是_Gwaiting状态( 存放的是因为网络IO被阻塞的G),从其它地方拿到的G是_Grunnable状态
从全局队列取的G数量:N = min(len(GRQ)/GOMAXPROCS + 1, len(GRQ/2)) (根据GOMAXPROCS负载均衡)
从其它P本地队列窃取的G数量:N = len(LRQ)/2(平分)

GC 如何调优

1
2
3
4
5
6
1.控制内存分配的速度,限制 Goroutine 的数量,提高赋值器 mutator 的 CPU 利用率(降低GC的CPU利用率)
2.少使用+连接string
3.slice提前分配足够的内存来降低扩容带来的拷贝
4.避免map key对象过多,导致扫描时间增加
5.变量复用,减少对象分配,例如使用 sync.Pool 来复用需要频繁创建临时对象、使用全局变量等
6.增大 GOGC 的值,降低 GC 的运行频率 (不太用这个)

分表分库,不使用第三方中间件的话,自己根据分库分表的逻辑进行重写表名、库名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use Illuminate\Support\Str;
class Item extends Model {

public $uid;
//设置用户id,根据用户id进行取模(测试而已,正常用户信息可以放到token里,这样全局公用)
public function setUid($uid)
{
$this->uid = $uid;
}
//重写表名
public function getTable()
{
$tableName = str_replace('\\', '', Str::snake(Str::pluralStudly(class_basename($this)));
//假设当前道具表分成了10个
return $tableName.'_'.($this->uid % 10);
//return 'item_'.($this->uid % 10);
}
//重写数据库连接,提前配置好的,具体看自己的业务,不写就是默认连接
public function getConnection()
{
return "default";
}
}

1
2
composer dump-autoload --optimize
composer install --no-dev --prefer-dist --prefer-stable

这两个命令是在使用Composer时常用的,Composer是PHP的依赖管理工具。下面是这两个命令的解释:

1
2
3
4
5
6
7
8
9
10
1. `composer dump-autoload --optimize`:
- `composer dump-autoload`:这个命令会重新生成Composer的自动加载映射。在Laravel等PHP项目中,当你安装或更新依赖时,
Composer会自动创建或更新一个`autoload.php`文件,以及一个`vendor/composer`目录,这些文件和目录包含了类和接口的自动加载信息。
- `--optimize`:这个选项会优化自动加载的生成过程,减少自动加载文件的数量,从而加快自动加载的速度。这在生产环境中特别有用,因为它可以提高应用程序的启动速度。
2. `composer install --no-dev --prefer-dist --prefer-stable`:
- `composer install`:这个命令会根据`composer.json`文件中定义的依赖,安装所需的库。
- `--no-dev`:这个选项指示Composer只安装运行应用程序所需的依赖,而不包括开发时使用的依赖(如测试框架、代码分析工具等)。
这通常用于生产环境,因为开发依赖在生产环境中不需要。
- `--prefer-dist`:这个选项告诉Composer优先从远程仓库下载压缩包("dist"),而不是克隆整个源代码仓库。这可以加快安装速度,并且减少磁盘空间的使用。
- `--prefer-stable`:这个选项让Composer在安装依赖时优先选择稳定的版本,而不是预发布或开发中的版本。这有助于确保生产环境中的稳定性。

第一个命令用于优化自动加载过程,而第二个命令用于在生产环境中快速、稳定地安装项目依赖。

main.go

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
package main

import (
"net/rpc"
"net"
"log"
"net/rpc/jsonrpc"
)

//自己的数据类
type MyMath struct{

}

//加法--只能两个参数
func (mm *MyMath) Add(num map[string]float64,reply *float64) error {
*reply = num["num1"] + num["num2"]
return nil
}

//减法--只能两个参数
func (mm *MyMath) Sub(num map[string]string,reply *string) error {
*reply = num["num1"] + num["num2"]
return nil
}

func main() {
//注册MyMath类,以代客户端调用
rpc.Register(new(MyMath))
listener, err := net.Listen("tcp", ":1234")
if err != nil {
log.Fatal("listen error:", err)
}
for {
conn, err := listener.Accept()
if err != nil {
continue
}
//新协程来处理--json
go jsonrpc.ServeConn(conn)
}
}

index.php

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
<?php
class JsonRPC
{
private $conn;

function __construct($host, $port) {
$this->conn = fsockopen($host, $port, $errno, $errstr, 3);
if (!$this->conn) {
return false;
}
}

public function Call($method, $params) {
if ( !$this->conn ) {
return false;
}
$err = fwrite($this->conn, json_encode(array(
'method' => $method,
'params' => array($params),
'id' => 0,
))."\n");
if ($err === false)
return false;
stream_set_timeout($this->conn, 0, 3000);
$line = fgets($this->conn);
if ($line === false) {
return NULL;
}
return json_decode($line,true);
}
}

$client = new JsonRPC("127.0.0.1", 1234);
$r = $client->Call("MyMath.Add",array('num1'=>1,'num2'=>2));
var_export($r);
echo "<br/>";
$r = $client->Call("MyMath.Sub",array('num1'=>'1','num2'=>'2'));
var_dump($r);

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
<?php
// 框架核心应用层
$application = function($name) {
echo "this is a {$name} application\n";
};

// 前置校验中间件
$auth = function($handler) {
return function($name) use ($handler) {
echo "{$name} need a auth middleware\n";
return $handler($name);
};
};

// 前置过滤中间件
$filter = function($handler) {
return function($name) use ($handler) {
echo "{$name} need a filter middleware\n";
return $handler($name);
};
};

// 后置日志中间件
$log = function($handler) {
return function($name) use ($handler) {
$return = $handler($name);
echo "{$name} need a log middleware\n";
return $return;
};
};

// 中间件栈
$stack = [];

// 打包
function pack_middleware($handler, $stack)
{
foreach (array_reverse($stack) as $key => $middleware)
{
$handler = $middleware($handler);
}
return $handler;
}

// 注册中间件
// 这里用的都是全局中间件,实际应用时还可以为指定路由注册局部中间件
$stack['log'] = $log;
$stack['filter'] = $filter;
$stack['auth'] = $auth;

$run = pack_middleware($application, $stack);
$run('do');

输出

1
2
3
4
Laravle need a filter middleware
Laravle need a auth middleware
this is a Laravle application
Laravle need a log middleware

中间件的执行顺序是由打包函数(pack_middleware)决定,这里返回的闭包实际上相当于:

1
2
$run = $log($filter($auth($application)));
$run('do');
1
2
3
4
5
6
7
8
9
Laravel 框架中间件使用 array_reverse 的原因是为了实现中间件的逆序执行。‌

在 Laravel 框架中,‌中间件是通过管道流(‌Pipeline)‌的方式执行的,‌其中 Illuminate\Pipeline\Pipeline
类负责协调中间件的执行顺序。‌中间件的执行顺序对于应用程序的处理流程至关重要,‌而 Laravel 通过 array_reverse 函数来实现中间件的逆序执行,‌
确保了中间件的处理逻辑能够按照预期的方式工作。‌
具体来说,‌array_reverse 函数用于将数组反转,‌使得数组的元素按照相反的顺序排列。‌在 Laravel 的管道流实现中,‌中间件数组通过 array_reverse
函数进行反转后,‌再通过 array_reduce 函数进行处理。‌这样做的好处是,‌当闭包函数(‌即管道流的下一步)‌被调用时,‌它能够按照从内到外的顺序执行中间件,‌
形成了一个类似洋葱的结构,‌其中最内层的中间件首先被执行,‌最外层的中间件最后被执行。‌这种执行顺序确保了中间件的处理逻辑能够按照预期的方式工作,‌
从而实现了 Laravel 框架中间件的功能和性能优化。‌
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
<?php
interface Middleware
{
public static function handle(Closure $next);
}

class Middleware1 implements Middleware
{
public static function handle(Closure $next)
{
echo "Middleware1 before\n";
$next();
echo "Middleware1 after\n";
}
}

class Middleware2 implements Middleware
{
public static function handle(Closure $next)
{
echo "Middleware2 before\n";
$next();
echo "Middleware2 after\n";
}
}

class Middleware3 implements Middleware
{
public static function handle(Closure $next)
{
echo "Middleware3 before\n";
$next();
echo "Middleware3 after\n";
}
}

function getSlice() // 返回一个函数,与上文的f一致
{
return function ($stack, $pipe) {
return function () use ($stack, $pipe) {
return $pipe::handle($stack);
};
};
}

function then()
{
$pipes = [
"Middleware1",
"Middleware2",
"Middleware3",
];

$firstSlice = function () { // 上文的目标函数 target
echo "请求向路由器传递,返回响应.\n";
};
//嵌套闭包的解包方式是先从外到内,然后从内回归到外面,所以要根据注册顺序进行逆序
$pipes = array_reverse($pipes);
$closure = array_reduce($pipes, getSlice(), $firstSlice);
var_dump($closure);//打印下该闭包
// 因为最终返回了一个函数,所以需要call_user_func
call_user_func(array_reduce($pipes, getSlice(), $firstSlice));
}
then();
echo "\n";
//array_reduce($pipes, getSlice(), $firstSlice),执行完成后相当于生成了f嵌套闭包
//function f()
//{
// return Middleware1::handle(
// function () {
// return Middleware2::handle(
// function () {
// return Middleware3::handle(function () {
// echo "请求向路由器传递,返回响应.\n";
// });
// }
// );
// }
// );
//}
//call_user_func('f');

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
<?php

class Subject implements SplSubject {
private $observers = [];
private $state;

public function attach(SplObserver $observer) {
$this->observers[] = $observer;
}

public function detach(SplObserver $observer) {
foreach ($this->observers as $key => $obs) {
if ($obs === $observer) {
unset($this->observers[$key]);
}
}
}

public function notify() {
foreach ($this->observers as $observer) {
$observer->update($this);
}
}

public function setState($state) {
$this->state = $state;
$this->notify();
}

public function getState() {
return $this->state;
}
}

class Observer implements SplObserver {
private $name;

public function __construct($name) {
$this->name = $name;
}

public function update(SplSubject $subject) {
echo "观察者==》{$this->name} 开始执行事件: " . $subject->getState() . "\n";
}
}

// 创建主题
$subject = new Subject();

// 创建观察者
$observer1 = new Observer("观察者 1");
$observer2 = new Observer("观察者 2");

// 将观察者添加到主题
$subject->attach($observer1);
$subject->attach($observer2);

// 改变状态
$subject->setState("事件");

基础版本

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
<?php

/**
* 观察者模式
*/

/**
* 抽象主题角色
*/
interface Subject {

/**
* 增加一个新的观察者对象
* @param Observer $observer
*/
public function attach(Observer $observer);

/**
* 删除一个已注册过的观察者对象
* @param Observer $observer
*/
public function detach(Observer $observer);

/**
* 通知所有注册过的观察者对象
*/
public function notifyObservers();
}

/**
* 具体主题角色
*/
class ConcreteSubject implements Subject {

private $_observers;

public function __construct() {
$this->_observers = array();
}

/**
* 增加一个新的观察者对象
* @param Observer $observer
*/
public function attach(Observer $observer) {
return array_push($this->_observers, $observer);
}

/**
* 删除一个已注册过的观察者对象
* @param Observer $observer
*/
public function detach(Observer $observer) {
$index = array_search($observer, $this->_observers);
if ($index === FALSE || ! array_key_exists($index, $this->_observers)) {
return FALSE;
}

unset($this->_observers[$index]);
return TRUE;
}

/**
* 通知所有注册过的观察者对象
*/
public function notifyObservers() {
if (!is_array($this->_observers)) {
return FALSE;
}

foreach ($this->_observers as $observer) {
$observer->update();
}

return TRUE;
}

}

/**
* 抽象观察者角色
*/
interface Observer {

/**
* 更新方法
*/
public function update();
}

class ConcreteObserver implements Observer {

/**
* 观察者的名称
* @var <type>
*/
private $_name;

public function __construct($name) {
$this->_name = $name;
}

/**
* 更新方法
*/
public function update() {
echo 'Observer', $this->_name, ' has notified.<br />';
}

}
//实例化类:
$subject = new ConcreteSubject();

/* 添加第一个观察者 */
$observer1 = new ConcreteObserver('aaa');
$subject->attach($observer1);
$subject->notifyObservers();

/* 添加第二个观察者 */
$observer2 = new ConcreteObserver('bbb');
$subject->attach($observer2);
$subject->notifyObservers();

/* 删除第一个观察者 */
$subject->detach($observer1);
$subject->notifyObservers();

工作原理就是把你写好的 php 代码编译成 c,然后你可以将其以扩展.so的形式添加到 ‘php.ini’ 文件中。功能稍微少一点,适合简单场景

安装解释器 https://github.com/zephir-lang/php-zephir-parser

1
2
3
4
5
6
7
8
9
git clone https://github.com/zephir-lang/php-zephir-parser.git
cd php-zephir-parser
phpize
./configure
make && make install
编辑php.ini
vim /usr/local/php/etc/php.ini
[Zephir Parser]
extension=zephir_parser.so

或者直接一键安装 pecl install zephir_parser

安装zephir.phar

1
2
3
4
5
6
wget https://github.com/zephir-lang/zephir/releases/download/0.17.0/zephir.phar

# 移动到 bin
mv zephir.phar /usr/bin/
chmod 755 zephir.phar
ln -s /usr/bin/zephir.phar zephir

验证是否安装正确:

1
zephir help

开始编写代码

1
zephir init utils

执行之后,一个目录称为“utils”创建在当前工作目录:

1
2
3
$ cd utils
$ ls
ext/ utils/ config.json

utils/utils/greeting.zep

1
2
3
4
5
6
7
8
9
10
11
namespace Utils;

class Greeting
{

public static function say()
{
echo "hello world!";
}

}

现在,我们需要告诉Zephir编译和生成的扩展,必须在代码根目录,也就是utils/utils目录下:

1
zephir build

如果一切顺利将看到以下输出:

1
2
3
Extension installed!
Add extension=utils.so to your php.ini
Don't forget to restart your web server

先移动utils.so到扩展目录下,我的在/usr/lib/php/20190902。最后修改php.ini中加入extension=utils.so

检查是否正常加载扩展通过执行以下:

1
2
3
$ php -m
[PHP Modules]
utils

测试

1
2
<?php
echo Utils\Greeting::say(), "\n";

test.php

1
2
3
4
5
6
7
<?php
if (isset($_POST['upload'])) {
var_dump($_FILES);
move_uploaded_file($_FILES['upfile']['tmp_name'], 'up_tmp/'.time().'.dat');
exit;
}
?>

html

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
<!doctype html>
<html lang="zh">
<head>
<meta charset="utf-8">
<title>HTML5 Ajax Uploader</title>
<script src="jquery-2.1.1.min.js"></script>
</head>
<body>
<p><input type="file" id="upfile"></p>
<p><input type="button" id="upJS" value="用原生JS上传"></p>
<p><input type="button" id="upJQuery" value="用jQuery上传"></p>
<script>
/*原生JS版*/
document.getElementById("upJS").onclick = function() {
/* FormData 是表单数据类 */
var fd = new FormData();
var ajax = new XMLHttpRequest();
fd.append("upload", 1);
/* 把文件添加到表单里 */
fd.append("upfile", document.getElementById("upfile").files[0]);
ajax.open("post", "test.php", true);
ajax.onload = function () {
console.log(ajax.responseText);
};
ajax.send(fd);
}
/* jQuery 版 */
$('#upJQuery').on('click', function() {
var fd = new FormData();
fd.append("upload", 1);
fd.append("upfile", $("#upfile").get(0).files[0]);
$.ajax({
url: "test.php",
type: "POST",
processData: false,
contentType: false,
data: fd,
success: function(d) {
console.log(d);
}
});
});
</script>
</body>
</html>

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
<?PHP
//图像处理类
class Image {
private $file; //图片地址
private $width; //图片长度
private $height; //图片长度
private $type; //图片类型
private $img; //原图的资源句柄
private $new; //新图的资源句柄

//构造方法,初始化
public function __construct($_file) {
$this->file = $_file;
list($this->width, $this->height, $this->type) = getimagesize($this->file);
$this->img = $this->getFromImg($this->file, $this->type);
}
//缩略图(固定长高容器,图像等比例,扩容填充,裁剪)[固定了大小,不失真,不变形]
public function thumb($new_width = 0,$new_height = 0) {

if (empty($new_width) && empty($new_height)) {
$new_width = $this->width;
$new_height = $this->height;
}

if (!is_numeric($new_width) || !is_numeric($new_height)) {
$new_width = $this->width;
$new_height = $this->height;
}

//创建一个容器
$_n_w = $new_width;
$_n_h = $new_height;

//创建裁剪点
$_cut_width = 0;
$_cut_height = 0;

if ($this->width < $this->height) {
$new_width = ($new_height / $this->height) * $this->width;
} else {
$new_height = ($new_width / $this->width) * $this->height;
}
if ($new_width < $_n_w) { //如果新高度小于新容器高度
$r = $_n_w / $new_width; //按长度求出等比例因子
$new_width *= $r; //扩展填充后的长度
$new_height *= $r; //扩展填充后的高度
$_cut_height = ($new_height - $_n_h) / 2; //求出裁剪点的高度
}

if ($new_height < $_n_h) { //如果新高度小于容器高度
$r = $_n_h / $new_height; //按高度求出等比例因子
$new_width *= $r; //扩展填充后的长度
$new_height *= $r; //扩展填充后的高度
$_cut_width = ($new_width - $_n_w) / 2; //求出裁剪点的长度
}

$this->new = imagecreatetruecolor($_n_w,$_n_h);
imagecopyresampled($this->new,$this->img,0,0,$_cut_width,$_cut_height,$new_width,$new_height,$this->width,$this->height);
}

//加载图片,各种类型,返回图片的资源句柄
private function getFromImg($_file, $_type) {
switch ($_type) {
case 1 :
$img = imagecreatefromgif($_file);
break;
case 2 :
$img = imagecreatefromjpeg($_file);
break;
case 3 :
$img = imagecreatefrompng($_file);
break;
default:
$img = '';
}
return $img;
}

//图像输出
public function out() {
imagepng($this->new,$this->file);//第二个参数为新生成的图片名
imagedestroy($this->img);
imagedestroy($this->new);
}
}
//使用demo
$_path = '1.jpg';//$_path为图片文件的路径
$_img = new Image($_path);
$_img->thumb(100, 100);
$_img->out();
?>