IT牛人博客聚合网站 发现IT技术最优秀的内容, 寻找IT技术的价值 http://www.udpwork.com/ zh_CN http://www.udpwork.com/about hourly 1 Sat, 24 Feb 2018 00:19:09 +0800 <![CDATA[终端软件里正确设置 ALT 键和 BACKSPACE 键]]> http://www.udpwork.com/item/16668.html http://www.udpwork.com/item/16668.html#reviews Fri, 23 Feb 2018 15:42:59 +0800 skywind http://www.udpwork.com/item/16668.html 不管你在终端下使用 vim/neovim, emacs, nano 或者 zsh,你都会碰到使用 ALT 键的情况(终端下叫做 meta键),而由于历史原因,大部分终端软件的默认设置都无法正确使用 ALT 键。

要在终端下正确使用 ALT键最简单的做法是:首先将终端软件的 “使用 Alt键作为 Meta键” 的功能打开,意思是如果你在终端下按下 ALT+X,那么终端软件将会发送<ESC>x两个字节过去,字节码为:0x27, 0x78。

SecureCRT:终端设置

XShell4 终端设置:

其他终端软件里:

  • Putty/MinTTY 默认ALT+X 就是发送<ESC>x过去
  • Mac下面的 iTerm2/Terminal.app 需要跟 XShell / SecureCRT一样设置一下
  • Ubuntu 下面的 GnomeTerminal 默认也是发送<ESC>x过去的
  • 任意平台下面的 xterm 可以配置~/.Xdefaults来设置这个行为。

这样的话,终端里的软件就能识别你的 ALT 组合键了,设置好以后,你可以在终端下使用命令:

showkey -a

来查看自己的设置正确不,是不是按下 ALT_a 后正确发送了 0x1b, 0x61 两个字节过去了?

功能键超时

那么终端里按下 ALTX 和先按 ESC 键再按 X键有什么区别呢?答案是没有区别,你在 emacs 中快速先后按下 ESC 和 v 的话,emacs 就会以为你按了 ALTv,然后给你来个上翻页了。

远程主机一般靠超时来区别到底是 ALTX 还是先按 ESC 再按 X 键,如果你 100 毫秒内先后发送了 ESC 和 X 过来,远程主机会识别成 ALTX 键,否则识别成 ESC 和 X 两个键,这个超时时间可以设置,一般设置成 100ms 或者 50ms ,但不要低于 25ms,否则网络一卡可能有概率会判断错误。

在 Vim/NeoVim 中你可以通过 ttimeoutlen 来设置功能键超时检测为 50 毫秒,比如:

:set ttimeout ttimeoutlen=50

而 tmux 中,也有类似配置,比如:

set-option -g escape-time 50

就连 ncurses 库中也有类似功能键超时检测的设置,来区别到底是功能键/组合键,还是数个单独的按键。这里你可能觉得这样靠超时检测很不可靠,那你要问 VT100, VT220 的标准制定者了。不过我个人常年设置成 50 毫秒后,连接到各种国内外速度不一的主机上并没有被网速慢把功能键卡成两个键的情况,应该是终端软件中本身也在尽量保证同一组按键序列能够尽量同时发送,所以我链接到国外 rtt=400 左右的主机上工作时,尽管网速经常不稳定,也没有发生错误识别的问题,那其他网速正常的主机就更不用担心了。

更友好的终端设置

上面在 SecureCRT / XShell 中设置了将 alt 键作为发送 +ESC x 的 meta 键后,你会发现,终端软件中固有的一些 ALT 组合键全部失效了,比如原来在终端中 ALT1 到 ALT9 可以切换终端的 TAB,ALT_B 可以打开链接管理器,这下都全部用不了了,这是件比较坑爹的事情,能不能保留有限的几个 ALT 组合键给终端软件使用,剩下的全部当作 meta 键呢?答案是可以的,先取消终端里 ALT 当作 meta 键的设置,恢复成默认状态,然后打开终端软件 keymap 设置窗口,将你不需要保留的 ALT 组合键全部设置成发送 +ESC x 字符串。

那么一个个设置可能有些麻烦,对于 SecureCRT 的话,我生成了一个配置文件:

A   VK_A                    "\033a"
A   VK_D                    "\033d"
A   VK_E                    "\033e"
A   VK_G                    "\033g"
A   VK_H                    "\033h"
....
AS  VK_A                    "\033A"
AS  VK_B                    "\033B"
AS  VK_C                    "\033C"
AS  VK_D                    "\033D"

可以到这里下载现成的,在 keymap editor 窗口中加载进去即可。

这份 keymap 配置除了保留了 SecureCRT 常用的 ALT1 – ALT9 ,ALTB, ALTR 和 ALTI 外,其他的 alt 组合都设置成了 +ESC x 的 meta 键序列。并且将 ALTSHIFT1 到 ALTSHIFT_9 映射到了终端里的 +ESC 1 到 +ESC 9 ,也就是说你的 ALT+数字 被保留给软件切换TAB用了,而 ALT+SHIFT+数字 被映射成了终端链接中的 ALT+数字,这样在终端里碰到需要 ALT+数字 的地方,可以用 ALT+SHIFT+数字 来代替。

设置好以后,可以继续使用 Linux 下的showkey -a命令查看一下是否正常。

Vim里识别 ALT 键

前面在终端软件里配置好 ALT键,但是 Vim 的话,由于历史原因,需要在你的 vimrc 里加一段键盘码配置:

function! Terminal_MetaMode(mode)
    if has('nvim') || has('gui_running')
        return
    endif
    function! s:metacode(mode, key)
        if a:mode == 0
            exec "set <M-".a:key.">=\e".a:key
        else
            exec "set <M-".a:key.">=\e]{0}".a:key."~"
        endif
    endfunc
    for i in range(10)
        call s:metacode(a:mode, nr2char(char2nr('0') + i))
    endfor
    for i in range(26)
        call s:metacode(a:mode, nr2char(char2nr('a') + i))
        call s:metacode(a:mode, nr2char(char2nr('A') + i))
    endfor
    if a:mode != 0
        for c in [',', '.', '/', ';', '[', ']', '{', '}']
            call s:metacode(a:mode, c)
        endfor
        for c in ['?', ':', '-', '_']
            call s:metacode(a:mode, c)
        endfor
    else
        for c in [',', '.', '/', ';', '{', '}']
            call s:metacode(a:mode, c)
        endfor
        for c in ['?', ':', '-', '_']
            call s:metacode(a:mode, c)
        endfor
    endif
    set ttimeout
    if $TMUX != ''
        set ttimeoutlen=30
    elseif &ttimeoutlen > 80 || &ttimeoutlen <= 0
        set ttimeoutlen=80
    endif
endfunc

call Terminal_MetaMode(0)

然后你就可以正确在 Vim 映射 ALT 键了,具体原理见:help set-termcap以及:

http://www.skywind.me/blog/archives/1846

其他的诸如 emacs, nano 和 neovim 等都不需要额外设置。

终端里正确设置 BS 键

还是 VT100 的历史原因,BACKSPACE 键和 CTRL-H 给混淆起来了,默认情况下,终端里不管按 CTRL-H 还是 BACKSPACE 时都是发送 ASCII 码为 0x08 的^H过去。导致我们想在 Vim/Emacs 中映射 CTRL-H 去干别的事情时会影响到 BACKSPACE 键的使用。

因此得按照 VT220 的新标准修改一下 BACKSPACE 的设置,让它发送 ASCII 码 0x7f 即^?过去:

  • SecureCRT : Session Options -> Terminal -> Emulation -> Mapped Keys, 勾选 Backspace sends delete
  • XShell : Properties -> Terminal -> Keyboard 里,把<BS>设置成 127,而<DEL>设置成 VT220 Del
  • Putty : 好像默认是 ^? 的不过需要到:Configuration -> Terminal -> Keyboard 下面下确认下 The Backspace key 是 Control-? (127)
  • Terminal.app : 好像默认是发送^?的,你也可以到 Profiles Advanced 下面确认下 “Delete sends Control-H” 没有勾选。
  • iTerm2 : 默认也是发送 ^? 的,可以到 Profiles -> Keys下面确认一下 “Delete key sends ^H” 没有被勾选。
  • Gnome-Terminal : 默认发送 ^? 的,参见具体文本配置文件。
  • MinTTY : 设置 vt220/xterm 的话,默认发送 ^? 的,似乎还不能改。

你要深究原因的话,可以见:

http://www.skywind.me/blog/archives/1857

修改好以后可以继续运行:

showkey -a

检查一下,你的 BACKSPACE 键被按下时是否正确发送了 0x7f 字符过去。设置成功的话,终端下 CTRL-h 和 backspace 就不会出现混淆问题了。

]]>
不管你在终端下使用 vim/neovim, emacs, nano 或者 zsh,你都会碰到使用 ALT 键的情况(终端下叫做 meta键),而由于历史原因,大部分终端软件的默认设置都无法正确使用 ALT 键。

要在终端下正确使用 ALT键最简单的做法是:首先将终端软件的 “使用 Alt键作为 Meta键” 的功能打开,意思是如果你在终端下按下 ALT+X,那么终端软件将会发送<ESC>x两个字节过去,字节码为:0x27, 0x78。

SecureCRT:终端设置

XShell4 终端设置:

其他终端软件里:

  • Putty/MinTTY 默认ALT+X 就是发送<ESC>x过去
  • Mac下面的 iTerm2/Terminal.app 需要跟 XShell / SecureCRT一样设置一下
  • Ubuntu 下面的 GnomeTerminal 默认也是发送<ESC>x过去的
  • 任意平台下面的 xterm 可以配置~/.Xdefaults来设置这个行为。

这样的话,终端里的软件就能识别你的 ALT 组合键了,设置好以后,你可以在终端下使用命令:

showkey -a

来查看自己的设置正确不,是不是按下 ALT_a 后正确发送了 0x1b, 0x61 两个字节过去了?

功能键超时

那么终端里按下 ALTX 和先按 ESC 键再按 X键有什么区别呢?答案是没有区别,你在 emacs 中快速先后按下 ESC 和 v 的话,emacs 就会以为你按了 ALTv,然后给你来个上翻页了。

远程主机一般靠超时来区别到底是 ALTX 还是先按 ESC 再按 X 键,如果你 100 毫秒内先后发送了 ESC 和 X 过来,远程主机会识别成 ALTX 键,否则识别成 ESC 和 X 两个键,这个超时时间可以设置,一般设置成 100ms 或者 50ms ,但不要低于 25ms,否则网络一卡可能有概率会判断错误。

在 Vim/NeoVim 中你可以通过 ttimeoutlen 来设置功能键超时检测为 50 毫秒,比如:

:set ttimeout ttimeoutlen=50

而 tmux 中,也有类似配置,比如:

set-option -g escape-time 50

就连 ncurses 库中也有类似功能键超时检测的设置,来区别到底是功能键/组合键,还是数个单独的按键。这里你可能觉得这样靠超时检测很不可靠,那你要问 VT100, VT220 的标准制定者了。不过我个人常年设置成 50 毫秒后,连接到各种国内外速度不一的主机上并没有被网速慢把功能键卡成两个键的情况,应该是终端软件中本身也在尽量保证同一组按键序列能够尽量同时发送,所以我链接到国外 rtt=400 左右的主机上工作时,尽管网速经常不稳定,也没有发生错误识别的问题,那其他网速正常的主机就更不用担心了。

更友好的终端设置

上面在 SecureCRT / XShell 中设置了将 alt 键作为发送 +ESC x 的 meta 键后,你会发现,终端软件中固有的一些 ALT 组合键全部失效了,比如原来在终端中 ALT1 到 ALT9 可以切换终端的 TAB,ALT_B 可以打开链接管理器,这下都全部用不了了,这是件比较坑爹的事情,能不能保留有限的几个 ALT 组合键给终端软件使用,剩下的全部当作 meta 键呢?答案是可以的,先取消终端里 ALT 当作 meta 键的设置,恢复成默认状态,然后打开终端软件 keymap 设置窗口,将你不需要保留的 ALT 组合键全部设置成发送 +ESC x 字符串。

那么一个个设置可能有些麻烦,对于 SecureCRT 的话,我生成了一个配置文件:

A   VK_A                    "\033a"
A   VK_D                    "\033d"
A   VK_E                    "\033e"
A   VK_G                    "\033g"
A   VK_H                    "\033h"
....
AS  VK_A                    "\033A"
AS  VK_B                    "\033B"
AS  VK_C                    "\033C"
AS  VK_D                    "\033D"

可以到这里下载现成的,在 keymap editor 窗口中加载进去即可。

这份 keymap 配置除了保留了 SecureCRT 常用的 ALT1 – ALT9 ,ALTB, ALTR 和 ALTI 外,其他的 alt 组合都设置成了 +ESC x 的 meta 键序列。并且将 ALTSHIFT1 到 ALTSHIFT_9 映射到了终端里的 +ESC 1 到 +ESC 9 ,也就是说你的 ALT+数字 被保留给软件切换TAB用了,而 ALT+SHIFT+数字 被映射成了终端链接中的 ALT+数字,这样在终端里碰到需要 ALT+数字 的地方,可以用 ALT+SHIFT+数字 来代替。

设置好以后,可以继续使用 Linux 下的showkey -a命令查看一下是否正常。

Vim里识别 ALT 键

前面在终端软件里配置好 ALT键,但是 Vim 的话,由于历史原因,需要在你的 vimrc 里加一段键盘码配置:

function! Terminal_MetaMode(mode)
    if has('nvim') || has('gui_running')
        return
    endif
    function! s:metacode(mode, key)
        if a:mode == 0
            exec "set <M-".a:key.">=\e".a:key
        else
            exec "set <M-".a:key.">=\e]{0}".a:key."~"
        endif
    endfunc
    for i in range(10)
        call s:metacode(a:mode, nr2char(char2nr('0') + i))
    endfor
    for i in range(26)
        call s:metacode(a:mode, nr2char(char2nr('a') + i))
        call s:metacode(a:mode, nr2char(char2nr('A') + i))
    endfor
    if a:mode != 0
        for c in [',', '.', '/', ';', '[', ']', '{', '}']
            call s:metacode(a:mode, c)
        endfor
        for c in ['?', ':', '-', '_']
            call s:metacode(a:mode, c)
        endfor
    else
        for c in [',', '.', '/', ';', '{', '}']
            call s:metacode(a:mode, c)
        endfor
        for c in ['?', ':', '-', '_']
            call s:metacode(a:mode, c)
        endfor
    endif
    set ttimeout
    if $TMUX != ''
        set ttimeoutlen=30
    elseif &ttimeoutlen > 80 || &ttimeoutlen <= 0
        set ttimeoutlen=80
    endif
endfunc

call Terminal_MetaMode(0)

然后你就可以正确在 Vim 映射 ALT 键了,具体原理见:help set-termcap以及:

http://www.skywind.me/blog/archives/1846

其他的诸如 emacs, nano 和 neovim 等都不需要额外设置。

终端里正确设置 BS 键

还是 VT100 的历史原因,BACKSPACE 键和 CTRL-H 给混淆起来了,默认情况下,终端里不管按 CTRL-H 还是 BACKSPACE 时都是发送 ASCII 码为 0x08 的^H过去。导致我们想在 Vim/Emacs 中映射 CTRL-H 去干别的事情时会影响到 BACKSPACE 键的使用。

因此得按照 VT220 的新标准修改一下 BACKSPACE 的设置,让它发送 ASCII 码 0x7f 即^?过去:

  • SecureCRT : Session Options -> Terminal -> Emulation -> Mapped Keys, 勾选 Backspace sends delete
  • XShell : Properties -> Terminal -> Keyboard 里,把<BS>设置成 127,而<DEL>设置成 VT220 Del
  • Putty : 好像默认是 ^? 的不过需要到:Configuration -> Terminal -> Keyboard 下面下确认下 The Backspace key 是 Control-? (127)
  • Terminal.app : 好像默认是发送^?的,你也可以到 Profiles Advanced 下面确认下 “Delete sends Control-H” 没有勾选。
  • iTerm2 : 默认也是发送 ^? 的,可以到 Profiles -> Keys下面确认一下 “Delete key sends ^H” 没有被勾选。
  • Gnome-Terminal : 默认发送 ^? 的,参见具体文本配置文件。
  • MinTTY : 设置 vt220/xterm 的话,默认发送 ^? 的,似乎还不能改。

你要深究原因的话,可以见:

http://www.skywind.me/blog/archives/1857

修改好以后可以继续运行:

showkey -a

检查一下,你的 BACKSPACE 键被按下时是否正确发送了 0x7f 字符过去。设置成功的话,终端下 CTRL-h 和 backspace 就不会出现混淆问题了。

]]>
0
<![CDATA[Node 定时器详解]]> http://www.udpwork.com/item/16667.html http://www.udpwork.com/item/16667.html#reviews Fri, 23 Feb 2018 08:43:45 +0800 阮一峰 http://www.udpwork.com/item/16667.html JavaScript 是单线程运行,异步操作特别重要。

只要用到引擎之外的功能,就需要跟外部交互,从而形成异步操作。由于异步操作实在太多,JavaScript 不得不提供很多异步语法。这就好比,有些人老是受打击, 他的抗打击能力必须变得很强,否则他就完蛋了。

Node 的异步语法比浏览器更复杂,因为它可以跟内核对话,不得不搞了一个专门的库libuv做这件事。这个库负责各种回调函数的执行时间,毕竟异步任务最后还是要回到主线程,一个个排队执行。

为了协调异步任务,Node 居然提供了四个定时器,让任务可以在指定的时间运行。

  • setTimeout()
  • setInterval()
  • setImmediate()
  • process.nextTick()

前两个是语言的标准,后两个是 Node 独有的。它们的写法差不多,作用也差不多,不太容易区别。

你能说出下面代码的运行结果吗?

// test.js
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));
(() => console.log(5))();

运行结果如下。

$ node test.js
5
3
4
1
2

如果你能一口说对,可能就不需要再看下去了。本文详细解释,Node 怎么处理各种定时器,或者更广义地说,libuv 库怎么安排异步任务在主线程上执行。

一、同步任务和异步任务

首先,同步任务总是比异步任务更早执行。

前面的那段代码,只有最后一行是同步任务,因此最早执行。

(() => console.log(5))();

二、本轮循环和次轮循环

异步任务可以分成两种。

  • 追加在本轮循环 的异步任务
  • 追加在次轮循环 的异步任务

所谓"循环",指的是事件循环(event loop)。这是 JavaScript 引擎处理异步任务的方式,后文会详细解释。这里只要理解,本轮循环一定早于次轮循环执行即可。

Node 规定,process.nextTick和Promise的回调函数,追加在本轮循环,即同步任务一旦执行完成,就开始执行它们。而setTimeout、setInterval、setImmediate的回调函数,追加在次轮循环。

这就是说,文首那段代码的第三行和第四行,一定比第一行和第二行更早执行。

// 下面两行,次轮循环执行
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
// 下面两行,本轮循环执行
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));

三、process.nextTick()

process.nextTick这个名字有点误导,它是在本轮循环执行的,而且是所有异步任务里面最快执行的。

Node 执行完所有同步任务,接下来就会执行process.nextTick的任务队列。所以,下面这行代码是第二个输出结果。

process.nextTick(() => console.log(3));

基本上,如果你希望异步任务尽可能快地执行,那就使用process.nextTick。

四、微任务

根据语言规格,Promise对象的回调函数,会进入异步任务里面的"微任务"(microtask)队列。

微任务队列追加在process.nextTick队列的后面,也属于本轮循环。所以,下面的代码总是先输出3,再输出4。

process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));
// 3
// 4

注意,只有前一个队列全部清空以后,才会执行下一个队列。

process.nextTick(() => console.log(1));
Promise.resolve().then(() => console.log(2));
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));
// 1
// 3
// 2
// 4

上面代码中,全部process.nextTick的回调函数,执行都会早于Promise的。

至此,本轮循环的执行顺序就讲完了。

  1. 同步任务
  2. process.nextTick()
  3. 微任务

五、事件循环的概念

下面开始介绍次轮循环的执行顺序,这就必须理解什么是事件循环(event loop)了。

Node 的官方文档是这样介绍的。

"When Node.js starts, it initializes the event loop, processes the provided input script which may make async API calls, schedule timers, or call process.nextTick(), then begins processing the event loop."

这段话很重要,需要仔细读。它表达了三层意思。

首先,有些人以为,除了主线程,还存在一个单独的事件循环线程。不是这样的,只有一个主线程,事件循环是在主线程上完成的。

其次,Node 开始执行脚本时,会先进行事件循环的初始化,但是这时事件循环还没有开始,会先完成下面的事情。

  • 同步任务
  • 发出异步请求
  • 规划定时器生效的时间
  • 执行process.nextTick()等等

最后,上面这些事情都干完了,事件循环就正式开始了。

六、事件循环的六个阶段

事件循环会无限次地执行,一轮又一轮。只有异步任务的回调函数队列清空了,才会停止执行。

每一轮的事件循环,分成六个阶段。这些阶段会依次执行。

  1. timers
  2. I/O callbacks
  3. idle, prepare
  4. poll
  5. check
  6. close callbacks

每个阶段都有一个先进先出的回调函数队列。只有一个阶段的回调函数队列清空了,该执行的回调函数都执行了,事件循环才会进入下一个阶段。

下面简单介绍一下每个阶段的含义,详细介绍可以看官方文档,也可以参考 libuv 的源码解读

(1)timers

这个是定时器阶段,处理setTimeout()和setInterval()的回调函数。进入这个阶段后,主线程会检查一下当前时间,是否满足定时器的条件。如果满足就执行回调函数,否则就离开这个阶段。

(2)I/O callbacks

除了以下操作的回调函数,其他的回调函数都在这个阶段执行。

  • setTimeout()和setInterval()的回调函数
  • setImmediate()的回调函数
  • 用于关闭请求的回调函数,比如socket.on('close', ...)

(3)idle, prepare

该阶段只供 libuv 内部调用,这里可以忽略。

(4)Poll

这个阶段是轮询时间,用于等待还未返回的 I/O 事件,比如服务器的回应、用户移动鼠标等等。

这个阶段的时间会比较长。如果没有其他异步任务要处理(比如到期的定时器),会一直停留在这个阶段,等待 I/O 请求返回结果。

(5)check

该阶段执行setImmediate()的回调函数。

(6)close callbacks

该阶段执行关闭请求的回调函数,比如socket.on('close', ...)。

七、事件循环的示例

下面是来自官方文档的一个示例。

const fs = require('fs');

const timeoutScheduled = Date.now();

// 异步任务一:100ms 后执行的定时器
setTimeout(() => {
  const delay = Date.now() - timeoutScheduled;
  console.log(`${delay}ms`);
}, 100);

// 异步任务二:至少需要 200ms 的文件读取
fs.readFile('test.js', () => {
  const startCallback = Date.now();
  while (Date.now() - startCallback 

上面代码有两个异步任务,一个是 100ms 后执行的定时器,一个是至少需要 200ms 的文件读取。请问运行结果是什么?

脚本进入第一轮事件循环以后,没有到期的定时器,也没有已经可以执行的 I/O 回调函数,所以会进入 Poll 阶段,等待内核返回文件读取的结果。由于读取小文件一般不会超过 100ms,所以在定时器到期之前,Poll 阶段就会得到结果,因此就会继续往下执行。

第二轮事件循环,依然没有到期的定时器,但是已经有了可以执行的 I/O 回调函数,所以会进入 I/O callbacks 阶段,执行fs.readFile的回调函数。这个回调函数需要 200ms,也就是说,在它执行到一半的时候,100ms 的定时器就会到期。但是,必须等到这个回调函数执行完,才会离开这个阶段。

第三轮事件循环,已经有了到期的定时器,所以会在 timers 阶段执行定时器。最后输出结果大概是200多毫秒。

八、setTimeout 和 setImmediate

由于setTimeout在 timers 阶段执行,而setImmediate在 check 阶段执行。所以,setTimeout会早于setImmediate完成。

setTimeout(() => console.log(1));
setImmediate(() => console.log(2));

上面代码应该先输出1,再输出2,但是实际执行的时候,结果却是不确定,有时还会先输出2,再输出1。

这是因为setTimeout的第二个参数默认为0。但是实际上,Node 做不到0毫秒,最少也需要1毫秒,根据官方文档,第二个参数的取值范围在1毫秒到2147483647毫秒之间。也就是说,setTimeout(f, 0)等同于setTimeout(f, 1)。

实际执行的时候,进入事件循环以后,有可能到了1毫秒,也可能还没到1毫秒,取决于系统当时的状况。如果没到1毫秒,那么 timers 阶段就会跳过,进入 check 阶段,先执行setImmediate的回调函数。

但是,下面的代码一定是先输出2,再输出1。

const fs = require('fs');

fs.readFile('test.js', () => {
  setTimeout(() => console.log(1));
  setImmediate(() => console.log(2));
});

上面代码会先进入 I/O callbacks 阶段,然后是 check 阶段,最后才是 timers 阶段。因此,setImmediate才会早于setTimeout执行。

九、参考链接

(完)

文档信息

]]>
JavaScript 是单线程运行,异步操作特别重要。

只要用到引擎之外的功能,就需要跟外部交互,从而形成异步操作。由于异步操作实在太多,JavaScript 不得不提供很多异步语法。这就好比,有些人老是受打击, 他的抗打击能力必须变得很强,否则他就完蛋了。

Node 的异步语法比浏览器更复杂,因为它可以跟内核对话,不得不搞了一个专门的库libuv做这件事。这个库负责各种回调函数的执行时间,毕竟异步任务最后还是要回到主线程,一个个排队执行。

为了协调异步任务,Node 居然提供了四个定时器,让任务可以在指定的时间运行。

  • setTimeout()
  • setInterval()
  • setImmediate()
  • process.nextTick()

前两个是语言的标准,后两个是 Node 独有的。它们的写法差不多,作用也差不多,不太容易区别。

你能说出下面代码的运行结果吗?

// test.js
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));
(() => console.log(5))();

运行结果如下。

$ node test.js
5
3
4
1
2

如果你能一口说对,可能就不需要再看下去了。本文详细解释,Node 怎么处理各种定时器,或者更广义地说,libuv 库怎么安排异步任务在主线程上执行。

一、同步任务和异步任务

首先,同步任务总是比异步任务更早执行。

前面的那段代码,只有最后一行是同步任务,因此最早执行。

(() => console.log(5))();

二、本轮循环和次轮循环

异步任务可以分成两种。

  • 追加在本轮循环 的异步任务
  • 追加在次轮循环 的异步任务

所谓"循环",指的是事件循环(event loop)。这是 JavaScript 引擎处理异步任务的方式,后文会详细解释。这里只要理解,本轮循环一定早于次轮循环执行即可。

Node 规定,process.nextTick和Promise的回调函数,追加在本轮循环,即同步任务一旦执行完成,就开始执行它们。而setTimeout、setInterval、setImmediate的回调函数,追加在次轮循环。

这就是说,文首那段代码的第三行和第四行,一定比第一行和第二行更早执行。

// 下面两行,次轮循环执行
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
// 下面两行,本轮循环执行
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));

三、process.nextTick()

process.nextTick这个名字有点误导,它是在本轮循环执行的,而且是所有异步任务里面最快执行的。

Node 执行完所有同步任务,接下来就会执行process.nextTick的任务队列。所以,下面这行代码是第二个输出结果。

process.nextTick(() => console.log(3));

基本上,如果你希望异步任务尽可能快地执行,那就使用process.nextTick。

四、微任务

根据语言规格,Promise对象的回调函数,会进入异步任务里面的"微任务"(microtask)队列。

微任务队列追加在process.nextTick队列的后面,也属于本轮循环。所以,下面的代码总是先输出3,再输出4。

process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));
// 3
// 4

注意,只有前一个队列全部清空以后,才会执行下一个队列。

process.nextTick(() => console.log(1));
Promise.resolve().then(() => console.log(2));
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));
// 1
// 3
// 2
// 4

上面代码中,全部process.nextTick的回调函数,执行都会早于Promise的。

至此,本轮循环的执行顺序就讲完了。

  1. 同步任务
  2. process.nextTick()
  3. 微任务

五、事件循环的概念

下面开始介绍次轮循环的执行顺序,这就必须理解什么是事件循环(event loop)了。

Node 的官方文档是这样介绍的。

"When Node.js starts, it initializes the event loop, processes the provided input script which may make async API calls, schedule timers, or call process.nextTick(), then begins processing the event loop."

这段话很重要,需要仔细读。它表达了三层意思。

首先,有些人以为,除了主线程,还存在一个单独的事件循环线程。不是这样的,只有一个主线程,事件循环是在主线程上完成的。

其次,Node 开始执行脚本时,会先进行事件循环的初始化,但是这时事件循环还没有开始,会先完成下面的事情。

  • 同步任务
  • 发出异步请求
  • 规划定时器生效的时间
  • 执行process.nextTick()等等

最后,上面这些事情都干完了,事件循环就正式开始了。

六、事件循环的六个阶段

事件循环会无限次地执行,一轮又一轮。只有异步任务的回调函数队列清空了,才会停止执行。

每一轮的事件循环,分成六个阶段。这些阶段会依次执行。

  1. timers
  2. I/O callbacks
  3. idle, prepare
  4. poll
  5. check
  6. close callbacks

每个阶段都有一个先进先出的回调函数队列。只有一个阶段的回调函数队列清空了,该执行的回调函数都执行了,事件循环才会进入下一个阶段。

下面简单介绍一下每个阶段的含义,详细介绍可以看官方文档,也可以参考 libuv 的源码解读

(1)timers

这个是定时器阶段,处理setTimeout()和setInterval()的回调函数。进入这个阶段后,主线程会检查一下当前时间,是否满足定时器的条件。如果满足就执行回调函数,否则就离开这个阶段。

(2)I/O callbacks

除了以下操作的回调函数,其他的回调函数都在这个阶段执行。

  • setTimeout()和setInterval()的回调函数
  • setImmediate()的回调函数
  • 用于关闭请求的回调函数,比如socket.on('close', ...)

(3)idle, prepare

该阶段只供 libuv 内部调用,这里可以忽略。

(4)Poll

这个阶段是轮询时间,用于等待还未返回的 I/O 事件,比如服务器的回应、用户移动鼠标等等。

这个阶段的时间会比较长。如果没有其他异步任务要处理(比如到期的定时器),会一直停留在这个阶段,等待 I/O 请求返回结果。

(5)check

该阶段执行setImmediate()的回调函数。

(6)close callbacks

该阶段执行关闭请求的回调函数,比如socket.on('close', ...)。

七、事件循环的示例

下面是来自官方文档的一个示例。

const fs = require('fs');

const timeoutScheduled = Date.now();

// 异步任务一:100ms 后执行的定时器
setTimeout(() => {
  const delay = Date.now() - timeoutScheduled;
  console.log(`${delay}ms`);
}, 100);

