Windows 下的 Apache + FastCGI 部署 ROR 应用

系统需求

  • Apache
  • Ruby
  • Rails

安装 Ruby 建议使用 Ruby 1.8.6 One-Click Installer,而不要使用 Ruby 1.8.6 Binary ,因为后者不包含 FCGI 模块,即使使用 gem install fcgi 安装也必须编译才能使用。
如果没有 FCG 模块,则 Apache 启动 FastCGI 进程可能会报以下错误:

The pipe has been ended.  : modfcgid: get overlap result error_

安装 Rails : gem install rails

安装 Apache Http Server

获得 mod_fcgid,该模块依赖 Visual C++ 2008 Redistributable Package;解压 mod_fcgid.so 到 Apache 目录的 modules 子目录下。
fcgid 进程默认只有一个 FCGISHUTDOWN_EVENT_ 环境变量,而 Windows DNS 解析必须有一些特定的环境变量,否则会导致无法解析 DNS,应用报以下错误:

getaddrinfo: no address associated with hostname.

下面的配置描述了如何解决该问题。

我们这里作一些假设:

  • ROR 应用目录:D:/WebRoot/redmine/

配置 Apache,添加以下脚本:

1
2
LoadModule fcgid_module et/mod/mod_fcgid.so
DefaultInitEnv RAILS_ENV production

设置以下环境变量以保证 DNS 解析正确
如果不设置,则会报:

getaddrinfo: no address associated with hostname.

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
DefaultInitEnv PATH "C:/WINDOWS/system32;C:/WINDOWS;C:/WINDOWS/System32/Wbem;"
DefaultInitEnv SystemRoot "C:/Windows"
DefaultInitEnv SystemDrive "C:"
DefaultInitEnv TEMP "C:/WINDOWS/TEMP"
DefaultInitEnv TMP "C:/WINDOWS/TEMP"
DefaultInitEnv windir "C:/WINDOWS"

Alias "/redmine" "D:/WebRoot/redmine/public/"

<Directory "D:/WebRoot/redmine/public/">
  Options Indexes ExecCGI FollowSymLinks
  AllowOverride all
  Order Deny,Allow
  Allow from All
</Directory>

<Location /redmine/>
    AddHandler fcgid-script .fcgi
    FCGIWrapper "C:/ruby/bin/ruby.exe D:/WebRoot/redmine/public/dispatch.fcgi" .fcgi
    RewriteEngine on
    RewriteBase /redmine
    RewriteRule ^$ index.html [QSA]
    RewriteRule ^([^.]+)$ $1.html [QSA]
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^(.*)$ dispatch.fcgi?$1 [QSA,L]
</Location>

Zealic @ 2008-05-16

View Comments |
Categories: tech
Tags:

好用的 Abyss Web Server

今天在家中研究如何实现脱离 IIS 部署 ASP.Net,方法有很多种:

  • mod_aspdotnet
    这个东西可以直接与 Apache 集成,不过 Apache Foundation 已经不再维护该模块,因此放弃。
  • UltiDev Cassini
    还没用过,明天补上。
  • Abyss Web Server
    很好很强大,跨平台的 Web Server,支持 ASP、ASP.Net、ROR、PHP,提供了网页配置接口,用起来也非常的方便。
    它使用 FastCGI 来统一各种技术。
    我在 Win2003 下用它成功的运行了 Gemini(ASP.Net) 和 Redmine(ASP.Net)。

我选择了 Abyss Web Server 来试验我的想法,它非常轻巧,安装包仅仅 700 多K,并且它还有一个免费的 X1 版本。并且可以根据你的需求设置为手动启动、开机启动或安装为 Windows 服务。

关于如何在 Abyss Web Server 下部署 ROR,请看这里

不过我这里用它部署 Remine 的时候,发生了一点小问题,没办法通过 URL http://localhost/ 访问 Redmine 的主页;要修订这个问题,我们需要在原有的 URL Rewriting 规则之上增加一条规则:


Virtual Path Regular Expression : ^/$
Redirect to : /dispatch.fcgi
Next Action : Next action

可以考虑将 Apache 和 Abyss Web Server 结合起来做一个反向代理,这样就可以集中一个端口提供服务,同时也可以作为集群的基础。

Apache 下的 FastCGI 配起来可是会让人吐血的;当然,也可以考虑 lighttpd,不过 lighttpd 的 Windows 版本实在是感觉不爽,以后 Mono XSP + ROR + PHP 诸如此类的混合网站,再考虑在 Linux 下的 lighttpd 吧。

终于,可以和 IIS 这个肥猪说拜拜了。(IIS 幽幽的说:你会回来的~)

Zealic @ 2008-05-10

View Comments |
Categories: tech
Tags:

Zealic's Linux FAQ

前言:

该 FAQ 记录 Zealic 学习使用 Linux 的经验,包括 Ubuntu 发行版但不限于 Ubuntu 发行版,理论上兼容所有 debian 系的发行版,但不排除个别例外情况。

欢迎转载,但你所看到版本不一定是最新的。

