2005年04月27日

用C#实现基于TCP协议的网络通讯

 TCP协议是一个基本的网络协议,基本上所有的网络服务都是基于TCP协议的,如HTTP,FTP等等,所以要了解网络编程就必须了解基于TCP协议的编程。然而TCP协议是一个庞杂的体系,要彻底的弄清楚它的实现不是一天两天的功夫,所幸的是在.net framework环境下,我们不必要去追究TCP协议底层的实现,一样可以很方便的编写出基于TCP协议进行网络通讯的程序。
   
  要进行基于TCP协议的网络通讯,首先必须建立同远程主机的连接,连接地址通常包括两部分——主机名和端口,如www.yesky.com:80中,www.yesky.com就是主机名,80指主机的80端口,当然,主机名也可以用IP地址代替。当连接建立之后,就可以使用这个连接去发送和接收数据包,TCP协议的作用就是保证这些数据包能到达终点并且能按照正确的顺序组装起来。
   
  在.net framework的类库(Class Library)中,提供了两个用于TCP网络通讯的类,分别是TcpClient和TcpListener。由其英文意义显而易见,TcpClient类是基于TCP协议的客户端类,而TcpListener是服务器端,监听(Listen)客户端传来的连接请求。TcpClient类通过TCP协议与服务器进行通讯并获取信息,它的内部封装了一个Socket类的实例,这个Socket对象被用来使用TCP协议向服务器请求和获取数据。因为与远程主机的交互是以数据流的形式出现的,所以传输的数据可以使用.net framework中流处理技术读写。在我们下边的例子中,你可以看到使用NetworkStream类操作数据流的方法。
   
  在下面的例子中,我们将建立一个时间服务器,包括服务器端程序和客户端程序。服务器端监听客户端的连接请求,建立连接以后向客户端发送当前的系统时间。
   
  先运行服务器端程序,下面截图显示了服务器端程序运行的状况:
   
   
   
  然后运行客户端程序,客户端首先发送连接请求到服务器端,服务器端回应后发送当前时间到客户端,这是客户端程序的截图:
   
   
   
  发送完成后,服务器端继续等待下一次连接:
   
   
   
  通过这个例子我们可以了解TcpClient类的基本用法,要使用这个类,必须使用System.Net.Socket命名空间,本例用到的三个命名空间如下:
   
  using System;
  using System.Net.Sockets;
  using System.Text;//从字节数组中获取字符串时使用该命名空间中的类
   
  首先讨论一下客户端程序,开始我们必须初始化一个TcpClient类的实例:
   
  TcpClient client = new TcpClient(hostName, portNum);
   
  然后使用TcpClient类的GetStream()方法获取数据流,并且用它初始化一个NetworkStream类的实例:
   
  NetworkStream ns = client.GetStream();
   
  注意,当使用主机名和端口号初始化TcpClient类的实例时,直到跟服务器建立了连接,这个实例才算真正建立,程序才能往下执行。如果因为网络不通,服务器不存在,服务器端口未开放等等原因而不能连接,程序将抛出异常并且中断执行。
   
  建立数据流之后,我们可以使用NetworkStream类的Read()方法从流中读取数据,使用Write()方法向流中写入数据。读取数据时,首先应该建立一个缓冲区,具体的说,就是建立一个byte型的数组用来存放从流中读取的数据。Read()方法的原型描述如下:
   
  public override int Read(in byte[] buffer,int offset,int size)
   
  buffer是缓冲数组,offset是数据(字节流)在缓冲数组中存放的开始位置,size是读取的字节数目,返回值是读取的字节数。在本例中,简单地使用该方法来读取服务器反馈的信息:
   
  byte[] bytes = new byte[1024];//建立缓冲区
  int bytesRead = ns.Read(bytes, 0, bytes.Length);//读取字节流
   
  然后显示到屏幕上:
   
  Console.WriteLine(Encoding.ASCII.GetString(bytes,0,bytesRead));
   
  最后不要忘记关闭连接:
   
  client.Close();
   
  下面是本例完整的程序清单:
   
  using System;
  using System.Net.Sockets;
  using System.Text;
   
  namespace TcpClientExample
  {
  public class TcpTimeClient
  {
  private const int portNum = 13;//服务器端口,可以随意修改
  private const string hostName = "127.0.0.1";//服务器地址,127.0.0.1指本机
   
  [STAThread]
  static void Main(string[] args)
  {
  try
  {
  Console.Write("Try to connect to "+hostName+":"+portNum.ToString()+"\r\n");
  TcpClient client = new TcpClient(hostName, portNum);
  NetworkStream ns = client.GetStream();
  byte[] bytes = new byte[1024];
  int bytesRead = ns.Read(bytes, 0, bytes.Length);
   
  Console.WriteLine(Encoding.ASCII.GetString(bytes,0,bytesRead));
   
  client.Close();
  Console.ReadLine();//由于是控制台程序,故为了清楚的看到结果,可以加上这句
   
  }
  catch (Exception e)
  {
  Console.WriteLine(e.ToString());
  }
  }
  }
  }
   
  上面这个例子清晰地演示了客户端程序的编写要点,下面我们讨论一下如何建立服务器程序。这个例子将使用TcpListener类,在13号端口监听,一旦有客户端连接,将立即向客户端发送当前服务器的时间信息。
   
  TcpListener的关键在于AcceptTcpClient()方法,该方法将检测端口是否有未处理的连接请求,如果有未处理的连接请求,该方法将使服务器同客户端建立连接,并且返回一个TcpClient对象,通过这个对象的GetStream方法建立同客户端通讯的数据流。事实上,TcpListener类还提供一个更为灵活的方法AcceptSocket(),当然灵活的代价是复杂,对于比较简单的程序,AcceptTcpClient()已经足够用了。此外,TcpListener类提供Start()方法开始监听,提供Stop()方法停止监听。
   
  首先我们使用端口初始化一个TcpListener实例,并且开始在13端口监听:
   
  private const int portNum = 13;
  TcpListener listener = new TcpListener(portNum);
  listener.Start();//开始监听
   
  如果有未处理的连接请求,使用AcceptTcpClient方法进行处理,并且获取数据流:
   
  TcpClient client = listener.AcceptTcpClient();
  NetworkStream ns = client.GetStream();
   
  然后,获取本机时间,并保存在字节数组中,使用NetworkStream.Write()方法写入数据流,然后客户端就可以通过Read()方法从数据流中获取这段信息:
   
  byte[] byteTime = Encoding.ASCII.GetBytes(DateTime.Now.ToString());
  ns.Write(byteTime, 0, byteTime.Length);
  ns.Close();//不要忘记关闭数据流和连接
  client.Close();
   
  服务器端程序完整的程序清单如下:
   
  using System;
  using System.Net.Sockets;
  using System.Text;
   
   
  namespace TimeServer
  {
  class TimeServer
  {
  private const int portNum = 13;
   
  [STAThread]
  static void Main(string[] args)
  {
  bool done = false;
  TcpListener listener = new TcpListener(portNum);
  listener.Start();
  while (!done)
  {
  Console.Write("Waiting for connection…");
  TcpClient client = listener.AcceptTcpClient();
   
  Console.WriteLine("Connection accepted.");
  NetworkStream ns = client.GetStream();
   
  byte[] byteTime = Encoding.ASCII.GetBytes(DateTime.Now.ToString());
   
  try
  {
  ns.Write(byteTime, 0, byteTime.Length);
  ns.Close();
  client.Close();
  }
  catch (Exception e)
  {
  Console.WriteLine(e.ToString());
  }
  }
   
  listener.Stop();
  }
  }
  }
   
  把上面两段程序分别编译运行,OK,我们已经用C#实现了基于TCP协议的网络通讯,怎么样?很简单吧!
   
  使用上面介绍的基本方法,我们可以很容易的编写出一些很有用的程序,如FTP,电子邮件收发,点对点即时通讯等等,你甚至可以自己编制一个QQ来!