// 异步任务二:至少需要 200ms 的文件读取
fs.readFile('test.js', () => {
  const startCallback = Date.now();
  while (Date.now() - startCallback 

上面代码有两个异步任务,一个是 100ms 后执行的定时器,一个是至少需要 200ms 的文件读取。请问运行结果是什么?

脚本进入第一轮事件循环以后,没有到期的定时器,也没有已经可以执行的 I/O 回调函数,所以会进入 Poll 阶段,等待内核返回文件读取的结果。由于读取小文件一般不会超过 100ms,所以在定时器到期之前,Poll 阶段就会得到结果,因此就会继续往下执行。

第二轮事件循环,依然没有到期的定时器,但是已经有了可以执行的 I/O 回调函数,所以会进入 I/O callbacks 阶段,执行fs.readFile的回调函数。这个回调函数需要 200ms,也就是说,在它执行到一半的时候,100ms 的定时器就会到期。但是,必须等到这个回调函数执行完,才会离开这个阶段。

第三轮事件循环,已经有了到期的定时器,所以会在 timers 阶段执行定时器。最后输出结果大概是200多毫秒。

八、setTimeout 和 setImmediate

由于setTimeout在 timers 阶段执行,而setImmediate在 check 阶段执行。所以,setTimeout会早于setImmediate完成。

setTimeout(() => console.log(1));
setImmediate(() => console.log(2));

上面代码应该先输出1,再输出2,但是实际执行的时候,结果却是不确定,有时还会先输出2,再输出1。

这是因为setTimeout的第二个参数默认为0。但是实际上,Node 做不到0毫秒,最少也需要1毫秒,根据官方文档,第二个参数的取值范围在1毫秒到2147483647毫秒之间。也就是说,setTimeout(f, 0)等同于setTimeout(f, 1)。

实际执行的时候,进入事件循环以后,有可能到了1毫秒,也可能还没到1毫秒,取决于系统当时的状况。如果没到1毫秒,那么 timers 阶段就会跳过,进入 check 阶段,先执行setImmediate的回调函数。

但是,下面的代码一定是先输出2,再输出1。

const fs = require('fs');

fs.readFile('test.js', () => {
  setTimeout(() => console.log(1));
  setImmediate(() => console.log(2));
});

上面代码会先进入 I/O callbacks 阶段,然后是 check 阶段,最后才是 timers 阶段。因此,setImmediate才会早于setTimeout执行。

九、参考链接

(完)

文档信息

]]>
0
<![CDATA[张扣扣事件依然有点云山雾罩]]> http://www.udpwork.com/item/16666.html http://www.udpwork.com/item/16666.html#reviews Wed, 21 Feb 2018 20:33:16 +0800 魏武挥 http://www.udpwork.com/item/16666.html

 

张扣扣杀人案是这个春节震惊全国的事件之一。

 

一气杀了三人;

未动妇孺;

年幼时目睹母亲被杀,隐忍二十余年

等等

 

都是构成舆论关注的点。在上千年以孝治天下的中国,血亲复仇是很有群众基础的。

 

媒体多有报道。

 

谈谈我这个读者的感受。

 

 

杀人案,本身并不复杂。

 

张扣扣本人在短暂逃离后自首,对其所作所为,供认不讳。

 

比较让人疑惑或者好奇的地方是,张扣扣于03年退伍,一直到18年才进行报复,期间一十五年,为何迟迟不动手。

 

毕竟张扣扣今年动手,也不是什么设计精巧处心积虑。

 

这不是一出基督山伯爵复仇的戏码。

 

张扣扣成年之后,时间线基本如下:

 

01年12月,应征入伍

03年12月,退伍

04年1月,去广东打工

07年夏天回家乡造房子有停留,但不过几天。

2017年5月-8月,有去阿根廷打工经历。

之后,一直窝在家中。

 

2007年到17年这十年,比较含糊。我看到的媒体报道,澎湃上提到04-07在广东打工之后,“辗转至浙江杭州工作”。界面则提到他在杭州进过苏珀尔厂,做过保安,在济南卖过凉皮,被骗去搞过传销。09年时,与发小一起去驻马店学过挖掘机(这是一段被骗经历)。

 

总体来说,这十年,张扣扣一直在奔波中。

 

界面报道援引张扣扣发小所语,张扣扣在阿根廷打工时看到越南人报复杀人,受到刺激和启发,这算是一个说法。发小还说,王家兄弟聚首,可能也是张扣扣想一次性全解决。

 

 

重点是发生在1996年的血案。

 

当年,张母在口角中为王家所杀,是张扣扣此次杀人的重要动机——虽然事隔二十二年,不过张的行为,基本上可以确定为报复杀人。

 

96年这场血案,至今来看,还是有些云山雾罩,这个议题很重要:

 

到底是王家哪位动手直接导致张母死亡?是未满十八岁的王正军,还是另有其人?

 

另外一个议题也比较重要:

 

张母先对王家老二王富军吐了唾沫且吐到脸上,这个现在看来没有太大疑问,但随后发生的事按照当年判决书的说法:张母用一节扁铁在闻讯赶来的老三王正军左额部及左脸部各打一下,然后才是王正军用木棒回击,结果导致张母不幸身亡。—— 这个说法虽然得到张扣扣发小的认同,但目前张家并未承认。

 

当年血案的判决,到底有没有可能是个糊涂案?糊涂案的背后,王家是不是有所运作?

 

媒体进行了采访报道。

 

我的视野里,大白新闻(公开信息显示,这是新闻期刊《法律与生活》的出品,后者为司法部主管、法律出版社主办的央级媒体)刊发过一篇,主要是张扣扣姐姐的说法。界面(上海报业旗下媒体)也刊发过一篇,主要是张扣扣发小的说法。另外还有上海报业旗下澎湃的一篇,相对来说综合一点。

 

 

在大白新闻题为《张扣扣姐姐:杀我妈凶手的哥哥是本地乡长 行凶后由弟弟顶罪》的文章里,主要是张扣扣姐姐的单方面陈述。

 

标题已经几乎表明了这篇文章的一切。在张姐看来,当年那场血案,就是糊涂案。

 

首先是行凶者并非未满十八周岁的老三王正军,而是老二王富军。

 

其次是在争吵中,张母被打(原话是被四个人围着打),然后张父到达带走张母,结果王父说打死她,王富军持棍行凶。

 

再次是张姐称王家老大王校军是乡长,暗示王家在当地颇有势力。

 

最后,张姐还提到,张母丧事酒席,是王家操办,同村人吃人嘴短。暗示王家用一顿酒席封了大家的嘴。

 

张姐的说法有没有旁证呢?

 

不晓得。

 

但张姐提到王富军才是直接导致张母死亡的说法,给我产生了一个疑惑。因为张扣扣是报复杀人,这次所杀三人,干掉了王自新(王父)、老大王校军(被称为乡长的那个)、老三王正军(当年坐牢的那个),恰恰王富军逃过了一劫。

 

张扣扣为母报仇,虽然在他看来,王家一家男丁四人都有责任,但到底王富军才是正主。退一万步说,其他人都能放过,王富军怎么可以放过?

 

这个仇,报得不够讲究啊。

 

 

在界面题为《知情人:张扣扣作案后高喊“22年终于把仇报了”》的文章中,基本上则是张扣扣发小张小万(化名)的说法。张小万的说法,和张姐大相径庭。

 

首先张小万用多段文字表示,张母其实不是个善茬。她比较爱骂人,与周边邻居都吵过架。还会耍个赖,甚至会做出一些有碍观瞻的事:与一户人家打架后跑人家门口屋檐底下睡了一阵,还在那户人家院子里拉屎。

 

张母不是善茬这个说法在澎湃新闻里,有其他村民说法为佐证:为人强势,“嘴上不饶人”,“和村里很多人都吵过架”。个性不好。等等。

 

其次是张王二家关系本来不错,张父和王父还是干兄弟关系。但后来因为收猪生意,王父认为张父能力不行不带人玩了,产生了矛盾——这个说法澎湃新闻里的表述不同,张父称是因为送西瓜送了别人没送王家导致。

 

重点在张小万描述的当年血案的经过,很细节很白描。

 

1、张母去洗脚路上对老二王富军吐口水,但没有吐到王身上。王并没有什么反应。

2、张母洗脚回来,又吐口水,这次吐到王富军脸上,王动手打了张母一巴掌。然后双方扭打。并导致王父及老三王正军、张父及张姐参与。事件已演变为两家打成一团。

3、张姐给了张母一根一米长从家中取来的钢条,张母对王正军头上打了两下,打破了头。

4、张母似乎打赢了,张父拉着她准备往回走。

5、王正军从路边柴禾堆里操起胳膊粗的木棍——这点表示王是临时动手——一棒砸在张母太阳穴上。

6、张母并未立即死亡,还站了起来扶在树上干呕。考虑到她平时有耍赖行为,围观的熊孩子们还进行了“笑”的动作,认为张母又“装”。

7、张父扶住张母,并试图送进王家,但被后者拒绝。

8、各自去看医生,在医院中,医生宣布张母已经死亡。

 

根据张小万这段叙述,当年血案,法院的判决里“被害人汪秀萍对引发本案在起因上有一定过错责任”是能成立的。

 

但这段细节界面的新闻报道里并没有旁证,属于张小万一个人的说法。虽然当年血案,并不是只有张小万一个人看到。因为按照张小万所说,还有一堆熊孩子在围观嬉笑。

 

张小万还说,王家并非很有势力,也是普通家庭。老大王校军的确去乡政府部门工作,但也就是刚去的那种——具体在乡政府做什么职位,张小万没有提。至于张母给张扣扣留下“惨死”的印象,是因为验尸时不讲究,就在小孩面前把死者头颅锯开。

 

关于正主王富军躲过一劫,张小万的说法是,王富军因为相亲,在家里收拾房间,而不是值班没放假。

 

然后就是王家(王父似乎未去)上山烧纸祭祖,返回路上,并非抱团一起走,而是走得比较稀稀拉拉。

 

1、张扣扣先抹了老三脖子

2、追上走在前头的老大,一刀捅在侧腰,然后追捅数刀。直接捅死

3、老三被抹脖子后,并未立时气绝,张扣扣返回,背上补刀二十余刀。——张小万认为,这是张扣扣认定老三王正军是正主。

4、王父在家正在往袋子里装东西,张扣扣上前先一刀捅在脖子上,然后再捅到肚子上,断气。

5、张扣扣离开王家,后来又返回,烧掉了老大王校军的车。

6、张扣扣高喊二十二年,终于报仇。

7、张扣扣曾指着王母说:你是个女的,我今天不杀你。

 

也很细节很白描。

 

作为读者,我的疑惑是:

 

1、张小万对二十二年前的血案,记忆太深刻,细节白描得如此到位

2、张扣扣行凶过程,尤其是在山路上先抹老三脖子再去杀老大再回来补刀,张小万描述之细,给我一种就在旁边的感觉。他在界面报道里说,“村里不少人目击了扣扣杀人的经过”,难道也是援引别人的说法而自己并未亲眼所见?

3、老三被杀过程分为两段,先是被抹了脖子,然后张扣扣杀了老大后回来补刀。如果不少村民亲眼目睹,期间为何不对老三进行救助或阻止张扣扣补刀?

4、老二王富军躲过一劫:说是家里收拾房间,但张扣扣是在王家杀了王父,为何不杀王富军?还是说王富军这个在家里收拾房间的家,是另有其家?比如说,王富军自己的家。

 

但按照澎湃的报道,王家在城里有买房,如果王富军在“家”收拾房间指的是他这个城里的房子,那么,所谓张扣扣好不容易等他们四个男丁凑在一起企图一锅端的说法就有问题了。

 

 

最后看一下澎湃的报道。

 

与界面栩栩如生的写法不同,澎湃的报道非常硬,读起来的感觉肯定不如界面的。

 

澎湃文章的标题是:陕西南郑除夕凶案:一男子连杀邻家父子三人,两天后自首

 

今年这桩凶案的过程,澎湃描述的并不如界面的那般细节,只是通过几个村民之口,说张扣扣杀了三人(过程如何并未详述)还烧了车。

 

但澎湃提供了当年血案的一些增量信息。

 

首先是在当年的刑事附带民事状中,张父明确表示未满十八的老三王正军是凶手。但在今年又和张姐一起改口为王富军是正主,王家当年顶包。针对这个矛盾,张父称状子是别人代写的,这个代写人已经身故,自己当年为何这么表示也记不清了。

 

澎湃援引一名村民的说法说,当年王父曾向大家表示人是他打的。不过,村民也认为,可能是父亲想帮儿子顶事。

 

其次是,当年的判决下来后,张家是有过向中级法院递状子的行为的。但具体时间如何,张父又表示记不清了,且也没什么下文,感觉就是不了了之了。

 

澎湃点到即止,报道刊发时并未去汉中中院查阅。

 

多名村民向澎湃表示,张扣扣为人内向,这个说法在界面的张小万描述里,并非如此。张小万还说自己是外向人,怎么会和内向人做朋友。

 

澎湃报道里提及张扣扣一直有报仇想法,一位与其同年入伍并同团服役的人说,张扣扣在新兵训练时表示当兵就是为了锻炼好身体报仇。而界面里,张小万也称张扣扣和他说过要锻炼身体为了报仇之类的话。

 

 

关心96年血案的原因在于,如果当年判决无误,张扣扣这次三条人命,哪怕有自首情节,怕也是难逃一死。

 

如果当年判决是个糊涂案,张扣扣杀人还情有可原。

 

基层司法到底是不是对公民救济不足?底层群众是不是无处喊冤?

 

从目前看到的报道,我倾向于这样几个论点:

 

1、张母身故的正主,的确是老三王正军,当年判决,不太像是个糊涂案

2、张扣扣报复杀人,手段比较凶残

3、张扣扣常年奔波在外,混得并不如意,未有娶妻生子的主要原因是经济条件,并非什么要报仇不想拖累别人。在广东,曾有住在一起的相好,只是家里反对(可能女方离过婚的原因),未成正果

4、张家对当年血案真相究竟如何,至少张父,并非念兹在兹

5、张扣扣当兵时确想报仇,但真的有可能也就是想想或者说说,未必是天天在那里泣血磨刀要复仇的主。退伍后一直过了15年才真正发作。18年春节案发前,究竟发生了什么产生刺激到他这个念头付诸实施的tipping point,还不得而知。

6、当年两家打作一团,王家有王父、老二、老三,但老大并未在场。此番张扣扣报仇,老大也被杀死。张扣扣似乎认定,作为当时在政府部门工作的老大,对其母之死且未得以牙还牙以血还血,难辞其咎。

 

但也就是读完几篇报道后隐隐约约的倾向。

 

还是依然有些迷糊。

 

 

最后还有两个外围信息。

 

一个是有人发现,2017年11月,贴吧的南郑吧里有一篇题为“致红寺湖半年没发工资的职工们”的贴子,其中提到红寺湖领导叫王孝军。而张小万则提及王家老大王校军在出事前是红寺湖风景区管理处主任。

 

一个是关于张扣扣到底是个什么兵。

 

张小万称张扣扣并不是特种兵,也就是一个炮兵。我一位搞军事的朋友,通过张扣扣的退伍证,发现盖章是8663,而8663是武警驻新疆的快反部队,根据驻地的敏感性,张扣扣2003年去那里当兵,并不是什么很普通的兵。这位朋友还援引他另外一个特战旅服役朋友的话说,打迫击炮的,要提着底盘背着炮筒到处跑,意思是:身体贼棒,比他们猛。

 

澎湃报道里,曾有张扣扣战友提及,他在下雪的冬天穿着短袖骑摩托,还表示不冷。

哦,对,还有个细节。

 

张扣扣杀完人后,烧了老大王校军的车——张小万的说法是离开杀人现场王家,后又返回再烧车。到底怎么烧的,张小万属于一面之词,但结果肯定是烧了车。问题就来了:杀人就杀人,为啥要烧车呢?

—— 首发 扯氮集 ——

张扣扣事件依然有点云山雾罩,首发于扯氮集

]]>

 

张扣扣杀人案是这个春节震惊全国的事件之一。

 

一气杀了三人;

未动妇孺;

年幼时目睹母亲被杀,隐忍二十余年

等等

 

都是构成舆论关注的点。在上千年以孝治天下的中国,血亲复仇是很有群众基础的。

 

媒体多有报道。

 

谈谈我这个读者的感受。

 

 

杀人案,本身并不复杂。

 

张扣扣本人在短暂逃离后自首,对其所作所为,供认不讳。

 

比较让人疑惑或者好奇的地方是,张扣扣于03年退伍,一直到18年才进行报复,期间一十五年,为何迟迟不动手。

 

毕竟张扣扣今年动手,也不是什么设计精巧处心积虑。

 

这不是一出基督山伯爵复仇的戏码。

 

张扣扣成年之后,时间线基本如下:

 

01年12月,应征入伍

03年12月,退伍

04年1月,去广东打工

07年夏天回家乡造房子有停留,但不过几天。

2017年5月-8月,有去阿根廷打工经历。

之后,一直窝在家中。

 

2007年到17年这十年,比较含糊。我看到的媒体报道,澎湃上提到04-07在广东打工之后,“辗转至浙江杭州工作”。界面则提到他在杭州进过苏珀尔厂,做过保安,在济南卖过凉皮,被骗去搞过传销。09年时,与发小一起去驻马店学过挖掘机(这是一段被骗经历)。

 

总体来说,这十年,张扣扣一直在奔波中。

 

界面报道援引张扣扣发小所语,张扣扣在阿根廷打工时看到越南人报复杀人,受到刺激和启发,这算是一个说法。发小还说,王家兄弟聚首,可能也是张扣扣想一次性全解决。

 

 

重点是发生在1996年的血案。

 

当年,张母在口角中为王家所杀,是张扣扣此次杀人的重要动机——虽然事隔二十二年,不过张的行为,基本上可以确定为报复杀人。

 

96年这场血案,至今来看,还是有些云山雾罩,这个议题很重要:

 

到底是王家哪位动手直接导致张母死亡?是未满十八岁的王正军,还是另有其人?

 

另外一个议题也比较重要:

 

张母先对王家老二王富军吐了唾沫且吐到脸上,这个现在看来没有太大疑问,但随后发生的事按照当年判决书的说法:张母用一节扁铁在闻讯赶来的老三王正军左额部及左脸部各打一下,然后才是王正军用木棒回击,结果导致张母不幸身亡。—— 这个说法虽然得到张扣扣发小的认同,但目前张家并未承认。

 

当年血案的判决,到底有没有可能是个糊涂案?糊涂案的背后,王家是不是有所运作?

 

媒体进行了采访报道。

 

我的视野里,大白新闻(公开信息显示,这是新闻期刊《法律与生活》的出品,后者为司法部主管、法律出版社主办的央级媒体)刊发过一篇,主要是张扣扣姐姐的说法。界面(上海报业旗下媒体)也刊发过一篇,主要是张扣扣发小的说法。另外还有上海报业旗下澎湃的一篇,相对来说综合一点。

 

 

在大白新闻题为《张扣扣姐姐:杀我妈凶手的哥哥是本地乡长 行凶后由弟弟顶罪》的文章里,主要是张扣扣姐姐的单方面陈述。

 

标题已经几乎表明了这篇文章的一切。在张姐看来,当年那场血案,就是糊涂案。

 

首先是行凶者并非未满十八周岁的老三王正军,而是老二王富军。

 

其次是在争吵中,张母被打(原话是被四个人围着打),然后张父到达带走张母,结果王父说打死她,王富军持棍行凶。

 

再次是张姐称王家老大王校军是乡长,暗示王家在当地颇有势力。

 

最后,张姐还提到,张母丧事酒席,是王家操办,同村人吃人嘴短。暗示王家用一顿酒席封了大家的嘴。

 

张姐的说法有没有旁证呢?

 

不晓得。

 

但张姐提到王富军才是直接导致张母死亡的说法,给我产生了一个疑惑。因为张扣扣是报复杀人,这次所杀三人,干掉了王自新(王父)、老大王校军(被称为乡长的那个)、老三王正军(当年坐牢的那个),恰恰王富军逃过了一劫。

 

张扣扣为母报仇,虽然在他看来,王家一家男丁四人都有责任,但到底王富军才是正主。退一万步说,其他人都能放过,王富军怎么可以放过?

 

这个仇,报得不够讲究啊。

 

 

在界面题为《知情人:张扣扣作案后高喊“22年终于把仇报了”》的文章中,基本上则是张扣扣发小张小万(化名)的说法。张小万的说法,和张姐大相径庭。

 

首先张小万用多段文字表示,张母其实不是个善茬。她比较爱骂人,与周边邻居都吵过架。还会耍个赖,甚至会做出一些有碍观瞻的事:与一户人家打架后跑人家门口屋檐底下睡了一阵,还在那户人家院子里拉屎。

 

张母不是善茬这个说法在澎湃新闻里,有其他村民说法为佐证:为人强势,“嘴上不饶人”,“和村里很多人都吵过架”。个性不好。等等。

 

其次是张王二家关系本来不错,张父和王父还是干兄弟关系。但后来因为收猪生意,王父认为张父能力不行不带人玩了,产生了矛盾——这个说法澎湃新闻里的表述不同,张父称是因为送西瓜送了别人没送王家导致。

 

重点在张小万描述的当年血案的经过,很细节很白描。

 

1、张母去洗脚路上对老二王富军吐口水,但没有吐到王身上。王并没有什么反应。

2、张母洗脚回来,又吐口水,这次吐到王富军脸上,王动手打了张母一巴掌。然后双方扭打。并导致王父及老三王正军、张父及张姐参与。事件已演变为两家打成一团。

3、张姐给了张母一根一米长从家中取来的钢条,张母对王正军头上打了两下,打破了头。

4、张母似乎打赢了,张父拉着她准备往回走。

5、王正军从路边柴禾堆里操起胳膊粗的木棍——这点表示王是临时动手——一棒砸在张母太阳穴上。

6、张母并未立即死亡,还站了起来扶在树上干呕。考虑到她平时有耍赖行为,围观的熊孩子们还进行了“笑”的动作,认为张母又“装”。

7、张父扶住张母,并试图送进王家,但被后者拒绝。

8、各自去看医生,在医院中,医生宣布张母已经死亡。

 

根据张小万这段叙述,当年血案,法院的判决里“被害人汪秀萍对引发本案在起因上有一定过错责任”是能成立的。

 

但这段细节界面的新闻报道里并没有旁证,属于张小万一个人的说法。虽然当年血案,并不是只有张小万一个人看到。因为按照张小万所说,还有一堆熊孩子在围观嬉笑。

 

张小万还说,王家并非很有势力,也是普通家庭。老大王校军的确去乡政府部门工作,但也就是刚去的那种——具体在乡政府做什么职位,张小万没有提。至于张母给张扣扣留下“惨死”的印象,是因为验尸时不讲究,就在小孩面前把死者头颅锯开。

 

关于正主王富军躲过一劫,张小万的说法是,王富军因为相亲,在家里收拾房间,而不是值班没放假。

 

然后就是王家(王父似乎未去)上山烧纸祭祖,返回路上,并非抱团一起走,而是走得比较稀稀拉拉。

 

1、张扣扣先抹了老三脖子

2、追上走在前头的老大,一刀捅在侧腰,然后追捅数刀。直接捅死

3、老三被抹脖子后,并未立时气绝,张扣扣返回,背上补刀二十余刀。——张小万认为,这是张扣扣认定老三王正军是正主。

4、王父在家正在往袋子里装东西,张扣扣上前先一刀捅在脖子上,然后再捅到肚子上,断气。

5、张扣扣离开王家,后来又返回,烧掉了老大王校军的车。

6、张扣扣高喊二十二年,终于报仇。

7、张扣扣曾指着王母说:你是个女的,我今天不杀你。

 

也很细节很白描。

 

作为读者,我的疑惑是:

 

1、张小万对二十二年前的血案,记忆太深刻,细节白描得如此到位

2、张扣扣行凶过程,尤其是在山路上先抹老三脖子再去杀老大再回来补刀,张小万描述之细,给我一种就在旁边的感觉。他在界面报道里说,“村里不少人目击了扣扣杀人的经过”,难道也是援引别人的说法而自己并未亲眼所见?

3、老三被杀过程分为两段,先是被抹了脖子,然后张扣扣杀了老大后回来补刀。如果不少村民亲眼目睹,期间为何不对老三进行救助或阻止张扣扣补刀?

4、老二王富军躲过一劫:说是家里收拾房间,但张扣扣是在王家杀了王父,为何不杀王富军?还是说王富军这个在家里收拾房间的家,是另有其家?比如说,王富军自己的家。

 

但按照澎湃的报道,王家在城里有买房,如果王富军在“家”收拾房间指的是他这个城里的房子,那么,所谓张扣扣好不容易等他们四个男丁凑在一起企图一锅端的说法就有问题了。

 

 

最后看一下澎湃的报道。

 

与界面栩栩如生的写法不同,澎湃的报道非常硬,读起来的感觉肯定不如界面的。

 

澎湃文章的标题是:陕西南郑除夕凶案:一男子连杀邻家父子三人,两天后自首

 

今年这桩凶案的过程,澎湃描述的并不如界面的那般细节,只是通过几个村民之口,说张扣扣杀了三人(过程如何并未详述)还烧了车。

 

但澎湃提供了当年血案的一些增量信息。

 

首先是在当年的刑事附带民事状中,张父明确表示未满十八的老三王正军是凶手。但在今年又和张姐一起改口为王富军是正主,王家当年顶包。针对这个矛盾,张父称状子是别人代写的,这个代写人已经身故,自己当年为何这么表示也记不清了。

 

澎湃援引一名村民的说法说,当年王父曾向大家表示人是他打的。不过,村民也认为,可能是父亲想帮儿子顶事。

 

其次是,当年的判决下来后,张家是有过向中级法院递状子的行为的。但具体时间如何,张父又表示记不清了,且也没什么下文,感觉就是不了了之了。

 

澎湃点到即止,报道刊发时并未去汉中中院查阅。

 

多名村民向澎湃表示,张扣扣为人内向,这个说法在界面的张小万描述里,并非如此。张小万还说自己是外向人,怎么会和内向人做朋友。

 

澎湃报道里提及张扣扣一直有报仇想法,一位与其同年入伍并同团服役的人说,张扣扣在新兵训练时表示当兵就是为了锻炼好身体报仇。而界面里,张小万也称张扣扣和他说过要锻炼身体为了报仇之类的话。

 

 

关心96年血案的原因在于,如果当年判决无误,张扣扣这次三条人命,哪怕有自首情节,怕也是难逃一死。

 

如果当年判决是个糊涂案,张扣扣杀人还情有可原。

 

基层司法到底是不是对公民救济不足?底层群众是不是无处喊冤?

 

从目前看到的报道,我倾向于这样几个论点:

 

1、张母身故的正主,的确是老三王正军,当年判决,不太像是个糊涂案

2、张扣扣报复杀人,手段比较凶残

3、张扣扣常年奔波在外,混得并不如意,未有娶妻生子的主要原因是经济条件,并非什么要报仇不想拖累别人。在广东,曾有住在一起的相好,只是家里反对(可能女方离过婚的原因),未成正果

4、张家对当年血案真相究竟如何,至少张父,并非念兹在兹

5、张扣扣当兵时确想报仇,但真的有可能也就是想想或者说说,未必是天天在那里泣血磨刀要复仇的主。退伍后一直过了15年才真正发作。18年春节案发前,究竟发生了什么产生刺激到他这个念头付诸实施的tipping point,还不得而知。

6、当年两家打作一团,王家有王父、老二、老三,但老大并未在场。此番张扣扣报仇,老大也被杀死。张扣扣似乎认定,作为当时在政府部门工作的老大,对其母之死且未得以牙还牙以血还血,难辞其咎。

 

但也就是读完几篇报道后隐隐约约的倾向。

 

还是依然有些迷糊。

 

 

最后还有两个外围信息。

 

一个是有人发现,2017年11月,贴吧的南郑吧里有一篇题为“致红寺湖半年没发工资的职工们”的贴子,其中提到红寺湖领导叫王孝军。而张小万则提及王家老大王校军在出事前是红寺湖风景区管理处主任。

 

一个是关于张扣扣到底是个什么兵。

 

张小万称张扣扣并不是特种兵,也就是一个炮兵。我一位搞军事的朋友,通过张扣扣的退伍证,发现盖章是8663,而8663是武警驻新疆的快反部队,根据驻地的敏感性,张扣扣2003年去那里当兵,并不是什么很普通的兵。这位朋友还援引他另外一个特战旅服役朋友的话说,打迫击炮的,要提着底盘背着炮筒到处跑,意思是:身体贼棒,比他们猛。

 

澎湃报道里,曾有张扣扣战友提及,他在下雪的冬天穿着短袖骑摩托,还表示不冷。

哦,对,还有个细节。

 

张扣扣杀完人后,烧了老大王校军的车——张小万的说法是离开杀人现场王家,后又返回再烧车。到底怎么烧的,张小万属于一面之词,但结果肯定是烧了车。问题就来了:杀人就杀人,为啥要烧车呢?

—— 首发 扯氮集 ——

张扣扣事件依然有点云山雾罩,首发于扯氮集

]]>
0
<![CDATA[2018新年目标]]> http://www.udpwork.com/item/16665.html http://www.udpwork.com/item/16665.html#reviews Tue, 20 Feb 2018 11:53:59 +0800 Guancheng (G.C.) http://www.udpwork.com/item/16665.html 2018年,3个目标:

  • 夯实基础,继续打造细分领域业界第一的产品和服务
  • 带家人去旅行
  • 减重20斤

2018年,3个不变:

  • 保持持续学习的习惯:实践、读书、求教
  • 保持对世界的好奇心和饥渴感:拥抱新事物,新观点,新变化
  • 保持乐观的心态:痛苦,就是走上坡路的感受!
]]>
2018年,3个目标:

  • 夯实基础,继续打造细分领域业界第一的产品和服务
  • 带家人去旅行
  • 减重20斤

2018年,3个不变:

  • 保持持续学习的习惯:实践、读书、求教
  • 保持对世界的好奇心和饥渴感:拥抱新事物,新观点,新变化
  • 保持乐观的心态:痛苦,就是走上坡路的感受!
]]>
0
<![CDATA[开发环境上的代码同步]]> http://www.udpwork.com/item/16664.html http://www.udpwork.com/item/16664.html#reviews Thu, 15 Feb 2018 13:33:40 +0800 四火 http://www.udpwork.com/item/16664.html 最近在搭建开发环境,大致的布局是这样的:一个专门的数据库VM,一个用于编译和代码执行的VM(dev virt,装的RedHat),还有用来写代码和运行这两个虚拟环境的Mac(local)。这里我需要一个工具,可以满这样的需求:

  • 能够把Mac上写的代码同步到dev virt上去。
  • 不需要手动触发,每当有修改,应该能够自动同步。

我把我的解决办法简单记录在这里。在接下去记录之前,需要回答这样两个问题:

  • 为什么需要把编译和执行环境放到VM里面去?因为尽量使得代码的编译执行环境接近于生产线。
  • 为什么要在Mac上写代码,而不在dev virt那个VM上写代码?因为在Mac上使用第三方的工具,做一些操作系统上面的改变,编码环境的改变都比较方便,而且虚拟机中写代码有时候明显感到IDE不流畅。

下面一步一步来解决这个问题。

