Godtoy

Godtoy 查看完整档案

成都编辑重庆社会大学  |  Software engineering 编辑易猿云  |  devops 编辑 blog.oeynet.com 编辑
编辑

Technology change the 'hello world'
https://blog.oeynet.com
https://github.com/godtoy

孤孤单单自由职业,前有脱发,后不毕业,悲剧悲剧

个人动态

Godtoy 发布了文章 · 2020-06-16

Golang 协程Cover异常防止闪退

协程报错闪退

协程如果不cover异常会出现闪退问题,但是在大批量创建各种协程每次都需要cover会非常难处理,golang 异常处理我不是很熟悉,简单写了一个,不知道是否靠谱

common

package logger

import (
    "fmt"
    "reflect"
    "runtime"
    "strings"
)

//创建安全的协程
func CreateSafeGo(call func(), errCall func(err interface{})) {
    go func() {
        defer SafeGoRecover(errCall)
        call()
    }()
}

func SafeGoRecover(errCall func(err interface{})) {
    if err := recover(); err != nil {
        Error("recover error: %v", err)
        if errCall != nil {
            errCall(err)
        }
    }
}

func getCaller() string {
    src := "No Caller"
    _, file, lineno, ok := runtime.Caller(2)
    var strim = "src/"
    //fix # 不是每个前面都有src的,所以可以获取当前
    if ok {
        src = strings.Replace(fmt.Sprintf("%s:%d", stringTrim(file, strim), lineno), "%2e", ".", -1)
    }
    return src
}

//TODO Params
//反射实现参数恢复
func SafeGoWithParams(call interface{}, errCall func(err interface{}), params ...interface{}) {
    go func() {
        defer SafeGoRecover(errCall)
        funcValue := reflect.ValueOf(call)
        var paramsList []reflect.Value
        for _, v := range params {
            paramsList = append(paramsList, reflect.ValueOf(v))
        }
        funcValue.Call(paramsList)
    }()
}

test

package logger

import (
    "fmt"
    "testing"
    "time"
)

func TestCreateSafeGo(t *testing.T) {
    CreateSafeGo(func() {
        panic("1231")
    }, func(err interface{}) {
        Error("asdasd")

    })
    time.Sleep(time.Second)
}

func TestSafeGoRecover(t *testing.T) {
    go func() {
        defer SafeGoRecover(func(err interface{}) {
            Error("asdasd")
        })
        panic("1231")
    }()
    time.Sleep(time.Second)
}

func TestCreateSafeGoWithParams(t *testing.T) {
    SafeGoWithParams(func(aa int) {
        fmt.Println(aa)
    }, func(err interface{}) {

    }, 123)
    time.Sleep(time.Second)
}
查看原文

赞 0 收藏 0 评论 0

Godtoy 关注了用户 · 2020-02-04

LNMPRG源码研究 @php7internal

一群热爱代码的人 研究Nginx PHP Redis Memcache Beanstalk 等源码 以及一群热爱前端的人
希望交流的朋友请加微信 289007301 注明:思否 拉到交流群,也可关注公众号:LNMPRG源码研究

《PHP7底层设计与源码分析》勘误https://segmentfault.com/a/11...

《Redis5命令设计与源码分析》https://item.jd.com/12566383....

景罗 陈雷 李乐 黄桃 施洪宝 季伟滨 闫昌 李志 王坤 肖涛 谭淼 张仕华 方波 周生政 熊浩含 张晶晶(女) 李长林 朱栋 张晶晶(男) 陈朝飞 巨振声 杨晓伟 闫小坤 韩鹏 夏达 周睿 李仲伟 张根红 景罗 欧阳 孙伟 李德 twosee

关注 11661

Godtoy 赞了文章 · 2020-02-04

【Nginx源码分析】Nginx中http2浅析

运营研发 张仕华

本文通过一个小例子串一遍nginx处理http2的流程。主要涉及到http2的协议以及nginx的处理流程。

http2简介

http2比较http1.1主要有如下五个方面的不同:

二进制协议

http1.1请求行和请求头部都是纯文本编码,即可以直接按ascii字符解释,而http2是有自己的编码格式。并且nginx中http2必须建立在ssl协议之上。

头部压缩

