Windows网络编程基础 ---1 (转快乐魔导师E-Mail: euho@sina.com)

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

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

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

     

1.1 Window Socket介绍

 

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

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

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

 

1.2 第一个网络程序

 

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

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

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,注意使用htonl将IP地址转换为网络格式
        
   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_addr将IP地址转换为网络格式
 
      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();

}

 

1.3 WSAAsyncSelect模式

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

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

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

      下面是一段源代码,虽然我们是用它来写客户端,我还是把它的服务端代码放上来,一方面是有兴趣的朋友可以用他做测试和了解如何用它实现服务器;另一方面是客户端的代码可以很容易的从它修改而成,不同的地方只要参考一下1.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;

}

 

 

1.4 小节

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

posted on 2007-08-15 19:32  Wenguan  阅读(722)  评论(1)    收藏  举报

导航