转贴 使用C#制作《邮件特快专递》

http://itboy.cn/blog/read.asp?编号=9

源代码的下载地址(到下边的页面去下:
http://itboy.cn/product/index.asp
一、前言
  foxmail新版中有一个《邮件特快专递》的功能。起先搞不懂如何用,后来知道要在 工具->系统选项 那边设置 本地DNS服务器的IP地址。
  觉得这个新功能蛮好用的。不需要通过SMTP代理,可以直接通过本地往邮箱所在的邮件交换器发送邮件。在暑假一开始想在VC++中实现这个功能。用IRIS截包后,发现程序后 mx8.263.net发送邮箱,不知道这个是什么东西所以作罢。   后来才想到这个就是263.net的MX记录主机,原来特快专递的原理就是往这个主机上发送数据就行。
  运行nslookup程序:
  set type=mx
  263.net

  有了,有了,得到结果:
  Non-authoritative answer:
  263.net MX preference = 10, mail exchanger = mx06.263.net
  263.net MX preference = 10, mail exchanger =
mx08.263.net
  263.net MX preference = 10, mail exchanger = mx09.263.net
  263.net MX preference = 10, mail exchanger = mx11.263.net
  263.net MX preference = 10, mail exchanger = mx12.263.net
  263.net MX preference = 40, mail exchanger = mx03.263.net
  263.net MX preference = 10, mail exchanger = mx01.263.net

  没有错了。就是这个了。后来因为不知道怎么实现nslookup的功能,就放弃了,学了半个多月的C#。后来偶然在网上查找到了一些相关的文档。几次实验。把我的开发过程拿过来分享,我第一次写教程性文档。所以不规范之处,请大家包涵。本文涉及的域名、邮箱及IP均为真实的。

二、DNS协议原理
  我认为,要想成为一个好的网络软件程序员,必须得读懂RFC文档。因为本文是面向大多广泛程序爱好者,所以我尽量从细节上写,如果高手的话,可以跳过此部分。
  DNS协议的相关RFC文档:
   RFC1034-《 DOMAIN NAMES - CONCEPTS AND FACILITIES
  RFC1035-《 DOMAIN NAMES - IMPLEMENTATION AND SPECIFICATION
  网上的计算机用形如 220.162.75.1 这样称为IP地址的数字串来标识一台计算机。而如果每次访问一台计算机都是通过输入这样的东东来访问,那不就太可怕了?以是出了DNS这样的好东东,用要指示其绑定的IP地址,当我们在浏览器内输入 http://zzsy.com 时,浏览器不知道网页该到哪里取,于是就向设定好的DNS服务器查询 zzsy.com这个域名。DNS服务器会先寻找自己的记录库,如果没有发现就转向上一级DNS服务器进行查询(转发请求)。把找到后的IP告知你的浏览器。这里边浏览器查询的记录类型是A记录。RFC1035文档第11页中定义有16种记录类型,而 常见的有A(地址)记录、CNAME(别名)记录、MX(邮件交换)记录。我们本篇要关心的是MX记录。
  查询的过程一般是:客户向DNS服务器的53端口发送UDP报文,DNS服务器收到后进行处理,并把结果记录仍以UDP报文的形式返回过来。
  此UDP报文的一般格式:
 
    +---------------------+ 
    |        报文头       | 
    +---------------------+ 
    |         问题       | 向服务器提出的查询部分 
    +---------------------+ 
    |         回答       | 服务器回复的资源记录 
    +---------------------+ 
    |         授权        | 权威的资源记录 
    +---------------------+ 
    |        格外的       | 格外的资源记录 
    +---------------------+ 
  除了报文头是固定的12字节外,其他每一部分的长度均为不定字节数。
  我们在这边关心的是报文头、问题、回答这三个部分。

  其中 报文头的格式:
 
                                    1  1  1  1  1  1 
      0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 
    |                      ID                       | 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 
    |QR|   Opcode  |AA|TC|RD|RA|   Z    |   RCODE   | 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 
    |                    QDCOUNT                    | 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 
    |                    ANCOUNT                    | 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 
    |                    NSCOUNT                    | 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 
    |                    ARCOUNT                    | 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 

  好家伙,是什么鬼画符!
  其中最上边是位的数字标识,0-15(注意,后边的10-15写成上下的形式了,一开始我楞没看懂)。
  接下来是:
  ID:占16位,2个字节。此报文的编号,由客户端指定。DNS回复时带上此标识,以指示处理的对应请应请求。
  QR:占1位,1/8字节。0代表查询,1代表DNS回复
  Opcode:占4位,1/2字节。指示查询种类:0:标准查询;1:反向查询;2:服务器状态查询;3-15:未使用。
  AA:占1位,1/8字节。是否权威回复。
  TC:占1位,1/8字节。因为一个UDP报文为512字节,所以该位指示是否截掉超过的部分。
  RD:占1位,1/8字节。此位在查询中指定,回复时相同。设置为1指示服务器进行递归查询。
  RA:占1位,1/8字节。由DNS回复返回指定,说明DNS服务器是否支持递归查询。
  Z:占3位,3/8字节。保留字段,必须设置为0。
  RCODE:占4位,1/2字节。由回复时指定的返回码:0:无差错;1:格式错;2:DNS出错;3:域名不存在;4:DNS不支持这类查询;5:DNS拒绝查询;6-15:保留字段。 
  QDCOUNT:占16位,2字节。一个无符号数指示查询记录的个数。
  ANCOUNT:占16位,2字节。一个无符号数指明回复记录的个数。
  NSCOUNT:占16位,2字节。一个无符号数指明权威记录的个数。
  ARCOUNT:占16位,2字节。一个无符号数指明格外记录的个数。

  其中每个查询的资源记录格式:

 
                                    1  1  1  1  1  1 
      0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 
    |                                               | 
    /                     QNAME                     / 
    /                                               / 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 
    |                     QTYPE                     | 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 
    |                     QCLASS                    | 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

  QNAME:不定长,表示要查询的域名。(两边的方框用 / 来表示不定长)
  QTYPE:2字节,根据RFC1035及nslookup的帮助文档,我定义以下枚举类型:
enum QueryType //查询的资源记录类型。
{
A=0x01, //指定计算机 IP 地址。
NS=0x02, //指定用于命名区域的 DNS 名称服务器。
MD=0x03, //指定邮件接收站(此类型已经过时了,使用MX代替)
MF=0x04, //指定邮件中转站(此类型已经过时了,使用MX代替)
CNAME=0x05, //指定用于别名的规范名称。
SOA=0x06, //指定用于 DNS 区域的“起始授权机构”。
MB=0x07, //指定邮箱域名。
MG=0x08, //指定邮件组成员。
MR=0x09, //指定邮件重命名域名。
NULL=0x0A, //指定空的资源记录
WKS=0x0B, //描述已知服务。
PTR=0x0C, //如果查询是 IP 地址,则指定计算机名;否则指定指向其它信息的指针。
HINFO=0x0D, //指定计算机 CPU 以及操作系统类型。
MINFO=0x0E, //指定邮箱或邮件列表信息。
MX=0x0F, //指定邮件交换器。
TXT=0x10, //指定文本信息。
UINFO=0x64, //指定用户信息。
UID=0x65, //指定用户标识符。
GID=0x66, //指定组名的组标识符。
ANY=0xFF //指定所有数据类型。
};

QTYPE:2字节。 根据RFC1035及nslookup的帮助文档,我定义以下枚举类型:
enum QueryClass //指定信息的协议组。
{
IN=0x01, //指定 Internet 类别。
CSNET=0x02, //指定 CSNET 类别。(已过时)
CHAOS=0x03, //指定 Chaos 类别。
HESIOD=0x04,//指定 MIT Athena Hesiod 类别。
ANY=0xFF //指定任何以前列出的通配符。
};

  QTYPE中的A,MX,CNAME为常用,QCLASS中的IN为常用。

  其中每个回复的记录格式:

 
                                    1  1  1  1  1  1 
      0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 
    |                                               | 
    /                                               / 
    /                      NAME                     / 
    |                                               | 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 
    |                      TYPE                     | 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 
    |                     CLASS                     | 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 
    |                      TTL                      | 
    |                                               | 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 
    |                   RDLENGTH                    | 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--| 
    /                     RDATA                     / 
    /                                               / 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 
 
   NAME:回复查询的域名,不定长。
   TYPE:回复的类型。2字节,与查询同义。指示RDATA中的资源记录类型。
   CLASS:回复的类。2字节,与查询同义。指示RDATA中的资源记录类。
   TTL:生存时间。4字节,指示RDATA中的资源记录在缓存的生存时间。
   RDLENGTH:长度。2字节,指示RDATA块的长度。
   RDATA:资源记录。不定义,依TYPE的不同,此记录的格示不同,通常一个MX记录是由一个2字节的指示该邮件交换器的优先级值及不定长的邮件交换器名组成的。

  这边述说一下名称的组合形式。 名称由多个标识序列组成,每一个标识序列的首字节说明该标识符的长度,接着用是ASCII码表示字符,多个序列之后由字节0表示名字结束。其中某一个标识序列的首字符的长度若是0xC0的话,表示下一字节指示不是标识符序列,而是指示接下部分在本接收包内的偏移位置。
  比如 bbs.zzsy.com 以.分开bbs、zzsy、com三个部分。每个部分的长度为3、4、3
  在DNS报文中的形式就如 3 b b s 4 z z s y 3 c o m 0
  假如在包内的第12个字节位置存在有 4 z z s y 3 c o m 0 这样的名称了。
  那此时有可能为:3 b b s 4 z z s y 0xC0 0x0C 这样的形式。

三、DNS协议实例讲解
  说了这么多理论屁话,可能头都有两个大了吧。还是用一个实例的方法来说明吧。
  我选用著名的网络截包及协议分析工具IRIS 4.05,您可以从我的站点上下载:
   http://itboy.cn/data/Iris405Full.rar
  运行Iris,点击菜单的Filters 选 Port标签页 运用 53 端口后点确定。
  点击Iris工具栏上的绿色运行图标进行监听。

  在windows中运行nslookup程序。
  输入以下命令:
   set type=mx
  然后返回nslookup程序。
  再输入命令:
   yahoo.com.cn
  会得到
   yahoo.com.cn MX preference = 20, mail exchanger = mx5.mail.yahoo.com
   yahoo.com.cn MX preference = 10, mail exchanger = mta-v1.mail.vip.cnb.yahoo.com
  这样的两个MX资源记录。此时的Iris截包如图:


  当前的图示显示出的包是第二个报文,即从DNS回复过来的报文。第1个报文是查询报文,比回复报文简单多了。因此如果分析得懂回复报文,查询报文相信聪明如你一样可以轻松搞定。
  图中显示的红色部分的数据为DNS报文包,上边的数据内容分别为MAC协义包、PPP协议包,IPv4协议包及UDP协议包。这些包不在本文范围内,不关注。
  (插说一些废话:点选Iris左侧树中显示的条目,在右边的数据包中不一定正确反映,这点我一开始不知道,点中一个资源后按其错误的指示分析,分析了半天,也不知所言。我对包的分析是在此次写这个程序中学会的,以前大二时网络课没有好好去听我们学院那个少有的工程师兼教授的网络课,也怪我们系,咋不叫一个漂亮的MM过来教,那我会非常专心地听的:)。言归正传)
  为了说明的方便,我把包中经色的DNS协议部分处理一下,如下图:

  其中红色部分为包头,蓝色为查询部分,绿色为回复的第一条资源记录,金色为回复的第二条资源记录。其他的没有划起来的就权威记录、额外记录,不要分析考虑。

