精华内容
下载资源
问答
  • Windows经典网络编程

    2013-08-03 23:51:05
    内容很全面,具有丰富实例的windows网络编程书籍
  • windows 网络编程

    2009-09-19 14:56:42
    这是一本详细讲解windows网络编程技术的书籍,详细讲解了windows网络编程技术
  • windows 网络编程技术

    2009-12-11 11:14:57
    关于windows网络编程十分经典的书籍,对开发高质量的windows网络程序必读
  • 网络编程书籍

    千次阅读 2014-10-27 10:09:41
    C,C++网络编程学习简明指南 1. 扎实的C,C++基础知识 参考资料《C程序设计》,《C++ primer》。 2. TCP/IP协议 经典书是:W.Richard Stevens 著《TCP/IP详解》三卷书,卷1是协议,卷2是实现,卷3是TCP...
    C,C++网络编程学习简明指南
    1. 扎实的C,C++基础知识

    参考资料《C程序设计》,《C++ primer》。

    2. TCP/IP协议
    经典书是:W.Richard Stevens 著《TCP/IP详解》三卷书,卷1是协议,卷2是实现,卷3是TCP事务协议等。还有官方的协议文档:RFC
    当然也可以在网上下载电子书。
    经典的开源协议分析工具:Wireshark.
    简单的开源TCP/IP协议栈:LwIP,或者Linux 1.0里包含的协议栈,当然也可以看看FreeBSD的TCP/IP协议栈。

    3. 实际指导网络编程的书

    Winodws平台,经典书是《windows网络编程》第二版。
    Linux平台,经典书是W.Richard Stevens 著《UNIX网络编程》。

    4. VC++开发
    侯捷著 《深入浅出MFC》, 《windows程序设计》,《Windows核心编程》,Microsoft的MSDN。

    PS. 我自己用VC++6.0企业版,安装了MSDN 2001的版本。

    我抛砖引玉,请大家积极发言。              
    展开全文
  • 都是我看过或正要看的书,晾晾书架,希望对后来者也有一点作用,当年我也是浪费时间看了一些没有价值的书籍,颇为后悔,现将精华总结...  3:《TCP/IP协议及网络编程技术》 罗军舟等编着 清华大学出版社 国货上品,
  • Windows 网络编程技术

    2010-01-28 10:23:37
    作者:Anthony jones Jim Ohlund 关于WINDOWS网络编程的优秀书籍
  • Windows网络编程技术、Windows网络编程
    Windows网络编程技术、Windows网络编程
    展开全文
  • Windows网络编程

    万次阅读 2007-12-13 16:58:00
    第一章 序言 我写这个专题的目的,一方面是为了通过对网络编程再一次系统的总结,提高自己的网络编程水平,特别是Windows下的网络编程水平。同时,我也希望,能为众多初学网络编程的人提供一点帮助,因为我开始学习...

    第一章 序言

     

                  我写这个专题的目的,一方面是为了通过对网络编程再一次系统的总结,提高自己的网络编程水平,特别是Windows下的网络编程水平。同时,我也希望,能为众多初学网络编程的人提供一点帮助,因为我开始学习网络编程的时候,能找到的资料就很少。当然,花钱可以买到翻译版本的书:)

                  首先向大家推荐一本很好的参考书,Network Programming for Microsoft Windows 2nd

    初学网络编程的时候我还不知道有这样一本好书,只是上各大论坛把能找到的网络编程方面的文章和代码下载下来,然后自己研究。后来看到别人推荐这一本书,下载了一个,看了感觉非常好,里面的内容写得很规范,条理也很清楚,英文好的朋友可以直接阅读,不然就只好去弄一本翻译好的来研究了。、

                  我试着从Windows编程的基础开始,一直到探索建立高性能的网络应用程序。我说过,我并不是以高手的身份写这本书,而是以和大家一起学习的心态学习网络编程,写书只是让自己的思路更清晰,以后还可以翻阅。所以,我不保证书中所有的内容都是绝对正确和标准的,有不妥的地方,还希望高手批评指正。

                  这本书是完全免费的,读者可以任意使用书中的代码。但是如果需要转载,请注明原作者和出处。如果有商业运作的需求,请直接和我联系。

     

     

    第二章 Windows网络编程基础

     

                  这本书主要探索Windows网络编程,开发平台是Windows 2000 Visual C++.NET,从一个合格的C++程序员到网络编程高手,还是需要花不少功夫,至少我认为写一个聊天程序很简单,而要写一个能同时响应成千上万用户的高性能网络程序,的确不容易。这篇文章所介绍的方法也并不是能直接应用于每一个具体的应用程序,只能作为学习的参考资料。

                  开发高性能网络游戏恐怕是促使很多程序员研究网络编程的原因(包括我),现在的大型网络游戏对同时在线人数的要求比较高,真正的项目往往采取多个服务器(组)负荷分担的方式工作,我将首先把注意力放到单个服务器的情况。

                  大家都知道,我们用得最多的协议是UDPTCPUDP是不可靠传输服务,TCP是可靠传输服务。UDP就像点对点的数据传输一样,发送者把数据打包,包上有收信者的地址和其他必要信息,至于收信者能不能收到,UDP协议并不保证。而TCP协议就像(实际他们是一个层次的网络协议)是建立在UDP的基础上,加入了校验和重传等复杂的机制来保证数据可靠的传达到收信者。关于网络协议的具体内容,读者可以参考专门介绍网络协议的书籍,或者查看RFC中的有关内容。本书直接探讨编程实现网络程序的问题。

                 

     

     

    21 Window Socket介绍

     

                  Windows Socket是从UNIX Socket继承发展而来,最新的版本是2.2。进行Windows网络编程,你需要在你的程序中包含WINSOCK2.HMSWSOCK.H,同时你需要添加引入库WS2_32. LIBWSOCK32.LIB。准备好后,你就可以着手建立你的第一个网络程序了。

                  Socket编程有阻塞和非阻塞两种,在操作系统I/O实现时又有几种模型,包括SelectWSAAsyncSelectWSAEventSelect IO重叠模型,完成端口等。要学习基本的网络编程概念,可以选择从阻塞模式开始,而要开发真正实用的程序,就要进行非阻塞模式的编程(很难想象一个大型服务器采用阻塞模式进行网络通信)。在选择I/O模型时,我建议初学者可以从WSAAsyncSelect模型开始,因为它比较简单,而且有一定的实用性。但是,几乎所有人都认识到,要开发同时响应成千上万用户的网络程序,完成端口模型是最好的选择。

                  既然完成端口模型是最好的选择,那为什么我们不直接写出一个使用完成端口的程序,然后大家稍加修改就OK了。我认为这确实是一个好的想法,但是真正做项目的时候,不同的情况对程序有不同的要求,如果不深入学习网络编程的各方面知识,是不可能写出符合要求的程序,在学习网络编程以前,我建议读者先学习一下网络协议。

     

     

     

    22 第一个网络程序

     

    由于服务器/客户端模式的网络应用比较多,而且服务器端的设计是重点和难点。所以我想首先探讨服务器的设计方法,在完成服务器的设计后再探讨其他模式的网络程序。

    设计一个基本的网络服务器有以下几个步骤:

    1、初始化Windows Socket

    2、创建一个监听的Socket

    3、设置服务器地址信息,并将监听端口绑定到这个地址上

    4、开始监听

    5、接受客户端连接

    6、和客户端通信

    7、结束服务并清理Windows Socket和相关数据,或者返回第4

     

                  我们可以看出设计一个最简单的服务器并不需要太多的代码,它完全可以做一个小型的聊天程序,或进行数据的传输。但是这只是我们的开始,我们的最终目的是建立一个有大规模响应能力的网络服务器。如果读者对操作系统部分的线程使用还有疑问,我建议你现在就开始复习,因为我们经常使用线程来提高程序性能,其实线程就是让CPU不停的工作,而不是总在等待I/O,或者是一个CPI,累死了还是一个CPU。千万不要以为线程越多的服务器,它的性能就越好,线程的切换也是需要消耗时间的,对于I/O等待少的程序,线程越多性能反而越低。

                  下面是简单的服务器和客户端源代码。(阻塞模式下的,供初学者理解)

     

     

    TCPServer

     

    #include <winsock2.h>
       
    
      
        
      
    void main(void)
       
    {
       
       WSADATA              wsaData;
       
       SOCKET               ListeningSocket;
       
       SOCKET               NewConnection;
       
       SOCKADDR_IN          ServerAddr;
       
       SOCKADDR_IN          ClientAddr;
       
       int                  Port = 5150;
       
       
       
       // 初始化Windows Socket 2.2
       
       WSAStartup(MAKEWORD(2,2), &wsaData);
       
       
       
       // 创建一个新的Socket来响应客户端的连接请求
       
       ListeningSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
       
       
       
       // 填写服务器地址信息
       
       // 端口为5150
       
       // IP地址为INADDR_ANY,注意使用htonlIP地址转换为网络格式
       
       ServerAddr.sin_family = AF_INET;
       
       ServerAddr.sin_port = htons(Port);    
       
       ServerAddr.sin_addr.s_addr = htonl(INADDR_ANY);
       
              
       
       // 绑定监听端口
       
       bind(ListeningSocket, (SOCKADDR *)&ServerAddr, sizeof(ServerAddr));
       
    
      
        
      
       // 开始监听,指定最大同时连接数为5
       
          listen(ListeningSocket, 5); 
       
    
      
        
      
       // 接受新的连接
       
       NewConnection = accept(ListeningSocket, (SOCKADDR *) &ClientAddr,&ClientAddrLen));
       
    
      
        
      
       // 新的连接建立后,就可以互相通信了,在这个简单的例子中,我们直接关闭连接,
       
       // 并关闭监听Socket,然后退出应用程序
       
       //  
       
          closesocket(NewConnection);
       
          closesocket(ListeningSocket);
       
    
      
        
      
       // 释放Windows Socket DLL的相关资源
       
          WSACleanup();
       
    }
       

     

     

    TCPClient

     

    # include <winsock2.h>
       
    
      
        
      
    void main(void)
       
    {
       
       WSADATA              wsaData;
       
       SOCKET               s;
       
       SOCKADDR_IN          ServerAddr;
       
       int                  Port = 5150;
       
       
       
       //初始化Windows Socket 2.2
       
       WSAStartup(MAKEWORD(2,2), &wsaData);
       
       
       
       // 创建一个新的Socket来连接服务器
       
          s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
       
       
       
       // 填写客户端地址信息
       
       // 端口为5150
       
       // 服务器IP地址为"136.149.3.29",注意使用inet_addrIP地址转换为网络格式
       
          ServerAddr.sin_family = AF_INET;
       
          ServerAddr.sin_port = htons(Port);    
       
          ServerAddr.sin_addr.s_addr = inet_addr("136.149.3.29");
       
    
      
        
      
       // 向服务器发出连接请求
       
          connect(s, (SOCKADDR *) &ServerAddr, sizeof(ServerAddr)); 
       
          
       
       // 新的连接建立后,就可以互相通信了,在这个简单的例子中,我们直接关闭连接,
       
       // 并关闭监听Socket,然后退出应用程序
       
          closesocket(s);
       
    
      
        
      
       // 释放Windows Socket DLL的相关资源
       
          WSACleanup();
       

    }

     

    23 WSAAsyncSelect模式

                  前面说过,Windows网络编程模式有好几种,他们各有特点,实现起来复杂程度各不相同,适用范围也不一样。下图是Network Programming for Microsoft Windows 2nd 一书中对不同模式的一个性能测试结果。服务器采用Pentium 4 1.7 GHz XeonCPU768M内存;客户端有3PC,配置分别是Pentium 2 233MHz 128 MB 内存,Pentium 2 350 MHz 128 MB内存,Itanium 733 MHz 1 GB内存。

                  具体的结果分析大家可以看看原书中作者的叙述,我关心的是哪种模式是我需要的。首先是服务器,勿庸置疑,肯定是完成端口模式。那么客户端呢,当然也可以采用完成端口,但是不同模式是在不同的操作系统下支持的,看下图:

                  完成端口在Windows 98下是不支持的,虽然我们可以假定所有的用户都已经装上了Windows 2000Windows XP,。但是,如果是商业程序,这种想法在现阶段不应该有,我们不能让用户为了使用我们的客户端而去升级他的操作系统。Overlapped I/O可以在Windows 98下实现,性能也不错,但是实现和理解起来快赶上完成端口了。而且,最关键的一点,客户端程序不是用来进行大规模网络响应的,客户端的主要工作应该是进行诸如图形运算等非网络方面的任务。原书作者,包括我强烈推荐大家使用WSAAsyncSelect模式实现客户端,因为它实现起来比较直接和容易,而且他完全可以满足客户端编程的需求。

                  下面是一段源代码,虽然我们是用它来写客户端,我还是把它的服务端代码放上来,一方面是有兴趣的朋友可以用他做测试和了解如何用它实现服务器;另一方面是客户端的代码可以很容易的从它修改而成,不同的地方只要参考一下2.1节里的代码就知道了。

     

    #define WM_SOCKET WM_USER + 1
       
    #include <winsock2.h>
       
    #include <windows.h>
       
    
      
        
      
    int WINAPI WinMain(HINSTANCE hInstance, 
       
        HINSTANCE hPrevInstance, LPSTR lpCmdLine,
       
        int nCmdShow)
       
    {
       
        WSADATA wsd;
       
        SOCKET Listen;
       
        SOCKADDR_IN InternetAddr;
       
        HWND Window;
       
        // 创建主窗口
       
    
      
        
      
        Window = CreateWindow();
       
        // 初始化Windows Socket 2.2
       
    WSAStartup(MAKEWORD(2,2), &wsd);
       
    
      
        
      
    // 创建监听Socket
       
        Listen = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP);
       
    
      
        
      
        // 设置服务器地址
       
        InternetAddr.sin_family = AF_INET;
       
        InternetAddr.sin_addr.s_addr = htonl(INADDR_ANY);
       
        InternetAddr.sin_port = htons(5150);
       
    
      
        
      
        // 绑定Socket
       
        bind(Listen, (PSOCKADDR) &InternetAddr, sizeof(InternetAddr));
       
    
      
        
      
        // 设置Windows消息,这样当有Socket事件发生时,窗口就能收到对应的消息通知
       
    // 服务器一般设置 FD_ACCEPT │ FD_READ | FD_CLOSE
       
    // 客户端一般设置 FD_CONNECT │ FD_READ | FD_CLOSE
       
        WSAAsyncSelect(Listen, Window, WM_SOCKET, FD_ACCEPT │ FD_READ | FD_CLOSE);
       
    
      
        
      
       // 开始监听
       
       listen(Listen, 5);
       
    
      
        
      
        // Translate and dispatch window messages
       
        // until the application terminates
       
        while (1) {
       
         // ...
       
     }
       
    }
       
    
      
        
      
    BOOL CALLBACK ServerWinProc(HWND hDlg,UINT wMsg,
       
        WPARAM wParam, LPARAM lParam)
       
    {
       
        SOCKET Accept;
       
    
      
        
      
        switch(wMsg)
       
        {
       
            case WM_PAINT:
       
                // Process window paint messages
       
                break;
       
    
      
        
      
            case WM_SOCKET:
       
                // Determine whether an error occurred on the
       
                // socket by using the WSAGETSELECTERROR() macro
       
    
      
        
      
                if (WSAGETSELECTERROR(lParam))
       
                {
       
                     // Display the error and close the socket
       
                    closesocket( (SOCKET) wParam);
       
                    break;
       
                }
       
    
      
        
      
                // Determine what event occurred on the
       
                // socket
       
    
      
        
      
                switch(WSAGETSELECTEVENT(lParam))
       
                {
       
                    case FD_ACCEPT:
       
    
      
        
      
                        // Accept an incoming connection
       
                        Accept = accept(wParam, NULL, NULL);
       
    
      
        
      
                        // Prepare accepted socket for read,
       
                        // write, and close notification
       
    
      
        
      
                        WSAAsyncSelect(Accept, hDlg, WM_SOCKET,
       
                            FD_READ │ FD_WRITE │ FD_CLOSE);
       
                        break;
       
    
      
        
      
                    case FD_READ:
       
                        // Receive data from the socket in
       
                        // wParam
       
                        break;
       
    
      
        
      
                    case FD_WRITE:
       
                        // The socket in wParam is ready
       
                        // for sending data
       
                        break;
       
    
      
        
      
                    case FD_CLOSE:
       
                        // The connection is now closed
       
                        closesocket( (SOCKET)wParam);
       
                        break;
       
                }
       
                break;
       
        }
       
        return TRUE;
       

    }

     

     

    24 小节

                  目前为止,我非常简要的介绍了Windows网络编程的一些东西,附上了一些源代码。可以说,读者特别是初学者,看了后不一定就能马上写出程序来,而那些代码也不是可以直接应用于实际的项目。别急,万里长征才开始第一步呢,很多书里都是按照基础到应用的顺序来写的,但是我喜欢更直接一点,更实用一些的方式。而且,我写的这个专题,毕竟不是商业化的,时间上不能投入过多,只是作为给初学者的一个小小帮助。更多的还是希望读者自己刻苦研究,有问题的时候可以到我的论坛上给我留言,以后有机会我也会公布一些实际的代码。希望结交更多热爱编程和中国游戏事业的朋友。下一章里我将主要讲解完成端口编程,这也是我写这篇文章的初衷,希望对大家能有所帮助。

     

     

     

    第三章 完成端口模式下的高性能网络服务器

     

    31开始

                  完成端口听起来好像很神秘和复杂,其实并没有想象的那么难。这方面的文章在论坛上能找到的我差不多都看过,写得好点的就是CSDN.NET上看到的一组系列文章,不过我认为它只是简单的翻译了一下Network Programming for Microsoft Windows 2nd 中的相关内容,附上的代码好像不是原书中的,可能是另一本外文书里的。我看了以后,觉得还不如看原版的更容易理解。所以在我的开始部分,我主要带领初学者理解一下完成端口的有关内容,是我开发的经验,其他的请参考原书的相关内容。

                  采用完成端口的好处是,操作系统的内部重叠机制可以保证大量的网络请求都被服务器处理,而不是像WSAAsyncSelect WSAEventSelect的那样对并发的网络请求有限制,这一点从上一章的测试表格中可以清楚的看出。

                  完成端口就像一种消息通知的机制,我们创建一个线程来不断读取完成端口状态,接收到相应的完成通知后,就进行相应的处理。其实感觉就像WSAAsyncSelect一样,不过还是有一些的不同。比如我们想接收消息,WSAAsyncSelect会在消息到来的时候直接通知Windows消息循环,然后就可以调用WSARecv来接收消息了;而完成端口则首先调用一个WSARecv表示程序需要接收消息(这时可能还没有任何消息到来),但是只有当消息来的时候WSARecv才算完成,用户就可以处理消息了,然后再调用一个WSARecv表示等待下一个消息,如此不停循环,我想这就是完成端口的最大特点吧。

                  Per-handle Data Per-I/O Operation Data 是两个比较重要的概念,Per-handle Data用来把客户端数据和对应的完成通知关联起来,这样每次我们处理完成通知的时候,就能知道它是哪个客户端的消息,并且可以根据客户端的信息作出相应的反应,我想也可以理解为Per-Client handle Data吧。Per-I/O Operation Data则不同,它记录了每次I/O通知的信息,比如接收消息时我们就可以从中读出消息的内容,也就是和I/O操作有关的信息都记录在里面了。当你亲手实现完成端口的时候就可以理解他们的不同和用途了。

                  CreateIoCompletionPort函数中有个参数NumberOfConcurrentThreads,完成端口编程里有个概念Worker Threads。这里比较容易引起混乱,NumberOfConcurrentThreads需要设置多少,又需要创建多少个Worker Threads才算合适?NumberOfConcurrentThreads的数目和CPU数量一样最好,因为少了就没法利用多CPU的优势,而多了则会因为线程切换造成性能下降。Worker Threads的数量是不是也要一样多呢,当然不是,它的数量取决于应用程序的需要。举例来说,我们在Worker Threads里进行消息处理,如果这个过程中有可能会造成线程阻塞,那如果我们只有一个Worker Thread,我们就不能很快响应其他客户端的请求了,而只有当这个阻塞操作完成了后才能继续处理下一个完成消息。但是如果我们还有其他的Worker Thread,我们就能继续处理其他客户端的请求,所以到底需要多少的Worker Thread,需要根据应用程序来定,而不是可以事先估算出来的。如果工作者线程里没有阻塞操作,对于某些情况来说,一个工作者线程就可以满足需要了。

                  其他问题,Network Programming for Microsoft Windows 2nd中,作者还提出了如何安全的退出应用程序等等实现中的细节问题,这里我就不一一讲述了,请读者参考原书的相关内容,如果仍有疑问,可以联系我。

     

     

     

    32实现

    下面是一般的实现步骤

    1.    获得计算机信息,得到CPU的数量。创建一个完成端口,第四个参数置0,指定NumberOfConcurrentThreadsCPU个数。

    2.  Determine how many processors exist on the system.

    3.  Create worker threads to service completed I/O requests on the completion port using processor information in step 2. In the case of this simple example, we create one worker thread per processor because we do not expect our threads to ever get in a suspended condition in which there would not be enough threads to execute for each processor. When the CreateThread function is called, you must supply a worker routine that the thread executes upon creation. We will discuss the worker thread's responsibilities later in this section.

    4.  Prepare a listening socket to listen for connections on port 5150.

    5.  Accept inbound connections using the accept function.

    6.  Create a data structure to represent per-handle data and save the accepted socket handle in the structure.

    7.  Associate the new socket handle returned from accept with the completion port by calling CreateIoCompletionPort. Pass the per-handle data structure to CreateIoCompletionPort via the completion key parameter.

    8.  Start processing I/O on the accepted connection. Essentially, you want to post one or more asynchronous WSARecv or WSASend requests on the new socket using the overlapped I/O mechanism. When these I/O requests complete, a worker thread services the I/O requests and continues processing future I/O requests, as we will see later in the worker routine specified in step 3.

    9.  Repeat steps 5–8 until server terminates.

     

    那么学习完成端口编程从哪里开始比较好,对于初学者而言,直接进入编程并不是一个好主意,我建议初学者首先学习用异步Socket模式,即WSAEventSelect模式构建一个简单的聊天服务器。当把Windows网络编程的概念有一个清晰的认识之后,再深入研究完成端口编程。

    接着就是深入研究具体的编程实现了,从Network Programming for Microsoft Windows 2nd中摘录的这段经典代码可以说是非常合适的,这里我只简单解释一下其中比较关键的地方,还有不明白的可以参看原书,或者联系我。

     

    主程序段:

    1.    HANDLE CompletionPort;
       
    2.    WSADATA wsd;
       
    3.    SYSTEM_INFO SystemInfo;
       
    4.    SOCKADDR_IN InternetAddr;
       
    5.    SOCKET Listen;
       
    6.    int i;
       
    7.    
      
        
      
    8.    typedef struct _PER_HANDLE_DATA 
       
    9.    {
       
    10.  SOCKET          Socket;
       
    11.  SOCKADDR_STORAGE  ClientAddr;
       
    12.  // 在这里还可以加入其他和客户端关联的数据
        
    13.} PER_HANDLE_DATA, * LPPER_HANDLE_DATA;
       
    14.
       
         
       
    15.// 初始化Windows Socket 2.2
       
    16.StartWinsock(MAKEWORD(2,2), &wsd);
       
    17.
       
         
       
    18.// Step 1:
       
    19.// 创建完成端口
        
    20.
       
         
       
    21.CompletionPort = CreateIoCompletionPort(
       
    22.    INVALID_HANDLE_VALUE, NULL, 0, 0);
       
    23.
       
         
       
    24.// Step 2:
       
    25.// 检测系统信息
        
    26.
       
         
       
    27.GetSystemInfo(&SystemInfo);
       
    28.
       
         
       
    29.// Step 3: 创建工作者线程,数量和CPU的数量一样多
        
    30.// Create worker threads based on the number of
       
    31.// processors available on the system. For this
       
    32.// simple case, we create one worker thread for each
       
    33.// processor.
       
    34.
       
         
       
    35.for(i = 0; i < SystemInfo.dwNumberOfProcessors; i++)
       
    36.{
       
    37.    HANDLE ThreadHandle;
       
    38.
       
         
       
    39.    // Create a server worker thread, and pass the
       
    40.    // completion port to the thread. NOTE: the
       
    41.    // ServerWorkerThread procedure is not defined
       
    42.    // in this listing.
       
    43.
       
         
       
    44.    ThreadHandle = CreateThread(NULL, 0,
       
    45.        ServerWorkerThread, CompletionPort,
       
    46.        0, NULL;
       
    47.
       
         
       
    48.    // Close the thread handle
       
    49.    CloseHandle(ThreadHandle);
       
    50.}
       
    51.
       
         
       
    52.// Step 4:
       
    53.// 创建监听Socket
       
    54.
       
         
       
    55.Listen = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0,
       
    56.    WSA_FLAG_OVERLAPPED);
       
    57.
       
         
       
    58.InternetAddr.sin_family = AF_INET;
       
    59.InternetAddr.sin_addr.s_addr = htonl(INADDR_ANY);
       
    60.InternetAddr.sin_port = htons(5150);
       
    61.bind(Listen, (PSOCKADDR) &InternetAddr,
       
    62.    sizeof(InternetAddr));
       
    63.
       
         
       
    64.// 开始监听
        
    65.
       
         
       
    66.listen(Listen, 5);
       
    67.
       
         
       
    68.while(TRUE)
       
    69.{
       
    70.    PER_HANDLE_DATA *PerHandleData=NULL;
       
    71.    SOCKADDR_IN saRemote;
       
    72.    SOCKET Accept;
       
    73.    int RemoteLen;
       
    74.    // Step 5: 等待客户端连接,然后将客户端Socket加入完成端口
        
    75.    // Accept connections and assign to the completion
       
    76.    // port
       
    77.
       
         
       
    78.    RemoteLen = sizeof(saRemote);
       
    79.    Accept = WSAAccept(Listen, (SOCKADDR *)&saRemote, 
       
    80.    &RemoteLen);
       
    81.
       
         
       
    82.    // Step 6: 初始化客户端数据
        
    83.    // Create per-handle data information structure to 
       
    84.    // associate with the socket
       
    85.    PerHandleData = (LPPER_HANDLE_DATA) 
       
    86.        GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA));
       
    87.
       
         
       
    88.    printf("Socket number %d connected/n", Accept);
       
    89.    PerHandleData->Socket = Accept;
       
    90.    memcpy(&PerHandleData->ClientAddr, &saRemote, RemoteLen);
       
    91.
       
         
       
    92.    // Step 7:
       
    93.    // Associate the accepted socket with the
       
    94.    // completion port
       
    95.
       
         
       
    96.    CreateIoCompletionPort((HANDLE) Accept,
       
    97.        CompletionPort, (DWORD) PerHandleData, 0);
       
    98.
       
         
       
    99.    // Step 8: 发出对客户端的I/O请求,等待完成消息
        
    100.       //  Start processing I/O on the accepted socket.
       
    101.       //  Post one or more WSASend() or WSARecv() calls
       
    102.       //  on the socket using overlapped I/O.
       
    103.       WSARecv(...);
       
    104.   }
       
    105.   
      
        
      
    106.       
       

     

     

    工作者线程

     

    DWORD WINAPI ServerWorkerThread(LPVOID CompletionPortID)
       
    {
       
        HANDLE CompletionPort = (HANDLE) CompletionPortID;
       
        DWORD BytesTransferred;
       
        LPOVERLAPPED Overlapped;
       
        LPPER_HANDLE_DATA PerHandleData;
       
        LPPER_IO_DATA PerIoData;
       
        DWORD SendBytes, RecvBytes;
       
        DWORD Flags;
       
        
       
        while(TRUE)
       
        {
       
            // 等待完成端口消息,未收到消息德时候则阻塞线程
        
              
       
            ret = GetQueuedCompletionStatus(CompletionPort,
       
                &BytesTransferred,(LPDWORD)&PerHandleData,
       
                (LPOVERLAPPED *) &PerIoData, INFINITE);
       
    
      
        
      
            // First check to see if an error has occurred
       
            // on the socket; if so, close the 
       
            // socket and clean up the per-handle data
       
            // and per-I/O operation data associated with
       
            // the socket
       
    
      
        
      
            if (BytesTransferred == 0 &&
       
                (PerIoData->OperationType == RECV_POSTED ││
       
                 PerIoData->OperationType == SEND_POSTED))
       
            {
       
                // A zero BytesTransferred indicates that the
       
                // socket has been closed by the peer, so
       
                // you should close the socket. Note: 
       
                // Per-handle data was used to reference the
       
                // socket associated with the I/O operation.
       
     
       
                closesocket(PerHandleData->Socket);
       
    
      
        
      
                GlobalFree(PerHandleData);
       
                GlobalFree(PerIoData);
       
                continue;
       
            }
       
    
      
        
      
            // Service the completed I/O request. You can
       
            // determine which I/O request has just
       
            // completed by looking at the OperationType
       
            // field contained in the per-I/O operation data.
       
             if (PerIoData->OperationType == RECV_POSTED)
       
            {
       
                // Do something with the received data
       
                // in PerIoData->Buffer
       
            }
       
    
      
        
      
            // Post another WSASend or WSARecv operation.
       
            // As an example, we will post another WSARecv()
       
            // I/O operation.
       
    
      
        
      
            Flags = 0;
       
    
      
        
      
            // Set up the per-I/O operation data for the next
       
            // overlapped call
       
            ZeroMemory(&(PerIoData->Overlapped),
       
                sizeof(OVERLAPPED));
       
    
      
        
      
            PerIoData->DataBuf.len = DATA_BUFSIZE;
       
            PerIoData->DataBuf.buf = PerIoData->Buffer;
       
            PerIoData->OperationType = RECV_POSTED;
       
    
      
        
      
            WSARecv(PerHandleData->Socket, 
       
                &(PerIoData->DataBuf), 1, &RecvBytes,
       
                &Flags, &(PerIoData->Overlapped), NULL);
       
        }
       

    }

     

     

    33 小节

     

                  讲这么点就完了?你一定认为我介绍的东西并没有超过原书中的内容,实事上完成端口编程的精髓就是上面的代码和原书中的有关叙述。如果我再把他们完整的重复一遍,那又有什么意思呢?根据我的经验,设计网络服务器的真正难点,不在于完成端口技术,所以我想利用小节把自己编程中的一些经验告诉大家。

                  首先是服务器的管理,一个服务器首先要分析它的设计目标是应对很多的连接还是很大的数据传送量。这样在设计工作者线程时就可以最大限度的提高性能。管理客户端方面,我们可以将客户端的数据捆绑到Perhand-Data数据结构上,如果还有需要,可以建一个表来记录客户端的宏观情况。

                  Ares引擎中,我将文件传送和大容量数据传送功能也封装进了服务器和客户端。我建议服务器和客户端都应该封装这些功能,尽管我们并不是做FTP服务器,但是当客户端需要和服务器交换文件和大块数据时,你会发现这样做,灵活性和性能都能做得比用单纯的FTP协议来更好,所以在你的服务器和客户端可以传送数据包以后,把他们都做进去吧。

                  为了服务器不被黑客攻击,或被BUG弄崩溃,我们还需要认真设计服务器的认证机制,以及密切注意程序中的溢出,一定要在每一个使用缓冲区的地方加上检查代码。可以说并没有现成的办法来解决这个问题,不然就没有人研究网络安全了,所以我们要做的是尽量减少错误,即使出现错误也不会造成太大损失,在发现错误的时候能够很快纠正同类错误。

                  还有就是对客户端情况的检测,比如客户端的正常和非正常断开连接。如果不注意这一点,就会造成服务器资源持续消耗而最终崩溃,因为我们的服务器不可能总是重启,而是要持续的运行,越久越好。还有比如客户端断开连接后又尝试连接,但是在服务器看来这个客户“仍然在线“,这个时候我们不能单纯的拒绝客户端的连接,也不能单纯的接收。

                  讲了几点服务器设计中的问题,他们只是众多问题中的一小部分,限于时间原因,在这个版本的文章中就说这么多。你一定会发现,其实网络编程最困难和有成就的地方,并不是服务器用了什么模式等等,而是真正深入设计的时候碰到的众多问题。正是那些没有标准答案的问题,值得我们去研究和解决。

     

     

     

     

    第四章 作者的话

     

    写这篇文章的目的,一方面是简要的谈谈游戏编程中的网络部分。另一方面是结交众多开发的朋友。毕竟我们做东西不可能不和他人交流,也不可能只做非商业化的项目。我开发的Ares引擎就是同时为了这两个目的,到我写这篇文章的时候,引擎的版本仍然是3.2,并不是我不想继续开发,也不是没有新的改变了。恰恰相反,我有很多新的想法,急切想把他们加入新的版本中,只是现在手上还有短期的项目没有完成。

     
    展开全文
  • windows网络编程

    千次阅读 2012-09-08 00:16:48
     TCP/IP协议实际上就是在物理网上的一组完整的网络协议。其中TCP是提供传输层服务,而IP则是提供网络层服务。TCP/IP包括以下协议:(结构如图1.1) (图1.1)  IP: 网间协议(Internet Protocol) 负责主机间...
    一、TCP/IP 体系结构与特点

      1、TCP/IP体系结构

      TCP/IP协议实际上就是在物理网上的一组完整的网络协议。其中TCP是提供传输层服务,而IP则是提供网络层服务。TCP/IP包括以下协议:(结构如图1.1)


    (图1.1)

      IP: 网间协议(Internet Protocol) 负责主机间数据的路由和网络上数据的存储。同时为ICMP,TCP,   UDP提供分组发送服务。用户进程通常不需要涉及这一层。

      ARP: 地址解析协议(Address Resolution Protocol)
       此协议将网络地址映射到硬件地址。

      RARP: 反向地址解析协议(Reverse Address Resolution Protocol)
       此协议将硬件地址映射到网络地址

      ICMP: 网间报文控制协议(Internet Control Message Protocol)
       此协议处理信关和主机的差错和传送控制。

      TCP: 传送控制协议(Transmission Control Protocol)
       这是一种提供给用户进程的可靠的全双工字节流面向连接的协议。它要为用户进程提供虚电路服务,并为数据可靠传输建立检查。(注:大多数网络用户程序使用TCP)

      UDP: 用户数据报协议(User Datagram Protocol)
       这是提供给用户进程的无连接协议,用于传送数据而不执行正确性检查。

      FTP: 文件传输协议(File Transfer Protocol)
       允许用户以文件操作的方式(文件的增、删、改、查、传送等)与另一主机相互通信。

      SMTP: 简单邮件传送协议(Simple Mail Transfer Protocol)
       SMTP协议为系统之间传送电子邮件。

      TELNET:终端协议(Telnet Terminal Procotol)
       允许用户以虚终端方式访问远程主机

      HTTP: 超文本传输协议(Hypertext Transfer Procotol)
      
      TFTP: 简单文件传输协议(Trivial File Transfer Protocol)

      2、TCP/IP特点

      TCP/IP协议的核心部分是传输层协议(TCP、UDP),网络层协议(IP)和物理接口层,这三层通常是在操作系统内核中实现。因此用户一般不涉及。编程时,编程界面有两种形式:一、是由内核心直接提供的系统调用;二、使用以库函数方式提供的各种函数。前者为核内实现,后者为核外实现。用户服务要通过核外的应用程序才能实现,所以要使用套接字(socket)来实现。

      图1.2是TCP/IP协议核心与应用程序关系图。


    (图1.2)

      二、专用术语

      1、套接字

      套接字是网络的基本构件。它是可以被命名和寻址的通信端点,使用中的每一个套接字都有其类型和一个与之相连听进程。套接字存在通信区域(通信区域又称地址簇)中。套接字只与同一区域中的套接字交换数据(跨区域时,需要执行某和转换进程才能实现)。WINDOWS 中的套接字只支持一个域——网际域。套接字具有类型。

      WINDOWS SOCKET 1.1 版本支持两种套接字:流套接字(SOCK_STREAM)和数据报套接字(SOCK_DGRAM)

      2、WINDOWS SOCKETS 实现

      一个WINDOWS SOCKETS 实现是指实现了WINDOWS SOCKETS规范所描述的全部功能的一套软件。一般通过DLL文件来实现

      3、阻塞处理例程

      阻塞处理例程(blocking hook,阻塞钩子)是WINDOWS SOCKETS实现为了支持阻塞套接字函数调用而提供的一种机制。

      4、多址广播(multicast,多点传送或组播)

      是一种一对多的传输方式,传输发起者通过一次传输就将信息传送到一组接收者,与单点传送
    (unicast)和广播(Broadcast)相对应。

    一、客户机/服务器模式

      在TCP/IP网络中两个进程间的相互作用的主机模式是客户机/服务器模式(Client/Server model)。该模式的建立基于以下两点:1、非对等作用;2、通信完全是异步的。客户机/服务器模式在操作过程中采取的是主动请示方式:

      首先服务器方要先启动,并根据请示提供相应服务:(过程如下)

      1、打开一通信通道并告知本地主机,它愿意在某一个公认地址上接收客户请求。

      2、等待客户请求到达该端口。

      3、接收到重复服务请求,处理该请求并发送应答信号。

      4、返回第二步,等待另一客户请求

      5、关闭服务器。

      客户方:

      1、打开一通信通道,并连接到服务器所在主机的特定端口。

      2、向服务器发送服务请求报文,等待并接收应答;继续提出请求……

      3、请求结束后关闭通信通道并终止。

      二、基本套接字

      为了更好说明套接字编程原理,给出几个基本的套接字,在以后的篇幅中会给出更详细的使用说明。

      1、创建套接字——socket()

      功能:使用前创建一个新的套接字

      格式:SOCKET PASCAL FAR socket(int af,int type,int procotol);

      参数:af: 通信发生的区域

      type: 要建立的套接字类型

      procotol: 使用的特定协议

      2、指定本地地址——bind()

      功能:将套接字地址与所创建的套接字号联系起来。

      格式:int PASCAL FAR bind(SOCKET s,const struct sockaddr FAR * name,int namelen);

      参数:s: 是由socket()调用返回的并且未作连接的套接字描述符(套接字号)。

      其它:没有错误,bind()返回0,否则SOCKET_ERROR

      地址结构说明:

    struct sockaddr_in
    {
    short sin_family;//AF_INET
    u_short sin_port;//16位端口号,网络字节顺序
    struct in_addr sin_addr;//32位IP地址,网络字节顺序
    char sin_zero[8];//保留
    }

      3、建立套接字连接——connect()和accept()

      功能:共同完成连接工作

      格式:int PASCAL FAR connect(SOCKET s,const struct sockaddr FAR * name,int namelen);

      SOCKET PASCAL FAR accept(SOCKET s,struct sockaddr FAR * name,int FAR * addrlen);

      参数:同上

      4、监听连接——listen()

      功能:用于面向连接服务器,表明它愿意接收连接。

      格式:int PASCAL FAR listen(SOCKET s, int backlog);

      5、数据传输——send()与recv()

      功能:数据的发送与接收

      格式:int PASCAL FAR send(SOCKET s,const char FAR * buf,int len,int flags);

      int PASCAL FAR recv(SOCKET s,const char FAR * buf,int len,int flags);

      参数:buf:指向存有传输数据的缓冲区的指针。

      6、多路复用——select()

      功能:用来检测一个或多个套接字状态。

      格式:int PASCAL FAR select(int nfds,fd_set FAR * readfds,fd_set FAR * writefds,
    fd_set FAR * exceptfds,const struct timeval FAR * timeout);

      参数:readfds:指向要做读检测的指针

         writefds:指向要做写检测的指针

         exceptfds:指向要检测是否出错的指针

         timeout:最大等待时间

      7、关闭套接字——closesocket()

      功能:关闭套接字s

      格式:BOOL PASCAL FAR closesocket(SOCKET s);

    三、典型过程图

      2.1 面向连接的套接字的系统调用时序图



      2.2 无连接协议的套接字调用时序图



       2.3 面向连接的应用程序流程图


    Windows Socket1.1 程序设计

    一、简介

      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 & 0x0000FFFF;
    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()。可以这样理解篠ocket实际上是一个通信端点,通过它,用户的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是服务器可响应连接的最大数目
      {}
     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啊?



    用Winsock实现语音全双工通信使用

    摘要:在Windows 95环境下,基于TCP/IP协议,用Winsock完成了话音的端到端传输。采用双套接字技术,阐述了主要函数的使用要点,以及基于异步选择机制的应用方法。同时,给出了相应的实例程序。

       一、引言

      Windows 95作为微机的操作系统,已经完全融入了网络与通信功能,不仅可以建立纯Windows 95环境下的“对等网络”,而且支持多种协议,如TCP/IP、IPX/SPX、NETBUI等。在TCP/IP协议组中,TPC是一种面向连接的协义,为用户提供可靠的、全双工的字节流服务,具有确认、流控制、多路复用和同步等功能,适于数据传输。UDP协议则是无连接的,每个分组都携带完整的目的地址,各分组在系统中独立传送。它不能保证分组的先后顺序,不进行分组出错的恢复与重传,因此不保证传输的可靠性,但是,它提供高传输效率的数据报服务,适于实时的语音、图像传输、广播消息等网络传输。

      Winsock接口为进程间通信提供了一种新的手段,它不但能用于同一机器中的进程之间通信,而且支持网络通信功能。随着Windows 95的推出。Winsock已经被正式集成到了Windows系统中,同时包括了16位和32位的编程接口。而Winsock的开发工具也可以在Borland C++4.0、Visual C++2.0这些C编译器中找到,主要由一个名为winsock.h的头文件和动态连接库winsock.dll或wsodk32.dll组成,这两种动态连接库分别用于Win16和Win32的应用程序。

      本文针对话音的全双工传输要求,采用UDP协议实现了实时网络通信。使用VisualC++2.0编译环境,其动态连接库名为wsock32.dll。
    二、主要函数的使用要点

      通过建立双套接字,可以很方便地实现全双工网络通信。

      1.套接字建立函数:

    SOCKET socket(int family,int type,int protocol)

      对于UDP协议,写为:

    SOCKRET s;
    s=socket(AF_INET,SOCK_DGRAM,0);
    或s=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP)

      为了建立两个套接字,必须实现地址的重复绑定,即,当一个套接字已经绑定到某本地地址后,为了让另一个套接字重复使用该地址,必须为调用bind()函数绑定第二个套接字之前,通过函数setsockopt()为该套接字设置SO_REUSEADDR套接字选项。通过函数getsockopt()可获得套接字选项设置状态。需要注意的是,两个套接字所对应的端口号不能相同。此外,还涉及到套接字缓冲区的设置问题,按规定,每个区的设置范围是:不小于512个字节,大大于8k字节,根据需要,文中选用了4k字节。

      2.套接字绑定函数

    int bind(SOCKET s,struct sockaddr_in*name,int namelen)

      s是刚才创建好的套接字,name指向描述通讯对象的结构体的指针,namelen是该结构体的长度。该结构体中的分量包括:IP地址(对应name.sin_addr.s_addr)、端口号(name.sin_port)、地址类型(name.sin_family,一般都赋成AF_INET,表示是internet地址)。

      (1)IP地址的填写方法:在全双工通信中,要把用户名对应的点分表示法地址转换成32位长整数格式的IP地址,使用inet_addr()函数。

      (2)端口号是用于表示同一台计算机不同的进程(应用程序),其分配方法有两种:1)进程可以让系统为套接字自动分配一端口号,只要在调用bind前将端口号指定为0即可。由系统自动分配的端口号位于1024~5000之间,而1~1023之间的任一TCP或UDP端口都是保留的,系统不允许任一进程使用保留端口,除非其有效用户ID是零(超级用户)。

      2)进程可为套接字指定一特定端口。这对于需要给套接字分配一众所端口的服务器是很有用的。指定范围为1024和65536之间。可任意指定。

      在本程序中,对两个套接字的端口号规定为2000和2001,前者对应发送套接字,后者对应接收套接字。

      端口号要从一个16位无符号数(u_short类型数)从主机字节顺序转换成网络字节顺序,使用htons()函数。

      根据以上两个函数,可以给出双套接字建立与绑定的程序片断。

    //设置有关的全局变量
    SOCKET sr,ss;
    HPSTR sockBufferS,sockBufferR;
    HANDLE hSendData,hReceiveData;
    DWROD dwDataSize=1024*4;
    struct sockaddr_in therel.there2;
    #DEFINE LOCAL_HOST_ADDR 200.200.200.201
    #DEFINE REMOTE_HOST-ADDR 200.200.200.202
    #DEFINE LOCAL_HOST_PORT 2000
    #DEFINE LOCAL_HOST_PORT 2001
    //套接字建立函数
    BOOL make_skt(HWND hwnd)
    {
    struct sockaddr_in here,here1;
    ss=socket(AF_INET,SOCK_DGRAM,0);
    sr=socket(AF_INET,SOCK_DGRAM,0);
    if((ss==INVALID_SOCKET)||(sr==INVALID_SOCKET))
    {
    MessageBox(hwnd,“套接字建立失败!”,“”,MB_OK);
    return(FALSE);
    }
    here.sin_family=AF_INET;
    here.sin_addr.s_addr=inet_addr(LOCAL_HOST_ADDR);
    here.sin_port=htons(LICAL_HOST_PORT);
    //another socket
    herel.sin_family=AF_INET;
    herel.sin_addr.s_addr(LOCAL_HOST_ADDR);
    herel.sin_port=htons(LOCAL_HOST_PORT1);
    SocketBuffer();//套接字缓冲区的锁定设置
    setsockopt(ss,SOL_SOCKET,SO_SNDBUF,(char FAR*)sockBufferS,dwDataSize);
    if(bind(ss,(LPSOCKADDR)&here,sizeof(here)))
    {
    MessageBox(hwnd,“发送套接字绑定失败!”,“”,MB_OK);
    return(FALSE);
    }
    setsockopt(sr SQL_SOCKET,SO_RCVBUF|SO_REUSEADDR,(char FAR*)
    sockBufferR,dwDataSize);
    if(bind(sr,(LPSOCKADDR)&here1,sizeof(here1)))
    {
    MessageBox(hwnd,“接收套接字绑定失败!”,“”,MB_OK);
    return(FALSE);
    }
    return(TRUE);
    }
    //套接字缓冲区设置
    void sockBuffer(void)
    {
    hSendData=GlobalAlloc(GMEM_MOVEABLE|GMEM_SHARE,dwDataSize);
    if(!hSendData)
    {
    MessageBox(hwnd,“发送套接字缓冲区定位失败!”,NULL,
    MB_OK|MB_ICONEXCLAMATION);
    return;
    }
    if((sockBufferS=GlobalLock(hSendData)==NULL)
    {
    MessageBox(hwnd,“发送套接字缓冲区锁定失败!”,NULL,
    MB_OK|MB_ICONEXCLAMATION);
    GlobalFree(hRecordData[0];
    return;
    }
    hReceiveData=globalAlloc(GMEM_MOVEABLE|GMEM_SHARE,dwDataSize);
    if(!hReceiveData)
    {
    MessageBox(hwnd,"“接收套接字缓冲区定位败!”,NULL
    MB_OK|MB_ICONEXCLAMATION);
    return;
    }
    if((sockBufferT=Globallock(hReceiveData))=NULL)
    MessageBox(hwnd,"发送套接字缓冲区锁定失败!”,NULL,
    MB_OK|MB_ICONEXCLAMATION);
    GlobalFree(hRecordData[0]);
    return;
    }
    {

      3.数据发送与接收函数;

    int sendto(SOCKET s.char*buf,int len,int flags,struct sockaddr_in to,int
    tolen);
    int recvfrom(SOCKET s.char*buf,int len,int flags,struct sockaddr_in
    fron,int*fromlen)

      其中,参数flags一般取0。

      recvfrom()函数实际上是读取sendto()函数发过来的一个数据包,当读到的数据字节少于规定接收的数目时,就把数据全部接收,并返回实际接收到的字节数;当读到的数据多于规定值时,在数据报文方式下,多余的数据将被丢弃。而在流方式下,剩余的数据由下recvfrom()读出。为了发送和接收数据,必须建立数据发送缓冲区和数据接收缓冲区。规定:IP层的一个数据报最大不超过64K(含数据报头)。当缓冲区设置得过多、过大时,常因内存不够而导致套接字建立失败。在减小缓冲区后,该错误消失。经过实验,文中选用了4K字节。

      此外,还应注意这两个函数中最后参数的写法,给sendto()的最后参数是一个整数值,而recvfrom()的则是指向一整数值的指针。

      4.套接字关闭函数:closesocket(SOCKET s)

      通讯结束时,应关闭指定的套接字,以释与之相关的资源。

      在关闭套接字时,应先对锁定的各种缓冲区加以释放。其程序片断为:

    void CloseSocket(void)
    {
    GlobalUnlock(hSendData);
    GlobalFree(hSenddata);
    GlobalUnlock(hReceiveData);
    GlobalFree(hReceiveDava);
    if(WSAAysncSelect(ss,hwnd,0,0)=SOCKET_ERROR)
    {
    MessageBos(hwnd,“发送套接字关闭失败!”,“”,MB_OK);
    return;
    }
    if(WSAAysncSelect(sr,hwnd,0,0)==SOCKET_ERROR)
    {
    MessageBox(hwnd,“接收套接字关闭失败!”,“”,MB_OK);
    return;
    }
    WSACleanup();
    closesockent(ss);
    closesockent(sr);
    return;
    }


    三、Winsock的编程特点与异步选择机制

      1 阻塞及其处理方式

      在网络通讯中,由于网络拥挤或一次发送的数据量过大等原因,经常会发生交换的数据在短时间内不能传送完,收发数据的函数因此不能返回,这种现象叫做阻塞。Winsock对有可能阻塞的函数提供了两种处理方式:阻塞和非阻塞方式。在阻塞方式下,收发数据的函数在被调用后一直要到传送完毕或者出错才能返回。在阻塞期间,被阻的函数不会断调用系统函数GetMessage()来保持消息循环的正常进行。对于非阻塞方式,函数被调用后立即返回,当传送完成后由Winsock给程序发一个事先约定好的消息。

      在编程时,应尽量使用非阻塞方式。因为在阻塞方式下,用户可能会长时间的等待过程中试图关闭程序,因为消息循环还在起作用,所以程序的窗口可能被关闭,这样当函数从Winsock的动态连接库中返回时,主程序已经从内存中删除,这显然是极其危险的。

      2 异步选择函数WSAAsyncSelect()的使用

      Winsock通过WSAAsyncSelect()自动地设置套接字处于非阻塞方式。使用WindowsSockets实现Windows网络程序设计的关键就是它提供了对网络事件基于消息的异步存取,用于注册应用程序感兴趣的网络事件。它请求Windows Sockets DLL在检测到套接字上发生的网络事件时,向窗口发送一个消息。对UDP协议,这些网络事件主要为:

      FD_READ 期望在套接字收到数据(即读准备好)时接收通知;

      FD_WRITE 期望在套接字可发送数(即写准备好)时接收通知;

      FD_CLOSE 期望在套接字关闭时接电通知

      消息变量wParam指示发生网络事件的套接字,变量1Param的低字节描述发生的网络事件,高字包含错误码。如在窗口函数的消息循环中均加一个分支:

    int ok=sizeof(SOCKADDR);
    case wMsg;
    switch(1Param)
    {
    case FD_READ:
    //套接字上读数据
    if(recvfrom(sr.lpPlayData[j],dwDataSize,0,(struct sockaddr FAR*)&there1,

    (int FAR*)&ok)==SOCKET_ERROR0
    {
    MessageBox)hwnd,“数据接收失败!”,“”,MB_OK);
    return(FALSE);
    }
    case FD_WRITE:
    //套接字上写数据
    }
    break;

      在程序的编制中,应根据需要灵活地将WSAAsyncSelect()函灵敏放在相应的消息循环之中,其它说明可参见文献[1]。此外,应该指出的是,以上程序片断中的消息框主要是为程序调试方便而设置的,而在正式产品中不再出现。同时,按照程序容错误设计,应建立一个专门的容错处理函数。程序中可能出现的各种错误都将由该函数进行处理,依据错误的危害程度不同,建立几种不同的处理措施。这样,才能保证双方通话的顺利和可靠。

       四、结论

      本文是多媒体网络传输项目的重要内容之一,目前,结合硬件全双工语音卡等设备,已经成功地实现了话音的全双工的通信。有关整个多媒体传输系统设计的内容,将有另文叙述。

    VC编程轻松获取局域网连接通知
    摘要:本文从解决实际需要出发,通过采用Windows Socket API等网络编程技术实现了在局域网共享一条电话线的情况下,当服务器拨号上网时能及时通知各客户端通过代理服务器进行上网。本文还特别给出了基于Microsoft Visual C++ 6.0的部分关键实现代码。

       一、 问题提出的背景

      笔者所使用的局域网拥有一个服务器及若干分布于各办公室的客户机,通过网卡相连。服务器不提供专线上网,但可以拨号上网,而各客户机可以通过装在服务器端的代理服务器共用一条电话线上网,但前提必须是服务器已经拨号连接。考虑到经济原因,服务器不可能长时间连在网上,因此经常出现由于分布于各办公室的客户机不能知道服务器是否处于连线状态而造成的想上网时服务器没有拨号,或是服务器已经拨号而客户机却并不知晓的情况,这无疑会在工作中带来极大的不便。而笔者作为一名程序设计人员,有必要利用自己的专业优势来解决实际工作中所遇到的一些问题。通过对实际情况的分析,可以归纳为一点:当服务器在进行拨号连接时能及时通知在网络上的各个客户机,而各客户机在收到服务器发来的消息后可以根据自己的情况来决定是否上网。这样就可以在同一时间内同时为较多的客户机提供上网服务,此举不仅提高了利用效率也大大节省了上网话费。

       二、 程序主要设计思路及实现

      由于本网络是通过网卡连接的局域网,因此可以首选Windows Socket API进行套接字编程。整个系统分为两部分:服务端和客户端。服务端运行于服务器上负责监视服务器是否在进行拨号连接,一旦发现马上通过网络发送消息通知客户端;而客户端软件则只需完成同服务端软件的连接并能接收到从服务端发送来的通知消息即可。服务器端要完成比客户端更为繁重的任务。下面对这几部分的实现分别加以描述:

      (一)监视拨号连接事件的发生

      在采用拨号上网时,首先需要通过拨号连接通过电话线连接到ISP上,然后才能享受到ISP所提供的各种互联网服务。而要捕获拨号连接发生的事件不能依赖于消息通知,因为此时发出的消息同一个对话框出现在屏幕上时所产生的消息是一样的。唯一同其他对话框区别的是其标题是固定的"拨号连接",因此在无其他特殊情况下(如其他程序的标题也是"拨号连接"时)可以认定当桌面上的所有程序窗口出现以"拨号连接" 为标题的窗口时,即可认定此时正在进行拨号连接。因此可以通过搜寻并判断窗口标题的办法对拨号连接进行监视,具体可以用CWnd类的FindWindows()函数来实现:

    CWnd *pWnd=CWnd::FindWindow(NULL,"拨号连接");

      第一个参数为NULL,指定对当前所有窗口都进行搜索。第二个参数就是待搜寻的窗口标题,一旦找到将返回该窗口的窗口句柄。因此可以在窗口句柄不为空的情况下去通知客户端服务器现在正在拨号。由于一般的拨号连接都需要一段时间的连接应答后才能登录到ISP上,因此从提高程序运行效率角度出发可以通过定时器的使用来每间隔一段时间(如500毫秒)去搜寻一次,以确保能监视到每一次的拨号连接而又不致过分加重CPU的负担。

    (二)服务器端网络通讯功能的实现

      在此采用的是可靠的有连接的流式套接字,并且采用了多线程和异步通知机制能有效避免一些函数如accept()等的阻塞会引起整个程序的阻塞。由于套接字编程方面的书籍资料非常丰富,对其进行网络编程做了很详细的描述,故本文在此只针对一些关键部分做简要说明,有关套接字网络编程的详细内容请参阅相关资料。采用流式套接字的服务器端的主要设计流程可以归结为以下几步:

      1. 创建套接字

    sock=socket(AF_INET,SOCK_STREAM,0);

      该函数的第一个参数用于指定地址族,在Windows下仅支持AF_INET(TCP/IP地址);第二个参数用于描述套接字的类型,对于流式套接字提供有SOCK_STREAM;最后一个参数指定套接字使用的协议,一般为0。该函数的返回值保存了新套接字的句柄,在程序退出前可以用closesocket()函数来将其释放。

      2. 绑定套接字

      服务器方一旦获取了一个新的套接字后应通过bind()将该套接字与本机上的一个端口相关联。此时需要预先对一个指向包含有本机IP地址和端口信息的sockaddr_in结构填充一些必要的信息,如本地端口号和本地主机地址等。然后就可经过bind()将服务器进程在网络上标识出来。需要注意的是由于1024以内的埠号都是保留的端口号因此如无特别需要一般不能将sockin.sin_port的端口号设置为1024以内的值:

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

      3. 侦听套接字

    listen(sock,1);

      4. 等待客户机的连接

      这里需要通过accept()调用等待接收客户端的连接以完成连接的建立,由于该函数在没有客户端进行申请连接之前会处于阻塞状态,因此如果采取通常的单线程模式会导致整个程序一直处于阻塞状态而不能响应其他的外界消息,因此为该部分代码单独开辟一个线程,这样阻塞将被限制在该线程内而不会影响到程序整体。

    AfxBeginThread(Server,NULL);//创建一个新的线程
    ……
    UINT Server(LPVOID lpVoid)//线程的处理函数
    {
    //获取当前视类的指针,以确保访问的是当前的实例对象。
    CNetServerView* pView=((CNetServerView*)(
    (CFrameWnd*)AfxGetApp()->m_pMainWnd)->GetActiveView());
    while(pView->nNumConns<1)//当前的连接者个数
    {
    int nLen=sizeof(SOCKADDR);
    pView->newskt= accept(pView->sock,
    (LPSOCKADDR)& pView->sockin,(LPINT)& nLen);
    WSAAsyncSelect(pView->newskt,
    pView->m_hWnd,WM_SOCKET_MSG,FD_CLOSE);
    pView->nNumConns++;
    }
    return 1;
    }

      这里在accept ()后使用了WSAAsyncSelect()异步选择函数。对于网络事件的响应最好采取异步选择机制,只有采取这种方式才可以在由网络对方所引起的不可预知的网络事件发生时能马上在进程中做出及时的响应处理,而在没有网络事件到达时则可以处理其他事件,这种效率是很高的,而且完全符合Windows所标榜的消息触发原则。WSAAsyncSelect()函数便是实现网络事件异步选择的核心函数。通过第四个参数FD_CLOSE注册了应用程序感兴取的网络事件是网络断开,当客户方端开连接时该事件会被检测到,同时会发出由第三个参数指定的自定义消息WM_SOCKET_MSG。

      5. 发送/接收

      当客户机同服务器建立好连接后就可以通过send()/recv()函数进行发送和接收数据了,对于本程序只需在监测到有拨号连接事件发生时向客户机发送通知消息即可:

    char buffer[1]={'a'};
    send(newskt,buffer,1,0);//向客户机发送字符a,表示现在服务器正在拨号。

      6. 关闭套接字

      在全部通讯完成之后,在退出程序之前需要调用closesocket();函数把创建的套接字关闭。

      (三)客户机端的程序设计

      客户机的编程要相对简单许多,全部通讯过程只需以下四步:

      1. 创建套接字
      2. 建立连接
      3. 发送/接收
      4. 关闭套接字

      具体实现过程同服务器编程基本类似,只是由于需要接收数据,因此待监测的网络事件为FD_CLOSE和FD_READ,在消息响应函数中可以通过对消息参数的低位字节进行判断而区分出具体发生是何种网络事件,并对其做出响应的反应。下面结合部分主要实现代码对实现过程进行解释:

    ……
    m_ServIP=SERVERIP; //指定服务器的IP地址
    m_Port=htons(USERPORT); //指定服务器的端口号
    if((IPaddr=inet_addr(m_ServIP))==INADDR_NONE) //转换成网络地址
    return FALSE;
    else
    {
    sock=socket(AF_INET,SOCK_STREAM,0); //创建套接字
    sockin.sin_family=AF_INET; //填充结构
    sockin.sin_addr.S_un.S_addr=IPaddr;
    sockin.sin_port=m_Port;
    connect(sock,(LPSOCKADDR)&sockin,sizeof(sockin)); //建立连接
    //设定异步选择事件
    WSAAsyncSelect(sock,m_hWnd,WM_SOCKET_MSG,FD_CLOSE|FD_READ);
    //在这里可以通过震铃、弹出对话框等方式通知客户已经连上服务器
    }
    ……

    //网络事件的消息处理函数
    int message=lParam & 0x0000FFFF;//取消息参数的低位
    switch(message) //判断发生的是何种网络事件
    {
    case FD_READ: //读事件
    AfxBeginThread(Read,NULL);
    break;
    case FD_CLOSE: //服务器关闭事件
    ……
    break;
    }

      在读事件的消息处理过程中,单独为读处理过程开辟了一个线程,在该线程中接收从服务器发送过来的信息,并通过震铃、弹出对话框等方式通知客户端现在服务器正在拨号:

    ……
    int a=recv(pView->sock,cDataBuffer,1,0); //接收从服务器发送来的消息
    if(a>0)
    AfxMessageBox("拨号连接已启动!"); //通知用户
    ……


    三、必要的完善

      前面只是介绍了程序设计的整体框架和设计思路,仅仅是一个雏形,有许多重要的细节没有完善,不能用于实际使用。下面就对一些完全必要的细节做适当的完善:

      (一) 界面的隐藏

      由于本程序系自动检测、自动通知,完全不需要人工干预,因此可以将其视为后台运行的服务程序,因此程序主界面现在已无存在的必要,可以在应用程序类的初始化实例函数InitInstance()中将ShowWindow();的参数SW_SHOW改成SW_HIDE即可。当需要有对话框弹出通知用户时仅对话框出现,主界面仍隐藏,因此是完全可行的。

      (二) 自启动的实现

      由于服务端软件需要时刻监视有无进行拨号连接,所以必须具缸云舳奶匦浴6突Ф巳砑捎诮邮障⒑屯ㄖ突Ф伎梢宰远瓿桑虼巳绻芫弑缸云舳匦栽蚩梢酝耆牙胗没У母稍ざ〉媒细叩淖远潭取I柚米云舳奶匦裕梢源右韵录父鐾揪都右钥悸牵?BR>
      1. 在"启动"菜单上添加指向程序的快捷方式。
      
      2. 在Autoexec.bat中添加启动程序的命令行。

      3. 在Win.ini中的[windows]节的run项目后添加程序路径。

      4. 修改注册表,添加键值的具体路径为:

    "HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run"

      并将添加的键值修改为程序的存放路径即可。以上几种方法既可以手工添加,也可以通过编程使之自动完成。

      (三) 自动续联

      对于服务/客户模式的网络通讯程序普遍要求服务端要先于客户端运行,而本系统的客户、服务端均为自启动,不能保证服务器先于客户机启动,而且本系统要求只要客户机和服务器连接在网络上就要不间断保持连接,因此需要使客户和服务端都要具备自动续联的功能。

      对于服务器端,当客户端断开时,需要关闭当前的套接字,并重新启动一个新的套接字以等待客户机的再次连接。这可以放在FD_CLOSE事件对应的消息WM_SOCKET_MSG的消息响应函数中来完成。而对于客户端,如果先于服务器而启动,则connect()函数将返回失败,因此可以在程序启动时用SetTimer()设置一个定时器,每隔一段时间(10秒)就试图连接服务器一次,当connect()函数返回成功即服务器已启动并与之连接上之后可以用KillTimer()函数将定时器关闭。另外当服务器关闭时需要再次开启定时器,以确保当服务器再次运行时能与之建立连接,可以通过响应FD_CLOSE事件来捕获该事件的发生。

       小结:本文通过Windows Sockets API实现了基于TCP/IP协议的面向连接的流式套接字的网络通讯程序的设计,通过网络通讯程序的支持可以把服务器捕获到的拨号连接发生的事件及时通知给客户端,最后通过对一些必要的细节的完善很好解决了在局域网上能及时得到服务器拨号连接的消息通知。本文所述程序在Windows 98 SE下,由Microsoft Visual C++ 6.0编译通过;使用的代理服务器软件为WinGate 4.3.0;上网方式为拨号上网。

    VC++编程实现网络嗅探器

    引言

      从事网络安全的技术人员和相当一部分准黑客(指那些使用现成的黑客软件进行攻击而不是根据需要去自己编写代码的人)都一定不会对网络嗅探器(sniffer)感到陌生,网络嗅探器无论是在网络安全还是在黑客攻击方面均扮演了很重要的角色。通过使用网络嗅探器可以把网卡设置于混杂模式,并可实现对网络上传输的数据包的捕获与分析。此分析结果可供网络安全分析之用,但如为黑客所利用也可以为其发动进一步的攻击提供有价值的信息。可见,嗅探器实际是一把双刃剑。 虽然网络嗅探器技术被黑客利用后会对网络安全构成一定的威胁,但嗅探器本身的危害并不是很大,主要是用来为其他黑客软件提供网络情报,真正的攻击主要是由其他黑软来完成的。而在网络安全方面,网络嗅探手段可以有效地探测在网络上传输的数据包信息,通过对这些信息的分析利用是有助于网络安全维护的。权衡利弊,有必要对网络嗅探器的实现原理进行介绍。

      嗅探器设计原理

      嗅探器作为一种网络通讯程序,也是通过对网卡的编程来实现网络通讯的,对网卡的编程也是使用通常的套接字(socket)方式来进行。但是,通常的套接字程序只能响应与自己硬件地址相匹配的或是以广播形式发出的数据帧,对于其他形式的数据帧比如已到达网络接口但却不是发给此地址的数据帧,网络接口在验证投递地址并非自身地址之后将不引起响应,也就是说应用程序无法收取到达的数据包。而网络嗅探器的目的恰恰在于从网卡接收所有经过它的数据包,这些数据包即可以是发给它的也可以是发往别处的。显然,要达到此目的就不能再让网卡按通常的正常模式工作,而必须将其设置为混杂模式。

      具体到编程实现上,这种对网卡混杂模式的设置是通过原始套接字(raw socket)来实现的,这也有别于通常经常使用的数据流套接字和数据报套接字。在创建了原始套接字后,需要通过setsockopt()函数来设置IP头操作选项,然后再通过bind()函数将原始套接字绑定到本地网卡。为了让原始套接字能接受所有的数据,还需要通过ioctlsocket()来进行设置,而且还可以指定是否亲自处理IP头。至此,实际就可以开始对网络数据包进行嗅探了,对数据包的获取仍象流式套接字或数据报套接字那样通过recv()函数来完成。但是与其他两种套接字不同的是,原始套接字此时捕获到的数据包并不仅仅是单纯的数据信息,而是包含有 IP头、 TCP头等信息头的最原始的数据信息,这些信息保留了它在网络传输时的原貌。通过对这些在低层传输的原始信息的分析可以得到有关网络的一些信息。由于这些数据经过了网络层和传输层的打包,因此需要根据其附加的帧头对数据包进行分析。下面先给出结构.数据包的总体结构:

    数据包
    IP头TCP头(或其他信息头)数据

      数据在从应用层到达传输层时,将添加TCP数据段头,或是UDP数据段头。其中UDP数据段头比较简单,由一个8字节的头和数据部分组成,具体格式如下:

    16位16位
    源端口目的端口
    UDP长度UDP校验和

      而TCP数据头则比较复杂,以20个固定字节开始,在固定头后面还可以有一些长度不固定的可选项,下面给出TCP数据段头的格式组成:

    16位16位
    源端口目的端口
    顺序号
    确认号
    TCP头长(保留)7位URGACKPSHRSTSYNFIN窗口大小
    校验和紧急指针
    可选项(0或更多的32位字)
    数据(可选项)

      对于此TCP数据段头的分析在编程实现中可通过数据结构_TCP来定义:

    typedef struct _TCP{ WORD SrcPort; // 源端口
    WORD DstPort; // 目的端口
    DWORD SeqNum; // 顺序号
    DWORD AckNum; // 确认号
    BYTE DataOff; // TCP头长
    BYTE Flags; // 标志(URG、ACK等)
    WORD Window; // 窗口大小
    WORD Chksum; // 校验和
    WORD UrgPtr; // 紧急指针
    } TCP;
    typedef TCP *LPTCP;
    typedef TCP UNALIGNED * ULPTCP;

      在网络层,还要给TCP数据包添加一个IP数据段头以组成IP数据报。IP数据头以大端点机次序传送,从左到右,版本字段的高位字节先传输(SPARC是大端点机;Pentium是小端点机)。如果是小端点机,就要在发送和接收时先行转换然后才能进行传输。IP数据段头格式如下:

    16位16位
    版本IHL服务类型总长
    标识标志分段偏移
    生命期协议头校验和
    源地址
    目的地址
    选项(0或更多)

      同样,在实际编程中也需要通过一个数据结构来表示此IP数据段头,下面给出此数据结构的定义:

    typedef struct _IP{
    union{ BYTE Version; // 版本
    BYTE HdrLen; // IHL
    };
    BYTE ServiceType; // 服务类型
    WORD TotalLen; // 总长
    WORD ID; // 标识
    union{ WORD Flags; // 标志
    WORD FragOff; // 分段偏移
    };
    BYTE TimeToLive; // 生命期
    BYTE Protocol; // 协议
    WORD HdrChksum; // 头校验和
    DWORD SrcAddr; // 源地址
    DWORD DstAddr; // 目的地址
    BYTE Options; // 选项
    } IP;
    typedef IP * LPIP;
    typedef IP UNALIGNED * ULPIP;

      在明确了以上几个数据段头的组成结构后,就可以对捕获到的数据包进行分析了。


    嗅探器的具体实现

      根据前面的设计思路,不难写出网络嗅探器的实现代码,下面就给出一个简单的示例,该示例可以捕获到所有经过本地网卡的数据包,并可从中分析出协议、IP源地址、IP目标地址、TCP源端口号、TCP目标端口号以及数据包长度等信息。由于前面已经将程序的设计流程讲述的比较清楚了,因此这里就不在赘述了,下面就结合注释对程序的具体是实现进行讲解,同时为程序流程的清晰起见,去掉了错误检查等保护性代码。主要代码实现清单为:

    // 检查 Winsock 版本号,WSAData为WSADATA结构对象
    WSAStartup(MAKEWORD(2, 2), &WSAData);
    // 创建原始套接字
    sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW));
    // 设置IP头操作选项,其中flag 设置为ture,亲自对IP头进行处理
    setsockopt(sock, IPPROTO_IP, IP_HDRINCL, (char*)&flag, sizeof(flag));
    // 获取本机名
    gethostname((char*)LocalName, sizeof(LocalName)-1);
    // 获取本地 IP 地址
    pHost = gethostbyname((char*)LocalName));
    // 填充SOCKADDR_IN结构
    addr_in.sin_addr = *(in_addr *)pHost->h_addr_list[0]; //IP
    addr_in.sin_family = AF_INET;
    addr_in.sin_port = htons(57274);
    // 把原始套接字sock 绑定到本地网卡地址上
    bind(sock, (PSOCKADDR)&addr_in, sizeof(addr_in));
    // dwValue为输入输出参数,为1时执行,0时取消
    DWORD dwValue = 1;
    // 设置 SOCK_RAW 为SIO_RCVALL,以便接收所有的IP包。其中SIO_RCVALL
    // 的定义为: #define SIO_RCVALL _WSAIOW(IOC_VENDOR,1)
    ioctlsocket(sock, SIO_RCVALL, &dwValue);

      前面的工作基本上都是对原始套接字进行设置,在将原始套接字设置完毕,使其能按预期目的工作时,就可以通过recv()函数从网卡接收数据了,接收到的原始数据包存放在缓存RecvBuf[]中,缓冲区长度BUFFER_SIZE定义为65535。然后就可以根据前面对IP数据段头、TCP数据段头的结构描述而对捕获的数据包进行分析:

    while (true)
    {
    // 接收原始数据包信息
    int ret = recv(sock, RecvBuf, BUFFER_SIZE, 0);
    if (ret > 0)
    {
    // 对数据包进行分析,并输出分析结果
    ip = *(IP*)RecvBuf;
    tcp = *(TCP*)(RecvBuf + ip.HdrLen);
    TRACE("协议: %s\r\n",GetProtocolTxt(ip.Protocol));
    TRACE("IP源地址: %s\r\n",inet_ntoa(*(in_addr*)&ip.SrcAddr));
    TRACE("IP目标地址: %s\r\n",inet_ntoa(*(in_addr*)&ip.DstAddr));
    TRACE("TCP源端口号: %d\r\n",tcp.SrcPort);
    TRACE("TCP目标端口号:%d\r\n",tcp.DstPort);
    TRACE("数据包长度: %d\r\n\r\n\r\n",ntohs(ip.TotalLen));
    }
    }

      其中,在进行协议分析时,使用了GetProtocolTxt()函数,该函数负责将IP包中的协议(数字标识的)转化为文字输出,该函数实现如下:

    #define PROTOCOL_STRING_ICMP_TXT "ICMP"
    #define PROTOCOL_STRING_TCP_TXT "TCP"
    #define PROTOCOL_STRING_UDP_TXT "UDP"
    #define PROTOCOL_STRING_SPX_TXT "SPX"
    #define PROTOCOL_STRING_NCP_TXT "NCP"
    #define PROTOCOL_STRING_UNKNOW_TXT "UNKNOW"
    ……
    CString CSnifferDlg::GetProtocolTxt(int Protocol)
    {
    switch (Protocol){
    case IPPROTO_ICMP : //1 /* control message protocol */
    return PROTOCOL_STRING_ICMP_TXT;
    case IPPROTO_TCP : //6 /* tcp */
    return PROTOCOL_STRING_TCP_TXT;
    case IPPROTO_UDP : //17 /* user datagram protocol */
    return PROTOCOL_STRING_UDP_TXT;
    default:
    return PROTOCOL_STRING_UNKNOW_TXT;
    }

      最后,为了使程序能成功编译,需要包含头文件winsock2.h和ws2tcpip.h。在本示例中将分析结果用TRACE()宏进行输出,在调试状态下运行,得到的一个分析结果如下:

    协议: UDP
    IP源地址: 172.168.1.5
    IP目标地址: 172.168.1.255
    TCP源端口号: 16707
    TCP目标端口号:19522
    数据包长度: 78
    ……
    协议: TCP
    IP源地址: 172.168.1.17
    IP目标地址: 172.168.1.1
    TCP源端口号: 19714
    TCP目标端口号:10
    数据包长度: 200
    ……

      从分析结果可以看出,此程序完全具备了嗅探器的数据捕获以及对数据包的分析等基本功能。

      小结

      本文介绍的以原始套接字方式对网络数据进行捕获的方法实现起来比较简单,尤其是不需要编写VxD虚拟设备驱动程序就可以实现抓包,使得其编写过程变的非常简便,但由于捕获到的数据包头不包含有帧信息,因此不能接收到与 IP 同属网络层的其它数据包, 如 ARP数据包、RARP数据包等。在前面给出的示例程序中考虑到安全因素,没有对数据包做进一步的分析,而是仅仅给出了对一般信息的分析方法。通过本文的介绍,可对原始套接字的使用方法以及TCP/IP协议结构原理等知识有一个基本的认识。本文所述代码在Windows 2000下由Microsoft Visual C++ 6.0编译调试通过。

    展开全文
  • 经典书籍Windows 网络编程第二版,英文版(没有中文版……)
  • windows经典网络编程第二版,经典书籍,带书签,高清
  • 本资源包括4本书,可能年代不是最新的,但是是4本Windows方面的4本重要的书籍,有需要的下载。资源来源互联网,侵删
  • 学习windows 网络编程的优秀书籍 其中包括 第二版的 中文版 英文版 和 源码
  • 两年前就给自己列出了一个读书清单,但进展缓慢,看的经典书籍仍然寥寥可数,惭愧中......  现在将这个书单重新修改,一方面鞭策自己学习,另一方面也表达对大牛们有如滔滔江水般的敬仰之意。  书单中列举的都是...
  • 084_《Windows网络编程之Delphi篇》

    千次阅读 2010-12-18 14:53:00
    本书以编程实例为主线,辅以必要的技术要点,详细地介绍了网络编程中的各个方面,从内容上覆盖了网络通讯中使用的多数协议,包括网上聊天、网络参数的获取、电子邮件的收发、Ping、FIP客户机、Web服务器与浏览器、...
  • 导读: 书单中列举的都是相关领域的经典书籍,必读之作。此书单的编辑参考了很多网站,包括一些名家的推荐,例如侯捷,孟岩,荣耀,潘爱民等等,在此也向这些前辈表示感谢。^_^ 1、C++ Language -------------------...
  • 书单中列举的都是相关领域的经典书籍,必读之作。此书单的编辑参考了很多网站,包括一些名家的推荐,例如侯捷,孟岩,荣耀,潘爱民等等,在此也向这些前辈表示感谢。^_^  1、C++ Language  --------------------...
  • [推荐]windows网络编程经典入门

    千次阅读 2004-05-09 11:26:00
    [推荐]windows网络编程经典入门 caiyi9000 原作 对于一个windows网络编程初学者,下面方法是经典入门。 初学者建议不要用MFC提供的类,而用windows API做一个简单服务器和客户端,这样有助于对socket编程机制的理解...
  • [推荐]windows网络编程经典入门[推荐]windows网络编程经典入门 caiyi9000 原作 对于一个windows网络编程初学者,下面方法是经典入门。 初学者建议不要用MFC提供的类,而用windows API做一个简单服务器和客户端,这样...
  • windows 网络编程第二版 中文版(pdf) 英文版(chm) 很清晰的

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 30,076
精华内容 12,030
关键字:

windows网络编程书籍