强迫症的 Mac 设置指南

如何配置一个高效的 Mac 工作环境

Table of Contents

  1. OS X
  2. 常用工具
  3. 开发工具

一直想写这么一篇文章,把我从同事那里学到的经验分享出来。市面上有很多类似的文章,写得都非常好,让我受益匪浅。不过我还是有一些自己总结出来的经验想要分享。

在工作中,我一般会在 1 到 10 人的团队中,经常会结对编程,即两个人共用一台 Mac 工作,因此也经常会把 Mac 外接一个大显示器、鼠标和键盘。我的常用开发平台有 Java、Ruby、Node.js、Web 等,使用 JetBrains 的开发工具,比如 IntelliJ IDEA、RubyMine、WebStorm 等。

我深知自己的知识有限,所以写下本文以便和大家切磋交流。同时更有效率的方法和更好的工具也在不断涌现,我也贪心的希望把更好的方法和工具都收集更到到这里,我会不断更新本文,让它尽量不过时。最新内容请访问:https://github.com/macdao/ocds-guide-to-setting-up-mac。欢迎通过 GitHub 的Issues或者直接Pull Requests方式来分享你的经验。期待你的反馈。

我认为“一个高效的 Mac 工作环境”有以下几个特点:

  • 自动化

    举个例子。手动安装一个应用,需要1)打开浏览器,2)搜索应用的名字,3)打开应用网站,4)寻找下载链接和安装方法,5)下载并等待下载完成,6)安装下载文件,7)可能还有后续的安装步骤。而自动化安装一个应用,只需要1)打开终端工具,2)敲入安装命令,3)等待完成这几个步骤。

    自动化可以大大简化操作,提高效率。

  • 统一

    我经常结对编程,偶尔会遇到快捷键不一样,命令不同等问题。我强烈建议,至少在一个团队中,大家尽量使用相同的快捷键、命令等环境。(我记得有个实践就是这个,可是我一直没找到该实践的名字和出处,求告诉)

  • 够用

    够用就好,如果系统本身已经满足了我的需求,我不会再使用第三方工具。

  • 效率

    效率,一切都是为了效率。

本文对于第三方应用如何安装和使用只有最简单的介绍,具体还请参考官方网站和相关文档。

有些章节标题标注了[OCD],意思是这些章节带有我强烈的个人色彩,如果你跟我臭味相投,欢迎借鉴,如果你并不认同,请忽略掉好了。

PS:虽然本文名为“强迫症”,但其实并不是真正意义上的强迫症,真正意义上的强迫症是一种会对患者的日常生活产生负面影响的疾病。

1. OS X

本节介绍操作系统本身的一些设置。

功能键

默认情况下,F1-F12 都是特殊功能,比如调节屏幕亮度。而当你需要键入 F1-F12 时(比如在使用 IntelliJ IDEA 的快捷键时),需要同时按住 Fn。这对于开发人员来说是非常不方便的。

把 F1-F12 改成标准功能键:选择System Preferences > Keyboard,在Keyboard标签页中选中Use all F1, F2, etc. keys as standard function keys

全键盘控制

当你在 Sublime Text 里关闭文件时,可能会遇到这样的对话框:

dialog-box-without-all-controls

注意这个Save按钮跟其他两个按钮不太一样,它的底色是蓝的。这种按钮被称为默认按钮,除了用鼠标点击触发外,还可以通过回车键触发。

那么问题来了,如果你不想保存,想点击Don't Save,是不是只能用鼠标点击了呢?

并不是这样:选择System Preferences > Keyboard,在Shortcuts标签页中选择All controls;或者使用快捷键⌃F7。之后这个对话框会变成这样:

dialog-box-with-all-controls

这个Don't Save按钮有了一圈蓝边,这个意味着你可以通过空格键触发。不仅如此,你还可以用Tab键把蓝边转移到其他按钮,来实现全键盘控制。

除了All controls这个方法,你还可以用⌘⌫来选择Don't Save⌘⌫的作用是在包含“删除”或“不存储”按钮的对话框中选择“删除”或“不存储”。

除了上述两个办法之外,居然还有个方法!就是按⌘D!据说是因为按⌘+按钮的大写首字母可以触发该按钮。可是!我按了⌘C⌘S想取消和保存都没用!但是⌘D真的有用!如果仅仅是这也就算了,可是我又手贱试了下 TextEdit,在关闭未保存的文件时弹出的对话框上有三个按钮DeleteCancelSave。然而⌘D⌘C都没用,但是!⌘S可以保存!我完全不能理解!我整个人几乎都是崩溃的,只好以咆哮体写下这段文字。如果谁能解释请务必告诉我,必有重谢!

⌘C不能用应该是因为它绑定到了复制功能;而⌘D不能用因为它的作用是从“打开”对话框或“存储”对话框中选择“桌面”文件夹。

在这个对话框上,你可以用Esc来执行Cancel操作。

Spotlight 快捷键

中文版 OS X 的 Spotlight 的快捷键是⌃Space。这个快捷键有一些问题:

  • JetBrains 的 IDE,比如 IntelliJ IDEA、WebStorm 等都使用⌃Space作为自动完成这个最常用功能的快捷键。我不建议更改 IDE 的快捷键,而建议更改 Spotlight 的快捷键。
  • 对于没有添加中文输入法的 Mac 来说,Spotlight 的快捷键是⌘Space。英语国家的人都是这样的。所以我建议把 Spotlight 的快捷键设置为⌘Space,跟他们一致。

输入法快捷键

一般来说切换输入法的快捷键是⌘Space。由于我建议把 Spotlight 的快捷键设置为⌘Space,所以我建议把切换输入法的快捷键设置为⌥Space

其他快捷键

让双手尽量多的键盘和快捷键,少使用鼠标和触摸板,可以大大提高效率。

设置 Trackpad 轻点来点按

默认情况下按下触摸板才是点按(click)。我喜欢设置成用轻点作为点按:

选择System Preferences > Trackpad,在Point & Click标签页中选中Tap to click

语音

OS X 自带了语音功能,可以用say命令让 Mac 开口说话:

say hello

可以和&&或者;配合使用来提示你某任务已经完成:

brew update && brew upgrade && brew cleanup ; say mission complete

通过命令行来听取发音还是有点麻烦。其实我们几乎可以在任何地方选中单词,然后使用快捷键⌥+ESC发音。仅仅需要这样设置一下:选择System Preferences > Dictation & Speech,在Text to Speech标签页中选中Speak selected text when the key is pressed

词典

OS X 自带了词典(Dictionary)。你几乎可以在任何应用中通过三指轻拍触摸板来现实对应单词的释义。

也可以打开 Dictionary 应用来查找单词。

可以在 Dictionary 应用中添加英汉汉英词典。

Dock Position

默认 Dock 在屏幕下方。我们的屏幕一般都是 16:10,Dock 在屏幕下方的话会占据本来就不大的垂直空间。建议把 Dock 放到左边或者右边。

更改 Caps Lock 键为 Control 键

我经常用到Control键,但这个键在键盘的左下角,很难按到。同时我发现我很少使用Caps Lock键,我一般会用Shift键加字母来输入大写字母,或者先输入小写再(通过快捷键)转换成大写。

基于以上原因,我把Caps Lock键的功能改成了Control键。很多同事也都这么做的,可能是受到HHKB 的影响。

设置方法:选择System Preferences > Keyboard,在Keyboard标签页中点击Modifier Keys...按钮,在弹出的窗口中,把Caps Lock (⇪) Key:对应的选项改成⌃ Control

Remove all Dock icons[OCD]

本条目对于强迫症适用。

默认情况下 Dock 被一堆系统自带的应用占据着,而其中大部分我都很少使用,当我打开几个常用应用后,Dock 上会有很多图标,每个图标都会被挤得很小。所以我会把所有 Dock 上固定的图标都删掉,这样一来 Dock 上只有我打开的应用。

PS:Finder 图标是删不掉的。

重置 Launchpad 上图标位置[OCD]

本条目对于强迫症适用。

新的应用被安装后,经常会跑到 Launchpad 的第一屏,所以它们的位置跟安装的顺序有关系,而我更希望它们可以按照某种更加稳定的顺序排列,比如按照系统默认的顺序:

defaults write com.apple.dock ResetLaunchPad -bool true; killall Dock

在默认顺序中,Launchpad 第一屏只有 Apple 自家应用。

2. 常用工具

本节介绍一些常用的,跟开发没有直接关系的第三方应用及其设置。

Homebrew

包管理工具,官方称之为The missing package manager for OS X

安装步骤:先打开 Terminal 应用,输入:

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

有了 brew 以后,要下载工具,比如 MySQL、Gradle、Maven、Node.js 等工具,就不需要去网上下载了,只要一行命令就能搞定:

brew install mysql gradle maven node

PS:安装 brew 的时候会自动下载和安装 Apple 的 Command Line Tools。

brew 的替代品有 MacPorts,现在基本没人用它。

Homebrew Cask

brew-cask 允许你使用命令行安装 OS X 应用。比如你可以这样安装 Chrome:brew cask install google-chrome。还有 Evernote、Skype、Sublime Text、VirtualBox 等都可以用 brew-cask 安装。

brew-cask 是社区驱动的,如果你发现 brew-cask 上的应用不是最新版本,或者缺少你某个应用,你可以自己提交 pull request。

安装:

brew install caskroom/cask/brew-cask

应用也可以通过 App Store 安装,而且有些应用只能通过 App Store 安装,比如 Xcode 等一些 Apple 的应用。App Store 没有对应的命令行工具,还需要 Apple ID。倒是更新起来很方便。

几乎所有常用的应用都可以通过 brew-cask 安装,而且是从应用的官网上下载,所以你要安装新的应用时,建议用 brew-cask 安装。如果你不知道应用在 brew-cask 中的 ID,可以先用brew cask search命令搜索。

iTerm2

iTerm2 是最常用的终端应用,是 Terminal 应用的替代品。提供了诸如Split Panes一群实用特性。它默认的黑色背景让我毫不犹豫的抛弃了 Terminal。

安装:

brew cask install iterm2

感谢 brew-cask,我们可以通过命令行自动安装 iTerm2 了。

在终端里,除了可以用⌃E等快捷键(详见其他快捷键)之外,还可以使用⌥B⌥F等快捷键(具体可以参考这里)。前提是这样设置一下:

选择Iterm菜单 > Preferences > Profiles,选择你在使用的 Profile(默认是Default),在Keys标签页中把Left option (⌥) key acts asRight option (⌥) key acts as都设置成+ESC

在打开新的窗口/标签页的时候,默认情况下新窗口总是 HOME 目录,还需要我每次敲命令才能进入工作目录。如果想要这个新窗口在打开的时候就自动进入工作目录,需要如下设置:

选择Iterm菜单 > Preferences > Profiles,选择你在使用的 Profile(默认是Default),在General标签页中的Working Directory部分中选择Reuse previous seesion's directory

至此,Terminal 应用已经出色的完成了其历史使命。后面命令行就交给 iTerm2 啦。

在 iTerm2 中双击会自动选中对应的词,三击会选中对应的整行。选中的内容会自动进入剪贴板,不需要再按⌘C复制。

