init commit

This commit is contained in:
2024-03-19 01:05:51 +08:00
commit 199bbf2628
393 changed files with 34883 additions and 0 deletions

9
docs/blog.mdx Normal file
View File

@ -0,0 +1,9 @@
---
title: BLOG
hide_title: true
sidebar_position: 1
---
import README from "../README.md";
<README />

6
docs/dev/README.md Normal file
View File

@ -0,0 +1,6 @@
---
title: 开发
sidebar_position: 1
---
这里是开发相关的内容,包括编程语言、框架、开发工具、算法等等。

4
docs/dev/_category_.json Normal file
View File

@ -0,0 +1,4 @@
{
"label": "开发",
"position": 1
}

View File

@ -0,0 +1,6 @@
---
id: golang
title: Golang
---
Golang

View File

@ -0,0 +1,4 @@
{
"label": "Golang",
"position": 2
}

186
docs/dev/golang/go-slice.md Normal file
View File

@ -0,0 +1,186 @@
---
title: GO Slice(切片)
---
> 感觉切片只要知道底层是引用的一个数组对象,就挺好理解了.这里写下一些笔记,方便记忆和以后再来查找.
### 切片和数组
切片由三个部分组成:指针(指向底层数组),长度(当前切片使用的长度),容量(切片能包含多少个成员)
然后还有一句和数组相关的:当调用一个函数的时候,函数的每个调用参数将会被赋值给函数内部的参数变量,所以函数参数变量接收的是一个复制的副本,并不是原始调用的变量。(所以数组作为参数,是低效的,还需要进行一次数组的拷贝,可以使用数组指针)
然后如果我们想要传递给一个函数一个数组,函数需要对数组进行修改,我们必须使用数组指针(用 return 当然也不是不行啦?)
但是切片就不需要,来个例子:
```go
func Test_Func(t *testing.T) {
var arr = [...]int{1, 2, 3}
var slice = []int{1, 2, 3}
ModifyArray(arr)
ModifySlice(slice)
t.Logf("%v %v\n", arr, slice)
assert.NotEqual(t, arr[2], 1)
assert.Equal(t, slice[2], 1)
}
func ModifyArray(arr [3]int) {
println(arr)
arr[2] = 1
}
func ModifySlice(slice []int) {
println(slice)
slice[2] = 1
}
```
凭啥切片就行,大家明明都长得都差不多.
前面说了,切片是由三个部分组成,然后数组传入的是数组的副本.其实我觉得 go 里所有的类型传入的都是对应的副本,切片也是,指针也是的(值传递).
那都是副本拷贝,那么咋切片可以修改?
`切片是由:数组指针,长度,容量组成的`,来划一下重点.
副本传的也是上面这些东西.然后修改切片的时候呢,实际上是通过切片里面的数组指针去修改了,并没有修改切片的值(数组指针,长度,容量).
等看完下面再写另外一个情况,在函数里面,给切片增加成员,会怎么样?
### 切片
定义一个数组和定义一个切片的区别是[...]和\[\](当然还有其他的定义方式)
```go
func Test_DefineSlice(t *testing.T) {
var arr = [...]int{1, 2, 3}
var slice1 = []int{1, 2, 3}
var slice2 = make([]int, 3)
var slice3 = arr[:]
fmt.Printf("arr type=%v len=%d cap=%d\n", reflect.TypeOf(arr).String(), len(arr), cap(arr))
fmt.Printf("slice1 type=%v len=%d cap=%d\n", reflect.TypeOf(slice1).String(), len(slice1), cap(slice1))
fmt.Printf("slice2 type=%v len=%d cap=%d\n", reflect.TypeOf(slice2).String(), len(slice2), cap(slice2))
fmt.Printf("slice3 type=%v len=%d cap=%d\n", reflect.TypeOf(slice3).String(), len(slice3), cap(slice3))
}
//Result:
//arr type=[3]int len=3 cap=3
//slice1 type=[]int len=3 cap=3
//slice2 type=[]int len=3 cap=3
//slice3 type=[]int len=3 cap=3
```
上面方法中的切片是会自动创建一个底层数组,如果切片直接引用一个创建好了的数组呢?
我的猜想是在切片里面修改值,原数组也会跟着一起变(切片指针指向的就是这一个数组)
然后我想再验证一下,如果我的切片再增加一个成员(超出数组限制),那么还会变化吗?
我的猜想是会重新分配到另外一个数组去,然后导致引用的数组不会发生改变(切片指针指向的已经是另外一个数组了)
```go
func Test_Modify(t *testing.T) {
arr := [...]int{1, 2, 3, 4, 5, 6, 7}
slice := arr[:]
slice[4] = 8
t.Logf("arr[4]=%v,slice[4]=%v\n", arr[4], slice[4])
assert.Equal(t, slice[4], arr[4])
slice = append(slice, 9)
slice[5] = 10
t.Logf("arr[4]=%v,slice[4]=%v\n", arr[4], slice[4])
assert.Equal(t, slice[4], arr[4])
t.Logf("arr[5]=%v,slice[5]=%v\n", arr[5], slice[5])
assert.NotEqual(t, slice[5], arr[5])
}
```
验证通过^\_^
再来试试两个切片共享一个数组
```go
func Test_ModifyTwoSlice(t *testing.T) {
arr := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
slice1 := arr[1:5]
slice2 := arr[3:8]
slice1[2] = 8
t.Logf("%v %v %v\n", arr, slice1, slice2)
assert.Equal(t, slice1[2], slice2[0], arr[3])
}
```
一样的全部一起修改成功了
### append
然后我们来看看 append
```go
func Test_Append(t *testing.T) {
slice := []int{1, 2, 3}
println(slice)
slice = append(slice, 1)
println(slice)
slice = append(slice, 1)
println(slice)
}
// Result:
// slice type=[]int len=3 cap=3
// [3/3]0xc00005e3e0
// slice type=[]int len=4 cap=6
// [4/6]0xc00008c060
// slice type=[]int len=5 cap=6
// [5/6]0xc00008c060
```
当容量够的时候切片的内存地址没有发生变化,不够的时候进行了扩容,地址改变了.
刚刚写的时候发现了一个问题,就是每次扩容的大小,我写了一个循环来展示
```go
func Test_Cap(t *testing.T) {
slice1 := []int{1, 2, 3}
slice2 := make([]int, 3, 3)
last := [2]int{0, 0}
for i := 0; i < 100; i++ {
slice1 = append(slice1, 1)
slice2 = append(slice2, 1)
if last[0] != cap(slice1) {
println(slice1)
last[0] = cap(slice1)
}
if last[1] != cap(slice2) {
println(slice2)
last[1] = cap(slice2)
}
}
}
```
好吧,我以为扩容的容量,~~如果不是 make 的话是按照前一次容量的两倍来扩容的,是 make 就是每次增加的容量是固定的,事实证明我想多了~~
### End
再回到最开始,在函数里面,增加切片的成员.我想应该有了答案.
```go
func ModifySlice(slice []int) {
slice[2] = 1
slice = append(slice, 4)
slice[2] = 3
}
```
我把之前的`ModifySlice`方法修改了一下,然后成员没加,后面再修改回去为 3 也没有发生变化了.
这是因为 append 的时候因为容量不够扩容了,导致底层数组指针发生了改变,但是传进来的切片是外面切片的副本,修改这个切片里面的数组指针不会影响到外面的切片
#### 奇淫巧技
例如这个(go 圣经里面抄的 233),reverse 的参数是切片,但是我们需要处理的是一个数组,这里我们就可以直接用`arr[:]`把数组转化为切片进行处理
```go
func Test_Demo(t *testing.T) {
arr := [...]int{1, 2, 3, 4}
reverse(arr[:])
t.Logf("%v\n", arr)
}
func reverse(s []int) {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
}
```
> 暂时只想到这些,再有的话,以后继续补.如有错误希望指教.

View File

@ -0,0 +1,4 @@
{
"label": "Linux",
"position": 4
}

View File