对于许多初学者来说,网络通信程序的开发,普遍的一个现象就是觉得难以入手。许多概念,诸如:同步(Sync)/异步(Async),阻塞(Block)/非阻塞(Unblock)等,初学者往往迷惑不清,只知其所以而不知起所以然。
    阻塞套接字是指执行此套接字的网络调用时,直到成功才返回,否则一直阻塞在此网络调用上,比如调用recv()函数读取网络缓冲区中的数据,如果没有数据到达,将一直挂在recv()这个函数调用上,直到读到一些数据,此函数调用才返回;   
    而非阻塞套接字是指执行此套接字的网络调用时,不管是否执行成功,都立即返回。比如调用recv()函数读取网络缓冲区中数据,不管是否读到数据都立即返回,而不会一直挂在此函数调用上。在实际Windows网络通信软件开发中,异步非阻塞套接字是用的最多的。平常所说的C/S(客户端/服务器)结构的软件就是异步非阻塞模式的。
    下面用一个最简单的例子说明异步非阻塞Socket的基本原理和工作机制。目的是让初学者不仅对Socket异步非阻塞的概念有个非常透彻的理解,而且也给他们提供一个用Socket开发网络通信应用程序的快速入门方法。操作系统是Windows 98(或NT4.0),开发工具是Visual C++6.0。
    MFC提供了一个异步类CAsyncSocket,它封装了异步、非阻塞Socket的基本功能,用它做常用的网络通信软件很方便。但它屏蔽了Socket的异步、非阻塞等概念,开发人员无需了解异步、非阻塞Socket的原理和工作机制。因此,建议初学者学习编网络通信程序时,暂且不要用MFC提供的类,而先用Winsock2 API,这样有助于对异步、非阻塞Socket编程机制的理解。
    为了简单起见,服务器端和客户端的应用程序均是基于MFC的标准对话框,网络通信部分基于Winsock2API实现。先做服务器端应用程序。
    用MFC向导做一个基于对话框的应用程序SocketSever,注意第三步中不要选上Windwos Sockets选项。在做好工程后,创建一个SeverSock,将它设置为异步非阻塞模式,并为它注册各种网络异步事件,然后与自定义的网络异步事件联系上,最后还要将它设置为监听模式。在自定义的网络异步事件的回调函数中,你