Oh My Zsh

默认的 Bash 是黑白的,没有色彩。而 Oh My Zsh 可以带你进入彩色时代。Oh My Zsh 同时提供一套插件和工具,可以简化命令行操作。后面我们会看到很多介绍,你会看到我爱死这家伙了。

安装:

sh -c "$(curl -fsSL https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"

目前我使用的插件有:git z sublime history rbenv bundler rake

Oh My Zsh 使用了 Z shell(zsh),一个和 Bash 相似的 Shell,而非 Bash。

在 Z shell 中,~/.zshrc是最重要的配置文件。Oh My Zsh 在安装的时候会把当前环境的$PATH写入~/.zshrc中。这并不是我期望的行为,因为使用了 brew,我们基本不再需要去定制$PATH,而 Oh My Zsh 提供的默认$PATH$HOME/bin:/usr/local/bin:$PATH是非常合适的一个值,它把$HOME/bin加入了$PATH,可以让我们把自己用的脚本放到$HOME/bin下。

所以建议把~/.zshrc重置:

cp ~/.oh-my-zsh/templates/zshrc.zsh-template ~/.zshrc

Oh My Zsh 还有很多有价值的插件

替代品有 Oh My Fish,使用了 Fishshell 作为基础。

Git 常用别名

几乎每个人都会使用一些方法比如 Git 别名来提高效率,几乎所有人都会把使用git st来代替git status。然而这需要手动设置,每个人也都不完全一样。

Oh My Zsh 提供了一套系统别名(alias),来达到相同的功能。比如gst作为git status的别名。而且 Git 插件是 Oh My Zsh 默认启用的,相当于你使用了 Oh My Zsh,你就拥有了一套高效率的别名,而且还是全球通用的。是不是棒棒哒?下面是一些我常用的别名:

Alias Command
gapa git add --patch
gc! git commit -v --amend
gcl git clone --recursive
gclean git reset --hard && git clean -dfx
gcm git checkout master
gcmsg git commit -m
gco git checkout
gd git diff
gdca git diff --cached
glola git log --graph --pretty = format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --all
gp git push
grbc git rebase --continue
gst git status
gup git pull --rebase
gwip git add -A; git rm $(git ls-files --deleted) 2> /dev/null; git commit -m "--wip--"

完整列表请参考:https://github.com/robbyrussell/oh-my-zsh/wiki/Plugin:git

Scroll Reverser

当你在浏览一个很长的网页时,你看完了当前显示的内容,想要看后续的内容,你可以在 Trackpad 上双指上滑,或者鼠标滚轮向上滚动。这是被称作“自然”的滚动方向。

然而在 Windows 里鼠标滚动的行为是相反的:鼠标滚轮向下滚动才会让浏览器显示后续的内容,向上滚动会达到页面的顶部。你可以在 OS X 的系统偏好设置里修改(选择System Preferences > Trackpad,在Scroll & Zoom标签页中不选中Scroll direction: natural),但是这样会同时改变鼠标滚轮的方向和 Trackpad 的方向。

要想只改变鼠标滚轮的方向,而保持 Trackpad 依旧是“自然”的,我们需要 Scroll Reverser:

brew cask install scroll-reverser

PS:这货会让三指点击失效

ShiftIt

原生 OS X 下只能手动调整窗口大小,所以我们需要窗口管理工具。我用过很多窗口管理工具,可惜大部分工具都存在快捷键冲突的问题(对我来说主要是 IntelliJ IDEA)。ShiftIt 是少见的没有冲突的窗口管理工具:

brew cask install shiftit

PS:ShiftIt的旧版本需要安装 X11,最新版本已经修正了这个问题。

替代者有 SizeUp,主要快捷键和 ShiftIt 相同。

当然如果喜欢 hacking,Slate 是个不错的 hackable 的窗口管理工具。配置可以参照http://thume.ca/howto/2012/11/19/using-slate/

Sublime Text 2

安装:

brew cask install sublime-text

在命令行中指定使用 Sublime Text 打开某文件,是一个非常常用的功能,一般我们会按照 OS X Command Line 中所说执行 ln -s "/Applications/Sublime Text 2.app/Contents/SharedSupport/bin/subl" ~/bin/subl 来增加subl链接。但是如果你用 brew-cask 安装的话,恭喜你,你不需要运行这个命令,因为 brew-cask 自动帮你做了这件事情。而且你卸载 Sublime Text 的时候 brew-cask 会自动删掉这个链接。

同时 Oh My Zsh 也提供了 Sublime Text 插件,叫做sublime。参考:https://github.com/robbyrussell/oh-my-zsh/tree/master/plugins/sublime,这个插件和通过 brew-cask 安装的 Sublime Text 完美兼容。

替代品有 Atom、TextMate、Sublime Text 3 等,跟 Sublime Text 2 一样,用 brew-cask 安装的话命令行工具会被自动加入$PATH

MacDown

MacDown 是 Markdown 编辑器。由于 Mou 一直不支持代码高亮,我就转向了 MacDown。完美支持GFM

我特别喜欢 Markdown,我用 Makdown 来写文章(包括本文),写幻灯片(reveal.js)。Markdown 可以让我专注于内容本身,而无需花精力在排版和样式上。

安装:

brew cask install macdown

z

在打开终端后,你是怎么进入项目的工作目录?是cd xxx⌃R还是用别名?

z 工具可以帮你快速进入目录。比如在我的 Mac 上运行z cask就会进入/usr/local/Library/Taps/caskroom/homebrew-cask/Casks目录。

这货的安装非常方便,甚至都不需要下载任何东西,因为它已经整合在了 Oh My Zsh 中。编辑~/.zshrc文件,在plugins=(git)这行中加上z变成plugins=(git z),然后运行source ~/.zshrc重新加载配置文件,就可以使用 z 了。

替代品有 autojump。autojump 需要使用 brew 安装。

Vimium

Vimium 是一个 Google Chrome 扩展,让你可以纯键盘操作 Chrome,把你的 Chrome 变成“黑客的浏览器”。

安装方法请参考官方网站。

其他浏览器也有类似的工具,比如 FireFox 的 KeySnail

LastPass

LastPass 是管理密码的工具,支持二次验证,提供所有浏览器插件以及 Mac 桌面版本。

最重要的是,它提供 命令行 的版本,可以直接通过 brew 安装

brew install lastpass-cli --with-pinentry

之后,只需要登陆:

lpass login you@email.com

就可以拷贝密码或者集成到其他命令中了:

lpass show --password gmail.com -c

SourceTree

SourceTree 是 Atlassian 公司出品的一款优秀的 Git 图形化客户端。如果你发现命令行无法满足你的要求,可以试试 SourceTree。

安装:

brew cask install sourcetree

用 brew-cask 安装会自动增加命令行工具stree$PATH里。在命令行中输入stree可以快速用 SourceTree 打开当前 Git 仓库。详细用法请参见stree --help

CheatSheet

CheatSheet 能够显示当前程序的快捷键列表,默认的快捷键是长按

CheatSheet

安装:

brew cask install cheatsheet

3. 开发工具

Java

现在 OS X 都不会自带 JDK 了,所以进行 Java 开发的话,需要下载 JDK。在 brew-cask 之前,我们需要从 https://developer.apple.com/downloads/ 或者 Oracle 网站上下载。还有更麻烦的--卸载 JDK 和升级 JDK。

JDK 安装文件是 pkg 格式,卸载和.app不一样,且没有自动卸载方式。

而 brew-cask 提供了自动安装和卸载功能,能够自动从官网上下载并安装 JDK 8。

brew cask install java

如果你需要安装 JDK 7 或者 JDK 6,可以使用homebrew-cask-versions

brew tap caskroom/versions
brew cask install java6

在 OS X 上,你可以同时安装多个版本的 JDK。你可以通过命令/usr/libexec/java_home -V来查看安装了哪几个 JDK。

那问题来了,当你运行java或者 Java 程序时使用的是哪个 JDK 呢?在 OS X 下,java也就是/usr/bin/java在默认情况下指向的是已经安装的最新版本。但是你可以设置环境变量JAVA_HOME来更改其指向:

$ java -version
java version "1.8.0_60"
Java(TM) SE Runtime Environment (build 1.8.0_60-b27)
Java HotSpot(TM) 64-Bit Server VM (build 25.60-b23, mixed mode)
$ JAVA_HOME=/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home java -version
java version "1.6.0_65"
Java(TM) SE Runtime Environment (build 1.6.0_65-b14-466.1-11M4716)
Java HotSpot(TM) 64-Bit Server VM (build 20.65-b04-466.1, mixed mode)

其中JAVA_HOME=/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home可以用JAVA_HOME=`/usr/libexec/java_home -v 1.6`这种更加通用的方式代替。

jEnv

也可以使用 jEnv 来管理不同版本的 JDK,这个工具跟 rbenv 类似,通过当前目录下的.java-version来决定使用哪个 JDK。jEnv 也可以用 brew 安装。不过要使用 jEnv 要有几个问题:

  • 需要手动把eval "$(jenv init -)"加入 profile,没有 Oh My Zsh 插件。这点是我非常反感的。

    可以把eval "$(jenv init -)"加入~/.zlogin,这样可以避免修改~/.zshrc

  • 需要手动添加 JDK,不会自动采集系统 JDK。跟 Ruby 不同,OS X 已经提供/usr/libexec/java_home工具来管理安装的 JDK。
  • 需要 jenv rehash。这个是跟 rbenv 学的。

所以我建议不要使用 jEnv。

Java[OCD]

作为一个强迫症患者,每当我看到 Java 的错误写法就想纠正过来。

当指编程语言时,Java 的正确写法是首字母大写,其余小写。其他写法比如JAVAjava都是不对的。

在其他一些地方会使用小写的java

  • java命令
  • 原文件Main.java
  • 包名java.lang

只有在全大写的标题里使用JAVA或者环境变量JAVA_HOME

IntelliJ IDEA

Java 开发必备工具 IntelliJ IDEA。可以安装 Ultimate Edition:

brew cask install intellij-idea

也可以安装开源免费的 Community Edition:

brew cask install intellij-idea-ce

IntelliJ IDEA 有几套内建的快捷键方案(Keymap)。其中适用于 OS X 的有Mac OS XMac OS X 10.5+两种。区别是:

  • Mac OS X方案和其他平台上的快捷键类似,
  • Mac OS X 10.5+更加符合 OS X 常用的快捷键。

一个团队使用不同的快捷键会严重影响效率。可以用View | Quick Switch Scheme⌃ Back Quote)快速切换 Keymap。

如果可以选择的话,我建议使用Mac OS X方案。因为我经常遇到使用 Windows 的客户,而 Windows 平台上的快捷键和Mac OS X方案类似。

可以从 IDEA 的Help > Default Keymap Reference打开快捷键的参考手册。不过从这里打开的是Mac OS X 10.5+方案的,而Mac OS X方案的可以从这里找到:http://www.basrikahveci.com/static/ij_keymap_mac.pdf

rbenv

人人都需要一个 Ruby 版本管理工具。rbenv 就是这样一个轻量级工具,它可以通过 brew 安装。

安装:

brew install rbenv ruby-build

然后在~/.zshrc中加上rbenv插件。否则你需要手动添加eval "$(rbenv init -)"~/zshrc或者~/.zprofile文件里。

有时候项目会依赖一些奇怪的版本号,比如ruby-2.1.0,这个时候你需要 rbenv-aliases 帮忙:

brew install rbenv-aliases

替代品有 RVM、chruby。因为 RVM 不能通过 brew 安装,并且安装的时候会没有节操的修改一堆文件,所以被我早早的弃用了。chruby 也是一个轻量级工具,而且可以完美的和 Oh My Zsh 集成在一起,我看到有些生产环境在用它。

Ruby 常用别名

几乎所有 Ruby 开发人员都会把bi作为bundle install的别名。Oh My Zsh 提供builder插件,这个插件提供了一套别名,比如bibe。同时还能让你在运行一些常用 gem 的时候直接输入rspec,不需要be rspec这样了。具体包括哪些命令请参考这里

Z shell 对于[]符号有特殊的处理,所以在运行rake task[parameter]的时候会报错,你需要改成rake task\[parameter\]或者noglob rake task[parameter]。然而 Oh My Zsh 已经看穿这一切,自带的 rake 插件已经解决了这个问题:brake task[parameter]

添加插件的时候注意把rake放到bundler后面,例如这样:

plugins=(git z sublime history rbenv bundler rake)

参考资料

redis info命令详解

以一种易于解释(parse)且易于阅读的格式,返回关于 Redis 服务器的各种信息和统计数值。

通过给定可选的参数 section ,可以让命令只返回某一部分的信息:

  • server : 一般 Redis 服务器信息,包含以下域:

    • redis_version : Redis 服务器版本
    • redis_git_sha1 : Git SHA1
    • redis_git_dirty : Git dirty flag
    • os : Redis 服务器的宿主操作系统
    • arch_bits : 架构(32 或 64 位)
    • multiplexing_api : Redis 所使用的事件处理机制
    • gcc_version : 编译 Redis 时所使用的 GCC 版本
    • process_id : 服务器进程的 PID
    • run_id : Redis 服务器的随机标识符(用于 Sentinel 和集群)
    • tcp_port : TCP/IP 监听端口
    • uptime_in_seconds : 自 Redis 服务器启动以来,经过的秒数
    • uptime_in_days : 自 Redis 服务器启动以来,经过的天数
    • lru_clock : 以分钟为单位进行自增的时钟,用于 LRU 管理
  • clients : 已连接客户端信息,包含以下域:

    • connected_clients : 已连接客户端的数量(不包括通过从属服务器连接的客户端)
    • client_longest_output_list : 当前连接的客户端当中,最长的输出列表
    • client_longest_input_buf : 当前连接的客户端当中,最大输入缓存
    • blocked_clients : 正在等待阻塞命令(BLPOP、BRPOP、BRPOPLPUSH)的客户端的数量
  • memory : 内存信息,包含以下域:

    • used_memory : 由 Redis 分配器分配的内存总量,以字节(byte)为单位
    • used_memory_human : 以人类可读的格式返回 Redis 分配的内存总量
    • used_memory_rss : 从操作系统的角度,返回 Redis 已分配的内存总量(俗称常驻集大小)。这个值和 topps 等命令的输出一致。
    • used_memory_peak : Redis 的内存消耗峰值(以字节为单位)
    • used_memory_peak_human : 以人类可读的格式返回 Redis 的内存消耗峰值
    • used_memory_lua : Lua 引擎所使用的内存大小(以字节为单位)
    • mem_fragmentation_ratio : used_memory_rssused_memory 之间的比率
    • mem_allocator : 在编译时指定的, Redis 所使用的内存分配器。可以是 libc 、 jemalloc 或者 tcmalloc 。
    在理想情况下, used_memory_rss 的值应该只比 used_memory 稍微高一点儿。
    rss > used ,且两者的值相差较大时,表示存在(内部或外部的)内存碎片。
    内存碎片的比率可以通过 mem_fragmentation_ratio 的值看出。
    used > rss 时,表示 Redis 的部分内存被操作系统换出到交换空间了,在这种情况下,操作可能会产生明显的延迟。

    Because Redis does not have control over how its allocations are mapped to memory pages, high used_memory_rss is often the result of a spike in memory usage.

    当 Redis 释放内存时,分配器可能会,也可能不会,将内存返还给操作系统。
    如果 Redis 释放了内存,却没有将内存返还给操作系统,那么 used_memory 的值可能和操作系统显示的 Redis 内存占用并不一致。
    查看 used_memory_peak 的值可以验证这种情况是否发生。
  • persistence : RDBAOF 的相关信息

  • stats : 一般统计信息

  • replication : 主/从复制信息

  • cpu : CPU 计算量统计信息

  • commandstats : Redis 命令统计信息

  • cluster : Redis 集群信息

  • keyspace : 数据库相关的统计信息

除上面给出的这些值以外,参数还可以是下面这两个:

  • all : 返回所有信息
  • default : 返回默认选择的信息

当不带参数直接调用 INFO 命令时,使用 default 作为默认参数。

不同版本的 Redis 可能对返回的一些域进行了增加或删减。

因此,一个健壮的客户端程序在对 INFO 命令的输出进行分析时,应该能够跳过不认识的域,并且妥善地处理丢失不见的域。

可用版本:
>= 1.0.0
时间复杂度:
O(1)
返回值:
具体请参见下面的测试代码。
redis> INFO
# Server
redis_version:2.5.9
redis_git_sha1:473f3090
redis_git_dirty:0
os:Linux 3.3.7-1-ARCH i686
arch_bits:32
multiplexing_api:epoll
gcc_version:4.7.0
process_id:8104
run_id:bc9e20c6f0aac67d0d396ab950940ae4d1479ad1
tcp_port:6379
uptime_in_seconds:7
uptime_in_days:0
lru_clock:1680564

# Clients
connected_clients:1
client_longest_output_list:0
client_biggest_input_buf:0
blocked_clients:0

# Memory
used_memory:439304
used_memory_human:429.01K
used_memory_rss:13897728
used_memory_peak:401776
used_memory_peak_human:392.36K
used_memory_lua:20480
mem_fragmentation_ratio:31.64
mem_allocator:jemalloc-3.0.0

# Persistence
loading:0
rdb_changes_since_last_save:0
rdb_bgsave_in_progress:0
rdb_last_save_time:1338011402
rdb_last_bgsave_status:ok
rdb_last_bgsave_time_sec:-1
rdb_current_bgsave_time_sec:-1
aof_enabled:0
aof_rewrite_in_progress:0
aof_rewrite_scheduled:0
aof_last_rewrite_time_sec:-1
aof_current_rewrite_time_sec:-1

# Stats
total_connections_received:1
total_commands_processed:0
instantaneous_ops_per_sec:0
rejected_connections:0
expired_keys:0
evicted_keys:0
keyspace_hits:0
keyspace_misses:0
pubsub_channels:0
pubsub_patterns:0
latest_fork_usec:0

# Replication
role:master
connected_slaves:0

# CPU
used_cpu_sys:0.03
used_cpu_user:0.01
used_cpu_sys_children:0.00
used_cpu_user_children:0.00

Win10开发必备:Visual Studio 2015正式版下载

7月21日凌晨消息,面向大众用户的Visual Studio 2015集成开发工具正式版免费试用版已经推出。本文帮大家汇总一下简体中文社区版、专业版以及企业版在线安装版以及ISO离线安装镜像下载地址。

Visual Studio Community 2015简体中文版(社区版,针对个人免费):

在线安装 || ISO镜像

镜像SHA1:1044F9F4E0EA1304AFECF6780BF599F1DA248DF8

Visual Studio Professional 2015简体中文版(专业版):

在线安装 || ISO镜像

镜像SHA1:629E7154E2695F08A3C692C0B3F6CE19DF6D3A72

Visual Studio Enterprise 2015简体中文版(企业版):

在线安装 || ISO镜像

镜像SHA1:4FFA1EE3E2D3337D3EDAE550A3583ABE9C426BEF

更多版本点此下载

高效运维最佳实践(03):Redis集群技术及Codis实践

专栏介绍

“高效运维最佳实践”是InfoQ在2015年推出的精品专栏,由触控科技运维总监萧田国撰写,InfoQ总编辑崔康策划。

前言

诚如开篇文章所言,高效运维包括管理的专业化和技术的专业化。前两篇我们主要在说些管理相关的内容,本篇说一下技术专业化。希望读者朋友们能适应这个转换,谢谢。

互联网早在几年前就已进入Web 2.0时代,对后台支撑能力的要求,提高了几十倍甚至几百倍。在这个演化过程中,缓存系统扮演了举足轻重的角色。

运维进化到今天,已经不是重复造轮子的时代。所以,我们在架构优化和自动化运维中,可以尽可能地选用优秀的开源产品,而不是自己完全从头再来(各种技术geek除外)。

本文主要讨论Redis集群相关技术及新发展,关于Redis运维等内容,以后另开主题讨论。

本文重点推荐Codis——豌豆荚开源的Redis分布式中间件(该项目于4个月前在GitHub开源,目前star已超过2100)。其和Twemproxy相比,有诸多激动人心的新特性,并支持从Twemproxy无缝迁移至Codis。

本文主要目录如下,对Redis比较了解的朋友,可跳过前两部分,直接欣赏Codis相关内容。

1. Redis常见集群技术
1.1 客户端分片
1.2 代理分片
1.3 Redis Cluster
2. Twemproxy及不足之处
3. Codis实践
3.1 体系架构
3.2 性能对比测试
3.3 使用技巧、注意事项

好吧我们正式开始。

1. Redis常见集群技术

长期以来,Redis本身仅支持单实例,内存一般最多10~20GB。这无法支撑大型线上业务系统的需求。而且也造成资源的利用率过低——毕竟现在服务器内存动辄100~200GB。

为解决单机承载能力不足的问题,各大互联网企业纷纷出手,“自助式”地实现了集群机制。在这些非官方集群解决方案中,物理上把数据“分片”(sharding)存储在多个Redis实例,一般情况下,每一“片”是一个Redis实例。

包括官方近期推出的Redis Cluster,Redis集群有三种实现机制,分别介绍如下,希望对大家选型有所帮助。

1.1 客户端分片

这种方案将分片工作放在业务程序端,程序代码根据预先设置的路由规则,直接对多个Redis实例进行分布式访问。这样的好处是,不依赖于第三方分布式中间件,实现方法和代码都自己掌控,可随时调整,不用担心踩到坑。

这实际上是一种静态分片技术。Redis实例的增减,都得手工调整分片程序。基于此分片机制的开源产品,现在仍不多见。

这种分片机制的性能比代理式更好(少了一个中间分发环节)。但缺点是升级麻烦,对研发人员的个人依赖性强——需要有较强的程序开发能力做后盾。如果主力程序员离职,可能新的负责人,会选择重写一遍。

所以,这种方式下,可运维性较差。出现故障,定位和解决都得研发和运维配合着解决,故障时间变长。

这种方案,难以进行标准化运维,不太适合中小公司(除非有足够的DevOPS)。

1.2 代理分片

这种方案,将分片工作交给专门的代理程序来做。代理程序接收到来自业务程序的数据请求,根据路由规则,将这些请求分发给正确的Redis实例并返回给业务程序。

这种机制下,一般会选用第三方代理程序(而不是自己研发),因为后端有多个Redis实例,所以这类程序又称为分布式中间件。

这样的好处是,业务程序不用关心后端Redis实例,运维起来也方便。虽然会因此带来些性能损耗,但对于Redis这种内存读写型应用,相对而言是能容忍的。

这是我们推荐的集群实现方案。像基于该机制的开源产品Twemproxy,便是其中代表之一,应用非常广泛。

1.3 Redis Cluster

在这种机制下,没有中心节点(和代理模式的重要不同之处)。所以,一切开心和不开心的事情,都将基于此而展开。

Redis Cluster将所有Key映射到16384个Slot中,集群中每个Redis实例负责一部分,业务程序通过集成的Redis Cluster客户端进行操作。客户端可以向任一实例发出请求,如果所需数据不在该实例中,则该实例引导客户端自动去对应实例读写数据。

Redis Cluster的成员管理(节点名称、IP、端口、状态、角色)等,都通过节点之间两两通讯,定期交换并更新。

由此可见,这是一种非常“重”的方案。已经不是Redis单实例的“简单、可依赖”了。可能这也是延期多年之后,才近期发布的原因之一。

这令人想起一段历史。因为Memcache不支持持久化,所以有人写了一个Membase,后来改名叫Couchbase,说是支持Auto Rebalance,好几年了,至今都没多少家公司在使用。

这是个令人忧心忡忡的方案。为解决仲裁等集群管理的问题,Oracle RAC还会使用存储设备的一块空间。而Redis Cluster,是一种完全的去中心化……

本方案目前不推荐使用,从了解的情况来看,线上业务的实际应用也并不多见。

2. Twemproxy及不足之处

Twemproxy是一种代理分片机制,由Twitter开源。Twemproxy作为代理,可接受来自多个程序的访问,按照路由规则,转发给后台的各个Redis服务器,再原路返回。

这个方案顺理成章地解决了单个Redis实例承载能力的问题。当然,Twemproxy本身也是单点,需要用Keepalived做高可用方案。

我想很多人都应该感谢Twemproxy,这么些年来,应用范围最广、稳定性最高、最久经考验的分布式中间件,应该就是它了。只是,他还有诸多不方便之处。

Twemproxy最大的痛点在于,无法平滑地扩容/缩容。

这样导致运维同学非常痛苦:业务量突增,需增加Redis服务器;业务量萎缩,需要减少Redis服务器。但对Twemproxy而言,基本上都很难操作(那是一种锥心的、纠结的痛……)。

或者说,Twemproxy更加像服务器端静态sharding。有时为了规避业务量突增导致的扩容需求,甚至被迫新开一个基于Twemproxy的Redis集群。

Twemproxy另一个痛点是,运维不友好,甚至没有控制面板。

Codis刚好击中Twemproxy的这两大痛点,并且提供诸多其他令人激赏的特性。

3. Codis实践

Codis由豌豆荚于2014年11月开源,基于Go和C开发,是近期涌现的、国人开发的优秀开源软件之一。现已广泛用于豌豆荚的各种Redis业务场景(已得到豌豆荚@刘奇同学的确认,呵呵)。

从3个月的各种压力测试来看,稳定性符合高效运维的要求。性能更是改善很多,最初比Twemproxy慢20%;现在比Twemproxy快近100%(条件:多实例,一般Value长度)。

3.1 体系架构

Codis引入了Group的概念,每个Group包括1个Redis Master及至少1个Redis Slave,这是和Twemproxy的区别之一。这样做的好处是,如果当前Master有问题,则运维人员可通过Dashboard“自助式”切换到Slave,而不需要小心翼翼地修改程序配置文件。

为支持数据热迁移(Auto Rebalance),出品方修改了Redis Server源码,并称之为Codis Server。

Codis采用预先分片(Pre-Sharding)机制,事先规定好了,分成1024个slots(也就是说,最多能支持后端1024个Codis Server),这些路由信息保存在ZooKeeper中。

ZooKeeper还维护Codis Server Group信息,并提供分布式锁等服务。

3.2 性能对比测试

Codis目前仍被精益求精地改进中。其性能,从最初的比Twemproxy慢20%(虽然这对于内存型应用而言,并不明显),到现在远远超过Twemproxy性能(一定条件下)。

我们进行了长达3个月的测试。测试基于redis-benchmark,分别针对Codis和Twemproxy,测试Value长度从16B~10MB时的性能和稳定性,并进行多轮测试。

一共有4台物理服务器参与测试,其中一台分别部署codis和twemproxy,另外三台分别部署codis server和redis server,以形成两个集群。

从测试结果来看,就Set操作而言,在Value长度<888B时,Codis性能优越优于Twemproxy(这在一般业务的Value长度范围之内)。

就Get操作而言,Codis性能一直优于Twemproxy。

3.3 使用技巧、注意事项

Codis还有很多好玩的东东,从实际使用来看,有些地方也值得注意。

1)无缝迁移Twemproxy

出品方贴心地准备了Codis-port工具。通过它,可以实时地同步 Twemproxy 底下的 Redis 数据到你的 Codis 集群。同步完成后,只需修改一下程序配置文件,将 Twemproxy 的地址改成 Codis 的地址即可。是的,只需要做这么多。

2)支持Java程序的HA

Codis提供一个Java客户端,并称之为Jodis(名字很酷,是吧?)。这样,如果单个Codis Proxy宕掉,Jodis自动发现,并自动规避之,使得业务不受影响(真的很酷!)。

3)支持Pipeline

Pipeline使得客户端可以发出一批请求,并一次性获得这批请求的返回结果。这提升了Codis的想象空间。

从实际测试来看,在Value长度小于888B字节时,Set性能迅猛提升;

Get性能亦复如是。

4)Codis不负责主从同步

也就是说, Codis仅负责维护当前Redis Server列表,由运维人员自己去保证主从数据的一致性。

这是我最赞赏的地方之一。这样的好处是,没把Codis搞得那么重。也是我们敢于放手在线上环境中上线的原因之一。

5)对Codis的后续期待?

好吧,粗浅地说两个。希望Codis不要变得太重。另外,加pipeline参数后,Value长度如果较大,性能反而比Twemproxy要低一些,希望能有改善(我们多轮压测结果都如此)。

因篇幅有限,源码分析不在此展开。另外Codis源码、体系结构及FAQ,参见如下链接:https://github.com/wandoulabs/codis

PS:线上文档的可读性,也是相当值得称赞的地方。一句话:很走心,赞!

最后,Redis初学者请参考这个链接:http://www.gamecbg.com/bc/db/redis/13852.html,文字浅显易懂,而且比较全面。

本文得到Codis开发团队刘奇和黄东旭同学的大力协助,并得到Tim Yang老师等朋友们在内容把控方面的指导。本文共同作者为赵文华同学,他主要负责Codis及Twemproxy的对比测试。在此一并谢过。

关于作者

萧田国,男,硕士毕业于北京科技大学,ACMUG核心成员,目前为触控科技运维负责人。拥有十多年运维及团队管理经验。先后就职于联想集团(Oracle数据库主管)、搜狐畅游(数据库主管)、智明星通及世纪互联等。从1999年开始,折腾各种数据库如Oracle/MySQL/MS SQL Server/NoSQL等,兼任数据库培训讲师若干年。

曾经的云计算行业从业者,现在喜欢琢磨云计算及评测、云端数据库,及新技术在运维中的应用。主张管理学科和运维体系的融合、人性化运维管理,打造高效、专业运维团队。

近来有时参加一些大小技术会议,做做演讲嘉宾或主持人(有空找我来玩呀:)我的个人微信号:xiaotianguo。如需更多了解,请百度“萧田国”。

另外,我也有微信公众号叻,微信搜索“开心南瓜by萧田国”或扫描如下二维码,和我进行微信互动。公众号里将有我原创的各类技术和非技术文章,及我所喜欢的文章/帖子。一起来吧~


感谢崔康对本文的策划,丁晓昀对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ@丁晓昀),微信(微信号:InfoQChina)关注我们,并与我们的编辑和其他读者朋友交流。

谈谈REST与ASP.NET Web API

本节目录

 

Web API简介

REST

REST是“REpresentational State Transfer”的缩写,可以翻译成“表现状态转换”.

REST是一种软件架构风格,与技术无关,但是大部分基于REST风格的Web服务都是基于HTTP的

(虽然WCF在3.5以后支持REST,但是WCF太庞大了,Web API更适合做REST架构)

 

SOAP与REST

SOAP Web API采用RPC(面向方法Remote Procedure Call)风格,它采用面向功能的架构,所以在设计之初首先需要考虑的是提供怎样的功能。

RESTful Web API采用ROA(面向资源Resouce Oriented Architecture)架构,所以在设计之初首先需要考虑的是有哪些资源可供操作。

 

HTTP协议

HTTP采用简单的请求/响应模式的消息交换旨在实现针对某个Web资源的某种操作。

至于针对资源的操作类型,不外乎CRUD(Create、Retrieve、Update和Delete)而已。

一个HTTP请求除了利用URI标志目标资源之外,还需要通过HTTP方法指名针对资源的操作类型。

HTTP方法:包括GET(查)、POST(增)、PUT(改)、DELETE(删)、HEAD、OPTIONS、TRACE、CONNECTION和PATCH等

 

HTTP协议

1
2
3
4
5
6
7
8
9
GET http://neverc.cn/ HTTP/1.1
Host: neverc.cn
Connection: keep-alive
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.15 Safari/537.36
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8
Cookie:

第1行是HTTP的3个基本属性,method,uri,vesion

其他都是HTTP的请求报头header,http定义很多原生的header,也可以添加自定义header(实际就是键值对)

除了报头,一个HTTP请求还可以包括一个请求主体内容,可以是任意格式.

 

与HTTP请求一样,HTTP响应也是由报头和报文2部分组成.

1
2
3
4
5
6
7
8
9
10
11
12
HTTP/1.1 200 OK
Cache-Control: private
Content-Type: text/html; charset=utf-8
Vary: Accept-Encoding
Server: Microsoft-IIS/7.5
X-AspNetMvc-Version: 5.0
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Fri, 18 Sep 2015 05:39:50 GMT
Content-Length: 12003
<!DOCTYPE html>

第1行是vesion和statu(除了200 OK外,常见的有401 Not Authorized、404 Not Found)

第3行Content-Type表示媒体(或者叫资源/数据)类型.

  • text/html:HTML格式的文档。
  • text/xml(application/xml):XML格式的文本。
  • text/json(application/json): JSON格式的文本。
  • image/gif(image/jpeg、image/png):GIF(JPEG、PNG)格式的图片。
  • audio/mp4(audio/mpeg、audio/vnd.wave):MP4(MPEG、WAVE)格式的音频文件。
  • video/mp4(video/mpeg、video/quicktime):MP4(MPEG、QUICKTIME)格式的视频文件。

 