第一步,配置VM在NAT下的端口映射,允许从Mac上可以SSH(默认是22号端口)到dev virt上:

开发环境上的代码同步

为什么上面选择了2222号端口,主要是考虑避免和常规的SSH冲突。这样配置以后,连接localhost的2222端口,就可以映射到VM上的22号端口去了。

第二步,创建SSH keys。Mac上运行ssh-keygen,创建公钥和私钥。把公钥从~/.ssh/id_rsa.pub拷贝到dev virt,放在~/.ssh下面,并重命名成authorized_keys。注意.ssh权限必须是700,而authorized_keys必须是600。

第三步,配置dev virt上面的/etc/ssh/sshd_config,具体参数根据情况调整,完成以后需要重启SSH服务:service sshd restart。

第四步,尝试连接,在Mac上执行SSH命令,比如ssh ray@127.0.0.1 -p 2222,如果不能访问,考虑修改/etc/ssh/sshd_config,把日志改成verbose:LogLevel VERBOSE,再重启SSH服务,这样就可以通过tail -f /var/log/secure查看无法连接的错误信息。

第五步,创建一个同步脚本,比如叫做rsync.sh,里面就只有一行rsync的命令,比如:rsync -avz -e “ssh -p 2222″ ~/Projects ray@127.0.0.1:~,其中的~/Projects是Mac上的代码环境,要同步到dev virt的~上去。

第六步,安装fswatch,它可以监视文件夹下面的变动。brew install fswatch。

第六步,把fswatch和rsync串起来,比如:fswatch -v -0 ~/Projects/ | xargs -0 -n1 ~/rsync.sh,第一次执行比较慢,花了几分钟。但之后有修改的时候,因为是增量同步,几秒钟就自动同步过去了。rsync因为支持压缩,所以性能还不错。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接《四火的唠叨》

分享到:
]]>
最近在搭建开发环境,大致的布局是这样的:一个专门的数据库VM,一个用于编译和代码执行的VM(dev virt,装的RedHat),还有用来写代码和运行这两个虚拟环境的Mac(local)。这里我需要一个工具,可以满这样的需求:

  • 能够把Mac上写的代码同步到dev virt上去。
  • 不需要手动触发,每当有修改,应该能够自动同步。

我把我的解决办法简单记录在这里。在接下去记录之前,需要回答这样两个问题:

  • 为什么需要把编译和执行环境放到VM里面去?因为尽量使得代码的编译执行环境接近于生产线。
  • 为什么要在Mac上写代码,而不在dev virt那个VM上写代码?因为在Mac上使用第三方的工具,做一些操作系统上面的改变,编码环境的改变都比较方便,而且虚拟机中写代码有时候明显感到IDE不流畅。

下面一步一步来解决这个问题。

第一步,配置VM在NAT下的端口映射,允许从Mac上可以SSH(默认是22号端口)到dev virt上:

开发环境上的代码同步

为什么上面选择了2222号端口,主要是考虑避免和常规的SSH冲突。这样配置以后,连接localhost的2222端口,就可以映射到VM上的22号端口去了。

第二步,创建SSH keys。Mac上运行ssh-keygen,创建公钥和私钥。把公钥从~/.ssh/id_rsa.pub拷贝到dev virt,放在~/.ssh下面,并重命名成authorized_keys。注意.ssh权限必须是700,而authorized_keys必须是600。

第三步,配置dev virt上面的/etc/ssh/sshd_config,具体参数根据情况调整,完成以后需要重启SSH服务:service sshd restart。

第四步,尝试连接,在Mac上执行SSH命令,比如ssh ray@127.0.0.1 -p 2222,如果不能访问,考虑修改/etc/ssh/sshd_config,把日志改成verbose:LogLevel VERBOSE,再重启SSH服务,这样就可以通过tail -f /var/log/secure查看无法连接的错误信息。

第五步,创建一个同步脚本,比如叫做rsync.sh,里面就只有一行rsync的命令,比如:rsync -avz -e “ssh -p 2222″ ~/Projects ray@127.0.0.1:~,其中的~/Projects是Mac上的代码环境,要同步到dev virt的~上去。

第六步,安装fswatch,它可以监视文件夹下面的变动。brew install fswatch。

第六步,把fswatch和rsync串起来,比如:fswatch -v -0 ~/Projects/ | xargs -0 -n1 ~/rsync.sh,第一次执行比较慢,花了几分钟。但之后有修改的时候,因为是增量同步,几秒钟就自动同步过去了。rsync因为支持压缩,所以性能还不错。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接《四火的唠叨》

分享到:
]]>
0
<![CDATA[狼人杀记录器]]> http://www.udpwork.com/item/16663.html http://www.udpwork.com/item/16663.html#reviews Tue, 13 Feb 2018 17:39:09 +0800 s5s5 http://www.udpwork.com/item/16663.html 狼人杀记录器

这两天抽空把一直想做的狼人杀记录器做好了,欢迎大家使用并反馈问题~

URL:http://lang.misuisui.com

祝大家新年快乐~

扫码关注米随随

]]>
狼人杀记录器

这两天抽空把一直想做的狼人杀记录器做好了,欢迎大家使用并反馈问题~

URL:http://lang.misuisui.com

祝大家新年快乐~

扫码关注米随随

]]>
0
<![CDATA[10秒钟,让你的方法变为RPC服务]]> http://www.udpwork.com/item/16662.html http://www.udpwork.com/item/16662.html#reviews Tue, 13 Feb 2018 17:16:37 +0800 鸟窝 http://www.udpwork.com/item/16662.html rpcx一个服务治理的Go RPC框架, 拥有非常多的特性,支持跨语言的服务调用。 众多的特性可以参考doc.rpcx.site。它的服务治理的特性深受阿里巴巴的Dubbo框架的启发。

在实际的产品应用中,用户使用两台服务器+8台日志搜集服务(Client),轻松处理每天几十亿的服务调用, 除了中间一个路由器硬件闪断, 整个系统平稳运行多半年。 相比较之前Java的实现, 服务器节省了一半。 用户使用rpcx框架重构后的系统每月为公司节省了几十万元的成本。

rpcx框架的一个设计哲学就是简单 。不希望用户需要花费大量的时间在框架的学习上,并且不需要proto文件或者重复且冗余的服务配置。最少只需要10行代码就可以创建一个服务, 如果需要额外的配置,也只需要几十行的代码。

虽然rpcx开发简单,但是作为开发人员来说,如果可以更加的偷懒, 那更是极好的一件事情了,这就是xgen开发的目的。

这个工具可以搜寻指定的package下可以配置成rpcx服务的类型, 并且生成一个服务器程序,将这些服务注册到服务器程序中。你可以指定是否需要zookeeper、etcd、consul作为注册中心。

这个工具的开发参考了Go的tools的实现以及DigitalOcean公司的Fatih Arslan
开发的gomodifytags的实现。

首先看一下这个工具参数:

12345678910
$ xgen -hUsage of xgen:  -o string    	specify the filename of the output  -pkg    	process the whole package instead of just the given file  -r string    	registry type. support etcd, consul, zookeeper, mdns  -tags string    	build tags to add to generated file

你可以使用xgen file1.go file2.go file3.go搜寻指定的文件生成服务,也可以xgen -pkg github.com/rpcx-ecosystem/rpcx-examples3/xgen为GOPATH中指定的package生成服务。-pkg选项优先于程序参数。

-o选项指定生成的程序输出到哪个文件,如果不指定,则输出到控制台Stdout。

-r选项指定注册中心的类型,支持zookeeper、etcd、consul和mdns。如果不指定,则采用点对点的rpc调用方式。

-tags选项指定生成的文件是否要加上build conditions。

看一个例子,rpcx-examples3/xgen中有一个server.go文件,它定义几个类型和方法。

1234567891011121314151617181920212223242526272829303132333435
package xgenimport (	"context"	"fmt"	"time"	example "github.com/rpcx-ecosystem/rpcx-examples3")type Arith intfunc (t *Arith) Mul(ctx context.Context, args example.Args, reply *example.Reply) error {	reply.C = args.A * args.B	return nil}func (t *Arith) Add(ctx context.Context, args *example.Args, reply *example.Reply) error {	reply.C = args.A + args.B	return nil}type Echo stringfunc (s *Echo) Echo(ctx context.Context, args string, reply *string) error {	*reply = fmt.Sprintf("Hello %s from %s", args, *reply)	return nil}type TimeS struct{}func (s *TimeS) Time(ctx context.Context, args time.Time, reply *time.Time) error {	*reply = time.Now()	return nil}

这三个类型Arith、Echo、TimeS都有符合rpcx服务的方法。

rpcx的服务的方法需要满足下面的规则:

  • 类型和参数都是exported
  • 方法有三个参数,并且第一个参数是context.Context
  • 方法的第三个参数是指针类型
  • 方法类型为error

现在你就可以使用xgen生成服务端代码。

1
xgen -o cmd/main.go -r "etcd" -pkg github.com/rpcx-ecosystem/rpcx-examples3/xgen

或者

1
xgen -o cmd/main.go -r "etcd" ./server.go

这样就生成了一个服务器端的代码。

你可以运行你的服务器了:go run -tags "etcd" cmd/main.go,就这么简单。

]]>
rpcx一个服务治理的Go RPC框架, 拥有非常多的特性,支持跨语言的服务调用。 众多的特性可以参考doc.rpcx.site。它的服务治理的特性深受阿里巴巴的Dubbo框架的启发。

在实际的产品应用中,用户使用两台服务器+8台日志搜集服务(Client),轻松处理每天几十亿的服务调用, 除了中间一个路由器硬件闪断, 整个系统平稳运行多半年。 相比较之前Java的实现, 服务器节省了一半。 用户使用rpcx框架重构后的系统每月为公司节省了几十万元的成本。

rpcx框架的一个设计哲学就是简单 。不希望用户需要花费大量的时间在框架的学习上,并且不需要proto文件或者重复且冗余的服务配置。最少只需要10行代码就可以创建一个服务, 如果需要额外的配置,也只需要几十行的代码。

虽然rpcx开发简单,但是作为开发人员来说,如果可以更加的偷懒, 那更是极好的一件事情了,这就是xgen开发的目的。

这个工具可以搜寻指定的package下可以配置成rpcx服务的类型, 并且生成一个服务器程序,将这些服务注册到服务器程序中。你可以指定是否需要zookeeper、etcd、consul作为注册中心。

这个工具的开发参考了Go的tools的实现以及DigitalOcean公司的Fatih Arslan
开发的gomodifytags的实现。

首先看一下这个工具参数:

12345678910
$ xgen -hUsage of xgen:  -o string    	specify the filename of the output  -pkg    	process the whole package instead of just the given file  -r string    	registry type. support etcd, consul, zookeeper, mdns  -tags string    	build tags to add to generated file

你可以使用xgen file1.go file2.go file3.go搜寻指定的文件生成服务,也可以xgen -pkg github.com/rpcx-ecosystem/rpcx-examples3/xgen为GOPATH中指定的package生成服务。-pkg选项优先于程序参数。

-o选项指定生成的程序输出到哪个文件,如果不指定,则输出到控制台Stdout。

-r选项指定注册中心的类型,支持zookeeper、etcd、consul和mdns。如果不指定,则采用点对点的rpc调用方式。

-tags选项指定生成的文件是否要加上build conditions。

看一个例子,rpcx-examples3/xgen中有一个server.go文件,它定义几个类型和方法。

1234567891011121314151617181920212223242526272829303132333435
package xgenimport (	"context"	"fmt"	"time"	example "github.com/rpcx-ecosystem/rpcx-examples3")type Arith intfunc (t *Arith) Mul(ctx context.Context, args example.Args, reply *example.Reply) error {	reply.C = args.A * args.B	return nil}func (t *Arith) Add(ctx context.Context, args *example.Args, reply *example.Reply) error {	reply.C = args.A + args.B	return nil}type Echo stringfunc (s *Echo) Echo(ctx context.Context, args string, reply *string) error {	*reply = fmt.Sprintf("Hello %s from %s", args, *reply)	return nil}type TimeS struct{}func (s *TimeS) Time(ctx context.Context, args time.Time, reply *time.Time) error {	*reply = time.Now()	return nil}

这三个类型Arith、Echo、TimeS都有符合rpcx服务的方法。

rpcx的服务的方法需要满足下面的规则:

  • 类型和参数都是exported
  • 方法有三个参数,并且第一个参数是context.Context
  • 方法的第三个参数是指针类型
  • 方法类型为error

现在你就可以使用xgen生成服务端代码。

1
xgen -o cmd/main.go -r "etcd" -pkg github.com/rpcx-ecosystem/rpcx-examples3/xgen

或者

1
xgen -o cmd/main.go -r "etcd" ./server.go

这样就生成了一个服务器端的代码。

你可以运行你的服务器了:go run -tags "etcd" cmd/main.go,就这么简单。

]]>
0
<![CDATA[再见,亚马逊时光]]> http://www.udpwork.com/item/16661.html http://www.udpwork.com/item/16661.html#reviews Tue, 13 Feb 2018 13:57:54 +0800 四火 http://www.udpwork.com/item/16661.html 再见,亚马逊时光新入职Oracle已经超过一周了,但是一直没敢下笔,写一点东西纪念将近6年的亚马逊时光,总有惶恐的感觉。现在觉得不能再拖了,文字不在多寡,仿佛一种仪式,把整个亚马逊的经历画上句号。离开老东家的时候,往往是喜忧参半的,并且难免对前任颇有微词。在我离开华为的时候,便是如此,多为感怀和想念,但是诚实地说,也有一些厌烦的情绪,于是有释放之后的舒坦。这其中的缘由,我在以前的文中写到过。但是离开亚马逊,我却仿佛不再有这些负面的情绪,除了感伤和怀念,便是感激。要说明的是,如今亚马逊的股票直往上蹿,它却远非完美,也有诸多令人遗憾的风言风语。我觉得它在某些方面可以被称为“美国的华为”,做企业的成就自不必说,但是总有许多声音在抱怨对员工施仁不够。我相信这些声音大部分都是真实的,都是客观的,但是公司太大,团队独立性太强,就我的经历而言,并没有体味到这些问题。

时光回到六年前,在我对互联网的概念还迷迷糊糊的时候,在云计算之类概念才兴起不久的时候,我仍然享受着和同事开心的工作氛围,但实在决定受够了在华为的做事风格,于是下决心离开,也想离开传统电信行业,到别的行业去过过瘾,探探险。决定做得有点唐突和冒失,而且早早告诉了我的团队经理,看起来是一个一时冲动的想法而已,不过迷迷糊糊就把那些陆陆续续的挽留给拒绝了。现在我依然觉得,其实当时根本没有把问题想清楚,有时候做起决定来真是没头没脑得吓人。几个月时间里,借着周末和请假的空隙,从南京,到上海、杭州,再到北京,面试了好几家企业,我把这些和不同的公司接触的经历深深地印在脑海里,在权衡几份机会的时候,陈皓凭借他描述的做事的氛围和方式,或者说奇特的工程师文化打动了我,我决定从南京远上北京,加入他的团队,加入亚马逊。老实说,在当时我对亚马逊知之甚少,可能不比“卖书的”这三个字多多少,并不能算非常理智的决定,但是回过头来看,无疑是幸运和正确的选择,为此我也很感谢他和我的这些对话。2012年的2月份,正式加入亚马逊,GFBA项目组,开始和全球开店和商品配送打交道。

从一家民营电信巨头,跑到互联网外企,实在是一个巨大的改变,其中的“鸿沟”,对当时的我来说,确实有点艰难。我记得第一个月的时候,我和老板说,我觉得大家每一个人都比我强。不过,相较于看起来似乎更“平易近人”的技术,英文才是我最大的问题。读那些英文文档几乎是每天上班最痛苦的事情,不只是心里的痛苦,而且还胃痛,是真的胃痛,此处没有修辞手法。看到英文就胃痛,这也是蛮有趣的。我记得当时坐在边上的同事,用半问半嘲讽的语气跟我说,“你英文不适应怎么行啊”。我没有回答,觉得有些难受,似乎觉得不公平,但又没什么机会反驳。其实我自己并不确定,而且读书的时候英文就不好,也就谈不上什么信心。

接下来,你可能会以为我要灌鸡汤,写发愤图强的事情了。错了,我既没有发奋,也没有拿那什么“涂墙”。住在公司附近,交通上时间开销很小。每天下班以后,觉得时间很多,想起在华为动辄9、10点钟回家的生活,忽然觉得很幸福。有时候看看其它书,有时候就干脆打暗黑III,总之就是坚决不看在上班的时候已经苦不堪言的那堆似乎看不完的英文材料。好在老板不给我很大压力,工作时间比较自由,我们学习各种互联网技术,或者亚马逊的技术,然后在团队里分享。工作效率不能说有多高,但是可以在一个相对轻松的环境里面接触这些以前从没见到过的技术和方法,特别是有时候可以琢磨琢磨那些在外企更有经验的程序员的态度和工作方法,实在是令我收获颇丰。

在度过了漫长的适应期以后,组织上变化的关系,GFBA这个组要解散,面前有一些别的组可以选择。于是加入了Demand Forecasting,和商品销量的预测打交道。相对而言,这个组做的事情似乎会更有趣一点。接着的时间我得以有机会去接触一些机器学习的内容,也就是从那个时候开始,我逐渐觉得,学历在这一行的很多方面可能和能力并不挂钩,但是在某些领域却不是,例如机器学习的内容,人工智能的内容,数学基础好的,高校里面研究过这方面的,就是有更专业的方法。而我这样的很多东西都是半路出家自己学习的,既称不上系统,又难说深入,在这些方面的差距很难弥补。随着年岁的增长,不再心比天高,越来越能够正视自己的不足和局限,也越来越能够理解自己在哪些方面更容易遇到天花板,自然也更清楚自己什么部分更有优势。

我觉得当时我们团队在Forecasting这个新领域发展得非常不错。在2013年下半年的时候,在promote之后,我开始不安和躁动起来,开始酝酿野心,想尝试一些别的体验。我曾经说过,我生活过的每一个地方都像是一个奇妙的盒子,我会在未来“寻找其它颜色和风格的盒子”。而下一个盒子,就可能是硅谷,或者西雅图,来到更远处的一个“软件之乡”。在我开始尝试寻找和接洽在美国的团队的时候,正好,有一个机会可以来到西雅图,而且保持这个组不变。我就和我老婆商量,一切都是未知数,心里也发毛,这似乎是一个看起来挺不错的机会,不换组的话工作上的变动可以尽量小一点,压力就会小一点,我们应该抓住。于是我们两人就开始准备,专门正儿八经地学习英文,这似乎是在读书之后都没有的经历了。

2014年5月的时候,我们搬到了西雅图。接下去的时间,最初的三个月内,遇到的困难实在不少,我老婆在生活上给了很大的帮助,于是过渡这个既新鲜又痛苦的过程变得相对顺利。无论如何,能够在世界上最大的两个软件国度拥有居住和工作的体验,作为工程师来说,真是宝贵的经历。

在L签证的前提下,没有跳槽的可能,可是由于组内人事变动,躁动的心又开始作祟,想去一个更加“国际化”的环境工作,也想把自己在数据处理方面的短板补一补,于是考虑换组。和一些经理谈过之后,面了5个觉得感兴趣的组,2015年11月,加入了Contribution Profit这个组,计算成本和利润。从做的事情上看,算是真真正正天天都得和名副其实的“大数据”打交道了。很高兴学到不少有趣的东西,Scala,Spark,distributed workflow等等。

2016年算是因为各种原因,老实了一年,当然,拿到了H签证,这意味着跳槽成为了可能。

2017年下半年,面了一堆公司,最终决定加入Oracle,而整个冗长的手续,一直拖到2018年初才办好。一周前离开的时候,回想起在亚马逊的时光,恍如昨日。我在亚马逊学到了各种各样的技术,但是这些并不是我最引以为傲的。我见识到了大型互联网企业是怎样运作的,在云计算的巨头工作“是怎样一种体验”(知乎体)。工作上我认识了一群伙伴,来自世界各地,五花八门的风格,其中不少还有挺不错的私交。我学到了一个工程师应该具备怎样的严谨,怎样争论问题,怎样铺陈事实,怎样做trade-off……美好的体验太多,6年了,仿佛只是惊鸿一瞥的时间。

贴一张在本来发在朋友圈的,我写给给同事们留念的贺卡。并且非常负责任地说,所有的头像都丑爆了……

再见,亚马逊时光

还有这张用了6年的已经快要磨烂的工牌:

再见,亚马逊时光

就这样吧。谨以此文,纪念无限回味的亚马逊时光。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接《四火的唠叨》

分享到:
]]>
再见,亚马逊时光新入职Oracle已经超过一周了,但是一直没敢下笔,写一点东西纪念将近6年的亚马逊时光,总有惶恐的感觉。现在觉得不能再拖了,文字不在多寡,仿佛一种仪式,把整个亚马逊的经历画上句号。离开老东家的时候,往往是喜忧参半的,并且难免对前任颇有微词。在我离开华为的时候,便是如此,多为感怀和想念,但是诚实地说,也有一些厌烦的情绪,于是有释放之后的舒坦。这其中的缘由,我在以前的文中写到过。但是离开亚马逊,我却仿佛不再有这些负面的情绪,除了感伤和怀念,便是感激。要说明的是,如今亚马逊的股票直往上蹿,它却远非完美,也有诸多令人遗憾的风言风语。我觉得它在某些方面可以被称为“美国的华为”,做企业的成就自不必说,但是总有许多声音在抱怨对员工施仁不够。我相信这些声音大部分都是真实的,都是客观的,但是公司太大,团队独立性太强,就我的经历而言,并没有体味到这些问题。

时光回到六年前,在我对互联网的概念还迷迷糊糊的时候,在云计算之类概念才兴起不久的时候,我仍然享受着和同事开心的工作氛围,但实在决定受够了在华为的做事风格,于是下决心离开,也想离开传统电信行业,到别的行业去过过瘾,探探险。决定做得有点唐突和冒失,而且早早告诉了我的团队经理,看起来是一个一时冲动的想法而已,不过迷迷糊糊就把那些陆陆续续的挽留给拒绝了。现在我依然觉得,其实当时根本没有把问题想清楚,有时候做起决定来真是没头没脑得吓人。几个月时间里,借着周末和请假的空隙,从南京,到上海、杭州,再到北京,面试了好几家企业,我把这些和不同的公司接触的经历深深地印在脑海里,在权衡几份机会的时候,陈皓凭借他描述的做事的氛围和方式,或者说奇特的工程师文化打动了我,我决定从南京远上北京,加入他的团队,加入亚马逊。老实说,在当时我对亚马逊知之甚少,可能不比“卖书的”这三个字多多少,并不能算非常理智的决定,但是回过头来看,无疑是幸运和正确的选择,为此我也很感谢他和我的这些对话。2012年的2月份,正式加入亚马逊,GFBA项目组,开始和全球开店和商品配送打交道。

从一家民营电信巨头,跑到互联网外企,实在是一个巨大的改变,其中的“鸿沟”,对当时的我来说,确实有点艰难。我记得第一个月的时候,我和老板说,我觉得大家每一个人都比我强。不过,相较于看起来似乎更“平易近人”的技术,英文才是我最大的问题。读那些英文文档几乎是每天上班最痛苦的事情,不只是心里的痛苦,而且还胃痛,是真的胃痛,此处没有修辞手法。看到英文就胃痛,这也是蛮有趣的。我记得当时坐在边上的同事,用半问半嘲讽的语气跟我说,“你英文不适应怎么行啊”。我没有回答,觉得有些难受,似乎觉得不公平,但又没什么机会反驳。其实我自己并不确定,而且读书的时候英文就不好,也就谈不上什么信心。

接下来,你可能会以为我要灌鸡汤,写发愤图强的事情了。错了,我既没有发奋,也没有拿那什么“涂墙”。住在公司附近,交通上时间开销很小。每天下班以后,觉得时间很多,想起在华为动辄9、10点钟回家的生活,忽然觉得很幸福。有时候看看其它书,有时候就干脆打暗黑III,总之就是坚决不看在上班的时候已经苦不堪言的那堆似乎看不完的英文材料。好在老板不给我很大压力,工作时间比较自由,我们学习各种互联网技术,或者亚马逊的技术,然后在团队里分享。工作效率不能说有多高,但是可以在一个相对轻松的环境里面接触这些以前从没见到过的技术和方法,特别是有时候可以琢磨琢磨那些在外企更有经验的程序员的态度和工作方法,实在是令我收获颇丰。

在度过了漫长的适应期以后,组织上变化的关系,GFBA这个组要解散,面前有一些别的组可以选择。于是加入了Demand Forecasting,和商品销量的预测打交道。相对而言,这个组做的事情似乎会更有趣一点。接着的时间我得以有机会去接触一些机器学习的内容,也就是从那个时候开始,我逐渐觉得,学历在这一行的很多方面可能和能力并不挂钩,但是在某些领域却不是,例如机器学习的内容,人工智能的内容,数学基础好的,高校里面研究过这方面的,就是有更专业的方法。而我这样的很多东西都是半路出家自己学习的,既称不上系统,又难说深入,在这些方面的差距很难弥补。随着年岁的增长,不再心比天高,越来越能够正视自己的不足和局限,也越来越能够理解自己在哪些方面更容易遇到天花板,自然也更清楚自己什么部分更有优势。

我觉得当时我们团队在Forecasting这个新领域发展得非常不错。在2013年下半年的时候,在promote之后,我开始不安和躁动起来,开始酝酿野心,想尝试一些别的体验。我曾经说过,我生活过的每一个地方都像是一个奇妙的盒子,我会在未来“寻找其它颜色和风格的盒子”。而下一个盒子,就可能是硅谷,或者西雅图,来到更远处的一个“软件之乡”。在我开始尝试寻找和接洽在美国的团队的时候,正好,有一个机会可以来到西雅图,而且保持这个组不变。我就和我老婆商量,一切都是未知数,心里也发毛,这似乎是一个看起来挺不错的机会,不换组的话工作上的变动可以尽量小一点,压力就会小一点,我们应该抓住。于是我们两人就开始准备,专门正儿八经地学习英文,这似乎是在读书之后都没有的经历了。

2014年5月的时候,我们搬到了西雅图。接下去的时间,最初的三个月内,遇到的困难实在不少,我老婆在生活上给了很大的帮助,于是过渡这个既新鲜又痛苦的过程变得相对顺利。无论如何,能够在世界上最大的两个软件国度拥有居住和工作的体验,作为工程师来说,真是宝贵的经历。

在L签证的前提下,没有跳槽的可能,可是由于组内人事变动,躁动的心又开始作祟,想去一个更加“国际化”的环境工作,也想把自己在数据处理方面的短板补一补,于是考虑换组。和一些经理谈过之后,面了5个觉得感兴趣的组,2015年11月,加入了Contribution Profit这个组,计算成本和利润。从做的事情上看,算是真真正正天天都得和名副其实的“大数据”打交道了。很高兴学到不少有趣的东西,Scala,Spark,distributed workflow等等。

2016年算是因为各种原因,老实了一年,当然,拿到了H签证,这意味着跳槽成为了可能。

2017年下半年,面了一堆公司,最终决定加入Oracle,而整个冗长的手续,一直拖到2018年初才办好。一周前离开的时候,回想起在亚马逊的时光,恍如昨日。我在亚马逊学到了各种各样的技术,但是这些并不是我最引以为傲的。我见识到了大型互联网企业是怎样运作的,在云计算的巨头工作“是怎样一种体验”(知乎体)。工作上我认识了一群伙伴,来自世界各地,五花八门的风格,其中不少还有挺不错的私交。我学到了一个工程师应该具备怎样的严谨,怎样争论问题,怎样铺陈事实,怎样做trade-off……美好的体验太多,6年了,仿佛只是惊鸿一瞥的时间。

贴一张在本来发在朋友圈的,我写给给同事们留念的贺卡。并且非常负责任地说,所有的头像都丑爆了……

再见,亚马逊时光

还有这张用了6年的已经快要磨烂的工牌:

再见,亚马逊时光

就这样吧。谨以此文,纪念无限回味的亚马逊时光。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接《四火的唠叨》

分享到:
]]>
0
<![CDATA[Docker 微服务教程]]> http://www.udpwork.com/item/16660.html http://www.udpwork.com/item/16660.html#reviews Tue, 13 Feb 2018 09:31:50 +0800 阮一峰 http://www.udpwork.com/item/16660.html Docker 是一个容器工具,提供虚拟环境。很多人认为,它改变了我们对软件的认识。

站在 Docker 的角度,软件就是容器的组合:业务逻辑容器、数据库容器、储存容器、队列容器......Docker 使得软件可以拆分成若干个标准化容器,然后像搭积木一样组合起来。

这正是微服务(microservices)的思想:软件把任务外包出去,让各种外部服务完成这些任务,软件本身只是底层服务的调度中心和组装层。

微服务很适合用 Docker 容器实现,每个容器承载一个服务。一台计算机同时运行多个容器,从而就能很轻松地模拟出复杂的微服务架构。

上一篇教程介绍了 Docker 的概念和基本用法,本文接着往下介绍,如何在一台计算机上实现多个服务,让它们互相配合,组合出一个应用程序。

我选择的示例软件是WordPress。它是一个常用软件,全世界用户据说超过几千万。同时它又非常简单,只要两个容器就够了(业务容器 + 数据库容器),很适合教学。而且,这种"业务 + 数据库"的容器架构,具有通用性,许多应用程序都可以复用。

为了加深读者理解,本文采用三种方法,演示如何架设 WordPress 网站。

  • 方法 A:自建 WordPress 容器
  • 方法 B:采用官方的 WordPress 容器
  • 方法 C:采用 Docker Compose 工具

一、预备工作:image 仓库的镜像网址

本教程需要从仓库下载 image 文件,但是国内访问 Docker 的官方仓库很慢,还经常断线,所以要把仓库网址改成国内的镜像站。这里推荐使用官方镜像 registry.docker-cn.com 。下面是我的 Debian 系统的默认仓库修改方法,其他系统的修改方法参考官方文档

打开/etc/default/docker文件(需要sudo权限),在文件的底部加上一行。

DOCKER_OPTS="--registry-mirror=https://registry.docker-cn.com"

然后,重启 Docker 服务。

$ sudo service docker restart

现在就会自动从镜像仓库下载 image 文件了。

二、方法 A:自建 WordPress 容器

前面说过,本文会用三种方法演示 WordPress 的安装。第一种方法就是自建 WordPress 容器。

2.1 官方 的 PHP image

首先,新建一个工作目录,并进入该目录。

$ mkdir docker-demo && cd docker-demo

然后,执行下面的命令。

$ docker container run \
  --rm \
  --name wordpress \
  --volume "$PWD/":/var/www/html \
  php:5.6-apache

上面的命令基于php的 image 文件新建一个容器,并且运行该容器。php的标签是5.6-apache,说明装的是 PHP 5.6,并且自带 Apache 服务器。该命令的三个参数含义如下。

  • --rm:停止运行后,自动删除容器文件。
  • --name wordpress:容器的名字叫做wordpress。
  • --volume "$PWD/":/var/www/html:将当前目录($PWD)映射到容器的/var/www/html(Apache 对外访问的默认目录)。因此,当前目录的任何修改,都会反映到容器里面,进而被外部访问到。