@ -0,0 +1,31 @@
> 最近在使用 webpack 打包工具的时候,使用 watch 模式不管用,最开始以为是配置的问题,将就着写者,后面同一个项目在 windows 上可以,但是我用我的 ubuntu 运行 watch 模式怎么也不管用,通过 google 找到了解决方案。
## 解决方案
DebianRedHat 或其他类似的 Linux 发行版
```shell
echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
```
Arch Linux
```shell
echo fs.inotify.max_user_watches=524288 | sudo tee /etc/sysctl.d/40-max-user-watches.conf && sudo sysctl --system
```
在终端中输入上面的命令
## Why
这是因为在 Linux 下监控的文件有一定的限制当我们项目的文件超过这个数量的时候其他的文件就不会再监控webpack 无法监测到文件的变化,所以 watch 模式就失效了
使用下面命令可以查看限制的数量:
```shell
cat /proc/sys/fs/inotify/max_user_watches
```
## 参考
[https://github.com/guard/listen/wiki/Increasing-the-amount-of-inotify-watchers](https://github.com/guard/listen/wiki/Increasing-the-amount-of-inotify-watchers)

View File

@ -0,0 +1,140 @@
---
title: 微服务架构本地尝试(一)-RPC
---
**本系列在 github 中更新,源码和 docker 都可在 github 项目中找到:[https://github.com/CodFrm/learnMicroService](https://github.com/CodFrm/learnMicroService)**
**GitHub 文章:[https://github.com/CodFrm/learnMicroService/blob/master/doc/%E5%BE%AE%E6%9C%8D%E5%8A%A1%E6%9E%B6%E6%9E%84%E6%9C%AC%E5%9C%B0%E5%B0%9D%E8%AF%95(%E4%B8%80)-rpc.md](<https://github.com/CodFrm/learnMicroService/blob/master/doc/%E5%BE%AE%E6%9C%8D%E5%8A%A1%E6%9E%B6%E6%9E%84%E6%9C%AC%E5%9C%B0%E5%B0%9D%E8%AF%95(%E4%B8%80)-rpc.md>)**
> 微服务架构最近非常的流行,我的公司年后也准备使用微服务架构.而且之前我也很想学习接触架构层面的知识,了解一些比较前沿的东西,所以利用这个假期来尝试学习一下微服务架构.
>
> 之前我已经了解了微服务大概是一个怎么样的东西,对于一些理论的东西并不打算写太多.个人觉得微服务的技术核心在于 rpc 和服务发现/注册;思想主要是不同业务之间的拆分
>
> 我打算尝试使用 golang 和 gRPC 框架一步一步的去摸索,带着一些问题去实践
## RPC
RPCRemote Procedure Call—远程过程调用,简单来说就是,在 A 服务器上调用 B 服务器上的方法.rpc 可以通过 http 协议但不仅限于来实现.大多 rpc 框架也会支持多种协议.利用 rpc 框架可以不让我们关注 rpc 实现层,让我们调用一个远程的方法就像在本地调用一样.
通常会将登录/注册(权限服务)拆分为一个微服务,当有一些操作(另外的微服务)需要验证某项权限时,可以通过 rpc 调用权限微服务上的方法来验证当前用户是否拥有权限.感觉像是通过 rpc 来将各个独立的微服务关联起来.
### [gRPC](https://grpc.io/about/)
[gRPC](https://grpc.io/about/)是 google 的一个跨语言的 rpc 框架.其实一开始是想用[rpcx](http://rpcx.site/)的,虽然中文文档挺齐全的,而且也是国人开发的,但是一搜索网上都没有什么资料,就暂时不了解了.
### protocol buffers
[protocol buffers](https://developers.google.com/protocol-buffers/docs/proto3)是一种轻便高效的结构化数据储存结构,这是 gRPC 默认的数据协议.我们也需要去了解一下它.
### 开始
先装好 golang 环境,获取包,先用官方提供给我们的例子试一试
```sh
# 安装grpc
go get -u google.golang.org/grpc
# 安装Protocol Buffers v3
go get -u github.com/golang/protobuf/protoc-gen-go
```
### 例子分析
我在 [client](https://github.com/CodFrm/learnMicroService/tree/master/examples/rpc/helloworld/greeter_client/main.go),[server](https://github.com/CodFrm/learnMicroService/tree/master/examples/rpc/helloworld/greeter_server/main.go)和[helloworld.pb.go](https://github.com/CodFrm/learnMicroService/tree/master/examples/rpc/helloworld/helloworld/helloworld.pb.go)的源码中做了一些注释,方法都可以在官方文档中看到[grpc](https://godoc.org/google.golang.org/grpc)
```sh
# 将例子复制过来
cp -r $GOPATH/src/google.golang.org/grpc/examples examples/rpc
# 编译例子 server和client 然后运行
cd examples/rpc/helloworld
```
默认端口是 50051,我竟然不能打开,好像是被占用了,我直接就换了一个,完成.
先运行服务端,然后运行客户端可以看到效果
![](img/01-rpc.assets/rpc_c_s-300x144.png)
helloworld.pb.go 是通过的插件`protoc-gen-go`编译`helloworld.proto`生成的.虽然可以自动生成,但我还想了解一下实现的方法.然后还需要了解 proto 的语法,我们才能制作属于我们的 rpc 调用接口
### 定义接口
#### protoc
这里就不弄太复杂了,我们可以参照例子给我们的来写,来写一个简单的,就比如权限验证,通过 token 和接口名字获取该用户信息和是否有权限使用接口.
```sh
syntax = "proto3"; # 定义版本
package = lms; # 包名
service UserAuth { # 定义服务
# 验证token(TokenMsg),然后返回用户信息(UserMsg)
rpc isvalid(TokenMsg) returns (UserMsg) {}
}
message TokenMsg { # 消息模型
string token = 1; # string 字符串类型 token 名字
string api = 2;
}
message UserMsg {
int32 uid = 1;
bool access =2;
string name = 3;
string group = 4;
}
```
更复杂的消息结构还能嵌套,枚举,这里就先只用这一些
上面的服务定义是 单项 RPC 的形式,调用完就没了,还有 服务端流式 RPC,客户端流式 RPC 和客户端流式 RPC,具体的可以去看一下其他的文章[https://colobu.com/2017/04/06/dive-into-gRPC-streaming/](https://colobu.com/2017/04/06/dive-into-gRPC-streaming/),感觉通常单项 RPC 就可以了,其他的应该是在一些数据传输的场景使用.
#### 生成 go 文件
写好 proto 文件后,我们需要用工具编译成 golang 代码.windows 需要去下载工具[https://github.com/protocolbuffers/protobuf/releases](https://github.com/protocolbuffers/protobuf/releases)
```sh
protoc -I ./proto --go_out=plugins=grpc:./proto ./proto/learnMicroService.proto
```
### 编写服务
继续参照给我们的例子写,具体代码看我源码吧 [auth 权限验证微服务](https://github.com/CodFrm/learnMicroService/tree/master/auth/main.go)
这里我构建了一个权限验证的微服务,然后另外构建一个发帖的,可以去看我的源码 [post 帖子微服务](https://github.com/CodFrm/learnMicroService/tree/master/post/main.go)
主要在发帖的时候判断是否拥有权限
```go
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
userMsg, err := authService.Isvalid(ctx, &micro.TokenMsg{
Token: req.PostFormValue("token"),
Api: "post",
})
if err != nil {
ret = "rpc调用错误"
} else if !userMsg.Access {
ret = "没有权限"
} else {
ret = userMsg.Name + "post 请求成功"
posts = append(posts, req.PostFormValue("title"))
}
```
我用 postman 测试:
![](img/rpc.assets/rpc_debug_1-300x120.png)
![](img/rpc.assets/rpc_debug_2-300x197.png)
到这里,两个非常非常简陋的微服务算是完成了,一个权限验证,一个发帖 hhhh

View File

@ -0,0 +1,65 @@
---
title: 微服务架构本地尝试(二)-api网关
---
**本系列在 github 中更新,源码和 docker 都可在 github 项目中找到:[https://github.com/CodFrm/learnMicroService](https://github.com/CodFrm/learnMicroService)**
**GitHub 文章:[https://github.com/CodFrm/learnMicroService/blob/master/doc/%E5%BE%AE%E6%9C%8D%E5%8A%A1%E6%9E%B6%E6%9E%84%E6%9C%AC%E5%9C%B0%E5%B0%9D%E8%AF%95(%E4%BA%8C)-api%E7%BD%91%E5%85%B3.md](<https://github.com/CodFrm/learnMicroService/blob/master/doc/%E5%BE%AE%E6%9C%8D%E5%8A%A1%E6%9E%B6%E6%9E%84%E6%9C%AC%E5%9C%B0%E5%B0%9D%E8%AF%95(%E4%BA%8C)-api%E7%BD%91%E5%85%B3.md>)**
> 上一节学习了 rpc 框架,现在有一个问题,拆开是拆开了,但是不同的服务直接对外都会提供不同的接口,上一节我只简略的为帖子服务写了接口,现在我用户要登录获取 token 了,我就得给他一个登录的接口,我现在是同一台电脑上,80 端口被帖子微服务占用了,我登录微服务难道要开 81 端口给用户服务?当然是不可能的,这时候就要用 api 网关了,当然 api 网关可不止这一个功能,授权、监控、负载均衡、缓存等都能通过 api 网关实现.
>
> 通过同一个入口,然后根据 api 的路径访问不同的微服务.顺便尝试自己做了张图,emm 感觉很差,当我们这一节结束后,我们的架构大概就是下面这样了,就像是对外一个统一的接口.
![](img/api%E7%BD%91%E5%85%B3.assets/api_flow_chart.png)
API 网关有很多选择,这里列举几个:[Tyk](https://tyk.io/),[Kong](https://konghq.com/),[zuul](https://github.com/Netflix/zuul)等
这里我选择了 Kong 来布置我的网关.
## 开始
> 我本地当然是选择 Docker 安装,感觉 Docker 是真的方便,就算在 Docker 中弄错了也可以删过重来,不像在物理机上,弄错了万一还删错了东西,系统崩溃 T_T 各种毛病,而且清理起来也很方便
其实我尝试了几次....第一次用的 Tyk 然后发现需要收费,系统还蜜汁登陆不进去,然后就换了 Kong,然后又遇到了一个大坑,安装的版本是 1.0.2,我 Google 搜索到推荐的面板是`kong dashboard`结果这个很长没维护了,好不容易安装好了,结果最高只支持 0.15 版本=\_=,只怪当初没仔细看这些吧....我还以为这个是官方维护的项目,然后又搜到了一个面板`konga`,终于折腾好了,效果如下.
![](img/api%E7%BD%91%E5%85%B3.assets/api_dashboard.png)
注册一个账号,然后填好参数之后就可以进去了.
### 安装
搭建请使用 Docker,我已经将 docker-compose 写好了,直接在项目根目录执行:
`docker-compose up`就可以了,这里还有一个要注意的地方就是需要先运行**kong_migrations**,生成数据库文件,如果没有那么**kong**会运行失败.我是运行之后,如果**kong**报错就手动重启**kong_migrations**容器然后再运行**kong**容器的.
参考文档:
- [konga 面板](https://github.com/pantsel/konga/blob/master/README.md)
- [kong docker 安装](https://docs.konghq.com/install/docker/?_ga=2.219796185.1115565600.1548736826-1002927840.1548736826)
- [konga 文档](https://pantsel.github.io/konga/)
api 网关默认地址:[http://127.0.0.1:8000/](:http://127.0.0.1:8000/),不过现在还没添加 api,返回的是`{"message":"no Route matched with those values"}`
面板默认地址:[http://127.0.0.1:1337/](http://127.0.0.1:1337/)
### 使用
点击到`SERVICE`中,添加一个新的服务,我把我上一节的帖子服务加入进去,如图
![](img/api%E7%BD%91%E5%85%B3.assets/api_add_service.png)
后面的默认就行,Url 可以为空,如果写的话可以写:http://10.0.75.1:8004/,添加后其实会自动分解成下面的选项(这里帖子服务的端口和我kong admin 的端口冲突了,所以我改成了 8004)
10.0.75.1 是我主机的 ip 地址(在 docker 中)
service(服务)在这里的概念可以对应我们的微服务,将我们微服务暴露的接口通过上面的步骤添加进去,配置中的 upstream server(上游服务)就是我们微服务的一些 ip 信息,协议那里是 http 或 https,不过 kong 1.0 好像已经开始支持 gRPC 了,还没去研究.
添加完服务之后,到 service 里去查看,然后添加 route
![](img/api%E7%BD%91%E5%85%B3.assets/api_add_route.png)
主要是 paths 和 methods,paths 是匹配的路径,我这里写的/v1,methods 允许 GET 和 POST
![](img/api%E7%BD%91%E5%85%B3.assets/api_kong_demo.png)
浏览器输入[http://127.0.0.1:8000/v1/post](http://127.0.0.1:8000/v1/post)成功得到结果
kong 还有很多强大的功能,还可以授权,统计,负载均衡等等,也有丰富的插件,不过暂时我还用不到,先到这里,下一节预计开始研究 服务注册/发现

View File

@ -0,0 +1,182 @@
---
title: 微服务架构本地尝试(三)-服务发现
---
**本系列在 github 中更新,源码和 docker 都可在 github 项目中找到:[https://github.com/CodFrm/learnMicroService](https://github.com/CodFrm/learnMicroService)**
**GitHub 文章:[点我去 GitHub 看](<https://github.com/CodFrm/learnMicroService/blob/master/doc/%E5%BE%AE%E6%9C%8D%E5%8A%A1%E6%9E%B6%E6%9E%84%E6%9C%AC%E5%9C%B0%E5%B0%9D%E8%AF%95(%E4%B8%89)-%E6%9C%8D%E5%8A%A1%E5%8F%91%E7%8E%B0.md>)**
> 上一节完成了 api 网关,这一节来了解微服务架构中的服务发现
>
> 我们现在的微服务 rpc 调用我们是直接的填入的 ip 和端口,客户端连接服务端.但是当我们的微服务多起来,而且是集群部署那么问题就出来了,我们不可能每一个都填好 ip 和端口,这样是不方便动态扩容和部署的,而且当我们调用的时候也无法做到很好的负载均衡.关于这个有两种模式,客户端服务发现和服务端服务发现,这里我们只探讨服务端服务发现
>
> 那么我们就需要一个服务中心,微服务进程创建的时候告诉他,我能提供 xx 服务,我的 ip 端口是 xx,然后其他的微服务或客户需要调用的时候来服务中心问,我需要 xx 服务的信息就 OK 了
![](img/03-%E6%9C%8D%E5%8A%A1%E5%8F%91%E7%8E%B0.assets/service_chart.png)
服务发现的框架常见的有
- zookeeper
- eureka
- etcd
- consul
这里我们选择[Consul](https://www.consul.io/)来实现
## 开始
> 自然和之前一样,继续用我们的 docker
docker-compose.yml 中直接加入如下,然后访问 8500 端口就可以进入面板了
```yml
service_center:
image: consul
hostname: service_center
container_name: micro_service_center
ports:
- 8500:8500
- 8600:8600
- 8300:8300
networks:
- micro
```
![](img/03-%E6%9C%8D%E5%8A%A1%E5%8F%91%E7%8E%B0.assets/service_consul.png)
## 使用
### 服务注册/发现
[https://www.consul.io/api/agent/service.html#register-service](https://www.consul.io/api/agent/service.html#register-service)
服务注册使用 api,发送请求,这里我用 postman 先来测试添加
官网给了一个例子
```json
PUT /agent/service/register json
{
"ID": "redis1",# 服务id
"Name": "redis",# 服务名
"Tags": [# 标签
"primary",
"v1"
],
"Address": "127.0.0.1",# 服务地址
"Port": 8000,# 服务端口
"Meta": {# 服务的一些信息
"redis_version": "4.0"#表示redis版本
},
"EnableTagOverride": false,# 是否开启Tag覆盖,更多细节请参考:https://www.consul.io/docs/agent/services.html#enable-tag-override-and-anti-entropy 我也看不太懂啊,hhh
"Check": {# 校验规则
"DeregisterCriticalServiceAfter": "90m",
"Args": ["/usr/local/bin/check_redis.py"],
"HTTP": "http://localhost:5000/health",
"Interval": "10s",
"TTL": "15s"
},
"Weights": {# 权重,应该是用于负载均衡的
"Passing": 10,
"Warning": 1
}
}
```
我先尝试自己添加几个服务,然后我的面板上就有了一个新的**post_micro**服务,点进去,虽然都挂了 emmmm 因为我现在还没开启
![](img/03-%E6%9C%8D%E5%8A%A1%E5%8F%91%E7%8E%B0.assets/service_2.png)
![](img/03-%E6%9C%8D%E5%8A%A1%E5%8F%91%E7%8E%B0.assets/service_3.png)
```sh
curl -X PUT \
http://127.0.0.1:8500/v1/agent/service/register \
-H 'Content-Type: application/json' \
-H 'Postman-Token: b2089dbe-8ad3-487b-97e0-25217e6ee346' \
-H 'cache-control: no-cache' \
-d '{
"ID": "post2",
"Name": "post_micro",
"Tags": [
"post",
"v1"
],
"Address": "10.0.75.1",
"Port": 8004,
"Check": {
"DeregisterCriticalServiceAfter": "90m",
"HTTP": "http://10.0.75.1:8004/post",
"Interval": "10s"
}
}'
```
然后 consul 也提供了服务发现(查询)的接口,来试一试,除了这种接口的方式外,还可以使用 DNS,我们只要如果配合 kong 的话需要用这种形式,等下介绍.post_micro 就是我们的服务名了
```sh
curl -X GET \
http://127.0.0.1:8500/v1/catalog/service/post_micro \
-H 'Postman-Token: ce54bd1d-c21e-46e0-ae0e-0230de9e6f8d' \
-H 'cache-control: no-cache'
```
域名默认格式为 servicename.service.consul
```sh
/ # dig @127.0.0.1 -p 8600 post_micro.service.consul SRV
; <<>> DiG 9.11.5 <<>> @127.0.0.1 -p 8600 post_micro.service.consul SRV
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 25596
;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 5
;; WARNING: recursion requested but not available
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;post_micro.service.consul. IN SRV
;; ANSWER SECTION:
post_micro.service.consul. 0 IN SRV 1 1 8005 0a004b01.addr.dc1.consul.
post_micro.service.consul. 0 IN SRV 1 1 8004 0a004b01.addr.dc1.consul.
;; ADDITIONAL SECTION:
0a004b01.addr.dc1.consul. 0 IN A 10.0.75.1
service_center.node.dc1.consul. 0 IN TXT "consul-network-segment="
0a004b01.addr.dc1.consul. 0 IN A 10.0.75.1
service_center.node.dc1.consul. 0 IN TXT "consul-network-segment="
;; Query time: 0 msec
;; SERVER: 127.0.0.1#8600(127.0.0.1)
;; WHEN: Wed Jan 30 05:02:21 UTC 2019
;; MSG SIZE rcvd: 266
```
因为 kong 要用 consul 的 dns,整合起来,然后到这里的时候遇到个坑...首先需要 ip 地址,不能是 hostname 了...然后我是在 docker 里面...还需要 docker-compose 静态 ip,可以看我的 compose 文件的变化,然后设置了一个`KONG_DNS_RESOLVER`参数,但还不行,因为你的 dns 变了,`KONG_PG_HOST`之前是 hostname,现在我的 dns 不能解析,所以也要给数据库设置一个静态 ip(设置静态 ip 主要是防止变化后不能使用).之前的容器最好 down 掉,network 也删除,重新来一遍
成功之后在 konga 面板的 info 中可以看到 dns_resolver 是我们设置的值了
![](img/03-%E6%9C%8D%E5%8A%A1%E5%8F%91%E7%8E%B0.assets/service_4.png)
### 接入网关
然后和前一节一样,添加 service,因为我重新构建了,所以之前的数据是没了的,也没关系,重新来
![添加service](img/03-%E6%9C%8D%E5%8A%A1%E5%8F%91%E7%8E%B0.assets/service_5.png)
![添加route](img/03-%E6%9C%8D%E5%8A%A1%E5%8F%91%E7%8E%B0.assets/service_6.png)
有没有发现我前面是添加了两个不同的端口,然后这里没有添加端口了,是的,consul 帮你自动匹配了,负载均衡了解一下,你就当我这俩是集群好了,效果的话,我故意的文件改了一下,刷新出来的页面不同(我只是为了演示效果,实际是绝对不行的...)
![](img/03-%E6%9C%8D%E5%8A%A1%E5%8F%91%E7%8E%B0.assets/service_7.png)
## End
这一节感觉还不算完全完成,注册和发现的代码我们还没写呢
![](img/03-%E6%9C%8D%E5%8A%A1%E5%8F%91%E7%8E%B0.assets/bqb_1.jpg)
留到下一节好了,今天几乎是配置了一天的环境,下一节预计就要开始容器化微服务了,然后等下吧文章搬到博客去,之前没直接放博客的原因主要是怕半途弃坑,多丢人,2333

View File

@ -0,0 +1,90 @@
---
title: 微服务架构本地尝试(四)-容器
---
> 容器的好处就不多说了,一次构建到处运行.对于微服务来说,需要将各个微服务部署到多个主机上,而且所需要的环境还不一定相同,甚至冲突,那么容器就很好的解决了这个问题,容器所占用的资源比虚拟机小多了.而且容器部署起来很是方便.然后 go 和 docker 也是很配的一对,go 编译之后可以直接的放入容器中运行,而不需要依赖环境.
>
> 这一节的话我们还需要在代码里面种加入服务发现和注册的功能,以便在容器中能够动态扩容.
![](img/04-%E5%AE%B9%E5%99%A8.assets/4_1.png)
## 服务注册
先获取开发包
```shell
go get -u github.com/hashicorp/consul
# 其实好像不要-u...我习惯加上然后帮我编译了...
```
我另外封装了一下,可以去看我的代码[consul](https://github.com/CodFrm/learnMicroService/blob/master/common/consul.go)
这里另外说一下,consul 的 tag,我们解析的时候可以带上 tag
例如我这里的发帖微服务,一个是对外的 restful,一个是内部的 rpc,我们可以注册两个相同名字的服务,然后给他们打上 tag,解析的时候 tag 可以放最前面,例如:restful.post_micro.service.consul
Demo:
```go
//注册服务
rpcService := consul.Service{
Name: "auth_micro",
Tags: []string{"rpc"},
Address: consul.LocalIP(),
Port: 5000,
}
defer rpcService.Deregister()
err = rpcService.Register()
//使用
rpcService := consul.Service{
Name: "auth_micro",
Tags: []string{"rpc"},
}
rpcConn, err = rpcService.GetRPCService()
```
## Dockerfile
go 的话我们可以多阶段构建,第一阶段编译,第二阶段运行,这样可以减小我们最终的镜像大小.我们可以先把 golang 的镜像 pull 下来`docker pull golang`然后基于这个镜像来编译,然后再吧编译好的放到第二阶段中运行,我们可以选择最小的 alpine 来运行
> 这里又遇到坑,我要获取一些被墙了的包...然而我容器里面又没有工具...后面只好再装一个镜像来折腾了,然而不支持 google.golang.org...只能 git clone 了,我有点遭不住
>
> ![](img/04-%E5%AE%B9%E5%99%A8.assets/bqb_2.jpg)
>
> 为了做好这个镜像,还用了个奇淫巧技...在后面加入`;exit 0`防止报错停止构建
emmmm 上面放弃了,git clone 下来还有些依赖,依赖又需要去 clone,太复杂了,然后我灵机一动...好像可以直接用我的代理...真香...
```sh
ENV HTTP_PROXY=http://10.0.75.1:1080/ \
HTTPS_PROXY=http://10.0.75.1:1080/
```
折腾了挺久....算是吧 dockerfile 写好了,然后在 docker-compose 里面添加两个服务,post 端口暴露出来,我就可以直接试试能不能访问了
```yaml
post_1:
image: post:latest
container_name: micro_post_micro_1
ports:
- 8004:8004
networks:
- micro
auth_1:
image: auth:latest
container_name: micro_auth_microo_1
networks:
- micro
```
![](img/04-%E5%AE%B9%E5%99%A8.assets/4_2.png)
直接运行 compose,一次成功,很是欣慰
![](img/04-%E5%AE%B9%E5%99%A8.assets/4_3.png)
右边那个就是我们容器中运行的了,有一个报错的,没有自动清理掉,consul 好像不能自动清理,可以在健康监测那里添加一个`DeregisterCriticalServiceAfter`参数,到期删除,不知道能不能设置一个比较短的时间,然后每隔一段时间就重新注册一次服务,刷新有效期这样达到自动清理的效果.或者结束的时候调用注销方法(但是在容器里面每次都是强制结束...没有调用到注销的方法)
到这里的时候我们已经可以将我们的微服务集群动态扩容了,但是还缺少很多东西这一节就到这里了....然后还需要用到数据库重写一下服务,或者选一个 web 框架,后面还有难点要整.

View File

@ -0,0 +1,64 @@
---
title: 微服务架构本地尝试(五)-数据库拆分
---
> 原本打算是用 k8s 的,但是这么一弄感觉越来越往运维方向走了.现在的网关和服务中心已经够用了.所以只写了一部分,作为附录,如果以后有时间会继续填坑.
>
> 这一节的话,主要是数据库拆分和我们之前分开的两个微服务(权限和发帖)接上数据库(主要是代码).
## 未拆分前
![](img/05-%E6%95%B0%E6%8D%AE%E5%BA%93%E6%8B%86%E5%88%86.assets/5_db_1.png)
未拆分前,我们的架构大概是这样的,所有的微服务从同一个数据库中请求,这样的好处是简单,但是微服务之间的关系又觉得没有区分开来,微服务架构的一个非常明显的功能就是一个服务所拥有的数据只能通过这个服务的 API 来访问.
## 拆分后
![](img/05-%E6%95%B0%E6%8D%AE%E5%BA%93%E6%8B%86%E5%88%86.assets/5_db_2.png)
拆分后大概是上图这样,一个微服务对应一个数据库,这样更容易微服务的扩展,而且微服务的独立性更强,其中一个数据库蹦了之后也不会互相影响.看起来是有点像分库分表,拆分后的好处明显,但是带来的困难也很明显.分布式事务,跨服务查询等等一些问题.后面就要去解决这些问题.
在本地,我使用 compose 搭建了两个容器来模拟这种情况
```yaml
post_db:
image: mysql
hostname: post_db
container_name: micro_post_db
environment:
- MYSQL_USER=post
- MYSQL_ROOT_PASSWORD=micro_db_pwd
- MYSQL_DATABASE=post
ports:
- 3308:3306
networks:
- micro
auth_db:
image: mysql
hostname: auth_db
container_name: micro_auth_db
environment:
- MYSQL_USER=auth
- MYSQL_ROOT_PASSWORD=micro_db_pwd
- MYSQL_DATABASE=auth
ports:
- 3307:3306
networks:
- micro
```
名字也能看出一个是帖子的数据库一个是用户的数据库,然后在代码中,数据库连接那里的 ip 要更改成对应的`hostname`.
```go
//连接数据库
err = db.Connect("127.0.0.1", 3306, "root", "", "test")
//查询帖子列表
rows, err := db.Query("select a.id,b.user,a.title from posts as a join user as b on a.uid=b.uid")
```
之前是使用的本地 ip,不过更改完后,我们的帖子服务中的 GET 请求获取帖子列表是无法使用的.因为用到了 join 查询用户表中的用户名,现在我们已经将数据库拆分开来了,物理上帖子数据库中是没有用户数据的.然后报出如下错误
![](img/05-%E6%95%B0%E6%8D%AE%E5%BA%93%E6%8B%86%E5%88%86.assets/5_post.png)
这一节主要就是将代码进行了修改使用了数据库,然后用 compose 搭建了两个数据库.后面几节将解决拆分分库之后出现的一些问题.

View File

@ -0,0 +1,165 @@
---
title: 微服务架构本地尝试(六)-聚合数据
---
> 微服务架构中,每个微服务所拥有的数据对当前微服务来说是私有的,只能通过其提供的 API 进行访问.我们需要实现业务的事务在多个服务之间保持一致性,还有就是不同服务中数据的数据聚合.
>
> 最终我选择了尝试领域驱动设计和 CQRS,可以从 git 看到我对本节有不少改变=\_=,尝试一下,希望不要误人子弟,对于理论的知识我也不会在本文中写太多,我会贴我觉得不错的一些文章.
## 方案
这一节将来探究不同的微服务之间,不同服务中数据的数据聚合.我将列出几种方法.
### 字段冗余
这一种方法是在查询的表中增加一些字段存储另外一些表的数据,这种方法实现起来比较方便,但是需要改变表结构,而且如果所需要的字段比较多,这又会出现不少问题.
### 业务中合并
通过调用所需要的微服务接口得到数据,使用代码将数据合并起来,达到 join 的效果.这种方法虽然对数据库没有什么影响,而且也符合微服务的思想,但是性能损耗过大.
### 数据库 FEDERATED 引擎
MySQL 数据库能够使用 FEDERATED 引擎映射所需要的表,有点像复制但并不是的,然后可以使用 join 查询达到我们想要的效果.这个也许是一个不错的解决方案,但是不符合我们微服务的设计思想,不易于独立交付,对方微服务表结构修改之后可能会出现问题.
### 共享数据库
将需要 join 的数据表放在同一个数据库上,其余的还是作为独立的数据库.感觉优缺点和上面差不多.
### CQRS 架构
命令查询的责任分离 Command Query Responsibility Segregation (简称 CQRS)模式是一种架构体系模式,能够使改变模型的状态的命令和模型状态的查询实现分离。这是微服务跨数据库查询的一个比较流行的解决方案,具体怎么样我也不细说了,怕误人子弟
![](img/06-%E8%81%9A%E5%90%88%E6%95%B0%E6%8D%AE.assets/bqb_3.jpg)
### 事件驱动架构(EDA)
CQRS 使用了 EDA 的思想,所以感觉起来非常的相似,CQRS 的 C 可以是同步实现,也可以为异步.EDA 则是异步,也没有 Event Store 的要求.
## CQRS+DDD(领域驱动设计)
### DDD
对于领域驱动设计,我觉得不要去考虑数据库,当做数据库不存在去创建我们的模型,不然很难脱离原来的设计方式.应该是数据库来迎合我们,而不是去迎合数据库.我们的数据都是存储在内存当中,也就是内存保存到数据库.
然后对于领域模型存储到数据库中可以使用一个中间件转换,而不是将 sql 写在领域模型中.
实体和值对象:
> 引用于:[https://www.cnblogs.com/youxin/archive/2013/05/25/3099175.html](https://www.cnblogs.com/youxin/archive/2013/05/25/3099175.html)
- 实体核心是用唯一的标识符来定义,而不是通过属性来定义。即即使属性完全相同也可能是两个不同的对象。同时实体本身有状态的,实体又演进的生命周期,实体本身会体现出相关的业务行为,业务行为会实体属性或状态造成影响和改变。
- 值对象 Value Object它用于描述领域的某个方面本身没有概念标识的对象值对象被实例化后只是提供值或叫设计元素我们只关心这些设计元素是什么而不关心这些设计元素是谁。书里面谈到颜色数字是常见的值对象。这种对象无状态本身不产生行为不存在生命周期演进。
### 思路
> 之前我认为是使用 CQRS 维护物化视图,实际我们所维护的物化视图就是我们的数据库(因为之前我认为物化视图只是展示给用户看的一个表,数据库中还需要在原来的表中存储一次数据),我的思想还是停留在迎合数据库上,虽然这看上去并没有什么区别.
我现在的思路是这样的(现在我们的发帖和权限两个微服务来说),使用 kafka 作为消息中间件,发布事件.
这个版本,我加入一个积分的微服务,当发送一条帖子时,增加积分.这只是一个小功能,我偷懒的将它和认证服务放在了一起.
用户改名的时候发布一条 user_update_msg 消息,然后订阅这个消息,修改用户名.(我认为如果在业务中用户名是不允许修改的话,可以将其作为值对象,如果允许修改且要随之变化的话,应该将用户作为一个实体,也需要实现这一消息,这些都是对于发帖微服务来说的)
~~然后关于发帖或者回帖时发布一个 post_msg/post_reply_msg 消息,对于这个看上去感觉是没有必要发送消息的,让系统变得更复杂,我们可以直接的写入 Q 端数据库.不过加入事件也有好处,假设当我们再加入一个积分微服务的时候,我们就不需要再改代码了,直接订阅该消息.~~
我放弃了回帖功能- -...改为积分功能,积分微服务订阅事件,进行处理
如果之前的数据迁移的话,我想的是创建数据库的时候,使用微服务 api(或者直接将数据复制过来进行操作),在代码中聚合数据,按照符合现在结构的方式插入,不知道这是不是一个可行的方法.
这一切都是一个菜鸡的尝试...如果不正确希望能够得到指教.
## 实现
### kafka
> Event Store 和 MQ 我们使用 Kafka,虽然用 Kafka 作为事件溯源不一定是好的,但是它能用啊.
依旧使用 docker 来搭建我们的环境,由于 Kafka 的基于集群的高可用特性是建基于 Zookeeper称 zk之上的,因此构建可用的 Kafka 镜像,是需要依赖于 zk 基础的.
```yaml
zookeeper:
image: wurstmeister/zookeeper
hostname: zookeeper
container_name: micro_zookeeper
ports:
- 2181:2181
networks:
- micro
kafka_mq:
image: wurstmeister/kafka
hostname: kafka_mq
container_name: micro_kafka_mq
environment:
- KAFKA_ADVERTISED_HOST_NAME=10.0.75.1
- KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181
ports:
- 9092:9092
networks:
- micro
kafka_manager:
image: sheepkiller/kafka-manager
container_name: micro_kafka_manager
depends_on:
- zookeeper
environment:
- ZK_HOSTS=zookeeper
ports:
- 9000:9000
networks:
- micro
```
9000 端口是 kafka 的一个 web 管理,后来感觉不好用...可以考虑去掉,我用了一个桌面的程序,kafka tools
kafka 还有挺多概念的,这里的话我就不细说了.我在 examples 下写了一个 demo,大家可以去看看,效果如下,大概模拟的多个微服务(群组 1 和群组 2 为两个微服务)消费一条事件(不能重复消费,然后分布到不同的微服务实例上)
![](img/06-%E8%81%9A%E5%90%88%E6%95%B0%E6%8D%AE.assets/6_1.png)
### CQRS
![](img/06-%E8%81%9A%E5%90%88%E6%95%B0%E6%8D%AE.assets/cqrs.jpg)
我创建了一个 ddd 的目录,里面包括了下面这些文件夹:
- commands 命令事件以及命令处理(图中的 Command Bus 和 Command Handler)
- events 领域事件和事件处理器
- domain 领域模型(放置聚合根之类)
- repository 领域模型持久化仓库
- services 领域服务
command 由 user interface(Controller 之类的)生成然后分配到 command bus,然后由 command bus 分配给 command handler 处理,之后给 domain services 处理业务逻辑
我们将原来的代码进行大量修改,主要是之前服务注册,连接数据库什么的都一股脑写在了 main 函数里面....现在我们进行了一些分层,使用写配置的方式来启动,有些地方也可以使用 IoC 来进行解耦
现在启动方式变成了这样,docker 和 docker-compose 也进行了修改,其中有对 postgres 数据库进行了一些修改(主要是因为 windows10 下的 docker 有个 bug,不能持久化数据)
这里因为 kafka 的原因...启动之后微服务可能会出现 kafka 连接不成功的问题,因为 kafka 容器启动之后不一定端口那些服务也启动了...所以可能需要手动启动一下微服务,我使用的 vscode
![](img/06-%E8%81%9A%E5%90%88%E6%95%B0%E6%8D%AE.assets/6_2.png)
```shell
go run auth #启动auth微服务
go run post #启动post微服务
```
![](img/06-%E8%81%9A%E5%90%88%E6%95%B0%E6%8D%AE.assets/6_3.png)
效果暂时就是现在这样- -...
下一节开始学习分布式事务
## 参考
- [https://www.cnblogs.com/yangecnu/p/Introduction-CQRS.html](https://www.cnblogs.com/yangecnu/p/Introduction-CQRS.html)
- [https://juejin.im/entry/58a15a4e0ce4630056440166](https://juejin.im/entry/58a15a4e0ce4630056440166)
- [https://www.jianshu.com/p/d4ca2133875c](https://www.jianshu.com/p/d4ca2133875c)
- [https://www.cnblogs.com/daoqidelv/p/7499662.html](https://www.cnblogs.com/daoqidelv/p/7499662.html)
- [https://www.cnblogs.com/KendoCross/p/9480955.html](https://www.cnblogs.com/KendoCross/p/9480955.html)
- [https://www.cnblogs.com/netfocus/archive/2012/02/12/2347938.html](https://www.cnblogs.com/netfocus/archive/2012/02/12/2347938.html)

View File

@ -0,0 +1,172 @@
---
title: 微服务架构本地尝试(七)-分布式事务
---
> 来了解一下分布式事务,事务简单来说就是:"要么什么都不做,要么做全套"
## 本地事务
平常我们的事务都是在同一个数据库中执行.
举个栗子:当你发帖的时候,需要发表帖子存入数据库,然后再给这个发帖的用户增加积分.如果帖子存入数据库失败,那么就不再往下执行;但是你增加积分失败了,然而你的帖子已经发布出去了,总不可能再去删除刚刚发的那条帖子吧.如果你使用了事务的话,那么可以在一项失败的时候,回滚事务,一切就像没有发生一样.
### ACID 模型
说到事务就不得不说 A(原子性)C(一致性)I(隔离性)D(持久性),来贴一下百科说明.
- Atomicity原子性一个事务transaction中的所有操作或者全部完成或者全部不完成不会结束在中间某个环节。事务在执行过程中发生错误会被恢复Rollback到事务开始前的状态就像这个事务从来没有执行过一样。即事务不可分割、不可约简。
- Consistency一致性在事务开始之前和事务结束以后数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设约束、触发器、级联回滚等。
- Isolation隔离性数据库允许多个并发事务同时对其数据进行读写和修改的能力隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别包括读未提交Read uncommitted、读提交read committed、可重复读repeatable read和串行化Serializable
- Durability持久性事务处理结束后对数据的修改就是永久的即便系统故障也不会丢失。
## 分布式事务
像上面一样的场景,但是这个时候我们是微服务架构啦.帖子数据库和积分数据库不在一起,他们分别在两个不同的数据库上面.
如果发帖失败,我们可以选择不再往下走,增加积分(我就当发帖先执行,积分增加后执行).但是如果发帖成功了,但是积分增加失败,这样就导致了数据的不一致.这种情况还算好,但是如果订单下成功了,钱没扣,造成的损失就大了不是.
于是我们需要通过一些手段,实现不同数据库之间的事务,这就是分布式事务了.
还有一些分库分表的应用也会用到分布式事务.
### CAP 定理
CAP 定理指的是在分布式**系统**中最多只能满足 C,A,P 中的两个需求.
#### 一致性(Consistency)
一致性指的在不同的服务器中,数据都是一致的.在分库分表的环境下,这个一致指的是 A 库中的 A 表和 B 库中的 A 表里面的数据是一模一样的;在我们上面所说的场景中指的是,发帖应得到的积分,和我们实际的积分数量应该是一致的.(为什么说这些,因为我感觉好多地方说的一致都是指的前者,我这个理解应该没错吧 hhh)
一致性也可以分为下面 3 类
- 强一致性:执行完成后,后面不管什么时候读取,数据都是更新完毕后的.比如发完贴之后,我立刻就可以看到我的积分增加.
- 弱一致性:执行完成后,不能保证数据会更新,多久会更新.比如发完贴之后,过了很久很久,我的积分都不一定变化了.(感觉是不是失败了...可能是某些场景下的吧)
- 最终一致性:执行完后,一段时间内,数据更新.比如发帖完之后,过了一两秒,我就能看到我的积分变化了.这个是弱一致性的一种特殊情况,大多数分布式系统也是采用这种模式.
#### 可用性(Availability)
每一次请求,都能得到服务器的非错响应.
可用性好理解,就是系统能否使用呗.能够访问帖子数据也能够访问积分数据库,这就算可用了,但是中间一项宕机那么这个系统就是不可用了(因为完成不了这个操作).
#### 分区容错性(Partition tolerance)
因为网络或者机器故障等影响,数据可能无法送达,还要保证系统能够继续正常运行,这时候我们需要在 CA 中做出选择.
又拿我上面的场景举个栗子:
我发帖服务器的数据无法传达给积分数据库.
如果我选择 A 可用性,那就是不理会积分数据库宕机,就会导致 C 不一致;如果我选择 C 一致性,那就回滚事务,不能够发帖成功,A 不可用.
如果是成年人我全都要
![](img/07-%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1.assets/bqb_4.gif)
我又要可用,又要一致.那我们基本就是是单机系统了,没有了 P,也就没有了网络因素,然后因为单机爆炸,一无所有.
分布式系统的话,因为无法 100%保证数据是否到达,必定会出现分区现象,当一旦出现故障,那么就必须选择 P,如果不选的话间接造成 CA 错误(数据不回滚,还特么报错),一无所有,所以按照 CAP 理论来说,分布式 P 是必选项.
### BASE 模型
BASE 模型是对 CAP 中 AP 的一个扩展.
- Basically Available 基本可用,出现故障时,允许损失部分可用的功能,保证核心功能可用.
- Soft state 软状态,允许系统中存在中间状态,这个状态不影响系统可用性,这里指的是 CAP 中的不一致.
- Eventually consistent 最终一致,一段时间后数据达到一致.
上一节中的 CQRS 和 EDA 就是属于这种模型,面向最终一致性.
## 分布式事务解决方案
大多数情况下,我们还是遵循不用分布式事务就不用分布式事务.这里是因为我们的微服务引出分布式事务.
下面列出一些分布式事务的解决方案.
### CQRS/EDA
先来说说我们上一节所用到的 CQRS,这种架构就是依赖 MQ 来保证的最终一致性
在本地开启一个事务,然后通过 MQ 发送一个消息,消息发送成功,完成事务,消息发送失败回滚事务.消息发送成功后就不用再考虑我们的发帖服务了,直接反馈给用户成功.
然后再通过 MQ 将消息发送到积分服务,如果这时候积分服务是不可用的,那么这条消息就会存储在 MQ 中,当积分服务复活时,再继续吧消息推送给积分服务,直到积分服务告诉 MQ 成功了为止,达到最终一致.
这个架构基本符合上面的 BASE 模型.
### 2PC(两阶段提交)
顾名思义就是两个阶段 0.0,感觉有点像五大流氓的投票,主流数据库也有相关的实现.
首先得有一个协调者(coordinator),若干参与者(participant)
#### 第一阶段
协调者:我们来商量一件事情
- 参与者 1:收到
- 参与者 2:收到
- 参与者 3:收到
如果有一个参与者没有收到,表示不能提交,不再继续执行.
#### 第二阶段
协调者:好了,都商量好了,执行吧
- 参与者 1:收到
- 参与者 2:收到
- 参与者 3:收到
如果有一个参与者,不干(执行失败之类的),那么协调者就会通知其他参与者算了不干了(回滚)
这个方案虽然简单,主流数据库也有相关实现,但是因为单个协调者,同步堵塞的问题不太适合高并发的场景.
### TCC(Try-Confirm-Cancel)
- Try:对所有业务进行检查,并预留必须业务资源
- Confirm:执行业务,不做用进行检查,只能使用 Try 阶段预留的业务资源.
- Cancel:取消业务执行,回滚事务.
Confirm 和 Cancel 的操作要求幂等(不管执行多少次,结果都是一样的)
假设 A 花 3 元买一瓶快乐水
Try 阶段:先检查 A 是否有足够的钱,水是否有足够的货;然后锁住 A 的金额和水的库存.如果全部成功进入 confirm 阶段,否则 cancel 阶段
Confirm 阶段:减去 A 中的钱和水的库存(只操作 Try 预留的业务资源);如果执行失败,继续进行 Confirm(所以要求幂等);直到执行成功为止.
Cancel 阶段:取消锁,或者回滚操作;如果执行失败也继续重试 Cancel,直到成功为止.
Confirm 和 Cancel,应该是可以异步的,适合于一些要求隔离性高,一致性强的业务.
### Saga
Saga 将各个事务拆分出来变成若干小事务$T_i$,每一个小事务都对应着一个回滚操作$C_i$.其中每一个操作也要求是幂等,而且$C_i$要求肯定是成功.
Saga 的执行顺序也很好理解:
成功顺序:$T_1>T_2>T_3>...T_n$
失败顺序:$T_1>T_2>...T_i>C_i>C_(i-1)...>C_2>C_1$
Saga 也可以不需要取消操作,但是要求 T 最终是成功的,失败重试,一直到成功为止.
对比 TCC 来说,Saga 的隔离性没有 TCC 的强,而且撤销操作每一步都需要一个 C(虽然不需要可以重试),感觉上是 TCC 更好,但是既然存在肯定是有它所使用的场景的.
如果再拿上面的场景举例子,就变成了下面这样:
成功:A 扣钱->水减货
失败:A 扣钱->水减货失败->回滚水减货->回滚 A 扣钱
感觉更适合有多步操作,而不需要立刻到达最终状态的场景
例如购买商品:
| 事务 | 取消 |
| -------- | -------- |
| 发起订单 | 取消订单 |
| 付款 | 退款 |
| 发货 | 回库 |
| 收货 | 退货 |
| 完成 | 打款 |

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,224 @@
---
title: Git命令总结
---
> Git 是一款免费、开源的分布式版本控制系统,用于敏捷高效地处理任何或小或大的项目。[1] Git 的读音为/gɪt/。
> Git 是一个开源的分布式版本控制系统,可以有效、高速的处理从很小到非常大的项目版本管理。[2] Git 是 Linus Torvalds 为了帮助管理 Linux 内核开发而开发的一个开放源码的版本控制软件。
>
> 学习了一下 git,但是很多命令暂时都还没记住,于是将现在所学到的 git 命令记录下来,方便查询
>
> 至于它们的作用,可以从下面的链接去学习
>
> [GIT 教程跳转](http://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000)
![img](img/Git命令总结.assets/0.jpg)
## 初始配置
配置用户名
git config --global user.name "CodFrm"
配置邮箱
git config --global user.email "yz@ggnb.top"
--global 表示全局 在整个 git 里生效
## Git 仓库命令
初始化 git 仓库
git init
将文件加入 git 仓库
git add -f file1 file2
-f 表示强制添加
将文件提交到仓库
git commit -m “提交说明”
查看仓库状态
git status
查看上一次文件变动
git diff
查看指定文件变动
git diff -- file
文件删除
git rm file
跳转到其他版本,如果是老的版本,使用 git log 将看不到之前的新版本,使用 git reflog
git reset --hard HEAD^
HEAD^表示上一个版本 HEAD^^表示上两个 HEAD~10 表示上 10 个
HEAD^ 也可以用 commit_id 表示想要跳转到的版本
### 分支
创建并切换分支
git checkout -b 分支名字
创建分支
git branch 分支
切换分支
git checkout 分支
查看分支,当前分支有\*
git branch
删除分支
git branch -d 分支
-D 强制删除
合并分支到当前分支
git merge 分支
### 日志
提交历史,查看每个版本的信息
git log
显示比较简单的信息 --pretty=oneline
显示分支合并图 --graph
命令历史,查看每个命令的信息
git reflog
撤销修改,回到该版本最初样式
git checkout -- file
将暂存区的文件撤回
git reset HEAD file
## 远程仓库
创建 SSH KEY
ssh-keygen -t rsa -C "code.farmer@qq.com"
关联远程库
git remote add origin git@github.com:CodFrm/StudyGit.git
克隆远程仓库
git clone git@github.com:CodFrm/StudyGit.git
克隆分支到本地
git checkout -b 本地分支名 origin/远程分支名
查看远程库的信息
git remote
git remote -v 显示更详细的信息
推送分支
git push origin 分支名字
第一次请加上-u 参数 git push -u origin 分支名字
从远程库获取新版本然后合并
git pull origin master
从远程库获取最新的分支版本
git fetch origin master
pull 和 fetch 理解还真有点困难....最好实践一下
fetch 是从远程拉取新加入的版本,要用 reset 跳转到这个新的版本上去才行,否则只是拉取
pull 就是从远程拉取了这些新的版本,然后将最新的和现在的合并
相当于 fetch 然后再 merge
### 暂存现场
储存现场
git stash
查看现场
git stash list
还原现场
git stash apply
删除现场
git stash drop
还原并删除现场
git stash pop
### 标签
设置标签
git tag 标签名字 commit_id
commit_id 可选
删除标签
git tag -d 标签名字
推送标签,推送标签到远程仓库
git push origin 标签名
git push origin --tags 推送所有标签
查看标签
git tag
查看标签信息
git show 标签名字
给标签说明
git tag -a 标签名字 -m "说明" commit_id
删除远程标签
git push origin :refs/tags/标签名字
## 其他
给 git 命令设置别名
git config --global alias.别名 命令

View File

@ -0,0 +1,187 @@
## git 和 github
> git 和 github 其实并不是同一样东西
### git
git 是由`Linus Torvalds`(没错,就是写 linux 内核的那个大佬)开发的一个分布式版本管理系统,主要用于代码的版本控制.
最简单的功能比如:你可以查看你之前的代码提交,与现在的代码进行比较,查看修改了什么内容,如果这个版本出现了 bug,你也可以找到写这个 bug 的罪魁祸首.
还有相同功能的软件:svn.
工具的下载地址:[https://git-scm.com/download/win](https://git-scm.com/download/win) linux 平台可以使用相关包管理工具安装,例如 Ubuntu:`apt install git`.本篇没有对 git 使用说明,可以去看我之前的文章:[Git 命令总结](./Git命令总结)
如果你觉得命令麻烦,也可以去下载 gui 工具,例如:[SourceTree](https://www.sourcetreeapp.com/)
### github
而 github 是一个面向开源软件源码托管的平台,也可以在 github 上建立自己的 git 私有仓库.也是一个大型的同性交友平台(雾)
如果你关注计算机方面的内容,那你应该听过`github万星xxx`.
当然不止软件了,也有很多奇奇怪怪的项目在 github 上收到欢迎.(女装仓库,地球今天毁灭了吗?)
与此相关的平台还有 gitlab(用法和 github 差不多,并且可以自己部署平台)
## 来一个 github 账号
github 账号注册的门槛是很低的,直接访问:[https://github.com/](https://github.com/,),在对话框中输入你的账号就可以了.
![](img/github不完全指南.assets/5e3f60abfef2b1e1e26e817cd087a671-20240316210151748.png)
## 主页
> 可以看我的图,应该是很详细了,我的主页是:[https://github.com/CodFrm](https://github.com/CodFrm),欢迎关注和点星星 ⭐?
![](img/github不完全指南.assets/663363614eb8c06fd44646fe8b215911-20240316210211762.png)
对于想了解你的人来说,第一眼看的应该是 contributions,如果你下方越绿,证明你提交的次数越多,第一映像也会很好(对求职来说),不过也会有一些很无聊的人,提交一些无用的信息去刷 commit.
最下方的贡献图还有一张表情包
![](img/github不完全指南.assets/776dfe3817e22477f0821137c024d803.png)
所以也是你努力的一种证明啦.
中间的项目,是可以自己选择的,一般都是选自己比较自信的项目放在主页,展示给别人看,如果你给其它项目提交过 pr(pull request,后面会说).你也可以将它展示在你的主页(哪怕你只提交了一行,甚至是给它删了 n 行![](img/github不完全指南.assets/90f33163cdebfabfbefa8b9768136d36.png))
## 仓库
### 创建仓库
你可以在最右上角找到这个,点击就可以进行创建的步骤了
![](img/github不完全指南.assets/32ec450ca5a75898cd0c88ff28745ad0.png)
Owner 是仓库的所有者,你可以给自己的账号创建,也可以在组织中创建(有权限的话),public 和 private 表示公有仓库和私有仓库.
`Initialize this repository with a README`是创建一个`README`的文档,以`README`作为文件名的文件,会作为仓库的一个说明文档,显示在你项目主页的下方.
.gitignore 是 git 提交时忽略的文件列表,license 是项目的开源协议(一般都不需要勾选)
![](img/github不完全指南.assets/bf7d05eafb73055e49307be39d8d43a4.png)
创建完之后会有一些命令引导,提交仓库
![](img/github不完全指南.assets/2c1181a13665c1f2617b7970f408ca10.png)
### 提交
#### 添加密钥到 github
提交之前,我们需要安装`git for windows`(如果你是 windows 系统的话),然后打开`Git Bash`,生成密钥添加到 github(因为这里我推荐是使用 ssh 模式,不推荐 https 模式),步骤如下
![](img/github不完全指南.assets/88c26ecc9d3884421657ebb9ed6772c0.png)
执行`ssh-keygen -t rsa -C "youremail@example.com"`命令就开源了,邮箱为你的注册邮箱,然后输入`cat ~/.ssh/id_rsa.pub`(命令含义就是将你**用户**目录下的.ssh/id_rsa.pub 输出到窗口),会输出一大串字母,这就是你的公钥了,将它复制提交到 github(git bash 要右键,copy,不能 ctrl+c)
![](img/github不完全指南.assets/cbce7865f88401dec815fb2eef0196f1.png)
setting->SSH and GPG keys->New ssh key
![](img/github不完全指南.assets/573cec1e7f6e6e50aafb331711f85d64.png)
然后 Add ssh key 就 ok 啦,后面好像会要你输入你的密码.
#### 提交源码到 github
依旧是刚刚的`git bash`,如果之前没有 git 仓库,需要先初始化一次,然后绑定远程仓库(远程仓库就是你的 github 仓库啦)
```bash
git init
git remote add origin git@github.com:CodFrm/test.git
```
如果已经有远程仓库了,那么执行第二行就可以了.
然后开始你的代码编写或者加点文件进去....ing
完成后:
命令含义你可以看我的另外一篇博文:[Git 命令总结](./Git命令总结)
```bash
git add *
git commit -m "我添加了一个文件"
git push origin master
```
然后刷新看看 github 上面的仓库主页:
![](img/github不完全指南.assets/7ef3511b2fba244c1e778d0f50af1417.png)
这里我推荐一个插件:[Octotree](https://chrome.google.com/webstore/detail/octotree/bkhaagjahfmjljalopjnoealnfndnagc?hl=en),可以用来看仓库文件,是一个很方便的插件
### 一些常见的东西
#### star
这就是我们常说的星星 ⭐ 了,大概相对于我们的赞吧,越多代表你的项目越受欢迎.(所以快给我点个吧:[https://github.com/scriptscat/scriptcat](https://github.com/scriptscat/scriptcat)?)
#### Watch
你项目的关注人数,当你关注的时候,项目发生更新(新的 issue,pr 等)的时候都会通知你
#### fork
相对于复制一份你的项目,fork 之后,别人就可以帮你修改你的代码.
#### issues
有点像论坛,你的代码出现 bug/问题询问/意见反馈,其他人都可以在这里开一个帖子.
#### pull request
简称 pr,如果你看 issue,你可能会看到仓库作者说,`提个pr`,意思就这个啦.其他人 fork 了你的仓库后,在他自己的仓库对你的代码进行修改,修改好之后,就可以在你的项目里面提交一个`pull request`,你可以将他的修改合并到你的代码里面,当然也可以拒绝.
#### Action
是 github 最近推出的一项 CI/CD 服务, 具体可以去看我原来的文章:[github action 入门](../../ops/CI&CD/github-actions-入门)
#### project
是当前窗口的工作薄,将做些什么内容(不过好像大部分人都没用)
#### wiki
顾名思义,仓库的文章(不过也好像没啥人用...)
#### commit
仓库的提交数量,点进去也可以看到详细的提交内容
#### branch
仓库的分支,点进去可以看到分支详细内容
#### release
当前仓库发布的版本,一般都是可以稳定使用的版本,点进去可以进行下载.
#### contributions
贡献人数,你可以点进去看看有那些大佬为这个仓库做了贡献,然后 follow 他
![](img/github不完全指南.assets/3XKDB6V81@6UOPTK3739-300x257.png)
#### 其它
如果你不想用 git,你也可以在网页上进行一些简单的操作
![](img/github不完全指南.assets/09c82b870a00634bd45e69726c20264f.png)
## 首页
![](img/github不完全指南.assets/78417ee77ecceffbfe15bcd586b32f87.png)
首页其实也没啥,我最主要的用处就是,看那些关注了的大佬的动向,看大佬又 star 了什么厉害的项目,大佬又有什么新的动作了,然后就是一些 github 的新闻之类了
## End
> 推荐几个项目
github 不仅仅是一个程序员的平台,在上面还有漂亮的小哥哥:[https://github.com/komeiji-satori/Dress](https://github.com/komeiji-satori/Dress),帅气的小姐姐:[https://github.com/greenaway07/GirlDress](https://github.com/greenaway07/GirlDress),教你使用 github 的一些基本操作,也有教你炒股买房的教程:[https://github.com/houshanren/hangzhou_house_knowledge](https://github.com/houshanren/hangzhou_house_knowledge).如果你遇到什么需要的东西,也可以去 github 先去搜一搜
![](img/github不完全指南.assets/0b880e9274128c9631e0f2520974d295.png)
不皮了,安利几个正经的项目:
- [https://github.com/ruanyf/weekly](https://github.com/ruanyf/weekly) 科技爱好者周刊,每周五发布,建议 watch,每天看新报
- [https://github.com/sindresorhus/awesome](https://github.com/sindresorhus/awesome) 各种计算机很棒的资料
- [https://github.com/ruanyf/free-books](https://github.com/ruanyf/free-books) 互联网上的免费书籍

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

6
docs/note/README.md Normal file
View File

@ -0,0 +1,6 @@
---
title: 笔记
sidebar_position: 0
---
这里是笔记记录相关的内容,包括学习笔记、工具资源、日常生活等等。

View File

@ -0,0 +1,4 @@
{
"label": "笔记",
"position": 5
}

View File

@ -0,0 +1,54 @@
---
title: 我为什么换掉Wordpress而选择Docusaurus
---
在此之前,我一直使用 Wordpress 来做我的博客,最近准备重新开始写,为什么决定换掉它呢?
首先 Wordpress 很强大完全符合我的所有需求评论、统计、SEO、主题、插件等等但是我还是决定换掉它原因主要有以下几点
- 维护成本高Wordpress 需要配置 PHP、Mysql、Nginx 等环境而且插件、主题、Wordpress 本身都需要定期更新,这些都需要花费时间。我很久没有更新了,结果代码高亮插件出了问题,可能是兼容性问题,也可能是我自己的配置问题,并且后台总是能看到告警之类的信息,总之,我不想花时间去解决了。
- 迁移成本高:在之前我换过好几次服务器环境,每次都需要重新配置环境,迁移数据库,迁移文件,这些都需要花费时间;虽然现在应该很少会迁移了。
- 资源占用高Wordpress 占用资源较高,虽然并没有对我产生什么压力,但是我还是希望能够用更少的资源来跑我的博客。
- 编辑模式不友好Wordpress 的编辑模式不够友好,虽然可以安装 Markdown 编辑器,但是我更喜欢使用本地的 Markdown 编辑器。
其实最主要的原因还是编辑模式的问题,我更喜欢使用本地的 Markdown 编辑器,然后通过 Git 来管理我的文章,这样我可以更方便的查看历史版本,也可以更方便的进行版本控制。
## 为什么选择了 Docuasaurus
在选择新的博客程序的时候我也考虑了很多比如Typecho、Hexo、VitePress、Hugo、docsify 等等。
我主要是从下面几个方面来考虑的,你也可以作为参考:
- 界面美观
- 本地 Markdown 编辑
- SSG静态站点生成
- 使用 git 管理文章
SSG 是希望能够生成静态站点,这样就不需要配置 PHP、Mysql、Nginx 等环境了,也不需要担心安全问题,而且可以更快的访问速度。
然后就是 git 管理和本地 Markdown 编写,这样对于博客程序的选择影响就不会太大了,如果用得不喜欢可以很方便的切换,博客程序出了问题,使用 git 也可以很方便的回滚。
这么筛选下来其实还是有很多选择的Hexo 和 Hugo 都是很不错的,他们也是很流行的 BLOG 程序,并且有丰富的主题,
相反 VitePress、docsify、Docusaurus 它们更适合文档程序,主题相对较少,界面相对简洁。
但是我很快就否决的 Hexo 和 Hugo因为他们的界面不够美观我更喜欢简洁的界面而且我也不需要那么多的主题很多主题都比较花哨。
并且我看见了很多大佬也是用 VitePress、docsify、Docusaurus 来写博客的,他们的首页都是很简洁的,我也很喜欢。
最开始其实是有些想使用 VitePress 的,和 Docuasaurus 的官网首页相比,我更喜欢 VitePress 的界面。
但是我主要是使用 React 进行开发的,然后看到了一些使用 Docuasaurus 的博客,他们都进行了一些定制,界面也是很不错的,于是我也就选择了 Docuasaurus并且参考了他们的博客写了一下我的首页。
如果没有开发能力的话,我还是很推荐使用 Hugo、Hexo 的,他们的主题很多,界面也很美观,而且也很流行,有很多人使用,遇到问题也比较容易找到解决方案。
## Docusaurus 的定制
Docusaurus 是有 blog 功能的,但是 blog 不能支持左侧的目录树,然后 docs 又不支持文章时间,于是我写了一个 docs 时间生成的插件。
然后 Docuasaurus 也只是一个静态站点生成器,不支持评论、统计等等功能,需要一些外部依赖来实现。我主要改造如下,你也可以进入我的博客仓库查看:
- 修改首页
- 增加 Docs 文章时间排序
- 接入 giscus 评论
- 接入 Google Analytics 统计
- 增加了 Markdown lint
在以后或许还会用 golang 写一个提供服务的后端,来实现其他更多的功能。
总之不同的博客程序都有不同的优势,也有不同的劣势,选择适合自己的就好。

View File

@ -0,0 +1,144 @@
---
title: PHP 扩展开发(一)-骨架
---
> 学习了这么久的 php,还一直停留在 CURD 也太捞了,来接触一下扩展开发
> 官方的文档:[http://php.net/manual/zh/internals2.php](http://php.net/manual/zh/internals2.php) 可以 mark 一下
## 环境
- php7.2
- ubuntu18.04
- gcc 7.3.0
- make 4.1
## 开始
### ext_skel
> [http://php.net/manual/zh/internals2.buildsys.skeleton.php](http://php.net/manual/zh/internals2.buildsys.skeleton.php)
首先我们要利用 php 给我们提供的 ext_skel 脚本工具生成我们扩展的骨架,这个文件一般在 php 的源码的 ext 目录下面
这里我弄一个扩展名字为**study**的扩展 --extname 这个参数为扩展名字,还有其他的参数可以在上面的链接文档中看到
```shell
huanl@huanl-CN15S:/www/server/php/72/src/ext$ sudo ./ext_skel --extname=study
Creating directory study
Creating basic files: config.m4 config.w32 .gitignore study.c php_study.h CREDITS EXPERIMENTAL tests/001.phpt study.php [done].
To use your new extension, you will have to execute the following steps:
1. $ cd ..
2. $ vi ext/study/config.m4
3. $ ./buildconf
4. $ ./configure --[with|enable]-study
5. $ make
6. $ ./sapi/cli/php -f ext/study/study.php
7. $ vi ext/study/study.c
8. $ make
Repeat steps 3-6 until you are satisfied with ext/study/config.m4 and
step 6 confirms that your module is compiled into PHP. Then, start writing
code and repeat the last two steps as often as necessary.
```
这里创建完成之后,就多了一个 study 的目录,我们可以进入这个扩展目录,进行一些操作,这里的话,因为我的用户没有权限,所以我直接的给了 777 权限
```shell
huanl@huanl-CN15S:/www/server/php/72/src/ext$ sudo chmod -R 777 study/
```
### 编译安装
#### 目录结构
之后我们进入这个扩展目录,有这些文件,官方的文档:[http://php.net/manual/zh/internals2.structure.files.php](http://php.net/manual/zh/internals2.structure.files.php)
```shell
huanl@huanl-CN15S:/www/server/php/72/src/ext$ cd study/
huanl@huanl-CN15S:/www/server/php/72/src/ext/study$ ls
config.m4 CREDITS php_study.h study.php
config.w32 EXPERIMENTAL study.c tests
```
等下我们需要修改**config.m4**文件,这相当于是一个编译配置的文档,是 Unix 下的,还有一个**config.w32**看名字就知道是 Windows 下的
**study.c**和**php_study.h**这是依照我们的扩展名称来帮我们生成的两个源文件,包括了一些宏的定义和函数声明等等
**study.php**可以用 php cli 来测试我们的扩展是否安装成功
#### config.m4
> [http://php.net/manual/zh/internals2.buildsys.configunix.php](http://php.net/manual/zh/internals2.buildsys.configunix.php)
要进行一下修改,是动态编译成 so 库还是静态编译进 php 里面,这里我们扩展开发自然是动态编译成 so 库,不然还得重新编译 php
去掉前面的 dnl,例如下面这个
```
dnl If your extension references something external, use with:
# 编译成so库
PHP_ARG_WITH(study, for study support,
Make sure that the comment is aligned:
[ --with-study Include study support])
dnl Otherwise use enable:
# 静态编译
dnl PHP_ARG_ENABLE(study, whether to enable study support,
dnl Make sure that the comment is aligned:
dnl [ --enable-study Enable study support])
```
### 编译
我们需要用**phpize**生成编译的配置的文件**configure**,然后**make**,**make install**就完成了,make install 的时候注意用 sudo
```shell
huanl@huanl-CN15S:/www/server/php/72/src/ext/study$ phpize
Configuring for:
PHP Api Version: 20170718
Zend Module Api No: 20170718
Zend Extension Api No: 320170718
huanl@huanl-CN15S:/www/server/php/72/src/ext/study$ ./configure --with-php-config=/www/server/php/72/bin/php-config
# 这里注意配置php的配置文件
huanl@huanl-CN15S:/www/server/php/72/src/ext/study$ make
huanl@huanl-CN15S:/www/server/php/72/src/ext/study$ sudo make install
Installing shared extensions: /www/server/php/72/lib/php/extensions/no-debug-non-zts-20170718/
```
### 配置
完成之后我们要在**php.ini**文件中加载扩展,不然我们运行`php study.php`的时候不会成功
这样可以看 ini 文件在哪里
```shell
huanl@huanl-CN15S:/www/server/php/72/src/ext/study$ php --ini
Configuration File (php.ini) Path: /www/server/php/72/etc
Loaded Configuration File: /www/server/php/72/etc/php.ini
Scan for additional .ini files in: (none)
Additional .ini files parsed: (none)
```
增加一行
```
extension=study.so
```
## 完成
这时候我们执行目录下的那个`study.php`
```shell
huanl@huanl-CN15S:/www/server/php/72/src/ext/study$ php study.php
Functions available in the test extension:
confirm_study_compiled
Congratulations! You have successfully modified ext/study/config.m4. Module study is now compiled into PHP.
```

View File

@ -0,0 +1,237 @@
---
title: PHP扩展开发(二)-函数
---
> 弄好骨架之后,我们得给我们的扩展增加些 php 能够调用的函数,这里我们使用 vscode 进行开发
## 开发环境
> 给我们的 vscode 装好扩展,然后配置一下 include 路径
![](img/02-%E5%87%BD%E6%95%B0.assets/2018-09-21-14-20-32-%E7%9A%84%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE-300x132.png)
点击生成一个配置文件我的配置如下php 的路径看自己的来定,我这里是宝塔安装的,路径为:**/www/server/php/72/include/php**,主要是自动提示
```json
{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/**",
"/www/server/php/72/include/php",
"/www/server/php/72/include/php/main",
"/www/server/php/72/include/php/Zend",
"/www/server/php/72/include/php/TSRM"
],
"defines": [],
"compilerPath": "/usr/bin/gcc",
"cStandard": "c11",
"cppStandard": "c++17",
"intelliSenseMode": "clang-x64"
}
],
"version": 4
}
```
## 开发
### 返回和输出
> 假设我这里想添加一个输出一些东西的函数
这里用到了三个宏,**PHP_FUNCTION**,**RETURN\_\***,**PHP_FE**
php_printf 是 php 提供的一个输出函数
```c
PHP_FUNCTION(study_ext_print) {
php_printf("我是输出到页面的内容\n");
RETURN_STRING("学习PHP扩展开发~~");
}
/* }}} */
/* {{{ study_functions[]
*
* Every user visible function must have an entry in study_functions[].
*/
const zend_function_entry study_functions[] = {
PHP_FE(confirm_study_compiled, NULL) /* For testing, remove later. */
PHP_FE(study_ext_print, NULL) /* 学习插件输出 */
PHP_FE_END /* Must be the last line in study_functions[] */
};
/* }}} */
```
然后再 makesudo make install
#### 调试跑一次
修改我们目录下的`study.php`,在最后写一行,我们刚刚编写的这个函数
```php
echo study_ext_print()."\n";
```
输出,成功
```
huanl@huanl-CN15S:/www/server/php/72/src/ext/study$ php study.php
Functions available in the test extension:
confirm_study_compiled
study_ext_print
Congratulations! You have successfully modified ext/study/config.m4. Module study is now compiled into PHP.
我是输出到页面的内容
学习PHP扩展开发~~
```
#### 这里再说下这三个宏
##### PHP_FUNCTION
使用这个宏会将我们的函数最终定义成如下的形式
```cpp
void zif_study_ext_print(zend_execute_data *execute_data, zval *return_value)
```
~~官网上的是 php5.3 的版本,我这里是 php7所以是这样如果有什么错误还望指出~~
然后因为这里的返回值是 void所以在我们写函数的时候`return`不能带值
##### RETURN\_\*
这个宏一看就知道是 php 给我们的返回值,除了我上面所写的`RETURN_STR`外还有其他的类型
```c
#define RETURN_BOOL(b) { RETVAL_BOOL(b); return; }
#define RETURN_NULL() { RETVAL_NULL(); return;}
#define RETURN_LONG(l) { RETVAL_LONG(l); return; }
#define RETURN_DOUBLE(d) { RETVAL_DOUBLE(d); return; }
#define RETURN_STR(s) { RETVAL_STR(s); return; }
#define RETURN_INTERNED_STR(s) { RETVAL_INTERNED_STR(s); return; }
#define RETURN_NEW_STR(s) { RETVAL_NEW_STR(s); return; }
#define RETURN_STR_COPY(s) { RETVAL_STR_COPY(s); return; }
#define RETURN_STRING(s) { RETVAL_STRING(s); return; }
#define RETURN_STRINGL(s, l) { RETVAL_STRINGL(s, l); return; }
#define RETURN_EMPTY_STRING() { RETVAL_EMPTY_STRING(); return; }
#define RETURN_RES(r) { RETVAL_RES(r); return; }
#define RETURN_ARR(r) { RETVAL_ARR(r); return; }
#define RETURN_OBJ(r) { RETVAL_OBJ(r); return; }
#define RETURN_ZVAL(zv, copy, dtor) { RETVAL_ZVAL(zv, copy, dtor); return; }
#define RETURN_FALSE { RETVAL_FALSE; return; }
#define RETURN_TRUE { RETVAL_TRUE; return; }
```
##### PHP_FE
这个宏帮助我们生成一个和 php 函数相关的结构体
```c
//宏如下PHP_替换成了ZEND_
#define ZEND_FENTRY(zend_name, name, arg_info, flags) { #zend_name, name, arg_info, (uint32_t) (sizeof(arg_info)/sizeof(struct _zend_internal_arg_info)-1), flags },
#define ZEND_FE(name, arg_info) ZEND_FENTRY(name, ZEND_FN(name), arg_info, 0)
//感觉就是帮我们省事了,不需要我们去重写结构体,上面那些结构体也是
typedef struct _zend_function_entry {
const char *fname;//我们的php函数名
zif_handler handler;//相当于再调用一次PHP_FUNCTIONc中函数的指针
const struct _zend_internal_arg_info *arg_info;//参数的信息就上一个函数来说我们是NULL
uint32_t num_args;//参数个数
uint32_t flags;//flags这里是0
} zend_function_entry;
```
### 参数
> 假设来一个两数相加的函数,这时候我们就需要传送参数了
> 参考:[https://wiki.php.net/rfc/fast_zpp](https://wiki.php.net/rfc/fast_zpp)
我的 c 代码如下
```cpp
//定义参数结构
ZEND_BEGIN_ARG_INFO(add_param,0)
ZEND_ARG_INFO(0,num1)
ZEND_ARG_INFO(0,num2)
ZEND_END_ARG_INFO()
//函数
PHP_FUNCTION(study_add)
{
long long num1=0,num2=0;
if(zend_parse_parameters(ZEND_NUM_ARGS() , "ll", &num1,&num2)==FAILURE){
RETURN_LONG(-1)
}
RETURN_LONG(num1+num2)
}
/* }}} */
/* {{{ study_functions[]
*
* Every user visible function must have an entry in study_functions[].
*/
const zend_function_entry study_functions[] = {
PHP_FE(confirm_study_compiled, NULL) /* For testing, remove later. */
PHP_FE(study_ext_print, NULL) /* 学习插件输出 */
PHP_FE(study_add, add_param) /* 两数相加 */
PHP_FE_END /* Must be the last line in study_functions[] */
};
```
```php
echo "study_add:".study_add(10,123456);
```
#### 宏
##### ZEND_BEGIN_ARG_INFOZEND_ARG_INFOZEND_END_ARG_INFO
定义参数,我把源码贴上来
```cpp
#define ZEND_BEGIN_ARG_INFO_EX(name, _unused, return_reference, required_num_args) \
static const zend_internal_arg_info name[] = { \
{ (const char*)(zend_uintptr_t)(required_num_args), 0, return_reference, 0 },
#define ZEND_BEGIN_ARG_INFO(name, _unused) \
ZEND_BEGIN_ARG_INFO_EX(name, 0, ZEND_RETURN_VALUE, -1)
#define ZEND_END_ARG_INFO() };
#define ZEND_ARG_INFO(pass_by_ref, name) { #name, 0, pass_by_ref, 0},
```
那么通过这些宏的转换,变成了这样
```cpp
static const zend_internal_arg_info add_param[] = {
{ (const char*)(zend_uintptr_t)(-1), 0, 0, 0 },
{ "num1", 0, 0, 0},
{ "num2", 0, 0, 0},
};
```
#### zend_parse_parameters
获取参数
```cpp
//声明
ZEND_API int zend_parse_parameters(int num_args, const char *type_spec, ...);
```
第一个参数是我们要获取的参数的个数,第二个是 参数的格式化字符串,后面的是变量指针
| 数据类型 | 字符 | c 对应类型 |
| -------- | ---- | ----------- |
| Boolean | b | zend_bool |
| Long | l | long |
| Double | d | double |
| String | s | char\*, int |
| Resource | r | zval\* |
| Array | a | zval\* |
| Object | o | zval\* |
| zval | z | zval\* |
#### ZEND_NUM_ARGS
参数个数,一般这样填就好了

View File

@ -0,0 +1,385 @@
---
title: PHP扩展开发(三)---类
---
> 前面已经了解了函数和参数,今天来了解一下类
## 例子
定义了一个 **study_ext_class** 类,里面只有一个 **print** 方法
类使用 **PHP_ME**和**PHP_METHOD** 宏,与方法最大的不同的地方是类需要注册
这里我写了一个 **init_class** 方法,**PHP_MINIT_FUNCTION**中调用,主要是需要注册类
```c
/* {{{ PHP_MINIT_FUNCTION
*/
PHP_MINIT_FUNCTION(study)
{
/* If you have INI entries, uncomment these lines
REGISTER_INI_ENTRIES();
*/
init_class();
return SUCCESS;
}
/* }}} */
PHP_METHOD(study_ext_class,print)
{
php_printf("你调用了study_ext_class的print方法\n");
}
/* }}} */
const zend_function_entry study_class_method[]={
PHP_ME(study_ext_class,print,NULL,ZEND_ACC_PUBLIC)/* study_ext_class的print方法 */
PHP_FE_END
};
zend_class_entry *study_ce;
void init_class(){
zend_class_entry ce;
INIT_CLASS_ENTRY(ce, "study_ext_class" , study_class_method);
study_ce = zend_register_internal_class(&ce);
}
```
### PHP_MINIT_FUNCTION
这是我们扩展启动时会执行的一个函数,所以在这里注册类
```c
#define ZEND_MODULE_STARTUP_N(module) zm_startup_##module
#define INIT_FUNC_ARGS int type, int module_number
#define ZEND_MODULE_STARTUP_D(module) int ZEND_MODULE_STARTUP_N(module)(INIT_FUNC_ARGS)
//之后
int zm_startup_study(int type, int module_number);
```
### PHP_METHOD
这两个宏和我们前面函数哪里的 **PHP_FE** **PHP_FUNCTION** 差不多
```c
#define ZEND_MN(name) zim_##name
#define ZEND_METHOD(classname, name) ZEND_NAMED_FUNCTION(ZEND_MN(classname##_##name))
//PHP_METHOD 最终定义成了这样
void zim_study_ext_class_print(zend_execute_data *execute_data, zval *return_value);
```
### PHP_ME
**PHP_ME** 相比原来的 **PHP_FE** 多了几个参数,主要是方法的属性和类名
```c
#define ZEND_ME(classname, name, arg_info, flags) ZEND_FENTRY(name, ZEND_MN(classname##_##name), arg_info, flags)
```
flags 是方法的属性,我们可以用 **|** 连接它们
```c
/* method flags (types)方法类型 */
#define ZEND_ACC_STATIC 0x01
#define ZEND_ACC_ABSTRACT 0x02
#define ZEND_ACC_FINAL 0x04
#define ZEND_ACC_IMPLEMENTED_ABSTRACT 0x08
/* method flags (visibility)访问属性 */
/* The order of those must be kept - public < protected < private */
#define ZEND_ACC_PUBLIC 0x100
#define ZEND_ACC_PROTECTED 0x200
#define ZEND_ACC_PRIVATE 0x400
#define ZEND_ACC_PPP_MASK (ZEND_ACC_PUBLIC | ZEND_ACC_PROTECTED | ZEND_ACC_PRIVATE)
#define ZEND_ACC_CHANGED 0x800
#define ZEND_ACC_IMPLICIT_PUBLIC 0x1000
/* method flags (special method detection)构造函数和析构 */
#define ZEND_ACC_CTOR 0x2000
#define ZEND_ACC_DTOR 0x4000
/* method flag used by Closure::__invoke() */
#define ZEND_ACC_USER_ARG_INFO 0x80
/* method flag (bc only), any method that has this flag can be used statically and non statically. */
#define ZEND_ACC_ALLOW_STATIC 0x10000
```
### 类注册
完成了上面的,编译&安装,实例化我们的类是是会报错的,因为 php 不知道你有那些类
方法的话,在最后面通过 **ZEND_GET_MODULE** 生成了一个 **get_module** 的接口将方法进行了返回
```c
zend_class_entry *study_ce;
void init_class(){
zend_class_entry ce;
INIT_CLASS_ENTRY(ce, "study_ext_class" , study_class_method);
study_ce = zend_register_internal_class(&ce);
}
```
#### INIT_CLASS_ENTRY
这一个宏的作用是生成这个类的结构,包括类的名称,方法,然后返回在 ce 这个变量中
```c
//太长了,就只贴了一部分
#define INIT_OVERLOADED_CLASS_ENTRY(class_container, class_name, functions, handle_fcall, handle_propget, handle_propset) \
INIT_OVERLOADED_CLASS_ENTRY_EX(class_container, class_name, sizeof(class_name)-1, functions, handle_fcall, handle_propget, handle_propset, NULL, NULL)
#define INIT_CLASS_ENTRY(class_container, class_name, functions) \
INIT_OVERLOADED_CLASS_ENTRY(class_container, class_name, functions, NULL, NULL, NULL)
```
#### zend_register_internal_class
显而易见的是将我们的类的信息告诉 php注册进去还有一个 **zend_register_internal_class_ex** 的函数,可以指定父类,然后这个函数返回我们的这个类的指针
## 构造和析构
函数列表哪里标记一下 **ZEND_ACC_CTOR** 或者 **ZEND_ACC_DTOR** 就好了
```c
PHP_METHOD(study_ext_class,__construct)
{
php_printf("study_ext_class构造函数\n");
}
PHP_METHOD(study_ext_class,__destruct)
{
php_printf("study_ext_class析构函数\n");
}
const zend_function_entry study_class_method[]={
PHP_ME(study_ext_class,__construct,NULL,ZEND_ACC_CTOR)/* 构造 */
PHP_ME(study_ext_class,__destruct,NULL,ZEND_ACC_DTOR)/* 析构 */
PHP_ME(study_ext_class,print,NULL,ZEND_ACC_PUBLIC)/* study_ext_class的print方法 */
PHP_FE_END
};
```
## 类属性
在初始化注册类的时候,使用 **zend*declare_property*\*** 给我们的类添加属性,还可以给他们赋予默认值
```c
void init_class(){
zend_class_entry ce;
INIT_CLASS_ENTRY(ce, "study_ext_class" , study_class_method);
study_ce = zend_register_internal_class(&ce);
zend_declare_property_null(study_ce,"attr",sizeof("attr")-1,ZEND_ACC_PUBLIC);
zend_declare_property_long(study_ce,"num",sizeof("num")-1,100,ZEND_ACC_STATIC|ZEND_ACC_PUBLIC);
}
```
```php
$a->attr="2333s";
echo '静态属性:study_ext_class::$num:'.study_ext_class::$num."\n";
echo '属性:study_ext_class::$attr:'.$a->attr."\n";
```
输出
```
静态属性:study_ext_class::$num:100
属性:study_ext_class::$attr:2333s
```
### 类指针和属性读取
上面写了怎么去定义属性,但是如果是在类里面要怎么使用属性呢?我们需要用一个 **getThis** 的宏来获取当前这个类的指针,我就偷懒直接在原来的 **print** 中添加了
```c
zval *attr;
attr=zend_read_property(Z_OBJCE_P(getThis()),getThis(),"attr",sizeof("attr")-1,0,NULL);
php_var_dump(attr, 1);
if(Z_TYPE_P(attr)==IS_STRING){
php_printf("attr的值为:%s\n",attr->value.str->val);
}
```
#### zend_read_property
这个函数用于获取属性,还有`zend_read_static_property`,用法相同,不过这个是获取静态的属性,关于更新属性可以使用`zend_update_property`
```c
ZEND_API zval *zend_read_property(zend_class_entry *scope, zval *object, const char *name, size_t name_length, zend_bool silent, zval *rv);
```
第一个参数 **scope** 是这个类的指针,在之前的`study_ce = zend_register_internal_class(&ce);`获取,不过也可以这样获取`Z_OBJCE_P(getThis())`
第二个参数 **object** 是当前的对象,我们可以用`getThis`这个宏获取
第三个参数和第四个参数分别是 属性的名称和属性的长度
第五个参数 **silent** 用于是假设属性不存在的情况下是否报错
最后一个参数 **rv** 为魔术方法所返回的,如果不是魔术方法所返回的是一个**NULL**值,可以看我下面这个例子
```c
zval *attr,*rv=NULL;
attr=zend_read_property(study_ce,getThis(),"attr",sizeof("attr")-1,0,rv);
if(Z_TYPE_P(attr)==IS_STRING){
php_printf("attr的值为:%s,%d\n",attr->value.str->val,rv);
}
```
#### getThis
获取对象指针,不多说了
```c
#define EX(element) ((execute_data)->element)
#define getThis() ((Z_TYPE(EX(This)) == IS_OBJECT) ? &EX(This) : NULL)
```
### 类参数
其实和函数的参数一样,还有一个类似的`zend_parse_method_parameters`我用的时候总是错误,还没明白这个函数是干什么的,而且找不到说明的资料=\_=,后面附上两个源码的区别再看看
```c
PHP_METHOD(study_ext_class,sum)
{
zend_long parma_num=0;
zval* this=getThis();
zval* static_num=zend_read_static_property(Z_OBJCE_P(this),"num",sizeof("num")-1,0);
if(zend_parse_parameters(ZEND_NUM_ARGS(),"l",&parma_num)==FAILURE){
RETURN_LONG(-1)
}
if(Z_TYPE_P(static_num)==IS_LONG){
RETURN_LONG(static_num->value.lval+parma_num)
}
RETURN_LONG(-1)
}
ZEND_BEGIN_ARG_INFO(sum_arg,0)
ZEND_ARG_INFO(0,num)
ZEND_END_ARG_INFO()
```
### 探究
如果我们的第二个参数**this_ptr**为 NULL 或者不是**OBJECT**类型的话,那么效果和**zend_parse_parameters**一样,我之前填的是 this 指针,所以跳到了 else 分支
else 分之第一句就是`p++;`表示字符串往后面移动一位,我填的参数是是一个单独的 **l** 然后一移动....没啦,后面还有两个 va_arg
通过后面这两个得知,我们的两个参数,一个是 **zval** 的,一个是 **zend_class_entry\*** 我们传入的 **this_ptr** 参数会赋值给 **object** 也就是我们后面的第四个参数,第五个是我们类的指针
```c
object = va_arg(va, zval **);
ce = va_arg(va, zend_class_entry *);
*object = this_ptr;
```
看后面这一段,好像是校验类的,所以我觉得这个`zend_parse_method_parameters``zend_parse_parameters`的区别就在这里method 能够对类进行校验
```c
if (ce && !instanceof_function(Z_OBJCE_P(this_ptr), ce)) {
zend_error_noreturn(E_CORE_ERROR, "%s::%s() must be derived from %s::%s",
ZSTR_VAL(Z_OBJCE_P(this_ptr)->name), get_active_function_name(), ZSTR_VAL(ce->name), get_active_function_name());
}
ZEND_API zend_bool ZEND_FASTCALL instanceof_function(const zend_class_entry *instance_ce, const zend_class_entry *ce) /* {{{ */
{
if (ce->ce_flags & ZEND_ACC_INTERFACE) {
return instanceof_interface(instance_ce, ce);
} else {
return instanceof_class(instance_ce, ce);
}
}
static zend_always_inline zend_bool instanceof_class(const zend_class_entry *instance_ce, const zend_class_entry *ce) /* {{{ */
{
while (instance_ce) {
if (instance_ce == ce) {//会循环校验父类是否相等
return 1;
}
instance_ce = instance_ce->parent;
}
return 0;
}
```
```c
ZEND_API int zend_parse_parameters(int num_args, const char *type_spec, ...) /* {{{ */
{
va_list va;
int retval;
int flags = 0;
va_start(va, type_spec);
retval = zend_parse_va_args(num_args, type_spec, &va, flags);
va_end(va);
return retval;
}
/* }}} */
ZEND_API int zend_parse_method_parameters(int num_args, zval *this_ptr, const char *type_spec, ...) /* {{{ */
{
va_list va;
int retval;
int flags = 0;
const char *p = type_spec;
zval **object;
zend_class_entry *ce;
/* Just checking this_ptr is not enough, because fcall_common_helper does not set
* Z_OBJ(EG(This)) to NULL when calling an internal function with common.scope == NULL.
* In that case EG(This) would still be the $this from the calling code and we'd take the
* wrong branch here. */
zend_bool is_method = EG(current_execute_data)->func->common.scope != NULL;
if (!is_method || !this_ptr || Z_TYPE_P(this_ptr) != IS_OBJECT) {
va_start(va, type_spec);
retval = zend_parse_va_args(num_args, type_spec, &va, flags);
va_end(va);
} else {
p++;
va_start(va, type_spec);
object = va_arg(va, zval **);
ce = va_arg(va, zend_class_entry *);
*object = this_ptr;
if (ce && !instanceof_function(Z_OBJCE_P(this_ptr), ce)) {
zend_error_noreturn(E_CORE_ERROR, "%s::%s() must be derived from %s::%s",
ZSTR_VAL(Z_OBJCE_P(this_ptr)->name), get_active_function_name(), ZSTR_VAL(ce->name), get_active_function_name());
}
retval = zend_parse_va_args(num_args, p, &va, flags);
va_end(va);
}
return retval;
}
/* }}} */
```
#### 使用
这里的**type_spec**我还加了一个 O因为在源码中`p++;`这里跳过了一个字符,那么我们后面`retval = zend_parse_va_args(num_args, p, &va, flags);`的时候传入的就是 **l 了, O** 这里应该是可以乱填一个字符的
**&this** 又传回来了- -
```c
PHP_METHOD(study_ext_class,sum)
{
zend_long parma_num=0;
zval* this=getThis();
zval* static_num=zend_read_static_property(Z_OBJCE_P(this),"num",sizeof("num")-1,0);
// zval
if(zend_parse_method_parameters(ZEND_NUM_ARGS(),this,"Ol",&this,study_ce,&parma_num)==FAILURE){
RETURN_LONG(-1)
}
if(Z_TYPE_P(static_num)==IS_LONG){
RETURN_LONG(static_num->value.lval+parma_num)
}
RETURN_LONG(-1)
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -0,0 +1,47 @@
---
title: SMTP+SSL协议研究-PHP实现
---
> 突然的就想尝试一下实现邮件发送协议,尤其是 SMTP+SSL 之类的方式,SMTP 协议全是明文的,写起来倒是不困难,但是到现在还完全不了解+SSL 的工作方式
## 开头
github:https://github.com/huanl-php/protocol
打算以后将实现的协议都放在这里,所以要做好规划
### Socket
[php socket](http://www.php.net/sockets "php socket")
先了解好 php 的 socket 函数,和 c 的 socket 非常像.为什么这里我们使用 socket 来实现,而不是用 swoole,因为在大多数的情况下,swoole 扩展并不一定安装了,这是非常不方便的
### client 类
> 这个是用来连接服务器的,由这一个类扩展出其他的协议,需要在这个里面写好连接和发送,接收的一些功能
这里不贴代码了,可以去 github 看:https://github.com/huanl-php/protocol/blob/master/src/Client.php
### smtp
> 这里我拿我的阿里云的邮箱测试,参考这篇文章 [邮件实现详解(二)------手工体验 smtp 和 pop3 协议](https://www.cnblogs.com/ysocean/p/7653252.html#_label0),顺便学习了一波 telnet,这篇文章是真的详细,我就不写过程了
![](img/SMTP+SSL%E5%8D%8F%E8%AE%AE%E7%A0%94%E7%A9%B6-PHP%E5%AE%9E%E7%8E%B0.assets/TIM%E6%88%AA%E5%9B%BE20180822150356-300x201.png)
### 实现
[SMTP](https://github.com/huanl-php/protocol/blob/master/src/SMTP.php)代码在这里了- -...然后看看 SSL...的工作方式
## SSL
> SSL 相当于是中间的一套层,客户端发送消息经过 SSL 层加密发送给服务器,然后经过服务器的 SSL 层又解密给服务器
在 php 中,ssl 套层实现非常的简单...用`stream_socket_client`连接 server,然后使用`stream_socket_enable_crypto`设置 ssl 链接,之后`fwrite`发送数据(大概这就是 linux 的哲学**万物皆文件**的体现吧),如果是这样,那么只需要在 Client 类中,重写一次 connect 和 send 那些方法就够了
### 源码
[SSLClient](https://github.com/huanl-php/protocol/blob/master/src/SSLClient.php)

View File

@ -0,0 +1,136 @@
---
title: SSL/TLS client hello 解析
---
> 摘抄:SSL 因为应用广泛已经成为互联网上的事实标准。IETF 就在那年把 SSL 标准化。标准化之后的名称改为 TLS是“Transport Layer Security”的缩写中文叫做“传输层安全协议
> 上次写了个 ssl 的 smtp 协议,但是 ssl 实现那里,php 只需要随便调用几个函数就好了,觉得不过瘾,所以这次来看一下 ssl 的实现
## 准备
我们需要一个抓包工具**Wireshark**
![](img/SSL-TLS-client-hello.assets/TIM%E6%88%AA%E5%9B%BE20180828092927.png)
这是我捕获到的,为了方便,我是抓的我的博客,右键刷新源码,然后就停止抓包
上面是过滤内容,ip 地址等于我的博客的地址,并且是 ssl 协议(tls)
在此之前我看了不少的相关知识,但是都只是说应用和流程,好处什么的,还有说一堆算法的,老夫看不懂,老夫才不管这些什么,老夫写代码就是一把梭
阮老师这篇文章说得挺容易理解[图解 SSL/TLS 协议](http://www.ruanyifeng.com/blog/2014/09/illustration-ssl.html)
但是没有找到实现,后来谷歌搜了一下,才找到了一篇[client hello](https://blog.csdn.net/leinchu/article/details/80196025)解析的(再一次吐槽百度),通过这一篇文章我才有一点头绪
## SSL 握手
> 经过了 TCP 三次握手之后,就开始 SSL 的握手
### 结构
> 先了解一下大概结构
我们用 Wireshark 抓到了数据,写得非常的详细,看第一个包就是**client hello**,点它,然后点**secure sockets layer**这就是我们 ssl 的数据了,然后会帮我们选中我们的数据,非常的直接,前面那些应该是 tcp 协议相关的数据
![](img/SSL-TLS-client-hello.assets/TIM%E6%88%AA%E5%9B%BE20180828142314.png)
看这个,感觉大概结构应该是这样,首先是包类型版本号和长度,然后是内容
```cpp
struct ssl_handshake{
char type;
short version;
short length;
char *content;
}
```
握手包,点开 handshake Protocol 可以看到,client hello 和 server hello,前一部分都是这样
```cpp
//随机数
struct ssl_random{
int timestamp;
char random[28];
}
struct ssl_hello{
char type;
char length[3];
short version;
struct ssl_random random;
}
```
这个结构我写到了随机数截止,因为后面的大多是不同的且变化
#### session
**session**,这个是用来复用的,我记得这个只是一个记录,并不一定需要,于是我通过其他软件访问网页,抓到了一个没有 session 的包来比对,所以我们就可以直接在后面填充一个 0,然后继续后面的
![](img/SSL-TLS-client-hello.assets/TIM%E6%88%AA%E5%9B%BE20180829110033.png)
#### cipher suites
**cipher suites**应该是客户端支持的秘钥类型,里面应该都是一些常量值
意义,例如:TLS***ECDHE*****RSA**_WITH_**AES_128_GCM**\_ **SHA256** (0xc02f)
**ECDHE**秘钥交换算法
**RSA**身份验证算法
**AES_128_GCM**对称加密算法
**SHA256**摘要算法
这样按照顺序拆分开来看....
值的话,我只从 RFC 里面找到了零零散散的,不过我想干脆就从我们抓到的包里面提取几个吧
```
Cipher Suite: TLS_RSA_WITH_AES_128_GCM_SHA256 (0x009c)
Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xc02f)
Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA256 (0x003c)
```
#### Extension
> 跳过了 Compression Methods,等下按照我们抓到的值填就好了
Extension 听名字就知道是扩展项,感觉和原来的 radius 的一样
```cpp
struct ssl_extension{
short type;
short length;
char *content;
}
```
我想先吧他们复制一下试试能不能行(偷懒,滑稽)
### client hello
> 客户端先给服务端打声招呼,告诉客户端支持的加密算法 balabala,先尝试写代码,发送一个 hello 的包给服务器看看回答
代码如下:
```php
public function testSSLHello() {
$str = SSL::pac_ssl_handshake(22, SSL::TLSv3,
SSL::pack_ssl_hello(1, SSL::TLSv3, SSL::pack_ssl_random(),
hex2bin('00') . SSL::pack_ciphersuites(['009c', 'c02f', '003c']) .
hex2bin('0100005800000014001200000f626c6f672e69636f6465662e636f6d000500050100000000000a00080006001d00170018000b00020100000d001400120401050102010403050302030202060106030023000000170000ff01000100')
)
);//那一大串十六进制是我复制的扩展区
static::$client->send($str);
static::$client->recv($buf, 2048);
}
```
发送之后成功接收到了服务器的应答,扩展区里面还包含了服务器的一些信息,如果换服务器可能还要修改一下`Extension: server_name`
~~今天坑先挖到这里,明天看 server hello~~
## 参考
[https://blog.csdn.net/mrpre/article/details/77867439](https://blog.csdn.net/mrpre/article/details/77867439)
[图解 SSL/TLS 协议](http://www.ruanyifeng.com/blog/2014/09/illustration-ssl.html)
[client hello](https://blog.csdn.net/leinchu/article/details/80196025)

View File

@ -0,0 +1,114 @@
---
title: android 邀请链接,安装apk注册自动填写邀请人
---
> 今天又遇到一个 app 推广的问题,要求通过下载链接下载安装之后,注册的时候自动填写这个邀请链接的邀请人,但是又怎么吧这个邀请人的信息填入这个 apk 包呢?开始想着在文件的最后直接写入用户的 uid,结果破坏的 apk 的结构,后来发现 apk 其实就是一个 zip 包,通过网上的资料找到了思路,通过邀请链接下载的时候就修改这个 zip 包的注释将邀请者的 uid 填入
## ZIP 文件结构
随手一搜就可以找到 zip 的文件结构
**[官方文档](https://pkware.cachefly.net/webdocs/APPNOTE/APPNOTE-6.2.0.txt) [参考文章](https://blog.csdn.net/a200710716/article/details/51644421)** 这个文章后面还拿了一个简单的文件做讲解,挺不错的
其他的都不管他,我只想要注释那一段的结构,
### End of central directory record(EOCD) 目录结束标识
目录结束标识存在于整个归档包的结尾,用于标记压缩的目录数据的结束。每个压缩文件必须有且只有一个 EOCD 记录。
| Offset | Bytes | Description | 翻译 |
| ------ | ----- | ----------------------------------------------------------------------------- | ----------------------------- |
| 0 | 4 | End of central directory signature = 0x06054b50 | 核心目录结束标记0x06054b50 |
| 4 | 2 | Number of this disk | 当前磁盘编号 |
| 6 | 2 | Number of the disk with the start of the central directory | 核心目录开始位置的磁盘编号 |
| 8 | 2 | total number of entries in the central directory on this disk | 该磁盘上所记录的核心目录数量 |
| 10 | 2 | total number of entries in the central directory | 核心目录结构总数 |
| 12 | 2 | size of central directory (bytes) | 核心目录的大小 |
| 16 | 2 | offset of start of central directory with respect to the starting disk number | 核心相对于 archive 开始的位移 |
| 20 | 2 | .ZIP file comment length(n) | 注释长度 |
| 22 | n | .ZIP Comment | 注释内容 |
### 先用易语言测试一下
先打包一个文件
![](img/android-%E9%82%80%E8%AF%B7%E9%93%BE%E6%8E%A5.assets/TIM%E6%88%AA%E5%9B%BE20180401230107-300x194.png)
随手一读
```
.版本 2
.支持库 spec
.局部变量 data, 字节集
data 读入文件 (“test.zip”)
调试输出 (取字节集中间 (data, 寻找字节集 (data, { 80, 75, 5, 6 }, ), 50))
调试输出 (到字节集 (“test123”))
```
读取出来的数据中确实有 test123
然后看看怎么写注释进去,这回读取 apk 的
```
//80,75,5,6,0,0,0,0,18,0,18,0,18,5,0,0,55,14,17,0,0,0,60,105,110,118,62,50,60,47,105,110,118,62
//读出来这玩意,先理清楚结构
//80,75,5,6 EOCD
//0,0 磁盘编号
//0,0 开始位置的磁盘编号
//18,0 磁盘上所记录的核心目录数量
//18,0 核心目录结构总数
//18,5,0,0 核心目录的大小
//55,14,17,0 核心目录开始位置相对于archive开始的位移
//0,0 注释长度
//0,60,105,110,118,62,50,60,47,105,110,118,62未知内容,不管了,我们只需要修改注释
.版本 2
.局部变量 data, 字节集
.局部变量 tmpByte, 字节集
.局部变量 pos, 整数型
data 读入文件 (“test.apk”)
pos 寻找字节集 (data, { 80, 75, 5, 6 }, )
data [pos 20] 8
tmpByte 到字节集 (“12345678”) { 0 } 取字节集右边 (data, 取字节集长度 (data) pos 21)
data 取字节集左边 (data, 寻找字节集 (data, { 80, 75, 5, 6 }, ) 21)
data data tmpByte
写到文件 (“ddd.apk”, data)
成功写入压缩包,看看能不能在手机上安装
成功可以安装使用
```
### PHP 下载代码
php 的话就和前面一下,修改注释内容,直接贴 php 的代码了
php 的文件操作就是文本操作....十六进制要用双引号\0xff 表示
```php
public function download_apk($inv = 1) {
if (!userModel::existUser($inv)) {//判断用户是否存在
$inv = 0;
}
$invStr = '<inv>' . $inv . '</inv>';//设置注释
$data = file_get_contents('app/kana.apk');//读取apk源数据
$pos = strpos($data, "\x50\x4b\x05\x06");
//搜索标志位置(一般都是文件尾部,不考虑了)
$data = substr($data, 0, $pos + 20);
//取出标志+20左边,等下直接从注释长度那块合成
$dec = dechex(strlen($invStr));
//将长度转为十六进制,后面再将十六进制转为byte,只考虑一位,反正ff,255个长度也够了
$data .= hex2bin(strlen($dec) <= 1 ? ('0' . $dec) : $dec) . "\x00$invStr";//合成注释
header("Content-Type: application/octet-stream");
header("Accept-Ranges: bytes");
header("Accept-Length: " . strlen($data));
header("Content-Disposition: attachment; filename=kana.apk");
echo $data;//输出文件数据
}
```
### android 读取
暂时空着,android 又不是我写 ┓( ´∀` )┏,贴一下思路吧
读取本地安装的 apk 包数据,然后在里面搜索这个注释(怎么感觉和没讲一样)
总之很简单的

View File

@ -0,0 +1,277 @@
---
title: c 协程
---
## 前言
协程可以说是用同步的代码写出异步的效果,前几天还看了异步,这些都算是在高性能系统中的一部分,都是压榨我们的 cpu将 io 堵塞的时间去做其他事情。
异步的解决方式是执行立刻返回,我们的代码继续往下走,当完成之后通知我们。
协程的方式是代码执行立刻返回,之后我们将该条协程挂起,然后这段时间去执行其他的协程,等待 io 完成后恢复这条协程继续往下执行。相比于异步,至少在代码上就不会出现那种回调地狱了。
和线程相比,协程更加轻量,占用资源更少,通过协作的方式利用资源(因为是在单条线程内,不会同时执行)而不是抢占(多条线程可能同时执行读取某一数据),线程是由系统进行调度,协程是用户自己进行调度。
## 实现
在 c 中有好几种协程的实现方式,这里我使用 ucontext 实现
- 利用 switch-case 奇淫巧技实现
- asm 汇编实现
- 利用 c 的 setjmp 和 longjmp 函数实现
- ucontext 保存上下文实现
- boost.context
### ucontext 函数
先了解一下[ucontext](http://pubs.opengroup.org/onlinepubs/7908799/xsh/ucontext.h.html)的函数
```c
int getcontext(ucontext_t *);
int setcontext(const ucontext_t *);
void makecontext(ucontext_t *, (void *)(), int, ...);
int swapcontext(ucontext_t *, const ucontext_t *);
```
### get/setcontext
[http://pubs.opengroup.org/onlinepubs/7908799/xsh/getcontext.html](http://pubs.opengroup.org/onlinepubs/7908799/xsh/getcontext.html)
用于获取当前和设置上下文
#### 例子
这个例子会一直输出 hello和 goto 有点相似,不同的是,它可以在不同的函数之中进行跳转
getcontext 把当前的上下文保存到 ucp 中,后面使用 setcontext 恢复
```c
ucontext_t ucp;
void print() {
printf("hello\n");
setcontext(&ucp);
}
int main() {
getcontext(&ucp);
sleep(1);
print();
return 0;
}
```
### make/swapcontext
修改 getcontext 初始化的 ucp 上下文,调用 swapcontext 或 setcontext 恢复的时候程序将调用 func可以自己分配堆栈**uc_link**可以指定执行完后的上下文
swapcontext 将当前上下文保存在 ocup 上,并将上下文设置为 ucp
#### 例子
```c
void print() {
printf("hello\n");
}
int main() {
ucontext_t ucp, print_ucp;
getcontext(&ucp);
print_ucp = ucp;
char stack[10 * 1204];
print_ucp.uc_stack.ss_sp = stack;
print_ucp.uc_stack.ss_size = sizeof(stack);
print_ucp.uc_stack.ss_flags = 0;
print_ucp.uc_link = &ucp;
makecontext(&print_ucp, print, 0);
swapcontext(&ucp, &print_ucp);
printf("end\n");
return 0;
}
```
### 协程
协程的一个关键就是上下文切换,在我们需要的时候切换至其他的协程
这里我只是写了一个非常简单的协程,只有创建,挂起,恢复功能
### 定义
定义了协程的状态,大小,协程函数指针,我的协程的结构体
**重点是我们的结构体里面,两个 ucontext_t我们需要利用他们进行上下文的切换**
```c
#define STACK_SIZE 1024*128
#define CO_RUN 1
#define CO_HANG 2
#define CO_OVER 3
typedef void (*co_func)(struct coroutine *co);
struct coroutine {
char stack[STACK_SIZE];//栈
ucontext_t ctx;//协程上下文
ucontext_t ucp;//主线程上下文
char status;//协程状态
};
```
#### 创建协程
这里我没有直接的指向**func**因为还有一个完成状态要标记,所以我们定义了一个我们的协程主函数进行一些处理
我的创建协程主要是做的事情是,初始化上下文,调起函数
```c
void co_main(struct coroutine *co, co_func func) {
func(co);
co->status = CO_OVER;
}
void co_create(struct coroutine *co, co_func func) {
getcontext(&co->ucp);
co->ctx = co->ucp;
co->ctx.uc_stack.ss_sp = co->stack;
co->ctx.uc_stack.ss_size = STACK_SIZE;
co->ctx.uc_stack.ss_flags = 0;
co->ctx.uc_link = &co->ucp;
co->status = CO_RUN;
makecontext(&co->ctx, co_main, 2, co, func);//指向的co_main
swapcontext(&co->ucp, &co->ctx);
}
```
### 挂起/恢复协程
挂起和恢复的时候我们都用 status 判断了一下是否执行结束
挂起和恢复就是上下文的交换,调用的**swapcontext**,挂起时将我们协程的上下文记录,切换到线程的上下文,交给线程去进行调度,恢复则相反
```c
void co_yield(struct coroutine *co) {
if (co->status == CO_OVER) {
return;
}
co->status = CO_HANG;
swapcontext(&co->ctx, &co->ucp);
}
void co_resume(struct coroutine *co) {
if (co->status == CO_OVER) {
return;
}
co->status = CO_RUN;
swapcontext(&co->ucp, &co->ctx);
}
```
### 完整源码
```c
#include <stdio.h>
#include <ucontext.h>
#include <mhash.h>
#define STACK_SIZE 1024*128
#define CO_RUN 1
#define CO_HANG 2
#define CO_OVER 3
typedef void (*co_func)(struct coroutine *co);
struct coroutine {
char stack[STACK_SIZE];//栈
ucontext_t ctx;//ucp
ucontext_t ucp;
char status;//协程状态
};
void co_main(struct coroutine *co, co_func func) {
func(co);
co->status = CO_OVER;
}
void co_create(struct coroutine *co, co_func func) {
getcontext(&co->ucp);
co->ctx = co->ucp;
co->ctx.uc_stack.ss_sp = co->stack;
co->ctx.uc_stack.ss_size = STACK_SIZE;
co->ctx.uc_stack.ss_flags = 0;
co->ctx.uc_link = &co->ucp;
co->status = CO_RUN;
makecontext(&co->ctx, co_main, 2, co, func);
swapcontext(&co->ucp, &co->ctx);
}
void co_yield(struct coroutine *co) {
if (co->status == CO_OVER) {
return;
}
co->status = CO_HANG;
swapcontext(&co->ctx, &co->ucp);
}
void co_resume(struct coroutine *co) {
if (co->status == CO_OVER) {
return;
}
co->status = CO_RUN;
swapcontext(&co->ucp, &co->ctx);
}
int co_status(struct coroutine *co) {
return co->status;
}
void print1(struct coroutine *co) {
for (int i = 0; i < 50; i++) {
printf("1号协程:%d\n", i);
co_yield(co);
}
}
void print2(struct coroutine *co) {
for (int i = 100; i < 200; i++) {
printf("2号协程:%d\n", i);
co_yield(co);
}
}
int main() {
struct coroutine co1, co2;
co_create(&co1, print1);
co_create(&co2, print2);
while (co_status(&co1) != CO_OVER || co_status(&co2) != CO_OVER) {
co_resume(&co1);
co_resume(&co2);
}
return 0;
}
```
### 栈
贴一个例子,将上面的第二个 swapcontext 那里的例子print 换成下面这个,帮助大家理解一下栈,看输出,暂时就不写说明了
```c
void test() {
int a = 1, len = ((int64_t) (&stack[10 * 1024 - 1]) - (int64_t) (&a)), b = 23;
for (int i = 10 * 1024 - len - 8; i < 10 * 1024; i++) {
if (i % 20 == 0) {
printf("\n");
}
printf("%d\t", stack[i]);
}
}
void print() {
int a = 1, len = ((int64_t) (&stack[10 * 1024 - 1]) - (int64_t) (&a)), b = 23;
printf("hello,%ld,%ld,%d\n", &a, &stack[10 * 1024 - 1], (int64_t) (&stack[10 * 1024 - 1]) - (int64_t) (&a));
test();
// for (int i = 10 * 1024 - len - 8; i < 10 * 1024; i++) {
// if (i % 20 == 0) {
// printf("\n");
// }
// printf("%d\t", stack[i]);
// }
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View File

@ -0,0 +1,72 @@
---
title: linux aio 异步io
---
> linux 下的 aio 有 glibc 的和内核所提供的glibc 是使用的多线程的模式模拟的,另外一种是真正的内核异步通知了,已经使用在了 nginx 上,前面看了一下 swoole 的实现,是类似与 glibc 那种多线程的模式。不过两种方法都有一定的毛病,多线程模拟自然是有所效率损失,然而内核不能利用系统的缓存,只能以 O_DIRECT 方式做直接 IO所以看知乎上有一个**linux 下的异步 IOAIO是否已成熟**的问题,不过那是 2014 年的事情了,不知道现在怎么样。
在此之前需要安装好 **libaio**
```
sudo apt install libaio-dev
```
## 函数
头文件:`#include <libaio.h>`
## 例子
> 一个一步读取文件内容的例子
```c
#include <stdio.h>
#include <fcntl.h>
#include <libaio.h>
#include <malloc.h>
#include <mhash.h>
#define MAX_EVENT 10
#define BUF_LEN 1024
void callback(io_context_t ctx, struct iocb *iocb, long res, long res2) {
printf("test call\n");
printf("%s\n", iocb->u.c.buf);
}
int main() {
int fd = open("/home/huanl/client.ovpn", O_RDONLY, 0);
io_context_t io_context;
struct iocb io, *p = &io;
struct io_event event[MAX_EVENT];
char *buf = malloc(BUF_LEN);
memset(buf, 0, BUF_LEN);
memset(&io_context, 0, sizeof(io_context));
if (io_setup(10, &io_context)) {
printf("io_setup error");
return 0;
}
if (fd < 0) {
printf("open file error");
return 0;
}
io_prep_pread(&io, fd, buf, BUF_LEN, 0);
io_set_callback(&io, callback);
if (io_submit(io_context, 1, &p) < 0) {
printf("io_submit error");
return 0;
}
int num = io_getevents(io_context, 1, MAX_EVENT, event, NULL);
for (int i = 0; i < num; i++) {
io_callback_t io_callback = event[i].data;
io_callback(io_context, event[i].obj, event[i].res, event[i].res2);
}
return 0;
}
```
## 参考
[https://jin-yang.github.io/post/linux-program-aio.html](https://jin-yang.github.io/post/linux-program-aio.html)

View File

@ -0,0 +1,234 @@
---
title: swoole学习笔记(一)-swoole环境配置(树莓派安装)
---
> 打算开始学习 swoole 了(原来好像弄过,不过那次只是接触了一下,并未太过深入,这次重新来过 (° ー °〃)
> swoole 虽然能在 windows 上搭建,不过我觉得意义不大....需要安装 CygWin 这和在 linux 上有什么区别呢 ┑( ̄ Д  ̄)┍,刚好现在手上有一台空闲的树莓派 zero,试试在上面搭建
## 编译 php
> 之所以要编译安装是因为在 swoole 编译的时候需要用到 phpize,apt-get 安装的时候没发现有
现在这个上面什么东西都没有,先安装 php,我选最新的 php7.2.6,zero 配置是真的好低....解压和编译 cpu 都 100%了很慢....趁这个时间去干点别的吧
下载,解压源码,安装依赖
强烈建议使用国内镜像....不然可能一些依赖 lib 按照失败,导致编译错误
```
sudo -i
wget http://hk1.php.net/get/php-7.2.6.tar.gz/from/this/mirror
mv mirro php.tar.gz
tar -zxvf php.tar.gz
apt-get update
apt-get install libxml2* libbz2-dev libjpeg-dev libmcrypt-dev libssl-dev openssl libxslt1-dev libxslt1.1 libcurl4-gnutls-dev libpq-dev build-essential git make
```
编译配置,复制的网上的 lnmp 编译- -...去掉了和 Nginx 有关的编译项,我只需要编译出 php 就行,不需要 Nginx 那些环境,当然如果你之前已经有了这些,这一部分就可以跳过了
```
cd php-7.2.6
./configure \
--prefix=/usr/local/php \
--exec-prefix=/usr/local/php \
--bindir=/usr/local/php/bin \
--sbindir=/usr/local/php/sbin \
--includedir=/usr/local/php/include \
--libdir=/usr/local/php/lib/php \
--mandir=/usr/local/php/php/man \
--with-config-file-path=/usr/local/php/etc \
--with-mysql-sock=/var/lib/mysql/mysql.sock \
--with-mcrypt=/usr/include \
--with-mhash \
--with-openssl \
--with-mysql=shared,mysqlnd \
--with-mysqli=shared,mysqlnd \
--with-pdo-mysql=shared,mysqlnd \
--with-gd \
--with-iconv \
--with-zlib \
--enable-zip \
--enable-inline-optimization \
--disable-debug \
--disable-rpath \
--enable-shared \
--enable-xml \
--enable-bcmath \
--enable-shmop \
--enable-sysvsem \
--enable-mbregex \
--enable-mbstring \
--enable-ftp \
--enable-gd-native-ttf \
--enable-pcntl \
--enable-sockets \
--with-xmlrpc \
--enable-soap \
--without-pear \
--with-gettext \
--enable-session \
--with-curl \
--with-freetype-dir \
--enable-opcache \
--enable-redis \
--enable-fpm \
--enable-fastcgi \
--disable-fileinfo
```
![](img/01-swoole%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE.assets/TIM%E6%88%AA%E5%9B%BE20180619143040-300x207.png)
CPU 100% 有点怕,树莓派 zero 性能确实是弱...编译好慢....解决了编译配置的问题后就开始编译,我是真的睡了一觉(第二天)才起来 make install
```
make && make install
```
设置一下 php.ini 文件
```
cp php.ini-production /usr/local/php/etc/php.ini
//我输入php -v之后发现没反应,但是php确实是成功了,在/usr/local/php/bin里面./php -v也有反应,想到可能是没有链接到/usr/bin 目录里,用ln命令链接一下
ln -s /usr/local/php/bin/php /usr/bin/php
//链接phpize
ln -s /usr/local/php/bin/phpize /usr/bin/phpize
```
成功之后,老套路
```
php -v
```
![](img/01-swoole%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE.assets/TIM%E6%88%AA%E5%9B%BE20180620085032-300x87.png)
成功,终于可以下一步了,进入 swoole 编译配置
## swoole 编译
从 git 上下载源码[https://github.com/swoole/swoole-src/releases](https://github.com/swoole/swoole-src/releases),开始编译
```
wget https://github.com/swoole/swoole-src/archive/v4.0.0.zip
unzip v4.0.0.zip
mv swoole-src-4.0.0/ swoole
cd swoole
phpize
```
这里我提示了一个错误...
Cannot find autoconf. Please check your autoconf installation and the
$PHP_AUTOCONF environment variable. Then, rerun this script.
解决办法:
```
apt-get install m4 autoconf
```
phpize 成功之后继续运行编译配置和开始编译(但愿这次不用那么久了...)
开启一些需要的:[编译配置项](https://wiki.swoole.com/wiki/page/437.html)
```
./configure --with-php-config=/usr/local/php/bin/php-config --enable-sockets --enable-swoole-debug --enable-openssl --enable-mysqlnd --enable-coroutine
make && make install
```
![](img/01-swoole%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE.assets/TIM%E6%88%AA%E5%9B%BE20180620095954-300x207.png)
然后需要在 php.ini 中配置下
```
vi /usr/local/php/etc/php.ini
//添加
extension=swoole.so
```
然后`php -m`
![](img/01-swoole%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE.assets/TIM%E6%88%AA%E5%9B%BE20180620113804-300x207.png)
有这一项就代表成啦~
## 测试
> 安装编译都完成之后,当然来试试是不是真的能用了
复制官方的例子,嘿嘿嘿~
```php
<?php
//创建websocket服务器对象监听0.0.0.0:9502端口
$ws = new swoole_websocket_server("0.0.0.0", 9502);
//监听WebSocket连接打开事件
$ws->on('open', function ($ws, $request) {
var_dump($request->fd, $request->get, $request->server);
$ws->push($request->fd, "hello, welcome\n");
});
//监听WebSocket消息事件
$ws->on('message', function ($ws, $frame) {
echo "Message: {$frame->data}\n";
$ws->push($frame->fd, "server: {$frame->data}");
});
//监听WebSocket连接关闭事件
$ws->on('close', function ($ws, $fd) {
echo "client-{$fd} is closed\n";
});
$ws->start();
```
`php swoole.php`
web:
```html
<script>
var ws = new WebSocket("ws://localhost:9502");
ws.onopen = function () {
ws.send("send data");
};
ws.onmessage = function (evt) {
var received_msg = evt.data;
console.log(received_msg);
};
ws.onclose = function () {
console.log("连接关闭");
};
</script>
```
成了~
![](img/01-swoole%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE.assets/TIM%E6%88%AA%E5%9B%BE20180620114807-300x207.png)
---
## 问题解决
### redis 扩展安装
> 弄完后...并没有用,然后重新编译一次成了....= =,不过还是记着
在 swoole 编译完成后,又遇到了一个问题....
```
php: symbol lookup error: /usr/local/php/lib/php/extensions/no-debug-non-zts-20170718/swoole.so: undefined symbol: swoole_redis_coro_init
```
查资料后发现可能是需要给 php 安装 redis 扩展....[redis 源码下载](https://pecl.php.net/package/redis)
```
wget https://pecl.php.net/get/redis-4.0.2.tgz
tar -zxvf redis-4.0.2.tgz
cd redis-4.0.2
phpize
./configure --with-php-config=/usr/local/php/bin/php-config
make && make install
```
然后在 php.ini 中加上`extension = redis.so`就好了,注意这个配置一定要放在 swoole 的配置的前面,因为这些扩展都是按照顺序加载的
---
**历时一天,终于搞定了 编译真的是漫长的过程=\_=**

View File

@ -0,0 +1,134 @@
---
title: swoole学习笔记(二)-开发环境配置
---
> swoole 可以跑了,然后开始弄开发环境
> 后面的 xdebug,在协程中 tm 不能用!...有挺多问题的,不推荐配置了,写 log 吧
## 代码自动上传
我的开发环境一般是 windows,phpstorm,然而我的树莓派和 swoole 的环境又不在一起,这时候就可以用 phpstorm 的一个功能,可以自动同步代码
File->setting->Deployment
添加一个,选择 sftp,然后输入 pi 的信息
![](img/02-%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE.assets/TIM%E6%88%AA%E5%9B%BE20180620131748-300x204.png)
![](img/02-%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE.assets/TIM%E6%88%AA%E5%9B%BE20180620132024-1-300x204.png)
![](img/02-%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE.assets/TIM%E6%88%AA%E5%9B%BE20180620132024-2-300x204.png)
添加好服务器后,再设置 options
![](img/02-%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE.assets/TIM%E6%88%AA%E5%9B%BE20180620133255-300x204.png)
自动上传就配置好了,当你保存的时候就会自动上传到服务器
![](img/02-%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE.assets/TIM%E6%88%AA%E5%9B%BE20180620133409-300x96.png)
## 代码自动提示
> 对于我这种百度型程序员,自动提示是必不可少的
### swoole-ide-helper
https://github.com/eaglewu/swoole-ide-helper
这是一个**Swoole 在 IDE 下自动识别类、函数、宏,自动补全函数名**
#### 安装方法
##### phpstrom
将项目 clone 或者直接下载下来,解压
![](img/02-%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE.assets/TIM%E6%88%AA%E5%9B%BE20180622124636-300x216.png)
![](img/02-%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE.assets/TIM%E6%88%AA%E5%9B%BE20180622124713-300x227.png)
##### composer
要是你的项目中使用了 composer,你可以直接
```bash
composer require --dev "eaglewu/swoole-ide-helper:dev-master"
```
## 远程调试配置
> 虽然可以通过 echo 之类的来调试,但是断点调试也是必不可少的
### XDebug 安装
[https://github.com/xdebug/xdebug](https://github.com/xdebug/xdebug)
```bash
wget https://github.com/xdebug/xdebug/archive/2.6.0.tar.gz
tar -zxvf 2.6.0.tar.gz
cd xdebug-2.6.0/
./configure --with-php-config=/usr/local/php/bin/php-config
make && make install
```
然后在 php.ini 中配置
```toml
zend_extension=xdebug.so
[xdebug]
xdebug.remote_enable=true
xdebug.remote_host=127.0.0.1
xdebug.remote_port=9000
xdebug.remote_handler=dbgp
```
保存之后`php -m`,出现 xdebug 就算安装成功了
![](img/02-%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE.assets/TIM%E6%88%AA%E5%9B%BE20180622161650-300x203.png)
### phpstorm 配置
setting 中 php 配置,设置一下远程 cli
![](img/02-%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE.assets/TIM%E6%88%AA%E5%9B%BE20180622161818-300x204.png)
在之前已经配置了远程自动同步的话,这里是会有服务器可以选择的
![](img/02-%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE.assets/TIM%E6%88%AA%E5%9B%BE20180622162052-300x107.png)
点击 OK 之后,phpstorm 会自动获取远程的 php 信息,如下
![](img/02-%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE.assets/TIM%E6%88%AA%E5%9B%BE20180622162146-300x246.png)
之后选择我们刚刚添加的(我重命名了 pi_zero php7.2)
![](img/02-%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE.assets/TIM%E6%88%AA%E5%9B%BE20180622162303-300x204.png)
然后下方的 path mappings,也需要设置(我这里默认设置好了),对本地与远程的目录进行映射
![](img/02-%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE.assets/TIM%E6%88%AA%E5%9B%BE20180622162622-300x108.png)
xdebug 的端口 9000,一开始就是这样的,如果你改了的话,这里注意也改一下
![](img/02-%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE.assets/TIM%E6%88%AA%E5%9B%BE20180622162413-300x107.png)
这些配置好之后就可以开始配置调试选项了
![](img/02-%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE.assets/TIM%E6%88%AA%E5%9B%BE20180622162413-1-300x203.png)
配置启动文件
![](img/02-%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE.assets/TIM%E6%88%AA%E5%9B%BE20180622162829-300x254.png)
之后就可以开始调试了,在我们的源码下下断点,然后点击调试按钮,成功~!
![](img/02-%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE.assets/TIM%E6%88%AA%E5%9B%BE20180622163913-300x219.png)
当收到信息/连接的时候:
![](img/02-%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE.assets/TIM%E6%88%AA%E5%9B%BE20180622164021-300x238.png)
非常舒服,嘿嘿嘿
![](img/02-%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE.assets/1LE84QO9K@A1N9.jpg)

View File

@ -0,0 +1,445 @@
---
title: swoole学习笔记(三)-UDP radius协议实现
---
> 又开新坑-swoole 作为 radius 服务器,huanlphp 写业务 [php-radius](https://github.com/CodFrm/php-radius "php-radius") 主要还是为了学习 swoole 和实验我的框架,所以这里记录一下 radius 协议的结构和使用(原来用 python 实现过一次,容易崩溃还写得垃圾),文章中只写了 auth,没有写 account 的记录,openvpn 需要 auth 和 account 才能实现连接成功,可以看我完整的实现[python 实现](https://github.com/CodFrm/stuShare/blob/master/radius/main.py)
## 协议&工具
- [rfc2865 radius 身份认证](https://tools.ietf.org/html/rfc2865)
- [rfc2866 radius 计费](https://tools.ietf.org/html/rfc2866)
测试工具我用的 NTRadPing:[http://www.winsite.com/internet/server-tools/ntradping/](http://www.winsite.com/internet/server-tools/ntradping/)
![](img/03-UDP-radius%E5%8D%8F%E8%AE%AE%E5%AE%9E%E7%8E%B0.assets/TIM%E6%88%AA%E5%9B%BE20180622220548-300x188.png)
## 结构
### pack fromat
> 关于计费和验证的格式都是一样的
验证:[https://tools.ietf.org/html/rfc2865#page-13](https://tools.ietf.org/html/rfc2865#page-13)
计费:[https://tools.ietf.org/html/rfc2866#page-5](https://tools.ietf.org/html/rfc2866#page-5)
```
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Code | Identifier | Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| Authenticator |
| |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Attributes ...
+-+-+-+-+-+-+-+-+-+-+-+-+-
```
看得有些隐晦,我将它转换成 c 的 struct 结构吧
char 1 个字节,8bit short 两个字节,16bit
```c
struct radius_format{
char code;//包类型,请求类型根据这个判断
char identifier;//鉴定码,服务器和客户端这个值需要一样
short lenght; //本次数据包的总长度(20-4096)
char authenticator[16];//数据hash,用来校验数据是否正确,一个数据的md5码
};
```
在响应的时候 authenticator 的计算方式为:MD5(Code+ID+Length+RequestAuth+Attributes+**Secret**) 在计算中的**RequestAuth**为请求的时候的 authenticator,**Secret**记笔记,是双方的一个 key
后面的 Attributes 长度不一定(length-20),里面包含着 **用户/NAS** 的一些信息,比如账号,密码,ip,之类的,格式看后面,这里也可以用来扩展自己的协议,带上自己的一些奇奇怪怪的值- -
#### code 意义
主要就是前面 5 个了
```
RADIUS Codes (decimal) are assigned as follows:
1 Access-Request //验证请求,一般由客户端使用
2 Access-Accept //验证通过,一般由服务端处理返回
3 Access-Reject //验证拒绝,一般由服务端处理返回
4 Accounting-Request //计费请求,客户端发起
5 Accounting-Response //计费返回,服务器发起
11 Access-Challeng //服务端主动请求再次验证,客户端要求必须返回
12 Status-Server (experimental)//服务器状态???没看到说明
13 Status-Client (experimental)//同上
255 Reserved//保留
```
### Attributes
[https://tools.ietf.org/html/rfc2865#page-22](https://tools.ietf.org/html/rfc2865#page-22)
```
0 1 2
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
| Type | Length | Value ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
```
```c
struct radius_attr{
char type;//类型,有不少...
char length;//type+length+value的长度
}
```
value 是不固定的(长度:1-253),根据 length 来判定(就是 length-2 咯)
#### type
> 偷懒了,不写意义了
验证属性:[https://tools.ietf.org/html/rfc2865#page-24](https://tools.ietf.org/html/rfc2865#page-24)
计费属性:[https://tools.ietf.org/html/rfc2866#page-11](https://tools.ietf.org/html/rfc2866#page-11)
```
This specification concerns the following values:
1 User-Name //登录用户名
2 User-Password //登录密码
3 CHAP-Password
4 NAS-IP-Address //NAS的IP地址
5 NAS-Port //NAS的端口
6 Service-Type
7 Framed-Protocol
8 Framed-IP-Address
9 Framed-IP-Netmask
10 Framed-Routing
11 Filter-Id
12 Framed-MTU
13 Framed-Compression
14 Login-IP-Host
15 Login-Service
16 Login-TCP-Port
17 (unassigned)
18 Reply-Message
19 Callback-Number
20 Callback-Id
21 (unassigned)
22 Framed-Route
23 Framed-IPX-Network
24 State
25 Class
26 Vendor-Specific
27 Session-Timeout
28 Idle-Timeout
29 Termination-Action
30 Called-Station-Id
31 Calling-Station-Id
32 NAS-Identifier
33 Proxy-State
34 Login-LAT-Service
35 Login-LAT-Node
36 Login-LAT-Group
37 Framed-AppleTalk-Link
38 Framed-AppleTalk-Network
39 Framed-AppleTalk-Zone
40-59 (reserved for accounting)//计费的,包含流量信息和连接时间什么的
60 CHAP-Challenge
61 NAS-Port-Type
62 Port-Limit
63 Login-LAT-Port
```
## 实现
> 利用 NTR 工具发送测试数据,使用 php-swoole 来接收和处理
### 解包
php 中使用 unpack 来解包,还是很方便的
```php
public function onPacket(swoole_server $serv, string $data, array $clientInfo) {
//解包
$radius = unpack('ccode/cidentifier/nlength/a16authenticator', $data);
print_r($radius);
echo bin2hex($radius['authenticator']) . "\n";
echo bin2hex($data);
}
```
![](img/03-UDP-radius%E5%8D%8F%E8%AE%AE%E5%AE%9E%E7%8E%B0.assets/TIM%E6%88%AA%E5%9B%BE20180623221807-300x101.png)
后面还需要处理 attr 属性,这是我解码的代码
```php
<?php
class radius {
/**
* @var swoole_server
*/
public $server;
/**
* 密钥
* @var string
*/
public $secret_key;
public static $ATTR_TYPE = [1 => 'User-Name',
2 => 'User-Password', 3 => 'CHAP-Password', 4 => 'NAS-IP-Address', 5 => 'NAS-Port', 6 => 'Service-Type',
7 => 'Framed-Protocol', 8 => 'Framed-IP-Address', 9 => 'Framed-IP-Netmask', 10 => 'Framed-Routing',
11 => 'Filter-Id', 12 => 'Framed-MTU', 13 => 'Framed-Compression', 14 => 'Login-IP-Host', 15 => 'Login-Service',
16 => 'Login-TCP-Port', 17 => '(unassigned)', 18 => 'Reply-Message', 19 => 'Callback-Number',
20 => 'Callback-Id', 21 => '(unassigned)', 22 => 'Framed-Route', 23 => 'Framed-IPX-Network', 24 => 'State',
25 => 'Class', 26 => 'Vendor-Specific', 27 => 'Session-Timeout', 28 => 'Idle-Timeout', 29 => 'Termination-Action',
30 => 'Called-Station-Id', 31 => 'Calling-Station-Id', 32 => 'NAS-Identifier', 33 => 'Proxy-State',
34 => 'Login-LAT-Service', 35 => 'Login-LAT-Node', 36 => 'Login-LAT-Group', 37 => 'Framed-AppleTalk-Link',
38 => 'Framed-AppleTalk-Network', 39 => 'Framed-AppleTalk-Zone',
60 => 'CHAP-Challenge', 61 => 'NAS-Port-Type', 62 => 'Port-Limit', 63 => 'Login-LAT-Port'];
/**
* 收到udp数据包
* @param swoole_server $serv
* @param string $data
* @param array $clientInfo
*/
public function onPacket(swoole_server $serv, string $data, array $clientInfo) {
$attr = [];
$struct = $this->unpack($data, $attr);
print_r($struct);
print_r($attr);
}
/**
* 解码radius数据包
* @param string $bin
* @param array $attr
* @return array|bool
*/
public function unpack(string $bin, array &$attr): array {
//一个正常的radius封包长度是绝对大于等于20的
if (strlen($bin) < 20) {
return [];
}
//解包
$radius = unpack('ccode/cidentifier/nlength/a16authenticator', $bin);
//获取后面的属性长度,并且对数据包进行验证
if (strlen($bin) != $radius['length']) {
return [];
}
$attr_len = $radius['length'] - 20;
//处理得到后面的Attributes,并且解包
$attr = $this->unpack_attr(substr($bin, 20, $attr_len));
if ($attr == []) {
return [];
}
return $radius;
}
/**
* 处理Attributes
* @param string $bin
* @return array
*/
public function unpack_attr(string $bin): array {
$attr = [];
$offset = 0;
$len = strlen($bin);
while ($offset < $len) {
$attr_type = ord($bin[$offset]);//属性类型
$attr_len = ord($bin[$offset + 1]);//属性长度
$attr[static::$ATTR_TYPE[$attr_type]] = substr($bin, $offset + 2, $attr_len - 2);//属性值
//跳到下一个
$offset += $attr_len;
}
//判断offset和$len是否相等,不相等认为无效,抛弃这个封包
if ($offset != $len) {
return [];
}
return $attr;
}
/**
* 运行服务器
* @param int $authPort
* @param int $accountPort
*/
public function run(string $secret_key, int $authPort = 1812, int $accountPort = 1813) {
$server = new swoole_server("0.0.0.0", $authPort, SWOOLE_PROCESS, SWOOLE_SOCK_UDP);
$server->on('Packet', array($this, 'onPacket'));
$server->start();
$this->server = $server;
$this->secret_key = $secret_key;
}
}
$server = new radius();
$server->run('test123');
```
![](img/03-UDP-radius%E5%8D%8F%E8%AE%AE%E5%AE%9E%E7%8E%B0.assets/TIM%E6%88%AA%E5%9B%BE20180624151246-300x156.png)
### 密码验证
> 密码有两种的加密方式 `User-Password(PAP加密)`和`CHAP-Password(CHAP加密)`
### User-Password
[https://tools.ietf.org/html/rfc2865#page-27](https://tools.ietf.org/html/rfc2865#page-27)
> 在传输时,密码是隐藏的。首先在密码的末尾用空值填充 16 个字节的倍数。单向 MD5 哈希是通过由共享密钥组成的八位字节流来计算的,后面是请求身份验证器。该值与密码的前 16 个八位位组段进行异或并放置在用户密码属性的字符串字段的前 16 个八位位组中。
>
> 如果密码长度超过 16 个字符,则第二个单向 MD5 散列值将通过由共享密钥组成的八位字节流计算,然后是第一个异或结果。该散列与密码的第二个 16 位八位字节进行异或,并置于用户密码属性的字符串字段的第二个 16 位字节中。
>
> 调用**共享密钥 S**和伪随机 128 位请求认证器**RA** 。 将密码分成 16 个八位字节块 p1p2 等。 最后一个填充为零,最终为 16 个八位字节的边界。 调用密文块 c1c2我们需要中间值 b1b2 等。
```
b1 = MD5(S + RA) c(1) = p1 xor b1
b2 = MD5(S + c(1)) c(2) = p2 xor b2
. .
. .
. .
bi = MD5(S + c(i-1)) c(i) = pi xor bi
The String will contain c(1)+c(2)+...+c(i) where + denotes
concatenation.
```
上面其实是 google 翻译来的- -...把我理解的说一下
当密码位数小于 16 位的时候,先计算出**md5(密钥(secret_key)+Authenticator)**的 md5,然后再与我们接收到的**User-Password**进行位运算,遇到`\x0`结尾字符串结束
如果超过了 16 位而且还没有遇到`\x0`那么继续和我们的**User-Password**进行位运算,但是这回的用于位运算的 md5 为**md5(密钥(secret_key)+前 16 个字符)**,这里有个坑....文档说的是加密的过程,我们需要实现的是解密,所以前 16 个字符串不是我们解密后的 16 个字符,而是加密后的 16 个字符,=\_=害我拿原文(解密后的)一直在算,不对
实现代码:
```php
public function decode_user_passwd($bin, $Authenticator) {
$passwd = '';
$S = $this->secret_key;
$len = strlen($bin);
//b1 = MD5(S + RA)
$hash_b = md5($S . $Authenticator, true);
for ($offset = 0; $offset < $len; $offset += 16) {
//每次拿16字符进行解码
for ($i = 0; $i < 16; $i++) {
$pi = ord($bin[$offset + $i]);
$bi = ord($hash_b[$i]);
//c(i) = pi xor bi
$chr = chr($pi ^ $bi);
if ($chr == "\x0") {
//文本标志\x0结尾
return $passwd;
}
$passwd .= $chr;
}
//判断一下是不是已经结束了,然后返回
if ($len == $offset + 16) {
return $passwd;
}
//bi = MD5(S + c(i-1))
$hash_b = md5($S . substr($bin, $offset, 16), true);
}
//都循环完了,还没看见结束,返回空
return '';
}
```
### CHAP-Password
> 从上面的代码来看`User-Password`是不安全的,只要知道了**secret_key**,密码明文完全可以解密出来,CHAP 密码原文是无法解密出来的
>
> RADIUS 服务器根据用户名查找密码,使用 MD5 在 CHAP ID 八位位组,密码和 CHAP 质询(如果存在 CHAP-Challenge 属性,否则从请求身份验证器)加密质询,以及 将该结果与 CHAP 密码进行比较。 如果它们匹配,则服务器发回一个访问接受,否则它将发回一个访问拒绝。
>
> 此属性指示由 PPP 挑战握手认证协议CHAP用户响应挑战而提供的响应值。 它仅用于 Access-Request 数据包。
>
> 如果存在于数据包中,则在 CHAP-Challenge 属性60中找到 CHAP 质询值,否则在请求验证器字段中找到。
[https://tools.ietf.org/html/rfc2865#page-8](https://tools.ietf.org/html/rfc2865#page-8)
[https://tools.ietf.org/html/rfc2865#page-28](https://tools.ietf.org/html/rfc2865#page-28)
CHAP-Password 的结构是这样的,中间还有一个 CHAP Ident,后面 string 是一个 16 个字节的字符串,一听就觉得是 md5
```
A summary of the CHAP-Password Attribute format is shown below. The
fields are transmitted from left to right.
0 1 2
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
| Type | Length | CHAP Ident | String ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
```
感觉 CHAP 比 PAP 好写多了,直接验证**md5(chapid+passwd+CHAP-Challenge(没有则用 Authenticator))**和 String 是否相等就好了
```php
//调用
echo "isPass:" . (
$this->verify_chap_passwd($attr['CHAP-Password'],
'qwe123',
$attr['CHAP-Challenge'] ?? $struct['authenticator']
) ? 'true' : 'false');
public function verify_chap_passwd(string $bin, string $pwd, string $chap): bool {
if (strlen($bin) != 17) return false;
$chapid = $bin[0];
$string = substr($bin, 1);
return md5($chapid . $pwd . $chap, true) == $string;
}
```
### 封包
> 处理完后,总还得人家一个回信吧
[https://tools.ietf.org/html/rfc2865#page-19](https://tools.ietf.org/html/rfc2865#page-19)
都是一样的,只是吧解包变成封包一个逆向操作,值得注意的是**Authenticator**,在接收的时候这个是随机的,发送的时候,我们需要带进去计算**MD5(Code+ID+Length+RequestAuth+Attributes+Secret)**
```php
/**
* 封包
* @param int $code
* @param int $identifier
* @param string $reqAuthenticator
* @param array $attr
* @return string
*/
public function pack(int $code, int $identifier, string $reqAuthenticator, array $attr = []): string {
$attr_bin = '';
foreach ($attr as $key => $value) {
$attr_bin .= $this->pack_attr($key, $value);
}
$len = 20 + strlen($attr_bin);
//MD5(Code+ID+Length+RequestAuth+Attributes+Secret)
$send = pack('ccna16',
$code, $identifier, $len,
md5(chr($code) . chr($identifier) . pack('n', $len) .
$reqAuthenticator . $attr_bin . $this->secret_key, true)
) . $attr_bin;
//这里实际使用的时候有错误,因为NTR工具没有校验Response Authenticator...现在已经修改了
return $send;
}
/**
* 封包属性
* @param $code
* @param string $data
* @return string
*/
public function pack_attr($code, string $data): string {
return pack('cc', $code, 2 + strlen($data)) . $data;
}
```
![](img/03-UDP-radius%E5%8D%8F%E8%AE%AE%E5%AE%9E%E7%8E%B0.assets/TIM%E6%88%AA%E5%9B%BE20180624194116-300x147.png)
> 完整代码中的封包有错误,请看上面的注释,在代码中修改了
完成,完整代码: [start.php](./img/03-UDP-radius协议实现.assets/start.zip)

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Some files were not shown because too many files have changed in this diff Show More