红色部分:
第0字节,第1字节 00 03 标识一个ID。
第2字节,第3字字 81 80 化成二进制形式 1000 0001 1000 0000
QR(0)为1表示是回复报文。
Opcode(2-5)为0表示标准查询。
AA(6)为0表示非权威查询。
TC(7)为0表示不超过512字节的包不截断。
RD(8)为1表示nslookup程序指示DNS进行递归查询。
RA(9)为1表示DNS支持递归查询。
Z(10-12)保留字段
RCODE(13-16)为0表示查询无查错
第4字节,第5字节 00 01 表示查询的资源记录数为1
第6字节,第7字节 00 02 表示回复的资源记录数为2
第8字节,第9字节 00 06 表示权威的资源记录数为6
第10字节,第11字节 00 04 表示额外的资源记录数为4

接着到达了蓝色部分,查询的资源记录部分:
一开始是查询的域名。
开始 05 表示后边的五个字节是序列字符,把 79 61 68 6F 6F 转为ASCII码为yahoo
到达 03 表示后边的三个字节是序列字符,把 63 6F 6D 转为ASCII码为com
到达 02 表示后边的二个字节是序列字符,把 63 6E 转为ASCII码为cn
到达 00 表示结束。
整个串起来,用.连接,即为:yahoo.com.cn
接下来的两个字节 00 0F 表示查询类型为15,即为MX记录查询。
再接下来两个字节 00 01 表示查询类为1,即为Internet连接。
Iris包中的第一个包中的DNS报文只包含红线及蓝线,分析方法相同。不再赘述。

绿色部分,回复的第一个资源记录部分:
一开始为查询的域名,
首字节C0表示压缩,接下来的位为偏移字节位。
接下来是0C表示跳到整个包中的第12个字节上,即为蓝色的第一个字节。
然后接着与上边的分析相同,得到域名为yahoo.com.cn
然后返回,到下一个字节
绿色包中的3-4字节 00 0F 表示查询类型为15,即为MX记录查询。
再接下来的5-6字节 00 01 表示查询类为1,即为Internet连接。
再接下来的7-10字节 00 00 05 95 化为十进制等于1429秒。即缓存时间为23分49秒。
接着11-12字节 00 16 指示本资源记录的数据部分为22字节。即剩下来的字节数。
从13字节开始的22字节为MX记录的数据部分。此部分的格式为两字节的邮件交换器优先级值,不定长的邮件交换器名。
第13-14字节 00 14 表示优先级值为20
接着从15开始是邮件交换器名部分,依蓝色的域名分析方法,得到mx5.mail.yahoo.com
(此段所说字节均是绿色块中的相对位置,而非整个DNS包的绝对位置)

金色那块的方析方法与绿色的大同小异,但是有一点不同的是,看金色包的邮件交换器名部分。
从包的绝对位置,第78位(图中标紫色78的那里)开始是邮件交换器名。我们进行分析,得到mta-v1.mail.vip.cnb此时接下来是0XC0表示压缩,再接下来的偏移是0x0C又跳到包的12字节上,得到yahoo.com.cn。整个合起来就是第二资源记录的邮件交换器名:mta-v1.mail.vip.cnb.yahoo.com

  这样的结果与nslookup运行结果:
   yahoo.com.cn MX preference = 20, mail exchanger = mx5.mail.yahoo.com
   yahoo.com.cn MX preference = 10, mail exchanger = mta-v1.mail.vip.cnb.yahoo.com
  相同。

  希望此节能帮助网络爱好者学会包的分析方法。下边我们现行DNS中MX记录查询的编程要点分析。

四、DNS查询MX记录编程要点分析
  foxmail 5.0 的邮件特快专递有一个很垃圾的地方是无法自动获得本地ISP的DNS服务器,还需要用户的手工输入,我想是因为API的局限或是foxmail开发组没有想到方法或是其他的不为我知的缘故吧。不管他,在C#中利用WMI,很容易的:

 
string[] dnses; 
ManagementClass mc = new ManagementClass("Win32_NetworkAdapterConfiguration"); 
ManagementObjectCollection moc = mc.GetInstances(); 

//枚举当前机子上的所有网卡 
foreach(ManagementObject mo in moc) 
{ 
    if((bool)mo["ipEnabled"]) 
    { 
        dnses = (string[]) mo["DNSServerSearchOrder"]; 
        if (dnses!=null) 
        { 
            dnsServer=dnses[0];    //使用第一个找到的DNS服务器。 
        } 
    } 
}
  当然,您不会忘记在 项目->添加引用 中加入对System.Management的引用吧?
  把这个放在DnsQuery类的静态构造函数中。

  还有一个自定义的MxRecord结构,用于存放一个MX资源记录的信息。
 
    struct MxRecord 
    { 
        public string domain;    //查询的域名 
        public QueryType queryType;    //查询类型 
        public QueryClass queryClass;    //查询类 
        public TimeSpan liveTime;    //生存时间 
        public int dataLength;    //资源部分的长度,即指示邮件服务器的优先级及名称的那部分资源的字节数。 
        public int preference;    //优先级值,其值越小越优先. 
        public string name;    //邮件交换器名 
    } 
  把两个字节组成一个INT16型的整数无法用到BitConverter类,因为与DNS服务器的端位法不同。所以来是用 << 的方法进行位移。
  其他的就不多说了,在程序的DnsQuery类中,我做了详细的代码注释,如果您有一些开发经验的话,应该很容易看得懂的,如果有疑问的话,欢迎联系我。我在精力及能力许可的范围内帮您解答。 

五、分析Foxmail的特快专递发送数据。

  1.运行ipconfig/all
  把得到的Dns Servers的第一个IP地址记录下来。

  2.还是运行Iris.设置Filters为SMTP,25端口。运行以便监听。

  3.运行Foxmail5.0,在 工具 -> 系统设置 内的“邮件特快专递”标签页设定域名服务器1为刚刚的IP地址。
  点“撰写”,用文本文件的格式编写。
  收件人: dreamchild@263.net
  抄送:
  主题: 你好dreamchild,冒昧打饶
  内容:
   尊敬的dreamchild先生:
    这是一封邮件。
  附件:(添加 mm.gif 及 说明.txt 两个文件。为了保证此教程的完整性,把这两个额外的东东也放进包内了。)

  4.完成后停止Iris,点其菜单栏的Decode 得到以下内容:
 
220 Welcome to coremail System(With Anti-Spam) 2.1 for 263(040326) 
HELO dreamchild 
250 mta6.x263.net 
MAIL FROM: <> 
250 Ok 
RCPT TO: 
   
   
    
     
250 Ok 
Data 
354 End data with 
    
    
     
     
      
      .
      
      
       
       
        
         
Date: Thu, 9 Sep 2004 01:00:35 +0800 
From: "=?gb2312?B?w87Qobqi?=" <
        
        > 