文中假设的操作用户名为 zealic,主机名为 host

  1. 如何判断 Linux 的发行版?
    [zealic@host]cat /etc/issue
    /etc/issue 的用途是作为在显示登陆提示符之前的提示信息,大多数情况下,都是显示为系统的发行版。
    此外还可以通过 /etc/issue.net 文件来判断。

  2. 如何恢复使用 Ctrl+Z 挂起的进程?
    启用 cat 进程
    [zealic@host]cat
    按 Ctrl+Z 挂起进程,输出如下:
    [1]+Stopped cat
    [zealic@host]fg 1
    这里 [1] 代表挂起的进程 ID,使用 jobs 命令可以列出所有挂起的进程。
    使用 bg 命令可获得上一个被挂起的进程。
    上面使用的 fg 命令则是恢复被挂起的进程。
    注 : 这里的挂起,是指程序在后台运行。

  3. 如何修改登陆系统时的消息?
    修改 /etc/motd 文件,motd 的全义是 : Message Of ToDay,该文件的作用是,每次用户登陆时,该文件的内容会显示到终端。
    不过上述方法仅能更改当时的登陆消息,当重新启动系统后,又会被替换为 一部分动态生成的内容 + /etc/motd.tail 的内容。
    所以想要持久性的修改,最好修改 /etc/motd.tail 。
    实际上 modtd 是由 /etc/bootmisc.sh 生成的。
    更多信息请使用以下命令参考:
    [zealic@host]man motd
    [zealic@host]man motd.tail

  4. 如何修改网卡和 DNS 设置?
    网卡配置文件:/etc/network/interfaces
    DNS 配置文件:/etc/resolv.conf

  5. 如何统计目录或文件的大小?
    使用 du 命令。

  6. 查看当前系统挂载的分区?
    使用 df 命令。

  7. 无法启用 vim 的语法高亮?
    某些 Linux 发行版安装的 vim 可能不是 vim,而是直接链接到 vi。
    使用 vim --version,查看是否 vim 及具体版本。
    如果的确是 vim 的话,使用 "syntax on" 依旧无法启用语法高亮并报以下错误:
    E319: Sorry, the command is not available in this version: syntax on
    则有可能是没有完整的安装 vim (比如我目前使用的 Ubuntu-8.04 CLI)。
    解决方法,重新安装 vim:
    [zealic@host]sudo apt-get install vim

  8. lsof -i tcp 命令无效?
    使用 sudo 前缀命令即可。

  9. 如何激活或锁定用户?
    激活 root 用户:
    [zealic@host]sudo passwd root
    锁定 root 用户:
    [zealic@host]sudo passwd root

  10. 如何修改命令提示符?
    修改 PS1 环境变量。如果要永久性的修改,则可以直接修改 ~/.bashrc 文件对 PS1 赋值的脚本,如果 .bashrc 没有对 PS1 赋值,则可以修改 /etc/profile。

  11. 如何配置本地编码支持?
    使用以下命令:
    [zealic@host]sudo locale-gen zh_CN.GBK
    上述命令生成 zh_CN.GBK 本地化支持,要完全启用该支持,需要在 /etc/environment (需要重启) 或 ~/.profile (需要重登陆) 或在 SHELL (直接生效)中定义以下变量:
    LANGUAGE="zh_CN:zh"
    LANG="zh_CN.GBK"
    LC_ALL="zh_CN.GBK"
    支持的编码有 UTF-8,GB2312,GBK,GB18030,可以在 /usr/lib/locale 中找到生成的字符集文件;在/var/lib/locales/supported.d/local 文件中可以看到目前已安装的字符集。
    这里我推荐使用 GBK 或 GB18030 编码。
    如果是桌面环境,可能需要重新配置 locale:
    [zealic@host]sudo dpkg-reconfigure locales

  12. 查看用户所属的用户组?
    使用 groups 命令即可查看当前登陆用户所属的用户组,如要查看指定用户的所属的用户组,在命令后加上用户名即可。
    使用 id 可达到相同的效果,并且信息更加详细。

  13. 显示文件或目录的详细信息?
    使用 stat 命令即可。

  14. 执行简单数学运算
    使用 let 命令即可,这是 bash 内建的命令。

  15. 执行计划任务
    使用 crontab 命令,或直接放置脚本到以下目录:

    • /etc/cron.daily/
    • /etc/cron.hourly/
    • /etc/cron.monthly/
    • /etc/cron.weekly/

以上目录的脚本是通过 /etc/crontab 来执行的。
也可使用 crontab -u root -e 命令来编辑指定用户的计划任务脚本,编辑的脚本放在以下目录中:
/var/spool/cron/crontabs/

  1. <<待续>>

Zealic @ 2008-05-08

View Comments |
Categories: tech.posix
Tags:

小记安装配置 Ubuntu 8.04

基本的安装很简单,省略不提。

1. 配置网络

  • 修改网络配置 : /etc/network/interfaces