举个例子,HTTP1.1传一个header <method: GET>,需要11个字符.http2中有一个静态索引表,客户端传索引键,例如1,nginx通过查表能知道1代表method: GET.nginx中除了该静态表,还会有一个动态表,保存例如host这种变化的头部

多路复用

http1.1一个连接上只能传输一个请求,当一个请求结束之后才能传输下一个请求。所以对http1.1协议的服务发起请求时,一般浏览器会建立6条连接,并行的去请求不同的资源。而http2的二进制协议中有一个frame的概念,每个frame有自己的id,所以一个连接上可以同时多路复用传输多个不同id的frame

主动push

http1.1是请求-响应模型,而http2可以主动给客户端推送资源

优先级

既然多路复用,所有数据跑在了一条通道上,必然会有优先级的需求

本文的例子主要通过解析报文说明头三个特性

配置环境

NGINX配置如下:

    server {
        listen 8443 ssl http2;
        access_log  logs/host_server2.access.log  main;
        ssl_certificate /home/xiaoju/nginx-2/nginx-selfsigned.crt;
        ssl_certificate_key /home/xiaoju/nginx-2/nginx-selfsigned.key;
        ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;

        location / {
            root   html;
            index  index.html index.htm /abc.html;
            access_log  logs/host_location3.access.log  main;
            http2_push /favicon.ico;
            http2_push /nginx.png;
        }
    }

客户端按如下方式发起请求:

curl  -k  -I   -L https://IP:8443
HTTP/2 200  //可以看到,返回是http/2
server: nginx/1.14.0
date: Tue, 11 Dec 2018 09:20:33 GMT
content-type: text/html
content-length: 664
last-modified: Tue, 11 Dec 2018 04:19:32 GMT
etag: "5c0f3ad4-298"
accept-ranges: bytes

请求解析

客户端请求问题

先思考一个问题,上文配置中使用curl发送请求时,为何直接返回的是http/2,而不是http/1.1(虽然服务端配置了使用http2,但万一客户端未支持http2协议,直接返回http2客户端会解析不了)

因为nginx中http2必须在ssl之上,所以我们首先通过在nginx代码中的ssl握手部分打断点gdb跟一下.