运行上面的命令以后,如果一切正常,命令行会提示容器对外的 IP 地址,请记下这个地址,我们要用它来访问容器。我分配到的 IP 地址是 172.17.0.2。

打开浏览器,访问 172.17.0.2,你会看到下面的提示。

Forbidden
You don't have permission to access / on this server.

这是因为容器的/var/www/html目录(也就是本机的docker-demo目录)下面什么也没有,无法提供可以访问的内容。

请在本机的docker-demo目录下面,添加一个最简单的 PHP 文件index.php。

<?php 
phpinfo();
?>

保存以后,浏览器刷新172.17.0.2,应该就会看到熟悉的phpinfo页面了。

2.2 拷贝 WordPress 安装包

既然本地的docker-demo目录可以映射到容器里面,那么把 WordPress 安装包拷贝到docker-demo目录下,不就可以通过容器访问到 WordPress 的安装界面了吗?

首先,在docker-demo目录下,执行下面的命令,抓取并解压 WordPress 安装包。

$ wget https://cn.wordpress.org/wordpress-4.9.4-zh_CN.tar.gz
$ tar -xvf wordpress-4.9.4-zh_CN.tar.gz

解压以后,WordPress 的安装文件会在docker-demo/wordpress目录下。

这时浏览器访问http://172.17.0.2/wordpress,就能看到 WordPress 的安装提示了。

2.3 官方的 MySQL 容器

WordPress 必须有数据库才能安装,所以必须新建 MySQL 容器。

打开一个新的命令行窗口,执行下面的命令。

$ docker container run \
  -d \
  --rm \
  --name wordpressdb \
  --env MYSQL_ROOT_PASSWORD=123456 \
  --env MYSQL_DATABASE=wordpress \
  mysql:5.7

上面的命令会基于 MySQL 的 image 文件(5.7版本)新建一个容器。该命令的五个命令行参数的含义如下。

  • -d:容器启动后,在后台运行。
  • --rm:容器终止运行后,自动删除容器文件。
  • --name wordpressdb:容器的名字叫做wordpressdb
  • --env MYSQL_ROOT_PASSWORD=123456:向容器进程传入一个环境变量MYSQL_ROOT_PASSWORD,该变量会被用作 MySQL 的根密码。
  • --env MYSQL_DATABASE=wordpress:向容器进程传入一个环境变量MYSQL_DATABASE,容器里面的 MySQL 会根据该变量创建一个同名数据库(本例是WordPress)。

运行上面的命令以后,正常情况下,命令行会显示一行字符串,这是容器的 ID,表示已经新建成功了。

这时,使用下面的命令查看正在运行的容器,你应该看到wordpress和wordpressdb两个容器正在运行。

$ docker container ls

其中,wordpressdb是后台运行的,前台看不见它的输出,必须使用下面的命令查看。

$ docker container logs wordpressdb

2.4 定制 PHP 容器

现在 WordPress 容器和 MySQL 容器都已经有了。接下来,要把 WordPress 容器连接到 MySQL 容器了。但是,PHP 的官方 image 不带有mysql扩展,必须自己新建 image 文件。

首先,停掉 WordPress 容器。

$ docker container stop wordpress

停掉以后,由于--rm参数的作用,该容器文件会被自动删除。

然后,在docker-demo目录里面,新建一个Dockerfile文件,写入下面的内容。

FROM php:5.6-apache
RUN docker-php-ext-install mysqli
CMD apache2-foreground

上面代码的意思,就是在原来 PHP 的 image 基础上,安装mysqli的扩展。然后,启动 Apache。

基于这个 Dockerfile 文件,新建一个名为phpwithmysql的 image 文件。

$ docker build -t phpwithmysql .

2.5 Wordpress 容器连接 MySQL

现在基于 phpwithmysql image,重新新建一个 WordPress 容器。

$ docker container run \
  --rm \
  --name wordpress \
  --volume "$PWD/":/var/www/html \
  --link wordpressdb:mysql \
  phpwithmysql

跟上一次相比,上面的命令多了一个参数--link wordpressdb:mysql,表示 WordPress 容器要连到wordpressdb容器,冒号表示该容器的别名是mysql。

这时还要改一下wordpress目录的权限,让容器可以将配置信息写入这个目录(容器内部写入的/var/www/html目录,会映射到这个目录)。

$ chmod -R 777 wordpress

接着,回到浏览器的http://172.17.0.2/wordpress页面,点击"现在就开始!"按钮,开始安装。

WordPress 提示要输入数据库参数。输入的参数如下。

  • 数据库名:wordpress
  • 用户名:root
  • 密码:123456
  • 数据库主机:mysql
  • 表前缀:wp_(不变)

点击"下一步"按钮,如果 Wordpress 连接数据库成功,就会出现下面的页面,这就表示可以安装了。

至此,自建 WordPress 容器的演示完毕,可以把正在运行的两个容器关闭了(容器文件会自动删除)。

$ docker container stop wordpress wordpressdb

三、方法 B:Wordpress 官方镜像

上一部分的自建 WordPress 容器,还是挺麻烦的。其实不用这么麻烦,Docker 已经提供了官方WordPressimage,直接用那个就可以了。有了上一部分的基础,下面的操作就很容易理解了。

3.1 基本用法

首先,新建并启动 MySQL 容器。

$ docker container run \
  -d \
  --rm \
  --name wordpressdb \
  --env MYSQL_ROOT_PASSWORD=123456 \
  --env MYSQL_DATABASE=wordpress \
  mysql:5.7

然后,基于官方的 WordPress image,新建并启动 WordPress 容器。

$ docker container run \
  -d \
  --rm \
  --name wordpress \
  --env WORDPRESS_DB_PASSWORD=123456 \
  --link wordpressdb:mysql \
  wordpress

上面命令中,各个参数的含义前面都解释过了,其中环境变量WORDPRESS_DB_PASSWORD是 MySQL 容器的根密码。

上面命令指定wordpress容器在后台运行,导致前台看不见输出,使用下面的命令查出wordpress容器的 IP 地址。

$ docker container inspect wordpress

上面命令运行以后,会输出很多内容,找到IPAddress字段即可。我的机器返回的 IP 地址是172.17.0.3。

浏览器访问172.17.0.3,就会看到 WordPress 的安装提示。

3.2 WordPress 容器的定制

到了上一步,官方 WordPress 容器的安装就已经成功了。但是,这种方法有两个很不方便的地方。

  • 每次新建容器,返回的 IP 地址不能保证相同,导致要更换 IP 地址访问 WordPress。
  • WordPress 安装在容器里面,本地无法修改文件。

解决这两个问题很容易,只要新建容器的时候,加两个命令行参数就可以了。

先把刚才启动的 WordPress 容器终止(容器文件会自动删除)。

$ docker container stop wordpress

然后,使用下面的命令新建并启动 WordPress 容器。

 $ docker container run \
  -d \
  -p 127.0.0.2:8080:80 \
  --rm \
  --name wordpress \
  --env WORDPRESS_DB_PASSWORD=123456 \
  --link wordpressdb:mysql \
  --volume "$PWD/wordpress":/var/www/html \
  wordpress

上面的命令跟前面相比,命令行参数只多出了两个。

  • -p 127.0.0.2:8080:80:将容器的 80 端口映射到127.0.0.2的8080端口。
  • --volume "$PWD/wordpress":/var/www/html:将容器的/var/www/html目录映射到当前目录的wordpress子目录。

浏览器访问127.0.0.2:8080:80就能看到 WordPress 的安装提示了。而且,你在wordpress子目录下的每次修改,都会反映到容器里面。

最后,终止这两个容器(容器文件会自动删除)。

$ docker container stop wordpress wordpressdb

四、方法 C:Docker Compose 工具

上面的方法 B 已经挺简单了,但是必须自己分别启动两个容器,启动的时候,还要在命令行提供容器之间的连接信息。因此,Docker 提供了一种更简单的方法,来管理多个容器的联动。

4.1 Docker Compose 简介

Compose是 Docker 公司推出的一个工具软件,可以管理多个 Docker 容器组成一个应用。你需要定义一个YAML格式的配置文件docker-compose.yml,写好多个容器之间的调用关系。然后,只要一个命令,就能同时启动/关闭这些容器。

# 启动所有服务
$ docker-compose up
# 关闭所有服务
$ docker-compose stop

4.2 Docker Compose 的安装

Mac 和 Windows 在安装 docker 的时候,会一起安装 docker compose。Linux 系统下的安装参考官方文档

安装完成后,运行下面的命令。

$ docker-compose --version

4.3 WordPress 示例

在docker-demo目录下,新建docker-compose.yml文件,写入下面的内容。

mysql:
    image: mysql:5.7
    environment:
     - MYSQL_ROOT_PASSWORD=123456
     - MYSQL_DATABASE=wordpress
web:
    image: wordpress
    links:
     - mysql
    environment:
     - WORDPRESS_DB_PASSWORD=123456
    ports:
     - "127.0.0.3:8080:80"
    working_dir: /var/www/html
    volumes:
     - wordpress:/var/www/html

上面代码中,两个顶层标签表示有两个容器mysql和web。每个容器的具体设置,前面都已经讲解过了,还是挺容易理解的。

启动两个容器。

$ docker-compose up

浏览器访问 http://127.0.0.3:8080,应该就能看到 WordPress 的安装界面。

现在关闭两个容器。

$ docker-compose stop

关闭以后,这两个容器文件还是存在的,写在里面的数据不会丢失。下次启动的时候,还可以复用。下面的命令可以把这两个容器文件删除(容器必须已经停止运行)。

$ docker-compose rm

五、参考链接

(完)

文档信息

]]>
Docker 是一个容器工具,提供虚拟环境。很多人认为,它改变了我们对软件的认识。

站在 Docker 的角度,软件就是容器的组合:业务逻辑容器、数据库容器、储存容器、队列容器......Docker 使得软件可以拆分成若干个标准化容器,然后像搭积木一样组合起来。

这正是微服务(microservices)的思想:软件把任务外包出去,让各种外部服务完成这些任务,软件本身只是底层服务的调度中心和组装层。

微服务很适合用 Docker 容器实现,每个容器承载一个服务。一台计算机同时运行多个容器,从而就能很轻松地模拟出复杂的微服务架构。

上一篇教程介绍了 Docker 的概念和基本用法,本文接着往下介绍,如何在一台计算机上实现多个服务,让它们互相配合,组合出一个应用程序。

我选择的示例软件是WordPress。它是一个常用软件,全世界用户据说超过几千万。同时它又非常简单,只要两个容器就够了(业务容器 + 数据库容器),很适合教学。而且,这种"业务 + 数据库"的容器架构,具有通用性,许多应用程序都可以复用。

为了加深读者理解,本文采用三种方法,演示如何架设 WordPress 网站。

  • 方法 A:自建 WordPress 容器
  • 方法 B:采用官方的 WordPress 容器
  • 方法 C:采用 Docker Compose 工具

一、预备工作:image 仓库的镜像网址

本教程需要从仓库下载 image 文件,但是国内访问 Docker 的官方仓库很慢,还经常断线,所以要把仓库网址改成国内的镜像站。这里推荐使用官方镜像 registry.docker-cn.com 。下面是我的 Debian 系统的默认仓库修改方法,其他系统的修改方法参考官方文档

打开/etc/default/docker文件(需要sudo权限),在文件的底部加上一行。

DOCKER_OPTS="--registry-mirror=https://registry.docker-cn.com"

然后,重启 Docker 服务。

$ sudo service docker restart

现在就会自动从镜像仓库下载 image 文件了。

二、方法 A:自建 WordPress 容器

前面说过,本文会用三种方法演示 WordPress 的安装。第一种方法就是自建 WordPress 容器。

2.1 官方 的 PHP image

首先,新建一个工作目录,并进入该目录。

$ mkdir docker-demo && cd docker-demo

然后,执行下面的命令。

$ docker container run \
  --rm \
  --name wordpress \
  --volume "$PWD/":/var/www/html \
  php:5.6-apache

上面的命令基于php的 image 文件新建一个容器,并且运行该容器。php的标签是5.6-apache,说明装的是 PHP 5.6,并且自带 Apache 服务器。该命令的三个参数含义如下。

  • --rm:停止运行后,自动删除容器文件。
  • --name wordpress:容器的名字叫做wordpress。
  • --volume "$PWD/":/var/www/html:将当前目录($PWD)映射到容器的/var/www/html(Apache 对外访问的默认目录)。因此,当前目录的任何修改,都会反映到容器里面,进而被外部访问到。

运行上面的命令以后,如果一切正常,命令行会提示容器对外的 IP 地址,请记下这个地址,我们要用它来访问容器。我分配到的 IP 地址是 172.17.0.2。

打开浏览器,访问 172.17.0.2,你会看到下面的提示。

Forbidden
You don't have permission to access / on this server.

这是因为容器的/var/www/html目录(也就是本机的docker-demo目录)下面什么也没有,无法提供可以访问的内容。

请在本机的docker-demo目录下面,添加一个最简单的 PHP 文件index.php。

<?php 
phpinfo();
?>

保存以后,浏览器刷新172.17.0.2,应该就会看到熟悉的phpinfo页面了。

2.2 拷贝 WordPress 安装包

既然本地的docker-demo目录可以映射到容器里面,那么把 WordPress 安装包拷贝到docker-demo目录下,不就可以通过容器访问到 WordPress 的安装界面了吗?

首先,在docker-demo目录下,执行下面的命令,抓取并解压 WordPress 安装包。

$ wget https://cn.wordpress.org/wordpress-4.9.4-zh_CN.tar.gz
$ tar -xvf wordpress-4.9.4-zh_CN.tar.gz

解压以后,WordPress 的安装文件会在docker-demo/wordpress目录下。

这时浏览器访问http://172.17.0.2/wordpress,就能看到 WordPress 的安装提示了。

2.3 官方的 MySQL 容器

WordPress 必须有数据库才能安装,所以必须新建 MySQL 容器。

打开一个新的命令行窗口,执行下面的命令。

$ docker container run \
  -d \
  --rm \
  --name wordpressdb \
  --env MYSQL_ROOT_PASSWORD=123456 \
  --env MYSQL_DATABASE=wordpress \
  mysql:5.7

上面的命令会基于 MySQL 的 image 文件(5.7版本)新建一个容器。该命令的五个命令行参数的含义如下。

  • -d:容器启动后,在后台运行。
  • --rm:容器终止运行后,自动删除容器文件。
  • --name wordpressdb:容器的名字叫做wordpressdb
  • --env MYSQL_ROOT_PASSWORD=123456:向容器进程传入一个环境变量MYSQL_ROOT_PASSWORD,该变量会被用作 MySQL 的根密码。
  • --env MYSQL_DATABASE=wordpress:向容器进程传入一个环境变量MYSQL_DATABASE,容器里面的 MySQL 会根据该变量创建一个同名数据库(本例是WordPress)。

运行上面的命令以后,正常情况下,命令行会显示一行字符串,这是容器的 ID,表示已经新建成功了。

这时,使用下面的命令查看正在运行的容器,你应该看到wordpress和wordpressdb两个容器正在运行。

$ docker container ls

其中,wordpressdb是后台运行的,前台看不见它的输出,必须使用下面的命令查看。

$ docker container logs wordpressdb

2.4 定制 PHP 容器

现在 WordPress 容器和 MySQL 容器都已经有了。接下来,要把 WordPress 容器连接到 MySQL 容器了。但是,PHP 的官方 image 不带有mysql扩展,必须自己新建 image 文件。

首先,停掉 WordPress 容器。

$ docker container stop wordpress

停掉以后,由于--rm参数的作用,该容器文件会被自动删除。

然后,在docker-demo目录里面,新建一个Dockerfile文件,写入下面的内容。

FROM php:5.6-apache
RUN docker-php-ext-install mysqli
CMD apache2-foreground

上面代码的意思,就是在原来 PHP 的 image 基础上,安装mysqli的扩展。然后,启动 Apache。

基于这个 Dockerfile 文件,新建一个名为phpwithmysql的 image 文件。

$ docker build -t phpwithmysql .

2.5 Wordpress 容器连接 MySQL

现在基于 phpwithmysql image,重新新建一个 WordPress 容器。

$ docker container run \
  --rm \
  --name wordpress \
  --volume "$PWD/":/var/www/html \
  --link wordpressdb:mysql \
  phpwithmysql

跟上一次相比,上面的命令多了一个参数--link wordpressdb:mysql,表示 WordPress 容器要连到wordpressdb容器,冒号表示该容器的别名是mysql。

这时还要改一下wordpress目录的权限,让容器可以将配置信息写入这个目录(容器内部写入的/var/www/html目录,会映射到这个目录)。

$ chmod -R 777 wordpress

接着,回到浏览器的http://172.17.0.2/wordpress页面,点击"现在就开始!"按钮,开始安装。

WordPress 提示要输入数据库参数。输入的参数如下。

  • 数据库名:wordpress
  • 用户名:root
  • 密码:123456
  • 数据库主机:mysql
  • 表前缀:wp_(不变)

点击"下一步"按钮,如果 Wordpress 连接数据库成功,就会出现下面的页面,这就表示可以安装了。

至此,自建 WordPress 容器的演示完毕,可以把正在运行的两个容器关闭了(容器文件会自动删除)。

$ docker container stop wordpress wordpressdb

三、方法 B:Wordpress 官方镜像

上一部分的自建 WordPress 容器,还是挺麻烦的。其实不用这么麻烦,Docker 已经提供了官方WordPressimage,直接用那个就可以了。有了上一部分的基础,下面的操作就很容易理解了。

3.1 基本用法

首先,新建并启动 MySQL 容器。

$ docker container run \
  -d \
  --rm \
  --name wordpressdb \
  --env MYSQL_ROOT_PASSWORD=123456 \
  --env MYSQL_DATABASE=wordpress \
  mysql:5.7

然后,基于官方的 WordPress image,新建并启动 WordPress 容器。

$ docker container run \
  -d \
  --rm \
  --name wordpress \
  --env WORDPRESS_DB_PASSWORD=123456 \
  --link wordpressdb:mysql \
  wordpress

上面命令中,各个参数的含义前面都解释过了,其中环境变量WORDPRESS_DB_PASSWORD是 MySQL 容器的根密码。

上面命令指定wordpress容器在后台运行,导致前台看不见输出,使用下面的命令查出wordpress容器的 IP 地址。

$ docker container inspect wordpress

上面命令运行以后,会输出很多内容,找到IPAddress字段即可。我的机器返回的 IP 地址是172.17.0.3。

浏览器访问172.17.0.3,就会看到 WordPress 的安装提示。

3.2 WordPress 容器的定制

到了上一步,官方 WordPress 容器的安装就已经成功了。但是,这种方法有两个很不方便的地方。

  • 每次新建容器,返回的 IP 地址不能保证相同,导致要更换 IP 地址访问 WordPress。
  • WordPress 安装在容器里面,本地无法修改文件。

解决这两个问题很容易,只要新建容器的时候,加两个命令行参数就可以了。

先把刚才启动的 WordPress 容器终止(容器文件会自动删除)。

$ docker container stop wordpress

然后,使用下面的命令新建并启动 WordPress 容器。

 $ docker container run \
  -d \
  -p 127.0.0.2:8080:80 \
  --rm \
  --name wordpress \
  --env WORDPRESS_DB_PASSWORD=123456 \
  --link wordpressdb:mysql \
  --volume "$PWD/wordpress":/var/www/html \
  wordpress

上面的命令跟前面相比,命令行参数只多出了两个。

  • -p 127.0.0.2:8080:80:将容器的 80 端口映射到127.0.0.2的8080端口。
  • --volume "$PWD/wordpress":/var/www/html:将容器的/var/www/html目录映射到当前目录的wordpress子目录。

浏览器访问127.0.0.2:8080:80就能看到 WordPress 的安装提示了。而且,你在wordpress子目录下的每次修改,都会反映到容器里面。

最后,终止这两个容器(容器文件会自动删除)。

$ docker container stop wordpress wordpressdb

四、方法 C:Docker Compose 工具

上面的方法 B 已经挺简单了,但是必须自己分别启动两个容器,启动的时候,还要在命令行提供容器之间的连接信息。因此,Docker 提供了一种更简单的方法,来管理多个容器的联动。

4.1 Docker Compose 简介

Compose是 Docker 公司推出的一个工具软件,可以管理多个 Docker 容器组成一个应用。你需要定义一个YAML格式的配置文件docker-compose.yml,写好多个容器之间的调用关系。然后,只要一个命令,就能同时启动/关闭这些容器。

# 启动所有服务
$ docker-compose up
# 关闭所有服务
$ docker-compose stop

4.2 Docker Compose 的安装

Mac 和 Windows 在安装 docker 的时候,会一起安装 docker compose。Linux 系统下的安装参考官方文档

安装完成后,运行下面的命令。

$ docker-compose --version

4.3 WordPress 示例

在docker-demo目录下,新建docker-compose.yml文件,写入下面的内容。

mysql:
    image: mysql:5.7
    environment:
     - MYSQL_ROOT_PASSWORD=123456
     - MYSQL_DATABASE=wordpress
web:
    image: wordpress
    links:
     - mysql
    environment:
     - WORDPRESS_DB_PASSWORD=123456
    ports:
     - "127.0.0.3:8080:80"
    working_dir: /var/www/html
    volumes:
     - wordpress:/var/www/html

上面代码中,两个顶层标签表示有两个容器mysql和web。每个容器的具体设置,前面都已经讲解过了,还是挺容易理解的。

启动两个容器。

$ docker-compose up

浏览器访问 http://127.0.0.3:8080,应该就能看到 WordPress 的安装界面。

现在关闭两个容器。

$ docker-compose stop

关闭以后,这两个容器文件还是存在的,写在里面的数据不会丢失。下次启动的时候,还可以复用。下面的命令可以把这两个容器文件删除(容器必须已经停止运行)。

$ docker-compose rm

五、参考链接

(完)

文档信息

]]>
0
<![CDATA[最近玩的几款游戏]]> http://www.udpwork.com/item/16659.html http://www.udpwork.com/item/16659.html#reviews Mon, 12 Feb 2018 21:36:46 +0800 云风 http://www.udpwork.com/item/16659.html 最近和一位新同事一起在开发新的 3d engine 。还在构建基础的东西。从第一次提交到现在已经过去了 24 天,有 129 个 commits 。短期内还不太可能开源(即使开放仓库,估计也没几个人知道怎么构建出来)。提一句,只是说明这个项目正在进行中。

从 2017 年底到现在倒是玩到了不少非常不错的游戏。这些游戏没有用到什么华丽的技术,但它们都有一些能抓住玩家的不一样的东西。

首先是 Slay the Spire 。这款结合卡牌构建和 Roguelike 元素的游戏,从我第一眼看到起就深深的被吸引了。上一款类似的游戏是 Dream Quest ,但这款各方面都远超前者。Rogue Like 的元素可以增加很大的耐玩性,每局游戏能收集到的东西都是未知的,所以需要对当前的收集情况做很多预判。而永久死亡机制会让人决策的时候非常小心。达成目标后的惊喜感是可以不断 S/L 的玩法所无法比拟的。不断的重来,积累的更多是对游戏的理解,也同时减轻了复杂系统的学习难度。

我玩的时候还没有中文,但学习门槛却不高,现在加了中文支持应该更容易了。只需要失败几次,就能掌握更多。

我一直认为,Rogue Like 的核心机制能带给各种类型的游戏更多的玩点,只是过去对此挖掘不够,上面谈到的卡牌构筑类是一例,另一例就是前段时间吞噬了我一百多小时的 They are Billons 了。

如果非要给 TAB (They Are Billons)贴一个标签,我会称它为 Rogue Like 塔防。但实际上,它或许更像 即时战略、城市建造、 塔防的综合体。当然 Rogue Like 的核心机制之一:不能反悔、永久死亡是它的灵 魂。

它的界面外观第一眼看上去是一个即时战略,像极了星际。玩过的人也都称它还原了当年星际的 7v1 地图的核心乐趣。不过我觉得,一旦即时战略加上了随时暂停,并鼓励暂停,就完全不是一种体验了。

同样是修筑大量防御建筑阻挡海量的无脑敌军的进攻,TAB 和传统的塔防游戏也有不大相同的体验。之前很少有塔防游戏把 2d 开放地图的地形元素做得如此重要,以至于玩 TAB 时,开局都需要把基地周围的环境探查一遍就可以在心中制定出截然不同的计划。是早点造兵去清图、还是速攀科技、还是用防御塔为主抵御中期敌人的进攻,战略的制定和随机出来的地图息息相关。能让随机地图变得有趣,我觉得是 TAB 的成功点之一。

利用资源的相互制约,把建筑所需地块面积也纳入核心制约条件中,我认为是 TAB 对传统塔防的最大改进:你铺的摊子越大,防守越难,成本越高;用更合理的布局,来发挥建筑用地的最高效能,这原本是城市建造类游戏的核心玩法,就这样无形融合到了一款塔防游戏中,简直是完美。

我大概花了 120 个小时才用 100% 难度通了第一张图,只要打过一次,后面就基本没有难度了。虽然没有看别人的攻略,但这个时间来看我的悟性算比较低的。在我的好友圈子里打听,悟性高的人平均 60 个小时就能破解作者的设计。

我很享受这个过程。头 20 个小时基本都是在学习游戏的基本策略,怎样在游戏前期活下来。然后的数十小时都是在试错。试错的时间成本很高,每局平均要 4 到 5 小时,才会有所新的认识。但这个时间也充分让玩家积累情绪,认识深刻。我是很晚才意识到不仅要节流,还要开源的。整个游戏过程中不能停止发展,才是通关的要素。而之前花了十多盘的失败,尝试各种精妙的操作,企图用最少的投资构建精妙的阵形来完成游戏。最终意识到正确的玩法后,有种恍然大悟的感觉。

我想,我喜欢的那类游戏都有这种共性:在玩的过程中不断的失败,设计者通过玩家的失败而不断的把设计点传达出来。玩的过程就是在不断的选择中,找到设计者想表达的那个正确选择。如果玩的过程中选择不够丰富、或是正确的选项太容易找到,都会让人感觉无趣。

另外还在 Switch 上玩到了几款有趣的游戏。

Darkest Dungeon 之前在 PC 上玩了许久,搬到 Switch 上重新支持了一把。好游戏,但这次不多展开说了。

Overcooked 非常推荐给和大一点的小孩或情侣一起玩。作为一个双人(多人)游戏,比 1-2 Switch 不知道高到哪去了。

重点想说说的是 沙漠老鼠团 (Of mice and sand) 。自从 Hayday 之后,我比较少玩这种单纯的制造链管理游戏。但玩了这一款后,我突然发现我对这类游戏的理解远远不够,其实这里面是可以挖掘出很多新鲜的玩点出来的。很惭愧,前几年有过一个类似机制的游戏的构思,但是因为找不到什么兴奋点,所以也就没有没有进展了。

我拿 Hayday 来比较其实是不恰当的,沙漠老鼠团完全不是一个放置游戏。我玩游戏的过程中基本没有闲下来过,关掉游戏也不会有任何的离线放置奖励。它完全就是在做制造链管理。但它却给我了许多新的启发。

比如:一个物品的制造配方其实不应该是单一的,如果多设计几条制造路线,其实会给游戏带来许多乐趣。玩家可以在不同维度的资源下作平衡。工厂、工人(老鼠)、时间的不同会让玩家在不同的资源配置情况下选择不同的制造方案。注意,这里的时间并不是放置内游戏的时间,而是游戏中的一种资源要素。更多的时间意味着要给游戏中的工人提供更多的食物来养活它们。

游戏中基础原料的产出来源于不同的区域,加上油料和时间的限制,也解决了传统制造类游戏中低级原料家孩子和高级原料价值相差太大的问题。对于网游也是如此:网游通常为了延长玩家的游戏时间,把各种资源链做的很长,最后新手玩家接触的资源对于高级玩家来说变得完全没有价值。

btw, 在沙漠老鼠团之前,我玩过缺氧。但是从时间上来看缺氧出的更晚一些。我严重怀疑缺氧的游戏概念深受了沙漠老鼠团的启发。

今天谈到的游戏,都可以在 steam 上或 switch 的 eshop 上用名字搜索到,我就不一一给出链接了。

]]>
最近和一位新同事一起在开发新的 3d engine 。还在构建基础的东西。从第一次提交到现在已经过去了 24 天,有 129 个 commits 。短期内还不太可能开源(即使开放仓库,估计也没几个人知道怎么构建出来)。提一句,只是说明这个项目正在进行中。

从 2017 年底到现在倒是玩到了不少非常不错的游戏。这些游戏没有用到什么华丽的技术,但它们都有一些能抓住玩家的不一样的东西。

首先是 Slay the Spire 。这款结合卡牌构建和 Roguelike 元素的游戏,从我第一眼看到起就深深的被吸引了。上一款类似的游戏是 Dream Quest ,但这款各方面都远超前者。Rogue Like 的元素可以增加很大的耐玩性,每局游戏能收集到的东西都是未知的,所以需要对当前的收集情况做很多预判。而永久死亡机制会让人决策的时候非常小心。达成目标后的惊喜感是可以不断 S/L 的玩法所无法比拟的。不断的重来,积累的更多是对游戏的理解,也同时减轻了复杂系统的学习难度。

我玩的时候还没有中文,但学习门槛却不高,现在加了中文支持应该更容易了。只需要失败几次,就能掌握更多。

我一直认为,Rogue Like 的核心机制能带给各种类型的游戏更多的玩点,只是过去对此挖掘不够,上面谈到的卡牌构筑类是一例,另一例就是前段时间吞噬了我一百多小时的 They are Billons 了。

如果非要给 TAB (They Are Billons)贴一个标签,我会称它为 Rogue Like 塔防。但实际上,它或许更像 即时战略、城市建造、 塔防的综合体。当然 Rogue Like 的核心机制之一:不能反悔、永久死亡是它的灵 魂。

它的界面外观第一眼看上去是一个即时战略,像极了星际。玩过的人也都称它还原了当年星际的 7v1 地图的核心乐趣。不过我觉得,一旦即时战略加上了随时暂停,并鼓励暂停,就完全不是一种体验了。

同样是修筑大量防御建筑阻挡海量的无脑敌军的进攻,TAB 和传统的塔防游戏也有不大相同的体验。之前很少有塔防游戏把 2d 开放地图的地形元素做得如此重要,以至于玩 TAB 时,开局都需要把基地周围的环境探查一遍就可以在心中制定出截然不同的计划。是早点造兵去清图、还是速攀科技、还是用防御塔为主抵御中期敌人的进攻,战略的制定和随机出来的地图息息相关。能让随机地图变得有趣,我觉得是 TAB 的成功点之一。

利用资源的相互制约,把建筑所需地块面积也纳入核心制约条件中,我认为是 TAB 对传统塔防的最大改进:你铺的摊子越大,防守越难,成本越高;用更合理的布局,来发挥建筑用地的最高效能,这原本是城市建造类游戏的核心玩法,就这样无形融合到了一款塔防游戏中,简直是完美。

