Snow Memory | Andrew Liu Snow Memory, Do one thing, Do it best. 2017-06-17T23:22:51.000Z http://andrewliu.in/ Andrew Liu Hexo Mac重装小计 http://andrewliu.in/2017/06/18/Mac重装小计/ 2017-06-17T22:52:41.000Z 2017-06-17T23:22:51.000Z
本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循署名-非商业用途-保持一致的创作共用协议.

很久没有更新博客了, 前段时间迷上了王者农药, 戒了农药后又重新入了暴雪爸爸暗黑3的大坑, 呵呵.
除此之外, 公司离职回来一直准备毕业论文、毕业答辩、毕业相关材料, 真是焦头烂额.
不过想想十九年求学生涯就要结束了, 即将AFK了, 简直幸福.

为什么要重装? Mac系统的乱七八糟的东西已经占据了90%的磁盘空间, 无法减少文件保持一定空闲磁盘空间, 这种情况已经严重影响了我的日常工作.

重装方案严格按照 Apple 官方文档 如何重新安装 macOS 执行.

系统偏好设置

  • 允许安装任何来源的APP: 安全性与隐私->通用. 若无该选项,请命令行执行sudo spctl --master-disable
  • 设置快捷键: 键盘->快捷键 更改输入法切换快捷键
  • 设置触摸板: 选取全部触摸板设置
  • 设置触发角: 桌面与屏幕保护程序->触发角
  • 设置密码验证: 系统偏好设置->安全性与隐私->选择 进入休眠或屏保后立即要求输入密码

  • 编辑 /etc/paths(sudo vim /etc/paths)

1
2
3
4
5
6
/usr/local/bin
/usr/local/sbin
/usr/bin
/usr/sbin
/bin
/sbin

软件安装

若不需要Xcode可直接跳过该步骤, 安装Homebrew时同样会自动安装Command Line Tools

  • 通过App Store 安装Xcode
  • 安装Command Line Tools
1
$ xcode-select --install

安装Homebrew

1
$ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

安装Git

1
2
3
4
5
6
7
$$ brew install git
# SSH-KeyGen, 设置SSH密钥
$ ssh-keygen -t rsa -C "your_email@youremail.com"
# 在Github中添加新生成的公钥, 验证是否成功请执行以下命令
$ ssh -T git@github.com

安装nodejs

1
2
3
4
5
6
$ brew install nodejs
# 安装nvm或者n作为nodejs版本控制工具
$ (sudo) npm install -g n
# 如果不行, 则使用nvm进行版本控制
$ n 4.2.4

安装 Ruby

1
2
3
4
5
6
$ brew install ruby
$ rbenv install -l # list all available versions
$ rbenv install 2.2.1 # install a Ruby version
$ rbenv global 2.2.1 # set the global version
$ rbenv versions # list all installed Ruby versions
1
2
3
4
$ gem sources --add https://gems.ruby-china.org/ --remove https://rubygems.org/
$ gem sources -l
https://gems.ruby-china.org
# 确保只有 gems.ruby-china.org

安装vim和MacVim

1
2
3
4
5
Step 1. Install homebrew from here: http://brew.sh
Step 1.1. Run export PATH=/usr/local/bin:$PATH
Step 2. Run brew update
Step 3. Run brew install vim && brew install macvim
Step 4. Run brew link macvim

安装zsh和Oh My Zsh

1
2
3
$ brew install zsh
# 安装oh-my-zsh
$ sh -c "$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"
  • 更换oh my zsh主题在~/.oh-my-zsh/themes/路径下, 在zshthem网站进行主题预览
  • 安装autojump, 一键跳转到目的目录, 不再不停的cd
  • 安装trash, 不再用rm命令.
1
2
3
4
# 安装autojump
$ brew install autojump
# 安装trash
$ npm install --global trash

持久化SSH连接, 安装mosh

1
$ brew install mobile-shell

安装python

1
2
3
4
5
6
7
8
9
10
11
12
$ brew install python
Pip and setuptools have been installed. To update them
pip install --upgrade pip setuptools
You can install Python packages with
pip install <package>
They will install into the site-package directory
/usr/local/lib/python2.7/site-packages
See: http://docs.brew.sh/Homebrew-and-Python.html

安装安装 MongoDB, MySQL

1
$ brew install mongodb mysql

设置开机自启动「可选」:

1
2
3
$ mkdir -p ~/Library/LaunchAgents
$ ln -sfv /usr/local/opt/mongodb/*.plist ~/Library/LaunchAgents
$ ln -sfv /usr/local/opt/mysql/*.plist ~/Library/LaunchAgents

.zshrc文件配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
alias zshconfig="vim ~/.zshrc"
alias rezsh="source ~/.zshrc"
alias ohmyzsh="cd ~/.oh-my-zsh"
# The default command paramters
alias vi='vim'
alias egrep='egrep --color=auto'
alias fgrep='fgrep --color=auto'
alias bc='bc -l'
alias wget='wget -c'
alias chown='chown --preserve-root'
alias chgrp='chgrp --preserve-root'
alias rm='rm -I --preserve-root'
alias ln='ln -i'
# Colorful grep output
alias grep='grep --color=auto'
export GREP_COLOR='1;33'
# Colorful ls
export LSCOLORS='Gxfxcxdxdxegedabagacad'
ls='ls --color=auto'
# autojump
[[ -s ~/.autojump/etc/profile.d/autojump.sh ]] && . ~/.autojump/etc/profile.d/autojump.sh

安装Sublime Text

  1. 安装Sublime Text 3
  2. 安装Package Control
  3. 自定义配置Settings

Sublime Text 3 安装Package Control的程序

1
import urllib.request,os;pf = 'Package Control.sublime-package';ipp = sublime.installed_packages_path(); urllib.request.install_opener( urllib.request.build_opener( urllib.request.ProxyHandler()) );open(os.path.join(ipp, pf), 'wb').write(urllib.request.urlopen( 'http://sublime.wbond.net/' + pf.replace(' ','%20')).read())

常用收费软件

  • Alfred 2(效率神器)
  • Dash 3(程序员专用-文档查询)
  • CleanMyMac 3(电脑清理软件)
  • PopClip(选中即复制)
  • Near Lock(靠近解锁软件)
  • Bartender 2(状态栏图标管理器)
  • Manico(更方便的软件切换软件)

常用免费软件

  • Caffeine(不息屏神器)
  • XtraFinder(增强Finder)
  • Manico(快速切换神器, 相当与window上Alt+Tab)
  • Clipy, 重装时发现ClipMenu不维护了, 找了个代替品(剪切板管理)
  • Bear,MacDown,Mou(Markdown编辑器)
  • Movist(播放器)
  • Snip(截图软件)
  • Chrome(神之浏览器)
  • iTerm2(Mac上遗失的Terminal)
  • 搜狗拼音(比原生的好用)
  • Window Tidy(窗口大小切换)
  • SmoothMouse
  • Reeder(RSS订阅)
  • SCROLL REVERSER (修改鼠标移动方向, 但不改变触摸板)

  • 百度网盘

  • 微信
  • QQ
  • 阿里旺旺
  • JetBrain全家桶
  • TinyCal
  • Shadowsocks
  • Spark
  • 网易云,QQ音乐
  • TickTick
  • 富途牛牛
  • EverNote
  • TeamViewer
  • MacTex

命令行管理Wifi

Managing WIFI connections using the Mac OSX terminal command line

参考链接

]>
<div class="tip"><br> 本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循<a href="http://creativecommons.org/licenses/by-nc-sa/3.0/deed.zh" target="_blank" rel="external">署名-非商业用途-保持一致</a>的创作共用协议.<br></div> <blockquote> <p>很久没有更新博客了, 前段时间迷上了王者农药, 戒了农药后又重新入了暴雪爸爸<code>暗黑3</code>的大坑, 呵呵.<br>除此之外, 公司离职回来一直准备毕业论文、毕业答辩、毕业相关材料, 真是焦头烂额.<br>不过想想十九年求学生涯就要结束了, 即将AFK了, 简直幸福.</p> </blockquote> <p>为什么要重装? Mac系统的乱七八糟的东西已经占据了90%的磁盘空间, 无法减少文件保持一定空闲磁盘空间, 这种情况已经严重影响了我的日常工作.</p> <p>重装方案严格按照 Apple 官方文档 <a href="https://support.apple.com/zh-cn/HT204904" target="_blank" rel="external">如何重新安装 macOS</a> 执行.</p> <h2 id="系统偏好设置"><a href="#系统偏好设置" class="headerlink" title="系统偏好设置"></a>系统偏好设置</h2><ul> <li>允许安装任何来源的APP: <code>安全性与隐私-&gt;通用</code>. 若无该选项,请命令行执行<code>sudo spctl --master-disable</code></li> <li>设置快捷键: <code>键盘-&gt;快捷键</code> 更改输入法切换快捷键</li> <li>设置触摸板: 选取全部触摸板设置</li> <li>设置触发角: <code>桌面与屏幕保护程序-&gt;触发角</code></li> <li><p>设置密码验证: <code>系统偏好设置-&gt;安全性与隐私-&gt;选择 进入休眠或屏保后立即要求输入密码</code></p> </li> <li><p>编辑 /etc/paths(<code>sudo vim /etc/paths</code>)</p> </li> </ul> <figure class="highlight c"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div></pre></td><td class="code"><pre><div class="line">/usr/local/bin</div><div class="line">/usr/local/sbin</div><div class="line">/usr/bin</div><div class="line">/usr/sbin</div><div class="line">/bin</div><div class="line">/sbin</div></pre></td></tr></table></figure>
2016成就墙完成情况总结 http://andrewliu.in/2017/01/17/2016成就墙完成情况总结/ 2017-01-17T14:03:46.000Z 2017-06-17T22:14:00.000Z 本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循署名-非商业用途-保持一致的创作共用协议.

一转眼,2016年又结束了,我的本命年就这样不知不觉溜走了,回头一看,很多年初许下的目标因为各种拖延症没有完成,心情还是有些小抑郁的。2016年算是奔波的一年吧,往返南京、北京、深圳三座城市也是挺心酸了,还不是为了生活的苟且。

本文想到哪里写到哪里,毕竟我没有过目不忘的记忆

2016年解锁成就:

  • Zhihu.inc全体验成就达成,拿到Offer后跑路
  • Baidu.inc全体验成就达成,拿到Offer后又干了一个月因其他原因不得不跑路
  • Tencent.inc实习成就达成,并期待解锁终生成就奖(然而已经错过了一波企鹅18岁的全员股票)
  • NJU最浪/最不务正业研二学生成就达成,一整年没回几次学校,没睡几次宿舍的床,感觉宿舍的床已经长毛了,不能再睡了。
  • BAT实习成就解锁2/3,预计剩余1/3是没有机会解锁了,毕竟企业文化是很重要的,选择一个适合的企业文化才能让自己开心的浪
  • iPhone 7成就达成,喊了这么多年想拥有一个iPhone,终于这一次iPhone发布会剁手,虽然没有太多的外形变化,但是总是比我的辣鸡魅族不知道高到哪里去了,辣鸡魅族,毁我青春,败我钱财
  • Apple三件套成就达成,还记得第一件iPad是去南大读书的时候,妈妈送我的礼物,还记得第二件Macbook是送给自己的开发礼物,用Apple的产品总是不会错的,虽然我一直鼓吹自己是个谷歌脑残粉,嗯,谷歌大法好!
  • PS4成就达成,并完全解锁火影4,待解锁巫师3,神海四件套,三人一狗,还是索尼大法好
  • 自如最佳送钱租客成就达成,两次自如强制节约,累计送出几个亿的违约金,住的房子也是越来越贵了,从不到2000租金的次卧到住进2000+租金的自如主卧,感觉好像就空间大了点,阳光多了点的样子。
  • 看书最少的一年成就解锁,累计看完三本书,剩下的时间都被狗吃了吗
  • Github凹凸commit记录成就解锁,当你看到最近一个月在频繁commit的时候,说明我实习结束了,当你看到我最近一个月几乎没有commit的时候,说明我又开始新的实习了
  • 校招成就解锁,经历了一次校招,还没什么感觉就结束了,我想去的offer收到了,就没心情继续做一名收offer狂魔了
  • 负心员工成就解锁,有了两度收到offer跑路的经历了,也是没谁了,第一次跑路是因为开发遇到大坑,不跑路也要被迫跑路,还不如主动点,第二次跑路是因为我收到理想中的offer并且签了三方,都把公司的脸打了,再不跑路难道当商业间谍吗?

  • 北京全景点解锁,南锣鼓巷,长城,圆明园,后海,故宫…全解锁,不想再留在北京作为一个吸霾卫士了

  • 深圳二次游成就解锁,时隔2年,又回到了个这个梦开始的地方,上一次我以一个参观者的身份到腾讯科兴科学园,这一次我以一个实习生的身份再一次走进科兴科学园,期待我下次以一名正式员工的身份归来。
  • 香港游成就解锁,第一次看到了资本主义社会的发展形态,听到只能在听歌的时候才能听到的粤语,发现中国香港人好像没大陆仔没什么区别,2333

  • 人生第一次爱情三周年成就解锁,不过貌似这两天又遇到感觉危机了,工作忙(借口?),沟通不畅,为什么别人家的女朋友总是善解人意,小鸟依然呢?这是一个值得思考的问题

  • 博客更新和维护成就解锁,累计写出不知道多少篇博客,至少没有中断过太久的更新。

2017年待解锁成就:

  • 浪漫情人节成就,准备发一波狗粮
  • Jaybird X3成就,写这篇文章的当天,该成就已成功解锁,非常赞的耳机,值得拥有
  • 粗粮加的手环和移动电源割草成就
  • Tencent.inc实习全体验成就
  • 南京大学成功毕业及毕业典礼成就
  • 毕业旅行成就
  • 正式入职成就
  • 读完十本书成就,包括但不限于开发相关书籍
  • PS4 三人一狗,神海四合集,巫师3成就
  • 王者农药黄金段位成就,如果玩农药没有梦想和咸鱼有什么两样,毕竟非洲血统,排位必遇挂机党,大神带我上排位
  • 订婚成就
  • 博客更新与维护成就
  • 参与开源项目开发成就
  • 更多成就待补充…
]>
<p>本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循<a href="http://creativecommons.org/licenses/by-nc-sa/3.0/deed.zh" target="_blank" rel="external">署名-非商业用途-保持一致</a>的创作共用协议.</p> <p>一转眼,2016年又结束了,我的本命年就这样不知不觉溜走了,回头一看,很多年初许下的目标因为各种拖延症没有完成,心情还是有些小抑郁的。2016年算是奔波的一年吧,往返南京、北京、深圳三座城市也是挺心酸了,还不是为了生活的苟且。</p> <blockquote> <p>本文想到哪里写到哪里,毕竟我没有过目不忘的记忆</p> </blockquote> <p>2016年解锁成就:</p> <ul> <li>Zhihu.inc全体验成就达成,拿到Offer后跑路</li> <li>Baidu.inc全体验成就达成,拿到Offer后又干了一个月因其他原因不得不跑路</li> <li>Tencent.inc实习成就达成,并期待解锁终生成就奖(然而已经错过了一波企鹅18岁的全员股票)</li> <li>NJU最浪/最不务正业研二学生成就达成,一整年没回几次学校,没睡几次宿舍的床,感觉宿舍的床已经长毛了,不能再睡了。</li> <li>BAT实习成就解锁2/3,预计剩余1/3是没有机会解锁了,毕竟企业文化是很重要的,选择一个适合的企业文化才能让自己开心的浪</li> <li>iPhone 7成就达成,喊了这么多年想拥有一个iPhone,终于这一次iPhone发布会剁手,虽然没有太多的外形变化,但是总是比我的辣鸡魅族不知道高到哪里去了,辣鸡魅族,毁我青春,败我钱财</li> <li>Apple三件套成就达成,还记得第一件iPad是去南大读书的时候,妈妈送我的礼物,还记得第二件Macbook是送给自己的开发礼物,用Apple的产品总是不会错的,虽然我一直鼓吹自己是个谷歌脑残粉,嗯,谷歌大法好!</li> </ul>
开发机安装配置golang和使用CGI http://andrewliu.in/2016/12/13/开发机安装配置golang和使用CGI/ 2016-12-13T06:46:08.000Z 2017-06-17T22:14:00.000Z 本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循署名-非商业用途-保持一致的创作共用协议.

安装golang

  1. Xshell5 登录跳板机->开发机
  2. 执行rz -bey选中本地电脑中下载的golang压缩包
  3. 执行以下命令解压并安装二进制文件到/usr/local
1
$ tar -C /usr/local -xzf go$VERSION.$OS-$ARCH.tar.gz
  1. 配置golang对应的环境变量
1
2
3
4
# 使用vim修改对应的配置文件
$ vim /etc/profile
# 配置环境变量
export PATH=$PATH:/usr/local/go/bin

什么是cgi?

  • Web服务器负责管理连接, 数据传输,网络交互
  • CGI脚本负责管理具体的业务逻辑, CGI是一种网页和程序通讯的协议

CGI程序负责生成动态内容, 然后返回给服务器, 由服务器转交给客户端。CGI是一个独立的程序可以独立运行

使用golang和cgi包

golang开发时, 需要对当前工作目录配置 GOPATH.

1
2
# 到工作目录下执行以下目录, 将当前目录设置为GOPATH
$ export "GOPATH=$PWD"

简单的机遇HTTP Server的cgi处理程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main
import (
"net/http"
"net/http/cgi"
"log"
"fmt"
)
func CgiTestHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println("get request...", r.URL.Path)
handler := new(cgi.Handler)
handler.Path = "/usr/local/go/bin/go/index.html"
// 运行脚本位置
script := "/root/AsyncLiu/Golang/CgiTest/script/index.html" + r.URL.Path
log.Println(handler.Path)
handler.Dir = "/root/AsyncLiu/Golang/CgiTest/script/index.html"
args := []string{"run", script}
handler.Args = append(handler.Args, args...)
handler.Env = append(handler.Env, "GOPATH=/root/AsyncLiu/Golang/CgiTest")
log.Println(handler.Args)
handler.ServeHTTP(w, r)
}
func main() {
http.HandleFunc("/index.html", CgiTestHandler)
fmt.Println("start web server....")
log.Fatal(http.ListenAndServe(":8888", nil))
}

对应的被执行的脚本

1
2
3
4
5
6
7
8
9
10
11
package main
import "fmt"
func init() {
fmt.Println("Content-Type: test/plain;charset=utf-8\n\n")
}
func main() {
fmt.Println("This is executing go script")
}
]>
<p>本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循<a href="http://creativecommons.org/licenses/by-nc-sa/3.0/deed.zh" target="_blank" rel="external">署名-非商业用途-保持一致</a>的创作共用协议.</p> <h2 id="安装golang"><a href="#安装golang" class="headerlink" title="安装golang"></a>安装golang</h2><ul> <li><a href="https://golang.org/dl/" target="_blank" rel="external">Golang download</a>下载合适版本的<code>golang</code>二进制发布包.</li> </ul>
传统软件公司/创业公司/大公司的工作机会如何选择? http://andrewliu.in/2016/11/21/传统软件公司-创业公司-大公司的工作机会如何选择?/ 2016-11-21T03:30:55.000Z 2017-06-17T22:14:00.000Z 本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循署名-非商业用途-保持一致的创作共用协议.

背景介绍: 第一家工作的公司是一家跨国外企安全公司, 骄傲的称自己不是互联网公司而是传统软件公司, 第二家公司是当下最热的知识分享社区, 创业公司. 第三家公司是挤走谷歌, 曾一度称霸中国的搜索引擎公司, 体量很大的著名三大互联网公司之一.

每一家公司我都接触的不是很久, 没能用一生的时间来体验一个公司, 只能说一下我在短时间看到的优势和缺陷

传统软件公司

优势:

  1. 最大优势, 几乎从不加班, 其中包括很多外企传统软件公司, 养老的最好去处, 五点老大带你一起下班回家
  2. 次要优势, 工作压力小, 当然核心项目除外 (因为要抢占市场之类的), 我当时所在的部门, 工作目标不要太轻松, 两三天完成本周工作目标, 然后可以自由看书(当然也可能是老板看我太菜, 想闲置我)

缺陷:

  • 技术栈不够丰富, 很难学到高深的姿势(大神除外), 很难接触到或者用到当下最流程的框架或者技术
  • 福利少, 思想僵化, 体制化严重

建议: 想在互联网行业发展, 不要去传统软件行业和外企, 长久呆在这些地方, 很难适应国内互联网公司的工作压力和工作节奏

创业公司

如果想去创业公司, 我觉得首先应该调查一下以下问题:

  1. 老板是否有互联网背景? 是否有创业成功经历? 是否曾连续创业
  2. 公司是否有足够的创业资金, 公司处于第几轮融资, 是否有广阔的前景(这个比较难判断), 公司当前的估值如果
  3. 公司的创业团队如何? 带队领导的业界风评怎么样

针对第一个问题, 问题比较大的是, 有些创业CEO本身毫无互联网行业背景, 总是想着只差一个程序员, 而又喜欢对技术实现指手画脚, 我个人不太喜欢这种创业公司. 针对第二个问题, 无论如何, 工作的本质都是为了挣钱, 最终实现财富自由, 所有公司有充足的资金比较重要, 我听过身边很多朋友说创业公司老板跑路或者发不起/拖欠工资. 针对第三个问题, 进入互联网行业的程序员很多人都有一个技术专家的梦想, 所以跟着一个技术大佬指引人生方向是很重要的(菊苣们不需要).

优势

  • 工作氛围年轻, 充满活力, 一起工作的人年龄都差不多, 简直不要太轻松愉悦
  • 福利比较好, 创业公司为了吸引人才是舍得下本钱的, 标配Mac, Dell U系列显示器, 人体工程椅, 大量零食, 定期Team building等等
  • 技术栈自由, 没有历史包袱, 可以任意使用新颖的框架和技术(但是会给以后埋坑)

缺陷

  • 很多创业公司缺少同一的标准, 代码混乱缺乏review, 技术栈混乱造轮子严重(这需要有一个重视这方面的leader来引导)
  • 技术积累比较弱, 大量使用外部开源项目, 很多时候都是业务堆积
  • 没有升职空间和与之对应的职业路线, 大多创业公司是简单的三层管理方式, 普通员工, leader(一般员工到这里就到顶了), CXO
  • 除非技术栈和技能点过硬, 否则跳槽比较困难, 创业公司因为没有固定的晋升体系, 并且职位和工作比较难被其他公司认可

大公司

互联网大公司有很多创业公司和传统软件公司所没有的优势, 而且业界容易被业界认可

优势:

  • 会有机会和一群名校毕业, 智商很高, 头脑清晰的人一起工作, 有时间压力就是动力(比如猪厂, 可能随便一个写脚本的都是清华毕业的)
  • 技术基本雄厚, 外界知名的开源项目可能很快就能在内部造出合适的轮子, 并且有专人维护跟进, 代码提交, 风格和审核有一定的标准, 不容易导致代码库紊乱
  • 跳槽相对容易, 俗称大公司镀金, 很多大公司晋升体系是被认可的, 如百度/腾讯的T序列, 阿里的P序列.
  • 大量的内部或者外部知识分享或培训, 完整的入职培训

缺陷:

  • 历史包袱比较严重, 可能一次简单的版本升级会引出大量的bug和不兼容, 写代码总是要小心翼翼.
  • 流程/规范/会议较多, 大量的时间被浪费在其中, 然而我们总想着让我静静
  • 不同团队间协作比较困难
  • 内部轮子过多, 每个小部门都喜欢自己造论子, 正如技术的特点: 总是短期被重视,长期被忽视.

总的来说, 我更喜欢大公司, 完整的工作体验和晋升流程 浓厚的技术积累适合刚毕业的同学们快速成长又能有一份不菲的薪资.

工作的选择并不是很容易做出, 要考虑的地方很多, 比如:

  1. 个人喜欢, 对公司的爱好, 对岗位的爱好
  2. 工作是否能够让你获得成就感
  3. 喜欢离家近一些还是离家远一些, 喜欢城市环境好一些还是城市节奏快一些

最后, 希望每个人都能拿到自己梦想的offer, 进行自己心仪的公司

TED心得

最近看了个TED, 感触颇深, 来干了这碗毒鸡汤

  • 大多数人的梦想是实现不了的, 然而也并不重要, 但是人总要有梦想吗, 万一实现了呢
  • 有些事情是可以自然而然的发生的, 如果你选择了这样的事情, 多停留一段时间一定会有收获的
  • 无论是什么年龄段的人, 攒人品是没必要的, 当前的痛苦并不一定带来未来的快乐, 未来的快乐也代替不了现在的快乐, 所以要活在当下, 珍惜身边人
  • 心情愉悦很重要

参考链接

TEDxDUFE:于宙 我们这一代的困惑

]>
<p>本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循<a href="http://creativecommons.org/licenses/by-nc-sa/3.0/deed.zh" target="_blank" rel="external">署名-非商业用途-保持一致</a>的创作共用协议.</p> <blockquote> <p>背景介绍: 第一家工作的公司是一家跨国外企安全公司, 骄傲的称自己不是互联网公司而是传统软件公司, 第二家公司是当下最热的知识分享社区, 创业公司. 第三家公司是挤走谷歌, 曾一度称霸中国的搜索引擎公司, 体量很大的著名三大互联网公司之一.</p> </blockquote> <p><strong>每一家公司我都接触的不是很久, 没能用一生的时间来体验一个公司, 只能说一下我在短时间看到的优势和缺陷</strong></p>
Linux内核设计与实现读书笔记 http://andrewliu.in/2016/11/15/Linux内核设计与实现读书笔记/ 2016-11-15T02:13:27.000Z 2017-06-17T22:14:00.000Z 本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循署名-非商业用途-保持一致的创作共用协议.

Unix强大的根本原因:

  1. Unix简洁, 提供几百个系统调用, 设计目的明确
  2. Unix中所有东西都被当做文件对待
  3. Unix内核和相关系统工具是用C语言开发的, 移植能力强大
  4. Unix进程创建迅速, 有独特的fork机制
  5. Unix提供简单稳定的进程间通信元语

Linux是类Unix系统, 借鉴了Unix设计并实现了Unix的API.
应用程序通常调用库函数(如C库函数)再由库函数通过系统调用界面, 让内核代其完成各种任务.

  • Linux支持动态加载内核模块
  • Linux支持对称多处理(SMP)机制
  • Linux为抢占式内核
  • Linux并不区分线程和其他的一般进程
  • Linux提供具有设备类的面向对象的设备模型, 热插拔事件, 以及用户控件的设备文件系统

中断和中断处理

中断是一种解决处理器和速度差异的方案, 只有在硬件需要的时候再向内核发出信号. 中断本质上是一种特殊的电信号.

  • 内核响应特定中断, 然后内核调用特定的中断处理程序, 终端处理程序是设备驱动程序的一部分
  • Linux中的终端处理程序是不可重入的, 同一个中断处理程序不会被同时调用
  • 中断上下文不可以睡眠(我理解当前被中断的程序再中断处理结束后需要继续执行)
  • 中断处理程序不在进程上下文中进行, 他们不能阻塞
  • 中断处理分为两部分, 上半部为中断处理程序, 要求尽可能快的执行, 下半部(用于减少中断处理程序的工作量)执行与中断处理密切相关但中断处理程序本身不执行的工作
  • 下半部的实现方法 软中断、tasklet、工作队列,

中断机制的实现: 设置产生中断, 通过电信号给处理器的特定管脚发送一个信号, 处理器听着当前处理工作, 关闭中断系统, 然后调到内存中预定义的位置(中断处理程序的入口点)开始执行.计算终端号, do_IRQ()对接收的中断进行应答, 禁止这条线上的中断传递.

内核同步

对于共享资源, 如果同时被多个线程访问和操作, 就可能发生各线程之间相互覆盖共享数据, 造成访问数据不一致.

同步实现通过主要锁机制对共享资源进行加锁, 只有持有锁的线程才能操作共享资源, 其他线程睡眠(或者轮询). 资源操作完成后, 持有锁的线程释放锁, 由等待线程抢锁.

内核同步方法:

  1. 原子操作
  2. 自旋锁, 特性是当线程无法获取锁, 会一直忙循环(忙等)等待锁重新可以, 适用于短期轻量级加锁
  3. 读/写自旋锁(共享/排它锁), 一个或多个任务可以并发的持有读者锁, 写者锁只能被一个写任务持有.
  4. 信号量(睡眠锁), 如果一个任务试图获得一个被占用的信用量时, 信号量会将其推进一个等待队列, 然后让其睡眠. 当信号量可用后, 等待队列中的任务会被唤醒. 适用于锁被长期占用的时候.
  5. mutex(计数为1的信号量), 这个是编程中最常见的.
  6. 顺序锁
  7. 屏障(barriers), 用于确保指令序列和读写的执行顺序

内核中造成并发的原因:

  • 中断, 几乎可以再任何时刻异步发生, 可能随时打断当前正在执行的代码
  • 软中断和tasklet, 内核能在任何时刻唤醒或调度软中断或tasklet, 打断当前正在执行的代码
  • 内核抢占
  • 睡眠及与用户空间的同步
  • 对称多处理, 多个处理器同时执行代码

内存管理

内核把物理页作为内存管理的基本单位, 内存管理单元(MMU, 管理内存并将虚拟地址转换为物理地址)通常以页为单位来管理系统中的页表.

内核把也划分为不同的区(zone), 使用区对具有相似特性的页进行分组

1
2
3
4
5
6
7
8
9
10
11
// <linux/gfp.h> 该函数分配2的order次方个连续`物理页`, 返回指针指向第一个页的page结构体
static inline struct page *
alloc_pages(gfp_t gfp_mask, unsigned int order)
// 释放物理页
extern void free_pages(unsigned long addr, unsigned int order);
//<linux/slab.h>以字节为单位分配一块内核内存(物理上连续)
static __always_inline void *kmalloc(size_t size, gfp_t flags)
//释放kmalloc分配的内存块
void kfree(const void *);

虚拟文件系统

虚拟文件系统为用户控件程序提供了文件和文件系统相关接口.

文件的元数据, 被存储在一个单独的数据结构中, 被称为inode(索引节点)

虚拟文件系统(VFS)有四个主要的对象模型:

  • 超级块对象, 代表一个具体的已安装文件系统, 存储特定文件系统的信息
  • 索引节点对象, 代表一个具体文件, 包含内核在操作文件或目录时需要的全部信息, 一个索引节点代表文件系统中的一个文件,
  • 目录项对象, 代表一个目录项, 是路径的一个组成部分, VFS把目录当做文件处理, 目录项对象没有对应的磁盘数据结构
  • 文件对象, 代表进程打开的文件, 进程直接处理的是文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// <linux/fs.h> 文件对象的数据结构
struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;
/*
* Protects f_ep_links, f_flags.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
struct mutex f_pos_lock;
loff_t f_pos;
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;
u64 f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data;
#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_head f_ep_links;
struct list_head f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
} __attribute__((aligned(4))); /* lest something weird decides that 2 is OK */

块I/O层

系统中能够随机访问固定大小数据片(chunks)的硬件设备称作块设备, 如硬盘. 按照字符流的方式被有序访问的硬件设备称为字符设备, 如键盘

1
# <linux/bio.h>I/O设备基本容器由bio结构体表示
  • I/O调度程序用于管理块设备的请求队列, 决定队列中的请求排列顺序以及什么时刻派发请求到挂设备. 这样有利于减少磁盘的寻址时间, 从而提高全局的吞吐量
  • linux实际使用的I/O调度程序有linux电梯, 最终期限I/O调度, 预测I/O调度程序, 空操作的I/O调度程序

进程地址空间

内核需要管理用户空间中进程的内存, 这个内存称为进程地址空间, 系统中所有进程之间以虚拟方式共享内存.

进程地址空间由进程可寻址的虚拟内存组成, 每个进程有32位或64位地址空间.

虚拟地址空间, 可被访问的合法地址空间称为内存区域:

  • 可执行文件代码的内存映射, 称为代码段
  • 可执行文件的已初始化全局变量的内存映射, 称为数据段
  • 包含未初始化全局变量,bss(block started by symbol)段的零页的内存映射
  • 用于进程用户空间栈的零页内存映射
  • 每一个如C库或动态链接程序等共享库的代码段、数据段和bss会被载入进程的地址空间
  • 任何内存映射文件
  • 任何共享内存段
  • 任何匿名的内存映射, 如malloc分配的内存

内核使用内存描述符结构体表示进程的地址空间, 内存描述符由mm_struct(<linux/sched.h>)结构体表示. 内核线程没有进程地址空间, 也没有相关的内存描述符, 所有内核线程没有用户上下文

应用程序操作的对象是映射到物理内存上的虚拟内存, 而处理器操作的是物理内存, Linux使用三级页表完成地址转换, 每个虚拟地址作为索引指向页表, 页表项则指向下一级的页表. 在多级页表中通过TLB(translate lookaside buffer)作为一个虚拟地址映射到物理地址的缓存

参考链接

<内核设计与实现>

]>
<p>本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循<a href="http://creativecommons.org/licenses/by-nc-sa/3.0/deed.zh" target="_blank" rel="external">署名-非商业用途-保持一致</a>的创作共用协议.</p> <p><strong>Unix强大的根本原因:</strong></p> <ol> <li>Unix简洁, 提供几百个系统调用, 设计目的明确</li> <li>Unix中<code>所有东西都被当做文件对待</code></li> <li>Unix内核和相关系统工具是用C语言开发的, 移植能力强大</li> <li>Unix进程创建迅速, 有独特的fork机制</li> <li>Unix提供简单稳定的进程间通信元语</li> </ol> <blockquote> <p>Linux是类Unix系统, 借鉴了Unix设计并实现了Unix的API.<br>应用程序通常调用库函数(如C库函数)再由库函数通过系统调用界面, 让内核代其完成各种任务.</p> </blockquote>
Google Protobuf源码剖析(一) http://andrewliu.in/2016/11/07/Google-Protobuf源码剖析-一/ 2016-11-07T13:52:00.000Z 2017-06-17T22:14:00.000Z 本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循署名-非商业用途-保持一致的创作共用协议.

很久之前写过一篇Google protobuf(C++) 学习笔记. google protobuf被大量用于公司的RPC通信中作为序列化和序列化工具, 高于JSON和XML的性能值得拥有. 刚好最近有时间, 准备强读一发google protobuf源码

前提

本文所有所有示例均基于官方示例addressbook.proto:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package tutorial;
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phone = 4;
}
message AddressBook {
repeated Person person = 1;
}
  • required字段初值是必须要提供的, 否则字段的便是未初始化的, 序列化的时候必须对required初始化
  • optional字段如果未进行初始化,那么一个默认值将赋予该字段
  • repeated字段可以理解为数组,

每个变量后的数字为标签, 用于标示了字段在二进制流中存放的位置

运行protoc -I=./ --cpp_out=./ ./addressbook.proto通过protoc来生成 .h和.cpp文件.

那么.h文件中到底生成了什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# Person类的基类为::google::protobuf::Message类
class Person : public ::google::protobuf::Message
# enum类型的数据, 可以通过Person::MOBILE来访问
typedef Person_PhoneType PhoneType;
static const PhoneType MOBILE =
Person_PhoneType_MOBILE;
static const PhoneType HOME =
Person_PhoneType_HOME;
static const PhoneType WORK =
Person_PhoneType_WORK;
# required和optional的普通类型, 产生的函数族都是差不多的. 对于每个字段会生成一个has函数、clear清除函数、set函数、get函数
// required string name = 1;
bool has_name() const; # has_xxx
void clear_name(); # clear_xxx
const ::std::string& name() const;
void set_name(const ::std::string& value);
void set_name(const char* value);
void set_name(const char* value, size_t size);
::std::string* mutable_name();
::std::string* release_name();
void set_allocated_name(::std::string* name);
# repeated类型有些不同, 没有has_xxx函数族
// repeated .tutorial.Person.PhoneNumber phone = 4;
int phone_size() const;
void clear_phone();
const ::tutorial::Person_PhoneNumber& phone(int index) const;
::tutorial::Person_PhoneNumber* mutable_phone(int index);
::tutorial::Person_PhoneNumber* add_phone();
::google::protobuf::RepeatedPtrField<::tutorial::Person_PhoneNumber >*
mutable_phone();
const ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >&
phone() const;

Protobuf主要类

  • Message类, 是一个抽象层, 记录了一个proto文件里的所有内容. Message继承自MessageLite
  • MessageLite类, 是一个轻量级的接口协议, 这个接口由所有协议的消息对象来实现, 这个类中包含大量定义的虚函数和纯虚函数. 使用MessageLite来生成代码, 需要在.proto中加入下面这行
1
option optimize_for = LITE_RUNTIME;
  • Arena类主要用于协议消息的内存分配和释放, 并且分配内存是线程安全的.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 内存块的链表组织结构
// Blocks are variable length malloc-ed objects. The following structure
// describes the common header for all blocks.
struct Block {
void* owner; // &ThreadCache of thread that owns this block, or
// &this->owner if not yet owned by a thread.
Block* next; // Next block in arena (may have different owner)
// ((char*) &block) + pos is next available byte. It is always
// aligned at a multiple of 8 bytes.
size_t pos;
size_t size; // total size of the block.
GOOGLE_ATTRIBUTE_ALWAYS_INLINE size_t avail() const { return size - pos; }
// data follows
};
// Arena核心的初始化函数
void Arena::Init() {
lifecycle_id_ = lifecycle_id_generator_.GetNext();
blocks_ = 0;
hint_ = 0;
owns_first_block_ = true;
cleanup_list_ = 0;
// options为构造函数的参数, 一个配置结构体ArenaOptions
if (options_.initial_block != NULL && options_.initial_block_size > 0) {
GOOGLE_CHECK_GE(options_.initial_block_size, sizeof(Block))
<< ": Initial block size too small for header.";
// Add first unowned block to list.
Block* first_block = reinterpret_cast<Block*>(options_.initial_block);
first_block->size = options_.initial_block_size;
first_block->pos = kHeaderSize;
first_block->next = NULL;
// Thread which calls Init() owns the first block. This allows the
// single-threaded case to allocate on the first block without taking any
// locks.
first_block->owner = &thread_cache();
SetThreadCacheBlock(first_block);
AddBlockInternal(first_block);
owns_first_block_ = false;
}
  • Reflection类, 是一个用于动态访问和修改协议消息各种变量域的类(也就是我们常说的反射机制), 该类只在Message中实现(MessageLite中没有)
  • Descriptor类, 用于描述协议消息,通过Message::GetDescriptor()来获取Message对应的

待续…

]>
<p>本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循<a href="http://creativecommons.org/licenses/by-nc-sa/3.0/deed.zh" target="_blank" rel="external">署名-非商业用途-保持一致</a>的创作共用协议.</p> <blockquote> <p>很久之前写过一篇<a href="http://andrewliu.in/2016/06/05/Google-protobuf-C-%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/">Google protobuf(C++) 学习笔记</a>. <code>google protobuf</code>被大量用于公司的RPC通信中作为序列化和序列化工具, 高于JSON和XML的性能值得拥有. 刚好最近有时间, 准备强读一发<code>google protobuf源码</code></p> </blockquote> <h2 id="前提"><a href="#前提" class="headerlink" title="前提"></a>前提</h2><p>本文所有所有示例均基于官方示例<code>addressbook.proto</code>:</p>
某度实习总结 http://andrewliu.in/2016/10/29/某度实习总结/ 2016-10-28T22:56:00.000Z 2017-06-17T22:14:00.000Z 本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循署名-非商业用途-保持一致的创作共用协议.

人呀,有时候也要考虑一下历史的行程,也要总结一下人生的经验。算算一个人来北京实习一年了,上半年在某乎,下半年在某度,而北京这个地方却没有给我留下太深的印象,喜欢的地方不多。马上要离开某度了,简单的总结一下自己吧,同时发泄一下最近消极的心情。过段时间可能会再写一篇,对传统互联网/创业公司/大体量公司的择业思考(又挖新坑)。

文章大概几个部分吧,北京印象,人文关怀,工作感受,个人心得。文章中可能会出现一些某乎和某度的对比,仅个人见解。本文一切均个人视角请勿对号入座,想到哪里写到哪里。

北京印象

2015年10月,带着对未来的憧憬踏入祖国帝都。

  • 去坐地铁的时候,让我印象深刻的是2号线地铁(好像是2号线)竟然没有护栏,简直恐怖。
  • 北京的生活从自如租的的带阳台的出租屋开始。

2015年10月,正式进入互联网行业实习。

  • 初入某乎实习,迎接我的是北京的雾霾天,我从来想到个一个地方的雾霾会那么严重,严重到大街上的所有人都不得不带着口罩,空气都被雾霾充斥,直到春夏雾霾渐歇。
  • 某乎的冬天,在北京完成了人生第一次滑雪,只记得滑完雪,屁股好疼,内裤都是冰水。某乎的冬天,也完成了人生的第一次射箭,然而印象了了。
  • 北京的冬天,会下雪,和我的老家一毛一样的。
  • 某乎的团建和年会,给我的北京之旅留下了浓重的一笔,大家一起写剧本,一起演舞台剧,一起拍摄某乎五周年的视频频段,然而最后不得不和某乎的朋友说再见。

2016年4月,进入互联网三巨头之一实习。

  • 初入某度,身边陌生,周围的同事也并不像某乎那样和蔼融洽。
  • 某度的北京印象,平平淡淡,后文详述。

  • 北京的人们,总是匆匆忙忙赶着地铁或者公交,快节奏的生活是在远方的小城中体验不到的。

  • 北京的地上,总是到处拥堵,尤其是在互联网创造奇迹的西二旗,听说小米新发布的小米Mix意思是Made in 西二旗
  • 北京的景点,多是众众众众,天安门,故宫,长城,后海,圆明园,清华北京,都有去过,体会这个城市过去时代的繁华。

人文关怀

叨叨了这么多才进入正题,年纪大了还是静不下来。

在某度,没有感受到太多的人文关怀,毕竟人那么多,哪有那么周全的照顾到你的情绪,还是要自己动手丰衣足食才行。

  1. 每天一人一份的水果或者酸奶
  2. 人均配置hp(i5/ssd/8G)加samsuang的19寸显示器,真心对眼睛不好,每天都拖着酸痛的眼睛回家,还是喜欢用自己的retina显示屏和大屏幕1080p的显示器,由奢入俭难呀。
  3. 某度半年也没有获得一个某熊吉祥物,这是我深深的怨念,给每个人入职的时候发一个不好吗,这样不是还能增加公司的归属感吗。
  4. 一周一次全公司抢票式报名的公开课,而我并未体验过,工作太忙?不感兴趣?我也不知道。
  5. 对新实习生的培训体系不健全,刚入职的时候全靠自己搜索,问身边的老司机,总算度过了当年痛苦的时光。
  6. 无限量供应的热水。还有咖啡,各种速溶冲泡饮料,然而喝了几天就再也不想碰了。
  7. 吃饭非免费,但可以报销,伙食并没有某乎的免费午餐那么好(不知道现在还有没有),毕竟我在某乎胖了十斤,而在某度又把这十斤瘦回去了。
  8. 晚上加班晚回去可以打车报销,然而我会说:我住的不是特别远(四十分钟的路)根本打不到车,女朋友经常嫌弃我回家太晚,其实心里挺内疚的,在这里很多时候,因为需求太多太急不得不加班,但是生活不仅仅有工作

一到秋冬季节,北京的雾霾就非常严重,前段时间又是头疼,又是感冒发烧,我只能把这个锅丢给北京雾霾了(毕竟我是经常去健身房锻炼,还会经常和妹纸打羽毛球好吗),听说一次北京雾霾能让医院增收10%,能让口罩产业增收300%(逃

工作收获

周围的同事都比较闷,或许是程序猿的共性?大家基本不聊天,吃饭的时候偶尔聊天也是和代码架构相关,但这些都掩盖不了周围的人们实力很强,和一群优秀的人在一起工作真是一种享受。

在某乎,最大的工作收获是:写代码要覆盖测试,虽然有时候测试写的不全会导致bug。然而即使有大量测试,也依然不能拯救某乎主站大姨妈式的服务器向你提了一个问题。

在某度,最大的工作收获是:学会了设计思维,写程序只是工作中很小很小的一部门,大部分时候应该是去全面思考当前工作的如何实现比较优秀。整理好自己的架构设计思路,拉上几个人一番头脑风暴,会有更多优化的地点,然后再开始写代码,一般效率更高。

在某乎,很多时候没有所谓的规范,没有日志规范,没有强制的代码规范,没有注释要求,带来的是各部门有自己规范或者说传统(代码段也有口口相传的),最后很难相互协作,出现问题只能看源码。

在某度,有强制的代码审查,有大量的注释(当然也有一些老坑需要自己看源码),注释写起来虽然浪费时间,但是有助于梳理函数功能和福泽后人。在某度所有的东西都规范化流程化了,虽然很好,但很多时间有大量的时间浪费在权限申请了,而且经常你不得不push别人帮你开通权限申请。

在某乎,这个年轻的公司,同事即朋友,代码库不大,跨部门开发总能找到合适的人结对编程,一起完成当前项目的推进,某乎的编程经历还是很开心的。

在某度,我最讨厌的就是跨部门合作,不是沟通的问题,而是很多跨部门合作是因为一方弱需求一方强需求,导致总有一方不配合,或者推出来一个新来的实习生背锅作为对接人,然而这有什么卵用,一问三不知?最后还不如自己好好看源码好,甩开膀子开干。

在某乎,我收获了好几个真诚的工友,现在依然可以偶然联系,微信吹吹流弊,ins互刷赞,一起膜蛤续命,还有我敬爱的郭教授(手动点赞)。

在某度,我没有收到几个朋友,大家都各自为政,每个人都有自己的家庭,也失去了年轻时候的朝气。
在某度,有些流程很拖沓,我不喜欢,做人做事干净利落,今天拖明天,明天拖后天,有什么意思?

在某乎,一个知识性社区,也是我现在依然每天都在刷,获取知识和时事的来源之一。记得当时,偶尔会有知友来公司送吃的以表感谢,感觉真是很好。

在某度,我来实习的时候,刚好是各种坏消息接踵而来的时候,我见到了很多big news,血友病卖吧,魏则西事件,某度外卖事件,PPT总监事件,各种无底线推广等等,虽然我也赞同技术无罪论,但我不喜欢这种为了追求kpi,为了追求盈利而无底线的做法,既然在国内属于这个技术顶尖的公司,就应该有这样公司的担当,勇于承认错误并改正,就像google那样选择不作恶。

在某度,我要强力吐槽一下,标配开发机,号称4G,双核CPU的开发机成天卡成狗,随便编译个代码就不能动了,你家开发机这么多人吐槽也不知道优化一下吗?(黑人问号,手动滑稽)。

不管怎么样,某乎和某度都是很赞的公司。

个人小结

说一些自己工作中的经验或者教训吧。

  1. 工作中,需要别人协作的时候,要多push,自己的事情自己不推,还有谁在乎呢?你不管,别人也不管,然而锅还是在你身上甩不掉。
  2. 工作中,有问题就问,不要怕被嘲笑,被鄙视。你不问清楚,最后吃亏的一定是你。我听说很多不那么正面的话,python的这个用法你都不会?这个工作随便一个实习生都能做这个算法你都不会,是怎么通过面试的?。虽然明白这个道理,但依然没有做太好。
  3. 工作中,要勇于争取自己的利益,你不争取,你的那份就是别人或者充公了,人非圣贤,孰能无钱,我很缺钱,毕竟北京生活成本那么高,实习工资那么少?
  4. 不要人云亦云,做好自己的本职工作,无中生有的东西,你再帮他说一遍,等于你也有责任吧
  5. 有一个严格要求对你的人是很好的,这是对你负责,如果一个mentor对你不管不问,那他不是一个好的mentor。认真严格也是一种品质,这是mentor教给我的。
  6. 把当前手头的工作写入邮件发给别人review,这样一是可以给未来留下工作梳理依据,二是让别人知道你当前在做什么,这很重要
  7. 离职的时候,尽量把自己的工作收尾,不要给人留下不可依赖的印象。

有时候很庆幸自己能够走进某度实习,感谢自己mentor和manager, 祝各自安好。

]>
<p>本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循<a href="http://creativecommons.org/licenses/by-nc-sa/3.0/deed.zh" target="_blank" rel="external">署名-非商业用途-保持一致</a>的创作共用协议.</p> <blockquote> <p>人呀,有时候也要考虑一下历史的行程,也要总结一下人生的经验。算算一个人来北京实习一年了,上半年在某乎,下半年在某度,而北京这个地方却没有给我留下太深的印象,喜欢的地方不多。马上要离开某度了,简单的总结一下自己吧,同时发泄一下最近消极的心情。过段时间可能会再写一篇,对传统互联网/创业公司/大体量公司的择业思考(又挖新坑)。</p> </blockquote> <p>文章大概几个部分吧,<strong>北京印象,人文关怀,工作感受,个人心得</strong>。文章中可能会出现一些某乎和某度的对比,仅个人见解。<strong>本文一切均个人视角请勿对号入座,想到哪里写到哪里。</strong></p>
Linux进程创建和调度学习笔记 http://andrewliu.in/2016/10/08/Linux进程创建和调度学习笔记/ 2016-10-08T12:45:56.000Z 2017-06-17T22:14:00.000Z

读书笔记

进程管理

进程是处于执行期的程序, 包含代码段, 打开描述符, 挂起信号, 内核内部数据, 处理器状态, 一个或多个具有内存映射的内存地址空间及一个或多个执行线程.
线程是进程中活动对象, 包含独立的程序计数器, 栈和一组进程寄存器. 线程间可共享虚拟内存, 但每个都拥有各自的虚拟处理器

  • 内核将进程的列表放在一个双向循环链表中, 每项为task_struct
  • 进程执行系统调用或者异常处理才会陷入内核空间(内核态)
  • Linux所有进程都是PID为1的init进程的后代

每个进程或线程都有三个数据结构,分别是 struct thread_info, struct task_struct内核栈

task_struct 结构体中的主要元素:

  • struct thread_info *thread_info。thread_info 指向该进程/线程的基本信息。
  • struct mm_struct *mm。mm_struct 对象用来管理该进程/线程的页表以及虚拟内存区。
  • struct mm_struct *active_mm。主要用于内核线程访问主内核页全局目录。
  • struct fs_struct *fs。fs_struct 是关于文件系统的对象。
  • struct files_struct *files。files_struct 是关于打开的文件的对象。
  • struct signal_struct *signal。signal_struct 是关于信号的对象。

进程创建

通过fork和exec来实现, fork() 拷贝当前进程创建一个子进程, exec() 负责读取可执行文件并将其载入地址空间开始执行.

  • Linux的 fork() 使用写时复制(copy-on-write)页实现, 资源的复制只有在需要写入的时候才进行. fork() 的实际开销是复制父进程的页表以及给子进程创建唯一的进程描述符
  • Linux通过 Clone() 实现 fork(). fork() -> clone(SIGCHLD) -> do_fork() -> copy_process()

copy_process()的工作:

  1. 调用dup_task_struct为新进程创建一个内核栈, thread_info结果和task_struct, 值保持与当前进程相同
  2. 检查当前用户进程数未超出给定分配资源的限制
  3. 子进程部分信息被清0或重设为初始值
  4. 子进程状态被设置为 TASK_UNINTERRUPTIBLE, 保证他不会投入运行
  5. 更新 task_struct的flags成员
  6. 调用 alloc_pid为新进程分配有效的PID(此时才分配新pid)
  7. 根据传递给clone的参数, 设置拷贝或共享的资源.
  8. 返回一个指向子进程的指针.

vfork 与 fork 的区别是, vfork不拷贝父进程的页表项

线程

  • 线程在Linux被视为一个与其他进程共享某些资源的进程
1
2
# 线程同样通过clone创建, 只是指定共享的资源不同
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);

内核线程

  • 内核线程用于完成内核需要在后台执行的一些操作
  • 内核线程和普通进程的区别在于内核线程没有独立的地址空间, 只在内核空间运行, 所有的内核线程共享内核地址空间
  • 内核线程可以被调度, 可以被抢占
  • 内核线程只能由其他内核线程创建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# <linux/kthread.h>, 内核线程创建
struct task_struct *kthread_create_on_node(int (*threadfn)(void *data),
void *data,
int node,
const char namefmt[], ...);
#define kthread_create(threadfn, data, namefmt, arg...) \
kthread_create_on_node(threadfn, data, NUMA_NO_NODE, namefmt, ##arg)
# 创建并运行, kthread_create创建, wake_up_process唤醒
#define kthread_run(threadfn, data, namefmt, ...) \
({ \
struct task_struct *__k \
= kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); \
if (!IS_ERR(__k)) \
wake_up_process(__k); \
__k; \
})

进程终止

进程终止一般是调用 exit(), 最终基本是通过 do_exit()完成.

  1. task_struct的标志成员设置为 PF_EXITING
  2. 调用del_timer_sync()删除任一内核定时器
  3. 调用exit_mm()函数释放进程占用的 mm_struct
  4. 调用sem_exit()函数
  5. 调用exit_file()和exit_fs() 分别递减文件描述符, 文件系统数据的引用计数
  6. 执行一些其他的退出动作
  7. 调用exit_notify()向父进程发送信号, 为其子进程找继父(线程组其他线程或init进程). 进程状态设置为EXIT_ZOMBIE
  8. 调度schedule()切换到新的进程. do_exit() 永不返回.

进程终止的清理工作和进程描述符的删除是分开执行的. 进程描述符的删除由wait4()系统调用完成. 两步完成所有资源才释放完成.

进程调度

进程调度程序可以看做在可运行态进程之间分配有限的处理器时间资源的内核子系统. Linux在2.6.23内核版本中使用了完全公平调度算法(CFS)

  • 进程分为I/O消耗型和处理器消耗型
  • Linux调度器以模块方式提供, 允许多种不同的可动态添加的调度算法并存.
  • Linux使用完全公平调度(CFS), 允许每个进程运行一段时间, 循环轮转, 选择运行最少的进程作为下一个运行进程(不采用通过nice计算并分配给进程时间片做法), 分配给进程的是一个处理的使用比重. nice值在CFS中被作为进程获得处理器运行比的权重. 高nice值获得更低的处理器权重. 为防止可运行进程过趋于无限时, 进程各自获得处理器使用比趋于0, CFS为每个进程设置一个最小粒度
  • CFS使用红黑树组织可运行进程队列, 选择可运行进程中最小vruntime(进程虚拟运行时间)的任务. 进程可运行或被fork()后被加入红黑树
  • Linux还提供两种实时调度策略(均为静态优先级) SCHED_FIFO和SCHED_RR由特殊的实时调度器管理. FIFO就是先入先出的调度策略. RR是一种带时间片的先入先出的调度策略
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# <linux/sched.h> struct sched_entity对进程运行时间做记录
struct sched_entity {
struct load_weight load; /* for load-balancing */
struct rb_node run_node;
struct list_head group_node;
unsigned int on_rq;
u64 exec_start;
u64 sum_exec_runtime;
u64 vruntime;
u64 prev_sum_exec_runtime;
u64 nr_migrations;
#ifdef CONFIG_SCHEDSTATS
struct sched_statistics statistics;
#endif
#ifdef CONFIG_FAIR_GROUP_SCHED
int depth;
struct sched_entity *parent;
/* rq on which this entity is (to be) queued: */
struct cfs_rq *cfs_rq;
/* rq "owned" by this entity/group: */
struct cfs_rq *my_q;
#endif
#ifdef CONFIG_SMP
/*
* Per entity load average tracking.
*
* Put into separate cache line so it does not
* collide with read-mostly values above.
*/
struct sched_avg avg ____cacheline_aligned_in_smp;
#endif
};

Linux优先级分两种. nice值(取值-20到+19, nice值最大优先级越低); 实时优先级(取值0到99, 实时优先级数值月大优先级越高)

上下文切换

schedule()执行进程调度时, 调用kernel/sched.c中的context_switch()函数执行:

  • 调用声明asm/mmu_context.h中的switch_mm(), 该函数把虚拟内存从上一个进程切换到新的进程中.
  • 调用<asm/system.h>中的switch_to(), 该函数负责从上一个进程的处理器状态切换到新进程的处理器状态. 包括保存、回复栈信息和寄存器信息

参考链接

  • Linux内核设计与实现 - 原书第3版
]>
<blockquote> <p>读书笔记</p> </blockquote> <h2 id="进程管理"><a href="#进程管理" class="headerlink" title="进程管理"></a>进程管理</h2><blockquote> <p>进程是处于执行期的程序, 包含代码段, 打开描述符, 挂起信号, 内核内部数据, 处理器状态, 一个或多个具有内存映射的内存地址空间及一个或多个执行线程.<br>线程是进程中活动对象, 包含<code>独立</code>的程序计数器, 栈和一组进程寄存器. <code>线程间可共享虚拟内存, 但每个都拥有各自的虚拟处理器</code></p> </blockquote> <ul> <li>内核将进程的列表放在一个双向循环链表中, 每项为<code>task_struct</code></li> <li>进程执行系统调用或者异常处理才会陷入内核空间(内核态)</li> <li>Linux所有进程都是PID为1的init进程的后代</li> </ul>
深入探索C++对象模式读书笔记 http://andrewliu.in/2016/09/30/深入探索C-对象模式读书笔记/ 2016-09-30T06:49:01.000Z 2017-06-17T22:14:00.000Z

什么是C++对象模型?

  1. 语言中直接支持面向对象程序设计的部分.
  2. 对于各种支持的底层实现机制.

Object Lessons

  1. C++封装并未增加布局成本, 数据成员内含在class object中(像struct), 成员函数不会出现在object中, 非内敛函数只会诞生一个实例, 内联函数对每个使用者产生一个实例
  2. C++在布局及存取时间上主要的额外负担是由virtual引起的
    • virtual function支持动态绑定
    • virtual base class实现单一而被共享的基类实例, 多用于多继承中

C++ Object Model

  1. 简单对象模式, object由很多slot组成, 每个slot(指向member的指针)指向一个memeber(包含数据成员和成员函数).
  2. 表格驱动对象模型, 将members相关信息分别放到data member table(持有data member)和member function table(member function的指针)中, class object本身包含指向两个表格的指针
  3. C++对象模型, 非静态数据成员配置在class object内, 静态数据成员存放在个别class object外.

C++对象模型之虚函数:

  1. 每个class(类)产生一堆指向虚函数的指针, 放在virtual table(vtbl)中
  2. 每个object(实例)包含一个指针(vptr), 指向相关的vitual table. vptr的设置和重置由class的构造/析构/拷贝赋值操作符完成. 每个class的type_info object(支持RTTI)也经由virtual table被指出来.

书中给出了一个struct和class结合使用的建议: 当要传递一个复杂的class object的全部或部分到C函数中, struct声明可以将数据封装起来, 并保证拥有与C兼容的空间布局.

C++程序设计模型支持三种programming paradigms(编程范式):

  • 程序模型, 像C一样
  • 抽象数据类型模型
  • 面向对象模型

多态的主要用途是经由一个共同的接口来影响类型的封装, 这个接口通常被定义在一个抽象的base class中

The Semantics of Constructors

Default Constructor

四种情况会造成编译器必须为未声明构造函数的 classes 合成一个 default constructor

  1. 带有Default Constructor的Member Class. 如果class没有任何构造函数, 但包含一个member object, 而后者有默认构造函数, 编译器会为该clas合成一个default constructor(只是为了满足编译器的需求). 如果class有默认构造函数, 且包含member object, 则编译器会扩张已存在的构造函数.
  2. 带有Default Constructor的Base Class. 一个没有任何构造函数的class派生自一个带有default constructor的 base class, 这个派生类中的default constructor将会被合成.
  3. 带有一个Virtual Function的class. 默认构造函数正确的初始化每个class的vptr. class声明一个虚函数会合成默认构造函数
  4. 带有一个Virtual Base class的Class, class派生自一个继承链, 其中有一个或者更多virtual base classes

带有虚函数类在编译器:

  1. 编译期一个virtual function table会被编译器产生, 内放class 的virtual functions地址
  2. 每个class object中, 一个额外的 pointer member(vptr) 被编译器合成出来, 内含相关与class vtbl的地址.

Copy Constructor

1
2
3
4
5
6
# 三个场景使用拷贝构造函数
class Base;
Base a;
Base b = a; //1. 显示拷贝
void (Base x); //2. object参数值传递
return b; //3. 值传递的函数返回object
  1. class未提供显式copy constructor时, 默认拷贝构造函数会把内建或派生的 data member 的值, 从某个object拷贝到另一个object上, 但不会拷贝其中的member class object

Member Initialization List

以下情况必须使用成员初始化列表:

  1. 当初始化一个 reference member 时
  2. 当初始化一个 const member 时
  3. 当调用一个 base class 的 constructor, 而它拥有一组参数时
  4. 当调用一个 member class 的 constructor, 而它拥有一组参数时
  • 初始化成员列表的顺序是由class中的members声明顺序决定的
  • 调用member function设定一个member的初值可行, 因为与此object 相关的 this指针已经被建构妥当
1
2
3
4
X::X(int val) :
i(xfoo(val)), // xfoo必须是 member function
j(val) {
}

The Semantics of Data

Data Member Layout

  • 非静态数据成员在class object的排列顺序和其被声明的顺序一样
  • 静态成员函数不会放入对象布局中, 会被放入data segment

C++标准对布局持放任态度.

Data Member get/set

  • static data member在class object中存取不会招致任何空间或执行时间上的额外负担(静态数据成员不在class object内部)
  • nonstatic data member存放在 class object 内, 存取效率和 c struct member 或 nonderived class member 一样.

继承(非虚继承)不会不会增加空间和存取时间上的额外负担

菱形继承的解决方案是导入虚拟继承

虚继承的实现: class 如果内包含一个或多个 virtual base class subobjects, 将被分割成两部分: 一个不变区域和一个共享区域, 不变区域中的数据, 不管后继如何演化, 总有固定的offset用于直接存取, 共享区域只能被间接存取(在虚表中放置 virtual base class 的 offset)

The Semantics of Function

Nonstatic Member Functions

C++设计准则之一: 非静态成员函数至少必须和一般的非成员函数有相同的效率.

编译器将类的成员函数转换为非成员函数

转换步骤:

  1. 改写函数signature, 增加一个额外的参数(this)
  2. 将每一个对 nonstatic data member 的存取操作改为经过this指针存取
  3. 将 member function 重新谢伟一个外部函数(对函数名特殊处理. name mangling)

Virtual Member Functions

多态: 以 public base class的指针(或引用) 寻址出一个 derived class object.

  • 一个类(不是对象)只有一个virtual table, 每个object被安插一个由编译器产生的指针(vptr).
  • 派生的子类,会 overriding 一个可能存在的 base class virtual function实例

1
2
3
4
class Derived : public Base1, public Base2 {};
# 两张虚表
vtbl_Derived; // 主要虚表
vtbl_Base2_Derived; // 次要虚表

多重继承情况下, 维护有多张虚表. 当Deriveed对象指定给Base1指针或Derived指针时, 被处理的virtual table 是主要表格vtbl_Derived, 当Deriveed对象指定给Base2时, 被处理的虚表为vtbl_Base2_Derived

Static Member Function

没有this指针

  • 静态成员函数将被转换为一般的非静态成员函数调用
  • 不能被声明为 const, volatile, virtual
  • 不需要经由class object 调用

Inline Functions

处理 inline函数的两个阶段:

  1. 分析函数定义, 以决定函数的 intrinsic inline ability(我理解为天生的内联能力).
  2. 真正的inline函数扩展操作是在调用的那一点上.

参考链接

  • 深入探索C++对象模型
]>
<blockquote> <p>什么是<code>C++对象模型</code>?</p> <ol> <li>语言中直接支持面向对象程序设计的部分.</li> <li>对于各种支持的底层实现机制.</li> </ol> </blockquote> <h2 id="Object-Lessons"><a href="#Object-Lessons" class="headerlink" title="Object Lessons"></a>Object Lessons</h2><ol> <li>C++封装并未增加布局成本, 数据成员内含在class object中(像struct), <code>成员函数不会出现在object中</code>, <strong>非内敛函数只会诞生一个实例, 内联函数对每个使用者产生一个实例</strong></li> <li>C++在布局及存取时间上主要的额外负担是由<code>virtual</code>引起的<ul> <li>virtual function支持动态绑定</li> <li>virtual base class实现单一而被共享的基类实例, 多用于多继承中</li> </ul> </li> </ol>
macOS Sierra 惊险升级 http://andrewliu.in/2016/09/24/macOS-Sierra-惊险升级/ 2016-09-24T10:26:27.000Z 2017-06-17T22:14:00.000Z 惊现问题

2016年9月21 Apple开始推送 macOS Sierra(10.12).

此处升级的亮点:

  • 亮点就是没有亮点!!!
  • 最大的升级是Mac OS X 改名为 macOS', 很大的改变有木有
  • Mac增加了Siri支持, 我知道我Mac多了个天气预报小助手
  • 可以使用Apple Watch自动近距离解锁Mac, 听说Near Lock已哭晕在厕所? 然而首先你要买一部 Apple Watch
  • 跨设备复制粘贴, 可以使用云端剪切板, iPhone上复制的东西可以在Mac上直接黏贴. 然而首先你要买一部 iPhone
  • Safari我就不喷了, 反正用了Chrome的我实在受不了龟速的Safari. 听说Safari很省电, 这个卖点不错!
  • 还有啥? 这次升级只有很少的App闪退阵亡.(呵呵

然后开始作死升级之路… 怎么升级就不说了, 正常人都知道….

告诉我macOS未能安装在电脑上, 磁盘没有足够的空间来安装..

我当时就哔了狗了, 你安装前不先检查磁盘剩余空间就敢给我安装?
重启后, 自动又开始安装, 然后再一次错误, 我就知道出事了…

都是我最近下片无数, 看完不删还想有空再回味一下, 这下子出事了… 磁盘不够了…

尝试解决

方法一. Failed. 安全模式方法, 重启后, 按住Shift等待出现苹果标志, 进入Mac模式, 听说此模式可以进入电脑, 这样我就能够删除我的大片了, 然后就有足够空间了, 然而我天真了, 真的再也进不去系统了, 一到80%或者100%就卡住不动了,太坑爹了…

方法二. Failed. 恢复模式方法, 重启后, 按住Command + R进入恢复模式, 在菜单栏的工具中, 打开Terminal, 满心以为可以通过Terminal中删除home文件下的文件. 结果我发现里面全是一些系统文件, 没有任何个人文件的影响, 此方法猝..

方法三. Failed. Ubuntu大法. 插入制作了Ubuntu的U盘, 按住Option开机, 开机后选址Efi(大概是这个名字)模式, 然后选择Try Ubuntu but not installing方式试用Ubuntu. 通过Ubuntu挂载Mac的硬盘, 然后对Mac硬盘的文件进行删除. 结果我又天真了, 由于硬盘未能安装成功更新, 导致挂载失败, 只能挂载恢复盘的文件, 我要这玩意有何用…

1
2
3
4
5
6
7
8
9
10
# 安装hfsprogs用于支持hfsplus
$ sudo apt-get install hfsprogs
# 测试要挂载盘的状态, sdXX表示要挂载的磁盘, 如sda1, sda2
$ sudo fsck.hfsplus -f /dev/sdXX
# 输出结果为 The volume #### appears to be OK表示可以挂载
# 挂载磁盘到本地的路径下, 此处我挂载sda2到本地/home/ubuntu路径下
sudo mount -t hfsplus -o fore,rw /dev/sda2 /home/ubuntu
# 若挂载成功可进行读写操作

  1. Success. 最后实在没办法了, 我又让他重启自动安装了, 结果发现这次成功了, 我还能说什么????

自我反省

  1. 以后再升级, 千万不能硬上了, 记得备份呀. 说的我好像买的起Time Machine一样
  2. 升级前一定要看看剩余的磁盘空间呀, 千万别装备呀, 千万别信Apple粑粑的升级包呀

最后我只想大声说一句: 重启大法好!!!!

]>
<h2 id="惊现问题"><a href="#惊现问题" class="headerlink" title="惊现问题"></a>惊现问题</h2><p><code>2016年9月21</code> Apple开始推送 <code>macOS Sierra(10.12)</code>.</p> <p><strong>此处升级的亮点:</strong></p> <ul> <li><strong>亮点就是没有亮点!!!</strong></li> <li>最大的升级是<code>Mac OS X</code> 改名为 <code>macOS&#39;</code>, 很大的改变有木有</li> <li>Mac增加了Siri支持, 我知道我Mac多了个天气预报小助手</li> <li>可以使用Apple Watch自动近距离解锁Mac, 听说<a href="">Near Lock</a>已哭晕在厕所? 然而首先你要买一部 <code>Apple Watch</code></li> <li>跨设备复制粘贴, 可以使用云端剪切板, iPhone上复制的东西可以在Mac上直接黏贴. 然而首先你要买一部 <code>iPhone</code></li> <li>Safari我就不喷了, 反正用了Chrome的我实在受不了龟速的Safari. 听说Safari很省电, 这个卖点不错!</li> <li>还有啥? 这次升级只有很少的App闪退阵亡.(呵呵</li> </ul> <blockquote> <p>然后开始作死升级之路… 怎么升级就不说了, 正常人都知道….</p> </blockquote>
Lua热更新 http://andrewliu.in/2016/09/17/Lua热更新/ 2016-09-17T08:53:51.000Z 2017-06-17T22:14:00.000Z 什么是热更新

Hot swapping is ability to alter the running code of a program without needing to interrupt its execution.
Wikipedia

热更新: lua虚拟机运行时, 修改出现bug或者想要增加新feature的代码, 不需要去重启整个服务.

require热更新方案

require的原理:

  1. 在registry[“_LOADED”]表中判断该模块是否已经加载过了, 如果是则直接换回, 避免重复加载某个模块代码
  2. 一次调用注册的loader来加载模块
  3. 将加载过的模块赋值给registry[“LOADER”]表

require实现的缓存机器会阻止重复加载相同模块. 所以使用require可以把package.loaded中对应的模块设置为nil, 然后重新进行require加载

  • 缺点: 数据会被同时更新(全局变量), 这并不是我们想要的
1
2
3
4
5
6
7
8
9
-- 1. 清除require缓存, 来实现热加载
function require_ex(module_name)
print(string.format("require_ex = %s", module_name))
if package.loaded[module_name] then
print(string.format("require_ex module[%s] reload.", module_name))
end
package.loaded[module_name] = nil -- 清除历史, 重新load
return require(module_name)
end

优化的热更新方案

upvalue被保留在热更新中非常重要, upvalue函数里用到的定义在该函数之前的local变量

优化的热更新方案主要是将upvalue保存下来, 重新加载时, 把upvalue的值更新进去, 使之整体看起来与原来一样.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
-- 2. 优化后的热加载
-- 利用_ENV环境,在加载的时候把数据加载到_ENV下,然后再通过对比的方式修改_G底下的值,从而实现热更新
function hot_swap(chunk, check_name)
local env = {}
setmetatable(env, { __index = _G })
local _ENV = env
local f, err = load(chunk, check_name, 't', env)
assert(f, err)
local ok, err = pcall(f)
assert(ok, err)
for name, value in pairs(env) do
local g_value = _G[name]
-- 原type和新类型不同则做覆盖, 否则保持原值
if type(g_value) ~= type(value) then
_G[name] = value
elseif type(value) == 'function' then
update_func(value, g_value, name, 'G'..' ')
_G[name] = value
elseif type(value) == 'table' then
update_table(value, g_value, name, 'G'..' ')
end
end
end
function update_func(env_f, g_f, name, deep)
-- 取得原值所有的upvalue,保存起来
local old_upvalue_map = {}
for i = 1, math.huge do
local name, value = debug.getupvalue(g_f, i)
if not name then break end
old_upvalue_map[name] = value
end
-- 遍历所有新的upvalue,根据名字和原值对比,如果原值不存在则进行跳过,如果为其它值则进行遍历env类似的步骤
for i = 1, math.huge do
local name, value = debug.getupvalue(env_f, i)
if not name then break end
local old_value = old_upvalue_map[name]
if old_value then
if type(old_value) ~= type(value) then
debug.setupvalue(env_f, i, old_value)
elseif type(old_value) == 'function' then
update_func(value, old_value, name, deep..' '..name..' ')
elseif type(old_value) == 'table' then
update_table(value, old_value, name, deep..' '..name..' ')
debug.setupvalue(env_f, i, old_value)
else
debug.setupvalue(env_f, i, old_value)
end
end
end
end
local protection = {
setmetatable = true,
pairs = true,
ipairs = true,
next = true,
require = true,
_ENV = true,
}
-- 防止重复的table替换,造成死循环
local visited_sig = {}
function update_table(env_t, g_t, name, deep)
-- 对某些关键函数不进行比对
if protection[env_t] or protection[g_t] then return end
--如果原值与当前值内存一致,值一样不进行对比
if env_t == g_t then return end
local signature = tostring(g_t)..tostring(env_t)
if visited_sig[signature] then return end
visited_sig[signature] = true
-- 遍历对比值,如进行遍历env类似的步骤
for name, value in pairs(env_t) do
local old_value = g_t[name]
if type(value) == type(old_value) then
if type(value) == 'function' then
update_func(value, old_value, name, deep..' '..name..' ')
g_t[name] = value
elseif type(value) == 'table' then
update_table(value, old_value, name, deep..' '..name..' ')
end
else
g_t[name] = value
end
end
--遍历table的元表,进行对比
local old_meta = debug.getmetatable(g_t)
local new_meta = debug.getmetatable(env_t)
if type(old_meta) == 'table' and type(new_meta) == 'table' then
update_table(new_meta, old_meta, name..'s Meta', deep..' '..name..'s Meta'..' ' )
end
end
function hot_reload(name)
local file_str
local fp = io.open(name)
if fp then
io.input(name)
file_str = io.read('*all')
io.close(fp)
end
if not file_str then
return -1
end
return hot_swap(file_str, name)
end