(gdb) b ngx_ssl_handshake_handler  //ssl握手函数
Breakpoint 1 at 0x47ddb5: file src/event/ngx_event_openssl.c, line 1373.
(gdb) c
Continuing.
Breakpoint 1, ngx_ssl_handshake_handler (ev=0x16141f0) at src/event/ngx_event_openssl.c:1373
1373    {

1390        c->ssl->handler(c); //实际处理逻辑位于ngx_http_ssl_handshake_handler
(gdb) s
ngx_http_ssl_handshake_handler (c=0x15da400) at src/http/ngx_http_request.c:782
782    {

(gdb) n
805            if (hc->addr_conf->http2) { //配置http2后hc->addr_conf->http2标志位为1

(gdb) n
808                SSL_get0_alpn_selected(c->ssl->connection, &data, &len);//从ssl协议中取出alpn


(gdb) n
820                if (len == 2 && data[0] == 'h' && data[1] == '2') { //如果为h2,说明客户端支持升级到http2协议

(gdb) n
821                    ngx_http_v2_init(c->read);//开始进入http2的初始化阶段

简单说就是通过ssl协议握手阶段获取一个alpn相关的配置,如果是h2,就进入http2的处理流程。我们通过wireshark抓包可以更直观的看出这个流程

clipboard.png

如上图,在ssl握手中的Client Hello 阶段有一个协议扩展alpn

http2报文格式

http2 以一个preface开头,接着是一个个的frame,其中每个frame都有一个header,如下:

clipboard.png

其中length代表frame内容的长度,type表明frame的类型,flag给frame做一些特殊的标记,sid代表的就是frame的id.

其中 frame有如下10种类型

#define NGX_HTTP_V2_DATA_FRAME           0x0 //body数据
#define NGX_HTTP_V2_HEADERS_FRAME        0x1 //header数据
#define NGX_HTTP_V2_PRIORITY_FRAME       0x2 //优先级设置
#define NGX_HTTP_V2_RST_STREAM_FRAME     0x3 //重置一个stream
#define NGX_HTTP_V2_SETTINGS_FRAME       0x4 //其他设置项,例如是否开启push,同时能够处理的stream数量等
#define NGX_HTTP_V2_PUSH_PROMISE_FRAME   0x5 //push
#define NGX_HTTP_V2_PING_FRAME           0x6 //ping
#define NGX_HTTP_V2_GOAWAY_FRAME         0x7 //goaway.发送此frame后会重新建立连接
#define NGX_HTTP_V2_WINDOW_UPDATE_FRAME  0x8 //窗口更新 流控使用
#define NGX_HTTP_V2_CONTINUATION_FRAME   0x9 //当一个frame发送不完数据时,可以按continuation格式继续发送

frame ID在客户端按奇数递增,例如1,3,5,偶数型id留给服务端推送push时使用,设置连接属性相关的frame id都为0

flags有如下定义:

#define NGX_HTTP_V2_NO_FLAG              0x00 //未设置
#define NGX_HTTP_V2_ACK_FLAG             0x01 //ack flag
#define NGX_HTTP_V2_END_STREAM_FLAG      0x01 //结束stream
#define NGX_HTTP_V2_END_HEADERS_FLAG     0x04 //结束headers
#define NGX_HTTP_V2_PADDED_FLAG          0x08 //填充flag
#define NGX_HTTP_V2_PRIORITY_FLAG        0x20 //优先级设置flag

如下是一个http头类型frame具体的内容格式:

clipboard.png

padded和priority由上文头部的flag决定是否有这两字段。接下来占8bit的flag决定header是否需要索引,如果需要,索引号是多少。

huff(1)表明该字段是否使用了huffman编码。header_value_len(7)和header_value是具体头字段的value值

如下是一个设置相关的frame

clipboard.png

如下是一个窗口更新的frame

clipboard.png

下边我们看一个具体的例子,来更直观的了解下。

http2报文解析

新版本的curl有一个–http2参数,可以直接指明使用http2进行通讯。我们将客户端命令修改如下:

curl --http2 -k  -I   -L https://10.96.79.14:8443

通过上边的gdb跟踪,我们看到http2初始化入口函数为ngx_http_v2_init,直接在此处打断点,继续跟踪代码.跟踪过程不再详细描述,当把报文读取进缓存之后,我们直接在gdb中bt查看调用路径,如下:

#0  ngx_http_v2_state_preface (h2c=0x15a9310, pos=0x164b0b0 "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", end=0x164b11e "")
    at src/http/v2/ngx_http_v2.c:713
#1  0x00000000004bca20 in ngx_http_v2_read_handler (rev=0x16141f0) at src/http/v2/ngx_http_v2.c:415
#2  0x00000000004bcf8a in ngx_http_v2_init (rev=0x16141f0) at src/http/v2/ngx_http_v2.c:328
#3  0x0000000000490a13 in ngx_http_ssl_handshake_handler (c=0x15da400) at src/http/ngx_http_request.c:821
#4  0x000000000047de24 in ngx_ssl_handshake_handler (ev=0x16141f0) at src/event/ngx_event_openssl.c:1390
#5  0x0000000000479637 in ngx_epoll_process_events (cycle=0x1597e30, timer=<optimized out>, flags=<optimized out>)
    at src/event/modules/ngx_epoll_module.c:902
#6  0x000000000046f9db in ngx_process_events_and_timers (cycle=0x1597e30) at src/event/ngx_event.c:242
#7  0x000000000047761c in ngx_worker_process_cycle (cycle=0x1597e30, data=<optimized out>) at src/os/unix/ngx_process_cycle.c:750
#8  0x0000000000475c50 in ngx_spawn_process (cycle=0x1597e30, proc=0x477589 <ngx_worker_process_cycle>, data=0x0,
    name=0x684922 "worker process", respawn=-3) at src/os/unix/ngx_process.c:199
#9  0x00000000004769aa in ngx_start_worker_processes (cycle=0x1597e30, n=1, type=-3) at src/os/unix/ngx_process_cycle.c:359
#10 0x0000000000477cb0 in ngx_master_process_cycle (cycle=0x1597e30) at src/os/unix/ngx_process_cycle.c:131
#11 0x0000000000450ea4 in main (argc=<optimized out>, argv=<optimized out>) at src/core/nginx.c:382

调用到ngx_http_v2_state_preface这个函数之后,开始处理http2请求,我们将请求内容打印出来看一下:

(gdb) p end-pos
$1 = 110
(gdb) p *pos@110
$2 = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n\000\000\022\004\000\000\000\000\000\000\003\000\000\000d\000\004@\000\000\000\000\002\000\000\000\000\000\000\004\b\000\000\000\000\000?\377\000\001\000\000%\001\005\000\000\000\001B\004HEAD\204\207A\214\b\027}\305\335}p\265q\346\232gz\210%\266Pë\266\322\340S\003*/*"

nginx接下来开始处理http2请求,处理方法可以按上述方法继续跟踪,我们直接按http2协议将上述报文解析一下,如下所示:

注意gdb打印出来的是八进制格式

clipboard.png

clipboard.png

http push抓包

注意上文nginx配置中配置了两条http2_push指令,即服务端会在请求index.html时主动将favicon.ico和nginx.png两个图片push下去。

wireshark中抓包如下:

clipboard.png

服务端首先发送一个push_promise报文,报文中会包括push的文件路径和frame id.第二个和第三个红框即开始push具体的信息,frame id分别为2和4

我们从浏览器端看一下push的请求:

clipboard.png

不主动push请求如下:

clipboard.png

浏览器必须首先将index.html加载之后才会知道接着去请求哪些资源,于是favicon.ico和nginx.png就会延迟加载。

问题

HTTP2如果在服务端动态索引header,会使http变成有状态的服务,集群之间如何解决header头缓存的问题?
静态资源文件首次请求后会在浏览器端缓存,push如何保证只推送一次(即只有首次请求时才push)?
参考资料
1.https://www.nginx.com/blog/ht...

2.https://httpwg.org/specs/rfc7540

查看原文

赞 11 收藏 8 评论 0

Godtoy 赞了问题 · 2020-01-15

解决go mod 怎么导入本地其它项目的包?

go version 1.13.1

在做微服务中使用了 grpc,每个微服务都用了 go mod 模式,因为grpc在创建client时 需要调用在另一个服务里的 pb包。请问怎么导入啊?
QQ20191015-180306@2x.png
如图:这样总是报错无法引入。
谢谢各位

关注 3 回答 2

Godtoy 发布了文章 · 2019-10-25

使用mongoose-paginate-v2查询缓慢问题

场景

mongoose-paginate-v2 是一个mongoose上的分页插件,我也用过很多次了,但是最近在创建项目遇到了问题。

老代码中不使用分页插件进行查询,然后自己使用中间件进行分页
old codes, 16ms

 @Get('')
  public async index(@Query() query, @Pager() pager: any, @Req() req: Request, @Res() res: Response) {
    const map: any = {};
    if (query.group && query.group !== '') {
      map.group = query.group;
    }
    if (query.username && query.username !== '') {
      map.username = { $regex: query.username };
    }
    const count = await this.model.countDocuments(map);
    const page = pager.parse(count);
    console.log(page);

    const list = await this.model.find(map).sort({ updateAt: -1 })
      .limit(page.limit)
      .skip(page.skip)
      .select('_id group username device.deviceType deviceType createAt updateAt status enable creator proxy')
      .populate('group');

    return res.status(HttpStatus.OK).json({
      message: 'success',
      data: {
        list: [],
      },
    });
  }

新代码中使用分页插件,耗时约1.5s 非常的缓慢
use paginate: 1532ms

 public async slow(@Query() query, @Pager() pager: any, @Req() req: Request, @Res() res: Response) {
    const map: any = {};
    if (query.group && query.group !== '') {
      map.group = query.group;
    }
    if (query.username && query.username !== '') {
      map.username = { $regex: query.username };
    }
    const count = await this.model.count(map);
    const page = pager.parser(count);
    console.log(page);
    const list = await this.model.paginate(map, {
      limit: pager.limit,
      offset: pager.skip,
      select: '_id group username device.deviceType deviceType createAt updateAt status enable creator proxy',
      populate: ['group'],
      sort: { updateAt: -1 },
    });

    return res.status(HttpStatus.OK).json({
      message: 'success',
      data: list,
    });
  }

提issue

我也挺忙的,所以没去看代码,直接提了issue,目前收到回复是需要更新到新版本v1.3.3 更新后回复正常

Issue地址

博客: https://github.com/zhaojunlike
查看原文

赞 1 收藏 1 评论 0

Godtoy 发布了文章 · 2019-10-24

puppeteer-firefox 开启扩展

puppeteer-firefox安装扩展

puppeteer-firefox 目前已经有许多人在投入开发工作,但是和chrome的launch打开扩展api不一致,在chrome中,我们可以很容易配置参数就可以打开插件,但是在firefox中我们要使用web-ext 去启动firefox并且使用connect去连接。

官方issue

Current tip-of-tree status of Puppeteer-Firefox is availabe at isPuppeteerFirefoxReady?

Add-ons

Firefox Add-ons differs from Chrome extensions, hence precess of its install is different.
Firefox Add-on can be installed using web-ext library which runs Firefox binary and can be connected using Puppeteer connect API.

const webExt = require('web-ext').default;
const pptrFirefox = require('puppeteer-firefox');
const getPort = require('get-port');
(async () => {
  const CDPPort = await getPort();
  await webExt.cmd.run(
      {
        sourceDir: 'path-to-add-on',
        firefox: pptrFirefox.executablePath(),
        args: [`-juggler=${CDPPort}`]
      },
      {
        // These are non CLI related options for each function.
        // You need to specify this one so that your NodeJS application
        // can continue running after web-ext is finished.
        shouldExitProgram: false
      }
    );
    const browser = await pptrFirefox.connect({
      browserWSEndpoint: `ws://127.0.0.1:${CDPPort}`
    });
})();

package.json example

{
  "dependencies": {
    ...
    "get-port": "^4.2.0",
    "web-ext": "^3.1.0",
    "puppeteer-firefox": "^0.5.0"
    ...
  },
}

chrome扩展开启方式

chrome extensions

博客: https://github.com/zhaojunlike
查看原文

赞 2 收藏 2 评论 0

Godtoy 发布了文章 · 2019-10-23

nest.js 使用express需要提供多个静态目录的操作

场景

在官方提供的文档中提供方式,
file

app.module.ts

 ServeStaticModule.forRoot({
      rootPath: path.join(process.cwd(), 'static'),
      serveStaticOptions: {
        maxAge: 10000,
      },
    }),

其中提供了一个静态资源目录,如果想使用多个静态目录,可以在app配置中间件

import * as express from 'express';
import * as path from 'path';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, { cors: true });
  //静态资源目录
  app.use(express.static(path.join(process.cwd(), './public')));
  
  const options = new DocumentBuilder()
    .setTitle('Nike Snkrs Open API')
    .setDescription('Nike snkrs 一些api')
    .setVersion('1.0')
    .build();

  const document = SwaggerModule.createDocument(app, options);
  SwaggerModule.setup('api', app, document);
  await app.listen(9011);
}
博客: https://github.com/zhaojunlike
查看原文

赞 1 收藏 1 评论 0

Godtoy 发布了文章 · 2019-10-19

windows中使用winsw进行服务管理开启启动服务

场景

常会用到cow 或者 frp这类的工具,因为基本都是go写的所以斌没有提供windows的一些服务控制脚本之类的,这个时候我们可以借助第三方工具winsw,它提供了xml文件可以进行服务的动态管理,方便不少。下面是一系列的使用方法,不喜欢我bb的可以直接看官方文档。

Link:https://github.com/kohsuke/winsw

安装

到如下链接获取到你想要的winsw版本:https://github.com/kohsuke/wi...

2019-10-19-20-53-25

现在后,我们可以一并下载一个配置文件,它的名字是这个软件的名字加上exe,例如:
2019-10-19-20-54-23

xml文件内容是winsw管理的核心服务,例如我现在管理一个我的frp服务,那么他的服务配置文件如下

    <service>
      <id>frpc</id>
      <name>frpc</name>
      <description>frpc service.</description>
      <executable>F:\tools\frp_0.29.0_windows_amd64\frpc.exe</executable>
      <arguments>-c F:\tools\frp_0.29.0_windows_amd64\frpc.ini</arguments>
      <logmode>none</logmode>
    </service>

具体的full版本配置文件可以参考:https://github.com/kohsuke/wi...,并且winsw,支持输出日志。

命令

先打开powershell,posershell 必须使用管理员执行

# 安装服务
./winsw install  

# 卸载服务
./winsw uninstall

# 启动服务
./winsw start

./winsw restart

./winsw status

2019-10-19-20-58-02

效果

这个是启动frp后的效果

2019-10-19-20-59-28

如何配置多个服务

issue:https://github.com/kohsuke/wi...

目前winsw并不支持,在xml配置多个服务,所以需要使用可以拷贝多个exe来进行管理

2019-10-19-21-08-28

查看原文

赞 0 收藏 0 评论 0

Godtoy 发布了文章 · 2019-10-08

腾讯滑块验证码识别和加速度模拟(1)

腾讯滑块验证码识别

腾讯滑块验证码识别,识别凹槽的x轴位置,mock滑块的加速度。该项目公开API,提供识别和加速度模拟部分,第二部分模拟滑动进行识别返回数据请求

项目地址:https://github.com/zhaojunlik...

原文地址:https://segmentfault.com/a/11...

安装python环境

参考:https://janikarhunen.fi/how-t...

sudo yum install https://centos7.iuscommunity.org/ius-release.rpm
sudo yum install python36u
python3.6 -V
sudo yum install python36u-pip
sudo yum install python36u-devel

创建环境 Creating a virtualenv

python3.6 -m venv venv
. venv/bin/activate
pip install [package_name]
# 安装依赖
pip install -r requirements.txt 

daemonize 运行

# 参考 https://www.digitalocean.com/community/tutorials/how-to-serve-flask-applications-with-uswgi-and-nginx-on-ubuntu-18-04
# Install the latest stable release:
pip install uwsgi
# ... or if you want to install the latest LTS (long term support) release,
pip install https://projects.unbit.it/downloads/uwsgi-lts.tar.gz

# 创建ln 
cp captcha.service /etc/systemd/system/captcha.service
systemctl enable captcha.service
systemctl start captcha.service
uwsgi --ini /usr/local/nginx/html/myblog/uwsgiconfig.ini

#后台运行
uwsgi --ini /usr/local/nginx/html/myblog/uwsgiconfig.ini --daemonize /usr/local/nginx/html/myblog/myblog.out

是用nginx做代理

在nginx部分做一个代理

        location /tx/ {
            add_header Access-Control-Allow-Origin *;
            include        uwsgi_params;
            uwsgi_pass     127.0.0.1:8008;
        }

访问api

请求图片识别和加速度模拟

http://127.0.0.1:5000/tx/image

POST /tx/image HTTP/1.1
Host:host
Content-Type: application/json
Accept: */*
Cache-Control: no-cache
Accept-Encoding: gzip, deflate
Content-Length: 1055
Connection: keep-alive
cache-control: no-cache

{
    "url": "图片的地址"
}

返回数据

{
    "data": {
        "list": [],//模拟的点
        "url": "",//图片地址
        "x": 515,// x轴的偏移量
    },
    "message": "解析成功"
}

模拟浏览器移动

            const slider = {width: 680, point: 0, move: 0, steps: 0, posX: 0};//原本的高度
            //开始计算移动的距离
            slider.point = bgSize.width / slider.width * x;
            slider.move = handle.x + slider.point - 5;
            slider.steps = Math.random() * 100 / 30 + 100;
            slider.posX = handle.x + handle.width / 2;

            logger.info(`开始识别和移动滑块`, slider);

            //滑块的位置
            await page.mouse.move(slider.posX, handle.y + handle.height / 3, {steps: slider.steps});
            await page.mouse.down();
            let val = handle.x;
            for (let i = 0; i < traces.length; i++) {
                val += bgSize.width / slider.width * (traces[i]);//缩放距离
                slider.move = val;
                if (val <= slider.posX) continue;
                await page.mouse.move(slider.move, handle.y + handle.height / 2 + 5);
            }
            await page.waitFor(100);
            await page.mouse.up();

验证码识别成功后悔返回验证识别结果的Ticket

2019-10-08-22-59-04

我的博客

https://blog.oeynet.com

协议

授权协议:只允许研究、学习目的的分享、使用、修改,不允许任何商业用途。转载请注明出处,感谢。

查看原文

赞 6 收藏 3 评论 0

Godtoy 关注了用户 · 2019-09-26

菜的黑人牙膏 @caideheirenyagao

关注 10

认证与成就

  • SegmentFault 讲师
  • 获得 187 次点赞
  • 获得 22 枚徽章 获得 1 枚金徽章, 获得 6 枚银徽章, 获得 15 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

注册于 2016-05-03
个人主页被 4.1k 人浏览