我大概花了 120 个小时才用 100% 难度通了第一张图,只要打过一次,后面就基本没有难度了。虽然没有看别人的攻略,但这个时间来看我的悟性算比较低的。在我的好友圈子里打听,悟性高的人平均 60 个小时就能破解作者的设计。

我很享受这个过程。头 20 个小时基本都是在学习游戏的基本策略,怎样在游戏前期活下来。然后的数十小时都是在试错。试错的时间成本很高,每局平均要 4 到 5 小时,才会有所新的认识。但这个时间也充分让玩家积累情绪,认识深刻。我是很晚才意识到不仅要节流,还要开源的。整个游戏过程中不能停止发展,才是通关的要素。而之前花了十多盘的失败,尝试各种精妙的操作,企图用最少的投资构建精妙的阵形来完成游戏。最终意识到正确的玩法后,有种恍然大悟的感觉。

我想,我喜欢的那类游戏都有这种共性:在玩的过程中不断的失败,设计者通过玩家的失败而不断的把设计点传达出来。玩的过程就是在不断的选择中,找到设计者想表达的那个正确选择。如果玩的过程中选择不够丰富、或是正确的选项太容易找到,都会让人感觉无趣。

另外还在 Switch 上玩到了几款有趣的游戏。

Darkest Dungeon 之前在 PC 上玩了许久,搬到 Switch 上重新支持了一把。好游戏,但这次不多展开说了。

Overcooked 非常推荐给和大一点的小孩或情侣一起玩。作为一个双人(多人)游戏,比 1-2 Switch 不知道高到哪去了。

重点想说说的是 沙漠老鼠团 (Of mice and sand) 。自从 Hayday 之后,我比较少玩这种单纯的制造链管理游戏。但玩了这一款后,我突然发现我对这类游戏的理解远远不够,其实这里面是可以挖掘出很多新鲜的玩点出来的。很惭愧,前几年有过一个类似机制的游戏的构思,但是因为找不到什么兴奋点,所以也就没有没有进展了。

我拿 Hayday 来比较其实是不恰当的,沙漠老鼠团完全不是一个放置游戏。我玩游戏的过程中基本没有闲下来过,关掉游戏也不会有任何的离线放置奖励。它完全就是在做制造链管理。但它却给我了许多新的启发。

比如:一个物品的制造配方其实不应该是单一的,如果多设计几条制造路线,其实会给游戏带来许多乐趣。玩家可以在不同维度的资源下作平衡。工厂、工人(老鼠)、时间的不同会让玩家在不同的资源配置情况下选择不同的制造方案。注意,这里的时间并不是放置内游戏的时间,而是游戏中的一种资源要素。更多的时间意味着要给游戏中的工人提供更多的食物来养活它们。

游戏中基础原料的产出来源于不同的区域,加上油料和时间的限制,也解决了传统制造类游戏中低级原料家孩子和高级原料价值相差太大的问题。对于网游也是如此:网游通常为了延长玩家的游戏时间,把各种资源链做的很长,最后新手玩家接触的资源对于高级玩家来说变得完全没有价值。

btw, 在沙漠老鼠团之前,我玩过缺氧。但是从时间上来看缺氧出的更晚一些。我严重怀疑缺氧的游戏概念深受了沙漠老鼠团的启发。

今天谈到的游戏,都可以在 steam 上或 switch 的 eshop 上用名字搜索到,我就不一一给出链接了。

]]>
0
<![CDATA[Sentry Error: sentry_email does not exist]]> http://www.udpwork.com/item/16658.html http://www.udpwork.com/item/16658.html#reviews Sun, 11 Feb 2018 20:32:41 +0800 Felix021 http://www.udpwork.com/item/16658.html 莫名其妙的一个错误,手头的两个sentry实例里都没有这个表,但是还是会报这个错。

没研究具体的代码,但是通过查找源码里的 sentry_email 发掘了表结构,建表并授权即可:

引用
$ psql -h $HOST -U root -W sentry
sentry=> create extension citext;
CREATE EXTENSION
sentry=> create table sentry_email (id bigserial primary key, email CITEXT, date_added timestamp with time zone);
CREATE TABLE
sentry=> grant all privileges on table sentry_email to sentry;
GRANT
sentry=> GRANT USAGE, SELECT ON SEQUENCE sentry_email_id_seq1 to sentry;
GRANT
]]>
莫名其妙的一个错误,手头的两个sentry实例里都没有这个表,但是还是会报这个错。

没研究具体的代码,但是通过查找源码里的 sentry_email 发掘了表结构,建表并授权即可:

引用
$ psql -h $HOST -U root -W sentry
sentry=> create extension citext;
CREATE EXTENSION
sentry=> create table sentry_email (id bigserial primary key, email CITEXT, date_added timestamp with time zone);
CREATE TABLE
sentry=> grant all privileges on table sentry_email to sentry;
GRANT
sentry=> GRANT USAGE, SELECT ON SEQUENCE sentry_email_id_seq1 to sentry;
GRANT
]]>
0
<![CDATA[机器学习时代的体验设计(下)-对创造人类行为学习系统的设计师和数据学家的启示]]> http://www.udpwork.com/item/16657.html http://www.udpwork.com/item/16657.html#reviews Sun, 11 Feb 2018 16:48:01 +0800 UXC http://www.udpwork.com/item/16657.html 人与机器之间的新关系

 

在上一篇文章中,我们会发现机器学习驱动的用户体验不是线性的,也不是基于静态的业务和设计规则的。它们会根据人类行为进行演变,并通过不断变化的数据模型进行更新。每件产品或服务仿佛都有生命一般,就像如谷歌的工作人员说的那样:“ 这是一项与众不同的工程”。我认为这也是一种与众不同的设计。例如,亚马逊将Echo定义为一台“随着时间的推移不断学习和增加更多功能”的设备,这个描述突出了我们需要为学习人类行为的用户体验系统进行设计的必要性。

机器学习的设计。图像来源:Mike Kuniavsky《物联网的预测行为的用户体验》

因此,除了要考虑首次接触和使用产品的体验之外,对于这类产品或服务而言,设计师同时需要考虑在使用1小时、1天、1年等时间后的体验。从Edyn花园传感器的宣传视频中,我们可以发现体验随着时间的不断演变:从建立照料花园的新习惯,到显示植物的未知情况,再到表达出对关键指标的信任,最后到保证一定程度的自动化灌溉时的高效时间利用。(注:此处的解释与上一篇文章中提到的机器学习时代用户体验的设计法则内容相呼应)

 

在设计这类数据产品时,设计师需要考虑各种情况,不光要考虑产品对人有用的情况,同时也要考虑到那些令人失望、尴尬、烦恼或停止工作等情况。“离线体验(offboarding experience)”的设计可能与“在线体验(onboarding experience)”一样重要。例如,据称有三分之一的Fitbit用户在6个月内停止佩戴该设备。这些数百万个被遗弃的设备会发生什么?它们生成的个人的数据会发生什么变化?有什么机会在不同的用户体验中使用它们?

通过不断向算法提供行为数据来实现演变的产品(例如Fitbit)是不可避免地走向灭亡的产品。资料来源:数据产品的生死攸关。另请参阅Megan Erin Miller《了解服务体验的生命周期》

有一种新的方式可以解决在数字与产品分离之后的出现的问题。数字服务工作在日益庞大的生态系统上,但用户数据往往呈现中心化的趋势。试想一下云信用的概念,它允许人们使用基于与另一种服务之间的关系的来使用其它服务。(注:芝麻信用的模式)

展望不久的将来,自然语言处理、知识表达、语音识别和自然语言生成方面的最新突破可以与机器建立更微妙和更强的关系。在几次迭代中,亚马逊的Echo可能会变得更加智能。人类学家Genevieve Bell预言了一种潜在演变:在AI的下一个浪潮中,人机交互到人机关系的转变是根植于人类文化和历史之中的:

“目前的人机交互框架并不是关于推荐系统的(目前大多数AI产品是这样做),但从根本上来说,其实是关于教育和关怀。如果现在的产品能够向这两个方向靠拢,那么我们就会处在一个从讨论人机交互转向人机关系的非常有趣的时刻。”

—— Genevieve Bell

在本节中,我们会发现算法已经逐渐融入到我们的日常生活中,数据为不断演变的关系提供了支撑。这种演变需要设计师和数学家之间密切合作。

 

设计师和数据学家之间的合作关系

 

通过目前的工作经验来展望数据和算法的用户体验,我发现它跟目前以人为中心的设计的做法是不同的。在D&A,数据学家的角色已经从反应模型和A / B测试开发人员提升为积极的合作伙伴,他们会思考工作的意义。我们的数据科学团队已经变成了直接与工程师、设计师和产品经理合作的团队。

 

当设计符合科学

 

在塑造体验的时候,我们会利用thick data和定性信息,来思考对人们生活的洞察(注:thick data为定性研究的相关信息,参见: 为什么大数据需要thick data),大数据来源于数以百万计的人的行为数据集合以及每个个体生成的“small data”。

传统上,设计师专注于从服务、功能或产品来定义体验。他们将这个概念融入到与之相关的更大的生态系统中。数据学家开发的算法将支持这种体验,并通过A / B测试进行评估。

在D&A工作的头几周里,我发现设计师和数据学家经常陷入僵持的交流中,这种交流通常听起来像这样:

设计师:你好!你的数据和算法可以告诉我什么?

数据学家:呃…你想知道什么?

产生这种情况的主要问题是缺乏对彼此的实践和目标的共同理解。例如,设计师将情境转化为一种体验形式。数据学家将数据和模型的内容转化为知识。设计师经常采用可以适应不断变化的环境和评估方式的设计路径。数据学家则倾向于采用类似于中心设计的方式,这种方式机械性更强但是灵活性更差。他们会严格遵循科学方法,认为这个方法是一个不断改良的循环过程。

一个恰当的研究问题有助于定义在原型阶段产生的假设和模型类型。这些模型是在产品得以上线生产之前建立起来的评估算法,我们称之为“data engine-数据引擎”。每当“数据引擎”所支持的体验没有达到预期的效果时,就需要经历一个重新构建问题、继续不断细化的循环过程。         

数据科学方法及其持续评价和细化的循环过程


接触点

 

科学的方法和任何设计方法一样,形成、作出新的评估与推进新的迭代都是必要的。然而,这不是一个开放式的过程。它有一个明确的开始和结束,但没有明确的时间表。数据学家Neal Lathia认为,“ 跨学科的工作很难,直到你们使用同一种语言 ”。另外,我相信设计师和数据学家必须沉浸在对方的实践中才能建立一个共同的节奏。到目前为止,我已经为设计师和数据科学家编写了几个重要的接触点,以便为算法提供有意义的用户体验。它们是:

1、共同创造包含优先事项、目标和范围的体验及解决方案的切实可行的构想

通过定量调查、桌面研究和实地调研的洞察评估任何假设;

2、从愿景和研究中阐明关键问题。这些问题可以是:团队是否提出正确的问题;算法是否可以提供可操作的解决方案

3、了解给出解决方案的数据模型的所有局限性;

4、指定一个理想体验的成功指标,并在测试发布之前对其进行验证评估。验证阶段作为项目的完成点,并且必须将其定义为项目目标的一部分(例如,将建议召回率提高5%,检测到85%的将要违约的客户);

5、评估“数据引擎”对用户体验的影响。正如Neal Lathia指出的那样,数据学家对算法进行“离线”操作,并且评估与实际用户体验提升相关的改进是非常困难的。

这种相互作用的协作表明了我正在试图阐明的一种新型设计。青蛙设计的CEO-Harry West在最新的一篇文章中提出了“系统行为设计”一词:

“以人为本的设计已经从对象设计(工业设计)扩展到体验设计(增强交互设计,视觉设计和空间设计),下一步将是系统行为设计:决定自动化或智能系统行为的算法的设计“

—— Harry West

 

愿景驱动的协作关系 

 

到目前为止,我认为“生活体验”是数据科学与设计的交汇点。对于设计师和数据学家来说,不可缺少的第一步是建立一个切实的愿景和结果(如体验、解决方案、优先事项、目标、范围和可行性意识)。Airbnb的产品总监Jonathan Golden称这是一种以愿景驱动的产品管理方法:

“公司的愿景就是你想要这个世界看起来像五年后的样子。团队协作将帮助公司实现这个目标。“

—— Jonathan Golden

然而,这个概念化阶段要求的愿景呈现不仅仅是在董事会议上播放一个完美的ppt。因此,我的方法是聘请设计/科学合作伙伴来设计。它与亚马逊的CTO—— Werner Vogels所描述的“ Working Backwards”相似:

“从客户出发,并“向后”推演工作,直到你实现以最低的技术要求实现想要达到的目标。我们的目标是通过持续、明确的客户关注点来推动精简化。“

—— Werner Vogels

通过与设计故事相结合的思考,创造一种具有潜在未来的技术来述说现在。由 Futures Cones和Matt Jones开发的图表:《跳到最后——实用设计说明》

设计故事的目的是使以下方面有形化,包括:技术的变革,设计语言、仪式、特别的时刻、挫折、“离线体验”等。它有助于项目的不同利益相关者参与到重要问题中,以了解目标体验意味着什么以及团队为什么要共同构建这个体验。购买下一代Garden Sensor有什么意义?你能用它做什么?你不被允许做什么?你不会再做什么?首次使用人们如何和这个技术进行交互?然后在一个月,一年或者更长的时间里如何进行常规的交互?具有创造性和切实可行的解决办法可能会在项目开始之前就产生,有时甚至从创建虚拟的客户评论、用户手册、新闻稿、广告就开始了。这些材料是将未来带到现实的一种方式,抑或是我们所说的“近未来实验室”:

“设计故事充当了讨论和评估变革的依据,这种变化可能会改变人们所期望的愿景和必要的计划。”

在D&A,这意味着我通过为数据学家和设计师的研究创建一个切实可行的愿景来将他们紧密团结起来。首先,我们列出正在进行的调研问题。

然后,我们将他们的演变映射到2-3次迭代中,以便了解:技术未来会是什么样的?它可以在什么场景下使用?谁会使用它,以及会是什么类型的体验?每个参与者通过讲故事的方式,使用虚构的广告模板来讲述他们的解决方案。最后将它们归类为未来的概念。

我们收集所有的材料,并推广最有前景的概念。之后,我们在内部通过一系列的文章和视频广告来分享这些成果,这些文章和广告会从我们的观点(可能的)和用户的观点(理想的)来描述体验的主要特点、属性以及性质。

这种类型的虚构材料可以让设计师和数据学家感受并获得对技术和体验的直观理解。这些成果有助于建立声誉、争取支持、回应质疑、创造动力和分享共同的愿景。最后,不同观点的人的反馈可以帮助预测机遇和挑战。

 

设计特点

 

在这篇文章中,我认为,随着机器学习和“人工智能”的发展,设计师和数据学家都有责任理解如何塑造改善生活的体验。或者正如Greg Borenstein在《 向用户致敬:一个未知的研究群体如何掌握关键技术来使用人工智能解决真正人类问题》一文中指出的:

“广泛应用AI首先需要理解如何构建用户界面,从而将这些系统的强大功能交付给用户。”

—— Greg Borenstein

这种系统行为设计代表了以人为中心设计变革的未来。到目前为止,我在机器学习时代创造有意义的体验的过程中,发现其具有以下特征:

反馈: 数据是行为学习系统的用户体验的生命线。通过精心设计的反馈循环机制,确保系统得到适当的数据补充。

关系: 数据和学习算法的结合可以引发多种体验的变革。定义人与机器之间的关系,例如创造符合人们兴趣的习惯、找到已知的未知、发现未知的未知、传达某种内心平和的状态、或者重视时间高效利用。此外,当事情开始变得令人失望、尴尬、烦恼、停止工作或有用时,准备“离线体验”的关键时刻就到来了。

Seamfulness(有缝性) :考虑将算法的能力和缺陷作为体验的一部分。例如,预测与通知不同,设计者必须考虑预测中的不确定性将如何支撑用户行为。

 

原文链接:https://medium.com/@girardin/experience-design-in-the-machine-learning-era-e16c87f4f2e2

译文仅作学习用途,转载请注明:本文来自UXC原创翻译,如有其它用途请联系原作者。

]]>
人与机器之间的新关系

 

在上一篇文章中,我们会发现机器学习驱动的用户体验不是线性的,也不是基于静态的业务和设计规则的。它们会根据人类行为进行演变,并通过不断变化的数据模型进行更新。每件产品或服务仿佛都有生命一般,就像如谷歌的工作人员说的那样:“ 这是一项与众不同的工程”。我认为这也是一种与众不同的设计。例如,亚马逊将Echo定义为一台“随着时间的推移不断学习和增加更多功能”的设备,这个描述突出了我们需要为学习人类行为的用户体验系统进行设计的必要性。

机器学习的设计。图像来源:Mike Kuniavsky《物联网的预测行为的用户体验》

因此,除了要考虑首次接触和使用产品的体验之外,对于这类产品或服务而言,设计师同时需要考虑在使用1小时、1天、1年等时间后的体验。从Edyn花园传感器的宣传视频中,我们可以发现体验随着时间的不断演变:从建立照料花园的新习惯,到显示植物的未知情况,再到表达出对关键指标的信任,最后到保证一定程度的自动化灌溉时的高效时间利用。(注:此处的解释与上一篇文章中提到的机器学习时代用户体验的设计法则内容相呼应)

 

在设计这类数据产品时,设计师需要考虑各种情况,不光要考虑产品对人有用的情况,同时也要考虑到那些令人失望、尴尬、烦恼或停止工作等情况。“离线体验(offboarding experience)”的设计可能与“在线体验(onboarding experience)”一样重要。例如,据称有三分之一的Fitbit用户在6个月内停止佩戴该设备。这些数百万个被遗弃的设备会发生什么?它们生成的个人的数据会发生什么变化?有什么机会在不同的用户体验中使用它们?

通过不断向算法提供行为数据来实现演变的产品(例如Fitbit)是不可避免地走向灭亡的产品。资料来源:数据产品的生死攸关。另请参阅Megan Erin Miller《了解服务体验的生命周期》

有一种新的方式可以解决在数字与产品分离之后的出现的问题。数字服务工作在日益庞大的生态系统上,但用户数据往往呈现中心化的趋势。试想一下云信用的概念,它允许人们使用基于与另一种服务之间的关系的来使用其它服务。(注:芝麻信用的模式)

展望不久的将来,自然语言处理、知识表达、语音识别和自然语言生成方面的最新突破可以与机器建立更微妙和更强的关系。在几次迭代中,亚马逊的Echo可能会变得更加智能。人类学家Genevieve Bell预言了一种潜在演变:在AI的下一个浪潮中,人机交互到人机关系的转变是根植于人类文化和历史之中的:

“目前的人机交互框架并不是关于推荐系统的(目前大多数AI产品是这样做),但从根本上来说,其实是关于教育和关怀。如果现在的产品能够向这两个方向靠拢,那么我们就会处在一个从讨论人机交互转向人机关系的非常有趣的时刻。”

—— Genevieve Bell

在本节中,我们会发现算法已经逐渐融入到我们的日常生活中,数据为不断演变的关系提供了支撑。这种演变需要设计师和数学家之间密切合作。

 

设计师和数据学家之间的合作关系

 

通过目前的工作经验来展望数据和算法的用户体验,我发现它跟目前以人为中心的设计的做法是不同的。在D&A,数据学家的角色已经从反应模型和A / B测试开发人员提升为积极的合作伙伴,他们会思考工作的意义。我们的数据科学团队已经变成了直接与工程师、设计师和产品经理合作的团队。

 

当设计符合科学

 

在塑造体验的时候,我们会利用thick data和定性信息,来思考对人们生活的洞察(注:thick data为定性研究的相关信息,参见: 为什么大数据需要thick data),大数据来源于数以百万计的人的行为数据集合以及每个个体生成的“small data”。

传统上,设计师专注于从服务、功能或产品来定义体验。他们将这个概念融入到与之相关的更大的生态系统中。数据学家开发的算法将支持这种体验,并通过A / B测试进行评估。

在D&A工作的头几周里,我发现设计师和数据学家经常陷入僵持的交流中,这种交流通常听起来像这样:

设计师:你好!你的数据和算法可以告诉我什么?

数据学家:呃…你想知道什么?

产生这种情况的主要问题是缺乏对彼此的实践和目标的共同理解。例如,设计师将情境转化为一种体验形式。数据学家将数据和模型的内容转化为知识。设计师经常采用可以适应不断变化的环境和评估方式的设计路径。数据学家则倾向于采用类似于中心设计的方式,这种方式机械性更强但是灵活性更差。他们会严格遵循科学方法,认为这个方法是一个不断改良的循环过程。

一个恰当的研究问题有助于定义在原型阶段产生的假设和模型类型。这些模型是在产品得以上线生产之前建立起来的评估算法,我们称之为“data engine-数据引擎”。每当“数据引擎”所支持的体验没有达到预期的效果时,就需要经历一个重新构建问题、继续不断细化的循环过程。         

数据科学方法及其持续评价和细化的循环过程


接触点

 

科学的方法和任何设计方法一样,形成、作出新的评估与推进新的迭代都是必要的。然而,这不是一个开放式的过程。它有一个明确的开始和结束,但没有明确的时间表。数据学家Neal Lathia认为,“ 跨学科的工作很难,直到你们使用同一种语言 ”。另外,我相信设计师和数据学家必须沉浸在对方的实践中才能建立一个共同的节奏。到目前为止,我已经为设计师和数据科学家编写了几个重要的接触点,以便为算法提供有意义的用户体验。它们是:

1、共同创造包含优先事项、目标和范围的体验及解决方案的切实可行的构想

通过定量调查、桌面研究和实地调研的洞察评估任何假设;

2、从愿景和研究中阐明关键问题。这些问题可以是:团队是否提出正确的问题;算法是否可以提供可操作的解决方案

3、了解给出解决方案的数据模型的所有局限性;

4、指定一个理想体验的成功指标,并在测试发布之前对其进行验证评估。验证阶段作为项目的完成点,并且必须将其定义为项目目标的一部分(例如,将建议召回率提高5%,检测到85%的将要违约的客户);

5、评估“数据引擎”对用户体验的影响。正如Neal Lathia指出的那样,数据学家对算法进行“离线”操作,并且评估与实际用户体验提升相关的改进是非常困难的。

这种相互作用的协作表明了我正在试图阐明的一种新型设计。青蛙设计的CEO-Harry West在最新的一篇文章中提出了“系统行为设计”一词:

“以人为本的设计已经从对象设计(工业设计)扩展到体验设计(增强交互设计,视觉设计和空间设计),下一步将是系统行为设计:决定自动化或智能系统行为的算法的设计“

—— Harry West

 

愿景驱动的协作关系 

 

到目前为止,我认为“生活体验”是数据科学与设计的交汇点。对于设计师和数据学家来说,不可缺少的第一步是建立一个切实的愿景和结果(如体验、解决方案、优先事项、目标、范围和可行性意识)。Airbnb的产品总监Jonathan Golden称这是一种以愿景驱动的产品管理方法:

“公司的愿景就是你想要这个世界看起来像五年后的样子。团队协作将帮助公司实现这个目标。“

—— Jonathan Golden

然而,这个概念化阶段要求的愿景呈现不仅仅是在董事会议上播放一个完美的ppt。因此,我的方法是聘请设计/科学合作伙伴来设计。它与亚马逊的CTO—— Werner Vogels所描述的“ Working Backwards”相似:

“从客户出发,并“向后”推演工作,直到你实现以最低的技术要求实现想要达到的目标。我们的目标是通过持续、明确的客户关注点来推动精简化。“

—— Werner Vogels

通过与设计故事相结合的思考,创造一种具有潜在未来的技术来述说现在。由 Futures Cones和Matt Jones开发的图表:《跳到最后——实用设计说明》

设计故事的目的是使以下方面有形化,包括:技术的变革,设计语言、仪式、特别的时刻、挫折、“离线体验”等。它有助于项目的不同利益相关者参与到重要问题中,以了解目标体验意味着什么以及团队为什么要共同构建这个体验。购买下一代Garden Sensor有什么意义?你能用它做什么?你不被允许做什么?你不会再做什么?首次使用人们如何和这个技术进行交互?然后在一个月,一年或者更长的时间里如何进行常规的交互?具有创造性和切实可行的解决办法可能会在项目开始之前就产生,有时甚至从创建虚拟的客户评论、用户手册、新闻稿、广告就开始了。这些材料是将未来带到现实的一种方式,抑或是我们所说的“近未来实验室”:

“设计故事充当了讨论和评估变革的依据,这种变化可能会改变人们所期望的愿景和必要的计划。”

在D&A,这意味着我通过为数据学家和设计师的研究创建一个切实可行的愿景来将他们紧密团结起来。首先,我们列出正在进行的调研问题。

然后,我们将他们的演变映射到2-3次迭代中,以便了解:技术未来会是什么样的?它可以在什么场景下使用?谁会使用它,以及会是什么类型的体验?每个参与者通过讲故事的方式,使用虚构的广告模板来讲述他们的解决方案。最后将它们归类为未来的概念。

我们收集所有的材料,并推广最有前景的概念。之后,我们在内部通过一系列的文章和视频广告来分享这些成果,这些文章和广告会从我们的观点(可能的)和用户的观点(理想的)来描述体验的主要特点、属性以及性质。

这种类型的虚构材料可以让设计师和数据学家感受并获得对技术和体验的直观理解。这些成果有助于建立声誉、争取支持、回应质疑、创造动力和分享共同的愿景。最后,不同观点的人的反馈可以帮助预测机遇和挑战。

 

设计特点

 

在这篇文章中,我认为,随着机器学习和“人工智能”的发展,设计师和数据学家都有责任理解如何塑造改善生活的体验。或者正如Greg Borenstein在《 向用户致敬:一个未知的研究群体如何掌握关键技术来使用人工智能解决真正人类问题》一文中指出的:

“广泛应用AI首先需要理解如何构建用户界面,从而将这些系统的强大功能交付给用户。”

—— Greg Borenstein

这种系统行为设计代表了以人为中心设计变革的未来。到目前为止,我在机器学习时代创造有意义的体验的过程中,发现其具有以下特征:

反馈: 数据是行为学习系统的用户体验的生命线。通过精心设计的反馈循环机制,确保系统得到适当的数据补充。

关系: 数据和学习算法的结合可以引发多种体验的变革。定义人与机器之间的关系,例如创造符合人们兴趣的习惯、找到已知的未知、发现未知的未知、传达某种内心平和的状态、或者重视时间高效利用。此外,当事情开始变得令人失望、尴尬、烦恼、停止工作或有用时,准备“离线体验”的关键时刻就到来了。

Seamfulness(有缝性) :考虑将算法的能力和缺陷作为体验的一部分。例如,预测与通知不同,设计者必须考虑预测中的不确定性将如何支撑用户行为。

 

原文链接:https://medium.com/@girardin/experience-design-in-the-machine-learning-era-e16c87f4f2e2

译文仅作学习用途,转载请注明:本文来自UXC原创翻译,如有其它用途请联系原作者。

]]>
0
<![CDATA[活跃粉丝数]]> http://www.udpwork.com/item/16656.html http://www.udpwork.com/item/16656.html#reviews Sat, 10 Feb 2018 10:32:46 +0800 Cat Chen http://www.udpwork.com/item/16656.html 在知乎我大概能感觉到粉丝总数不如月活粉(monthly active follower)、日活粉(daily active follower)重要。在我不怎么用知乎的时候,无论我有多少粉回答后都不能得到多少赞和评论,必须要我频繁回答问题一两个月后赞和评论才能跟上来。我觉得这是因为我的粉丝中的大部分都已经不活跃,所以基数大也没有用。只有不停地吸引新粉丝,才能把月活粉、日活粉质量提上去,然后才能看到赞和评论的明显改善。

因此我觉得各大网站显示一个粉丝数其实挺没有意思的,基数大可能看起来很有面子,但其实无法转化为任何东西因为不活跃的粉丝跟僵死粉本质上毫无区别。不过要计算月活粉、日活粉需要增加网站计算负担,估计大家都不会做。

做直播估计是间接测算月活粉的最好办法,如果观众出现在直播上那一定是活跃的,就算不是直播而是帖子这些粉丝应该也会乐意交互。当然好像我这么懒的,直播能不搞就不搞,所以还要再想个办法测算月活粉。
]]>
在知乎我大概能感觉到粉丝总数不如月活粉(monthly active follower)、日活粉(daily active follower)重要。在我不怎么用知乎的时候,无论我有多少粉回答后都不能得到多少赞和评论,必须要我频繁回答问题一两个月后赞和评论才能跟上来。我觉得这是因为我的粉丝中的大部分都已经不活跃,所以基数大也没有用。只有不停地吸引新粉丝,才能把月活粉、日活粉质量提上去,然后才能看到赞和评论的明显改善。

因此我觉得各大网站显示一个粉丝数其实挺没有意思的,基数大可能看起来很有面子,但其实无法转化为任何东西因为不活跃的粉丝跟僵死粉本质上毫无区别。不过要计算月活粉、日活粉需要增加网站计算负担,估计大家都不会做。

做直播估计是间接测算月活粉的最好办法,如果观众出现在直播上那一定是活跃的,就算不是直播而是帖子这些粉丝应该也会乐意交互。当然好像我这么懒的,直播能不搞就不搞,所以还要再想个办法测算月活粉。
]]>
0
<![CDATA[Docker 入门教程]]> http://www.udpwork.com/item/16655.html http://www.udpwork.com/item/16655.html#reviews Fri, 09 Feb 2018 05:53:27 +0800 阮一峰 http://www.udpwork.com/item/16655.html 2013年发布至今,Docker一直广受瞩目,被认为可能会改变软件行业。

但是,许多人并不清楚 Docker 到底是什么,要解决什么问题,好处又在哪里?本文就来详细解释,帮助大家理解它,还带有简单易懂的实例,教你如何将它用于日常开发。

一、环境配置的难题

软件开发最大的麻烦事之一,就是环境配置。用户计算机的环境都不相同,你怎么知道自家的软件,能在那些机器跑起来?

用户必须保证两件事:操作系统的设置,各种库和组件的安装。只有它们都正确,软件才能运行。举例来说,安装一个 Python 应用,计算机必须有 Python 引擎,还必须有各种依赖,可能还要配置环境变量。

如果某些老旧的模块与当前环境不兼容,那就麻烦了。开发者常常会说:"它在我的机器可以跑了"(It works on my machine),言下之意就是,其他机器很可能跑不了。

环境配置如此麻烦,换一台机器,就要重来一次,旷日费时。很多人想到,能不能从根本上解决问题,软件可以带环境安装?也就是说,安装的时候,把原始环境一模一样地复制过来。

二、虚拟机

虚拟机(virtual machine)就是带环境安装的一种解决方案。它可以在一种操作系统里面运行另一种操作系统,比如在 Windows 系统里面运行 Linux 系统。应用程序对此毫无感知,因为虚拟机看上去跟真实系统一模一样,而对于底层系统来说,虚拟机就是一个普通文件,不需要了就删掉,对其他部分毫无影响。

虽然用户可以通过虚拟机还原软件的原始环境。但是,这个方案有几个缺点。

(1)资源占用多

虚拟机会独占一部分内存和硬盘空间。它运行的时候,其他程序就不能使用这些资源了。哪怕虚拟机里面的应用程序,真正使用的内存只有 1MB,虚拟机依然需要几百 MB 的内存才能运行。

(2)冗余步骤多

虚拟机是完整的操作系统,一些系统级别的操作步骤,往往无法跳过,比如用户登录。

(3)启动慢

启动操作系统需要多久,启动虚拟机就需要多久。可能要等几分钟,应用程序才能真正运行。

三、Linux 容器

由于虚拟机存在这些缺点,Linux 发展出了另一种虚拟化技术:Linux 容器(Linux Containers,缩写为 LXC)。

Linux 容器不是模拟一个完整的操作系统,而是对进程进行隔离。 或者说,在正常进程的外面套了一个保护层。对于容器里面的进程来说,它接触到的各种资源都是虚拟的,从而实现与底层系统的隔离。

由于容器是进程级别的,相比虚拟机有很多优势。

(1)启动快

容器里面的应用,直接就是底层系统的一个进程,而不是虚拟机内部的进程。所以,启动容器相当于启动本机的一个进程,而不是启动一个操作系统,速度就快很多。

(2)资源占用少

容器只占用需要的资源,不占用那些没有用到的资源;虚拟机由于是完整的操作系统,不可避免要占用所有资源。另外,多个容器可以共享资源,虚拟机都是独享资源。

(3)体积小

容器只要包含用到的组件即可,而虚拟机是整个操作系统的打包,所以容器文件比虚拟机文件要小很多。

总之,容器有点像轻量级的虚拟机,能够提供虚拟化的环境,但是成本开销小得多。

四、Docker 是什么?

Docker 属于 Linux 容器的一种封装,提供简单易用的容器使用接口。 它是目前最流行的 Linux 容器解决方案。

Docker 将应用程序与该程序的依赖,打包在一个文件里面。运行这个文件,就会生成一个虚拟容器。程序在这个虚拟容器里运行,就好像在真实的物理机上运行一样。有了 Docker,就不用担心环境问题。

总体来说,Docker 的接口相当简单,用户可以方便地创建和使用容器,把自己的应用放入容器。容器还可以进行版本管理、复制、分享、修改,就像管理普通的代码一样。

五、Docker 的用途

Docker 的主要用途,目前有三大类。

(1)提供一次性的环境。 比如,本地测试他人的软件、持续集成的时候提供单元测试和构建的环境。

(2)提供弹性的云服务。 因为 Docker 容器可以随开随关,很适合动态扩容和缩容。

(3)组建微服务架构。 通过多个容器,一台机器可以跑多个服务,因此在本机就可以模拟出微服务架构。

六、Docker 的安装

Docker 是一个开源的商业产品,有两个版本:社区版(Community Edition,缩写为 CE)和企业版(Enterprise Edition,缩写为 EE)。企业版包含了一些收费服务,个人开发者一般用不到。下面的介绍都针对社区版。

Docker CE 的安装请参考官方文档。

安装完成后,运行下面的命令,验证是否安装成功。

$ docker version
# 或者
$ docker info

Docker 需要用户具有 sudo 权限,为了避免每次命令都输入sudo,可以把用户加入 Docker 用户组(官方文档)。

$ sudo usermod -aG docker $USER

Docker 是服务器----客户端架构。命令行运行docker命令的时候,需要本机有 Docker 服务。如果这项服务没有启动,可以用下面的命令启动(官方文档)。

# service 命令的用法
$ sudo service docker start

# systemctl 命令的用法
$ sudo systemctl start docker

六、image 文件

Docker 把应用程序及其依赖,打包在 image 文件里面。 只有通过这个文件,才能生成 Docker 容器。image 文件可以看作是容器的模板。Docker 根据 image 文件生成容器的实例。同一个 image 文件,可以生成多个同时运行的容器实例。

image 是二进制文件。实际开发中,一个 image 文件往往通过继承另一个 image 文件,加上一些个性化设置而生成。举例来说,你可以在 Ubuntu 的 image 基础上,往里面加入 Apache 服务器,形成你的 image。

# 列出本机的所有 image 文件。
$ docker image ls

# 删除 image 文件
$ docker image rm [imageName]

image 文件是通用的,一台机器的 image 文件拷贝到另一台机器,照样可以使用。一般来说,为了节省时间,我们应该尽量使用别人制作好的 image 文件,而不是自己制作。即使要定制,也应该基于别人的 image 文件进行加工,而不是从零开始制作。

为了方便共享,image 文件制作完成后,可以上传到网上的仓库。Docker 的官方仓库Docker Hub是最重要、最常用的 image 仓库。此外,出售自己制作的 image 文件也是可以的。

七、实例:hello world

下面,我们通过最简单的 image 文件"hello world",感受一下 Docker。

需要说明的是,国内连接 Docker 的官方仓库很慢,还会断线,需要将默认仓库改成国内的镜像网站,具体的修改方法在下一篇文章的第一节。有需要的朋友,可以先看一下。

首先,运行下面的命令,将 image 文件从仓库抓取到本地。

$ docker image pull library/hello-world

上面代码中,docker image pull是抓取 image 文件的命令。library/hello-world是 image 文件在仓库里面的位置,其中library是 image 文件所在的组,hello-world是 image 文件的名字。

由于 Docker 官方提供的 image 文件,都放在library组里面,所以它的是默认组,可以省略。因此,上面的命令可以写成下面这样。

$ docker image pull hello-world

抓取成功以后,就可以在本机看到这个 image 文件了。

$ docker image ls

现在,运行这个 image 文件。

$ docker container run hello-world

docker container run命令会从 image 文件,生成一个正在运行的容器实例。

注意,docker container run命令具有自动抓取 image 文件的功能。如果发现本地没有指定的 image 文件,就会从仓库自动抓取。因此,前面的docker image pull命令并不是必需的步骤。

如果运行成功,你会在屏幕上读到下面的输出。

$ docker container run hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.

... ...

输出这段提示以后,hello world就会停止运行,容器自动终止。

有些容器不会自动终止,因为提供的是服务。比如,安装运行 Ubuntu 的 image,就可以在命令行体验 Ubuntu 系统。

$ docker container run -it ubuntu bash

对于那些不会自动终止的容器,必须使用docker container kill命令手动终止。

$ docker container kill [containID]

八、容器文件

image 文件生成的容器实例,本身也是一个文件,称为容器文件。 也就是说,一旦容器生成,就会同时存在两个文件: image 文件和容器文件。而且关闭容器并不会删除容器文件,只是容器停止运行而已。

# 列出本机正在运行的容器
$ docker container ls

# 列出本机所有容器,包括终止运行的容器
$ docker container ls --all

上面命令的输出结果之中,包括容器的 ID。很多地方都需要提供这个 ID,比如上一节终止容器运行的docker container kill命令。

终止运行的容器文件,依然会占据硬盘空间,可以使用docker container rm命令删除。

$ docker container rm [containerID]

运行上面的命令之后,再使用docker container ls --all命令,就会发现被删除的容器文件已经消失了。

九、Dockerfile 文件

学会使用 image 文件以后,接下来的问题就是,如何可以生成 image 文件?如果你要推广自己的软件,势必要自己制作 image 文件。

这就需要用到 Dockerfile 文件。它是一个文本文件,用来配置 image。Docker 根据 该文件生成二进制的 image 文件。

下面通过一个实例,演示如何编写 Dockerfile 文件。

十、实例:制作自己的 Docker 容器

下面我以koa-demos项目为例,介绍怎么写 Dockerfile 文件,实现让用户在 Docker 容器里面运行 Koa 框架。

作为准备工作,请先下载源码

$ git clone https://github.com/ruanyf/koa-demos.git
$ cd koa-demos

10.1 编写 Dockerfile 文件

首先,在项目的根目录下,新建一个文本文件.dockerignore,写入下面的内容

.git
node_modules
npm-debug.log

上面代码表示,这三个路径要排除,不要打包进入 image 文件。如果你没有路径要排除,这个文件可以不新建。

然后,在项目的根目录下,新建一个文本文件 Dockerfile,写入下面的内容

FROM node:8.4
COPY . /app
WORKDIR /app
RUN npm install --registry=https://registry.npm.taobao.org
EXPOSE 3000

上面代码一共五行,含义如下。

  • FROM node:8.4:该 image 文件继承官方的 node image,冒号表示标签,这里标签是8.4,即8.4版本的 node。
  • COPY . /app:将当前目录下的所有文件(除了.dockerignore排除的路径),都拷贝进入 image 文件的/app目录。
  • WORKDIR /app:指定接下来的工作路径为/app。
  • RUN npm install:在/app目录下,运行npm install命令安装依赖。注意,安装后所有的依赖,都将打包进入 image 文件。
  • EXPOSE 3000:将容器 3000 端口暴露出来, 允许外部连接这个端口。

10.2 创建 image 文件

有了 Dockerfile 文件以后,就可以使用docker image build命令创建 image 文件了。

$ docker image build -t koa-demo .
# 或者
$ docker image build -t koa-demo:0.0.1 .

上面代码中,-t参数用来指定 image 文件的名字,后面还可以用冒号指定标签。如果不指定,默认的标签就是latest。最后的那个点表示 Dockerfile 文件所在的路径,上例是当前路径,所以是一个点。

如果运行成功,就可以看到新生成的 image 文件koa-demo了。

$ docker image ls

10.3 生成容器

docker container run命令会从 image 文件生成容器。

$ docker container run -p 8000:3000 -it koa-demo /bin/bash
# 或者
$ docker container run -p 8000:3000 -it koa-demo:0.0.1 /bin/bash

上面命令的各个参数含义如下:

  • -p参数:容器的 3000 端口映射到本机的 8000 端口。
  • -it参数:容器的 Shell 映射到当前的 Shell,然后你在本机窗口输入的命令,就会传入容器。
  • koa-demo:0.0.1:image 文件的名字(如果有标签,还需要提供标签,默认是 latest 标签)。
  • /bin/bash:容器启动以后,内部第一个执行的命令。这里是启动 Bash,保证用户可以使用 Shell。

如果一切正常,运行上面的命令以后,就会返回一个命令行提示符。

root@66d80f4aaf1e:/app#

这表示你已经在容器里面了,返回的提示符就是容器内部的 Shell 提示符。执行下面的命令。

root@66d80f4aaf1e:/app# node demos/01.js

这时,Koa 框架已经运行起来了。打开本机的浏览器,访问 http://127.0.0.1:8000,网页显示"Not Found",这是因为这个demo没有写路由。

这个例子中,Node 进程运行在 Docker 容器的虚拟环境里面,进程接触到的文件系统和网络接口都是虚拟的,与本机的文件系统和网络接口是隔离的,因此需要定义容器与物理机的端口映射(map)。

现在,在容器的命令行,按下 Ctrl + c 停止 Node 进程,然后按下 Ctrl + d (或者输入 exit)退出容器。此外,也可以用docker container kill终止容器运行。

# 在本机的另一个终端窗口,查出容器的 ID
$ docker container ls

# 停止指定的容器运行
$ docker container kill [containerID]

容器停止运行之后,并不会消失,用下面的命令删除容器文件。

# 查出容器的 ID
$ docker container ls --all

# 删除指定的容器文件
$ docker container rm [containerID]

也可以使用docker container run命令的--rm参数,在容器终止运行后自动删除容器文件。

$ docker container run --rm -p 8000:3000 -it koa-demo /bin/bash

10.4 CMD 命令

上一节的例子里面,容器启动以后,需要手动输入命令node demos/01.js。我们可以把这个命令写在 Dockerfile 里面,这样容器启动以后,这个命令就已经执行了,不用再手动输入了。

FROM node:8.4
COPY . /app
WORKDIR /app
RUN npm install --registry=https://registry.npm.taobao.org
EXPOSE 3000
CMD node demos/01.js

上面的 Dockerfile 里面,多了最后一行CMD node demos/01.js,它表示容器启动后自动执行node demos/01.js。

你可能会问,RUN命令与CMD命令的区别在哪里?简单说,RUN命令在 image 文件的构建阶段执行,执行结果都会打包进入 image 文件;CMD命令则是在容器启动后执行。另外,一个 Dockerfile 可以包含多个RUN命令,但是只能有一个CMD命令。

注意,指定了CMD命令以后,docker container run命令就不能附加命令了(比如前面的/bin/bash),否则它会覆盖CMD命令。现在,启动容器可以使用下面的命令。

$ docker container run --rm -p 8000:3000 -it koa-demo:0.0.1

10.5 发布 image 文件

容器运行成功后,就确认了 image 文件的有效性。这时,我们就可以考虑把 image 文件分享到网上,让其他人使用。

首先,去hub.docker.comcloud.docker.com注册一个账户。然后,用下面的命令登录。

$ docker login

接着,为本地的 image 标注用户名和版本。

$ docker image tag [imageName] [username]/[repository]:[tag]
# 实例
$ docker image tag koa-demos:0.0.1 ruanyf/koa-demos:0.0.1

也可以不标注用户名,重新构建一下 image 文件。

$ docker image build -t [username]/[repository]:[tag] .

最后,发布 image 文件。

$ docker image push [username]/[repository]:[tag]

发布成功以后,登录 hub.docker.com,就可以看到已经发布的 image 文件。

十一、其他有用的命令

docker 的主要用法就是上面这些,此外还有几个命令,也非常有用。

(1)docker container start

前面的docker container run命令是新建容器,每运行一次,就会新建一个容器。同样的命令运行两次,就会生成两个一模一样的容器文件。如果希望重复使用容器,就要使用docker container start命令,它用来启动已经生成、已经停止运行的容器文件。

$ docker container start [containerID]

(2)docker container stop

前面的docker container kill命令终止容器运行,相当于向容器里面的主进程发出 SIGKILL 信号。而docker container stop命令也是用来终止容器运行,相当于向容器里面的主进程发出 SIGTERM 信号,然后过一段时间再发出 SIGKILL 信号。

$ bash container stop [containerID]

这两个信号的差别是,应用程序收到 SIGTERM 信号以后,可以自行进行收尾清理工作,但也可以不理会这个信号。如果收到 SIGKILL 信号,就会强行立即终止,那些正在进行中的操作会全部丢失。

(3)docker container logs

docker container logs命令用来查看 docker 容器的输出,即容器里面 Shell 的标准输出。如果docker run命令运行容器的时候,没有使用-it参数,就要用这个命令查看输出。

$ docker container logs [containerID]

(4)docker container exec

docker container exec命令用于进入一个正在运行的 docker 容器。如果docker run命令运行容器的时候,没有使用-it参数,就要用这个命令进入容器。一旦进入了容器,就可以在容器的 Shell 执行命令了。

$ docker container exec -it [containerID] /bin/bash

(5)docker container cp

docker container cp命令用于从正在运行的 Docker 容器里面,将文件拷贝到本机。下面是拷贝到当前目录的写法。

$ docker container cp [containID]:[/path/to/file] .

非常感谢你一直读到了这里,这个系列还有下一篇,介绍如何使用 Docker 搭建真正的网站,欢迎继续阅读

(完)

文档信息

]]>
2013年发布至今,Docker一直广受瞩目,被认为可能会改变软件行业。

但是,许多人并不清楚 Docker 到底是什么,要解决什么问题,好处又在哪里?本文就来详细解释,帮助大家理解它,还带有简单易懂的实例,教你如何将它用于日常开发。

一、环境配置的难题

软件开发最大的麻烦事之一,就是环境配置。用户计算机的环境都不相同,你怎么知道自家的软件,能在那些机器跑起来?

用户必须保证两件事:操作系统的设置,各种库和组件的安装。只有它们都正确,软件才能运行。举例来说,安装一个 Python 应用,计算机必须有 Python 引擎,还必须有各种依赖,可能还要配置环境变量。

如果某些老旧的模块与当前环境不兼容,那就麻烦了。开发者常常会说:"它在我的机器可以跑了"(It works on my machine),言下之意就是,其他机器很可能跑不了。

环境配置如此麻烦,换一台机器,就要重来一次,旷日费时。很多人想到,能不能从根本上解决问题,软件可以带环境安装?也就是说,安装的时候,把原始环境一模一样地复制过来。

二、虚拟机

虚拟机(virtual machine)就是带环境安装的一种解决方案。它可以在一种操作系统里面运行另一种操作系统,比如在 Windows 系统里面运行 Linux 系统。应用程序对此毫无感知,因为虚拟机看上去跟真实系统一模一样,而对于底层系统来说,虚拟机就是一个普通文件,不需要了就删掉,对其他部分毫无影响。

虽然用户可以通过虚拟机还原软件的原始环境。但是,这个方案有几个缺点。

(1)资源占用多

虚拟机会独占一部分内存和硬盘空间。它运行的时候,其他程序就不能使用这些资源了。哪怕虚拟机里面的应用程序,真正使用的内存只有 1MB,虚拟机依然需要几百 MB 的内存才能运行。

(2)冗余步骤多

虚拟机是完整的操作系统,一些系统级别的操作步骤,往往无法跳过,比如用户登录。

(3)启动慢

启动操作系统需要多久,启动虚拟机就需要多久。可能要等几分钟,应用程序才能真正运行。

三、Linux 容器

由于虚拟机存在这些缺点,Linux 发展出了另一种虚拟化技术:Linux 容器(Linux Containers,缩写为 LXC)。

Linux 容器不是模拟一个完整的操作系统,而是对进程进行隔离。 或者说,在正常进程的外面套了一个保护层。对于容器里面的进程来说,它接触到的各种资源都是虚拟的,从而实现与底层系统的隔离。

由于容器是进程级别的,相比虚拟机有很多优势。

(1)启动快

容器里面的应用,直接就是底层系统的一个进程,而不是虚拟机内部的进程。所以,启动容器相当于启动本机的一个进程,而不是启动一个操作系统,速度就快很多。

(2)资源占用少

容器只占用需要的资源,不占用那些没有用到的资源;虚拟机由于是完整的操作系统,不可避免要占用所有资源。另外,多个容器可以共享资源,虚拟机都是独享资源。

(3)体积小

容器只要包含用到的组件即可,而虚拟机是整个操作系统的打包,所以容器文件比虚拟机文件要小很多。

总之,容器有点像轻量级的虚拟机,能够提供虚拟化的环境,但是成本开销小得多。

四、Docker 是什么?

Docker 属于 Linux 容器的一种封装,提供简单易用的容器使用接口。 它是目前最流行的 Linux 容器解决方案。

Docker 将应用程序与该程序的依赖,打包在一个文件里面。运行这个文件,就会生成一个虚拟容器。程序在这个虚拟容器里运行,就好像在真实的物理机上运行一样。有了 Docker,就不用担心环境问题。

总体来说,Docker 的接口相当简单,用户可以方便地创建和使用容器,把自己的应用放入容器。容器还可以进行版本管理、复制、分享、修改,就像管理普通的代码一样。

五、Docker 的用途

Docker 的主要用途,目前有三大类。

(1)提供一次性的环境。 比如,本地测试他人的软件、持续集成的时候提供单元测试和构建的环境。

(2)提供弹性的云服务。 因为 Docker 容器可以随开随关,很适合动态扩容和缩容。

(3)组建微服务架构。 通过多个容器,一台机器可以跑多个服务,因此在本机就可以模拟出微服务架构。

六、Docker 的安装

Docker 是一个开源的商业产品,有两个版本:社区版(Community Edition,缩写为 CE)和企业版(Enterprise Edition,缩写为 EE)。企业版包含了一些收费服务,个人开发者一般用不到。下面的介绍都针对社区版。

Docker CE 的安装请参考官方文档。

安装完成后,运行下面的命令,验证是否安装成功。

$ docker version
# 或者
$ docker info

Docker 需要用户具有 sudo 权限,为了避免每次命令都输入sudo,可以把用户加入 Docker 用户组(官方文档)。

$ sudo usermod -aG docker $USER

Docker 是服务器----客户端架构。命令行运行docker命令的时候,需要本机有 Docker 服务。如果这项服务没有启动,可以用下面的命令启动(官方文档)。

# service 命令的用法
$ sudo service docker start

# systemctl 命令的用法
$ sudo systemctl start docker

六、image 文件

Docker 把应用程序及其依赖,打包在 image 文件里面。 只有通过这个文件,才能生成 Docker 容器。image 文件可以看作是容器的模板。Docker 根据 image 文件生成容器的实例。同一个 image 文件,可以生成多个同时运行的容器实例。

image 是二进制文件。实际开发中,一个 image 文件往往通过继承另一个 image 文件,加上一些个性化设置而生成。举例来说,你可以在 Ubuntu 的 image 基础上,往里面加入 Apache 服务器,形成你的 image。

# 列出本机的所有 image 文件。
$ docker image ls

# 删除 image 文件
$ docker image rm [imageName]

image 文件是通用的,一台机器的 image 文件拷贝到另一台机器,照样可以使用。一般来说,为了节省时间,我们应该尽量使用别人制作好的 image 文件,而不是自己制作。即使要定制,也应该基于别人的 image 文件进行加工,而不是从零开始制作。

为了方便共享,image 文件制作完成后,可以上传到网上的仓库。Docker 的官方仓库Docker Hub是最重要、最常用的 image 仓库。此外,出售自己制作的 image 文件也是可以的。

七、实例:hello world

下面,我们通过最简单的 image 文件"hello world",感受一下 Docker。

需要说明的是,国内连接 Docker 的官方仓库很慢,还会断线,需要将默认仓库改成国内的镜像网站,具体的修改方法在下一篇文章的第一节。有需要的朋友,可以先看一下。

首先,运行下面的命令,将 image 文件从仓库抓取到本地。

$ docker image pull library/hello-world

上面代码中,docker image pull是抓取 image 文件的命令。library/hello-world是 image 文件在仓库里面的位置,其中library是 image 文件所在的组,hello-world是 image 文件的名字。

由于 Docker 官方提供的 image 文件,都放在library组里面,所以它的是默认组,可以省略。因此,上面的命令可以写成下面这样。

$ docker image pull hello-world

抓取成功以后,就可以在本机看到这个 image 文件了。

$ docker image ls

现在,运行这个 image 文件。

$ docker container run hello-world

docker container run命令会从 image 文件,生成一个正在运行的容器实例。

注意,docker container run命令具有自动抓取 image 文件的功能。如果发现本地没有指定的 image 文件,就会从仓库自动抓取。因此,前面的docker image pull命令并不是必需的步骤。

如果运行成功,你会在屏幕上读到下面的输出。

$ docker container run hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.

... ...

输出这段提示以后,hello world就会停止运行,容器自动终止。

有些容器不会自动终止,因为提供的是服务。比如,安装运行 Ubuntu 的 image,就可以在命令行体验 Ubuntu 系统。

$ docker container run -it ubuntu bash

对于那些不会自动终止的容器,必须使用docker container kill命令手动终止。

$ docker container kill [containID]

八、容器文件

image 文件生成的容器实例,本身也是一个文件,称为容器文件。 也就是说,一旦容器生成,就会同时存在两个文件: image 文件和容器文件。而且关闭容器并不会删除容器文件,只是容器停止运行而已。

# 列出本机正在运行的容器
$ docker container ls

# 列出本机所有容器,包括终止运行的容器
$ docker container ls --all

上面命令的输出结果之中,包括容器的 ID。很多地方都需要提供这个 ID,比如上一节终止容器运行的docker container kill命令。

终止运行的容器文件,依然会占据硬盘空间,可以使用docker container rm命令删除。

$ docker container rm [containerID]

运行上面的命令之后,再使用docker container ls --all命令,就会发现被删除的容器文件已经消失了。

九、Dockerfile 文件

学会使用 image 文件以后,接下来的问题就是,如何可以生成 image 文件?如果你要推广自己的软件,势必要自己制作 image 文件。

这就需要用到 Dockerfile 文件。它是一个文本文件,用来配置 image。Docker 根据 该文件生成二进制的 image 文件。

下面通过一个实例,演示如何编写 Dockerfile 文件。

十、实例:制作自己的 Docker 容器

下面我以koa-demos项目为例,介绍怎么写 Dockerfile 文件,实现让用户在 Docker 容器里面运行 Koa 框架。

作为准备工作,请先下载源码

$ git clone https://github.com/ruanyf/koa-demos.git
$ cd koa-demos

10.1 编写 Dockerfile 文件

首先,在项目的根目录下,新建一个文本文件.dockerignore,写入下面的内容

.git
node_modules
npm-debug.log

上面代码表示,这三个路径要排除,不要打包进入 image 文件。如果你没有路径要排除,这个文件可以不新建。

然后,在项目的根目录下,新建一个文本文件 Dockerfile,写入下面的内容

FROM node:8.4
COPY . /app
WORKDIR /app
RUN npm install --registry=https://registry.npm.taobao.org
EXPOSE 3000

上面代码一共五行,含义如下。

  • FROM node:8.4:该 image 文件继承官方的 node image,冒号表示标签,这里标签是8.4,即8.4版本的 node。
  • COPY . /app:将当前目录下的所有文件(除了.dockerignore排除的路径),都拷贝进入 image 文件的/app目录。
  • WORKDIR /app:指定接下来的工作路径为/app。
  • RUN npm install:在/app目录下,运行npm install命令安装依赖。注意,安装后所有的依赖,都将打包进入 image 文件。
  • EXPOSE 3000:将容器 3000 端口暴露出来, 允许外部连接这个端口。

10.2 创建 image 文件

有了 Dockerfile 文件以后,就可以使用docker image build命令创建 image 文件了。

$ docker image build -t koa-demo .
# 或者
$ docker image build -t koa-demo:0.0.1 .

上面代码中,-t参数用来指定 image 文件的名字,后面还可以用冒号指定标签。如果不指定,默认的标签就是latest。最后的那个点表示 Dockerfile 文件所在的路径,上例是当前路径,所以是一个点。

如果运行成功,就可以看到新生成的 image 文件koa-demo了。

$ docker image ls

10.3 生成容器

docker container run命令会从 image 文件生成容器。

$ docker container run -p 8000:3000 -it koa-demo /bin/bash
# 或者
$ docker container run -p 8000:3000 -it koa-demo:0.0.1 /bin/bash

上面命令的各个参数含义如下:

  • -p参数:容器的 3000 端口映射到本机的 8000 端口。
  • -it参数:容器的 Shell 映射到当前的 Shell,然后你在本机窗口输入的命令,就会传入容器。
  • koa-demo:0.0.1:image 文件的名字(如果有标签,还需要提供标签,默认是 latest 标签)。
  • /bin/bash:容器启动以后,内部第一个执行的命令。这里是启动 Bash,保证用户可以使用 Shell。

如果一切正常,运行上面的命令以后,就会返回一个命令行提示符。

root@66d80f4aaf1e:/app#

这表示你已经在容器里面了,返回的提示符就是容器内部的 Shell 提示符。执行下面的命令。

root@66d80f4aaf1e:/app# node demos/01.js

这时,Koa 框架已经运行起来了。打开本机的浏览器,访问 http://127.0.0.1:8000,网页显示"Not Found",这是因为这个demo没有写路由。

这个例子中,Node 进程运行在 Docker 容器的虚拟环境里面,进程接触到的文件系统和网络接口都是虚拟的,与本机的文件系统和网络接口是隔离的,因此需要定义容器与物理机的端口映射(map)。

现在,在容器的命令行,按下 Ctrl + c 停止 Node 进程,然后按下 Ctrl + d (或者输入 exit)退出容器。此外,也可以用docker container kill终止容器运行。

# 在本机的另一个终端窗口,查出容器的 ID
$ docker container ls

# 停止指定的容器运行
$ docker container kill [containerID]

容器停止运行之后,并不会消失,用下面的命令删除容器文件。

# 查出容器的 ID
$ docker container ls --all

# 删除指定的容器文件
$ docker container rm [containerID]

也可以使用docker container run命令的--rm参数,在容器终止运行后自动删除容器文件。

$ docker container run --rm -p 8000:3000 -it koa-demo /bin/bash

10.4 CMD 命令

上一节的例子里面,容器启动以后,需要手动输入命令node demos/01.js。我们可以把这个命令写在 Dockerfile 里面,这样容器启动以后,这个命令就已经执行了,不用再手动输入了。

FROM node:8.4
COPY . /app
WORKDIR /app
RUN npm install --registry=https://registry.npm.taobao.org
EXPOSE 3000
CMD node demos/01.js

上面的 Dockerfile 里面,多了最后一行CMD node demos/01.js,它表示容器启动后自动执行node demos/01.js。

你可能会问,RUN命令与CMD命令的区别在哪里?简单说,RUN命令在 image 文件的构建阶段执行,执行结果都会打包进入 image 文件;CMD命令则是在容器启动后执行。另外,一个 Dockerfile 可以包含多个RUN命令,但是只能有一个CMD命令。

注意,指定了CMD命令以后,docker container run命令就不能附加命令了(比如前面的/bin/bash),否则它会覆盖CMD命令。现在,启动容器可以使用下面的命令。

$ docker container run --rm -p 8000:3000 -it koa-demo:0.0.1

10.5 发布 image 文件

容器运行成功后,就确认了 image 文件的有效性。这时,我们就可以考虑把 image 文件分享到网上,让其他人使用。

首先,去hub.docker.comcloud.docker.com注册一个账户。然后,用下面的命令登录。

$ docker login

接着,为本地的 image 标注用户名和版本。

$ docker image tag [imageName] [username]/[repository]:[tag]
# 实例
$ docker image tag koa-demos:0.0.1 ruanyf/koa-demos:0.0.1

也可以不标注用户名,重新构建一下 image 文件。

$ docker image build -t [username]/[repository]:[tag] .

最后,发布 image 文件。

$ docker image push [username]/[repository]:[tag]

发布成功以后,登录 hub.docker.com,就可以看到已经发布的 image 文件。

十一、其他有用的命令

docker 的主要用法就是上面这些,此外还有几个命令,也非常有用。

(1)docker container start

前面的docker container run命令是新建容器,每运行一次,就会新建一个容器。同样的命令运行两次,就会生成两个一模一样的容器文件。如果希望重复使用容器,就要使用docker container start命令,它用来启动已经生成、已经停止运行的容器文件。

$ docker container start [containerID]

(2)docker container stop

前面的docker container kill命令终止容器运行,相当于向容器里面的主进程发出 SIGKILL 信号。而docker container stop命令也是用来终止容器运行,相当于向容器里面的主进程发出 SIGTERM 信号,然后过一段时间再发出 SIGKILL 信号。

$ bash container stop [containerID]

这两个信号的差别是,应用程序收到 SIGTERM 信号以后,可以自行进行收尾清理工作,但也可以不理会这个信号。如果收到 SIGKILL 信号,就会强行立即终止,那些正在进行中的操作会全部丢失。

(3)docker container logs

docker container logs命令用来查看 docker 容器的输出,即容器里面 Shell 的标准输出。如果docker run命令运行容器的时候,没有使用-it参数,就要用这个命令查看输出。

$ docker container logs [containerID]

(4)docker container exec

docker container exec命令用于进入一个正在运行的 docker 容器。如果docker run命令运行容器的时候,没有使用-it参数,就要用这个命令进入容器。一旦进入了容器,就可以在容器的 Shell 执行命令了。