参考链接

]>
<h2 id="什么是热更新"><a href="#什么是热更新" class="headerlink" title="什么是热更新"></a>什么是热更新</h2><blockquote> <p>Hot swapping is ability to alter the running code of a program without needing to interrupt its execution.<br> –<a href="https://en.wikipedia.org/wiki/Hot_swapping" target="_blank" rel="external">Wikipedia</a></p> </blockquote> <p>热更新: lua虚拟机运行时, 修改出现bug或者想要增加新feature的代码, 不需要去重启整个服务.</p>
Lua编码规范 http://andrewliu.in/2016/09/04/Lua编码规范/ 2016-09-04T13:08:18.000Z 2017-06-17T22:14:00.000Z

写代码是为了给别人读的, 所以总需要一些建议性的规范, 所以简单的翻译了一下国外的这篇Lua编码规范, 并做了一些适合自己的改动.

Introduction

代码写出来是给人读的, 保持一致性能够增加改善代码的可读性. 一致性(Consistency)在项目之间, 项目内部, 单个模块或单个函数中非常重要. 值得注意的是, 编码规范只是一种建议, 有时可能并不需要去遵守编码规范.

Lua包含自己的语法和常用方式, 本文的编码规范受到其他语言的编码规范的启发:

编码规范是一种艺术.

在定义Lua的风格建议时, 从一些Lua的源码中获得灵感(这些源码来自官方文档, Lua的作者们, 其他有名的源码) :

Formatting

缩进(Indentation) 缩进常常使用四个空格缩进(原文建议为两个空格). 使用四个空格缩进原因在于, 公司中C/C++编码风格和Python编码风格都是遵循四个缩进的.

1
2
3
4
5
6
7
8
t = {4, 5, 6, "hello"}
for i, v in ipairs(t) do
if type(v) == "string" then
print(v)
else
print("index = " .. i .. ", num = " .. v)
end
end

Naming

变量命名长度(Variable name length): 这个一个一般的规则, 范围大的变量名应该比范围小的变量有更多的描述词. 比如, 在一个大型程序中i作为全局变量的命名是非常糟糕的, 但作为一个小范围的计数器是最吼得.

值和对象命名(Value and object variable naming): 保存值和对象的变量的命名应该一般是小写并且比较短.

  • 布尔变量(Booleans): 布尔值或者函数的前缀应该对整体的意义是有帮助的. 例如is_directory而不是directory

函数命名(Function naming): 函数的命令规则一般类似于值和对象命名. 应该使用下划线和小写单词组合(原文使用getmetatable这种多个字母写在一起的方式). 例如print_table

Lua内部变量命名(Lua internal variable naming): 使用名字以下滑线开头, 后跟大写字母作为传统, 如(_VERSION). 这种命名常用于常量, 但不是必须的.

常量命名(Constants naming): 常量, 尤其是值比较简单的, 使用全大写的组合(ALL_CAPS).

模块/包命名(Mudule/Package naming): 使用小写字母加下划线组合的形式, 如: lua_module.lua

变量名只有一个_的变量, 表示希望忽略这个变量, 这只是一个传统(convention)

1
2
-- 忽略数组下标
for _, v in ipairs(t) do print(v) end

i, k, v, t常用于一下场景

1
2
3
for k,v in pairs(t) ... end
for i,v in ipairs(t) ... end
mt.__newindex = function(t, k, v) ... end

M常用于表示当前模块的table(current module table)

类名(Class names): 以大驼峰形式命名(ClassName).

匈牙利命名法?(Hungarian notations, 是这么翻译吗). 将语义信息编码到变量中能够帮助增加代码可读性, 尤其是当编码的含义不容易被理解时, 但是过度的命名可能会显示冗余并且降低复杂度(在黑OC呢?). 在一些静态语言中(如, C), 变量类型已知, 此时将变量类型加入命名是很冗余的.

1
2
3
4
5
6
local function paint(canvas, n_times)
for i = 1, n_times do
local hello_str_asc_lc_en_const = "hello world"
canvas:draw(hello_str_asc_lc_en_const:to_upper())
end
end

Libraries and Features

Scope

无论何时, 尽可能的使用local

1
2
3
4
5
local x = 0
local function count()
x = x + 1
print(x)
end

全局(global) 有更大的范围和生命周期, 并且会增加coupling)和复杂度. 不要污染环境. 在Lua中, 由于全局(global)需要运行时查找table, 所以访问localglobal更快, local保存在寄存器中. 参考Scope Tutorial

检测误用global的有效方法在DetectingUndefinedVariables给出. 在Lua中, globals有时可能导致错误拼写和潜在的错误.

有时通过do-blocks进一步限制local变量的范围非常有用

1
2
3
4
5
6
7
8
9
10
11
12
13
local v
do
local x = u2 * v3 - u3 * v2
local y = u3 * v1 - u1 * v3
local z = u1 * v2 - u2 * v1
v = {x, y, z}
end
local count
do
local x = 0
count = function() x = x + 1; return x end
end

Mudules

在Lua 5.1版本中, 模块系统常被推荐, 但也有一些需要批评的地方. 更详细的描述可以看Lua Module Function Critiqued. 你可能写出一下代码:

1
2
3
4
5
-- hello/mytest.lua
module(..., package.seeall)
local function test() print(123) end
function test1() test() end
function test2() test1(); test1() end

并且像下方一样使用

1
2
require "hello.mytest"
hello.mytest.test2()

这里需要批判的地方是: 在所有模块中创建了一个全局变量hello(包含副作用), 通过hello table全局环境曝光. 如: hello.mytest.print = _G.print.

这些问题可以通过不适用module函数, 而是通过简单的定义模块来解决:

1
2
3
4
5
6
7
8
-- hello/mytest.lua
local M = {}
local function test() print(123) end
function M.test1() test() end
function M.test2() M.test1(); M.test1() end
return M

然后通过如下方式import

1
2
local MT = require "hello.mytest"
MT.test2()

一个模块包含带构造函数的类可以通过多种方法封装. 此处提供一个好方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
-- file: finance/BankAccount.lua
local M = {}; M.__index = M
local function construct()
local self = setmetatable({balance = 0}, M)
return self
end
setmetatable(M, {__call = construct})
function M:add(value) self.balance = self.balance + 1 end
return M

一个模块通过以上方式定义一般只有一个类, 这个类就是模块本身.

使用方式如下:

1
2
local BankAccount = require "finance.BankAccount"
local account = BankAccount()

或者是

1
2
local new = require
local account = new "finance.BankAccount" ()

Commenting

单行注释在--后加一个空格

1
2
return nil -- not found (suggested)
return nil --not found (discouraged)

文档注释有多种写法. 可以使用下方的装饰器模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
local docstrings = setmetatable({}, {__mode = "kv"})
function document(str)
return function(obj) docstrings[obj] = str; return obj end
end
function help(obj)
print(docstrings[obj])
end
document[[Print the documentation for a given object]](help)
document[[Add a string as documentation for an object]](document)
f = document[[Print a hello message]](
function()
print("hello")
end
)
f()
help(f)

End终止符(End Terminator)

在end后增加一些语句结束的注释发会帮助增加可读性

1
2
3
4
5
for i, v in ipairs(t) do
if type(v) == "string" then
...lots of code here...
end -- if string
end -- for each t

Lua Idioms

  • 测试一个变量不等于nil, 下方的写法更简洁.

Lua中nil和false做为false, 其他所有值为true

1
2
3
4
5
6
7
8
local line = io.read()
if line then -- instead of line ~= nil
...
end
...
if not line then -- instead of line == nil
...
end

但是当值即可能等于nil也可能等于false时, 条件需要明确.

  • andor的短路求值特性也可以简化代码
1
2
3
4
5
6
local function test(x)
x = x or "idunno"
-- rather than if x == false or x == nil then x = "idunno" end
print(x == "yes" and "YES!" or x)
-- rather than if x == "yes" then print("YES!") else print(x) end
end
  • 克隆(clone)一个table(对表的大小有系统依赖限制)
1
u = {unpack(t)}
  • 判断一个table是否为空
1
if next(t) == nil then ...
  • 向数组中增加一个值
1
2
-- #t表示数组当前长度
t[#t + 1] = 1 -- 代替 table.insert(t, 1)

Design Patterns

Lua是一个轻量级语言, 通过少量的模块支持可以提供大量的强大的方式. 这种自由的设计模式需要自我组织.

Lua is small language with a small number of simple building blocks that can be combined in a vast number of powerful ways. With this freedom comes the need for self-discipline in the form of design patterns. Often there is an idiomatic way or design pattern to achieving a certain effect in Lua that can be reused frequently (e.g. ObjectOrientationTutorial or ReadOnlyTables). See the LuaTutorial and SampleCode for common solutions to such problems.(这部分翻译不好, 放上原文)

Coding Standards

在各种Lua项目中的编码标准:

参考链接

]>
<blockquote> <p>写代码是为了给别人读的, 所以总需要一些建议性的规范, 所以简单的翻译了一下国外的这篇<a href="http://lua-users.org/wiki/LuaStyleGuide" target="_blank" rel="external">Lua编码规范</a>, 并做了一些适合自己的改动.</p> </blockquote> <h2 id="Introduction"><a href="#Introduction" class="headerlink" title="Introduction"></a>Introduction</h2><p>代码写出来是给人读的, 保持<code>一致性</code>能够增加改善代码的可读性. <code>一致性(Consistency)</code>在项目之间, 项目内部, 单个模块或单个函数中非常重要. 值得注意的是, 编码规范只是<code>一种建议</code>, 有时可能并不需要去遵守编码规范.</p>
快学Lua http://andrewliu.in/2016/08/29/快学Lua/ 2016-08-29T08:01:26.000Z 2017-06-17T22:14:00.000Z 本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循署名-非商业用途-保持一致的创作共用协议.

工作需要用到Lua做一些脚本, 所以学习一下这个在游戏开发应用广泛的语言, 当然, 我更喜欢称之为撸啊撸语言…

简介

  • Lua是一种轻量语言,它的官方版本只包括一个精简的核心和最基本的库,
  • 性能方面, Lua比Python快(这也是脚本语言选型中不用python的原因)
  • Lua与C/C++交互方便, 易于扩展
  • Lua是一个动态弱类型语言, 支持增量式垃圾收集策略, 支持协程
  • 支持REPL(Read-Eval-Print Loop, 交互式解释器)
1
2
3
4
5
6
7
8
-- 命令行中输入lua, 使用REPL
andrew_liu:Lua/ $ lua
Lua 5.2.4 Copyright (C) 1994-2015 Lua.org, PUC-Rio
> print "hello world" -- 国际编程学习惯例
hello world
> print("hello world")
hello world
>

安装

已经懒得再安利brew了(不好用你干我), 一行命令完成安装

1
2
# 安装路径为/usr/local/Cellar/lua
$ brew install lua

基础语法

注释

单行注释使用--, 多行注释使用--[[--]]

1
2
3
4
5
6
7
-- 这种为单行注释
-- Date: 16/8/27
-- Time: 22:33
--[[
这里是多行注释
--]]

变量定义

  • lua中的所有变量默认为全局变量, 定义局部变量需要使用local关键字(是不是很变态)
  • 所有的数字都是双精度浮点型(double)
  • 字符串可以使用单引号和双引号, 类似于python中的字符串表示形式
  • 使用一个未定义的变量不会报错, 而是返回nil(类似于C/C++中的NULL)
  • 一行语句可以同时定义多个变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- variable
age = 0 -- 全局变量
sum = 2 -- 所有数字都是double型
str = '蛤丝' -- 字符串可以是单引号和双引号
str1 = "还是蛤丝"
-- 使用`[[]]`定义多行字符串时, 其空格都会被保留
str2 = [[多行的字符串
以两个方括号
开始和结尾。]]
sum = nil -- 撤销sum的定义
test = not_define
print(test)
name, age, sex = "andrew", 18, "male", "蛤丝" -- 最后一个变量会被丢弃, 而不会报错(闷死发大财才是最吼得)

if-else

if-else与python有些像, 不过需要注意if和elseif最后的then和末尾的end

1
2
3
4
5
6
7
8
9
10
11
-- if-else
age = 100
if age <= 40 and age >= 20 then
print("young man!")
elseif age > 40 and age < 100 then
print("old man")
elseif age < 20 then
print("too young")
else
print("monster")
end

循环

  • Lua同样支持三种循环, while循环, for循环, until循环(可以理解为C/C++中的do-while循环)

while循环

当条件满足, 会一直执行循环体

1
2
3
4
5
6
-- while
num = 0
while num < 10 do
print(num)
num = num + 1
end

for循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- for
sum = 0
-- 1表示从1开始, 10表示最大为10(包含10)
-- C++: for (int i = 1, i <= 10; ++i)
for i = 1, 100 do -- 包含1和100
sum = sum + i
end
-- 类似于Python中的range操作, 2表示, 每次循环后i执行+2操作
-- C++: for (int i = 1, i <= 10; i = i + 2)
for i = 1, 10, 2 do -- 1,3,5,7,9
print(i)
sum = sum + i
end
-- ..操作符用于连接字符串, 可以理解为python中字符串的+
操作
print("sum = " .. sum)

until循环, 类似于C/C++中的do-while语句

1
2
3
4
5
6
7
-- until(do-while)
sum = 2
-- 中文: 直到sum的值大于1000时, 才终止循环
repeat
sum = sum ^ 2 -- 幂操作
until sum > 1000
print("do-while, sum = "..sum)

函数

  • 函数在Lua中是一等公民
  • 支持有名和匿名函数
  • 函数最后有个end, 写C/C++/Python的可能不太适应
  • 函数支持一次`返回多个变量
  • 局部函数的定义只需要在函数前加个local, 和局部变量类似
1
2
3
4
5
6
7
8
9
10
-- function(return支持返回多个值), 函数是一等公民
function add(a, b)
return a + b
end
print("1 + 2 = "..add(1, 2))
-- 匿名函数
f = function (a, b)
return a * b
end
print ("匿名函数: " .. f(2, 3))

递归(经典: 斐波那契)

1
2
3
4
function fib(n)
if n < 2 then return n end
return fib(n - 2) + fib(n - 1)
end

Table

Table是Lua中核心数据结构, 我理解为其他语言中的Dict/Map数据结构(key-value的数据结构), 而数组等在Lua中都是通过Table来表示的

Table可以使用任何非nil的值作为key

1
2
3
4
5
6
7
8
9
10
-- Lua唯一的组合数据结构
-- 可以使用任何非nil值作为key
-- Table(key-value数据结构)
map = {
name = "andrew", -- 此处name默认使用字符串作为key
age = 18,
sex = "male"
}
-- 上下两种写法等价
--map = {['name'] = 'andrew', ['age'] = 18, ['sex'] = 'male'}

对Table创建, 读取, 更新和删除操作, key-value的读取可以通过.符号

1
2
3
4
5
-- Table的CRUD
map['hobby'] = "膜蛤" -- map.hobby = "膜蛤"
print(map.name)
map.age = 19
map.sex = nil -- 删除key

遍历一个Table

1
2
3
4
-- 遍历一个Table
for k, v in pairs(map) do
print(k, v)
end

数组/列表

  • 数组是通过Table来实现的

Lua中数组的下标是从1开始, 从1开始, 从1开始. 重要的事情说三遍.

1
2
3
4
5
6
7
8
9
-- array, 数组下标从1开始
-- 数组中可以是不同类型的数据
arr = {"string", 100, "andrew", function() print("hello world") end }
-- 等价于
-- arr = {[1]="string", [2]=100, [3]="andrew", [4]=40, [5]=function() print("hello world") end}
for i = 1, #arr do -- #arr表示数组的长度
print(arr[i])
end

元表(metatable)

Table的metatable提供一种类似于操作符重载的机制, 写个python的可能重写个__add__方法, 感觉有点类似

  • 通过setmetatable将自己实现的metamethod绑定到对应的Table对象中
1
2
3
4
5
6
7
8
9
10
11
12
13
-- table的元表提供一种机制,支持操作符重载
data_one = {a = 1, b = 2} -- data_one和data_two 可以看做表示1/2和3/4
data_two = {a = 3, b = 4 }
meta_fraction = {} -- 新定义一个元表
function meta_fraction.__add(f1, f2) -- __add是build-in的原方法
local sum = {}
sum.b = f1.b * f2.b
sum.a = f1.a * f2.b + f2.a * f1.b
return sum
end
setmetatable(data_one, meta_fraction) -- 为之前定义的两个table设置metatable
setmetatable(data_two, meta_fraction)
s = data_one + data_two -- 执行 + 操作
  • 元表的__index 可以重载用于查找的点操作符, 我理解有点类似于python中的__getattr__, 对类中的属性做查找
1
2
3
4
5
6
-- 当表要索引一个值时如table[key], Lua会首先在table本身中查找key的值,
-- 如果没有并且这个table存在一个带有__index属性的Metatable, 则Lua会按照__index所定义的函数逻辑查找
default_map = {name = 'andrew', sex = 'male' }
new_map = {age = 18 }
setmetatable(default_map, {__index = new_map})
print(default_map.name, default_map.age) -- 继承了new_map的属性

元方法(metamethod)

__add这种方法在Lua中被称为元方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- __add(a, b) for a + b
-- __sub(a, b) for a - b
-- __mul(a, b) for a * b
-- __div(a, b) for a / b
-- __mod(a, b) for a % b
-- __pow(a, b) for a ^ b
-- __unm(a) for -a
-- __concat(a, b) for a .. b
-- __len(a) for #a
-- __eq(a, b) for a == b
-- __lt(a, b) for a < b
-- __le(a, b) for a <= b
-- __index(a, b) <fn or a table> for a.b
-- __newindex(a, b, c) for a.b = c
-- __call(a, ...) for a(...)

继承

继承同样是通过元表和元方法来实现的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- table实现继承
Dog = {}
-- 这个new函数我理解为继承类的构造函数, 冒号(:)函数会使参数默认增加一个self作为参数
function Dog:new()
-- 下面这句相当于继承的类增加的新的成员变量
local new_obj = {sound = 'woof'}
self.__index = self
return setmetatable(new_obj, self)
end
function Dog:make_sound() -- 等价于Dog.make_sound(Dog)
print("say " .. self.sound)
end
-- 使用
new_dog = Dog:new() -- new_dog获得Dog的方法和变量列表
new_dog:make_sound()
  1. self.__index = self是防止self被扩展后改写,所以,让其保持原样
  2. setmetatable(new_obj, self)会返回第一个参数, new_obj会得到self的函数
  3. Dog:new()调用方式会增加一个隐式参数self

模块

  1. require函数, 第一次执行后有缓存, 所以载入同样的lua文件时, 只有第一次的时候会去执行, 多次执行同一句require, 只有第一次生效
  2. dofile函数, 类似于require, 但无缓存, 每次语句都会重新执行一次
  3. loadfile加载一个lua文件, 但是并不运行它, 只要在需要的时候再像函数一样执行它
1
2
3
4
5
6
7
-- module
-- 1. require
local test = require('module') -- require文件会被缓存
-- test.say_my_name() -- 不能调用local
test.say_hello()
-- 2. dofile 类似require 但不缓存
-- 3. loadfile加载一个lua文件, 但是并不运行它

参考链接

]>
<p>本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循<a href="http://creativecommons.org/licenses/by-nc-sa/3.0/deed.zh" target="_blank" rel="external">署名-非商业用途-保持一致</a>的创作共用协议.</p> <blockquote> <p>工作需要用到Lua做一些脚本, 所以学习一下这个在游戏开发应用广泛的语言, 当然, 我更喜欢称之为撸啊撸语言…</p> </blockquote> <h2 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h2><ul> <li><code>Lua</code>是一种轻量语言,它的官方版本只包括一个精简的核心和最基本的库, </li> <li>性能方面, Lua比Python快(这也是脚本语言选型中不用python的原因)</li> <li>Lua与C/C++交互方便, 易于扩展</li> <li><code>Lua</code>是一个动态弱类型语言, 支持增量式垃圾收集策略, 支持<code>协程</code></li> <li>支持REPL(Read-Eval-Print Loop, 交互式解释器)</li> </ul> <figure class="highlight python"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div></pre></td><td class="code"><pre><div class="line">-- 命令行中输入lua, 使用REPL</div><div class="line">andrew_liu:Lua/ $ lua </div><div class="line">Lua <span class="number">5.2</span><span class="number">.4</span> Copyright (C) <span class="number">1994</span><span class="number">-2015</span> Lua.org, PUC-Rio</div><div class="line">&gt; <span class="keyword">print</span> <span class="string">"hello world"</span> -- 国际编程学习惯例</div><div class="line">hello world</div><div class="line">&gt; print(<span class="string">"hello world"</span>)</div><div class="line">hello world</div><div class="line">&gt;</div></pre></td></tr></table></figure>
Kqueue学习笔记 http://andrewliu.in/2016/08/14/Kqueue学习笔记/ 2016-08-14T02:33:53.000Z 2017-06-17T22:14:00.000Z 本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循署名-非商业用途-保持一致的创作共用协议.

想在Mac上造点小轮子, 然而epoll是在Linux平台独有的, 所以想到了用kqueue来替代. 记录一下自己的学习过程.

Introduction

  • kqueue是在UNIX上高效的IO复用技术, 类比于linux平台中的epoll.
  • IO复用原理大概为: 网卡设备对应一个中断号, 当网卡收到网络端的消息的时候会向CPU发起中断请求, 然后CPU处理该请求. 通过驱动程序 进而操作系统得到通知, 系统然后通知epoll/kqueue, epoll/kqueue通知用户代码.

API

kqueue使用的头文件和api可以通过man kqueue来看到

1
2
3
#include <sys/types.h>
#include <sys/event.h>
#include <sys/time.h>
  • kqueue()系统调用会创建一个新的内核消息队列并返回描述符.
1
2
# 创建失败返回-1, 否则返回描述符
int kqueue(void);
  • kqueue的数据结构
1
2
3
4
5
6
7
8
struct kevent {
uintptr_t ident; /* identifier for this event, 该事件关联的文件描述符 */
int16_t filter; /* filter for event */
uint16_t flags; /* general flags, 用于指定事件操作类型, 比如EV_ADD, EV_ENABLE, EV_DELETE等, 通过|可以同时设置多个事件 */
uint32_t fflags; /* filter-specific flags */
intptr_t data; /* filter-specific data */
void *udata; /* opaque user data identifier */
};
  • EV_SET()在官方文档描述是一个宏, 用于初始化kevent数据结构
1
2
3
4
EV_SET(&kev, ident, filter, flags, fflags, data, udata);
// 使用范例, 将监听stdin描述符的可读事件初始化到ev数据结构
kevent ev;
EV_SET(&ev, STDIN_FILENO, EVFILT_READ, EV_ADD, 0, 0, 0);
  • kevent为核心函数, 初始时kqueue内核消息队列为空, 使用kevent进行事件填充, 在不设置超时参数时, 只有当收到某监听事件才会返回. 该函数返回接收到事件个数, 并将事件写入eventlist
1
2
# changelist为要注册的事件列表, eventlist用于返回已经就绪的事件列表
int kevent(int kq, const struct kevent *changelist, int nchanges, struct kevent *eventlist, int nevents, const struct timespec *timeout);

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#include <stdio.h> // fprintf
#include <sys/event.h> // kqueue
#include <netdb.h> // addrinfo
#include <arpa/inet.h> // AF_INET
#include <sys/socket.h> // socket
#include <assert.h> // assert
#include <string.h> // bzero
#include <stdbool.h> // bool
#include <unistd.h> // close
const size_t BUF_SIZE = 1024;
static bool s_stop = true;
// 信号处理函数
static void handle_signal(int sig) {
s_stop = false;
}
int learn_kqueue(const char* ip, int32_t port) {
std::cout << "ip: " << ip << " port: " << port << std::endl;
signal(SIGTERM, handle_signal);
int sock = socket(PF_INET, SOCK_STREAM, 0);
assert(sock > 0);
// 强制使用TIME_WAIT状态的socket地址
int reuse = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr); //主机序转网络序ip
address.sin_port = htons(port); //主机序转网络序
int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
assert(ret != -1);
ret = listen(sock, BACK_LOG);
assert(ret != -1);
//创建一个消息队列并返回kqueue描述符
int kq = kqueue();
assert(kq != -1);
struct kevent change_list[10]; //想要监控的事件
struct kevent event_list[10]; //用于kevent返回
char buf[BUF_SIZE];
// 监听sock的读事件
EV_SET(&change_list[0], sock, EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, 0);
// 监听stdin的读事件
EV_SET(&change_list[1], fileno(stdin), EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, 0);
int nevents;
while (s_stop) {
printf("new loop...\n");
// 等待监听事件的发生
nevents = kevent(kq, change_list, 2, event_list, 2, NULL);
if (nevents < 0) {
printf("kevent error.\n"); // 监听出错
} else if (nevents > 0) {
printf("get events number: %d\n", nevents);
for (int i = 0; i < nevents; ++i) {
printf("loop index: %d\n", i);
struct kevent event = event_list[i]; //监听事件的event数据结构
int clientfd = (int) event.ident; // 监听描述符
// 表示该监听描述符出错
if (event.flags & EV_ERROR) {
close(clientfd);
printf("EV_ERROR: %s\n", strerror(event_list[i].data));
}
// 表示sock有新的连接
if (clientfd == sock) {
printf("new connection\n");
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int new_fd = accept(sock, (struct sockaddr *) &client_addr, &client_addr_len);
char remote[INET_ADDRSTRLEN];
printf("connected with ip: %s, port: %d\n",
inet_ntop(AF_INET, &client_addr.sin_addr, remote, INET_ADDRSTRLEN),
ntohs(client_addr.sin_port));
}
if (clientfd == fileno(stdin)) {
memset(buf, 0, BUF_SIZE);
fgets(buf, BUF_SIZE, stdin);
printf("data from stdin: %s\n", buf);
}
}
}
}
close(sock);
return 0;
}

Reference

]>
<p>本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循<a href="http://creativecommons.org/licenses/by-nc-sa/3.0/deed.zh" target="_blank" rel="external">署名-非商业用途-保持一致</a>的创作共用协议.</p> <blockquote> <p>想在Mac上造点小轮子, 然而epoll是在Linux平台独有的, 所以想到了用kqueue来替代. 记录一下自己的学习过程.</p> </blockquote> <h2 id="Introduction"><a href="#Introduction" class="headerlink" title="Introduction"></a>Introduction</h2><ul> <li><code>kqueue</code>是在UNIX上高效的IO复用技术, 类比于linux平台中的<code>epoll</code>.</li> <li>IO复用原理大概为: 网卡设备对应一个中断号, 当网卡收到网络端的消息的时候会向CPU发起中断请求, 然后CPU处理该请求. 通过驱动程序 进而操作系统得到通知, 系统然后通知epoll/kqueue, epoll/kqueue通知用户代码. </li> </ul>
浅谈Raft http://andrewliu.in/2016/08/06/浅谈Raft/ 2016-08-06T06:10:48.000Z 2017-06-17T22:14:00.000Z 本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循署名-非商业用途-保持一致的创作共用协议.

工作忙了,总是骗自己说的没时间写博客,其实都是自己懒癌发作而已。题目写了浅谈,肯定文章是不够深入透彻的,刚刚爬进分布式门槛的婴儿,需要慢慢的学会走路。希望后面能通过自己总结再来一篇深入理解Raft(显然又在挖坑

一致性算法特性

  1. 安全性保证:在非拜占庭错误情况下,包括网络延迟、分区、丢包、冗余和乱序等错误都可以保证正确。
  2. 可用性:集群中只要有大多数的机器可运行并且能够相互通信、和客户端通信,就可以保证可用。因此,一个典型的包含 5 个节点的集群可以容忍两个节点的失败。服务器被停止就认为是失败。他们当有稳定的存储的时候可以从状态中恢复回来并重新加入集群。paxos在有2f + 1个节点时可以容忍f个节点失败.
  3. 不依赖时序来保证一致性:物理时钟错误或者极端的消息延迟在可能只有在最坏情况下才会导致可用性问题。
  4. 通常情况下,一条指令可以尽可能快的在集群中大多数节点响应一轮远程过程调用时完成。小部分比较慢的节点不会影响系统整体的性能。

Paxos算法

提到Raft算法的同时, 总是无法避免的要说起Paxos, 在Raft论文中也提到, Raft算法初衷是为了解决Paxos难以理解而提出, 所以总要看看Paxos算法到底是个什么东西

Paxos算法的缺陷:

  1. Paxos 算法难以理解
  2. Paxos算法没有提供一个足够好用来构建一个现实系统的基础

Raft算法

Raft是一种为了管理复制日志一致性算法

  • Raft 将一致性算法分解成了领导人(leader)选举、日志复制和安全性三个模块.

Raft算法的特性:

  • 强领导者:Raft 使用一种更强的领导能力形式。比如,日志条目只从领导者发送给其他的服务器。这种方式简化了对复制日志的管理并且使得 Raft 算法更加易于理解。
  • 领导选举:Raft 算法使用一个随机计时器来选举领导者。这种方式只是在任何一致性算法都必须实现的心跳机制上增加了一点机制。
  • 关系调整:Raft 使用一种共同一致的方法来处理集群成员变换的问题,在这种方法中,两种不同的配置都要求的大多数机器会重叠。这就使得集群在成员变换的时候依然可以继续工作。

领导人选举

  1. 在任何时刻,每一个服务器节点都处于这三个状态之一:领导人(leader)、跟随者(follower)或者候选人(candidate). 跟随者都是被动的:他们不会发送任何请求,只是简单的响应来自领导者或者候选人的请求。领导人处理所有的客户端请求
  2. Raft 把时间分割成任意长度的任期(terms), 如上面图中的Figure 5, 任期用连续的整数标记。每一段任期从一次选举开始,一个或者多个候选人尝试成为领导者。如果一个候选人赢得选举,然后他就在接下来的任期内充当领导人的职责。在某些情况下,一次选举过程会造成选票的瓜分。在这种情况下,这一任期会以没有领导人结束(t3);一个新的任期(和一次新的选举)会很快重新开始。Raft 保证了在一个给定的任期内,最多只有一个领导者
  3. 领导者周期性的向所有跟随者发送心跳包。如果一个跟随者在一段时间里没有接收到任何消息,也就是选举超时, 然后他就会认为系统中没有可用的领导者然后开始进行选举以选出新的领导者。
  4. 此跟随者先要增加自己的当前任期号并且转换到候选人状态, 开始进入领导人选举. 他会并行的向集群中的其他服务器节点发送RPC请求给自己投票。候选人会继续保持着当前状态直到以下三件事情之一发生:(a) 他自己赢得了这次的选举, 则向其他服务器发送心跳检测,(b) 其他的服务器成为领导者, 候选者选为跟随者,(c) 一段时间之后没有任何一个获胜的人, 每一个候选人都会超时,然后通过增加当前任期号来开始一轮新的选举。

日志复制

日志复制具有以下特性:

  • 如果在不同的日志中的两个条目拥有相同的索引和任期号,那么他们存储了相同的指令。
  • 如果在不同的日志中的两个条目拥有相同的索引和任期号,那么他们之前的所有日志条目也全部相同

可能因为领导者在日志提交前挂掉, 导致所有的服务器中的日志不一定的情况. 领导人处理不一致是通过强制跟随者直接复制自己的日志来解决了.

领导人针对每一个跟随者维护了一个 nextIndex,这表示下一个需要发送给跟随者的日志条目的索引地址。当一个领导人刚获得权力的时候,他初始化所有的 nextIndex 值为自己日志中的最后一条。如果一个跟随者的日志和领导人不一致,那么在下一次的附加日志 RPC 时的一致性检查就会失败。在被跟随者拒绝之后,领导人就会减小 nextIndex 值并进行重试。最终 nextIndex 会在某个位置使得领导人和跟随者的日志达成一致。当这种情况发生,附加日志 RPC 就会成功,这时就会把跟随者冲突的日志条目全部删除并且加上领导人的日志。一旦附加日志 RPC 成功,那么跟随者的日志就会和领导人保持一致,并且在接下来的任期里一直继续保持。

安全性

  1. 选举限制, 候选人发起RPC 中包含了候选人的日志信息,投票人会拒绝掉那些日志没有自己新的投票请求
  2. 如果已经服务器已经在某个给定的索引值应用了日志条目到自己的状态机里,那么其他的服务器不会应用一个不一样的日志到同一个索引值上

日志压缩

目的是减少不断增长的日志所占用服务器空间大小

采用的方法如下图:

客户端交互

  1. 客户端发送command给领导人, 若领导人未知,挑选任意节点,若该节点不是领导人,则重定向至领导人
  2. 领导人追加日志项,等待提交,更新本地状态机,最终响应客户端
  3. 若客户端超时,则不断重试,直至收到响应为止

参考链接

]>
<p>本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循<a href="http://creativecommons.org/licenses/by-nc-sa/3.0/deed.zh" target="_blank" rel="external">署名-非商业用途-保持一致</a>的创作共用协议.</p> <blockquote> <p>工作忙了,总是骗自己说的没时间写博客,其实都是自己懒癌发作而已。题目写了浅谈,肯定文章是不够深入透彻的,刚刚爬进分布式门槛的婴儿,需要慢慢的学会走路。希望后面能通过自己总结再来一篇<code>深入理解Raft</code>(显然又在挖坑</p> </blockquote>
shadowsocks源码剖析 http://andrewliu.in/2016/07/24/shadowsocks源码剖析/ 2016-07-24T14:09:02.000Z 2017-06-17T22:14:00.000Z

VPS上嗨皮的跑了那么久shadowsock的server了, 但一直比较想搞明白具体是怎么实现的. 前段时间比较忙, 最近总算有点私人时间了, 速度的强读一发shadowsocks源码

什么是shadowsocks?

  • 不想再强行安利一发了, 毕竟开发者都被请去喝茶了
  • 只需要知道和G * F * W有关就好了
  • 如果上面你都不知道, 说明这篇文章与你无缘, 叉掉吧

所读源码基于shadowsocks-go 1.1.5, 为什么选择golang实现版本? 我难道会告诉你, Golang的吉祥物太萌了, 我已经决定好好安利golang了. 正经点的回复, golang的语法灵活不失简洁, 棒棒哒.

终入正题

先来张图, 假装自己很专业…

流程概述:

  1. shadowsocks包含local和server两个程序, local运行在当前登录想要翻墙的机器上, server运行在墙外的服务器上(比如我的server运行在vultr家的VPS上)
  2. local监控本地1080端口, 提供socks v5协议接口, 浏览器请求和local的1080端口建立TCP连接, 首先进行local端对本机进程进行心跳检测, 然后与server端建立请求发出实际请求包(目标的地址和端口)
  3. 连接传输的数据通过加密,一般使用aes-256-cfb
  4. server端解密收到的数据, 然后与实际请求的目标ip和端口建立TCP连接, 将获取的数据写回到local端, local端最终写回到浏览器进程完成一次翻越长城.

show me your code

口说无凭, 也让人觉得云里雾里, 所以来是直接上代码吧

PS: 代码均进行了精简, 只保持核心逻辑

  • 首先来看shadowsocks-local文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// local的入口
func main() {
/*
* 1. 设置日志配置和命令行参数解析
* 2. 从文件中读取相关配置, 如server ip, port, 用户设置的密码, 本地监控端口等
*/
run(cmdLocal + ":" + strconv.Itoa(config.LocalPort))
}
// 实际监控函数
func run(listenAddr string) {
/*
* 1. 设置要监控的端口
* 2. 在一个死循环中, 不断接收请求, 然后在goroutine中调用handleConnection(conn)处理请求
*/
ln, err := net.Listen("tcp", listenAddr)
for { // 运行与一个死循环
conn, err := ln.Accept()
go handleConnection(conn)
}
}
// 运行在goroutine中请求处理函数
func handleConnection(conn net.Conn) {
/*
* 1. 与请求进程进行心跳检测
* 2. 解析请求获取原始数据, 用于获取实际请求ip和port
* 3. 与墙外的server请求建立TCP连接, 并发送数据
* 4. 等待墙外server写入数据, 然后将数据写回给请求进程
*/
handShake(conn)
rawaddr, addr, err := getRequest(conn)
_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x08, 0x43})
remote, err := createServerConn(rawaddr, addr)
go ss.PipeThenClose(conn, remote)
ss.PipeThenClose(remote, conn)
}

最后我们来看一下 PipeThenClose这个函数的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// PipeThenClose copies data from src to dst, closes dst when done.
func PipeThenClose(src, dst net.Conn) {
// closes dst when done.
defer dst.Close()
/*
* 1. 从leaky buffer中获取空闲buffer
* 2. 从source net.Conn中读取数据, 然后转发给dst net.Conn
*/
buf := leakyBuf.Get()
defer leakyBuf.Put(buf)
for {
SetReadTimeout(src)
n, err := src.Read(buf)
// read may return EOF with n > 0
// should always process n > 0 bytes before handling error
if n > 0 {
// Note: avoid overwrite err returned by Read.
if _, err := dst.Write(buf[0:n]); err != nil {
Debug.Println("write:", err)
break
}
}
if err != nil {
break
}
}
}
  • 再来看shadowsocks-server代码, 你会发现两者惊人的相似
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 主要来handleConnection
// goroutine, 内部需要自行保证线程安全
func handleConnection(conn *ss.Conn, auth bool) {
/*
* 1. 解析请求的ip:port
* 2. 建立TCP连接
* 3. 向目标站点发起请求, 响应数据写会到local
*/
host, ota, err := getRequest(conn, auth)
//对应ip:port建立TCP连接
remote, err := net.Dial("tcp", host)
if ota {
go ss.PipeThenCloseOta(conn, remote)
} else {
go ss.PipeThenClose(conn, remote)
}
// 从remote读取数据
ss.PipeThenClose(remote, conn)
closed = true
return
}

由local和server端总共四个PipeThenClose函数完成了由local到server, 再由server到local的数据传输. 但是要注意的是在server端是没有与local进行心跳检测的阶段

socks v5

socks v5代码TCP协议的流程:

  1. 用户进程与监听1080端口的进程建立TCP连接
  2. 心跳检测, 用户进程向监听进程发送VER, NMETHODS, METHODS, 监听进程向请求进程响应VER, METHOD
  3. 用户进程结束心跳检测, 向监听进程发起请求, 格式如下
  4. 监听进程对用户进程响应, 表示成功, 则表示用户进程可以进行发送数据.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
向监听进程的请求:
  +----+-----+-------+------+----------+----------+
  |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
  +----+-----+-------+------+----------+----------+
  | 1 |  1 | X'00' |  1 | Variable |   2  |
  +----+-----+-------+------+----------+----------+
监听进程的响应:
  +----+-----+-------+------+----------+----------+
  |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
  +----+-----+-------+------+----------+----------+
  | 1 | 1 | X'00' | 1  | Variable |   2  |
  +----+-----+-------+------+----------+----------+
// 对应shadowsocks-local中的:
_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x08, 0x43})

参考链接

]>
<blockquote> <p>VPS上嗨皮的跑了那么久shadowsock的server了, 但一直比较想搞明白具体是怎么实现的. 前段时间比较忙, 最近总算有点私人时间了, 速度的强读一发shadowsocks源码</p> </blockquote> <h2 id="什么是shadowsocks"><a href="#什么是shadowsocks" class="headerlink" title="什么是shadowsocks?"></a>什么是shadowsocks?</h2><ul> <li>不想再强行安利一发了, 毕竟开发者都被请去喝茶了</li> <li>只需要知道和<code>G * F * W</code>有关就好了</li> <li>如果上面你都不知道, 说明这篇文章与你无缘, 叉掉吧</li> </ul> <p>所读源码基于<a href="https://github.com/shadowsocks/shadowsocks-go" target="_blank" rel="external">shadowsocks-go 1.1.5</a>, 为什么选择golang实现版本? 我难道会告诉你, Golang的吉祥物太萌了, 我已经决定好好安利golang了. 正经点的回复, golang的语法灵活不失简洁, 棒棒哒.</p>
Golang RPC http://andrewliu.in/2016/07/02/Golang-RPC/ 2016-07-02T15:18:02.000Z 2017-06-17T22:14:00.000Z 本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循署名-非商业用途-保持一致的创作共用协议.

RPC(Remote Procedure Call)是分布式系统中的一个关键机制.

Go HTTP RPC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// 基于官方的example
package main
import (
"fmt"
"net"
"net/rpc"
"log"
"net/http"
)
type Args struct { // 参数封装
A, B int
}
type Arith int
func (t *Arith) Multiply(args *Args, reply *int) error {
*reply = args.A * args.B
return nil
}
func makeService() {
/*
func Register(rcvr interface{}) error
将Service(Arith)注册到默认的Server中
*/
rpc.Register(new(Arith))
/*
func HandleHTTP()
RPC消息有HTTP Handler来处理, Handler注册到默认的Server
*/
rpc.HandleHTTP()
/*
func Listen(net, laddr string) (Listener, error)
第一个参数为面向流的网络("tcp", "tcp4", "tcp6", "unix", "unixpacket").
第二个参数为字符串形式的地址, 如"127.0.0.1:1234", 省略host表示可以监听所有host
*/
l, e := net.Listen("tcp", ":1234")
if e != nil {
log.Fatal("listen error:", e)
}
/*
func Serve(l net.Listener, handler Handler) error
*/
go http.Serve(l, nil)
}
func makeClient() {
/*
func DialHTTP(network, address string) (*Client, error)
参数类似于net.Listen
*/
client, err := rpc.DialHTTP("tcp", "127.0.0.1" + ":1234")
defer client.Close() // 延迟关闭client
if err != nil {
log.Fatal("dialing:", err)
}
args := &Args{7, 8}
var reply int
/*
func (client *Client) Call(serviceMethod string, args interface{}, reply interface{}) error
client的方法, 第一个参数为调用服务的方法, 第二个参数方法需要的参数, 第三个参数为执行结果
*/
err = client.Call("Arith.Multiply", args, reply)
if err != nil {
log.Fatal("Arith rpc call error:", err)
}
fmt.Println(reply)
}
func main() {
// 只是为了一个启动使用了goroutine,正常情况应该分别启动一个客户端和一个服务器
makeService()
makeClient()
}

Go JSON RPC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package main
import (
"fmt"
"net"
"net/rpc"
"log"
"net/http"
"net/rpc/jsonrpc"
)
type Args struct { // 参数封装
A, B int
}
type Arith int
func (t *Arith) Multiply(args *Args, reply *int) error {
*reply = args.A * args.B
return nil
}
// 使用json作为rpc
func makeJSONService() {
rpc.Register(new(Arith))
tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234")
if err != nil {
panic(err)
}
/*
ListenTCP 类似于 Listen. func ListenTCP(net string, laddr *TCPAddr) (*TCPListener, error)
第一个参数为面向流的网络("tcp", "tcp4", or "tcp6").
第二个参数为字符串形式的地址, 如"127.0.0.1:1234", 省略host表示可以监听所有host
*/
listener, err := net.ListenTCP("tcp", tcpAddr)
if err != nil {
panic(err)
}
go func () {
for {
conn, err := listener.Accept()
if err != nil {
continue
}
// 注意此处的不同
/*
func ServeConn(conn io.ReadWriteCloser)
此函数阻塞运行一个JSON-RPC
*/
jsonrpc.ServeConn(conn)
}
}()
}
func makeJSONClient() {
// JSON-rpc的客户端基本和TCP一样
client, err := jsonrpc.Dial("tcp", "127.0.0.1:1234")
if err != nil {
log.Fatal("dialing:", err)
}
// Synchronous call
args := &Args{17, 8}
var reply int
err = client.Call("Arith.Multiply", args, &reply)
if err != nil {
log.Fatal("Arith rpc call error:", err)
}
fmt.Println(reply)
}

6.824 labrpc结构

数据结构:

  • Network用于模拟网络, 使整个RPC调用过程中在内部发生, 不需要真正的执行网路请求
  • Server一个RPC Server, 内部包含多个注册的Service
  • ClientEnd表示RPC客户端的数据结构
  • Service用于客户端来注册struct, 然后封装入该数据结构
  • reqSrc RPC调用的参数被解析然后重新封装到整个数据结构中
  • replyMsg封装RPC调用的返回结果

架构(可以选择性忽略Network部分)

参考链接

]>
<p>本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循<a href="http://creativecommons.org/licenses/by-nc-sa/3.0/deed.zh" target="_blank" rel="external">署名-非商业用途-保持一致</a>的创作共用协议.</p> <blockquote> <p>RPC(Remote Procedure Call)是<code>分布式系统</code>中的一个关键机制.</p> </blockquote> <h2 id="Go-HTTP-RPC"><a href="#Go-HTTP-RPC" class="headerlink" title="Go HTTP RPC"></a>Go HTTP RPC</h2>
Tornado源码剖析 http://andrewliu.in/2016/06/19/Tornado源码剖析/ 2016-06-19T15:05:25.000Z 2017-06-17T22:14:00.000Z pdb调试

简单的介绍一下pdb的调试, 更详细的命令查看python pdbf官方文档

1
2
3
import pdb
// 使用以下语句希望debug的地方打断点
pdb.set_trace()
命令 说明
n 运行下一行代码
p 计算p后面的表达式(当前上下文中), 并打印表达式的值
s 进入函数
r 从函数中返回
b 动态设置断点
w 打印当前栈信息
q 退出pdb

Tornado剖析

Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed. By using non-blocking network I/O, Tornado can scale to tens of thousands of open connections, making it ideal for long polling, WebSockets, and other applications that require a long-lived connection to each user.

源码剖析基于Tornado 2.0.0及以下测试源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import tornado.web
import tornado.httpserver
import tornado.ioloop
import tornado.options
import os.path
from tornado.options import define, options
define("port", default=8000, help="run on the given port", type=int)
# 继承Application
class Application(tornado.web.Application):
def __init__(self):
handlers = [
(r"/", MainHandler),
]
settings = dict(
template_path=os.path.join(os.path.dirname(__file__), "templates"),
static_path=os.path.join(os.path.dirname(__file__), "static"),
debug=True,
)
tornado.web.Application.__init__(self, handlers, **settings)
# URI Hanlder逻辑
class MainHandler(tornado.web.RequestHandler):
def get(self):
import pdb
pdb.set_trace()
self.write("Hello, World")
def run():
tornado.options.parse_command_line()
http_server = tornado.httpserver.HTTPServer(Application())
http_server.listen(options.port)
print "Start server, http://localhost:%s" % options.port
tornado.ioloop.IOLoop.instance().start()
if __name__ == "__main__":
run()

首先运行一上源码(服务器端), 另外开启一个Terminal来发出请求(也可以使用浏览器来访问). 服务端收到请求后, 会在我们pdb.set_trace()停下等待调试.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 新的Terminal命令行执行以下命令
$ curl http://localhost:8000
// 此时服务器端会进入`pdb`调试状态. 输入w以获得当前调用栈信息如下:
(Pdb) w
/Users/andrew_liu/Development/BackEnd/Python3/TornadoToturial/src/bin/run(13)<module>()
-> sys.exit(learn.learn_source.run())
/Users/andrew_liu/Development/BackEnd/Python3/TornadoToturial/src/learn/learn_source.py(41)run()
-> tornado.ioloop.IOLoop.instance().start()
/Users/andrew_liu/Development/BackEnd/Python3/TornadoToturial/src/.buildout/eggs/tornado-2.0-py2.7.egg/tornado/ioloop.py(233)start()
-> self._run_callback(callback)
/Users/andrew_liu/Development/BackEnd/Python3/TornadoToturial/src/.buildout/eggs/tornado-2.0-py2.7.egg/tornado/ioloop.py(370)_run_callback()
-> callback()
/Users/andrew_liu/Development/BackEnd/Python3/TornadoToturial/src/.buildout/eggs/tornado-2.0-py2.7.egg/tornado/stack_context.py(159)wrapped()
-> callback(*args, **kwargs)
/Users/andrew_liu/Development/BackEnd/Python3/TornadoToturial/src/.buildout/eggs/tornado-2.0-py2.7.egg/tornado/iostream.py(235)wrapper()
-> callback(*args)
/Users/andrew_liu/Development/BackEnd/Python3/TornadoToturial/src/.buildout/eggs/tornado-2.0-py2.7.egg/tornado/stack_context.py(159)wrapped()
-> callback(*args, **kwargs)
/Users/andrew_liu/Development/BackEnd/Python3/TornadoToturial/src/.buildout/eggs/tornado-2.0-py2.7.egg/tornado/httpserver.py(400)_on_headers()
-> self.request_callback(self._request)
/Users/andrew_liu/Development/BackEnd/Python3/TornadoToturial/src/.buildout/eggs/tornado-2.0-py2.7.egg/tornado/web.py(1282)__call__()
-> handler._execute(transforms, *args, **kwargs)
/Users/andrew_liu/Development/BackEnd/Python3/TornadoToturial/src/.buildout/eggs/tornado-2.0-py2.7.egg/tornado/web.py(927)_execute()
-> getattr(self, self.request.method.lower())(*args, **kwargs)
> /Users/andrew_liu/Development/BackEnd/Python3/TornadoToturial/src/learn/learn_source.py(33)get()
-> self.write("Hello, World")

一. 首先Application调用__init__, 通过self.add_handlers(".*$", handlers)配置路由, 配置静态资源资源路径.
二. tornado.httpserver.HTTPServer接收一个Application参数并命名为request_callback, 注意此处Application实例被命名为request_callback 留意HTTPServer中有一个属性self._sockets保存fdsocket object的映射.
三. 调用HTTPServerlisten(options.port)方法开始做socket监听. listen中做了两件事. 一: bind创建socket对象设置并设置非阻塞, 并进行socket.bind()socket.listen()监听. 并在self._sockets保存socket描述符和socket对象的映射. 二: start函数初始化一个IOLoop对象(单例对象), 并遍历self._socketssocket描述符, 将其添加到IOLoop并绑定读事件

为了方便查看, 本来去掉源码中的异常捕获和一些干扰阅读的细节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 步骤三中的核心代码
# 步骤三第一部分
class HTTPServer(object):
def bind(self, port, address=None, family=socket.AF_UNSPEC)
# 网编编程的一套, socket => bind => listen
sock = socket.socket(af, socktype, proto)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.setblocking(0)
sock.bind(sockaddr)
sock.listen(128)
self._sockets[sock.fileno()] = sock
# 步骤三第二部分
def start(self, num_processes=1)
if not self.io_loop:
self.io_loop = ioloop.IOLoop.instance() # 返回一个全局的IOLoop对象
for fd in self._sockets.keys(): # 对整个fd和socket对象的映射集合, 都加入到epoll, 并注册回调
self.io_loop.add_handler(fd, self._handle_events, ioloop.IOLoop.READ) # 给每个fd绑定读事件, self._handle_events为回调事件(当fd可读的时候, 则调用此函数)

四. tornado.ioloop.IOLoop.instance().start()中通过步骤三中全局的IOLoop对象执行start. 此处核心代码为event_pairs = self._impl.poll(0.2). 其中self._impl根据不同平台来选择select/poll/epoll/kqueue, 每当有事件可读时, 则执行其回调函数self._handle_events. 整个回调函数为HTTPServer中的handler_events函数

1
2
3
4
5
6
7
8
9
# 步骤四start中的核心代码
class IOLoop(object):
def start(self)
while True:
event_pairs = self._impl.poll(poll_timeout) # epoll对触发读事件的描述符返回, 此处说明有客户端访问服务器
self._events.update(event_pairs) # 将要处理的事件更新到self._events这个dict中
while self._events:
fd, events = self._events.popitem() # 随机取出一个事件(key-value)
self._handlers[fd](fd, events) # self._handlers是在第三步中add_handler中添加的, self._handlers是fd描述符和self._handle_events形成的映射(dcit). 此处从self._handlers中取出fd描述符对应的self._handle_events执行.

五. 最后我们发现, 让了一圈又回到HTTPServer._handle_events函数上. 函数中创建了IOStream对象和HTTPConnection对象.

1
2
3
4
5
6
7
8
9
# 步骤五的核心代码
# 当我们访问http://localhost:8000, epoll返回事件
class HTTPServer(object):
def _handle_events(fd, event):
while True:
connection, address = self._sockets[fd].accept() # 此处connection为客户端连接
stream = iostream.IOStream(connection, io_loop=self.io_loop)
HTTPConnection(stream, address, self.request_callback, self.no_keep_alive, self.xheaders)

其中iostream.IOstream回通过client socket(connection)来初始化, 并且完成将fd注册到IOLoop的epoll中. 当fd可读时, 会调用IOStream._handle_events函数做回调, 描述符可读, 即客户端发送了HTTP, 读取客户端发送的数据写入IOStream中的self._read_buffer读缓冲区中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class IOStream(object):
def __init__(self, socket, io_loop=None, max_buffer_size=104857600, read_chunk_size=4096):
self.socket = socket
self.socket.setblocking(False)
self.io_loop = io_loop or ioloop.IOLoop.instance()
self.max_buffer_size = max_buffer_size
self.read_chunk_size = read_chunk_size
self._state = self.io_loop.ERROR
with stack_context.NullContext():
self.io_loop.add_handler(
self.socket.fileno(), self._handle_events, self._state) # 将client socket注册到io_loop中, 并绑定回调事件self.handle_events(IOStream)
def _handle_events(self, fd, events):
if events & self.io_loop.READ: # 当客户端有链接时, 则event可读
self._handle_read()
def _handle_read(self):
while True:
result = self._read_to_buffer() # 将数据接入读缓冲区self._read_buffer中

然后通过iostream来初始化HTTPConnection, 注意HTTPConnection中的self.request_callback属性就是在HTTPServer初始化时的Application. 其中执行self.stream.read_until并读取缓冲区的数据, 然后回调HTTPConnection._on_headers解析HTTP请求的头部, 然后出现了最重要的一步!!!, self.request_callback(self._request), 终于出现了, 这是最吼的! self.request_callback就是我们用来初始化HTTPServerApplication对象呀!!! 这货有个黑魔法函数__call__, 可以直接对Application对象进行传参调用!!! 传入客户端的request请求到Application中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 这个类执行真正的客户端请求处理
class HTTPConnection(object):
"""Handles a connection to an HTTP client, executing HTTP requests.
"""
def __init__(self, stream, address, request_callback, no_keep_alive=False, xheaders=False):
self.stream = stream # IOStream包含数据缓冲区
self.address = address
self.request_callback = request_callback # Application类
self.no_keep_alive = no_keep_alive
self.xheaders = xheaders
self._request = None
self._header_callback = stack_context.wrap(self._on_headers)
self.stream.read_until(b("\r\n\r\n"), self._header_callback) # 通过指定分隔符从self.stream中读取数据, 然后回调self._header_callback, 即self._on_headers
def _on_headers(self, data):
self._request = HTTPRequest(connection=self, method=method, uri=uri, version=version,headers=headers, remote_ip=self.address[0]) # 对请求的头部进行解析, 然后生成HTTPRequest对象, 注意此处的connection(HTTPConnection), 向客户端发送数据会用到
self.request_callback(self._request) # 最后终于出现self.request_callback了!!! 这就是Application呀!! 执行他的黑魔法__call__方法

六. 重新看栈信息self.request_callback(self._request) web.py(1282)__call__()完全符合我们的分析. __call__接收self._request, 其中通过_request中host信息作路径匹配, 如果路径相匹配则返回我们创建的Handler类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Application(object):
def __call__(self, request):
handlers = self._get_host_handlers(request) # 获取与host匹配的handler
for spec in handlers:
match = spec.regex.match(request.path)
if match:
handler = spec.handler_class(self, request, **spec.kwargs) # 找到最初我们注册的MainHandler并创建实例.
handler._execute(transforms, *args, **kwargs)
def _get_host_handlers(self, request):
host = request.host.lower().split(':')[0]
for pattern, handlers in self.handlers:
if pattern.match(host):
return handlers

我们再次查看栈信息, 发现此时的handler即为MainHandler, 并执行对象的_execute方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-> handler._execute(transforms, *args, **kwargs)
(Pdb) l
1277 if getattr(RequestHandler, "_templates", None):
1278 for loader in RequestHandler._templates.values():
1279 loader.reset()
1280 RequestHandler._static_hashes = {}
1281
1282 -> handler._execute(transforms, *args, **kwargs)
1283 return handler
1284
1285 def reverse_url(self, name, *args):
1286 """Returns a URL path for handler named `name`
1287
(Pdb) print handler
<learn.learn_source.MainHandler object at 0x1054c7210>

七. handler._execute找到request中的HTTP方法, 然后执行对应的函数. 我们在MainHandler中实现了get方法, 此处会调用对应的方法. 并将数据发送给客户端.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class RequestHandler(object):
def _execute(self, transforms, *args, **kwargs):
self.prepare() # 次数调用了prepre方法
if not self._finished:
args = [self.decode_argument(arg) for arg in args]
kwargs = dict((k, self.decode_argument(v, name=k)) for (k,v) in kwargs.iteritems())
getattr(self, self.request.method.lower())(*args, **kwargs) # 获取对应的get/post等方法的具体实现并执行.
if self._auto_finish and not self._finished:
self.finish() # 数据的返回由finish完成
def finish(self, chunk=None):
# 中间执行一些构造响应头部的操作
if not self.application._wsgi:
self.flush(include_footers=True) # 数据刷新操作.
self.request.finish() # 移除客户端的相关操作
self._log()
self._finished = True
def flush(self, include_footers=False):
# Ignore the chunk and only write the headers for HEAD requests
if self.request.method == "HEAD":
if headers: self.request.write(headers)
return
if headers or chunk:
self.request.write(headers + chunk) # write执行写入操作
class HTTPRequest(object):
def write(self, chunk):
"""Writes the given chunk to the response stream."""
assert isinstance(chunk, bytes_type)
self.connection.write(chunk) # 此处connection是一个HTTPConnection
class HTTPConnection(object):
def write(self, chunk):
"""Writes a chunk of output to the stream."""
assert self._request, "Request closed"
if not self.stream.closed():
self.stream.write(chunk, self._on_write_complete) # 实现写入操作有IOStream完成.
# IOStream是对客户端socket的封装
class IOStream(object):
def write(self, data, callback=None):
"""Write the given data to this stream.
"""
assert isinstance(data, bytes_type)
self._check_closed()
self._write_buffer.append(data)
self._add_io_state(self.io_loop.WRITE) # 这里改变为WRITE, 则执行写操作
self._write_callback = stack_context.wrap(callback)
def _handle_events(self, fd, events):
if events & self.io_loop.WRITE:
if self._connecting:
self._handle_connect()
self._handle_write()
def _handle_write(self):
while self._write_buffer:
num_bytes = self.socket.send(self._write_buffer[0]) # 终于完成了数据发送

八. 完成数据写入client socket后, HTTPRequest.finish()被调用执行移除时间和关闭客户端socket操作. 是不是已经晕了, 稍等我们来画个图来理一理整个流程.

参考链接

]>
<h2 id="pdb调试"><a href="#pdb调试" class="headerlink" title="pdb调试"></a>pdb调试</h2><blockquote> <p>简单的介绍一下pdb的调试, 更详细的命令查看<a href="https://docs.python.org/2/library/pdb.html" target="_blank" rel="external">python pdbf官方文档</a></p> </blockquote> <figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div></pre></td><td class="code"><pre><div class="line">import pdb</div><div class="line">// 使用以下语句希望debug的地方打断点</div><div class="line">pdb.set_trace()</div></pre></td></tr></table></figure> <table> <thead> <tr> <th style="text-align:center">命令</th> <th style="text-align:center">说明</th> </tr> </thead> <tbody> <tr> <td style="text-align:center">n</td> <td style="text-align:center">运行下一行代码</td> </tr> <tr> <td style="text-align:center">p</td> <td style="text-align:center">计算p后面的表达式(当前上下文中), 并打印表达式的值</td> </tr> <tr> <td style="text-align:center">s</td> <td style="text-align:center">进入函数</td> </tr> <tr> <td style="text-align:center">r</td> <td style="text-align:center">从函数中返回</td> </tr> <tr> <td style="text-align:center">b</td> <td style="text-align:center">动态设置断点</td> </tr> <tr> <td style="text-align:center">w</td> <td style="text-align:center">打印当前栈信息</td> </tr> <tr> <td style="text-align:center">q</td> <td style="text-align:center">退出pdb</td> </tr> </tbody> </table>
Google protobuf(C++) 学习笔记 http://andrewliu.in/2016/06/05/Google-protobuf-C-学习笔记/ 2016-06-05T10:26:17.000Z 2017-06-17T22:14:00.000Z 本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循署名-非商业用途-保持一致的创作共用协议.

Google Protocol Buffers简称 Protobuf, 是Google公司内部的混合语言数据标准. 它提供一种轻量, 高效的结构化数据存储结构.

简介

为什么学习protobuf?

想通过protobuf的序列化来做一个C++的微型RPC框架.
本文主要是学习protobuf的使用, 大量参考官方文档.

为什么要使用protobuf?

  1. 因为protobuf是谷歌出的, 性能不过, 重要是的我是谷歌脑残粉(逃
  2. 官方文档中提到一些protobuf的优点, protobuf灵活高效的结构化数据存储格式. 方便用于序列化, 适合做RPC的数据交换.
  3. 相比XML, protobuf比 XML 更小、更快、更简单. 仅需要写一个*.proto文件描述需要的数据结构, protobuf会帮助你实现相关类和方法(自动化多好!).
  4. 目前提供C++, Java, Python, Go, C#等多种语言的API

安装

神奇Mac版homebrew帮你解决一切问题

1
2
3
4
5
6
7
# 安装最新的protobuf
$ brew install protobuf
# 验证protobuf是否安装成功
$ protoc --version
# 在我电脑上的输出
libprotoc 3.0.0

学习笔记

通过一个简单的官方例子来学习protobuf

我们首先定义一个addressbook.proto, 通过message来定义需要序列化的数据结构, message内包含许多key-value对

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# addressbook.proto
package tutorial;
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phone = 4;
}
message AddressBook {
repeated Person person = 1;
}

ps:

  1. message中有多种类型, bool, int32, float, double, ,string and enum以及内嵌的message
  2. message内可以定义optional、required、repeated字段

定义好*.proto文件后, 我们通过protobuf提供的protoc命令行工具来自动生成相关数据结构源码

1
2
3
4
5
// 编译.proto文件
$ protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto
// 范例, 源目录为当前文件, 输出路径为当前文件
$ protoc -I=./ --cpp_out=./ ./addressbook.proto
  • -I指明源文件所在目录
  • --cpp_out指向想要输出的文件路径
  • 最后一个参数为.proto所在的路径

使用protobuf

生成我们自定义的类后, 我们开始尝试使用生成的文件. add_person.cpp用于从用户的输入中, 创建一个Person并初始化一个Person对象, 并将该对象通过二进制的形式写入给定文件中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// 代码出自官方文档
// add_person.cpp
//
// Created by Andrew_liu on 16/4/17.
//
#include <iostream>
#include <fstream>
#include <string>
#include "addressbook.pb.h"
using namespace std;
// 基于用户输出填充Person message
void PromptForAddress(tutorial::Person* person) {
cout << "Enter person ID number: ";
int id;
cin >> id;
person->set_id(id);
cin.ignore(256, '\n');
cout << "Enter name: ";
getline(cin, *person->mutable_name());
cout << "Enter email address (blank for none): ";
string email;
getline(cin, email);
if(!email.empty()) {
person->set_email(email);
}
while(true) {
cout << "Enter a phone number: ";
string number;
getline(cin, number);
if(number.empty()) {
break;
}
tutorial::Person::PhoneNumber* phone_number = person->add_phone();
phone_number->set_number(number);
cout << "Is this a mobile, home, or work phone(type: mobile/home/work): ";
string type;
getline(cin, type);
if (type == "mobile") {
phone_number->set_type(tutorial::Person::MOBILE);
} else if (type == "home") {
phone_number->set_type(tutorial::Person::HOME);
} else if (type == "work") {
phone_number->set_type(tutorial::Person::WORK);
} else {
cout << "Unknown phone type. Using default." << endl;
}
}
}
int main(int argc, char* argv[]) {
GOOGLE_PROTOBUF_VERIFY_VERSION;
if (argc != 2) {
cerr << "Usage: " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
return -1;
}
tutorial::AddressBook address_book;
{
fstream input(argv[1], ios::in | ios::binary);
if (!input) {
cout << argv[1] << ": File not found. Creating a new file. " << endl;
} else if (!address_book.ParseFromIstream(&input)) {
cerr << "Failed to parse address book." << endl;
return -1;
}
}
// add an address
PromptForAddress(address_book.add_person());
{
// write new address book back to disk
fstream output(argv[1], ios::out | ios::trunc | ios::binary);
if(!address_book.SerializeToOstream(&output)) {
cerr << "Failed to write address book." << endl;
return -1;
}
}
// delete all global object allocated by libprotobuf
google::protobuf::ShutdownProtobufLibrary();
return 0;
}

list_person.cpp通过给定的文件名读取Person对象, 并将每个Person对象输出.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// list_person.cpp
// Created by Andrew_liu on 16/4/17.
//
#include <iostream>
#include <fstream>
#include <string>
#include "addressbook.pb.h"
using namespace std;
void ListPeople(const tutorial::AddressBook& address_book) {
for (int i = 0; i < address_book.person_size(); i++) {
const tutorial::Person& person = address_book.person(i);
cout << "Person ID: " << person.id() << endl;
cout << "Name: " << person.name() << endl;
if (person.has_email()) {
cout << "E-mail address: " << person.email() << endl;
}
for (int j = 0; j < person.phone_size(); ++j) {
const tutorial::Person::PhoneNumber& phone_number = person.phone(j);
switch (phone_number.type()) {
case tutorial::Person::MOBILE:
cout << "Mobile phone #: ";
break;
case tutorial::Person::HOME:
cout << "Home phone #: ";
break;
case tutorial::Person::WORK:
cout << "Work phone #: ";
}
cout << phone_number.number() << endl;
}
}
}
int main(int argc, char* argv[]) {
GOOGLE_PROTOBUF_VERIFY_VERSION;
if (argc != 2) {
cerr << "Usage: " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
return -1;
}
tutorial::AddressBook address_book;
{
fstream input(argv[1], ios::in | ios::binary);
if (!address_book.ParseFromIstream(&input)) {
cerr << "Failed to parse address book." << endl;
return -1;
}
}
ListPeople(address_book);
google::protobuf::ShutdownProtobufLibrary();
return 0;
}

完成后, 我们分别编译两个源文件, 编译命令如下:

1
2
3
4
5
# 编译add_person.cpp, 生成writer可执行文件
$ g++ -o writer add_person.cpp addressbook.pb.cc `pkg-config --cflags --libs protobuf`
# 编译list_person.cpp, 生成reader可执行文件
g++ -o reader list_person.cpp addressbook.pb.cc `pkg-config --cflags --libs protobuf`

图片为执行writerreader的结果

实现RPC

1
2
3
4
5
6
7
8
9
10
package news;
message NewsRequest {
required string message = 1;
};
message NewsResponse {
required string response = 1;
};
service NewsService {
rpc News(NewsRequest) returns (NewsResponse);
};

参考链接

]>
<p>本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循<a href="http://creativecommons.org/licenses/by-nc-sa/3.0/deed.zh" target="_blank" rel="external">署名-非商业用途-保持一致</a>的创作共用协议.</p> <blockquote> <p> <code>Google Protocol Buffers</code>简称 Protobuf, 是<code>Google</code>公司内部的混合语言数据标准. 它提供一种轻量, 高效的结构化数据存储结构. </p> </blockquote> <h2 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h2><p><strong>为什么学习protobuf?</strong></p> <p>想通过protobuf的序列化来做一个C++的微型RPC框架.<br>本文主要是学习protobuf的使用, 大量参考官方文档.</p>
The Google File System http://andrewliu.in/2016/05/15/The-Google-File-System/ 2016-05-15T15:28:55.000Z 2017-06-17T22:14:00.000Z 本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循署名-非商业用途-保持一致的创作共用协议.

GFS是一个共享的分布式文件系统

  • GFS提供了一个与位置无关的名字空间(namespace), 这使得数据可以为了负载均衡或灾难冗余等目的在不同位置间透明迁移
  • GFS并没有在文件系统层面提供任何Cache机制, 但借助了Linux底层cache机制.
  • GFS文件以分层目录的形式组织, 用路径名来表示, 文件的写入主要依赖追加(append)操作完成

架构

  • GFS集群包含单Master和多个chunkservers
  • 文件被划分为固定大小的块(chunk, 默认为64MB), 在块被创建时, 由Master分配给块的全局唯一不可变的64位的块句柄(chunk handle), 每个块默认存储三个副本到不同的chunserver上(容错)
  • Master维护所有的文件系统元数据. 这些元数据包括命名空间、访问控制信息、文件到块的映射信息、以及当前块的位置(命令空间, 文件到块映射和chunk所有副本的位置为Master的三大原信息存储在内存中保证访问速度, 命名空间和文件到块的映射会持久化到操作日志中). Master节点还控制着系统范围内的活动, 比如, 块租用管理、孤立块的垃圾回收、以及块在chunkserver间的迁移。Master用心跳信息(HeartBeat messages)周期地和每个chunkserver通讯, 向chunkserver发送指令并收集其状态.
  • 无论是客户端还是chunkserver都不需要缓存文件数据, chunkserver由linux自带的缓存策略来完成缓存

通信流程:

  1. GFS client使用固定的块大小(fix chunk size)将应用程序指定的文件名和字节偏移转换成文件中的块索引(chunk index)
  2. GFS client向master发送request(包含文件名和块索引)询问应该与那些chunkservers通信
  3. master 响应对应的块句柄(chunk handle)和块副本位置信息
  4. GFS client发送request到某个副本(距离最近的一个). request中包含快句柄和块内字节范围. 对同一个块的进一步读取不再需要client和master的交互了, 直到client缓存(可以通信的chunkservers, 从步骤3中缓存)信息过期或者文件被重新打开

一致性模型

GFS是松一致性模型(a relaxed consistency model)

  • 文件命名空间的变更(如文件创建)是原子性的. 它们只能由master控制:命名空间锁(namspace locking)保证了原子性和正确性

系统交互

控制流

  1. 客户机询问master哪一个chunkserver持有该块当前的租约以及其它副本的位置. 如果没有chunkserver持有租约, master将租约授权给它选择的副本.
  2. master将主副本(被master授予租约的副本)的标识符以及其它副本(次级副本)的位置返回给客户机. 客户机为将来的变更缓存这些数据. 只有在主副本不可达, 或者其回应它已不再持有租约的时候,客户机才需要再一次联系master
  3. 客户机将数据推送到所有副本.客户机可以以任意的顺序推送数据. chunkserver将数据存储在内部LRU 缓存中, 直到数据被使用或者过期. 通过从控制流解耦数据流, 我们可以基于网络拓扑而不管哪个chunksever上有主副本, 通过调度昂贵的数据流来提高系统性能
  4. 当所有的副本都确认接收到了数据, 客户机对主副本发送写请求. 这个请求标识了早前推送到所有副本的数据. 主副本为接收到的所有变更分配连续的序列号, 由于变更可能来自多个客户机, 这就提供了必要的序列化. 它以序列号的顺序把变更应用到它自己的本地状态中(并保存变更顺序)
  5. 主副本将写请求转发到所有的次级副本. 每个次级副本依照主副本分配的序列顺序应用变更
  6. 所有次级副本回复主副本并标明它们已经完成了操作
  7. 主副本回复客户机. 任何副本遇到的任何错误都报告给客户机. 出错的情况下,写操作可能在主副本和次级副本的任意子集上执行成功(如果在主副本失败, 就不会分配序列号和转发). 客户端请求被认定为失败, 被修改的域处于不一致的状态. 我们的客户机代码通过重试失败的变更来处理这样的错误.在退到从头开始重试之前, 客户机会将从步骤3到步骤7做几次重试

  • 步骤3描述将数据流从控制流中解耦. 在控制流从客户机到主副本再到所有次级级副本的同时, 数据以管道的方式, 线性地的沿着一个精心挑选的chunkserver链推送(push). 这样可以充分利用每个机器的带宽.
  • GFS提供了一种叫做记录追加(record append)的原子追加操作(类似UNIX以O_APPEND模式打开文件)
  • 快照操作(the snapshot operation)几乎瞬间完成对一个文件或者目录树的拷贝, 并且最小化对正在进行的变更的任何干扰. 快照使用标准的写时复制来实现.

Master操作

  1. Master的许多操作消耗大量时间, 为了不阻塞其他操作, 允许同时执行多个操作, 使用名称空间域上的锁(读写锁)来保证正确的串行化(serialization)
  2. GFS在文件删除后物理空间的回收是由垃圾收集(garbage collection)完成的

看到这里有个小疑惑, Master失效了会怎么选举?

容错和诊断

快速恢复和备份来实现GFS集群的高可用性

  • 快速回复, 使用直接kill系统中进程的方式来关闭服务器, 未完成的客户端请求则需要进行重连和重试.
  • 块拷贝, chunk副本默认会保证三个
  • Master拷贝, 此处回答了我的疑惑, 当Master挂掉, 首先尽快重启, 如果Master机器的硬件故障, 则监控系统会启动一个新的master进程. master中数据和状态的恢复通过操作日志(operation log)和检查点(checkpoints), 并且master操作日志和检查点都在多台机器上备份. 另外还有一种影子master(shadow master)策略来不保证在主master挂掉后, 系统依然可用.
  • 诊断工具, 诊断日志(diagnostic log), RPC日志来帮助定位问题, debug和性能分析.

GFS的优点:

  1. 支持大规模的读取和写入
  2. 高吞吐量
  3. 对数据(chunk)有良好的容错策略

GFS的缺点:

  1. Master的容错策略略有不足
  2. chunk默认64MB, 对小文件不友好.

参考链接

]>
<p>本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循<a href="http://creativecommons.org/licenses/by-nc-sa/3.0/deed.zh" target="_blank" rel="external">署名-非商业用途-保持一致</a>的创作共用协议.</p> <p><img src="http://ww1.sinaimg.cn/large/ab508d3djw1f2sj4b9nasj20q80begnu.jpg" alt=""></p> <blockquote> <p>GFS是一个共享的分布式文件系统</p> </blockquote> <ul> <li>GFS提供了一个与位置无关的名字空间(namespace), 这使得数据可以为了负载均衡或灾难冗余等目的在不同位置间透明迁移</li> <li>GFS并没有在文件系统层面提供任何Cache机制, 但借助了Linux底层cache机制.</li> <li>GFS文件以分层目录的形式组织, 用路径名来表示, 文件的写入主要依赖追加(append)操作完成</li> </ul>