自我寄宿

建立项目

  • Model:一个类库项目,定义实体
  • WebApi:一个类库项目,定义API控制器(引用Model项目)
  • SelfHost:一个控制台项目,寄宿API服务(引用WebApi项目)

涉及到的引用的程序集

 

创建实体

在Model项目中,新建一个Contact类

1
2
3
4
5
public class Contact
{
    public string Id { get; set; }
    public string Name { get; set; }
}

 

创建控制器

在WebApi项目中,引用System.Web.Http类库并创建API控制器

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
public class ContactsController : ApiController
{
    #region Data
    static readonly List<Contact> contacts;
    static int counter = 2;
    static ContactsController()
    {
        contacts = new List<Contact>
        {
            new Contact{Id = "001",Name = "张三"},
            new Contact{Id = "002",Name = "李四"}
        };
    }
    #endregion
    public IEnumerable<Contact> Get(string id = null)
    {
        return from contact in contacts
               where contact.Id == id || string.IsNullOrEmpty(id)
               select contact;
    }
    public void Post(Contact contact)
    {
        //多线程并发处理
        Interlocked.Increment(ref counter);
        contact.Id = counter.ToString("D3");
        contacts.Add(contact);
    }
    public void Put(Contact contact)
    {
        contacts.Remove(contacts.First(c => c.Id == contact.Id));
        contacts.Add(contact);
    }
    public void Delete(string id)
    {
        contacts.Remove(contacts.First(c => c.Id == id));
    }
}

 

自我寄宿

在SelfHost中,引用System.Web.Http、System.Net.Http、System.Web.Http.SelfHost类库并实现寄宿

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void Main(string[] args)
{
    //对于SelfHost来说,HttpController类型的解析在默认情况下只会针对加载到当前应用程序域中的程序集列表
    //通过手工加载,让该程序集加载到当前应用程序域中。
    Assembly.Load("WebApi, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null");
    var configuration = new HttpSelfHostConfiguration("http://localhost/selfhost");
    using (var httpServer = new HttpSelfHostServer(configuration))
    {
        httpServer.Configuration.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional });
        httpServer.OpenAsync().Wait();
        Console.WriteLine("寄宿Web API服务成功");
        Console.Read();
    }
}

 

测试

运行SelfHost控制台,浏览器访问http://localhost/selfhost。

注意:(由于此处会注册http.sys,所以需要管理员身份运行VS)

 

 

IIS寄宿

使用IIS寄宿非常简单,只要注册好路由数据即可

 

建立项目

  • WebHost:一个空的Web项目(引用Model和WebApi项目)

 

注册路由

新建Global文件,注册HttpRoute

1
2
3
4
5
6
7
8
9
10
public class Global : HttpApplication
{
    protected void Application_Start(object sender, EventArgs e)
    {
        GlobalConfiguration.Configuration.Routes.MapHttpRoute(
          name: "DefaultApi",
          routeTemplate: "api/{controller}/{id}",
          defaults: new { id = RouteParameter.Optional });
    }
}

 

测试

运行WebHost项目,浏览器访问http://~/api/Contacts。

 

调用Web API

因为Web API是基于HTTP的,所以对于开发人员,就像普通请求网站数据一样

  • jQuery
  • MVVM/MVC框架的JS,AngularJS,Knockout.js
  • 后台可以使用HttpClient、WebClient、HttpWebRequest等

 

这里演示一个HttpClient完整的例子,对于异步有疑问,可阅读我的博客:[C#] 谈谈异步编程async await

 

新建一个控制台项目即可,实现Program类:

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
static HttpClient httpClient = new HttpClient();
        static void Main(string[] args)
        {
            //由于HttpClient类中的方法大部分为异步
            //Main方法不支持Async关键字
            //故新建一个方法,使其同步运行
            Process();
            Console.Read();
        }
        async static void Process()
        {
            //获取当前联系人列表
            ListContacts();
            //添加新的联系人
            var contact = new Contact { Name = "王五" };
            await httpClient.PostAsJsonAsync("http://localhost/selfhost/api/contacts", contact);
            Console.WriteLine("添加新联系人“王五”:");
            ListContacts();
            //修改现有的某个联系人
            var response = await httpClient.GetAsync("http://localhost/selfhost/api/contacts/001");
            contact = (await response.Content.ReadAsAsync<IEnumerable<Contact>>()).First();
            contact.Name = "赵六";
            await httpClient.PutAsJsonAsync("http://localhost/selfhost/api/contacts/001", contact);
            Console.WriteLine("修改联系人“001”信息:");
            ListContacts();
            //删除现有的某个联系人
            await httpClient.DeleteAsync("http://localhost/selfhost/api/contacts/002");
            Console.WriteLine("删除联系人“002”:");
            ListContacts();
        }
        async static void ListContacts()
        {
            var response = await httpClient.GetAsync("http://localhost/selfhost/api/contacts");
            IEnumerable<Contact> contacts = await response.Content.ReadAsAsync<IEnumerable<Contact>>();
            Console.WriteLine("当前联系人列表:");
            foreach (Contact contact in contacts)
            {
                Console.WriteLine("{0,-6}{1,-6}", contact.Id, contact.Name);
            }
            Console.WriteLine();
        }

 

Web API原理

Web API借用了MVC的设计,以Controller形式定义服务,Action代表具体的操作.

Web API借助于URL路由得到控制器,再根据路由对象,通过http方法找到对应的action.(实际上,如果根据url解析不到action的时候,才会通过http方法)

 

路由注册

1
2
3
4
5
config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

由于在模板中没有定义action,所以只能通过httpmethod来找action.(并且是根据方法前缀匹配即可)

 

通过浏览器api/Contacts查看的时候,会返回一个xml格式的数据.

实际上,webapi是先检查accept,从左到右,去匹配序列化器,如果没有匹配到则使用默认的json序列化器.

 

 

管道式设计

Web API也采用了管道式设计,这是一个不同于MVC的管道.虽然很多地方和MVC相似.

Route对象为HttpRoute

Handle对象为HttpControllerHandler(由于实现了IHttpAsyncHandler接口,所以默认走BeginProcessRequest异步方法)

 

本文地址:http://neverc.cnblogs.com/p/4603935.html

参考:http://www.cnblogs.com/artech/p/how-asp-net-web-api-works.html

Redis系列(五)-Opserver的监控

阅读目录:

  1. 基本介绍
  2. 使用配置
  3. 部署实例
  4. 面板属性

基本介绍

Opserver是Stack Exchange的一个开源监控系统,基于Net、MVC开发,所以Net程序员可以轻松基于它二次开发。它主要监控:

  • servers
  • SQL clusters/instances
  • redis
  • elastic search
  • exception logs
  • haproxy

Opserver提供详细的面板,用来快速展示被监控系统的总体情况。 下面Opserver的监控UI界面示例,非常详细:

使用配置

项目地址:https://github.com/opserver/Opserver

下载后用VS打开或IIS直接部署即可,下面是它的支持监控系统的view目录,结构比较清晰。

安全配置

Opserver系统本身后登陆验证,支持3种安全认证方式:

复制代码
<?xml version="1.0" encoding="utf-8"?>
<SecuritySettings provider="AD">
    <!-- 可选, 下面的网络可以不用验证直接访问 -->
    <InternalNetworks>
        <Network name="SE Internal" cidr="10.0.0.0/8" />
    </InternalNetworks>
</SecuritySettings>

<!-- 
每个人都是管理都可访问
<SecuritySettings provider="alladmin" />
-->
复制代码

如果使用活动目录验证,可以直接在web.config配置ViewGroups、AdminGroups,也可以单独在每个系统监控json配置文件里面添加ViewGroups、AdminGroups:

复制代码
"viewGroups": "*",
"adminGroups": "SysAdmins",
"user": "user",
"password": "pass",
"adminUser": "adminuser",
"adminPassword": "adminpass",
复制代码

监控配置

配置监控的地方在/Config/目录,Stack Exchange提供对应系统的配置示例,如图: 如果没有配置任何系统监控文件,浏览OpServer页面时,会报’No Configuration’的警告提示。 这里以Redis为例,监控配置如下:

复制代码
{
    "allServers": {
        "name": "All",
        "instances": [
              {
                "name": "本地master",
                "port": "6379"
            },
            {
                "name": "本地slave1",
                "port": "6380"
            },
            {
                "name": "本地master2",
                "port": "6382"
            }
        ]
          
    },
    "Servers": [
        { "name": "127.0.0.1" }
    ]
}
复制代码

部署实例

认证配置<SecuritySettings provider=”alladmin”>所有人都是管理员,打开浏览器访问,输入账号admin,密码admin:

可以看到有2组实例,其中6380是slave,6379是master,从图表上可以清晰看到层架关系。

实例列表:

点击单个Redis实例进去看到。

面板属性

面板展示的属性都是可以通过redis info命令获取到,opserver做了更清晰的展示。

Ops(/sec)  每秒处理量

memory(used)即used_memory_rss(used_memory)

used_memory_rss : 从操作系统的角度,返回 Redis 已分配的内存总量(俗称常驻集大小)。这个值和 top 、 ps等命令的输出一致。

used_memory_peak : Redis 的内存消耗峰值(以字节为单位)

used_memory : 由 Redis 分配器分配的内存总量,以字节(byte)为单位

 

Summary是总体概览部分。

Memory是内存使用情况,重要。

persistence 是RDB和AOF的状态。
keyspace key存储的情况,analyze进去可以查看详细分布。
stats  客户端命令的key命中率和处理量
clients 查看有哪个ip(或机器名)过来的连接数多,很方便的定位到那台应用端机器长时间没有释放连接,重要。

slow command log 服务端接受的命令日志。

 

 

Opserver 算是个比较轻量级的监控系统,部署修改都非常方便,比如增加连接数或者内存报警功能。

Redis系列(五)-Opserver的监控

基本介绍

Opserver是Stack Exchange的一个开源监控系统,基于Net、MVC开发,所以Net程序员可以轻松基于它二次开发。它主要监控:

  • servers
  • SQL clusters/instances
  • redis
  • elastic search
  • exception logs
  • haproxy

Opserver提供详细的面板,用来快速展示被监控系统的总体情况。 下面Opserver的监控UI界面示例,非常详细: 技术分享技术分享

使用配置

项目地址:https://github.com/opserver/Opserver

下载后用VS打开或IIS直接部署即可,下面是它的支持监控系统的view目录,结构比较清晰。

技术分享

安全配置

Opserver系统本身后登陆验证,支持3种安全认证方式:

<?xml version="1.0" encoding="utf-8"?>
<SecuritySettings provider="AD">
    <!-- 可选, 下面的网络可以不用验证直接访问 -->
    <InternalNetworks>
        <Network name="SE Internal" cidr="10.0.0.0/8" />
    </InternalNetworks>
</SecuritySettings>

<!-- 
每个人都是管理都可访问
<SecuritySettings provider="alladmin" />
-->

如果使用活动目录验证,可以直接在web.config配置ViewGroups、AdminGroups,也可以单独在每个系统监控json配置文件里面添加ViewGroups、AdminGroups:

"viewGroups": "*",
"adminGroups": "SysAdmins",
"user": "user",
"password": "pass",
"adminUser": "adminuser",
"adminPassword": "adminpass",

监控配置

配置监控的地方在/Config/目录,Stack Exchange提供对应系统的配置示例,如图: 如果没有配置任何系统监控文件,浏览OpServer页面时,会报‘No Configuration‘的警告提示。 这里以Redis为例,监控配置如下:

{
    "allServers": {
        "name": "All",
        "instances": [
              {
                "name": "本地master",
                "port": "6379"
            },
            {
                "name": "本地slave1",
                "port": "6380"
            },
            {
                "name": "本地master2",
                "port": "6382"
            }
        ]
          
    },
    "Servers": [
        { "name": "127.0.0.1" }
    ]
}

部署实例

认证配置<SecuritySettings provider=”alladmin”>所有人都是管理员,打开浏览器访问,输入账号admin,密码admin:

技术分享

可以看到有2组实例,其中6380是slave,6379是master,从图表上可以清晰看到层架关系。

技术分享

实例列表:

技术分享

点击单个Redis实例进去看到。

技术分享

面板属性

面板展示的属性都是可以通过redis info命令获取到,opserver做了更清晰的展示。

Ops(/sec)  每秒处理量

memory(used)即used_memory_rss(used_memory)

used_memory_rss : 从操作系统的角度,返回 Redis 已分配的内存总量(俗称常驻集大小)。这个值和 top 、 ps等命令的输出一致。

used_memory_peak : Redis 的内存消耗峰值(以字节为单位)

used_memory : 由 Redis 分配器分配的内存总量,以字节(byte)为单位

 

Summary是总体概览部分。

Memory是内存使用情况,重要。

persistence 是RDB和AOF的状态。
keyspace key存储的情况,analyze进去可以查看详细分布。
stats  客户端命令的key命中率和处理量
clients 查看有哪个ip(或机器名)过来的连接数多,很方便的定位到那台应用端机器长时间没有释放连接,重要。

slow command log 服务端接受的命令日志。

总结

Opserver 算是个比较轻量级的监控系统,部署修改都非常方便,比如增加连接数或者内存报警功能。

Opserver简单部署

一、下载opserver项目

地址:https://github.com/opserver/Opserver/

二、用vs2012及以上版本打卡opserver项目,如图

三、右击Opserver,点“设为启动项”

四、调试(F5)

调试之后,发现报错

停止调试进入Opserver项目下的Config目录找到SecuritySettings.config.example文件。清单如下:

  1. <?xml version=“1.0” encoding=“utf-8”?>
  2. <SecuritySettings provider=“AD”>
  3.     <  Optional, these networks can see the overview dashboard without authentication  >
  4.     <InternalNetworks>
  5.         <Network name=“SE Internal” cidr=“10.0.0.0/8” />
  6.     </InternalNetworks>
  7. </SecuritySettings>
  8. <!–Example of global access for everyone:
  9. <SecuritySettings provider=“alladmin” >–>

 

修改cidr配置为你的本地地址如:192.168.0.0/24或者127.0.0.1【可选 这些网络无须身份验证就可以看到概览仪表板】

我这里直接将文件改为:

  1. <?xml version=“1.0” encoding=“utf-8”?>
  2. <SecuritySettings provider=“alladmin” >
  3. </SecuritySettings>

 

保存为SecuritySettings.config(去掉example),再次调试:

因为安全设置是alladmin,所以直接点击Login in即可,进去之后你会看到:

这是因为还没有配置以下相关监控文件

这里以SQL为例:

找到Opserver下面的config/SQLSettings.json.example文件,双击编辑:

  1. {
  2.   “defaultConnectionString”“Data Source=$ServerName$;Initial Catalog=master;Integrated Security=SSPI;”,
  3.   “instances”: [
  4.       {
  5.           “name”“实例名”,
  6.         “connectionString”“Data Source=实例名;Initial Catalog=DB;UID=sa;pwd=****;”
  7.       },
  8.     { “name”“实例名” }
  9. }

再次调试,看到以下界面

点击Node可以看到实例详细。

完毕~微笑

跟蓝狐学MVC教程–MiniProfiler.EF6监控调试MVC5和EF6的性能

以前开发Webform的时候可以开启trace来跟踪页面事件,这对于诊断程序的性能是有很大的帮助的,起到事半功倍的作用,今天我就来谈用mvc开发项目的调试和性能监控。EF框架自动给我生成sql语句,当我们的程序遇到性能问题的时候我们可以用MiniProfiler.EF来监控调试MVC和EF的性能,查看生成的sql语句、运行了哪些sql,以及所花的时间。MiniProfiler.EF,一个轻量级开源的mvc性能调试、监控组件MiniProfiler专门为EF定制的版本。下面通过一个具体一例子的说明怎么在我们的项目中用MiniProfiler.EF6监控调试MVC和EF的性能。下面的项目是基于我上面的一篇文章的,MVC5与EF6 Code First 第一个入门完整实例教程

1、安装MiniProfiler.EF6

nuget搜索框中输入MiniProfiler,将出现下面结果:

点击安装将把MiniProfiler.EF6相关的dll加到项目中。

2、添加MiniProfiler.EF相关代码到项目里面

1、在Global.asax加入MiniProfiler相关的监控代码

修改之后完整内容为:

  1. using System.Web.Mvc;
  2. using System.Web.Optimization;
  3. using System.Web.Routing;
  4. using StackExchange.Profiling;
  5. using StackExchange.Profiling.EntityFramework6;
  6. namespace MiniProfilerDemo
  7. {
  8. public class MvcApplication : System.Web.HttpApplication
  9. {
  10. protected void Application_Start()
  11. {
  12. AreaRegistration.RegisterAllAreas();
  13. FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
  14. RouteConfig.RegisterRoutes(RouteTable.Routes);
  15. BundleConfig.RegisterBundles(BundleTable.Bundles);
  16. MiniProfilerEF6.Initialize();
  17. }
  18. protected void Application_BeginRequest()
  19. {
  20. MiniProfiler.Start();
  21. }
  22. protected void Application_EndRequest()
  23. {
  24. MiniProfiler.Stop();
  25. }
  26. }
  27. }

其中是在Application_Start加入了MiniProfilerEF6.Initialize()和添加了Application_BeginRequest、Application_BeginRequest两个Application的事件函数,这个的作用分别是初始化MiniProfilerEF6和开始、结束MiniProfiler监控。

2、修改_Layout.cshtml视图文件

在Views\Shared\_Layout.cshtml文件的body前面加上一段代码,让监控展示在页面上。

  1. @StackExchange.Profiling.MiniProfiler.RenderIncludes()

如下图:

3、在Web.config加入代码

  1. <system.webServer>
  2. <handlers>
  3. <add name=“MiniProfiler” path=“mini-profiler-resources/*” verb=“*” type=“System.Web.Routing.UrlRoutingModule” resourceType=“Unspecified” preCondition=“integratedMode” />
  4. </handlers>
  5. </system.webServer>

为了要在页面上显示MVC和EF的调试跟踪时间必须要加入上面的代码。如下图:

在system.webServer配置结点下的handlers结点,加入了一个名为MiniProfiler的handler。

3、查看运行结果

运行程序,查看页面如下图:

可以看到左角多了一个数字的区块,表示这个页面所花的毫秒数,点击上面的数字就可以弹出详细的时间跟踪信息,如下图:

可以看到这个页面运行了三个sql语句,sql所花时间为您673.4毫秒,占部时间为为12.5%。还可以点击这个sql的时间,将显示运行了哪些sql,如下图:

4、细微监控方法内部的时间

现在我们为ProductController加上监控,监控获取从数据库中获取Product记录所花的时间。我们把ProductController修改为:
  1. using MiniProfilerDemo.DAL;
  2. using System.linq;
  3. using System.Web.Mvc;
  4. using StackExchange.Profiling;
  5. using System.Collections.Generic;
  6. using MiniProfilerDemo.Models;
  7. namespace MiniProfilerDemo.Controllers
  8. {
  9. public class ProductController : Controller
  10. {
  11. public ActionResult Index()
  12. {
  13. using (EFDbContext db = new EFDbContext())
  14. {
  15. var profiler = MiniProfiler.Current;
  16. List<Product> m;
  17. using (profiler.Step(“获取Product列表”))
  18. {
  19. m = db.Products.ToList();
  20. }
  21. return View(m);
  22. }
  23. }
  24. }
  25. }

重新生成项目,再次运行查看页面,如下图:

可以看到上面多了我们刚才手动加的“获取Product列表”条记录。这样可以细微监控方法内部的时间,方便、快速地帮我们找出我们的程序的瓶颈所在。

本站文章除注明转载外,均为本站原创或翻译,欢迎任何形式的转载,但请务必注明出处,尊重他人劳动,共创和谐网络环境。
转载请注明:文章转载自:蓝狐软件工作室 » 跟蓝狐学MVC教程–MiniProfiler.EF6监控调试MVC5和EF6的性能
本文标题:跟蓝狐学MVC教程–MiniProfiler.EF6监控调试MVC5和EF6的性能
本文地址:http://www.lanhusoft.com/Article/125.html

Google Protocol Buffer 的使用和原理

Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,很适合做数据存储或 RPC 数据交换格式。它可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。目前提供了 C++、Java、Python 三种语言的 API。

刘 明, 软件工程师, 上海交大电子与通信系

2010 年 11 月 18 日

  • +内容

简介

什么是 Google Protocol Buffer? 假如您在网上搜索,应该会得到类似这样的文字介绍:

Google Protocol Buffer( 简称 Protobuf) 是 Google 公司内部的混合语言数据标准,目前已经正在使用的有超过 48,162 种报文格式定义和超过 12,183 个 .proto 文件。他们用于 RPC 系统和持续数据存储系统。

Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。目前提供了 C++、Java、Python 三种语言的 API。

或许您和我一样,在第一次看完这些介绍后还是不明白 Protobuf 究竟是什么,那么我想一个简单的例子应该比较有助于理解它。

一个简单的例子

安装 Google Protocol Buffer

在网站 http://code.google.com/p/protobuf/downloads/list上可以下载 Protobuf 的源代码。然后解压编译安装便可以使用它了。

安装步骤如下所示:

 tar -xzf protobuf-2.1.0.tar.gz 
 cd protobuf-2.1.0 
 ./configure --prefix=$INSTALL_DIR 
 make 
 make check 
 make install

关于简单例子的描述

我打算使用 Protobuf 和 C++ 开发一个十分简单的例子程序。

该程序由两部分组成。第一部分被称为 Writer,第二部分叫做 Reader。

Writer 负责将一些结构化的数据写入一个磁盘文件,Reader 则负责从该磁盘文件中读取结构化数据并打印到屏幕上。

准备用于演示的结构化数据是 HelloWorld,它包含两个基本数据:

  • ID,为一个整数类型的数据
  • Str,这是一个字符串

书写 .proto 文件

首先我们需要编写一个 proto 文件,定义我们程序中需要处理的结构化数据,在 protobuf 的术语中,结构化数据被称为 Message。proto 文件非常类似 java 或者 C 语言的数据定义。代码清单 1 显示了例子应用中的 proto 文件内容。

清单 1. proto 文件
 package lm; 
 message helloworld 
 { 
    required int32     id = 1;  // ID 
    required string    str = 2;  // str 
    optional int32     opt = 3;  //optional field 
 }

一个比较好的习惯是认真对待 proto 文件的文件名。比如将命名规则定于如下:

 packageName.MessageName.proto

在上例中,package 名字叫做 lm,定义了一个消息 helloworld,该消息有三个成员,类型为 int32 的 id,另一个为类型为 string 的成员 str。opt 是一个可选的成员,即消息中可以不包含该成员。

编译 .proto 文件

写好 proto 文件之后就可以用 Protobuf 编译器将该文件编译成目标语言了。本例中我们将使用 C++。

假设您的 proto 文件存放在 $SRC_DIR 下面,您也想把生成的文件放在同一个目录下,则可以使用如下命令:

 protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto

命令将生成两个文件:

lm.helloworld.pb.h , 定义了 C++ 类的头文件

lm.helloworld.pb.cc , C++ 类的实现文件

在生成的头文件中,定义了一个 C++ 类 helloworld,后面的 Writer 和 Reader 将使用这个类来对消息进行操作。诸如对消息的成员进行赋值,将消息序列化等等都有相应的方法。

编写 writer 和 Reader

如前所述,Writer 将把一个结构化数据写入磁盘,以便其他人来读取。假如我们不使用 Protobuf,其实也有许多的选择。一个可能的方法是将数据转换为字符串,然后将字符串写入磁盘。转换为字符串的方法可以使用 sprintf(),这非常简单。数字 123 可以变成字符串”123”。

这样做似乎没有什么不妥,但是仔细考虑一下就会发现,这样的做法对写 Reader 的那个人的要求比较高,Reader 的作者必须了 Writer 的细节。比如”123”可以是单个数字 123,但也可以是三个数字 1,2 和 3,等等。这么说来,我们还必须让 Writer 定义一种分隔符一样的字符,以便 Reader 可以正确读取。但分隔符也许还会引起其他的什么问题。最后我们发现一个简单的 Helloworld 也需要写许多处理消息格式的代码。

如果使用 Protobuf,那么这些细节就可以不需要应用程序来考虑了。

使用 Protobuf,Writer 的工作很简单,需要处理的结构化数据由 .proto 文件描述,经过上一节中的编译过程后,该数据化结构对应了一个 C++ 的类,并定义在 lm.helloworld.pb.h 中。对于本例,类名为 lm::helloworld。

Writer 需要 include 该头文件,然后便可以使用这个类了。

现在,在 Writer 代码中,将要存入磁盘的结构化数据由一个 lm::helloworld 类的对象表示,它提供了一系列的 get/set 函数用来修改和读取结构化数据中的数据成员,或者叫 field。

当我们需要将该结构化数据保存到磁盘上时,类 lm::helloworld 已经提供相应的方法来把一个复杂的数据变成一个字节序列,我们可以将这个字节序列写入磁盘。

对于想要读取这个数据的程序来说,也只需要使用类 lm::helloworld 的相应反序列化方法来将这个字节序列重新转换会结构化数据。这同我们开始时那个“123”的想法类似,不过 Protobuf 想的远远比我们那个粗糙的字符串转换要全面,因此,我们不如放心将这类事情交给 Protobuf 吧。

程序清单 2 演示了 Writer 的主要代码,您一定会觉得很简单吧?

清单 2. Writer 的主要代码
 #include "lm.helloworld.pb.h"
…

 int main(void) 
 { 
  
  lm::helloworld msg1; 
  msg1.set_id(101); 
  msg1.set_str(“hello”); 
    
  // Write the new address book back to disk. 
  fstream output("./log", ios::out | ios::trunc | ios::binary); 
        
  if (!msg1.SerializeToOstream(&output)) { 
      cerr << "Failed to write msg." << endl; 
      return -1; 
  }         
  return 0; 
 }

Msg1 是一个 helloworld 类的对象,set_id() 用来设置 id 的值。SerializeToOstream 将对象序列化后写入一个 fstream 流。

代码清单 3 列出了 reader 的主要代码。

清单 3. Reader
 #include "lm.helloworld.pb.h" 
…
 void ListMsg(const lm::helloworld & msg) { 
  cout << msg.id() << endl; 
  cout << msg.str() << endl; 
 } 
 
 int main(int argc, char* argv[]) { 

  lm::helloworld msg1; 
 
  { 
    fstream input("./log", ios::in | ios::binary); 
    if (!msg1.ParseFromIstream(&input)) { 
      cerr << "Failed to parse address book." << endl; 
      return -1; 
    } 
  } 
 
  ListMsg(msg1); 
  … 
 }

同样,Reader 声明类 helloworld 的对象 msg1,然后利用 ParseFromIstream 从一个 fstream 流中读取信息并反序列化。此后,ListMsg 中采用 get 方法读取消息的内部信息,并进行打印输出操作。

运行结果

运行 Writer 和 Reader 的结果如下:

 >writer 
 >reader 
 101 
 Hello

Reader 读取文件 log 中的序列化信息并打印到屏幕上。本文中所有的例子代码都可以在附件中下载。您可以亲身体验一下。

这个例子本身并无意义,但只要您稍加修改就可以将它变成更加有用的程序。比如将磁盘替换为网络 socket,那么就可以实现基于网络的数据交换任务。而存储和交换正是 Protobuf 最有效的应用领域。

和其他类似技术的比较

看完这个简单的例子之后,希望您已经能理解 Protobuf 能做什么了,那么您可能会说,世上还有很多其他的类似技术啊,比如 XML,JSON,Thrift 等等。和他们相比,Protobuf 有什么不同呢?

简单说来 Protobuf 的主要优点就是:简单,快。

这有测试为证,项目 thrift-protobuf-compare 比较了这些类似的技术,图 1 显示了该项目的一项测试结果,Total Time.

图 1. 性能测试结果

图 1. 性能测试结果Total Time 指一个对象操作的整个时间,包括创建对象,将对象序列化为内存中的字节序列,然后再反序列化的整个过程。从测试结果可以看到 Protobuf 的成绩很好,感兴趣的读者可以自行到网站 http://code.google.com/p/thrift-protobuf-compare/wiki/Benchmarking上了解更详细的测试结果。

Protobuf 的优点

Protobuf 有如 XML,不过它更小、更快、也更简单。你可以定义自己的数据结构,然后使用代码生成器生成的代码来读写这个数据结构。你甚至可以在无需重新部署程序的情况下更新数据结构。只需使用 Protobuf 对数据结构进行一次描述,即可利用各种不同语言或从各种不同数据流中对你的结构化数据轻松读写。

它有一个非常棒的特性,即“向后”兼容性好,人们不必破坏已部署的、依靠“老”数据格式的程序就可以对数据结构进行升级。这样您的程序就可以不必担心因为消息结构的改变而造成的大规模的代码重构或者迁移的问题。因为添加新的消息中的 field 并不会引起已经发布的程序的任何改变。

Protobuf 语义更清晰,无需类似 XML 解析器的东西(因为 Protobuf 编译器会将 .proto 文件编译生成对应的数据访问类以对 Protobuf 数据进行序列化、反序列化操作)。

使用 Protobuf 无需学习复杂的文档对象模型,Protobuf 的编程模式比较友好,简单易学,同时它拥有良好的文档和示例,对于喜欢简单事物的人们而言,Protobuf 比其他的技术更加有吸引力。

Protobuf 的不足

Protbuf 与 XML 相比也有不足之处。它功能简单,无法用来表示复杂的概念。

XML 已经成为多种行业标准的编写工具,Protobuf 只是 Google 公司内部使用的工具,在通用性上还差很多。

由于文本并不适合用来描述数据结构,所以 Protobuf 也不适合用来对基于文本的标记文档(如 HTML)建模。另外,由于 XML 具有某种程度上的自解释性,它可以被人直接读取编辑,在这一点上 Protobuf 不行,它以二进制的方式存储,除非你有 .proto 定义,否则你没法直接读出 Protobuf 的任何内容【 2 】。

高级应用话题

更复杂的 Message

到这里为止,我们只给出了一个简单的没有任何用处的例子。在实际应用中,人们往往需要定义更加复杂的 Message。我们用“复杂”这个词,不仅仅是指从个数上说有更多的 fields 或者更多类型的 fields,而是指更加复杂的数据结构:

嵌套 Message

嵌套是一个神奇的概念,一旦拥有嵌套能力,消息的表达能力就会非常强大。

代码清单 4 给出一个嵌套 Message 的例子。

清单 4. 嵌套 Message 的例子
 message Person { 
  required string name = 1; 
  required int32 id = 2;        // Unique ID number for this person. 
  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 Person 中,定义了嵌套消息 PhoneNumber,并用来定义 Person 消息中的 phone 域。这使得人们可以定义更加复杂的数据结构。

4.1.2 Import Message

在一个 .proto 文件中,还可以用 Import 关键字引入在其他 .proto 文件中定义的消息,这可以称做 Import Message,或者 Dependency Message。

比如下例:

清单 5. 代码
 import common.header; 

 message youMsg{ 
  required common.info_header header = 1; 
  required string youPrivateData = 2; 
 }

其中 ,common.info_header定义在common.header包内。

Import Message 的用处主要在于提供了方便的代码管理机制,类似 C 语言中的头文件。您可以将一些公用的 Message 定义在一个 package 中,然后在别的 .proto 文件中引入该 package,进而使用其中的消息定义。

Google Protocol Buffer 可以很好地支持嵌套 Message 和引入 Message,从而让定义复杂的数据结构的工作变得非常轻松愉快。

动态编译

一般情况下,使用 Protobuf 的人们都会先写好 .proto 文件,再用 Protobuf 编译器生成目标语言所需要的源代码文件。将这些生成的代码和应用程序一起编译。

可是在某且情况下,人们无法预先知道 .proto 文件,他们需要动态处理一些未知的 .proto 文件。比如一个通用的消息转发中间件,它不可能预知需要处理怎样的消息。这需要动态编译 .proto 文件,并使用其中的 Message。

Protobuf 提供了 google::protobuf::compiler 包来完成动态编译的功能。主要的类叫做 importer,定义在 importer.h 中。使用 Importer 非常简单,下图展示了与 Import 和其它几个重要的类的关系。

图 2. Importer 类

图 2. Importer 类Import 类对象中包含三个主要的对象,分别为处理错误的 MultiFileErrorCollector 类,定义 .proto 文件源目录的 SourceTree 类。

下面还是通过实例说明这些类的关系和使用吧。

对于给定的 proto 文件,比如 lm.helloworld.proto,在程序中动态编译它只需要很少的一些代码。如代码清单 6 所示。

清单 6. 代码
 google::protobuf::compiler::MultiFileErrorCollector errorCollector;
 google::protobuf::compiler::DiskSourceTree sourceTree; 

 google::protobuf::compiler::Importer importer(&sourceTree, &errorCollector); 
 sourceTree.MapPath("", protosrc); 

 importer.import(“lm.helloworld.proto”);

首先构造一个 importer 对象。构造函数需要两个入口参数,一个是 source Tree 对象,该对象指定了存放 .proto 文件的源目录。第二个参数是一个 error collector 对象,该对象有一个 AddError 方法,用来处理解析 .proto 文件时遇到的语法错误。

之后,需要动态编译一个 .proto 文件时,只需调用 importer 对象的 import 方法。非常简单。

那么我们如何使用动态编译后的 Message 呢?我们需要首先了解几个其他的类

Package google::protobuf::compiler 中提供了以下几个类,用来表示一个 .proto 文件中定义的 message,以及 Message 中的 field,如图所示。

图 3. 各个 Compiler 类之间的关系

图 3. 各个 Compiler 类之间的关系类 FileDescriptor 表示一个编译后的 .proto 文件;类 Descriptor 对应该文件中的一个 Message;类 FieldDescriptor 描述一个 Message 中的一个具体 Field。

比如编译完 lm.helloworld.proto 之后,可以通过如下代码得到 lm.helloworld.id 的定义:

清单 7. 得到 lm.helloworld.id 的定义的代码
 const protobuf::Descriptor *desc = 
    importer_.pool()->FindMessageTypeByName(“lm.helloworld”); 
 const protobuf::FieldDescriptor* field = 
    desc->pool()->FindFileByName (“id”);

通过 Descriptor,FieldDescriptor 的各种方法和属性,应用程序可以获得各种关于 Message 定义的信息。比如通过 field->name() 得到 field 的名字。这样,您就可以使用一个动态定义的消息了。

编写新的 proto 编译器

随 Google Protocol Buffer 源代码一起发布的编译器 protoc 支持 3 种编程语言:C++,java 和 Python。但使用 Google Protocol Buffer 的 Compiler 包,您可以开发出支持其他语言的新的编译器。

类 CommandLineInterface 封装了 protoc 编译器的前端,包括命令行参数的解析,proto 文件的编译等功能。您所需要做的是实现类 CodeGenerator 的派生类,实现诸如代码生成等后端工作:

程序的大体框架如图所示:

图 4. XML 编译器框图

图 4. XML 编译器框图在 main() 函数内,生成 CommandLineInterface 的对象 cli,调用其 RegisterGenerator() 方法将新语言的后端代码生成器 yourG 对象注册给 cli 对象。然后调用 cli 的 Run() 方法即可。

这样生成的编译器和 protoc 的使用方法相同,接受同样的命令行参数,cli 将对用户输入的 .proto 进行词法语法等分析工作,最终生成一个语法树。该树的结构如图所示。

图 5. 语法树

图 5. 语法树其根节点为一个 FileDescriptor 对象(请参考“动态编译”一节),并作为输入参数被传入 yourG 的 Generator() 方法。在这个方法内,您可以遍历语法树,然后生成对应的您所需要的代码。简单说来,要想实现一个新的 compiler,您只需要写一个 main 函数,和一个实现了方法 Generator() 的派生类即可。

在本文的下载附件中,有一个参考例子,将 .proto 文件编译生成 XML 的 compiler,可以作为参考。

Protobuf 的更多细节

人们一直在强调,同 XML 相比, Protobuf 的主要优点在于性能高。它以高效的二进制方式存储,比 XML 小 3 到 10 倍,快 20 到 100 倍。

对于这些 “小 3 到 10 倍”,“快 20 到 100 倍”的说法,严肃的程序员需要一个解释。因此在本文的最后,让我们稍微深入 Protobuf 的内部实现吧。

有两项技术保证了采用 Protobuf 的程序能获得相对于 XML 极大的性能提高。

第一点,我们可以考察 Protobuf 序列化后的信息内容。您可以看到 Protocol Buffer 信息的表示非常紧凑,这意味着消息的体积减少,自然需要更少的资源。比如网络上传输的字节数更少,需要的 IO 更少等,从而提高性能。

第二点我们需要理解 Protobuf 封解包的大致过程,从而理解为什么会比 XML 快很多。

Google Protocol Buffer 的 Encoding

Protobuf 序列化后所生成的二进制消息非常紧凑,这得益于 Protobuf 采用的非常巧妙的 Encoding 方法。

考察消息结构之前,让我首先要介绍一个叫做 Varint 的术语。

Varint 是一种紧凑的表示数字的方法。它用一个或多个字节来表示一个数字,值越小的数字使用越少的字节数。这能减少用来表示数字的字节数。

比如对于 int32 类型的数字,一般需要 4 个 byte 来表示。但是采用 Varint,对于很小的 int32 类型的数字,则可以用 1 个 byte 来表示。当然凡事都有好的也有不好的一面,采用 Varint 表示法,大的数字则需要 5 个 byte 来表示。从统计的角度来说,一般不会所有的消息中的数字都是大数,因此大多数情况下,采用 Varint 后,可以用更少的字节数来表示数字信息。下面就详细介绍一下 Varint。

Varint 中的每个 byte 的最高位 bit 有特殊的含义,如果该位为 1,表示后续的 byte 也是该数字的一部分,如果该位为 0,则结束。其他的 7 个 bit 都用来表示数字。因此小于 128 的数字都可以用一个 byte 表示。大于 128 的数字,比如 300,会用两个字节来表示:1010 1100 0000 0010

下图演示了 Google Protocol Buffer 如何解析两个 bytes。注意到最终计算前将两个 byte 的位置相互交换过一次,这是因为 Google Protocol Buffer 字节序采用 little-endian 的方式。

图 6. Varint 编码

图 6. Varint 编码消息经过序列化后会成为一个二进制数据流,该流中的数据为一系列的 Key-Value 对。如下图所示:

图 7. Message Buffer

图 7. Message Buffer采用这种 Key-Pair 结构无需使用分隔符来分割不同的 Field。对于可选的 Field,如果消息中不存在该 field,那么在最终的 Message Buffer 中就没有该 field,这些特性都有助于节约消息本身的大小。

以代码清单 1 中的消息为例。假设我们生成如下的一个消息 Test1:

 Test1.id = 10; 
 Test1.str = “hello”;

则最终的 Message Buffer 中有两个 Key-Value 对,一个对应消息中的 id;另一个对应 str。

Key 用来标识具体的 field,在解包的时候,Protocol Buffer 根据 Key 就可以知道相应的 Value 应该对应于消息中的哪一个 field。

Key 的定义如下:

 (field_number << 3) | wire_type

可以看到 Key 由两部分组成。第一部分是 field_number,比如消息 lm.helloworld 中 field id 的 field_number 为 1。第二部分为 wire_type。表示 Value 的传输类型。

Wire Type 可能的类型如下表所示:

表 1. Wire Type
Type Meaning Used For
0 Varint int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 64-bit fixed64, sfixed64, double
2 Length-delimi string, bytes, embedded messages, packed repeated fields
3 Start group Groups (deprecated)
4 End group Groups (deprecated)
5 32-bit fixed32, sfixed32, float

在我们的例子当中,field id 所采用的数据类型为 int32,因此对应的 wire type 为 0。细心的读者或许会看到在 Type 0 所能表示的数据类型中有 int32 和 sint32 这两个非常类似的数据类型。Google Protocol Buffer 区别它们的主要意图也是为了减少 encoding 后的字节数。

在计算机内,一个负数一般会被表示为一个很大的整数,因为计算机定义负数的符号位为数字的最高位。如果采用 Varint 表示一个负数,那么一定需要 5 个 byte。为此 Google Protocol Buffer 定义了 sint32 这种类型,采用 zigzag 编码。

Zigzag 编码用无符号数来表示有符号数字,正数和负数交错,这就是 zigzag 这个词的含义了。

如图所示:

图 8. ZigZag 编码

图 8. ZigZag 编码使用 zigzag 编码,绝对值小的数字,无论正负都可以采用较少的 byte 来表示,充分利用了 Varint 这种技术。

其他的数据类型,比如字符串等则采用类似数据库中的 varchar 的表示方法,即用一个 varint 表示长度,然后将其余部分紧跟在这个长度部分之后即可。

通过以上对 protobuf Encoding 方法的介绍,想必您也已经发现 protobuf 消息的内容小,适于网络传输。假如您对那些有关技术细节的描述缺乏耐心和兴趣,那么下面这个简单而直观的比较应该能给您更加深刻的印象。

对于代码清单 1 中的消息,用 Protobuf 序列化后的字节序列为:

 08 65 12 06 48 65 6C 6C 6F 77

而如果用 XML,则类似这样:

 31 30 31 3C 2F 69 64 3E 3C 6E 61 6D 65 3E 68 65 
 6C 6C 6F 3C 2F 6E 61 6D 65 3E 3C 2F 68 65 6C 6C 
 6F 77 6F 72 6C 64 3E 

一共 55 个字节,这些奇怪的数字需要稍微解释一下,其含义用 ASCII 表示如下:
 <helloworld> 
    <id>101</id> 
    <name>hello</name> 
 </helloworld>

封解包的速度

首先我们来了解一下 XML 的封解包过程。XML 需要从文件中读取出字符串,再转换为 XML 文档对象结构模型。之后,再从 XML 文档对象结构模型中读取指定节点的字符串,最后再将这个字符串转换成指定类型的变量。这个过程非常复杂,其中将 XML 文件转换为文档对象结构模型的过程通常需要完成词法文法分析等大量消耗 CPU 的复杂计算。

反观 Protobuf,它只需要简单地将一个二进制序列,按照指定的格式读取到 C++ 对应的结构类型中就可以了。从上一节的描述可以看到消息的 decoding 过程也可以通过几个位移操作组成的表达式计算即可完成。速度非常快。

为了说明这并不是我拍脑袋随意想出来的说法,下面让我们简单分析一下 Protobuf 解包的代码流程吧。

以代码清单 3 中的 Reader 为例,该程序首先调用 msg1 的 ParseFromIstream 方法,这个方法解析从文件读入的二进制数据流,并将解析出来的数据赋予 helloworld 类的相应数据成员。

该过程可以用下图表示:

图 9. 解包流程图

图 9. 解包流程图整个解析过程需要 Protobuf 本身的框架代码和由 Protobuf 编译器生成的代码共同完成。Protobuf 提供了基类 Message 以及 Message_lite 作为通用的 Framework,,CodedInputStream 类,WireFormatLite 类等提供了对二进制数据的 decode 功能,从 5.1 节的分析来看,Protobuf 的解码可以通过几个简单的数学运算完成,无需复杂的词法语法分析,因此 ReadTag() 等方法都非常快。 在这个调用路径上的其他类和方法都非常简单,感兴趣的读者可以自行阅读。 相对于 XML 的解析过程,以上的流程图实在是非常简单吧?这也就是 Protobuf 效率高的第二个原因了。

结束语

往往了解越多,人们就会越觉得自己无知。我惶恐地发现自己竟然写了一篇关于序列化的文章,文中必然有许多想当然而自以为是的东西,还希望各位能够去伪存真,更希望真的高手能不吝赐教,给我来信。谢谢。

参考资料

学习

讨论