$ docker container exec -it [containerID] /bin/bash

(5)docker container cp

docker container cp命令用于从正在运行的 Docker 容器里面,将文件拷贝到本机。下面是拷贝到当前目录的写法。

$ docker container cp [containID]:[/path/to/file] .

非常感谢你一直读到了这里,这个系列还有下一篇,介绍如何使用 Docker 搭建真正的网站,欢迎继续阅读

(完)

文档信息

]]>
0
<![CDATA[4 步教你写好商业化文案 - 读《爆款文案》]]> http://www.udpwork.com/item/16653.html http://www.udpwork.com/item/16653.html#reviews Thu, 08 Feb 2018 21:28:02 +0800 唐巧 http://www.udpwork.com/item/16653.html 最近读完了《爆款文案》,本书的作者是关健明,曾经是奥美的广告人,专职工作就是写商业化的文案。

好的文案和差的文案能差多少呢?我听过一些故事,也经历过一些故事,毫不夸张地说:效果差 10 倍不算多,有些能差接近 100 倍,可见文案的力量有多强大。

在《爆款文案》中,关健明将商业化的文案书写分成 4 步,这 4 步按照用户从感兴趣到下单的整个过程,每个环节都做了精心的准备,力图引导用户完成商品的下单。这 4 步按照顺序是:

  1. 标题抓人眼球
  2. 激发购买欲望
  3. 赢得读者信任
  4. 引导马上下单

接下来,我分别介绍一下书中对于这四步的详细阐述。

标题抓人眼球

在这个信息爆炸的时代,标题的重要性不言而喻。别管你的产品有多好,如果不能吸引用户点进来看,那么什么都是白搭。书中总结了起名的一些套路:

套路一:使用有「新闻感」的标题。比如:「2017NBA 全明星赛上场鞋照曝光,有 1 款今天 6 折!」。在使用这个套路时,得注意:

  • 树立新闻主角。比如故意「碰瓷」一些名人(比如高考状元),名企(比如苹果)。
  • 加入及时性词语。比如:今天。
  • 加入重大新闻常用词,如:全新、曝光、突破、发现、发明、蹿红、风靡。

套路二:好友对话。通过口语化的标题产生亲近感。比如:「他写微信软文赚了 1173 万元,愿意手把手教你文案秘籍—只在这周六!」,又比如:「恭喜你!在 25 岁前看到了这篇最最靠谱的眼霜评测!」。使用该套路,需要注意:

  • 通常要在标题中加入 “你” 这个词。
  • 所有书面语改为口语。
  • 加入惊叹词。

套路三:实用锦囊。让读者感觉到是满满的「干货」,比如:「新年礼物!拖延症晚期也能 1 年读完 100 本书」,又如:「你和老公总存不下钱?央礼理财专家给你 3 个建议」。使用该套路的注意事项是产生对比,这样才有货干的效果,所以需要:

  • 写出读者的苦恼。
  • 给出圆满解决方案(或解决后的效果)

套路四:惊喜优惠。比如「今天免邮!2.5 亿人在用的德国净水壶 半价 90 元」。在介绍优惠的时候,第一步不要着急报价,而是介绍产品卖点,因为用户本质上还是买有用的产品。然后再写明具体的低价政策。最后,要加上限时限量,营造一个稀缺感。

套路五:写意外故事。人们天生喜欢听故事,特别是意料之外的故事。要造成这种冲突,就需要先描述糟糕的开局,然后展现完美的结局。比如「大家都看不上的办法,他却用来挣了 1000 万」。

我自己的体会,要学习写标题,可以认真研究一下眯蒙的文章,它每篇文章都非常有冲突感,让人有非常强的点击欲望。

当然,我们也不能仅仅是标题党,所以接下来,我们看标题把用户引导点击之后,后面的页面应该怎么做。

激发购买欲望

《爆款文案》介绍了 6 个步骤来激发读者购买的欲望。

步骤一:感官占领。在文案中描述用户的眼睛、鼻子、耳朵、舌头、身体和心里的直接感受。这类似于产品经理还原用户场景一样,现场的感受都是细节和真实的,没有任何总结加工的成份。这种文案的写作方法:描述产品对感官的直接感受,把自己体验的过程记录下来,试图用充满激情的文案感染顾客。

步骤二:恐惧诉求。描述不用该产品的痛苦,比如洗碗机销售文案就可以讲人的时间花在洗碗这件事情上有多么无聊。该步骤的注意事项是:

  1. 不要激起逆反心理。最好说自己的恐惧,让别人感同身受,而不是直接说顾客的恐惧。
  2. 可以用几个简单的提问,帮助用户总结出恐惧或悲观的结论。

步骤三:对比竞品。通过对比让顾客认知到我们的优势。比如:

  1. 描述竞品的产品差(设计、功能、质量)
  2. 描述竞品的利益少(带给消费者的利益少,甚至可能某些情况下有坏处)

步骤四:描述使用场景。帮用户规划和设计好什么时候用我们的产品。

步骤五:畅销。讲我们的产品卖得多好。利用顾客的从众心理,描述我们的产品已经被大量用户认可。注意:

  • 描述畅销的时候也需要描述细节,要使用精确的数字以体现出真实性。
  • 如果是小众产品,可以描述局部热销的场景,营造出一种热销的现象。

步骤六:顾客证言。这个基本上每个电商页面都在使用。在使用时可以注意:

  • 证言要描述用户的核心顾虑。
  • 证言要有一些总结性的文字,帮助顾客理解。

在用了以上 6 步之后,顾客可能会想:「你描述的产品确实很吸引人,但是我为什么要相信你呢?万一你欺骗我呢?」这就需要接下来的要素。

赢得读者信任

如何让读者相信你没有欺骗他呢?文中介绍了 3 个方法。

方法一:权威转嫁。利用专家评价、行业大奖、权威认证。让用户对产品的质量产生信任。现在很多区块链项目找人背书,也是打的这个主意。不过用户对于专家、行业大奖、权威的了解并不一定清楚,所以需要介绍清楚这些权威的背景,这样才能真正产生信任感。

方法二:事实证明。这个步骤不能直接上数据,你上各种技术参数用户会是直接蒙逼的。所以需要把产品的核心质量,转化为客户能够容易理解的形式描述。比如卖纸的为了证明丝的韧性,在上面放 10 枚硬币。这样客户一下就能感受到质量了。

所以这个方法的步骤是:(1) 收集核心性能数据 (2)链接到熟悉的事物或者用各种实验来证明。

方法三:化解顾虑。淘宝电商常常使用的:包邮、货到付款、免费试用、15 天包退货。都是化解用户不信任的好办法。

好了,现在用户信任你讲的都是实话了,但是用户心里想的是:「我为什么现在一定得买呢?明天买不行吗?」一旦用户真的说服自己明天买,他明天十有八九都会忘记掉这件事情。所以,我们还需要引导用户马上下单!

引导马上下单

怎么引导马上下单呢?有 4 个办法。

方法一:价格锚点。通过找一些很贵的产品做对比,来凸显我们价格的便宜。用户买的不是便宜,而是一种占便宜的感觉。所以,你需要的是让用户感觉便宜,如果用户感觉不到便宜,即使你是真便宜,用户也不会买。

其实每个人对于价格的认知都是通过对比来的,给你商品让你猜价格,你可能猜得高,可能猜得低,但是不管你猜多少,你多比较几个商店的价格,就会觉得哪家相对实惠了。所以,设置价格锚点相当重要。

方法二:算帐。帮助用户算帐,把总价很高的产品,算得很便宜。

算帐可以使用「平摊法」:报名我们公司的斑马英语,相比线下 200 元以上的英语课程,我们每节课只需要 20 元。
算帐可以还可以使用「省钱法」:相对传统的产品,可以省多少钱,多少水,多少原料,多少时间等等。比如开新能源车,电费相比油费,每 10000 公里可以省 4000 元的费用。

方法三:正当消费。例如:为了上进、为了家人、为了健康。据调查,每个有孩子的家庭,把家庭收入的将近 1/3 都投入到孩子的教育当中。可见大家是多么把教育看作一个「正当消费」。而娱乐,通常都会被归为不正当消费,当收入不太高的时候,花太多钱心里总归有些愧疚感。

方法四:限时限量。让顾客产生稀缺感和机会损失感。所谓「机不可失,失不再来」,这是商家常见的套路。

小结

《爆款文案》一书将用户的购买过程拆解成:被标题吸引 -> 产生购买欲望 -> 赢得信任 -> 立即下单 这四个过程。每个过程,作者拆解出了一些关键的方法套路。

作者来自一线的营销经验,我认为还是非常有效的,推荐给大家。

]]>
最近读完了《爆款文案》,本书的作者是关健明,曾经是奥美的广告人,专职工作就是写商业化的文案。

好的文案和差的文案能差多少呢?我听过一些故事,也经历过一些故事,毫不夸张地说:效果差 10 倍不算多,有些能差接近 100 倍,可见文案的力量有多强大。

在《爆款文案》中,关健明将商业化的文案书写分成 4 步,这 4 步按照用户从感兴趣到下单的整个过程,每个环节都做了精心的准备,力图引导用户完成商品的下单。这 4 步按照顺序是:

  1. 标题抓人眼球
  2. 激发购买欲望
  3. 赢得读者信任
  4. 引导马上下单

接下来,我分别介绍一下书中对于这四步的详细阐述。

标题抓人眼球

在这个信息爆炸的时代,标题的重要性不言而喻。别管你的产品有多好,如果不能吸引用户点进来看,那么什么都是白搭。书中总结了起名的一些套路:

套路一:使用有「新闻感」的标题。比如:「2017NBA 全明星赛上场鞋照曝光,有 1 款今天 6 折!」。在使用这个套路时,得注意:

  • 树立新闻主角。比如故意「碰瓷」一些名人(比如高考状元),名企(比如苹果)。
  • 加入及时性词语。比如:今天。
  • 加入重大新闻常用词,如:全新、曝光、突破、发现、发明、蹿红、风靡。

套路二:好友对话。通过口语化的标题产生亲近感。比如:「他写微信软文赚了 1173 万元,愿意手把手教你文案秘籍—只在这周六!」,又比如:「恭喜你!在 25 岁前看到了这篇最最靠谱的眼霜评测!」。使用该套路,需要注意:

  • 通常要在标题中加入 “你” 这个词。
  • 所有书面语改为口语。
  • 加入惊叹词。

套路三:实用锦囊。让读者感觉到是满满的「干货」,比如:「新年礼物!拖延症晚期也能 1 年读完 100 本书」,又如:「你和老公总存不下钱?央礼理财专家给你 3 个建议」。使用该套路的注意事项是产生对比,这样才有货干的效果,所以需要:

  • 写出读者的苦恼。
  • 给出圆满解决方案(或解决后的效果)

套路四:惊喜优惠。比如「今天免邮!2.5 亿人在用的德国净水壶 半价 90 元」。在介绍优惠的时候,第一步不要着急报价,而是介绍产品卖点,因为用户本质上还是买有用的产品。然后再写明具体的低价政策。最后,要加上限时限量,营造一个稀缺感。

套路五:写意外故事。人们天生喜欢听故事,特别是意料之外的故事。要造成这种冲突,就需要先描述糟糕的开局,然后展现完美的结局。比如「大家都看不上的办法,他却用来挣了 1000 万」。

我自己的体会,要学习写标题,可以认真研究一下眯蒙的文章,它每篇文章都非常有冲突感,让人有非常强的点击欲望。

当然,我们也不能仅仅是标题党,所以接下来,我们看标题把用户引导点击之后,后面的页面应该怎么做。

激发购买欲望

《爆款文案》介绍了 6 个步骤来激发读者购买的欲望。

步骤一:感官占领。在文案中描述用户的眼睛、鼻子、耳朵、舌头、身体和心里的直接感受。这类似于产品经理还原用户场景一样,现场的感受都是细节和真实的,没有任何总结加工的成份。这种文案的写作方法:描述产品对感官的直接感受,把自己体验的过程记录下来,试图用充满激情的文案感染顾客。

步骤二:恐惧诉求。描述不用该产品的痛苦,比如洗碗机销售文案就可以讲人的时间花在洗碗这件事情上有多么无聊。该步骤的注意事项是:

  1. 不要激起逆反心理。最好说自己的恐惧,让别人感同身受,而不是直接说顾客的恐惧。
  2. 可以用几个简单的提问,帮助用户总结出恐惧或悲观的结论。

步骤三:对比竞品。通过对比让顾客认知到我们的优势。比如:

  1. 描述竞品的产品差(设计、功能、质量)
  2. 描述竞品的利益少(带给消费者的利益少,甚至可能某些情况下有坏处)

步骤四:描述使用场景。帮用户规划和设计好什么时候用我们的产品。

步骤五:畅销。讲我们的产品卖得多好。利用顾客的从众心理,描述我们的产品已经被大量用户认可。注意:

  • 描述畅销的时候也需要描述细节,要使用精确的数字以体现出真实性。
  • 如果是小众产品,可以描述局部热销的场景,营造出一种热销的现象。

步骤六:顾客证言。这个基本上每个电商页面都在使用。在使用时可以注意:

  • 证言要描述用户的核心顾虑。
  • 证言要有一些总结性的文字,帮助顾客理解。

在用了以上 6 步之后,顾客可能会想:「你描述的产品确实很吸引人,但是我为什么要相信你呢?万一你欺骗我呢?」这就需要接下来的要素。

赢得读者信任

如何让读者相信你没有欺骗他呢?文中介绍了 3 个方法。

方法一:权威转嫁。利用专家评价、行业大奖、权威认证。让用户对产品的质量产生信任。现在很多区块链项目找人背书,也是打的这个主意。不过用户对于专家、行业大奖、权威的了解并不一定清楚,所以需要介绍清楚这些权威的背景,这样才能真正产生信任感。

方法二:事实证明。这个步骤不能直接上数据,你上各种技术参数用户会是直接蒙逼的。所以需要把产品的核心质量,转化为客户能够容易理解的形式描述。比如卖纸的为了证明丝的韧性,在上面放 10 枚硬币。这样客户一下就能感受到质量了。

所以这个方法的步骤是:(1) 收集核心性能数据 (2)链接到熟悉的事物或者用各种实验来证明。

方法三:化解顾虑。淘宝电商常常使用的:包邮、货到付款、免费试用、15 天包退货。都是化解用户不信任的好办法。

好了,现在用户信任你讲的都是实话了,但是用户心里想的是:「我为什么现在一定得买呢?明天买不行吗?」一旦用户真的说服自己明天买,他明天十有八九都会忘记掉这件事情。所以,我们还需要引导用户马上下单!

引导马上下单

怎么引导马上下单呢?有 4 个办法。

方法一:价格锚点。通过找一些很贵的产品做对比,来凸显我们价格的便宜。用户买的不是便宜,而是一种占便宜的感觉。所以,你需要的是让用户感觉便宜,如果用户感觉不到便宜,即使你是真便宜,用户也不会买。

其实每个人对于价格的认知都是通过对比来的,给你商品让你猜价格,你可能猜得高,可能猜得低,但是不管你猜多少,你多比较几个商店的价格,就会觉得哪家相对实惠了。所以,设置价格锚点相当重要。

方法二:算帐。帮助用户算帐,把总价很高的产品,算得很便宜。

算帐可以使用「平摊法」:报名我们公司的斑马英语,相比线下 200 元以上的英语课程,我们每节课只需要 20 元。
算帐可以还可以使用「省钱法」:相对传统的产品,可以省多少钱,多少水,多少原料,多少时间等等。比如开新能源车,电费相比油费,每 10000 公里可以省 4000 元的费用。

方法三:正当消费。例如:为了上进、为了家人、为了健康。据调查,每个有孩子的家庭,把家庭收入的将近 1/3 都投入到孩子的教育当中。可见大家是多么把教育看作一个「正当消费」。而娱乐,通常都会被归为不正当消费,当收入不太高的时候,花太多钱心里总归有些愧疚感。

方法四:限时限量。让顾客产生稀缺感和机会损失感。所谓「机不可失,失不再来」,这是商家常见的套路。

小结

《爆款文案》一书将用户的购买过程拆解成:被标题吸引 -> 产生购买欲望 -> 赢得信任 -> 立即下单 这四个过程。每个过程,作者拆解出了一些关键的方法套路。

作者来自一线的营销经验,我认为还是非常有效的,推荐给大家。

]]>
0
<![CDATA[提高 lua 处理向量运算性能的一点尝试]]> http://www.udpwork.com/item/16623.html http://www.udpwork.com/item/16623.html#reviews Thu, 08 Feb 2018 11:42:49 +0800 云风 http://www.udpwork.com/item/16623.html 如果用纯 lua 来做向量/矩阵运算在性能要求很高的场合通常是不可接受的。但即使封装成 C 库,传统的方法也比较重。若把每个 vector 都封装为 userdata ,有效载荷很低。一个 float vector 4 ,本身只有 16 字节,而 userdata 本身需要额外 40 字节来维护;4 阶 float 矩阵也不过 64 字节。更不用说在向量运算过程中大量产生的临时对象所带来的 gc 负担了。

采用 lightuserdata 在内存额外开销方面会好一点点,但是生命期管理又会成为及其烦心的事。不像 C 中可以使用栈作临时储存,C++ 中有 RAII 。且使用 api 的时候也会变得比较繁琐。

我一度觉得在 lua 层面提供向量运算的基础模块是不是粒度太细了。曾经也想过许多方法来改善这方面。这两天实践了一下想了有一段时间的方案,感觉能初步满意。

我的应用场合是 3d game engine ,engine 计划用 ecs 框架搭建,这为向量运算模块的优化提供了不错的基础。至少在对象生命期管理方面有了 system/component 的约束,做起来会简单许多。

游戏引擎中用到向量运算的场合主要是两类,一类是处理某项事务时用到的大量临时对象,通常是 3x3 矩阵,vector3 和 vector4 ;另一类是引擎中的对象需要用矩阵记录下其空间状态。我认为,无论是 vector 还是 matrix ,虽然它们的数据尺寸比系统的字宽大,但它们依然应被视为值类型,而不是引用类型。但是由于 vector/matrix 的值长度大于语言支持的原生类型,无法直接在值类型的原生变量中放在,所以一般我们还是要借用某种引用语义。就好比 string 在 lua 中是值语义的,但使用引用类型的方式实现出来的。

也就是说,在设计库的时候,即使支持 A *= B 这样的运算,因为 A 和 B 都是按引用的方式实现的,但也并不是将 A * B 的结果直接覆盖到旧有的 A 引用的值空间上。这和 A = “hello" ; A = A .. " world" 一样,新的字符串 hello world 并没有覆盖已有的 hello 这个字符串。

lua 中的基础数据类型中,属于值类型可以用来做这个封装的有三种:number(id) ,lightuserdata 和 string 。我曾经想过直接用 string 将 vector/matrix 的值用 string.pack 打包,但细想一下并不比 userdata 好多少。lightuserdata 若是直接储存数据在内存中的地址的话,和 C 的裸指针没什么差别,用起来实在是不太放心。所以可选的就只有数字 id 了。

5.3 版以前的 lua 可以保存 52bit 有效精度的数字,5.3 版以后则在大多数平台上有 64bit 精度可用。用作索引 id 号的话绰绰有余。加上 id 这个间接层,我们可以很容易的识别出哪些无效 id ,比用指针安全很多。

因为游戏通常是按帧处理业务的,每帧之间没有特别的联系,如果在帧内没有特别的把某个值记下来,那么通常就不会再使用它了。我想,用帧号(版本号)+ 顺序数字 作为值对象的唯一 id ,即可以方便的索引到数据块(使用一大块连续内存数组即可),又能快速排除已经销毁的对象。构造新对象也是 O(1) 的操作,还可以用 O(1) 时间批量作废当前帧用到的所有临时对象。

即,任何向量运算操作,都产生新的值对象,一旦产生,就不再修改其值。每个对象用一个唯一数字 id 来表示。除非特别注明,一个值需要长期保留,这些对象的生存时间都不会长于一帧。每帧结束后,通过递增版本号的方式来让旧的临时 id 失效。

在运算操作方面,每个针对向量的一元或二元操作都增加一次 lua 到 C 的函数调用有时也显得重了。比如两个 vector4 相加,运算量不过是四次加法,而 lua 到 C 的函数调用则远大于此。我觉得借鉴 forth 的设计比较好。单独再设计一个数据栈,操作全部基于数据栈进行;如果设计复杂的操作流程,还可以增加指令栈(暂时还没用到,所以没有实现)。也就是把向量操作的相关操作都基于一个独立于 lua 语言本身的栈上去完成。比如两个矩阵相乘再去逆,可以写成 :

command ( mat1, mat2, "*~") 用来表示 ~ (mat1 * mat2)

也就是先把 mat1 和 mat2 两个 id 压栈, * 会弹出栈顶的两个对象做矩阵乘法,把结果压回。而随后的 ~ 取逆矩阵的操作则将栈顶的对象弹出,做逆矩阵运算,结果再放回去。

这样,一个 command 函数,通过若干参数,就可以完成一系列的运算操作。且连续的运算是通过一个字符串常量传递给 C 模块的,大大的减少了 lua 和 C 的交互次数。这些操作都是基于数据栈的,加上这个数据栈和 lua 本身的交互指令就完备了。

我暂时设计了一系列和数据栈操作有关的指令:

  • P 把栈顶元素弹出,并将 id 返回到 lua 中。
  • V 把栈顶元素弹出,并将数据内存地址以 lightuserdata 的形式返回到 lua 中。 用来传递到其它(比如渲染)模块,指针保证在当前帧有效。
  • T 把栈顶元素弹出,并将数据构造为一个 lua table ,方便调试、持久化等。
  • R 把栈顶元素移除 (不返回到 lua )。
  • D 复制栈顶元素。
  • M 把栈顶元素弹出,用其值构造一份新的持久化的值,并把新的持久化对象的 id 返回到 lua 。

操作这个运算模块只有一个函数,通过一些列不同的输入来完成操作。如果参数为 table 则把 table 的内容作为 vector 或 matrix 的值来构造出新的对象压入堆栈。

如果参数为数字,则当作过去构造出来的对象 id 压栈。其中,一个特别的设计是,如果这个 id 对应的是一个持久值(即不会在当前帧结束后失效),在这个 id 重新压会数据栈后,又会变成一个临时对象。这样,你可以放心的写

local a = command (a,b,"*M") -- 即 a = a * b 这个语义。

旧有的 a 即使是一个 M 值,也会被标记为失效。如果这里的 b 是一个持久化的常量,不希望临时回收,则可以采用另一个设定 :

local  a = command (a,-b,"*M") -- 当 id 为负的时候,可以返回压栈而不改变其持久化特性。

我昨天在实现的时候做了两个版本,前一个版本把持久化标记加在内部堆的 slot 上,但这样会导致堆空间不连续,每帧重利用的时候,要么加大了回收的负担,要么增加了新构造对象的负担。好处是做持久化标记代价很小。

后来我又实现了一版,把临时对象和持久化对象分开空间储存;临时对象用最简单的栈式连续内存分配,每帧结束复位一下栈顶指针即可。持久对象则用 freelist 管理。所有对象从外部入栈时都一定先放在临时区,用 M 指令转换到持久区,做一次值拷贝。而消除持久化,删除它只需要简单加到 wish freelist 里(按前文所述的规则),待帧结束后合并 freelist 和 wish freelist 两个链表即可,不必再做拷贝。

这块代码将来会随我们的游戏引擎开源并维护。暂时我把昨天一些初步的实现贴在了 gist 上,供参考。注:gist 上这个版本不会维护。

后续改进见:https://blog.codingnow.com/2018/02/linalg_improvement.html

]]>
如果用纯 lua 来做向量/矩阵运算在性能要求很高的场合通常是不可接受的。但即使封装成 C 库,传统的方法也比较重。若把每个 vector 都封装为 userdata ,有效载荷很低。一个 float vector 4 ,本身只有 16 字节,而 userdata 本身需要额外 40 字节来维护;4 阶 float 矩阵也不过 64 字节。更不用说在向量运算过程中大量产生的临时对象所带来的 gc 负担了。

采用 lightuserdata 在内存额外开销方面会好一点点,但是生命期管理又会成为及其烦心的事。不像 C 中可以使用栈作临时储存,C++ 中有 RAII 。且使用 api 的时候也会变得比较繁琐。

我一度觉得在 lua 层面提供向量运算的基础模块是不是粒度太细了。曾经也想过许多方法来改善这方面。这两天实践了一下想了有一段时间的方案,感觉能初步满意。

我的应用场合是 3d game engine ,engine 计划用 ecs 框架搭建,这为向量运算模块的优化提供了不错的基础。至少在对象生命期管理方面有了 system/component 的约束,做起来会简单许多。

游戏引擎中用到向量运算的场合主要是两类,一类是处理某项事务时用到的大量临时对象,通常是 3x3 矩阵,vector3 和 vector4 ;另一类是引擎中的对象需要用矩阵记录下其空间状态。我认为,无论是 vector 还是 matrix ,虽然它们的数据尺寸比系统的字宽大,但它们依然应被视为值类型,而不是引用类型。但是由于 vector/matrix 的值长度大于语言支持的原生类型,无法直接在值类型的原生变量中放在,所以一般我们还是要借用某种引用语义。就好比 string 在 lua 中是值语义的,但使用引用类型的方式实现出来的。

也就是说,在设计库的时候,即使支持 A *= B 这样的运算,因为 A 和 B 都是按引用的方式实现的,但也并不是将 A * B 的结果直接覆盖到旧有的 A 引用的值空间上。这和 A = “hello" ; A = A .. " world" 一样,新的字符串 hello world 并没有覆盖已有的 hello 这个字符串。

lua 中的基础数据类型中,属于值类型可以用来做这个封装的有三种:number(id) ,lightuserdata 和 string 。我曾经想过直接用 string 将 vector/matrix 的值用 string.pack 打包,但细想一下并不比 userdata 好多少。lightuserdata 若是直接储存数据在内存中的地址的话,和 C 的裸指针没什么差别,用起来实在是不太放心。所以可选的就只有数字 id 了。

5.3 版以前的 lua 可以保存 52bit 有效精度的数字,5.3 版以后则在大多数平台上有 64bit 精度可用。用作索引 id 号的话绰绰有余。加上 id 这个间接层,我们可以很容易的识别出哪些无效 id ,比用指针安全很多。

因为游戏通常是按帧处理业务的,每帧之间没有特别的联系,如果在帧内没有特别的把某个值记下来,那么通常就不会再使用它了。我想,用帧号(版本号)+ 顺序数字 作为值对象的唯一 id ,即可以方便的索引到数据块(使用一大块连续内存数组即可),又能快速排除已经销毁的对象。构造新对象也是 O(1) 的操作,还可以用 O(1) 时间批量作废当前帧用到的所有临时对象。

即,任何向量运算操作,都产生新的值对象,一旦产生,就不再修改其值。每个对象用一个唯一数字 id 来表示。除非特别注明,一个值需要长期保留,这些对象的生存时间都不会长于一帧。每帧结束后,通过递增版本号的方式来让旧的临时 id 失效。

在运算操作方面,每个针对向量的一元或二元操作都增加一次 lua 到 C 的函数调用有时也显得重了。比如两个 vector4 相加,运算量不过是四次加法,而 lua 到 C 的函数调用则远大于此。我觉得借鉴 forth 的设计比较好。单独再设计一个数据栈,操作全部基于数据栈进行;如果设计复杂的操作流程,还可以增加指令栈(暂时还没用到,所以没有实现)。也就是把向量操作的相关操作都基于一个独立于 lua 语言本身的栈上去完成。比如两个矩阵相乘再去逆,可以写成 :

command ( mat1, mat2, "*~") 用来表示 ~ (mat1 * mat2)

也就是先把 mat1 和 mat2 两个 id 压栈, * 会弹出栈顶的两个对象做矩阵乘法,把结果压回。而随后的 ~ 取逆矩阵的操作则将栈顶的对象弹出,做逆矩阵运算,结果再放回去。

这样,一个 command 函数,通过若干参数,就可以完成一系列的运算操作。且连续的运算是通过一个字符串常量传递给 C 模块的,大大的减少了 lua 和 C 的交互次数。这些操作都是基于数据栈的,加上这个数据栈和 lua 本身的交互指令就完备了。

我暂时设计了一系列和数据栈操作有关的指令:

  • P 把栈顶元素弹出,并将 id 返回到 lua 中。
  • V 把栈顶元素弹出,并将数据内存地址以 lightuserdata 的形式返回到 lua 中。 用来传递到其它(比如渲染)模块,指针保证在当前帧有效。
  • T 把栈顶元素弹出,并将数据构造为一个 lua table ,方便调试、持久化等。
  • R 把栈顶元素移除 (不返回到 lua )。
  • D 复制栈顶元素。
  • M 把栈顶元素弹出,用其值构造一份新的持久化的值,并把新的持久化对象的 id 返回到 lua 。

操作这个运算模块只有一个函数,通过一些列不同的输入来完成操作。如果参数为 table 则把 table 的内容作为 vector 或 matrix 的值来构造出新的对象压入堆栈。

如果参数为数字,则当作过去构造出来的对象 id 压栈。其中,一个特别的设计是,如果这个 id 对应的是一个持久值(即不会在当前帧结束后失效),在这个 id 重新压会数据栈后,又会变成一个临时对象。这样,你可以放心的写

local a = command (a,b,"*M") -- 即 a = a * b 这个语义。

旧有的 a 即使是一个 M 值,也会被标记为失效。如果这里的 b 是一个持久化的常量,不希望临时回收,则可以采用另一个设定 :

local  a = command (a,-b,"*M") -- 当 id 为负的时候,可以返回压栈而不改变其持久化特性。

我昨天在实现的时候做了两个版本,前一个版本把持久化标记加在内部堆的 slot 上,但这样会导致堆空间不连续,每帧重利用的时候,要么加大了回收的负担,要么增加了新构造对象的负担。好处是做持久化标记代价很小。

后来我又实现了一版,把临时对象和持久化对象分开空间储存;临时对象用最简单的栈式连续内存分配,每帧结束复位一下栈顶指针即可。持久对象则用 freelist 管理。所有对象从外部入栈时都一定先放在临时区,用 M 指令转换到持久区,做一次值拷贝。而消除持久化,删除它只需要简单加到 wish freelist 里(按前文所述的规则),待帧结束后合并 freelist 和 wish freelist 两个链表即可,不必再做拷贝。

这块代码将来会随我们的游戏引擎开源并维护。暂时我把昨天一些初步的实现贴在了 gist 上,供参考。注:gist 上这个版本不会维护。

后续改进见:https://blog.codingnow.com/2018/02/linalg_improvement.html

]]>
0
<![CDATA[Asch源码base模块基础之共识]]> http://www.udpwork.com/item/16654.html http://www.udpwork.com/item/16654.html#reviews Thu, 08 Feb 2018 00:00:00 +0800 yanyiwu http://www.udpwork.com/item/16654.html



Asch源码阅读:启动过程概述中,我们提到 init.js 初始化中的一个步骤就是初始化 base 模块。 在这里稍微展开谈谈 base 模块具体是什么,base 模块代码对应的目录是 ./src/base/ ,在此目录下主要有以下几个模块:

  • consensus.js
  • account.js
  • block.js
  • transaction.js

base 也是核心源码目录,但是可能容易会把 ./src/base/ 和 ./src/core/ 混淆,觉得为什么两个目录代码都是核心代码,但是为什么分开?

其实很简单区分,./src/base/ 主要是提供一些基础的操作函数,供其他模块调用,本身并不是事件触发,也没有任何事件回调函数。

而 ./src/core/ 的核心模块基本上都是事件触发,注册了各种 onBind, onNewBlock 之类的事件回调函数。

记住这点就不容易混淆了。

下面逐个谈谈具体模块负责的事情:

『consensus』

这个模块负责『共识』,或者叫一致性吧,其实很简单,就是让各个节点达成一致。而不分叉。共识是区块链的核心之一。

主要有以下几个功能函数:

「createVotes」

创建投票,先根据block的高度和id算出hash值, 然后使用当前节点配置里合法的受托人的密钥进行签名,一般一台服务端只配置一个受托人节点。 最后返回一个带有当前 block 高度,多个受托人签名的Votes 。 其实这里把 Votes 翻译成投票不是非常合理,更准确的说, 其实这个 Votes 就是受托人的记账,后续会在 block 生成的时候使用这个 Vote 去验证,包括新生成一个区块之后,广播到其他节点的时候需要广播区块信息和这个Vote信息给其他节点。 这样其他节点可以都可以验证这个 Vote 和 Block,确保 Block 的锻造过程是由合法的受托人通过正确的方式锻造出来的。

「hasEnoughVotes」

检查Votes是否包含足够多的受托人签名, 目前受托人人数是101,需要的签名至少 101 x 2 / 3 = 68 个受托人。 也就是说如果当时在线的受托人人数不满68人,则无法让这个区块链正常产块。

「hasEnoughVotesRemote」

Votes需要包含至少6个受托人签名才行。这个判断比上面那个 hasEnoughVotes 要求更轻一些。

「setPendingBlock」

要理解这个函数其实需要先理解 block 的锻造过程(core/blocks.js),

在block的锻造过程中,首先会判断 hasEnoughVotes 是否有足够的受托人签名(如上所述,所需受托人签名至少68人),

但是一般一个受托人节点,只会有一个受托人密钥,那其实是在锻造的时候 是不满足 >= 68 个签名的要求的, 所以会先把锻造的 block 先寄存起来,也就是 setPendingBlock 寄存起来。

然后会 createPropose 并广播到其他节点, 其他节点收到 Propose 之后会调用它们的 createVotes 生成它们的 Votes, 然后把它们的 votes 发送回来给 propose 发起者的 ip 。 这样,发起这个 Propose 的节点会收到其他节点的 votes, 也就能集齐超过 68 个受托人的 signatures, 就有足够的 signatures , 则满足 hasEnoughVotes 的条件, 然后再把这个 PendingBlock 取出来,然后真正完成这个block的区块锻造。

这个过程是打成共识的一个过程,但是在达成共识的等待时间里面, 需要这么一个 setPendingBlock 存储中间结果的过程。

「createPropose」

如上所述,当本节点发现自己受托人签名不满足锻造区块所需签名数量的时候, 会通过 createPropose 创建 Propose 并广播到其他节点,收集其他节点的受托人签名。

以上详解稍微零散一些,接下来大概总结一下整体的共识流程

『共识流程总结』

  1. 在受托人A锻造区块的时候,负责这一次区块锻造的受托人节点,首先需要用他的签名创建 votes (createVotes)。
  2. 但是这个受托人节点一般只有一个受托人密钥,所以不满足101x2/3=68 这个条件,无法顺利锻造这个区块 (hasEnoughVotes)。
  3. 所以这个受托人只好先把这个锻造的区块 setPendingBlock 暂时寄存起来。
  4. 然后通过 createPropose 去广播给其他受托人节点,去收集其他受托人节点和该区块有关的签名。
  5. 其他受托人验证这个Propose来路之后纷纷响应,把自己给这个区块的签名Votes发回给一开始的受托人A 。
  6. 然后受托人A一直收集,直到收集到的签名超过了101x2/3=68个之后,再把刚才寄存的block通过 getPendingBlock 取出来。
  7. 继续完成了这个block的锻造。

其实如果看懂了这个consensus,基本上也就看懂了 core/blocks.js 整个区块锻造过程了。

接下来下一篇文章会继续分析 base模块中的其他三个子模块。

]]>



Asch源码阅读:启动过程概述中,我们提到 init.js 初始化中的一个步骤就是初始化 base 模块。 在这里稍微展开谈谈 base 模块具体是什么,base 模块代码对应的目录是 ./src/base/ ,在此目录下主要有以下几个模块:

  • consensus.js
  • account.js
  • block.js
  • transaction.js

base 也是核心源码目录,但是可能容易会把 ./src/base/ 和 ./src/core/ 混淆,觉得为什么两个目录代码都是核心代码,但是为什么分开?

其实很简单区分,./src/base/ 主要是提供一些基础的操作函数,供其他模块调用,本身并不是事件触发,也没有任何事件回调函数。

而 ./src/core/ 的核心模块基本上都是事件触发,注册了各种 onBind, onNewBlock 之类的事件回调函数。

记住这点就不容易混淆了。

下面逐个谈谈具体模块负责的事情:

『consensus』

这个模块负责『共识』,或者叫一致性吧,其实很简单,就是让各个节点达成一致。而不分叉。共识是区块链的核心之一。

主要有以下几个功能函数:

「createVotes」

创建投票,先根据block的高度和id算出hash值, 然后使用当前节点配置里合法的受托人的密钥进行签名,一般一台服务端只配置一个受托人节点。 最后返回一个带有当前 block 高度,多个受托人签名的Votes 。 其实这里把 Votes 翻译成投票不是非常合理,更准确的说, 其实这个 Votes 就是受托人的记账,后续会在 block 生成的时候使用这个 Vote 去验证,包括新生成一个区块之后,广播到其他节点的时候需要广播区块信息和这个Vote信息给其他节点。 这样其他节点可以都可以验证这个 Vote 和 Block,确保 Block 的锻造过程是由合法的受托人通过正确的方式锻造出来的。

「hasEnoughVotes」

检查Votes是否包含足够多的受托人签名, 目前受托人人数是101,需要的签名至少 101 x 2 / 3 = 68 个受托人。 也就是说如果当时在线的受托人人数不满68人,则无法让这个区块链正常产块。

「hasEnoughVotesRemote」

Votes需要包含至少6个受托人签名才行。这个判断比上面那个 hasEnoughVotes 要求更轻一些。

「setPendingBlock」

要理解这个函数其实需要先理解 block 的锻造过程(core/blocks.js),

在block的锻造过程中,首先会判断 hasEnoughVotes 是否有足够的受托人签名(如上所述,所需受托人签名至少68人),

但是一般一个受托人节点,只会有一个受托人密钥,那其实是在锻造的时候 是不满足 >= 68 个签名的要求的, 所以会先把锻造的 block 先寄存起来,也就是 setPendingBlock 寄存起来。

然后会 createPropose 并广播到其他节点, 其他节点收到 Propose 之后会调用它们的 createVotes 生成它们的 Votes, 然后把它们的 votes 发送回来给 propose 发起者的 ip 。 这样,发起这个 Propose 的节点会收到其他节点的 votes, 也就能集齐超过 68 个受托人的 signatures, 就有足够的 signatures , 则满足 hasEnoughVotes 的条件, 然后再把这个 PendingBlock 取出来,然后真正完成这个block的区块锻造。

这个过程是打成共识的一个过程,但是在达成共识的等待时间里面, 需要这么一个 setPendingBlock 存储中间结果的过程。

「createPropose」

如上所述,当本节点发现自己受托人签名不满足锻造区块所需签名数量的时候, 会通过 createPropose 创建 Propose 并广播到其他节点,收集其他节点的受托人签名。

以上详解稍微零散一些,接下来大概总结一下整体的共识流程

『共识流程总结』

  1. 在受托人A锻造区块的时候,负责这一次区块锻造的受托人节点,首先需要用他的签名创建 votes (createVotes)。
  2. 但是这个受托人节点一般只有一个受托人密钥,所以不满足101x2/3=68 这个条件,无法顺利锻造这个区块 (hasEnoughVotes)。
  3. 所以这个受托人只好先把这个锻造的区块 setPendingBlock 暂时寄存起来。
  4. 然后通过 createPropose 去广播给其他受托人节点,去收集其他受托人节点和该区块有关的签名。
  5. 其他受托人验证这个Propose来路之后纷纷响应,把自己给这个区块的签名Votes发回给一开始的受托人A 。
  6. 然后受托人A一直收集,直到收集到的签名超过了101x2/3=68个之后,再把刚才寄存的block通过 getPendingBlock 取出来。
  7. 继续完成了这个block的锻造。

其实如果看懂了这个consensus,基本上也就看懂了 core/blocks.js 整个区块锻造过程了。

接下来下一篇文章会继续分析 base模块中的其他三个子模块。

]]>
0
<![CDATA[向量库的一点改进]]> http://www.udpwork.com/item/16652.html http://www.udpwork.com/item/16652.html#reviews Wed, 07 Feb 2018 22:20:10 +0800 云风 http://www.udpwork.com/item/16652.html 前段为 3d engine 写的向量运算库小伙伴在用,提了很多意见,所以这段时间一直在改进。

一开始觉得逆波兰表示法的运算表达式不太习惯,觉得需要绕个弯想问题,希望做一个表达式编译的东西,但是用了几天后,又觉得其实不是什么大问题,习惯了就好了。

但心智负担比较大的地方是那个 id 的正负号约定,也就是生命期管理。我想了一下,人为的去管理生命期,有些对象是要长期持有的,有些对象只在当前渲染帧使用,在使用的时候严格区分它们不太现实。

一开始的版本,我需要使用者在计算表达式中用一个 mark 'M' 指令,把一个临时对象转换成一个持久对象,这极大的增加了使用者的负担。尤其是更新一个对象的时候,需要先解除老对象的持久状态,再 mark 新生成的对象。使用的时候需要一直考虑这个对象是不是要更新,用起来太困难了。虽然有强检查,不会把程序弄混乱,但是稍不注意就会报告运行时错(对象 id 失效)。

今天,我做了极大的调整,去掉了之前 mark 语义,增加了引用语义。

之前没有实现引用语义是因为觉得值语义就够用了。现在看起来是不对的。不过引用语义比较难实现,一开始也没有找到合适的实现方式。考虑了很久后,我发现了一条实现引用语义的途径,那就是把引用语义的对象实现为 lua 中的 userdata 。

换句话说,假设 lua 中的 foobar 变量引用了一个 vector ,我们就显示的定义 foobar = math3d.ref "vector" 。

这里 math3d.ref "vector" 会生成一个引用 vector 的对象,在之后的程序中,我们可以改变 foobar 引用的值,但是不再修改 foobar 在 lua 中的值。

之后,我们在原来的指令操作栈中增加一个赋值指令 = ,如果我们想让 foobar 引用一个具体的 vector { 1,0,0,1 } ,就应该写:

command(foobar, { 1, 0, 0, 1} , "=")

这条命令的语义是,将 foobar 这个引用对象和 { 1,0,0,1 } 这个 vector 常量压栈,然后把栈顶的元素赋值给栈次顶的引用对象,然后将栈顶两个东西弹出。

我们之前在 command 序列中,数字 id 表示把一个对象压栈,字符串表示计算指令串,table 表示外部常量;现在需要增加特定的 userdata 表示引用语义的对象。

当然,我还给 foobar 增加了元表,可以用一系列便捷操作。例如 foobar(obj) 相当于把 obj 赋给 foobar , ~foobar 表示取得 foobar 引用对象的 C 指针(lightuserdata)用来传递给底层,等等。

我觉得这一版的设计要人性的多,但是实现上也遇到一些困难。

这是因为,原版的设计中,数据栈上都是数字 id ,所以可以很容易先实现一个 C 层的 stack 结构来操控。这次,数据栈上需要压入 lua userdata 了,就不再能简单的和 C 层数据结构对接。如果粗暴的用一个 hash table 来映射 lua 中的对象,其一,性能受到了损失;其二,userdata 的生命期管理变得非常复杂(需要很小心的加接引用)。

为此,我给 api 增加一个限制:引用语义的对象只可以在当前 command 指令串中使用,不可以把引用语义的对象压入堆栈,然后在下次 command 调用再赋值。所有压入指令栈的引用语义的对象在当前指令结束后,都会退化成值语义的量(如果指令结束还没有弹出)。

在实现层面,我没有修改之前的数据结构,而增加了一个专门保存引用对象的栈,让指令以双栈形式工作。引用对象栈记录的是当前 lua 栈上的数字 index ,等函数调用结束就自动失效了。实现也相对简单。

]]>
前段为 3d engine 写的向量运算库小伙伴在用,提了很多意见,所以这段时间一直在改进。

一开始觉得逆波兰表示法的运算表达式不太习惯,觉得需要绕个弯想问题,希望做一个表达式编译的东西,但是用了几天后,又觉得其实不是什么大问题,习惯了就好了。

但心智负担比较大的地方是那个 id 的正负号约定,也就是生命期管理。我想了一下,人为的去管理生命期,有些对象是要长期持有的,有些对象只在当前渲染帧使用,在使用的时候严格区分它们不太现实。

一开始的版本,我需要使用者在计算表达式中用一个 mark 'M' 指令,把一个临时对象转换成一个持久对象,这极大的增加了使用者的负担。尤其是更新一个对象的时候,需要先解除老对象的持久状态,再 mark 新生成的对象。使用的时候需要一直考虑这个对象是不是要更新,用起来太困难了。虽然有强检查,不会把程序弄混乱,但是稍不注意就会报告运行时错(对象 id 失效)。

今天,我做了极大的调整,去掉了之前 mark 语义,增加了引用语义。

之前没有实现引用语义是因为觉得值语义就够用了。现在看起来是不对的。不过引用语义比较难实现,一开始也没有找到合适的实现方式。考虑了很久后,我发现了一条实现引用语义的途径,那就是把引用语义的对象实现为 lua 中的 userdata 。

换句话说,假设 lua 中的 foobar 变量引用了一个 vector ,我们就显示的定义 foobar = math3d.ref "vector" 。

这里 math3d.ref "vector" 会生成一个引用 vector 的对象,在之后的程序中,我们可以改变 foobar 引用的值,但是不再修改 foobar 在 lua 中的值。

之后,我们在原来的指令操作栈中增加一个赋值指令 = ,如果我们想让 foobar 引用一个具体的 vector { 1,0,0,1 } ,就应该写:

command(foobar, { 1, 0, 0, 1} , "=")

这条命令的语义是,将 foobar 这个引用对象和 { 1,0,0,1 } 这个 vector 常量压栈,然后把栈顶的元素赋值给栈次顶的引用对象,然后将栈顶两个东西弹出。

我们之前在 command 序列中,数字 id 表示把一个对象压栈,字符串表示计算指令串,table 表示外部常量;现在需要增加特定的 userdata 表示引用语义的对象。

当然,我还给 foobar 增加了元表,可以用一系列便捷操作。例如 foobar(obj) 相当于把 obj 赋给 foobar , ~foobar 表示取得 foobar 引用对象的 C 指针(lightuserdata)用来传递给底层,等等。

我觉得这一版的设计要人性的多,但是实现上也遇到一些困难。

这是因为,原版的设计中,数据栈上都是数字 id ,所以可以很容易先实现一个 C 层的 stack 结构来操控。这次,数据栈上需要压入 lua userdata 了,就不再能简单的和 C 层数据结构对接。如果粗暴的用一个 hash table 来映射 lua 中的对象,其一,性能受到了损失;其二,userdata 的生命期管理变得非常复杂(需要很小心的加接引用)。

为此,我给 api 增加一个限制:引用语义的对象只可以在当前 command 指令串中使用,不可以把引用语义的对象压入堆栈,然后在下次 command 调用再赋值。所有压入指令栈的引用语义的对象在当前指令结束后,都会退化成值语义的量(如果指令结束还没有弹出)。

在实现层面,我没有修改之前的数据结构,而增加了一个专门保存引用对象的栈,让指令以双栈形式工作。引用对象栈记录的是当前 lua 栈上的数字 index ,等函数调用结束就自动失效了。实现也相对简单。

]]>
0
<![CDATA[NSView NSImage NSData转换]]> http://www.udpwork.com/item/16651.html http://www.udpwork.com/item/16651.html#reviews Wed, 07 Feb 2018 16:10:47 +0800 ideawu http://www.udpwork.com/item/16651.html NSBitmapImageRep *bitmap = [view bitmapImageRepForCachingDisplayInRect:[view visibleRect]]; [view cacheDisplayInRect:[view visibleRect] toBitmapImageRep:bitmap]; NSImage *image = [[NSImage alloc] initWithSize:NSMakeSize(width, height)]; [image addRepresentation:bitmap]; NSBitmapImageRep *bitmap = [NSBitmapImageRep imageRepWithData:data]; NSBitmapImageRep *bitmap = [[[NSBitmapImageRep alloc] initWithCGImage:CGImage];

Related posts:

  1. 流式布局的原理和代码实现
  2. iOS 正确接收 HTTP chunked 数据的方法
  3. [转]一个叫做家的地方
  4. 可爱机械战警小男孩入侵底特律城
  5. Objective-C 对二进制数据 NSData 进行 URL 编码
]]>
NSBitmapImageRep *bitmap = [view bitmapImageRepForCachingDisplayInRect:[view visibleRect]]; [view cacheDisplayInRect:[view visibleRect] toBitmapImageRep:bitmap]; NSImage *image = [[NSImage alloc] initWithSize:NSMakeSize(width, height)]; [image addRepresentation:bitmap]; NSBitmapImageRep *bitmap = [NSBitmapImageRep imageRepWithData:data]; NSBitmapImageRep *bitmap = [[[NSBitmapImageRep alloc] initWithCGImage:CGImage];

Related posts:

  1. 流式布局的原理和代码实现
  2. iOS 正确接收 HTTP chunked 数据的方法
  3. [转]一个叫做家的地方
  4. 可爱机械战警小男孩入侵底特律城
  5. Objective-C 对二进制数据 NSData 进行 URL 编码
]]>
0
<![CDATA[像素级监督:你们可能都监督错了地方]]> http://www.udpwork.com/item/16649.html http://www.udpwork.com/item/16649.html#reviews Mon, 05 Feb 2018 17:41:58 +0800 魏武挥 http://www.udpwork.com/item/16649.html  

 

前日,腾讯上线了一款资讯产品,名字就叫“腾讯立知”:摆明了就是腾讯出品。

像素级监督:你们可能都监督错了地方

(有一句讲一句,界面逼格很高)

我和腾讯omg部门有些来往,并未听说这个腾讯的主力内容部门做过这样一个产品。四处打听了一下,原来是mig应用宝的手笔。

这款产品需要邀请码才能进入使用,我看到微信上有个哥们在那里说,好像用什么码的产品,就一个f码(小米)还算结局顺利。

一语成谶。

当下,立知已经下架。

 

坊间对立知下架原因的主要猜测来自于即刻的控诉。

熟悉我个人过往文章的读者都知道,我对即刻这款产品一向很喜欢。

即刻刚出发时,走的是“主题式阅读”这样的道路。比如说,iOS又更新了,这算一个主题,用户订阅这个主题后,可以源源不断地获取ios更新的信息。

这是资讯聚合类产品中的一股小清新。小清新的意思有二:其一,以今日头条为代表的聚合资讯产品,并不以主题阅读为招牌——虽然讲到本质,主题阅读和兴趣分发其实很难说是两回事;其二,这类产品的用户基数不是很大,有点阳春白雪的意思。

打开立知,你的确可以看到类似ios又更新了这样的主题。

像素级监督:你们可能都监督错了地方

(立知的主题)

即刻的管理团队不是很爽,在微信发难,称之为抄袭。后来又有kol们说成是“像素级抄袭”。据说批评意见被马化腾看到。

像素级监督:你们可能都监督错了地方

(即刻的主题)

然后,立知就在App Store中消失不见。

 

如果说“主题阅读”,立知的确和即刻——严格说,是过去的即刻,当下的即刻,主题订阅和阅读已退居二线,需要点击“我”或者“发现”这个模块才能进去看到——非常类似。

但如果说做同样的事就是抄袭,这种批评,我以为,有点过。

立知在主题阅读这件事上,是有些新东西的。

在每条信息上,立知会提供一个标签,可能是“延展阅读”,可能是“事件追踪”,可能是“多方观点”。仔细看这些标签后的东西,你会发现,三个标签各有不同。

延展阅读着重于横向的更多相关的信息,事件追踪是纵向的过程梳理,多方观点则是主观意见表达。

当点击一条信息之后,你会到达一个中转页,这个页面有本信息的摘要,再点击才能去往目标页。我当时看到这样的做法,就猜想以后大概要被很多媒体骂。因为摘要看过还点击啥呢?

在这个议题上,立知的确涉嫌侵犯著作权 ,因为著作权项下有一个权利叫改编权。摘要改写就是改编。我本以为这会是立知将来的麻烦,没想到在“主题阅读”上倒被人控诉抄袭。

不过,当你仔细看这些摘要的时候,你可能会感受到人工智能的力量——反正我是这么猜想的。这些摘要并不是目标文章中简单的一段截取,而是真正意义上的改写。请仔细比较这两段:(有没有点摘要式洗稿的意思?哈哈哈)

我知道现在腾讯已经有能力在一场论坛中一个讲者讲完立刻推送出演讲稿的简写版,背后是ai的力量。而mig不是omg,并没有大规模的内容编辑队伍,所以,我猜想这很有可能是ai做的。

无论是三大标签,还是这个摘要制作,即刻并没有做过(关于即刻里的主题里的摘要,我个人的阅读体验是,要么是微博短文本直接复制过来,要么是标题,要么是截取,也有可能阅读体验没有覆盖到全部)。

 

我和倾向于认为是抄袭的keso在微信上讨论了几句。

keso觉得,ios又更新了,即刻有,立知也有,像素级抄袭很明显。

我不太以为然。要不改个叫法?ios莫名出了个新版本?

既然都要做主题阅读,且主题需要个名字,撞名其实并不像六神那句“你的风陵渡我的铁罗汉”被周某拿走那样需要鄙视。

keso举例说,做主题标签,知乎也做,并没有被指责抄袭即刻。但我总觉得,知乎是站内内容,即刻是全网抓取,并不是同一类产品。

更何况,知乎或许也像素级抄袭了“quora”?

keso曰是,所以知乎早年也挨骂。

这事怎么说呢。

远里说,微信还像素级抄袭了某些产品啊!

就近里说,跳一跳没像素级抄袭育碧的什么产品么?

 

主题阅读,是一种资讯产品类型。

这个类型你做的,我也做的。这种类型产品的背后,比拼的是技术能力:对内容进行颗粒分割,然后通过技术加以再聚合。这点上谁的能力越强,谁就更容易胜出些。

类型上,很难说什么抄袭不抄袭的。

门户有好几家,搜索也有好几家。尤其是后者,面上长的都是差不多的,还能变出什么花样来?

Facebook不是第一个搞社交的,苹果也不是第一个搞智能手机的。但他们后来都在自己的产品上做出了重大的创新。

而我觉得,三标签和摘要改写,就是立知与即刻非常不同的地方。当然,摘要改写这事,应该是需要得到内容制作方的授权。

在我看来,立知团队犯了一个瓜田李下式的错误。

按照即刻coo的说法,应用宝曾派人去和即刻探讨合作。虽然mig的公关负责人在微信里私下辩解了几句,但这事的确会让人浮想联翩。

大公司不要没事就派人去创业团队谈合作:尤其是你自己的确有团队(还是同一bg同一业务线下的)在做类似的事。

 

也许在像素级监督之下,腾讯最终下架了立知。

我不知道这款产品未来是从此消失不见还是改头换面再卷土重来。

我是觉得有点过。

但也有可能也是做过腾讯门户、腾讯微博、腾讯应用宝等等诸多“像素级抄袭”产品的腾讯,在即刻这样的被投项目前,表态合作伙伴第一,也算是一种姿态吧。

这类产品的小众化是很明显的,即刻自己都在转弯。我认识一个还算成功的投资人,前日立知刚上线,就在那里说:我一点也不看好这个产品。

我想,我的言外之意已经很明显了。

 

还有一种关于下架原因的说法。

说是立知某个评论出现了不可描述的bug。

也的确,我手机里现在的立知,已经都不可评论了。

 

—— 首发扯氮集 ——

作者执教于上海交通大学媒体与传播学院,天奇阿米巴创投基金管理合伙人

 

 

 

 

 

 

像素级监督:你们可能都监督错了地方,首发于扯氮集

]]>
 

 

前日,腾讯上线了一款资讯产品,名字就叫“腾讯立知”:摆明了就是腾讯出品。

像素级监督:你们可能都监督错了地方

(有一句讲一句,界面逼格很高)

我和腾讯omg部门有些来往,并未听说这个腾讯的主力内容部门做过这样一个产品。四处打听了一下,原来是mig应用宝的手笔。

这款产品需要邀请码才能进入使用,我看到微信上有个哥们在那里说,好像用什么码的产品,就一个f码(小米)还算结局顺利。

一语成谶。

当下,立知已经下架。

 

坊间对立知下架原因的主要猜测来自于即刻的控诉。

熟悉我个人过往文章的读者都知道,我对即刻这款产品一向很喜欢。

即刻刚出发时,走的是“主题式阅读”这样的道路。比如说,iOS又更新了,这算一个主题,用户订阅这个主题后,可以源源不断地获取ios更新的信息。

这是资讯聚合类产品中的一股小清新。小清新的意思有二:其一,以今日头条为代表的聚合资讯产品,并不以主题阅读为招牌——虽然讲到本质,主题阅读和兴趣分发其实很难说是两回事;其二,这类产品的用户基数不是很大,有点阳春白雪的意思。

打开立知,你的确可以看到类似ios又更新了这样的主题。

像素级监督:你们可能都监督错了地方

(立知的主题)

即刻的管理团队不是很爽,在微信发难,称之为抄袭。后来又有kol们说成是“像素级抄袭”。据说批评意见被马化腾看到。

像素级监督:你们可能都监督错了地方

(即刻的主题)

然后,立知就在App Store中消失不见。

 

如果说“主题阅读”,立知的确和即刻——严格说,是过去的即刻,当下的即刻,主题订阅和阅读已退居二线,需要点击“我”或者“发现”这个模块才能进去看到——非常类似。

但如果说做同样的事就是抄袭,这种批评,我以为,有点过。

立知在主题阅读这件事上,是有些新东西的。

在每条信息上,立知会提供一个标签,可能是“延展阅读”,可能是“事件追踪”,可能是“多方观点”。仔细看这些标签后的东西,你会发现,三个标签各有不同。

延展阅读着重于横向的更多相关的信息,事件追踪是纵向的过程梳理,多方观点则是主观意见表达。

当点击一条信息之后,你会到达一个中转页,这个页面有本信息的摘要,再点击才能去往目标页。我当时看到这样的做法,就猜想以后大概要被很多媒体骂。因为摘要看过还点击啥呢?

在这个议题上,立知的确涉嫌侵犯著作权 ,因为著作权项下有一个权利叫改编权。摘要改写就是改编。我本以为这会是立知将来的麻烦,没想到在“主题阅读”上倒被人控诉抄袭。

不过,当你仔细看这些摘要的时候,你可能会感受到人工智能的力量——反正我是这么猜想的。这些摘要并不是目标文章中简单的一段截取,而是真正意义上的改写。请仔细比较这两段:(有没有点摘要式洗稿的意思?哈哈哈)

我知道现在腾讯已经有能力在一场论坛中一个讲者讲完立刻推送出演讲稿的简写版,背后是ai的力量。而mig不是omg,并没有大规模的内容编辑队伍,所以,我猜想这很有可能是ai做的。

无论是三大标签,还是这个摘要制作,即刻并没有做过(关于即刻里的主题里的摘要,我个人的阅读体验是,要么是微博短文本直接复制过来,要么是标题,要么是截取,也有可能阅读体验没有覆盖到全部)。

 

我和倾向于认为是抄袭的keso在微信上讨论了几句。

keso觉得,ios又更新了,即刻有,立知也有,像素级抄袭很明显。

我不太以为然。要不改个叫法?ios莫名出了个新版本?

既然都要做主题阅读,且主题需要个名字,撞名其实并不像六神那句“你的风陵渡我的铁罗汉”被周某拿走那样需要鄙视。

keso举例说,做主题标签,知乎也做,并没有被指责抄袭即刻。但我总觉得,知乎是站内内容,即刻是全网抓取,并不是同一类产品。

更何况,知乎或许也像素级抄袭了“quora”?

keso曰是,所以知乎早年也挨骂。

这事怎么说呢。

远里说,微信还像素级抄袭了某些产品啊!

就近里说,跳一跳没像素级抄袭育碧的什么产品么?

 

主题阅读,是一种资讯产品类型。

这个类型你做的,我也做的。这种类型产品的背后,比拼的是技术能力:对内容进行颗粒分割,然后通过技术加以再聚合。这点上谁的能力越强,谁就更容易胜出些。

类型上,很难说什么抄袭不抄袭的。

门户有好几家,搜索也有好几家。尤其是后者,面上长的都是差不多的,还能变出什么花样来?

Facebook不是第一个搞社交的,苹果也不是第一个搞智能手机的。但他们后来都在自己的产品上做出了重大的创新。

而我觉得,三标签和摘要改写,就是立知与即刻非常不同的地方。当然,摘要改写这事,应该是需要得到内容制作方的授权。

在我看来,立知团队犯了一个瓜田李下式的错误。

按照即刻coo的说法,应用宝曾派人去和即刻探讨合作。虽然mig的公关负责人在微信里私下辩解了几句,但这事的确会让人浮想联翩。

大公司不要没事就派人去创业团队谈合作:尤其是你自己的确有团队(还是同一bg同一业务线下的)在做类似的事。

 

也许在像素级监督之下,腾讯最终下架了立知。

我不知道这款产品未来是从此消失不见还是改头换面再卷土重来。

我是觉得有点过。

但也有可能也是做过腾讯门户、腾讯微博、腾讯应用宝等等诸多“像素级抄袭”产品的腾讯,在即刻这样的被投项目前,表态合作伙伴第一,也算是一种姿态吧。

这类产品的小众化是很明显的,即刻自己都在转弯。我认识一个还算成功的投资人,前日立知刚上线,就在那里说:我一点也不看好这个产品。

我想,我的言外之意已经很明显了。

 

还有一种关于下架原因的说法。

说是立知某个评论出现了不可描述的bug。

也的确,我手机里现在的立知,已经都不可评论了。

 

—— 首发扯氮集 ——

作者执教于上海交通大学媒体与传播学院,天奇阿米巴创投基金管理合伙人

 

 

 

 

 

 

像素级监督:你们可能都监督错了地方,首发于扯氮集

]]>
0