可以得到各种网络异步事件,根据它们的类型,做不同的处理。下面将详细介绍如何编写相关代码。
    在SocketSeverDlg.h文件的类定义之前增加如下定义:
    #define  NETWORK_EVENT  WM_USER+166  //定义网络事件
   
    SOCKET ServerSock; //服务器端Socket
    在类定义中增加如下定义:
    class CSocketSeverDlg : CDialog
    {
                 …
    public:
         SOCKET ClientSock[CLNT_MAX_NUM]; //存储用与客户端通信的Socket的数组

         /*各种网络异步事件的处理函数*/
          void OnClose(SOCKET CurSock);   //对端Socket断开
          void OnSend(SOCKET CurSock);   //发送网络数据包
          void OnReceive(SOCKET CurSock); //网络数据包到达
          void OnAccept(SOCKET CurSock);  //客户端连接请求

          BOOL InitNetwork();  //初始化网络函数
         void OnNetEvent(WPARAM wParam, LPARAM lParam); //网络异步事件回调函数
                …
    };
       
    在SocketSeverDlg.cpp文件中增加消息映射,其中OnNetEvent是异步事件回调函数名:
               ON_MESSAGE(NETWORK_EVENT,OnNetEvent)
    定义初始化网络函数,在SocketSeverDlg.cpp文件的OnInitDialog()中调此函数即可。
    BOOL CSocketSeverDlg::InitNetwork()
    {
       WSADATA wsaData;

       //初始化TCP协议
       BOOL ret = WSAStartup(MAKEWORD(2,2), &wsaData);
       if(ret != 0)
       {
               MessageBox("初始化网络协议失败!");
           return FALSE;
       }

        //创建服务器端套接字
           ServerSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
       if(ServerSock == INVALID_SOCKET)
       {
             MessageBox("创建套接字失败!");
            closesocket(ServerSock);
            WSACleanup();
             return FALSE;
           }

          //绑定到本地一个端口上
       sockaddr_in localaddr;
       localaddr.sin_family = AF_INET;
       localaddr.sin_port = htons(8888);  //端口号不要与其他应用程序冲突
       localaddr.sin_addr.s_addr = 0;
        if(bind(ServerSock ,(struct sockaddr*)&localaddr,sizeof(sockaddr))
                                          = = SOCKET_ERROR)
       {
           MessageBox("绑定地址失败!");
           closesocket(ServerSock);
           WSACleanup();
           return FALSE;
           }
   
   //将SeverSock设置为异步非阻塞模式,并为它注册各种网络异步事件,其中m_hWnd      
   //为应用程序的主对话框或主窗口的句柄
          if(WSAAsyncSelect(ServerSock, m_hWnd, NETWORK_EVENT,
              FD_ACCEPT | FD_CLOSE | FD_READ | FD_WRITE) == SOCKET_ERROR)
       {
          MessageBox("注册网络异步事件失败!");
          WSACleanup();
          return FALSE;
           }

       listen(ServerSock, 5); //设置侦听模式

           return TRUE;
    }

    下面定义网络异步事件的回调函数
    void CSocketSeverDlg::OnNetEvent(WPARAM wParam, LPARAM lParam)
    {
        //调用Winsock API函数,得到网络事件类型
        int iEvent = WSAGETSELECTEVENT(lParam);
         
        //调用Winsock API函数,得到发生此事件的客户端套接字
        SOCKET CurSock= (SOCKET)wParam;

        switch(iEvent)
        {
        case FD_ACCEPT:      //客户端连接请求事件
                OnAccept(CurSock);
                break;
        case FD_CLOSE:       //客户端断开事件:
                OnClose(CurSock);
                break;
        case FD_READ:        //网络数据包到达事件
                OnReceive(CurSock);
                break;
         case FD_WRITE:      //发送网络数据事件
                OnSend(CurSock);
                break;
         default: break;
         }
    }
   
    以下是发生在相应Socket上的各种网络异步事件的处理函数,其中OnAccept传进来的参数是服务器端创建的套接字,OnClose()、OnReceive()和OnSend()传进来的参数均是服务器端在接受客户端连接时新创建的用与此客户端通信的Socket。
    void CSocketSeverDlg::OnAccept(SOCKET CurSock)
    {
         //接受连接请求,并保存与发起连接请求的客户端进行通信Socket
     //为新的socket注册异步事件,注意没有Accept事件
    }
 
    void CSocketSeverDlg::OnClose(SOCET CurSock)
    {
        //结束与相应的客户端的通信,释放相应资源
    }

    void CSocketSeverDlg::OnSend(SOCET CurSock)
    {
        //在给客户端发数据时做相关预处理
    }

    void CSocketSeverDlg::OnReceive(SOCET CurSock)
    {
        //读出网络缓冲区中的数据包
    }       
       
    用同样的方法建立一个客户端应用程序。初始化网络部分,不需要将套接字设置为监听模式。注册异步事件时,没有FD_ACCEPT,但增加了FD_CONNECT事件,因此没有OnAccept()函数,但增加了OnConnect()函数。向服务器发出连接请求时,使用connect()函数,连接成功后,会响应到OnConnect()函数中。下面是OnConnect()函数的定义,传进来的参数是客户端Socket和服务器端发回来的连接是否成功的标志。

    void CSocketClntDlg::OnConnect(SOCKET CurSock, int error)
    {
    if(0 = = error)
    {
         if(CurSock = = ClntSock)
          MessageBox("连接服务器成功!");
        }
    }

    定义OnReceive()函数,处理网络数据到达事件;
    定义OnSend()函数,处理发送网络数据事件;
    定义OnClose()函数,处理服务器的关闭事件。
           
    以上就是用基于Windows消息机制的异步I/O模型做服务器和客户端应用程序的基本方法。另外还可以用事件模型、重叠模型或完成端口模型,读者可以参考有关书籍。
    在实现了上面的例子后,你将对Winsock编网络通信程序的机制有了一定的了解。接下来你可以进行更精彩的编程, 不仅可以在网上传输普通数据,而且还以传输语音、视频数据,你还可以自己做一个网络资源共享的服务器软件,和你的同学在实验室的局域网里可以共同分享你的成果。

오늘 여기 날씨 죽인다야…., 

5월달두 안됬는데,,벌써 32도 까지 올라 오다니………….

어제 우연히 사이트에서 화토노는 사이트 발견했다…..중요한것은 한국사이트가 아니구 연변사이트란것에서,,

정말기뻐보이구,,,ㅎㅎ,,근데,,그게임 다운하느데 다운이 안되지??

사이트는

http://gostop.yb.jl.cn/

조선맞고

새로운 단어,,역시 블로그와두 관계되구,,새래운 기술을 리용한,,무엇무엇이다….ㅎㅎ,,아래 내용을 보면 대개 알수 리해할수 있을것이다.

옮긴글: http://www.hof.pe.kr/wp/?p=548

    “What Is RSS?”를 보고 한글로 된 쉬운 RSS 설명서가 필요할것 같아 써봅니다. 기술적인 내용은 가능한 생략하였고 주로 블로그에서 초보자들이 RSS를 이해하고 이용할수 있도록 하는 것을 목적으로 합니다.


    RSS란 무엇인가?
RSS는 Really Simple Syndication의 머리글자를 딴 말이며, 사이트에 새로 올라온 글을 쉽게 구독할 수 있도록 하는 일종의 규칙입니다. 사이트에서는 바뀐 내용, 새로운 글을 RSS라는 규칙에 따라 제공하면 이용자는 RSS를 읽을 수 있는 프로그램 (보통 RSS리더기로 불리웁니다.)으로 그 내용을 받아올 수 있습니다.

    RSS로 할 수 있는 일은 무엇인가?
흔히 RSS는 컨텐트 수집(보내는 쪽에서는 배급)의 좋은 방법이라고 말합니다. 왜그러냐면 예를 들어 10개의 사이트에서 업데이트 된 내용을 확인하려면 브라우저를 열고 10개 사이트를 하나씩 방문해서 지난번 읽었던 곳을 찾고 그 뒤로 새로운 글이 올라왔는지를 확인해야 합니다. 1시간뒤에 또 확인해보려면 이 작업을 손으로 하나씩 다시 해줘야 합니다. 그런데 만약 이 10개의 사이트에서 RSS를 제공한다면 RSS리더기를 이용해서 순식간에 확인이 가능합니다. 게다가 일정한 시간간격마다 자동으로 확인을 해주죠. RSS를 이용해서 할수 있는 일은 아주 다양합니다만 블로그에서는 자신이 구독하는 블로그에 새로운 포스트가 올라왔는지를 확인하는 용도로 유용하게 사용할 수 있습니다.
        이메일과 무엇이 다른가?
        사이트에서 사용자에게 새로운 내용을 보내준다는 용도로 보면 이메일로 보내는 뉴스레터,이메일소식지와 비슷할 수도 있지만 RSS는 이메일과는 다릅니다. 우선 이메일은 내용을 보내주는 사이트에 나의 이메일주소를 알려주는 과정이 필요하고 나에게 뉴스레터를 이메일로 발송하면 받은편지함에서 받아봅니다. 스팸편지속에 뉴스레터가 섞일수도 있고 해당사이트에서 보관하고 있는 나의 이메일주소가 악용될 우려도 있습니다. 반면 RSS는 사이트에서 제공하는 RSS주소를 리더기에 입력하기만 하면 사용자가 일방적으로 내용을 긁어옵니다. 더이상 받고 싶지 않으면 RSS주소록에서 그 주소를 삭제하기만 하면 됩니다. 사이트에서는 강제로 RSS를 전송할 방법이 없습니다. 이것을 그림으로 그려보자면 아래와 같습니다.

                 이메일과 RSS

    RSS를 제공하는 방법은?
        RSS를 제공하는 것을 “RSS Feed”라고 말하기도 합니다. RSS는 일종의 규약이므로 이 규약에 맞게 작성해놓으면 됩니다. 그러나 사이트가 업데이트될때마다 RSS를 손으로 수정해주는 것은 흔히하는 말로 개노가다이며 그래서 대부분의 블로그에서는 이 RSS를 자동으로 생성해줍니다.
    RSS를 보는 방법은?
        RSS링크를 브라우저에서 열어봐도 되긴 하는데 사람이 보기에 그다지 편한 모양새가 아닙니다. RSS리더기를 이용해서 그 주소를 불러오면 알아서 보기편하게 정리해서 보여줍니다. 많이 쓰는 프로그램은 (이것도 그때그때 유행이 있나봅니다.) SharpReader가 있고 요즘은 웹에서 RSS리더기 기능을 구현해주는 bloglines라는 사이트에 많이 가입하시는듯 합니다. 더 많은 리더기는 RSScalendar의 RSS리더기 페이지나 Technology at Harvard Law의 Aggregators페이지에서 찾을 수 있습니다. RSS를 읽을 수 있는 프로그램이 준비되었다면 이제 RSS를 받아와야겠지요?

        RSS를 제공하는 사이트에서는 RSS링크를 아이콘으로 만들어서 찾기 쉽게 해놓고 있습니다.   등의 아이콘이나 “Syndicate this site” “RSS” 등의 글자로 링크를 만들어 놓고 있습니다. 이 링크의 주소를 복사해서 RSS리더기에서 불러오면 해당사이트의 RSS를 구독하게 되는 것입니다. 특정한 아이콘이나 글자링크를 써야만하는 것은 아니어서 사이트마다 조금씩 RSS링크를 지칭하는 아이콘이나 글자가 다르기도 합니다.
RSS를 제공하는 사이트 몇군데 입니다.
오마이뉴스 전체기사
중앙일보 전체기사
네이버 뉴스의 검색결과를 RSS로
드림위즈의 추천 RSS
Some sources of RSS 2.0 feeds. (Technology at Harvard Law)
Top 100 Most-Subscribed-To RSS Feeds (Radio Community Server)

참고링크들
블로그 배급과 구독을 위한 가장 쉬운 방법, RSS (호찬넷)

블로그용어 – RSS와 XML 아이콘, 피드(feed), RSS 구독기 (김중태문화원)

  • RSS 2.0 Specification (Technology at Harvard Law)

  • What is a site feed? (blogger.com)

  • What is RSS? (XML.com)
  • 보충:

    RSS閱讀器로 우의 것을 받을 수 있는데,,뭐看天下든가  周博通RSS阅读器든가 ,,또 외국의 것두 많다,,여기서 나 보기엔   周博通더 좋은것같다..看天下아직 버그(Bug)가 적지 않은것 같거든…

  • 혹시 블로그   뭔 지 모를수 있는데,,블로그는 작년에 제일 인기있는 검색어 였다구 한다. 중국말로는 网络日志라는 의미두 있는데,,,,ㅋㅋ,,,,아래 블로그에 관한 것을 인터넷에서 찾았는데,,,참고로되기바란다…  

    블로그 란?
     
     
     
    웹로그는 웹(web)기록(log)이라는 두 단어가 합쳐 만들어진, 웹 항해기록이라는 뜻의 단어입니다. 인터넷을 항해하다가 발견한 흥미로운 로그에 짧은 코멘트를 덧붙이는 것이 웹로그의 초기형태가 점차 대중화되면서 블로그(blog)라는 명칭을 갖게 되었습니다.

    블로그의 사용자가 점차 증가하면서 블로그가 담는 내용이나 성격 또한 무척 다양해졌습니다. 뉴스나 사회문제, 새로운 기술이나 상품에 관한 소식을 담는 블로그가 있는가 하면, 소박한 일상을 글이나 사진으로 담아내는 블로그도 있습니다. 자신의 취미나 관심분야에 대한 블로그를 만들기도 하고, 가족이나 친구들과의 커뮤니케이션을 목적으로 블로그를 운영하기도 하면서 지금의 블로그는 1인 미디어라는 별칭을 가지게 되었습니다.

    그렇다면 이렇게 다양한 웹페이지들을 하나같이 블로그라고 부를 수 있는 기준은 무엇일까? 블로그는 대체로 다음과 같은 공통적인 특징을 가집니다.

    •  여러 개의 글들이 날짜와 시간에 따라 배열된다.
    • 가장 최근의 글이 가장 위에 올라온다.
    • 글의 길이가 비교적 간결하다.
    • 업데이트 주기가 짧다.


    위와 같은 표면적인 특징 외에 점차 발전하면서 블로그가 갖게 된 가장 큰 매력은 개인이 자신이 만들어가는 자기만의 공간인 동시에 수많은 커넥션이 존재하여 다른 이들과 연결되어 아래의 구조를 형성하며 자연스럽게 커뮤니티가 구성된다는 것입니다.

    경기넷에서 제공하는 GGi 블로그를 이용하여 회원님들이 가지신 다양한 생각과 정보 의 표현과 발산, 공유를 통해 블로그의 매력에 푹 빠져보시기 바랍니다. 

    2005年04月26日
    。写在最前

    本文的内容只想以最通俗的语言说明钩子的使用方法,具体到钩子的详细介绍可以参照下面的网址:

    http://www.microsoft.com/china/community/program/originalarticles/techdoc/hook.mspx

    二。了解一下钩子

    从字面上理解,钩子就是想钩住些东西,在程序里可以利用钩子提前处理些Windows消息。

    例子:有一个Form,Form里有个TextBox,我们想让用户在TextBox里输入的时候,不管敲键盘的哪个键,TextBox里显示的始终为"A",这时我们就可以利用钩子监听键盘消息,先往Windows的钩子链表中加入一个自己写的钩子监听键盘消息,只要一按下键盘就会产生一个键盘消息,我们的钩子在这个消息传到TextBox之前先截获它,让TextBox显示一个"A",之后结束这个消息,这样TextBox得到的总是"A"。

    消息截获顺序:既然是截获消息,总要有先有后,钩子是按加入到钩子链表的顺序决定消息截获顺序。就是说最后加入到链表的钩子最先得到消息。

    截获范围:钩子分为线程钩子和全局钩子,线程钩子只能截获本线程的消息,全局钩子可以截获整个系统消息。我认为应该尽量使用线程钩子,全局钩子如果使用不当可能会影响到其他程序。




    三。开始通俗

        这里就以上文提到的简单例子做个线程钩子。

    第一步:声明API函数

    使用钩子,需要使用WindowsAPI函数,所以要先声明这些API函数。

    // 安装钩子
    [DllImport("user32.dll",CharSet=CharSet.Auto, CallingConvention=CallingConvention.StdCall)]
    public static extern int SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hInstance, int threadId);
    // 卸载钩子
    [DllImport("user32.dll",CharSet=CharSet.Auto, CallingConvention=CallingConvention.StdCall)]
    public static extern bool UnhookWindowsHookEx(int idHook);
    // 继续下一个钩子
    [DllImport("user32.dll",CharSet=CharSet.Auto, CallingConvention=CallingConvention.StdCall)]
    public static extern int CallNextHookEx(int idHook, int nCode, Int32 wParam, IntPtr lParam); 
    // 取得当前线程编号
    [DllImport("kernel32.dll")]
    static extern int GetCurrentThreadId();

    声明一下API函数,以后就可以直接调用了。

    第二步:声明、定义。

    public delegate int HookProc(int nCode, Int32 wParam, IntPtr lParam);
    static int hKeyboardHook = 0;
    HookProc KeyboardHookProcedure;

    先解释一下委托,钩子必须使用标准的钩子子程,钩子子程就是一段方法,就是处理上面例子中提到的让TextBox显示"A"的操作。

    钩子子程必须按照HookProc(int nCode, Int32 wParam, IntPtr lParam)这种结构定义,三个参数会得到关于消息的数据。

    当使用SetWindowsHookEx函数安装钩子成功后会返回钩子子程的句柄,hKeyboardHook变量记录返回的句柄,如果hKeyboardHook不为0则说明钩子安装成功。

    第三步:写钩子子程

    钩子子程就是钩子所要做的事情。

    private int KeyboardHookProc(int nCode, Int32 wParam, IntPtr lParam)
    {
        if (nCode >= 0)
        {
            textbox1.Text = "A";
            return 1;
        }
        return CallNextHookEx(hKeyboardHook, nCode, wParam, lParam);
    }

    我们写一个方法,返回一个int值,包括三个参数。如上面给出的代码,符合钩子子程的标准。

    nCode参数是钩子代码,钩子子程使用这个参数来确定任务,这个参数的值依赖于Hook类型。

    wParam和lParam参数包含了消息信息,我们可以从中提取需要的信息。

    方法的内容可以根据需要编写,我们需要TextBox显示"A",那我们就写在这里。当钩子截获到消息后就会调用钩子子程,这段程序结束后才往下进行。截获的消息怎么处理就要看子程的返回值了,如果返回1,则结束消息,这个消息到此为止,不再传递。如果返回0或调用CallNextHookEx函数则消息出了这个钩子继续往下传递,也就是传给消息真正的接受者。

    第四步:安装钩子、卸载钩子

    准备工作都完成了,剩下的就是把钩子装入钩子链表。

    我们可以写两个方法在程序中合适位置调用。代码如下:

    // 安装钩子
    public void HookStart()
    {
        if(hMouseHook == 0)
        {
            // 创建HookProc实例
            MouseHookProcedure = new HookProc(MouseHookProc);
            // 设置线程钩子
            hMouseHook = SetWindowsHookEx( 2, KeyboardHookProcedure, IntPtr.Zero,
                                          GetCurrentThreadId());
            // 如果设置钩子失败
            if(hMouseHook == 0 )   
            {
                HookStop();
                throw new Exception("SetWindowsHookEx failed.");
            }
        }
    }
    // 卸载钩子
    public void HookStop()
    {
        bool retKeyboard = true;
        if(hKeyboardHook != 0)
        {
            retKeyboard = UnhookWindowsHookEx(hKeyboardHook);
            hKeyboardHook = 0;
        }
        if (!(retMouse && retKeyboard)) throw new Exception("UnhookWindowsHookEx
                                                            failed.");
    }

    安装钩子和卸载钩子关键就是SetWindowsHookEx和UnhookWindowsHookEx方法。

    SetWindowsHookEx (int idHook, HookProc lpfn, IntPtr hInstance, int threadId) 函数将钩子加入到钩子链表中,说明一下四个参数:

    idHook 钩子类型,即确定钩子监听何种消息,上面的代码中设为2,即监听键盘消息并且是线程钩子,如果是全局钩子监听键盘消息应设为13,线程钩子监听鼠标消息设为7,全局钩子监听鼠标消息设为14。

    lpfn 钩子子程的地址指针。如果dwThreadId参数为0 或是一个由别的进程创建的线程的标识,lpfn必须指向DLL中的钩子子程。 除此以外,lpfn可以指向当前进程的一段钩子子程代码。钩子函数的入口地址,当钩子钩到任何消息后便调用这个函数。

    hInstance 应用程序实例的句柄。标识包含lpfn所指的子程的DLL。如果threadId 标识当前进程创建的一个线程,而且子程代码位于当前进程,hInstance必须为NULL。可以很简单的设定其为本应用程序的实例句柄。

    threaded 与安装的钩子子程相关联的线程的标识符。如果为0,钩子子程与所有的线程关联,即为全局钩子。

    上面代码中的SetWindowsHookEx方法安装的是线程钩子,用GetCurrentThreadId()函数得到当前的线程ID,钩子就只监听当前线程的键盘消息。

    UnhookWindowsHookEx (int idHook) 函数用来卸载钩子,卸载钩子与加入钩子链表的顺序无关,并非后进先出。

    四。节外生枝

          

    安装全局钩子

    上文使用的是线程钩子,如果要使用全局钩子在钩子的安装上略有不同。如下:

    SetWindowsHookEx( 13,KeyboardHookProcedure,
              Marshal.GetHINSTANCE(Assembly.GetExecutingAssembly().GetModules()[0]),0)

    这条语句即定义全局钩子。

    子程消息处理

           钩子子程可以得到两个关于消息信息的参数wPrama、lParam。怎么将这两个参数转成我们更容易理解的消息呢。

           对于鼠标消息,我们可以定义下面这个结构:

    public struct MSG 

        public Point p;
        public IntPtr HWnd;
        public uint wHitTestCode;
        public int dwExtraInfo;
    }

          

    对于键盘消息,我们可以定义下面这个结构:

    public struct KeyMSG
    {
        public int vkCode; 
        public int scanCode;
        public int flags; 
        public int time;
        public int dwExtraInfo;

    }

    然后我们可以在子程里用下面语句将lParam数据转换成MSG或KeyMSG结构数据

    MSG m = (MSG) Marshal.PtrToStructure(lParam, typeof(MSG));
    KeyMSG m = (KeyMSG) Marshal.PtrToStructure(lParam, typeof(KeyMSG));

          

    这样可以更方便的得到鼠标消息或键盘消息的相关信息,例如p即为鼠标坐标,HWnd即为鼠标点击的控件的句柄,vkCode即为按键代码。

    注:这条语句对于监听鼠标消息的线程钩子和全局钩子都可以使用,但对监听键盘消息的线程钩子使用会出错,目前在找原因。

           如果是监听键盘消息的线程钩子,我们可以根据lParam值的正负确定按键是按下还是抬起,根据wParam值确定是按下哪个键。

    // 按下的键
    Keys keyData = (Keys)wParam;
    if(lParam.ToInt32() > 0)       
    {
        // 键盘按下
    }
    if(lParam.ToInt32() < 0)       
    {
        // 键盘抬起

    }

          

    如果是监听键盘消息的全局钩子,按键是按下还是抬起要根据wParam值确定。

    wParam = = 0×100  // 键盘按下

    wParam = = 0×101  // 键盘抬起

    五。写在最后

    钩子的基本用法都介绍完了,总结一下,钩子就是从正常的消息作业中把要监听的消息钩出来,进入到钩子子程进行一些操作,之后再放回到正常的作业中或结束该消息



       오 늘 ㅊ ㅓ 음 만든 ㄴ ㅏ 의 블 로 그  인   데 많    ㅇ ㅣ 와  주 구 ㅈ ㅣ 지 해 주ㄱ ㅣ 를 ㅂ ㅏ 란  다..~~~~

              欢 迎 到 我 的 博 客 来   !~~