To: "dreamchild" 
        
        
          Subject: =?gb2312?B?xOO6w2RyZWFtY2hpbGSjrMOww8G08sjE?= X-mailer: Foxmail 5.0 [cn] Mime-Version: 1.0 Content-Type: multipart/mixed; boundary="=====001_Dragon788446150325_=====" This is a multi-part message in MIME format. --=====001_Dragon788446150325_===== Content-Type: text/plain; charset="gb2312" Content-Transfer-Encoding: base64 1/C+tLXEZHJlYW1jaGlsZM/Iyfqjug0KoaGhodXiysfSu7fi08q8/qGjDQo= --=====001_Dragon788446150325_===== Content-Type: image/gif; name="MM.GIF" Content-Transfer-Encoding: base64 Content-Disposition: attachment; filename="MM.GIF" R0lGODlheAB4AIddABwNCixIjJtjkLO8iZux0NK2xpVJT9XayJWNuDA0Vt5Xh+Sw7dLc7p6Fh3mD (这边省略去关于一陀 MM.gif 文件内容的Base64编码) w75UTdHFHwD/1Ex6cBSs0jHpKaySFXI4XCwAEhPkRsUSBQQAOw== --=====001_Dragon788446150325_===== Content-Type: application/octet-stream; name="=?gb2312?B?y7XD9y50eHQ=?=" Content-Transfer-Encoding: base64 Content-Disposition: attachment; filename="=?gb2312?B?y7XD9y50eHQ=?=" 1eK49k1Nv8mwrrK7v8mwrqGjDQo= --=====001_Dragon788446150325_=====-- . 250 Ok: queued as 7CDA613A26E QUIT 221 Bye 
        
       
       
      
      
     
     
    
    
   
   

  这边涉及到的就是SMTP协议了,其中文版的RFC821文档 http://itboy.cn/data/rfc821.doc
  因为是中文版的,所以大家花些时间看,这边就不再赘述原理了。只列出状态标识
   211 系统状态或系统帮助响应
   214 帮助信息
   220 服务就绪
    221 服务关闭传输信道
    250 要求的邮件操作完成
    251 用户非本地,将转发向
    354 开始邮件输入,以 . 结束
    421 服务未就绪,关闭传输信道(当必须关闭时,此应答可以作为对任何命令的响应)
    450 要求的邮件操作未完成,邮箱不可用(例如,邮箱忙)
    451 放弃要求的操作;处理过程中出错
    452 系统存储不足,要求的操作未执行
    500 格式错误,命令不可识别(此错误也包括命令行过长)
    501 参数格式错误
    502 命令不可实现
    503 错误的命令序列
    504 命令参数不可实现
    550 要求的邮件操作未完成,邮箱不可用(例如,邮箱未找到,或不可访问)
    551 用户非本地,请尝试
    552 过量的存储分配,要求的操作未执行
    553 邮箱名不可用,要求的操作未执行(例如邮箱格式错误)
    554 操作失败

  可以从此看出,与一般的通过SMTP代理不同的是少了SMTP服务器的指定及其验证的用户名跟密码。
  描述一下整个过程:
  首先通过前述的方法得到263.net的一个邮件交换器,然后连到这个交换器上。然后连到此服务器的25端口上,
  服务器返回220。
  然后依次指示用户名,发送邮箱(人),接收邮箱(人)。接收写入邮件的数据。
  数据分为邮件头及邮件的正文两部分。
  邮件头包含:时间,发送邮箱(人),接收邮箱(人),主题,发信程序,MIME版本号,邮件内容的类型及分割符。
  当中有一些用BASE64编码的字符串就是原来的中文汉字,其实,我们在制作无SMTP代理邮件发送程序时可以直接写成中文的。
  这边就讲一下邮件内容的类型及分割符,其他的很容易理解的。
  这边的邮件内容类型是 multipart/mixed; 说明是由多种格式混合成的。
  分隔符,是用于分隔邮件内容部分与各个附件。用 boundary关键字及键值来定义。
  比如本例用 =====001_Dragon788446150325_=====来表示,这边有一个细节问题,键值最好要用"引起来,并不要出现空格。举个例子,如果你用 boundary======001_Dragon788446150325_=====来表示的话,那FOXMAIl5.0将无法正确对邮件进行处理,邮件的内容部分被当成整个BASE64乱码文本,然而我登陆到263.net的网站去收信可以看到邮件被正常转化。
  而邮件的内容部分是通过两个减号 --再连上分隔符来分隔各部分的。

  邮件主体从第一个 --=====001_Dragon788446150325_=====开始,到第二个 --=====001_Dragon788446150325_=====为内容的第一部分
   Content-Type: text/plain;
  charset="gb2312" Content-Transfer-Encoding: base64

  这两句说明了其类型及内容的字符集和编码。
  在这边是指定的是base64,然后一个空行,再加上“尊敬的dreamchild先生:/r/n    这是一封邮件。”这个字符串的BASE64编码构成邮件的正文部分。
  实际上,我们可以指定 Content-Transfer-Encoding:8bit然后就可以在正文部分用上原本表示了。

  接下来是隔开的附件1部分,
  多了一个 Content-Disposition: attachment;以说明这部分是附件,以及相关的文件名 filename="MM.GIF"。
  附件内容部分是把文件读成一个字节数组,然后把字节数组转为base64编码的字符串。这边的是 mm.gif这个文件内容。

  第三部分是附件2 测试.txt 文件, 测试.txt 又被foxmail处理成base64格式了,可以用原文表示的。
  最后完了之后,用“回车换行加一个.号再一个回车换行”表示Data部分的结束。

  如若正确过发送到达服务器,那就返回一个 250状态。
  然后用 Quit命令跟服务器3166

六、邮件发送程序编程要点分析
  我们先定义一个邮件结构,以描述邮件的各个属性。
 
    public struct MailContent    //邮件的内容 
    { 
        public string to;//收件人地址  
        public string toname;//收件人姓名  
        public string from;//发件人地址  
        public string fromname;//发件人姓名 

        public string title;//主题  
        public string body;//文本内容  

        public bool useAttachment; //是否使用附件 
        public string [] attachmentList; //附件列表  
    } 
      
  接着从各控件中取值,赋给这个结构的实例。
  其中取值过程中包括判断邮箱格式是否正确,我们用正则表达式来判断。如下:
  Regex.IsMatch(邮箱字符串, @"^([/w-/.]+)@((/[[0-9]{1,3}/.[0-9]{1,3}/.[0-9]{1,3}/.)|(([/w-]+/.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(/]?)$")
  如果返回的值为假的话,就用错误提示器提示。
  赋值完后,就开创一个工作线程进行邮件的发送。
  线程一开始先通过当地DNS取得邮箱域名部分的邮件交换器列表。然后取得其最小优先级值的交换器用于以后的发送。 比如取得 dreamchild@263.net这个邮箱的域名 263.net的一个优先级值为10的MX记录 mx12.263.net
  然后就是连到服务器发送命令及接收返回信息。以下列出一些代码:

  关于创建Sock套接字:
 
    TcpClient sock = new TcpClient(); 
    NetworkStream netStream;         
     
    sock.NoDelay = true;    //不使用延时算法,以加快小数据包的发送。 
    sock.ReceiveTimeout = 10000; //接收超时为10秒。 
    sock.Connect(server,port); 
    netStream = sock.GetStream(); 
  Socket的Nagle算法将降低小数据报的发送速度,而系统默认是使用Nagle算法。所以设置NoDelay为true关掉它,以加快小数据的发送。

  关于Send方法:
 
    byte [] sendArray = Encoding.Default.GetBytes(sendString);         
    netStream.Write(sendArray,0,sendArray.Length); 
  先把字符串转为字节数组再发送出去

  关于Receive方法:
 
    const int MaxReceiveSize = 1460; 
    .... 
    byte [] buffer=new byte[MaxReceiveSize]; 
         
    length = netStream.Read(buffer, 0, MaxReceiveSize); 
    if (length == 0) 
        return null; 
    ....     
    receiveString = Encoding.Default.GetString(buffer, 0, length); 
  根据“在SOCK_STREAM方式下,如果单次发送数据超过1460,系统将分成多个数据报传送,在对方接受到的将是一个数据流,应用程序需要增加断帧的判断。当然可以采用修改注册表的方式改变1460的大小,但MicrcoSoft认为1460是最佳效率的参数,不建议修改。”
  这边设置一次接收1460字节,但其实返回码用不了这么多字节,这个是以前我在写基于HTTP网络程序时写的接收函数一部分,保留了这个方法。
  接收到后就再转回字符串返回。
 
    send = "Date: {$time}/r/n" 
    + "From: {$fromname} <{$from}>/r/n" 
    + "To: {$toname} <{$to}>/r/n" 
    + "Subject: {$title}/r/n" 
    + "X-mailer: NoSmtpSender [蔡晓晖 制作]/r/n" 
    + "MIME_Version:1.0/r/n" 
    + "Content-type:multipart/mixed;Boundary=/"{$splitline}/"/r/n" 
    + "/r/n" //头部结束,开始正文部分。  
    + "--{$splitline}/r/n" //内容部分。 
    + "Content-type:text/plain;Charset=gb2312/r/n" 
    + "Content-Transfer-Encoding:8bit/r/n" 
    + "/r/n"  
    + "{$body}/r/n" 
    + "/r/n"; 
     
    attachment = ""; 
    attachment += "--{$splitline}/r/n"; 
    attachment += "Content-Type:application/octet-stream;Name={$filename}/r/n"; 
    attachment += "Content-Disposition:attachment;FileName={$filename}/r/n"; 
    attachment += "Content-Transfer-Encoding:Base64/r/n"; 
    attachment += "/r/n"; 
    attachment = attachment.Replace("{$splitline}",splitLine); //先进行替换,以防加上附件内容后替换耗时。 
    attachment = attachment.Replace("{$filename}",file.Name); 
    attachment += Convert.ToBase64String(fileBytes,0,length); 
    attachment += "/r/n/r/n"; 
    send += attachment;  
  按上一节的分析,把格式给列出来,然后依次替换{$变量名}就OK了,其中time变量名的获得方法:
   DateTime.Now.ToString("R").Replace("GMT","中国标准时间")
  我不知道是不是VS2003的BUG,用"R"参数输出当前时间的RFC格式的格林威治时间,没有把本土化时间自动转换,比如现在的中国时间9点,它没有减去8小时。而只是直接在当前时间后边加上“GMT”。于是我们就把“GMT”改“中国标准时间”了以校时。

  按一问一答的方式与邮件交换器“交流”,如果其间有错的话,就把错误的信息保存下来,然后 return false;
  如果整个发送过程没有错的话就可以
   netStream.Close();
   sock.Close();
  结束。

  程序的界面截图:



七、最后的话
  非常感谢您很有耐心地看完我的啰嗦。我花了四个通宵(其实是早上睡觉)时间编写软件及教程的辛苦值得了。这份软件算我暑假学习半个月C#后的一份比较好的成品吧。我是从大二开始学习真正的编程,用了九个月的VB及写了一年又三个月的C++/MFC程序。现在在学C#。感觉有C++的基础入门C#挺容易的。
  本文件的邮件附件的发送方法参照罗前辈( http://www.luocong.com)的C++源码,不然可能又要多发上一天去思考了。在此感谢罗老师的无私。
  写完这个东东后,再过四天,我的暑假就结束了,下个学期是大四了。很迷惘的一个学年,我想在毕业后能到上海找到一份好工作。我想请业内的前辈指引我一下,以我现在的水平要跨过上海大软件公司的门槛还要爬多高?我在这剩下一年内会更加努力去超越的的。

您可以任意地传播本软件、源码及相关文档,但请保留完整性。
如若用于商业用途需经我的同意。
我的联系方式:
通讯地址:漳州师范学院082信箱(363000)
姓名:蔡晓晖
电邮:
QQ:22415
希望我们能交个朋友。

如果一年内有问题需要我解答的话,请到http://bbs.zzsy.com上的【信息技术】版块发贴询问。
我会在精力及能力许可范围内帮您解答的。
Programmed by 蔡晓晖
2004.9.7 - 2004.9.9


[网络编程]用C#下的Raw Socket编程实现网络封包监视
2004-9-3


谈起socket编程,大家也许会想起QQ和IE,没错。还有许多网络工具如P2P、NetMeeting等在应用层实现的应用程序,也是用socket来实现的。Socket是一个网络编程接口,实现于网络应用层,Windows Socket包括了一套系统组件,充分利用了Microsoft Windows 消息驱动的特点。Socket规范1.1版是在1993年1月发行的,并广泛用于此后出现的Windows9x操作系统中。Socket规范2.2版(其在Windows平台上的版本是Winsock2.2,也叫Winsock2)在 1996 年 5 月发行,Windows NT 5.0及以后版本的Windows系统支持Winsock2,在Winsock2中,支持多个传输协议的原始套接字,重叠I/O模型、服务质量控制等。

本文向大家介绍Windows Sockets的一些关于用C#实现的原始套接字(Raw Socket)的编程,以及在此基础上实现的网络封包监视技术。同Winsock1相比,Winsock2最明显的就是支持了Raw Socket套接字类型,使用Raw Socket,可把网卡设置成混杂模式,在这种模式下,我们可以收到网络上的IP包,当然包括目的不是本机的IP包,通过原始套接字,我们也可以更加自如地控制Windows下的多种协议,而且能够对网络底层的传输机制进行控制。

在本文例子中,我在nbyte.BasicClass命名空间实现了RawSocket类,它包含了我们实现数据包监视的核心技术。在实现这个类之前,需要先写一个IP头结构,来暂时存放一些有关网络封包的信息:


[StructLayout(LayoutKind.Explicit)]
 public struct IPHeader
 {
  [FieldOffset(0)] public byte    ip_verlen;        //I4位首部长度+4位IP版本号
  [FieldOffset(1)] public byte    ip_tos;            //8位服务类型TOS
  [FieldOffset(2)] public ushort  ip_totallength; //16位数据包总长度(字节)
  [FieldOffset(4)] public ushort  ip_id;             //16位标识
  [FieldOffset(6)] public ushort  ip_offset;       //3位标志位
  [FieldOffset(8)] public byte    ip_ttl;            //8位生存时间 TTL
  [FieldOffset(9)] public byte    ip_protocol;    //8位协议(TCP, UDP, ICMP, Etc.)
  [FieldOffset(10)] public ushort ip_checksum; //16位IP首部校验和
  [FieldOffset(12)] public uint   ip_srcaddr;     //32位源IP地址
  [FieldOffset(16)] public uint   ip_destaddr;   //32位目的IP地址
 }

这样,当每一个封包到达时候,可以用强制类型转化把包中的数据流转化为一个个IPHeader对象。
下面就开始写RawSocket类了,一开始,先定义几个参数,包括:
  private bool error_occurred;          //套接字在接收包时是否产生错误
  public bool KeepRunning;              //是否继续进行
  private static int len_receive_buf; //得到的数据流的长度
  byte [] receive_buf_bytes;          //收到的字节
  private Socket socket = null;       //声明套接字
还有一个常量:
const int SIO_RCVALL = unchecked((int)0x98000001);//监听所有的数据包

这里的SIO_RCVALL是指示RawSocket接收所有的数据包,在以后的IOContrl函数中要用,在下面的构造函数中,实现了对一些变量参数的初始化:

  public RawSocket()                    //构造函数
  {
   error_occurred=false;
   len_receive_buf = 4096;
   receive_buf_bytes = new byte[len_receive_buf];
  }

下面的函数实现了创建RawSocket,并把它与终结点(IPEndPoint:本机IP和端口)绑定:
  public void CreateAndBindSocket(string IP)                  //建立并绑定套接字
  {
   socket = new Socket(AddressFamily.InterNetwork, SocketType.Raw, ProtocolType.IP);
   socket.Blocking = false;                                         //置socket非阻塞状态
   socket.Bind(new IPEndPoint(IPAddress.Parse(IP), 0)); //绑定套接字

   if (SetSocketOption()==false) error_occurred=true;
  }
其中,在创建套接字的一句socket = new Socket(AddressFamily.InterNetwork, SocketType.Raw, ProtocolType.IP);中有3个参数:

第一个参数是设定地址族,MSDN上的描述是“指定 Socket 实例用来解析地址的寻址方案”,当要把套接字绑定到终结点(IPEndPoint)时,需要使用InterNetwork成员,即采用IP版本4的地址格式,这也是当今大多数套接字编程所采用一个寻址方案(AddressFamily)。

第二个参数设置的套接字类型就是我们使用的Raw类型了,SocketType是一个枚举数据类型,Raw套接字类型支持对基础传输协议的访问。通过使用 SocketType.Raw,你不光可以使用传输控制协议(Tcp)和用户数据报协议(Udp)进行通信,也可以使用网际消息控制协议 (Icmp) 和 Internet 组管理协议 (Igmp) 来进行通信。在发送时,您的应用程序必须提供完整的 IP 标头。所接收的数据报在返回时会保持其 IP 标头和选项不变。

第三个参数设置协议类型,Socket 类使用 ProtocolType 枚举数据类型向 Windows Socket API 通知所请求的协议。这里使用的是IP协议,所以要采用ProtocolType.IP参数。

在CreateAndBindSocket函数中有一个自定义的SetSocketOption函数,它和Socket类中的SetSocketOption不同,我们在这里定义的是具有IO控制功能的SetSocketOption,它的定义如下:

  private bool SetSocketOption()                           //设置raw socket
  {
   bool ret_value = true;
   try
   {
    socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.HeaderIncluded, 1);
    byte []IN = new byte[4]{1, 0, 0, 0};
    byte []OUT = new byte[4];
 
    //低级别操作模式,接受所有的数据包,这一步是关键,必须把socket设成raw和IP Level才可用SIO_RCVALL
    int ret_code = socket.IOControl(SIO_RCVALL, IN, OUT);
    ret_code = OUT[0] + OUT[1] + OUT[2] + OUT[3];//把4个8位字节合成一个32位整数
    if(ret_code != 0) ret_value = false;
   }
   catch(SocketException)
   {
    ret_value = false;
   }
   return ret_value;
  }

其中,设置套接字选项时必须使套接字包含IP包头,否则无法填充IPHeader结构,也无法获得数据包信息。
int ret_code = socket.IOControl(SIO_RCVALL, IN, OUT);是函数中最关键的一步了,因为,在windows中我们不能用Receive函数来接收raw socket上的数据,这是因为,所有的IP包都是先递交给系统核心,然后再传输到用户程序,当发送一个raws socket包的时候(比如syn),核心并不知道,也没有这个数据被发送或者连接建立的记录,因此,当远端主机回应的时候,系统核心就把这些包都全部丢掉,从而到不了应用程序上。所以,就不能简单地使用接收函数来接收这些数据报。要达到接收数据的目的,就必须采用嗅探,接收所有通过的数据包,然后进行筛选,留下符合我们需要的。可以通过设置SIO_RCVALL,表示接收所有网络上的数据包。接下来介绍一下IOControl函数。MSDN解释它说是设置套接字为低级别操作模式,怎么低级别操作法?其实这个函数与API中的WSAIoctl函数很相似。WSAIoctl函数定义如下:

int WSAIoctl(
  SOCKET s,                                                                          //一个指定的套接字
  DWORD dwIoControlCode,                                                      //控制操作码
  LPVOID lpvInBuffer,                                                              //指向输入数据流的指针
  DWORD cbInBuffer,                                                              //输入数据流的大小(字节数)
  LPVOID lpvOutBuffer,                                                           // 指向输出数据流的指针
  DWORD cbOutBuffer,                                                           //输出数据流的大小(字节数)
  LPDWORD lpcbBytesReturned,                                               //指向输出字节流数目的实数值
  LPWSAOVERLAPPED lpOverlapped,                                         //指向一个WSAOVERLAPPED结构
  LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine//指向操作完成时执行的例程
);

C#的IOControl函数不像WSAIoctl函数那么复杂,其中只包括其中的控制操作码、输入字节流、输出字节流三个参数,不过这三个参数已经足够了。我们看到函数中定义了一个字节数组:byte []IN = new byte[4]{1, 0, 0, 0}实际上它是一个值为1的DWORD或是Int32,同样byte []OUT = new byte[4];也是,它整和了一个int,作为WSAIoctl函数中参数lpcbBytesReturned指向的值。
因为设置套接字选项时可能会发生错误,需要用一个值传递错误标志:

  public bool ErrorOccurred
  {
   get
   {
    return error_occurred;
   }
  }

下面的函数实现的数据包的接收:

  //解析接收的数据包,形成PacketArrivedEventArgs事件数据类对象,并引发PacketArrival事件
  unsafe private void Receive(byte [] buf, int len)
  {
   byte temp_protocol=0;
   uint temp_version=0;
   uint temp_ip_srcaddr=0;
   uint temp_ip_destaddr=0;
   short temp_srcport=0;
   short temp_dstport=0;
   IPAddress temp_ip;
  
   PacketArrivedEventArgs e=new PacketArrivedEventArgs();//新网络数据包信息事件

   fixed(byte *fixed_buf = buf)
   {
    IPHeader * head = (IPHeader *) fixed_buf;//把数据流整和为IPHeader结构
    e.HeaderLength=(uint)(head->ip_verlen & 0x0F) << 2;
   
    temp_protocol = head->ip_protocol;
    switch(temp_protocol)//提取协议类型
    {
     case 1: e.Protocol="ICMP";     break;
     case 2: e.Protocol="IGMP";     break;
     case 6: e.Protocol="TCP";      break;
     case 17: e.Protocol="UDP";     break;
     default: e.Protocol= "UNKNOWN"; break;
    }

    temp_version =(uint)(head->ip_verlen & 0xF0) >> 4;//提取IP协议版本
    e.IPVersion = temp_version.ToString();

                   //以下语句提取出了PacketArrivedEventArgs对象中的其他参数
    temp_ip_srcaddr = head->ip_srcaddr;
    temp_ip_destaddr = head->ip_destaddr;
    temp_ip = new IPAddress(temp_ip_srcaddr);
    e.OriginationAddress =temp_ip.ToString();
    temp_ip = new IPAddress(temp_ip_destaddr);
    e.DestinationAddress = temp_ip.ToString();

    temp_srcport = *(short *)&fixed_buf[e.HeaderLength];
    temp_dstport = *(short *)&fixed_buf[e.HeaderLength+2];
    e.OriginationPort=IPAddress.NetworkToHostOrder(temp_srcport).ToString();
    e.DestinationPort=IPAddress.NetworkToHostOrder(temp_dstport).ToString();

    e.PacketLength =(uint)len;
    e.MessageLength =(uint)len - e.HeaderLength;

    e.ReceiveBuffer=buf;
    //把buf中的IP头赋给PacketArrivedEventArgs中的IPHeaderBuffer
    Array.Copy(buf,0,e.IPHeaderBuffer,0,(int)e.HeaderLength);
    //把buf中的包中内容赋给PacketArrivedEventArgs中的MessageBuffer
    Array.Copy(buf,(int)e.HeaderLength,e.MessageBuffer,0,(int)e.MessageLength);
   }
   //引发PacketArrival事件
   OnPacketArrival(e);
  }

大家注意到了,在上面的函数中,我们使用了指针这种所谓的不安全代码,可见在C#中指针和移位运算这些原始操作也可以给程序员带来编程上的便利。在函数中声明PacketArrivedEventArgs类对象,以便通过OnPacketArrival(e)函数通过事件把数据包信息传递出去。其中PacketArrivedEventArgs类是RawSocket类中的嵌套类,它继承了系统事件(Event)类,封装了数据包的IP、端口、协议等其他数据包头中包含的信息。在启动接收数据包的函数中,我们使用了异步操作的方法,以下函数开启了异步监听的接口:

public void Run() //开始监听
{
 IAsyncResult ar = socket.BeginReceive(receive_buf_bytes, 0, len_receive_buf, SocketFlags.None, new AsyncCallback(CallReceive), this);
}

Socket.BeginReceive函数返回了一个异步操作的接口,并在此接口的生成函数BeginReceive中声明了异步回调函数CallReceive,并把接收到的网络数据流传给receive_buf_bytes,这样就可用一个带有异步操作的接口参数的异步回调函数不断地接收数据包:

  private void CallReceive(IAsyncResult ar)//异步回调
  {
   int received_bytes;
   received_bytes = socket.EndReceive(ar);
   Receive(receive_buf_bytes, received_bytes);
   if (KeepRunning) Run();
  }

此函数当挂起或结束异步读取后去接收一个新的数据包,这样能保证让每一个数据包都能够被程序探测到。
下面通过声明代理事件句柄来实现和外界的通信:

public delegate void PacketArrivedEventHandler(Object sender, PacketArrivedEventArgs args);
//事件句柄:包到达时引发事件
public event PacketArrivedEventHandler PacketArrival;//声明时间句柄函数

这样就可以实现对数据包信息的获取,采用异步回调函数,可以提高接收数据包的效率,并通过代理事件把封包信息传递到外界。既然能把所有的封包信息传递出去,就可以实现对数据包的分析了:)不过RawSocket的任务还没有完,最后不要望了关闭套接字啊:

  public void Shutdown()                                       //关闭raw socket
  {
   if(socket != null)
   {
    socket.Shutdown(SocketShutdown.Both);
    socket.Close();
   }
  }

以上介绍了RawSocket类通过构造IP头获取了包中的信息,并通过异步回调函数实现了数据包的接收,并使用时间代理句柄和自定义的数据包信息事件类把数据包信息发送出去,从而实现了网络数据包的监视,这样我们就可以在外部添加一些函数对数据包进行分析了。












[网络编程]MX记录获取组件的编写
2004-9-4


一、MX记录的应用
Email是迄今为止互联网上最成功的应用了,试想一个触网者如果没有属于自己的Email邮箱,那将难以称作“网民”。互联网网上同Email相关的应用也增多。我们编写网络应用时,经常需要涉及将Email应用嵌入到自己的应用程序中。这种应用往往是将第三方的Email相关组件拿来使用,完成Email的撰写、发送、收取、解码。在发送Email的过程中,NT平台一般采用NT系统自带的CDO组件来完成,而这类组件往往依靠在本机的服务或一个特定的SMTP服务器来实现,不能够确保信件是否到达,而且由于存在中继延迟,还有丢失信件的可能。所以很多高手采用直接向目的邮箱的服务器投递Email的办法来实现Email发送,而实现此功能关键在于三个要点:
1、邮件的MIME编码
2、SMTP会话控制
3、MX记录的获取。
通过我的了解,互联网上关于1、2两点的文章很多,相应的参考代码很多,而对于如何定位目标邮箱接收服务器-获得邮箱域名的MX记录却很少详细讨论,搜索相关的组件得到一个ASP环境的服务器组件(收费),没有详细的源码可以参考。基于此,我通过学习Java语言的机会编写了一个类似的组件,帮助大家了解这方面的细节。
 
二、DNS记录分类以及原理
我们知道域名系统是互联网的基石,我们能够通过亲切的类似yahoo.com这样的名字来访问自己感兴趣的网站完全是域名系统的功劳。当您在浏览器敲下www.yahoo.com时,您的浏览器是不知道这个域名指向的网页该往哪儿去取,便会询问设定好的域名解析服务器(DNS),域名解析服务器会寻找自己的记录库是否存在请求的域名记录,如果存在会直接返回,不存在就向上一级域名解析服务器请求(转发请求),直到最后找到此域名的解析记录返回给您的浏览器,告诉您的浏览器该到哪一个IP地址上去获得相关的内容。这样一个将互联网域名转换为对应的IP的过程叫做解析。
域名记录有多种,常见的有A(地址)记录、别名(CNAME)记录、MX(邮件交换)记录。A记录就是一个域名对应一个IP的记录,刚才提到的www.yahoo.com对应某个IP地址即是A记录的查询过程。别名记录主要用于用一个已经存在A记录的域名替代需要解析的域名,譬如www.yahoo.com查询时会得到www.yahoo.akadns.net 的别名值,而进一步解析www.yahoo.akadns.net会得到IP地址64.58.76.177。邮件交换记录是用于邮件投递时采用的。譬如您需要发送一封到 abc@yahoo.com的Email,那么一般情况下,您的ISP的SMTP服务器(专业术语成为邮件代理,即代替用户直接投递邮件)会接受您发出的信件,然后SMTP服务器查询yahoo.com的MX记录,看应当将邮件发送到哪一个服务器上去,如果查询到yahoo.com的MX记录,那么您的ISP的SMTP服务器会建立到MX记录指定的服务器的TCP连接,并传出您的Email到目的服务器上去,完成Email的投递过程。
如果能够获得某个域名的MX记录,即获得了向目的邮箱直接投递的途径,而要获得这个MX记录,您必须能够正确的向域名服务器DNS提出您的请求,同时也要能够理解DNS返回的信息,读出自己需要的内容。这里面的细节相对复杂,我这里简单介绍一下:
客户向DNS的53端口发出的UDP报文,然后服务器查询(中间可能经过对此转发或者称作递归查询)后发回客户机需要的记录,也是UDP报文。此类报文的一般格式为:
| 2字节的标识 | 2字节的标志 | 2字节的问题个数 | 2字节的资源记录数 | 2字节的授权资源记录个数 | 2字节额外的资源记录数 | 查询的域名(不固定长度) | 针对请求的应答资源记录(长度不固定)| 授权资源记录(长度不固定) | 额外记录信息(长度不固定)

标识字段用于指出报文的编号,一般由客户指定,DNS服务器返回信息时带上此标识,告诉客户端回答的是哪一个请求。

标志字段的16比特划分为8个子字段,从左至右(高位到低位)分别为:
QR 1 bit :0 查询报文 1 响应报文
Opcode 4 bit :通常为0,表示标准查询 ,1 反向查询,2 服务器状态查询
AA 1 bit :用于服务器返回报文,表示是否是授权回答
TC 1 bit :由于UDP自身长度限制,往往会截断512字节后的内容,该位表示是否可截断
RD 1 bit :该为用于在查询报文中设置,并由服务器响应报文中返回。该位告诉服务器必须处理此查询,如果该位为0,且服务器返回的授权回答个数为0,那么服务器必须返回一个能够解答该查询的其他服务器的列表
RA 1 bit :如果服务器支持递归,那么服务器在响应报文中设定该位。
随后的3bit必须为0
rcode 4 bit :最后为返回码,0 无差错,3 名字差错,即在服务器上不存在要查询的域名的记录,一般用于从最终的授权名字服务器返回。

查询问题部分由查询名字 查询类型 查询类组成。查询名字由多个标识符的序列组成,每一个标识首字节说明该标识符的长度,最终由字节0表示名字结束。譬如cn..yahoo.com由2 c n 5 y a h o o 3 c o m 0组成。如果此域名后面还用到,一般在后面采用压缩格式,那么首字节不是长度了,而是一个最高位为1的字节,一般是0xc0,因为不会出现长度超过64的标识符。压缩格式的标志字节后是该域名的原标识的偏移值。查询类型为2字节,1 表示A记录查询 5 表示CNAME记录查询 15 表示MX记录查询。类表示是否是Internet数据。

应答报文中的应答记录由域名(长度不固定) 类型(2字节) 类(2字节) 生存时间(4字节,秒数) 资源数据长度(2字节) 资源数据(不固定)。域名的格式同查询域名格式相同。类型、类的解释同查询问题部分。资源数据根据记录类型不同而不同。
如果我们按照上面的格式同时结合自己的需求,向有域名解析功能的服务器发出UDP报问候就可以收到DNS服务器发回的应答,这样我们就可以获得想的记录。所以,主要难点在保文的构造和应答报文的分析。
 
三、编写MX记录组件
启动VJ。要求建立一个DLL工程,然后修改主类名为自己想要的名字,该工程建立完毕。如果一切无误的话,就会得到一个DLL,具有域名MX记录查询功能。具体代码见后面的源代码分析。关键就是结合自己的查询问题构造查询报文和解释得到的应答报文,同服务器的网络连接方式是UDP方式。
为了调试方便,我添加了一个Main函数入口,这样可以在DOS窗口调用jvew来察看域名MX记录的结果,同时也方便了调试。
 
四、应用举例及扩充
有了这样一个查询组件,可以直接获得域名的MX记录,可以帮助我们获得投递Email的方法。同时,我们也知道了该域名邮件接收服务器的位置,通过试探不同得用户名,我们还可以获得该域名的邮局存在那些邮箱用户名。你可能奇怪自己的邮箱如何被他人获得,很可能被一些Email搜索器通过查询MX记录,得到邮件服务器的地址,然后通过特别的用户名产生算法得到不同的用户名,一一试探从而得到邮箱。在你的应用程序中加入此功能,可以直接将邮件传递到目标务器,不依赖于服务器上的组件。甚至您可以编写邮件转发服务器,服务于您的用户(一般的发件服务器不对外开放的)。
 
五、源码及分析

  Code:[复制到剪贴板]  
import java.net.*; 
import java.io.*; 
import java.util.StringTokenizer;  
//import java.util.*;  
 
/** 
* This class is designed to be packaged with a COM DLL output format. 
* The class has no standard entry points, other than the constructor. 
* Public methods will be exposed as methods on the default COM interface
* @com.register ( clsid=20AE856F-E2F8-488E-B41E-753E6BEBD375, typelib=36611559-AFAB-479E-8572-9A0B1F06CCDA ) 
*/ 
public class MXDNS 

    private String theDnsServer; //当前DNS服务器的地址 
    private DatagramPacket outPk; //发送数据包 
    private DatagramPacket inPk; //接收数据包 
    private DatagramSocket UDPSocket ; //UDP陶接字 
    private InetAddress DNSServerIP; //域名解析服务器的IP 
    private int position,id,length;//分析的DNS记录时需要的变量 
    private String DMname; //查询的域名 
    private MXRec mxrecs; //DNS应答的记录 
    private static int DNS_PORT=53; //DNS服务的端口 
    private byte[] pkdata=new byte[512] ; //得到512字节的数据包 
 
    public MXDNS() //构造函数 
    { 
        id=(new java.util.Date()).getSeconds()* 60 * (new java.util.Random()).nextInt(); 
        //获得唯一ID 
    } 
    //以下为暴露的属性和方法 
 
    public void setDnsServer(String dnsserver) 
    { 
        theDnsServer=dnsserver; //设定当前的DNS服务器的名字或者IP 
    } 
 
    public String getMXRecords(String dm) //获得所有DNSMX记录数组 
    { 
        return(getMXRecords(dm,theDnsServer)); 
    } 
 
    public String getMXRecords(String dm,String DNSServer) 
    { 
        try 
        { 
            DNSServerIP=InetAddress.getByName(DNSServer) ;  
            //获得DNS服务器的InetAddress 
            outPk=new DatagramPacket(pkdata,pkdata.length,DNSServerIP,DNS_PORT );  
            //外发的数据报 
            UDPSocket=new DatagramSocket(DNS_PORT); //数据包端口  
            makeDNSQuery(id,dm); //产生查询报文 
            UDPSocket.send(outPk); //发送数据报文 
            inPk=new DatagramPacket(pkdata,pkdata.length );  
            UDPSocket.receive(inPk); //接收返回的应答报文 
            pkdata=inPk.getData();//获得字接 
            length=pkdata.length;  
        } 
        catch(UnknownHostException ue) 
        { 
        } 
        catch(SocketException se) 
        { 
        } 
        catch(IOException ioe) 
        { 
        } 
        return(getResponse()); //分析返回的数据报文,得到记录结果 
    } 
 
    public void makeDNSQuery(int id,String dm) 
    {//在PKdate byte数组中产生查询数据 
        for(int i=0;i<512;i++) 
        { 
            pkdata[i]=0; 
        } 
 
        pkdata[0]=(byte)(id>>8); 
        pkdata[1]=(byte)(id & 0xff); //查询的标识2字节 
        pkdata[2]=(byte)1; //Qrbit位为1,表示是查询报文 
        pkdata[3]=(byte)0;  
        pkdata[4]=(byte)0;  
        pkdata[5]=(byte)1; //1个问题 
        pkdata[6]=(byte)0; //资源记录数、授权资源记录、额外资源记录均为0个,因为是查询报文 
        pkdata[7]=(byte)0; 
        pkdata[8]=(byte)0; 
        pkdata[9]=(byte)0; 
        pkdata[10]=(byte)0; 
        pkdata[11]=(byte)0; 
        StringTokenizer st=new StringTokenizer(dm,".");//.分隔域名 
        String label; 
        position=12; //从第12字节开始生成查询问题 
        while(st.hasMoreTokens ()) 
        { 
            label=st.nextToken(); 
            pkdata[position++]=(byte)(label.length() & 0xFF);//转换为字节 
            byte[] b=label.getBytes(); 
            for(int j=0;j<b.length;j++) 
            { 
                pkdata[position++]=b[j]; 
            } 
        } 
        pkdata[position++]=(byte)0;//以0结束域名 
        pkdata[position++]=(byte)0; 
        pkdata[position++]=(byte)15; //查询类型15表示MX记录查询 
        pkdata[position++]=(byte)0;  
        pkdata[position++]=(byte)1; //Internet数据记录查询 
    }//构造查询完成 
 
    private String getResponse() //获得反馈信息 
    { 
        String temp=""; 
        int qCount=((pkdata[4] & 0xff)<<8) |(pkdata[5] & 0xFF); //获得问题数 
        if( qCount<0) 
        {return("");} //问题个数小于0,返回空字符串 
        int aCount=((pkdata[6] & 0xff)<<8)|(pkdata[7] & 0xff);//获得应答问题数 
        if (aCount<0) 
        {return(""); } //问题个数小于0,返回空字符串 
        position=12;//查询问题部分起始位置 
        for(int i=0;i<qCount;i++)//跳过问题查询部分 
        { 
            DMname=""; 
            position=Proc(position); 
            position+=4; //增加长度字节部分查询类型、查询类 
        } 
        for(int i=0;i<aCount;i++) 
        { 
            DMname=""; 
            position=Proc(position); 
            position+=10; //类型2字节、类2字节、生存时间4字节、资源长度2字节共10字节 
            int pref=(pkdata[position++]<<8) | (pkdata[position++] & 0xff); //得到MX记录的交换器基数 
            DMname=""; 
            position=Proc(position); //得到交换器的值,一般为一个标准域名 
            if( temp.equalsIgnoreCase("") ) 
            {temp="" +pref +" "+DMname; } //返回数据 
            else 
            {temp= temp + "," +pref +" "+DMname;} 
        } 
        return(temp); 
    } 
    private int Proc(int position) 
    {//该过程从pkdata字节数组中寻找域名 
        int len=(pkdata[position++]&0xff); //取得将要处理标识符的长度 
        if(len==0)//没有其他标识符,结束返回 
        {return position;} 
        int offset;//偏移 
        do
        { 
            if((len & 0xc0)==0xc0) //压缩格式么 
            { 
                if(position>=length) //超过包的大小,那么偏移显然错误 
                {return(-1);} 
                offset=((len&0x3f)<<8)|(pkdata[position++] & 0xff); //获得压缩源标识符的偏移 
                Proc(offset);//递归调用获得压缩前的名称 
                return(position); 
            } 
            else //非压缩格式 
            { 
                if((position + len)>length) //超过长度 
                { return(-1);} 
                DMname+=new String(pkdata,position,len); //获得域名标识符各个部分 
                position+=len; 
            } 
            if (position>length) 
            {return(-1);} 
            len=pkdata[position++] & 0xff;//最后是0结尾么 
            if(len!=0) 
            { 
                DMname+=".";//加上.构成完整域名 
            } 
        }while(len!=0); 
        return(position); 
    } 
    public static void main(String args[]) throws Exception  
    { 
        MXDNS mx=new MXDNS(); 
        String s=mx.getMXRecords("sina.com","202.96.128.68"); 
        System.out.println(s); 
        
int k=System.in.read() ; 
    } 



点击这里下载源代码>>















[网络编程]Windows 2000下的Raw Socket编程
2004-9-3


Windows2000在TCP/IP协议组件上做了很多改进,功能也有增强。比如在协议栈上的调整,增大了默认窗口大小,以及高延迟链接新算法。同时在安全性上,可应用IPSec加强安全性,比NT下有不少的改进。

Microsoft TCP/IP 组件包含“核心协议”、“服务”及两者之间的“接口”。传输驱动程序接口 (TDI) 与网络设备接口规范 (NDIS) 是公用的。 此外,还有许多用户模型应用程序的更高级接口。最常用的接口是 Windows Sockets、远程过程调用 (RPC) 和 NetBIOS。

Windows Sockets 是一个编程接口,它是在加州大学伯克利分校开发的套接字接口的基础上定义的。它包括了一组扩展件,以充分利用 Microsoft Windows 消息驱动的特点。规范的 1.1 版是在 1993 年 1 月发行的,2.2.0 版在 1996 年 5 月发行。Windows 2000 支持 Winsock 2.2 版。在Winsock2中,支持多个传输协议的原始套接字,重叠I/O模型、服务质量控制等。

这里介绍Windows Sockets的一些关于原始套接字(Raw Socket)的编程。同Winsock1相比,最明显的就是支持了Raw Socket套接字类型,通过原始套接字,我们可以更加自如地控制Windows下的多种协议,而且能够对网络底层的传输机制进行控制。

1、创建一个原始套接字,并设置IP头选项。

SOCKET sock;
sock = socket(AF_INET,SOCK_RAW,IPPROTO_IP);
或者:
s = WSASoccket(AF_INET,SOCK_RAW,IPPROTO_IP,NULL,0,WSA_FLAG_OVERLAPPED);

这里,我们设置了SOCK_RAW标志,表示我们声明的是一个原始套接字类型。创建原始套接字后,IP头就会包含在接收的数据中,如果我们设定 IP_HDRINCL 选项,那么,就需要自己来构造IP头。注意,如果设置IP_HDRINCL 选项,那么必须具有 administrator权限,要不就必须修改注册表:
HKEY_LOCAL_MACHINE/System/CurrentControlSet/Services/Afd/Parameter/
修改键:DisableRawSecurity(类型为DWORD),把值修改为 1。如果没有,就添加。

BOOL blnFlag=TRUE;
setsockopt(sock, IPPROTO_IP, IP_HDRINCL, (char *)&blnFlag, sizeof(blnFlag);

对于原始套接字在接收数据报的时候,要注意这么几点:
1、如果接收的数据报中协议类型和定义的原始套接字匹配,那么,接收的所有数据就拷贝到套接字中。
2、如果绑定了本地地址,那么只有接收数据IP头中对应的远端地址匹配,接收的数据就拷贝到套接字中。
3、如果定义的是外部地址,比如使用connect(),那么,只有接收数据IP头中对应的源地址匹配,接收的数据就拷贝到套接字中。


2、构造IP头和TCP头

这里,提供IP头和TCP头的结构:
// Standard TCP flags
#define URG 0x20
#define ACK 0x10
#define PSH 0x08
#define RST 0x04
#define SYN 0x02
#define FIN 0x01
typedef struct _iphdr //定义IP首部
{
unsigned char h_lenver; //4位首部长度+4位IP版本号
unsigned char tos; //8位服务类型TOS
unsigned short total_len; //16位总长度(字节)
unsigned short ident; //16位标识
unsigned short frag_and_flags; //3位标志位
unsigned char ttl; //8位生存时间 TTL
unsigned char proto; //8位协议 (TCP, UDP 或其他)
unsigned short checksum; //16位IP首部校验和
unsigned int sourceIP; //32位源IP地址
unsigned int destIP; //32位目的IP地址
}IP_HEADER;

typedef struct psd_hdr //定义TCP伪首部
{
unsigned long saddr; //源地址
unsigned long daddr; //目的地址
char mbz;
char ptcl; //协议类型
unsigned short tcpl; //TCP长度
}PSD_HEADER;

typedef struct _tcphdr //定义TCP首部
{
USHORT th_sport; //16位源端口
USHORT th_dport; //16位目的端口
unsigned int th_seq; //32位序列号
unsigned int th_ack; //32位确认号
unsigned char th_lenres; //4位首部长度/6位保留字
unsigned char th_flag; //6位标志位
USHORT th_win; //16位窗口大小
USHORT th_sum; //16位校验和
USHORT th_urp; //16位紧急数据偏移量
}TCP_HEADER;

TCP伪首部并不是真正存在的,只是用于计算检验和。校验和函数:

USHORT checksum(USHORT *buffer, int size)
{
unsigned long cksum=0;
while (size > 1)
{
cksum += *buffer++;
size -= sizeof(USHORT);
}
if (size)
{
cksum += *(UCHAR*)buffer;
}
cksum = (cksum >> 16) + (cksum & 0xffff);
cksum += (cksum >>16);
return (USHORT)(~cksum);
}

当需要自己填充IP头部和TCP头部的时候,就同时需要自己计算他们的检验和。

3、发送原始套接字数据报

填充这些头部稍微麻烦点,发送就相对简单多了。只需要使用sendto()就OK。

sendto(sock, (char*)&tcpHeader, sizeof(tcpHeader), 0, (sockaddr*)&addr_in,sizeof(addr_in));

下面是一个示例程序,可以作为SYN扫描的一部分。

#include
#include
#include

#define SOURCE_PORT 7234
#define MAX_RECEIVEBYTE 255

typedef struct ip_hdr //定义IP首部
{
unsigned char h_verlen; //4位首部长度,4位IP版本号
unsigned char tos; //8位服务类型TOS
unsigned short total_len; //16位总长度(字节)
unsigned short ident; //16位标识
unsigned short frag_and_flags; //3位标志位
unsigned char ttl; //8位生存时间 TTL
unsigned char proto; //8位协议 (TCP, UDP 或其他)
unsigned short checksum; //16位IP首部校验和
unsigned int sourceIP; //32位源IP地址
unsigned int destIP; //32位目的IP地址
}IPHEADER;

typedef struct tsd_hdr //定义TCP伪首部
{
unsigned long saddr; //源地址
unsigned long daddr; //目的地址
char mbz;
char ptcl; //协议类型
unsigned short tcpl; //TCP长度
}PSDHEADER;

typedef struct tcp_hdr //定义TCP首部
{
USHORT th_sport; //16位源端口
USHORT th_dport; //16位目的端口
unsigned int th_seq; //32位序列号
unsigned int th_ack; //32位确认号
unsigned char th_lenres; //4位首部长度/6位保留字
unsigned char th_flag; //6位标志位
USHORT th_win; //16位窗口大小
USHORT th_sum; //16位校验和
USHORT th_urp; //16位紧急数据偏移量
}TCPHEADER;

//CheckSum:计算校验和的子函数
USHORT checksum(USHORT *buffer, int size)
{
unsigned long cksum=0;
while(size >1)
{
cksum+=*buffer++;
size -=sizeof(USHORT);
}
if(size )
{
cksum += *(UCHAR*)buffer;
}

cksum = (cksum >> 16) + (cksum & 0xffff);
cksum += (cksum >>16);
return (USHORT)(~cksum);
}

void useage()
{
printf("******************************************/n");
printf("TCPPing/n");
printf("/t Written by Refdom/n");
printf("/t Email: refdom@263.net/n");
printf("Useage: TCPPing.exe Target_ip Target_port /n");
printf("*******************************************/n");
}

int main(int argc, char* argv[])
{
WSADATA WSAData;
SOCKET sock;
SOCKADDR_IN addr_in;
IPHEADER ipHeader;
TCPHEADER tcpHeader;
PSDHEADER psdHeader;

char szSendBuf[60]={0};
BOOL flag;
int rect,nTimeOver;

useage();

if (argc!= 3)
{ return false; }

if (WSAStartup(MAKEWORD(2,2), &WSAData)!=0)
{
printf("WSAStartup Error!/n");
return false;
}

if ((sock=WSASocket(AF_INET,SOCK_RAW,IPPROTO_RAW,NULL,0,WSA_FLAG_OVERLAPPED))==INVALID_SOCKET)
{
printf("Socket Setup Error!/n");
return false;
}
flag=true;
if (setsockopt(sock,IPPROTO_IP, IP_HDRINCL,(char *)&flag,sizeof(flag))==SOCKET_ERROR)
{
printf("setsockopt IP_HDRINCL error!/n");
return false;
}

nTimeOver=1000;
if (setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (char*)&nTimeOver, sizeof(nTimeOver))==SOCKET_ERROR)
{
printf("setsockopt SO_SNDTIMEO error!/n");
return false;
}
addr_in.sin_family=AF_INET;
addr_in.sin_port=htons(atoi(argv[2]));
addr_in.sin_addr.S_un.S_addr=inet_addr(argv[1]);

//
//
//填充IP首部
ipHeader.h_verlen=(4<<4 | sizeof(ipHeader)/sizeof(unsigned long));
// ipHeader.tos=0;
ipHeader.total_len=htons(sizeof(ipHeader)+sizeof(tcpHeader));
ipHeader.ident=1;
ipHeader.frag_and_flags=0;
ipHeader.ttl=128;
ipHeader.proto=IPPROTO_TCP;
ipHeader.checksum=0;
ipHeader.sourceIP=inet_addr("本地地址");
ipHeader.destIP=inet_addr(argv[1]);

//填充TCP首部
tcpHeader.th_dport=htons(atoi(argv[2]));
tcpHeader.th_sport=htons(SOURCE_PORT); //源端口号
tcpHeader.th_seq=htonl(0x12345678);
tcpHeader.th_ack=0;
tcpHeader.th_lenres=(sizeof(tcpHeader)/4<<4|0);
tcpHeader.th_flag=2; //修改这里来实现不同的标志位探测,2是SYN,1是FIN,16是ACK探测 等等
tcpHeader.th_win=htons(512);
tcpHeader.th_urp=0;
tcpHeader.th_sum=0;

psdHeader.saddr=ipHeader.sourceIP;
psdHeader.daddr=ipHeader.destIP;
psdHeader.mbz=0;
psdHeader.ptcl=IPPROTO_TCP;
psdHeader.tcpl=htons(sizeof(tcpHeader));

//计算校验和
memcpy(szSendBuf, &psdHeader, sizeof(psdHeader));
memcpy(szSendBuf+sizeof(psdHeader), &tcpHeader, sizeof(tcpHeader));
tcpHeader.th_sum=checksum((USHORT *)szSendBuf,sizeof(psdHeader)+sizeof(tcpHeader));

memcpy(szSendBuf, &ipHeader, sizeof(ipHeader));
memcpy(szSendBuf+sizeof(ipHeader), &tcpHeader, sizeof(tcpHeader));
memset(szSendBuf+sizeof(ipHeader)+sizeof(tcpHeader), 0, 4);
ipHeader.checksum=checksum((USHORT *)szSendBuf, sizeof(ipHeader)+sizeof(tcpHeader));

memcpy(szSendBuf, &ipHeader, sizeof(ipHeader));

rect=sendto(sock, szSendBuf, sizeof(ipHeader)+sizeof(tcpHeader),
0, (struct sockaddr*)&addr_in, sizeof(addr_in));
if (rect==SOCKET_ERROR)
{
printf("send error!:%d/n",WSAGetLastError());
return false;
}
else
printf("send ok!/n");

closesocket(sock);
WSACleanup();

return 0;
}

4、接收数据
和发送原始套接字数据相比,接收就比较麻烦了。因为在WIN我们不能用recv()来接收raw socket上的数据,这是因为,所有的IP包都是先递交给系统核心,然后再传输到用户程序,当发送一个raws socket包的时候(比如syn),核心并不知道,也没有这个数据被发送或者连接建立的记录,因此,当远端主机回应的时候,系统核心就把这些包都全部丢掉,从而到不了应用程序上。所以,就不能简单地使用接收函数来接收这些数据报。

要达到接收数据的目的,就必须采用嗅探,接收所有通过的数据包,然后进行筛选,留下符合我们需要的。可以再定义一个原始套接字,用来完成接收数据的任务,需要设置SIO_RCVALL,表示接收所有的数据。

SOCKET sniffersock;
sniffsock = WSASocket(AF_INET, SOCK_RAW, IPPROTO_IP, NULL, 0, WSA_FLAG_OVERLAPPED);

DWORD lpvBuffer = 1;
DWORD lpcbBytesReturned = 0 ;
WSAIoctl(sniffersock, SIO_RCVALL, &lpvBuffer, sizeof(lpvBuffer), NULL, 0, & lpcbBytesReturned, NULL, NULL);

创建一个用于接收数据的原始套接字,我们可以用接收函数来接收数据包了。然后在使用一个过滤函数达到筛选的目的,接收我们需要的数据包。
在 http://www.xici.net/board/doc.asp?id=5891453&sub=15 提供了一个完整的发送和接收的程序,可以参考。

 






如何把一个*.CS文件编译成一个DLL文件啊?(谢谢) 
http://community.csdn.net/Expert/topic/3367/3367953.xml?temp=.1050531
作  者:  lyzhong79 (雪狼湖) 
等  级:   
信 誉 值:  100 
所属社区:  .NET技术 C# 
问题点数:  10 
回复次数:  4 
发表时间:  2004-09-13 16:29:34 
  
 
  
比如我在一个项目中,添加了一个*.CS文件,当我编译这个项目的时候这个*.CS文件并没有被编译成*.DLL文件.是在一个项目中添加的*.CS文件,不是新建CLASS项目,请问大家,要怎么才能编译成*.DLL文件呢?
  谢谢了
 
  回复人: zyhjolly(zyhjolly) ( ) 信誉:100  2004-09-13 16:43:00  得分: 0 
 
 
   如果想把这个cs文件编译成dll,新建“类库”项目,把cs文件加进去再编译
 
 
Top 
 
 回复人: designonline(meetweb) ( ) 信誉:100  2004-09-13 16:43:00  得分: 0 
 
 
   项目中便编译的话,只有一个dll
如果需要单独编译
命令行示例
编译 File.cs 以产生 File.exe:
csc File.cs
编译 File.cs 以产生 File.dll:
csc /target:library File.cs
编译 File.cs 并创建 My.exe:
csc /out:My.exe File.cs
通过使用优化和定义 DEBUG 符号,编译当前目录中所有的 C# 文件。输出为 File2.exe:
csc /define:DEBUG /optimize /out:File2.exe *.cs
编译当前目录中所有的 C# 文件,以产生 File2.dll 的调试版本。不显示任何徽标和警告:
csc /target:library /out:File2.dll /warn:0 /nologo /debug *.cs
将当前目录中所有的 C# 文件编译为 Something.xyz(一个 DLL):
csc /target:library /out:Something.xyz *.cs


 
Top 
 
 回复人: laodeng72586(只是路过) ( ) 信誉:100  2004-09-13 16:49:00  得分: 0 
 
 
   例子:
启动VS.net的命令行工具;
csc /t:library /r:System.dll,System.Web.Services.dll,System.Xml.dll Service1.cs
说明:
csc:               C#编译器如果是 VB.net应该为vbc
/t:library         编译结果为dll文件
/r:                为引用的命名空间
Service1.cs        为要编译的代码文件
 
 
Top 
 
 回复人: lyzhong79(雪狼湖) ( ) 信誉:100  2004-09-13 16:58:00  得分: 0 
 
 
   不能用VS自动编译吗?这样做很麻烦的
   一楼说的方法,需要新建个项目-类库(这方法主要是不方便调试,如果编译出来了DLL文件,添加进项目了,如果有错,还的再打开类库这个项目来检查,重新编译,重新添加)
   二楼的办法和一楼差不多(不方便啊)
 

建类库,在类库里可以建控件类,再生成就可以了
新建一个项目1,项目菜单-->(项目文件名)-->输出类型,选类库.

新建一个项目2,在 新项目2的解决方案资源管理里添加应用,点浏览,找到生成的类库,确定

在项目2的源代码里添加using 项目一名称的命名空间.

这样毫无问题.




下面代码总是用在vb.net中,用c#写花了我不少时间。
 public static MainForm FormDefInstance;
 public static MainForm DefInstance
  {
   get
   {
    if (FormDefInstance == null || FormDefInstance.IsDisposed)
    {
     FormDefInstance = new MainForm();
    }
    return FormDefInstance;
   }
   set
   {
    FormDefInstance = value;
   }
  }
 



 static void Main()
  {
   Application.EnableVisualStyles();
   Application.DoEvents();  //处理完当前的消息队列。
   
   Application.Run( MainForm.DefInstance);
  }




http://www.vbcity.com/forums/topic.asp?tid=32927&highlight=regex%7Cemail
 Private Function IsValidEmail(ByVal UncheckedEmailAddr As String) As Boolean
            Const RegExpr As String = "^([a-zA-Z0-9_/-/.]+)@((/[[0-9]{1,3}/.[0-9]{1,3}/.[0-9]{1,3}/.)/(([a-zA-Z0-9/-]+/.)+))([a-zA-Z]{2,4}/[0-9]{1,3})(/]?)$"
            Dim InputEmail As String = UncheckedEmailAddr.Trim
            Try
                Dim r As New Regex(RegExpr)
                If r.IsMatch(InputEmail) Then
                    Return True
                Else
                    Return False
                End If
            Catch ex As Exception
                DebugWriter("Source of Error : " & ex.Source & vbCrLf & _
                            "Error Message : " & ex.Message)
            End Try
        End Function









/*
 * 模块名:操作INI文件。
 * 模块来源:英特网,
 * 备注:等有空把这个模块的功能完善一下。
 * 时间:2004年9月9日
 */


using System;
using System.Text;
using System.IO;
using System.Runtime.InteropServices;

namespace cxh_mk
{

 /// <summary>
 /// Ini文件操作的类
 /// </summary>
 public class IniFile
 {
  private string filename;     //INI完整文件名

  //声明读写INI文件的API函数   
  [DllImport("kernel32")]
  private static extern long WritePrivateProfileString(string section,string key,string val,string filePath);

  [DllImport("kernel32")]
  private static extern int GetPrivateProfileString(string section,string key,string def,StringBuilder retVal,int size,string filePath);
  
  /// <summary>
  /// 类的构造函数
  /// </summary>
  /// <param name="INIPath">INI文件名</param>
  public IniFile(string iniFilename)
  {
   if (iniFilename.IndexOf(@"://") == -1)
   {    
    filename = System.Windows.Forms.Application.StartupPath + "//" + iniFilename;
   }
   else
   {
    filename = iniFilename;
   }
  }

  /// <summary>
  /// 写INI文件
  /// </summary>
  /// <param name="Section">节</param>
  /// <param name="Key">关键字</param>
  /// <param name="Value">值</param>
  public void WriteValue(string Section,string Key,string Value)
  {
   WritePrivateProfileString(Section,Key,Value,this.filename);
  }
  /// <summary>
  /// 写INI文件
  /// </summary>
  /// <param name="Section">节</param>
  /// <param name="Key">关键字</param>
  /// <param name="Value">值</param>
  /// <param name="filename">ini文件所以路径</param>
  public static void WriteValue(string Section,string Key,string Value,string filename)
  {
   WritePrivateProfileString(Section,Key,Value,filename);
  }


  /// <summary>
  /// 读取INI文件指定
  /// </summary>
  /// <param name="Section">节</param>
  /// <param name="Key">关键字</param>
  /// <returns>指定节-关键字的值</returns>
  public string ReadValue(string Section,string Key)
  {
   StringBuilder temp = new StringBuilder(255);
   int i = GetPrivateProfileString(Section,Key,"",temp,255,this.filename);
   return temp.ToString();
  }

  /// <summary>
  /// 读取INI文件指定
  /// </summary>
  /// <param name="Section">节</param>
  /// <param name="Key">关键字</param>
  /// <returns>指定节-关键字的值</returns>
  /// <param name="filename">ini文件所在路径</param>
  public string ReadValue(string Section,string Key,string filename)
  {
   StringBuilder temp = new StringBuilder(255);
   int i = GetPrivateProfileString(Section,Key,"",temp,255,filename);
   return temp.ToString();
  }

 }

}











  // IniFile ini = new IniFile("user.ini");
  // txtTo.Text=ini.ReadValue("mailinfo","to");
  // txtToname.Text=ini.ReadValue("mailinfo","toname");
  // txtFrom.Text=ini.ReadValue("mailinfo","from");
  // txtFromname.Text=ini.ReadValue("mailinfo","fromname");
  // txtTitle.Text=ini.ReadValue("mailinfo","title");
  // if (ini.ReadValue("mailinfo","dns") == "")
  // {
  //  txtDns.Text=DnsQuery.dnsServer;
  //  
  // }
  // else
  // {
//
//    txtDns.Text=ini.ReadValue("mailinfo","dns");
//   }
//   txtBody.Text=ini.ReadValue("mailinfo","body");








 //IniFile ini = new IniFile("user.ini");
   //ini.WriteValue("mailinfo","to",mail.to);
   //ini.WriteValue("mailinfo","toname",mail.toname);
   //ini.WriteValue("mailinfo","from",mail.from);
   //ini.WriteValue("mailinfo","fromname",mail.fromname);
   //ini.WriteValue("mailinfo","title",mail.title);
   //ini.WriteValue("mailinfo","dns",DnsQuery.dnsServer);
   //ini.WriteValue("mailinfo","body",mail.body);
   
   
   //mail.attachmentList = new string[lstFile.Items.Count];
   //if (lstFile.Items.Count!=0)
   //{
   // lstFile.Items.CopyTo(mail.attachmentList,0);
   //}

  // Thread th = new Thread(new ThreadStart(this.Run));
  // th.Start();










评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值