1
2
3
4
iface eth0 inet static
address 192.168.1.10
netmask 255.255.255.0
gateway 192.168.1.1
  • 使设置生效:
    /etc/init.d/networking restart

2. 配置 APT

先说说源列表这个东西的概念

deb http://ubuntu.cn99.com/ubuntu/ hardy main restricted universe multiverse
  1. 包类型 : deb (deb 或 deb-src,分别代表二进制包和源代码包)
  2. 来源地址
  3. 发行版本 : hardy (hardy 为 ubuntu 8.04 的开发代号,也可以直接使用版本号)
  4. 库 : main | multiverse | restricted | universe

参考:http://wiki.ubuntu.org.cn/index.php?title=UbuntuHelp:Repositories/CommandLine/zh

配置源列表:

1
2
sudo cp /etc/apt/sources.list /ect/apt/sources.list.bak
sudo vim /etc/apt/sources.list

修改为:

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
deb [http://ubuntu.cn99.com/ubuntu/](http://ubuntu.cn99.com/ubuntu/) hardy main restricted universe multiverse
deb [http://ubuntu.cn99.com/ubuntu/](http://ubuntu.cn99.com/ubuntu/) hardy-security main restricted universe multiverse
deb [http://ubuntu.cn99.com/ubuntu/](http://ubuntu.cn99.com/ubuntu/) hardy-updates main restricted universe multiverse
deb [http://ubuntu.cn99.com/ubuntu/](http://ubuntu.cn99.com/ubuntu/) hardy-proposed main restricted universe multiverse
deb [http://ubuntu.cn99.com/ubuntu/](http://ubuntu.cn99.com/ubuntu/) hardy-backports main restricted universe multiverse
deb-src [http://ubuntu.cn99.com/ubuntu/](http://ubuntu.cn99.com/ubuntu/) hardy main restricted universe multiverse
deb-src [http://ubuntu.cn99.com/ubuntu/](http://ubuntu.cn99.com/ubuntu/) hardy-security main restricted universe multiverse
deb-src [http://ubuntu.cn99.com/ubuntu/](http://ubuntu.cn99.com/ubuntu/) hardy-updates main restricted universe multiverse
deb-src [http://ubuntu.cn99.com/ubuntu/](http://ubuntu.cn99.com/ubuntu/) hardy-proposed main restricted universe multiverse
deb-src [http://ubuntu.cn99.com/ubuntu/](http://ubuntu.cn99.com/ubuntu/) hardy-backports main restricted universe multiverse

deb [http://archive.ubuntu.org.cn/ubuntu/](http://archive.ubuntu.org.cn/ubuntu/) hardy main restricted universe multiverse
deb [http://archive.ubuntu.org.cn/ubuntu/](http://archive.ubuntu.org.cn/ubuntu/) hardy-security main restricted universe multiverse
deb [http://archive.ubuntu.org.cn/ubuntu/](http://archive.ubuntu.org.cn/ubuntu/) hardy-updates main restricted universe multiverse
deb [http://archive.ubuntu.org.cn/ubuntu/](http://archive.ubuntu.org.cn/ubuntu/) hardy-proposed main restricted universe multiverse
deb [http://archive.ubuntu.org.cn/ubuntu/](http://archive.ubuntu.org.cn/ubuntu/) hardy-backports main restricted universe multiverse
deb-src [http://archive.ubuntu.org.cn/ubuntu/](http://archive.ubuntu.org.cn/ubuntu/) hardy main restricted universe multiverse
deb-src [http://archive.ubuntu.org.cn/ubuntu/](http://archive.ubuntu.org.cn/ubuntu/) hardy-security main restricted universe multiverse
deb-src [http://archive.ubuntu.org.cn/ubuntu/](http://archive.ubuntu.org.cn/ubuntu/) hardy-updates main restricted universe multiverse
deb-src [http://archive.ubuntu.org.cn/ubuntu/](http://archive.ubuntu.org.cn/ubuntu/) hardy-proposed main restricted universe multiverse
deb-src [http://archive.ubuntu.org.cn/ubuntu/](http://archive.ubuntu.org.cn/ubuntu/) hardy-backports main restricted universe multiverse

deb [http://cn.archive.ubuntu.com/ubuntu](http://cn.archive.ubuntu.com/ubuntu) hardy main restricted universe multiverse
deb [http://cn.archive.ubuntu.com/ubuntu](http://cn.archive.ubuntu.com/ubuntu) hardy-security main restricted universe multiverse
deb [http://cn.archive.ubuntu.com/ubuntu](http://cn.archive.ubuntu.com/ubuntu) hardy-updates main restricted universe multiverse
deb [http://cn.archive.ubuntu.com/ubuntu](http://cn.archive.ubuntu.com/ubuntu) hardy-backports main restricted universe multiverse
deb [http://cn.archive.ubuntu.com/ubuntu](http://cn.archive.ubuntu.com/ubuntu) hardy-proposed main restricted universe multiverse
deb-src [http://cn.archive.ubuntu.com/ubuntu](http://cn.archive.ubuntu.com/ubuntu) hardy main restricted universe multiverse
deb-src [http://cn.archive.ubuntu.com/ubuntu](http://cn.archive.ubuntu.com/ubuntu) hardy-security main restricted universe multiverse
deb-src [http://cn.archive.ubuntu.com/ubuntu](http://cn.archive.ubuntu.com/ubuntu) hardy-updates main restricted universe multiverse
deb-src [http://cn.archive.ubuntu.com/ubuntu](http://cn.archive.ubuntu.com/ubuntu) hardy-backports main restricted universe multiverse
deb-src [http://cn.archive.ubuntu.com/ubuntu](http://cn.archive.ubuntu.com/ubuntu) hardy-proposed main restricted universe multiverse

更新源列表 :
sudo apt-get update

更多源列表信息参见:http://wiki.ubuntu.org.cn/index.php?title=Qref/Source&variant=zh-cn

3. 安装 SSH 服务

安装 openssh-server:

sudo apt-get install openssh-server

如果需要,配置 sshd:

sudo vim /etc/ssh/sshd_config

配置后需要重启
sudo /etc/init.d/ssh restart

之后使用 SecureCRT 或 putty 之类的 SSH Client 软件就可以连接该机器了。

Zealic @ 2008-05-08

View Comments |
Categories: tech.posix
Tags:

StringTemplate Perl 版

上回写了一篇 利用 GAWK 实现模板文件替换,实现了文本文件的替换,不过最近了解下 Perl,因此就此写了一个 Perl 版本的 StringTemplate.pl。
要真说起来,Perl 还是 AWK 的派生,功能为为强大,由于我这里只是将模板替换功能应用到替换一些简单的东西,要求并不是很高,用一个精简版的 Perl 足矣,这就是精简版的 MiniPerl.exe,可以由 Perl 的源代码编译使用。
这次实现的 StringTemplate.pl 相比之前用 GAWK 实现的功能要更强大一些:

  • 嵌套变量替换
  • 是否区分大小写替换

未来的版本考虑添加一个类似 Java 中的 -D 选项添加变量的功能。

看代码先:

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
#!/usr/bin/perl
#<code>
#<owner name="Zealic" email="rszealic@gmail.com"/>
#<version>1.0</version>
#<timestamp>2008-3-30</timestamp>
#</code>

# Initial
$len = scalar @ARGV;
if ($len >= 2 and $len <= 3)
{
  if($len == 3)
  {
    if(index(@ARGV[0], "-") != 0) {showHelp(); exit(101);}
    $ignoreCase = index(@ARGV[0], "i", 1) != -1;
    $nesting = index(@ARGV[0], "n", 1) != -1;
  }
  $dictFile = @ARGV[$len - 2];
  $templateFile = @ARGV[$len - 1];
}
else # Show help
{
  showHelp();
  exit(102);
}  

# Open files
open(FDICT, $dictFile) or die("Can't open file $dictFile.");
open(FTPL,$templateFile) or die("Can't open file $templateFile.");

# Parse dict
{
  @lines = <FDICT>;
  foreach $line(@lines)
  {
      if($line =~ /w+=/)
      {
          $key = substr($line, 0, $+[0] - 1);
          $value = substr($line, $+[0], length($line) - $+[0]);
          chomp($value);
          $dict{$key} = $value;
      }
  }
}

# Replace and output
{
  @contents = <FTPL>;
  foreach $line(@contents)
  {
      while($line =~ /$(w+)/)
      {
          local $old = $line;
          while (($key, $value) = each %dict)
          {
              if($ignoreCase)
              {
                  $line =~ s/$($key)/$value/gi;
              }
              else
              {
                  $line =~ s/$($key)/$value/g;
              }
          }
          if($old eq $line) {last;}
          unless($nesting) {last;}
      }
      print "$line";
  }
}

sub showHelp()
{
  print(
'StringTemplate 1.0
Copyright 2008 Zealic, All rights reserved.
Contact Me : e-mail:rszealic@gmail.com

Usage :
StringTemplate.pl : [-[i][n]] <DictFile> <TemplateFile>

Options :
  i : IGNORECASE variable for case-insensitive matches.
  n : Nest replacement.
');
}

根据这个东西,可以直接将 MiniPerl.exe 纳入版本控制中,成为自动构建的一部分,可以据此动态生成脚本、源文件,Manifest 文件;或通过 MSBuild Community Tasks 的 SVN/VSS/TFS Task,与我以前所写的 通过 TSVN 自动更新程序集版本信息 ,再加上 MSBuild,可以让版本信息生成/测试打包/安装文件生成等重复劳动更加轻松简单。
最后,你可以在这里下载本文所说的内容,包括 MiniPerl.exe、StringTemplate.pl、以及示例文件。

Zealic @ 2008-04-07

View Comments |
Categories: tech.posix
Tags:

利用 GAWK 实现模板文件替换

对于产品开发需要的情况下,我们通常会选择某些模板引擎生成一些文件,如 StringTemplate、Volecity,但是如果我们只是需要完成一些简单重复或者自动化的工作的话,依旧使用这样重量级的东西未免杀鸡用牛刀。

好在有个强大的 GNU AWK!
于是乎,花了一些时间来写了一个 awk 脚本,实现如下功能。
从 ini 文件读取键值,通过键名替换值。

其实就是一个简单的模板功能。

现在我们看一个简单的场景:
文件内容 Talk.tpl

1
2
$(Famale) : Who are you!  
$(Male_FirstName) : $(Male_FirstName), $(Male_FirstName) $(Male_LastName).

INI字典文件 Conf.ini

1
2
3
Famale=Jane  
Male_FirstName=James  
Male_LastName=Bond

执行以下命令:
gawk -f DictReplace.awk Conf.ini Talk.tpl > Talk.txt

则会生成文件 Talk.txt

1
2
Jane : Who are you!  
James : James, James Bond.

awk 脚本 DictReplace.awk

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
#!/usr/bin/gawk -f
#<code>
#<owner name="Zealic" email="rszealic@gmail.com"/>
#<version>1.0</version>
#</code>
BEGIN {
fileCount = 0;
fullContent[0] = "";
fullLength = 0;
}

# MAIN  
{
# File flag
if(FNR == 1)
{
  fileCount++;
}
# Load dict
if(fileCount == 1 && $0 ~ /w+=.*/)
{
  len = length($0);
  klen = index($0, "=") - 1;
  key = substr($0, 0, klen);
  value = substr($0, klen + 2, len);
  repDict[key] = value;
}
else if(fileCount == 2)
{
  # Replace and store
  outValue = $0;
  for(dKey in repDict)
  {
    # Dynamic reglur exp
    nowRegex = "\$\(" dKey "\)";
    dValue = repDict[dKey];
    gsub(nowRegex, dValue, outValue);
  }
  fullContent[fullLength] = outValue;
  fullLength++;
}
}

END {
# Output result
for(i=0;i<fullLength;i++)
{
  print(fullContent[i]);
}
}

上述脚本代码在 UnxUtils 的 gawk 下执行通过。
你可以直接在这里下载完整内容查看结果并获得 gawk.exe。

AWK 的动态构造正则真要命,搞了半天才发现,直接构造字符串就可以,然后直接放到参数中就可以作为正则使用,就是上面代码的蓝色部分。不过也基本学会 AWK,以后又有件利器可用啦。

稍后再测试能否在 Linux 下工作。

Zealic @ 2008-03-28

View Comments |
Categories: tech.posix
Tags:

发布 EasyTrac 0.1.0.11b3

在测试了 EasyTrac-0.1.0.11b3 接近一个月后,现在已经可以稳定的使用了。

因此现在发布 beta-3 版本,之后的下一个版本将会是 release,这会等到 Trac 发布 0.11-release 之后进行,目前 trac-0.11 的进度已经到达 97%,还有 17 个 Ticket 未关闭,估计可能在四月份发布 Release。

EasyTrac 0.1.0.11b3 改进如下:

  • BUG : 如果自定义了数据目录,依然会在默认的 {EasyTrac}/Data/ 目录下创建项目。
  • BUG : 修订了在使用 SVN-COPY | SVN-MOVE 命令修改工作拷贝后通过 HTTP 提交发生 400 bad request 的问题。
  • BUG : 安装 Subversion 服务时,如果自定义了数据目录,安装的服务使用的依然是默认的 {EasyTrac}/Data/SVN/ 目录。
  • NEW : 创建项目时默认为 admin 用户分配 TRAC_ADMIN 权限。
  • NEW : 设置 APR_ICONV_PATH 环境变量,使得 SVN 支持中文名称。

此外,强烈建议您在安装 EasyTrac 之前卸载已经安装的 Subversion 以及 Trac 避免造成冲突。

这里下载 EasyTrac,目前最新版本是 EasyTrac-0.1.0.11b3_Win32,已经较为稳定,现在您可以在生产环境中部署 EasyTrac。

如果您有任何问题以及意见反馈,请与我联系,我将尽可能帮助你。

Zealic @ 2008-03-25

View Comments |
Categories: tech.dev
Tags:

妙用扩展方法

1.参数检查

对于写库的大多数人来说,参数检查是一件极其麻烦的事情,我们需要重复写以下代码:

1
2
3
4
public void Foo(string name)
{
  if(name == null) throw new ArgumentNullException("name");
}

现在我们有了扩展方法,则可以这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// <summary>
/// 参数检查扩展方法类
/// </summary>
public static class RequireExtension
{
  public static void RequireNotNull(this object obj, string name)
  {
    if(name == null) throw new ArgumentNullException("name");
    if(obj is null) throw new ArgumentNullException(name);
  }
}

// 示例
public void Foo(string name)
{
  name.RequireNotNull("name");
}

2.本地化

当我们使用枚举,经常需要在枚举和本地化字符串之间转换,或者编码规范为英文,需要将枚举转换为中文,扩展方法可以让我们这样。

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
/// <summary>
/// 本地化后的友好名称
/// </summary>
public class LocalizedNameAttribute : Attribute
{
  public string Name { get;set;}
}

/// <summary>
/// 扩展枚举
/// </summary>
public static class EnumExtension
{
  public static void ToLocalized(this Enum e)
  {
    Type type = member.GetType();

    foreach (FieldInfo field in type.GetFields())
    {
      if (!field.IsDefined(typeof(LocalizedNameAttribute), false))
        continue;
      LocalizedNameAttribute att = (LocalizedNameAttribute)Attribute.
      GetCustomAttribute(field, typeof(LocalizedNameAttribute));
      return att.Name;
    }
    return member.ToString();
  }
}

/// <summary>
/// 性别
/// </summary>
public enum Gender
{
  [LocalizedName(Name = "男")]
  Male,
  [LocalizedName(Name = "女")]
  Famale,
  [LocalizedName(Name = "东方不败")]
  EastNotFail,
}

我们就可以 Gender.Famle.ToLocalized() 直接获取本地化后的字符串。

3.强类型转换扩展

利用强类型转换,我们可以在代码中少写很多强制转换和类型检查代码。
我们经常写出这样又臭又长的代码:

1
2
MyAttribute att = (MyAttribute)Attribute
  .GetCustomAttribute(methodInfo, typeof(MyAttribute));

现在,我们可以这样:

MyAttribute att = methodInfo.GetAttribute<MyAttribute>();

今天先到这里,下次有经验再分享。

Zealic @ 2008-03-15

View Comments |
Categories: tech.dotnet
Tags:

Socket 死连接详解

当使用 Socket 进行通信时,由于各种不同的因素,都有可能导致死连接停留在服务器端,假如服务端需要处理的连接较多,就有可能造成服务器资源严重浪费,对此,本文将阐述其原理以及解决方法。

在写 Socket 进行通讯时,我们必须预料到各种可能发生的情况并对其进行处理,通常情况下,有以下两种情况可能造成死连接:

  • 通讯程序编写不完善
  • 网络/硬件故障

a) 通讯程序编写不完善

这里要指出的一点就是,绝大多数程序都是由于程序编写不完善所造成的死连接,即对 Socket 未能进行完善的管理,导致占用端口导致服务器资源耗尽。当然,很多情况下,程序可能不是我们所写,而由于程序代码的复杂、杂乱等原因所导致难以维护也是我们所需要面对的。

网上有很多文章都提到 Socket 长时间处于 CLOSE_WAIT 状态下的问题,说可以使用 Keepalive 选项设置 TCP 心跳来解决,但是却发现设置选项后未能收到效果 。

因此,这里我分享出自己的解决方案:

Windows 中对于枚举系统网络连接有一些非常方便的 API:

  • GetTcpTable : 获得 TCP 连接表
  • GetExtendedTcpTable : 获得扩展后的 TCP 连接表,相比 GetTcpTable 更为强大,可以获取与连接的进程 ID
  • SetTcpEntry : 设置 TCP 连接状态,但据 MSDN 所述,只能设置状态为 DeleteTcb,即删除连接

相信大多数朋友看到这些 API ,就已经了解到我们下一步要做什么了;枚举所有 TCP 连接,筛选出本进程的连接,最后判断是否 CLOSE_WAIT 状态,如果是,则使用 SetTcpEntry 关闭。

其实 Sysinternal 的 TcpView 工具也是应用上述 API 实现其功能的,此工具为我常用的网络诊断工具,同时也可作为一个简单的手动式网络防火墙。

下面来看 Zealic 封装后的代码:


TcpManager.cs

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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
/**
<code>
  <revsion>$Rev: 0 $</revision>
  <owner name="Zealic" mail="rszealic(at)gmail.com" />
</code>
**/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;


namespace Zealic.Network
{
    /// <summary>
    /// TCP 管理器
    /// </summary>
    public static class TcpManager
    {
        #region PInvoke define
        private const int TCP_TABLE_OWNER_PID_ALL = 5;

        [DllImport("iphlpapi.dll", SetLastError = true)]
        private static extern uint GetExtendedTcpTable(
            IntPtr pTcpTable, ref int dwOutBufLen, bool sort, int ipVersion, int tblClass, int reserved);

        [DllImport("iphlpapi.dll")]
        private static extern int SetTcpEntry(ref MIB_TCPROW pTcpRow);


        [StructLayout(LayoutKind.Sequential)]
        private struct MIB_TCPROW
        {
            public TcpState dwState;
            public int dwLocalAddr;
            public int dwLocalPort;
            public int dwRemoteAddr;
            public int dwRemotePort;
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct MIB_TCPROW_OWNER_PID
        {
            public TcpState dwState;
            public uint dwLocalAddr;
            public int dwLocalPort;
            public uint dwRemoteAddr;
            public int dwRemotePort;
            public int dwOwningPid;
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct MIB_TCPTABLE_OWNER_PID
        {
            public uint dwNumEntries;
            private MIB_TCPROW_OWNER_PID table;
        }
        #endregion

        private static MIB_TCPROW_OWNER_PID[] GetAllTcpConnections()
        {
            const int NO_ERROR = 0;
            const int IP_v4 = 2;
            MIB_TCPROW_OWNER_PID[] tTable = null;
            int buffSize = 0;
            GetExtendedTcpTable(IntPtr.Zero, ref buffSize, true, IP_v4, TCP_TABLE_OWNER_PID_ALL, 0);
            IntPtr buffTable = Marshal.AllocHGlobal(buffSize);
            try
            {
                if (NO_ERROR != GetExtendedTcpTable(buffTable, ref buffSize, true, IP_v4, TCP_TABLE_OWNER_PID_ALL, 0)) return null;
                MIB_TCPTABLE_OWNER_PID tab =
                    (MIB_TCPTABLE_OWNER_PID)Marshal.PtrToStructure(buffTable, typeof(MIB_TCPTABLE_OWNER_PID));
                IntPtr rowPtr = (IntPtr)((long)buffTable + Marshal.SizeOf(tab.dwNumEntries));
                tTable = new MIB_TCPROW_OWNER_PID[tab.dwNumEntries];

                int rowSize = Marshal.SizeOf(typeof(MIB_TCPROW_OWNER_PID));
                for (int i = 0; i < tab.dwNumEntries; i++)
                {
                    MIB_TCPROW_OWNER_PID tcpRow =
                        (MIB_TCPROW_OWNER_PID)Marshal.PtrToStructure(rowPtr, typeof(MIB_TCPROW_OWNER_PID));
                    tTable[i] = tcpRow;
                    rowPtr = (IntPtr)((int)rowPtr + rowSize);
                }
            }
            finally
            {
                Marshal.FreeHGlobal(buffTable);
            }
            return tTable;
        }

        private static int TranslatePort(int port)
        {
            return ((port & 0xFF) << 8 | (port & 0xFF00) >> 8);
        }

        public static bool Kill(TcpConnectionInfo conn)
        {
            if (conn == null) throw new ArgumentNullException("conn");
            MIB_TCPROW row = new MIB_TCPROW();
            row.dwState = TcpState.DeleteTcb;
#pragma warning disable 612,618
            row.dwLocalAddr = (int)conn.LocalEndPoint.Address.Address;
#pragma warning restore 612,618
            row.dwLocalPort = TranslatePort(conn.LocalEndPoint.Port);
#pragma warning disable 612,618
            row.dwRemoteAddr = (int)conn.RemoteEndPoint.Address.Address;
#pragma warning restore 612,618
            row.dwRemotePort = TranslatePort(conn.RemoteEndPoint.Port);
            return SetTcpEntry(ref row) == 0;
        }

        public static bool Kill(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint)
        {
            if (localEndPoint == null) throw new ArgumentNullException("localEndPoint");
            if (remoteEndPoint == null) throw new ArgumentNullException("remoteEndPoint");
            MIB_TCPROW row = new MIB_TCPROW();
            row.dwState = TcpState.DeleteTcb;
#pragma warning disable 612,618
            row.dwLocalAddr = (int)localEndPoint.Address.Address;
#pragma warning restore 612,618
            row.dwLocalPort = TranslatePort(localEndPoint.Port);
#pragma warning disable 612,618
            row.dwRemoteAddr = (int)remoteEndPoint.Address.Address;
#pragma warning restore 612,618
            row.dwRemotePort = TranslatePort(remoteEndPoint.Port);
            return SetTcpEntry(ref row) == 0;
        }


        public static TcpConnectionInfo[] GetTableByProcess(int pid)
        {
            MIB_TCPROW_OWNER_PID[] tcpRows = GetAllTcpConnections();
            if (tcpRows == null) return null;
            List<TcpConnectionInfo> list = new List<TcpConnectionInfo>();
            foreach (MIB_TCPROW_OWNER_PID row in tcpRows)
            {
                if (row.dwOwningPid == pid)
                {
                    int localPort = TranslatePort(row.dwLocalPort);
                    int remotePort = TranslatePort(row.dwRemotePort);
                    TcpConnectionInfo conn =
                        new TcpConnectionInfo(
                            new IPEndPoint(row.dwLocalAddr, localPort),
                            new IPEndPoint(row.dwRemoteAddr, remotePort),
                            row.dwState);
                    list.Add(conn);
                }
            }
            return list.ToArray();
        }

        public static TcpConnectionInfo[] GetTalbeByCurrentProcess()
        {
            return GetTableByProcess(Process.GetCurrentProcess().Id);
        }

    }
}

TcpConnectionInfo.cs

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
/**
<code>
  <revsion>$Rev: 608 $</revision>
  <owner name="Zealic" mail="rszealic(at)gmail.com" />
</code>
**/
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.NetworkInformation;


namespace Zealic.Network
{
    /// <summary>
    /// TCP 连接信息
    /// </summary>
    public sealed class TcpConnectionInfo : IEquatable<TcpConnectionInfo>, IEqualityComparer<TcpConnectionInfo>
    {
        private readonly IPEndPoint _LocalEndPoint;
        private readonly IPEndPoint _RemoteEndPoint;
        private readonly TcpState _State;

        public TcpConnectionInfo(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, TcpState state)
        {
            if (localEndPoint == null) throw new ArgumentNullException("localEndPoint");
            if (remoteEndPoint == null) throw new ArgumentNullException("remoteEndPoint");
            _LocalEndPoint = localEndPoint;
            _RemoteEndPoint = remoteEndPoint;
            _State = state;
        }

        public IPEndPoint LocalEndPoint
        {
            get { return _LocalEndPoint; }
        }

        public IPEndPoint RemoteEndPoint
        {
            get { return _RemoteEndPoint; }
        }

        public TcpState State
        {
            get { return _State; }
        }

        public bool Equals(TcpConnectionInfo x, TcpConnectionInfo y)
        {
            return (x.LocalEndPoint.Equals(y.LocalEndPoint) && x.RemoteEndPoint.Equals(y.RemoteEndPoint));
        }

        public int GetHashCode(TcpConnectionInfo obj)
        {
            return obj.LocalEndPoint.GetHashCode() ^ obj.RemoteEndPoint.GetHashCode();
        }

        public bool Equals(TcpConnectionInfo other)
        {
            return Equals(this, other);
        }

        public override bool Equals(object obj)
        {
            if (obj == null || !(obj is TcpConnectionInfo))
                return false;
            return Equals(this, (TcpConnectionInfo)obj);
        }

    }
}

至此,我们可以通过 TcpManager 类的 GetTableByProcess 方法获取进程中所有的 TCP 连接信息,然后通过 Kill 方法强制关连接以回收系统资源,虽然很C很GX,但是很有效。

通常情况下,我们可以使用 Timer 来定时检测进程中的 TCP 连接状态,确定其是否处于 CLOSE_WAIT 状态,当超过指定的次数/时间时,就把它干掉。

不过,相对这样的解决方法,我还是推荐在设计 Socket 服务端程序的时候,一定要管理所有的连接,而非上述方法。

b) 网络/硬件故障

现在我们再来看第二种情况,当网络/硬件故障时,如何应对;与上面不同,这样的情况 TCP 可能处于 ESTABLISHED、CLOSE_WAIT、FIN_WAIT 等状态中的任何一种,这时才是 Keepalive 该出马的时候。

默认情况下 Keepalive 的时间设置为两小时,如果是请求比较多的服务端程序,两小时未免太过漫长,等到它时间到,估计连黄花菜都凉了,好在我们可以通过 Socket.IOControl 方法手动设置其属性,以达到我们的目的。

关键代码如下:

1
2
3
4
5
6
7
8
// 假设 accepted 到的 Socket 为变量 client
// ...
// 设置 TCP 心跳,空闲 15 秒,每 5 秒检查一次
byte[] inOptionValues = new byte[4 * 3];
BitConverter.GetBytes((uint)1).CopyTo(inOptionValues, 0);
BitConverter.GetBytes((uint)15000).CopyTo(inOptionValues, 4);
BitConverter.GetBytes((uint)5000).CopyTo(inOptionValues, 8);
client.IOControl(IOControlCode.KeepAliveValues, inOptionValues, null);

以上代码的作用就是设置 TCP 心跳为 5 秒,当三次检测到无法与客户端连接后,将会关闭 Socket。

相信上述代码加上说明,对于有一定基础读者理解起来应该不难,今天到此为止。

c) 结束语

其实对于 Socket 程序设计来说,良好的通信协议才是稳定的保证,类似于这样的问题,如果在应用程序通信协议中加入自己的心跳包,不仅可以处理多种棘手的问题,还可以在心跳中加入自己的简单校验功能,防止包数据被 WPE 等软件篡改。但是,很多情况下这些都不是我们所能决定的,因此,才有了本文中提出的方法。

警告 :本文系 Zealic 创作,并基于 CC 3.0 共享创作许可协议 发布,如果您转载此文或使用其中的代码,请务必先阅读协议内容。

Zealic @ 2008-03-15

View Comments |
Categories: tech
Tags:

发布 EasyTrac 0.1.0.11b2

由于之前发布的 EasyTrac 0.1.0.11b1 安装时有部分问题。因此做了部分改进后重新发布 EasyTrac 0.1.0.11b2。

改进如下:

  • 修订 0.1.0.11b1 版本发现的问题,详见 EasyTrac issue list 中的 defect。
  • 为安装程序添加了一个向导页,可以自定义 EasyTrac 的数据(SVN repository & Trac project)存放的目录。

此外,强烈建议您在安装 EasyTrac 之前卸载已经安装的 Subversion 以及 Trac 避免造成冲突。

这里下载 EasyTrac,目前最新版本是 EasyTrac-0.1.0.11b2_Win32,已经较为稳定,但依旧不推荐您在生产环境中部署。

如果您有任何问题以及意见反馈,请与我联系,我将尽可能帮助你。

Zealic @ 2008-02-24

View Comments |
Categories: tech.dev
Tags: