65.9K
CodeProject 正在变化。 阅读更多。
Home

单服务器多客户端:一个简单的 C++ 实现

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (97投票s)

2004年7月23日

14分钟阅读

viewsIcon

1680467

downloadIcon

17466

使用简单的 C++ 类实现具有多个客户端的客户端/服务器结构

0. 引言

本文介绍了 **TCP/IP** 套接字编程在 **C++** 中的细节。阅读本文后,您将能够构建自己的服务器,该服务器能够同时处理多个客户端。本文开发了几个关键类,希望您能在日常开发工作中用到这些类。第一个是myTcpSocket,这个类通过提供一个简单易用的接口来隐藏套接字编程的细节,它用于构建服务器和客户端。第二个类是myThread类,其主要目的是让服务器同时处理多个客户端:对于每一个传入的客户端呼叫,服务器都会创建一个单独的线程来与该客户端通信,因此它可以处理尽可能多的传入客户端。然而,在像这样的多线程环境中,这些线程的同步始终是一个重要问题。为了解决这个问题,我们需要我们最后一个主要类,即mySemaphore

以下是您可能对本文感兴趣的原因

  1. 您可能想了解客户端/服务器编程的细节。当然,.NET 都是关于 **XML** 和 Web 服务的,它提供了一个全新的框架来让您的生活更轻松,但其网络部分的基础仍然是套接字编程,了解套接字编程的细节将使我们不仅感觉更轻松,而且更快乐。
  2. 您可能想开发一个具有客户端/服务器结构的应用程序,但出于某种原因,您无法使用 MFCC# 中的 API/类。例如,如果应用程序要在 Unix/Linux 平台上开发,那么这些 API/类甚至都不可用。如果您遇到这种情况,希望您能记住我们在这里提供的类并尝试一下:它们轻量且相当通用,可以轻松使用,无需提供任何窗口句柄或其他内容,而且通过对这些类进行简单的修改,它们也可以轻松地集成到在 Unix/Linux 平台上开发的应用程序中。
  3. 您的应用程序不一定是面向客户端/服务器的,但您仍然需要一套通用的底层套接字构建块。一个例子是您的应用程序可能需要进程间通信 (IPC) 方法,那么 myTcpSocket 可以是您的候选者之一。其他应用程序可能涉及多线程,在这种情况下,myThreadmySemaphore 类可能对您的工作有所帮助。

最后一个原因是,我早些时候的文章,“C++ 中轻量级的客户端/服务器套接字类”(我们将在后面的讨论中称之为文章 #1),是关于单个服务器和单个客户端的,有一位读者问我是否可以使服务器处理多个客户端。好了,这就是给您的答案,希望您也有机会看到这篇文章。

希望这能激发您阅读本文的动力。下一节将描述本文实现的客户端/服务器场景,然后我们将讨论如何构建/编译该项目(如果您想自己尝试的话)。接下来的几节将详细介绍使用上述关键类实现客户端/服务器结构的细节——这也将作为在实际应用程序中使用这些通用类的示例。

1. 客户端/服务器场景

我们感兴趣的客户端/服务器结构描述如下。服务器将首先启动,启动后,我们希望服务器等待传入的客户端呼叫,并定期报告其状态:有多少客户端已连接到服务器,有多少客户端已与服务器断开连接。同时,一旦检测到并接受了传入的呼叫,服务器将创建一个单独的线程来处理该客户端,因此它将创建与每个传入客户端相对应的独立会话,并且它应该能够与其中任何一个客户端“对话”。一旦服务器从其中一个客户端收到 Quit/quit 消息,它将关闭与该特定客户端的连接。

再次,为了实现这个客户端/服务器场景,我们将首先构建几个关键类,即 myTcpSocketmyThreadmySemaphore。在下一节中,我们将讨论如何编译/构建项目,然后在接下来的几节中描述这些类和实现细节。

2. 如何构建和运行项目

您可以下载源代码,并且以下 .cpp.h 文件应包含在 zip 文件中

对于服务器

winSeverForMultipleClient.cpp
mySocket.cpp
myThread.cpp
myThreadArgument.cpp
mySemaphore.cpp
myHostInfo.cpp
myEvent.cpp
myException.cpp
myLog.cpp
mySocket.h
myThread.h
myThreadArgument.h
mySemaphore.h
myHostInfo.h
myEvent.h
myException.h
myLog.h

对于客户端

myClient.cpp
mySocket.cpp
myHostInfo.cpp
myException.cpp
myLog.cpp
mySocket.h
myHostInfo.h
myException.h
myLog.h

下载这些文件后,您可以构建两个项目:一个用于服务器,一个用于客户端。编译后,您应该先启动服务器。成功启动服务器将显示以下控制台屏幕

my localhost (server) information:
        Name:    liyang
        Address: 209.206.17.136

Summary of socket settings:
   Socket Id:     1936
   port #:        1200
   debug:         false
   reuse addr:    false
   keep alive:    false
   send buf size: 8192
   recv bug size: 8192
   blocking:      true
   linger on:     false
   linger seconds: 0

server finishes binding process...
server is waiting for client calls ...

这假设您将本地 PC 用作服务器,因此 Name 应显示您的域名(或在某些情况下,您的 PC 名称,如本例所示),而 Address 应显示您当前的 IP 地址(取决于您的 ISP,您的 IP 地址可能会不时更改。在我运行服务器时,此 IP 地址为 209.206.17.136)。

显然,为了与您的服务器通信,您需要让您的客户端知道服务器的 IP 地址。为此,您需要创建一个名为 serverConfig.txt 的简单文本文件,其中仅包含一行:服务器的 IP 地址。因此,将 209.206.17.136 添加到此文件中,并将其保存在客户端可执行文件所在的目录中,然后通过双击该可执行文件启动客户端。您可以启动任意数量的客户端,可以通过键盘输入在服务器和任何客户端之间发送消息,同样,您也可以通过在键盘上输入一个 string 从任何客户端向服务器发送消息。要结束一个客户端,请从客户端向服务器发送 Quitquit 消息,该客户端将被终止。尽情玩转这个简单的客户端/服务器系统吧!

在接下来的几节中,我们将详细讨论这些类,并展示如何使用这些类来实现这个客户端/服务器系统。

3. myTcpSocket 类

本节描述 myTcpSocket 类。事实上,文章 #1 对该类进行了相当详细的描述,因此在这里我们只通过以下示例展示该类的关键方法。简而言之,该类将与套接字相关的系统调用封装在一个类中,以提供一个简单易用的接口,其典型用法(作为服务器)如下所示

//
// server side
//
myTcpSocket myServer(PORTNUM);  // create the socket using the given port numberx
myServer.bindSocket();          // bind the socket to the port number
myServer.listenToClient();      // listening to the port/socket

while ( 1 )
{
   // waiting for the client call...
   string clientName;
   myTcpSocket* newClient = myServer.acceptClient(clientName);   

   // if we reach here, the server got a call already...
   // declare string messageToClient,messageFromClient;

   // receive message from client
   newClient->receiveMessage(messageFromClient);

   // send message to client
   newClient->sendMessage(messageToClient);

   // other stuff here ...
}     

以下代码显示了 myTcpSocket 类在客户端的使用情况

// 
// client side
//
myTcpSocket myClient(PORTNUM);  // create the client socket using the given port id
myClient.connectToServer("209.206.17.136",ADDRESS); 
         // connect to server at IP addr 209.206.17.136

while (1)
{
   // declare string messageToServer, messageFromServer...
   
   // send message to server
   myClient.sendMessage(messageToServer);

   // get message from server  
   int messageLength = myClient.recieveMessage(messageFromServer);
   
   // other stuff...
}

除了 myTcpSocket 类中的这些主要方法外,还有其他方法可供您操作套接字,例如

void setDebug(int);
void setReuseAddr(int);
void setKeepAlive(int);
void setLingerOnOff(bool);
void setLingerSeconds(int);
void setSocketBlocking(int);

说了这么多,现在很容易看到如何使用上述服务器端和客户端代码(参见文章 #1)构建一个非常基本的客户端/服务器系统。然而,在这个基本模型中仍然存在以下问题

  1. 服务器将无法处理多个客户端。
  2. 如果我们把上面的服务器端代码放在 main() 函数中,while 循环中的 myServer.acceptClient() 调用将阻塞所有内容,我们无法在 main() 中实现任何其他处理任务,例如,如果我们还想收集服务器的性能状态,例如连接了多少客户端等,阻塞结构会阻止我们做任何其他事情!

为了解决这些问题,我们需要另外两个类,即 myThreadmySemaphore。这些类将在下一节中讨论。

4. 另外两个关键类:myThread、mySemaphore

上一节中讨论的现有问题表明,多线程是最佳解决方案

  1. 为了处理多个客户端,我们可以为每个传入的客户端呼叫创建一个线程,该线程将处理服务器与该特定客户端之间的通信。
  2. 通过创建一个线程可以解决阻塞调用并继续处理的问题:您可以在该线程中调用阻塞函数,让主线程继续处理而不必等待。

myThread 可用于此情况,它也是一个通用类,可以在其他应用程序的开发中轻松使用。其主要功能包括:创建线程、启动线程执行、挂起/恢复线程、等待线程完成并获取其退出代码、访问线程的设置(例如,线程的优先级)以及报告线程的时间统计信息等。有关该类及其用法示例的详细信息,您可以阅读我之前的文章,“使用线程、信号量和事件的生产者/消费者实现”(我们将在后面的讨论中称之为文章 #2)。

现在让我们看看如何使用 myThread 类与 myTcpSocket 类结合来创建客户端/服务器系统并解决之前的两个问题。这是服务器端改进的(但也简化的)main() 函数(不用担心细节)

int main()
{
   // initialization, and declaration of variables, etc.

   // Initialize the winsock library
   myTcpSocket::initialize();

   // create the server: open socket on the local host(server)
   myTcpSocket myServer(PORTNUM);

   // create a thread to implement server process: 
   // listening to socket,accepting client calls,
   // communicating with clients, etc. This will free the 
   // main control (see below) to do other stuff.
   myThreadArgument* serverArgument = new myThreadArgument(
          &myServer,&coutSemaphore,serverName);
   myThread* serverThread = new myThread(serverHandleThread,(void*)serverArgument);
   serverThread->execute();

   // main control: since the above serverThread is handling the server functions,
   // this main control is free to do other things.
   while ( 1 )
   {
      // do whatever you need to do here, I am using 
      // Sleep() to make a little delay, 
      // pretending to be the other possible processings you might want to do...
      Sleep(50000);

      // report the server status here...
      //
      // code used to report server status
      //
   }
   return 1;
}

通过查看上面的 main() 函数,可以知道主线程不会调用阻塞函数来侦听/接受传入的客户端,而只会执行初始化工作并创建服务器实例,阻塞调用(acceptClient())已移至由主线程创建的线程中,该线程是 serverHandleThread。因此,main 函数可以自由执行您可能希望执行的任何其他处理,例如报告服务器状态(您可以在下载的源文件中看到详细信息)。如果您继续检查在 main() 函数中创建的服务器线程 serverHandleThread 的定义(再次,这是一个简化版本),这个讨论会更清楚。

DWORD WINAPI serverHandleThread(LPVOID threadInfo)
{
   // other stuff...

   // get the server
   myTcpSocket* myServer = serverArgument->getClientConnect();
   string serverName = serverArgument->getHostName();

   myServer->bindSocket();      // bind the server to the socket
   myServer->listenToClient();  // server starts to wait for client calls

   // initialize the threads that will be generated to handle
   // each incoming client
   myThreadArgument* clientArgument[MAX_NUM_CLIENTS];
   myThread* clientHandle[MAX_NUM_CLIENTS];
   for ( int i = 0; i < MAX_NUM_CLIENTS; i++ )
   {
      clientArgument[i] = NULL;
      clientHandle[i] = NULL;
   }

   int currNumOfClients = 0;
   while ( 1 )
   {
      // wait to accept a client connection,processing 
      // is suspended until the client connects
      myTcpSocket* client;    // connection dedicated for client communication
      string clientName;      // client name 
      client = myServer->acceptClient(clientName);    
    
      // other stuff...

      // for this client, generate a thread to handle it so we can 
      // continue to accept and handle as more clients as we want
      if ( currNumOfClients < MAX_NUM_CLIENTS-1 )
      {
         clientArgument[currNumOfClients] = new myThreadArgument(
            client,coutSemaphore,clientName);
         clientHandle[currNumOfClients] = new myThread(clientHandleThread,
            (void*)clientArgument[currNumOfClients]);
         serverArgument->addClientArgument(clientArgument[currNumOfClients]);
         clientHandle[currNumOfClients]->execute();
         currNumOfClients++;
      }
   }
   return 1;
}

为了了解我们如何处理多个客户端,请注意,一旦 acceptClient() 返回,即收到并接受了传入的客户端呼叫,上述服务器线程将不会开始与该客户端通信,而是会创建一个新线程,称为 clientHandleThread,将客户端连接传递给该线程,并让服务器与新客户端之间的通信成为该新创建线程的专职工作。然后它将返回等待另一个传入呼叫,并创建另一个新线程来处理新的传入客户端。通过这样做,服务器可以处理任意数量的客户端。clientHandleThread 中的主流程如下(简化版本)

DWORD WINAPI clientHandleThread(LPVOID threadInfo)
{
   // some related stuff ...

   // get the client connection: receiving messages from 
   // client & sending messages to the 
   // client will all be done by using this client connection
   myTcpSocket* clientConnection = clientArgument->getClientConnect();
   string clientName = clientArgument->getHostName();

   // the server is communicating with this client here
   while(1)
   {
      string messageFromClient = "";
        
      // receive from the client
      int numBytes = clientConnection->recieveMessage(messageFromClient);
    
      // send to the client
      // ... construct a message to send, saved in messageToClient
      clientConnection->sendMessage(string(messageToClient));
    
      // other possible stuff ...

   }

   return 1;
}

既然我们已经解决了简单客户端/服务器结构存在的两个问题,我们就需要看看客户端。幸运的是,在理解了上述解决方案之后,理解客户端代码就容易多了,您应该可以毫无问题地阅读您下载的代码。

本节还需要讨论的另一个类是 mySemaphore 类。同样,一旦涉及多个线程,我们就会立即面临同步问题。mySemaphore 类就是为此目的而开发的。例如,如果一些/所有客户端线程需要访问某些全局变量,这个类将非常有帮助。在我们的例子中,由于我们只开发客户端/服务器结构的框架,也就是说,我们没有一个特定的应用程序需要适配,所以我们实际上没有严重的同步问题。但是,为了展示该类的用法,我们将控制台屏幕和日志文件视为全局资源,并使用该类来同步对该全局资源的访问。

该类提供的基本函数集非常直观:您可以通过实际创建一个新的信号量实例来创建信号量实例,或者打开一个现有的信号量实例。获得信号量实例后,您可以锁定它(等待直到可以成功锁定它,或者立即尝试锁定并如果无法锁定则返回),解锁它,更改其设置(初始计数、最大计数等),等等。

很明显,这个类封装了 WIN32 信号量 API,事实上,您可以在 MFC 中找到一个 CSemaphore 类,它也提供了一些这些 API 的包装器,但是,信不信由你,CSemaphore 缺少一个等待信号量释放的方法,这也许被认为是该类的关键功能之一!由于该类的应用相当直接和直观,因此我们不再详细讨论该类,您可以阅读文章 #2 来了解更多关于该类的信息。还值得一提的是,该类足够通用,可以在其他应用程序开发中使用。

5. 几个辅助类:myHostInfo、myEvent、myThreadArgument、myLog 和 myException

到目前为止,您在阅读和理解代码时应该没有大的问题。当您浏览代码时,您可能还会注意到我们开发的其他几个类。在本节中,我们将简要讨论这些辅助类,同样,所有这些类都在文章 #2 中有更详细的介绍,如果您需要了解更多信息,可以阅读它。

在客户端/服务器环境中,即使在构建客户端/服务器结构之前,您也需要弄清楚几个琐碎但重要的问题:如果我使用本地 PC 作为服务器或客户端,我的本地 PC 的域名和 IP 地址是什么?对于远程服务器,如果我知道其域名是 www.codeproject.com,我怎么知道它的 IP 地址,或者,如果我知道服务器的 IP 地址,我怎么知道它的域名?所有这些问题都可以通过使用 myHostInfo 类来回答:如果您将域名传递给构造函数,该类将告诉您 IP 地址;如果您将 IP 地址传递给构造函数,它将告诉您域名。如果您使用的是本地 PC,您可以使用不带参数的构造函数,并可以从此类查询名称和 IP 地址。同样,请阅读文章 #1 以获取更多信息。

myEvent 类是另一个用于客户端/服务器结构的通用类。其主要目的是让主线程了解哪个客户端线程已终止其通信会话,以便主线程可以报告服务器状态。在文章 #2 中,它的主要用途是安全地终止线程。因此,在这两个应用程序中,该类都充当信号类。文章 #2 对该类进行了更详细的描述。此外,文章 #2 还详细介绍了 myThreadArgument 类,其主要目的是打包一组参数并将该组馈送给线程,以便线程及其父线程可以共享关键信息。

myLog 类顾名思义,主要是为了帮助理解系统中正在发生的事情,因为调试多线程系统可能会很困难。如果您不需要日志,可以搜索所有 .cpp 文件中的 winLog 并注释掉它们。此外,为了捕获可能的错误,我们提供了一个简单的 myException 类。我们建议您保留这个类,因为它相当简单易懂。

6. 一个例子

现在我们已经完成了所有必要的类,我们可以给出一个例子。为了节省 CP 服务器上的空间,我没有使用任何屏幕截图,而是将部分日志文件粘贴到了本文中。请注意,此日志文件来自服务器端,您可以看到服务器与每个客户端之间的所有会话,您还可以看到服务器生成的定期状态报告。同样,所有消息都是通过键盘输入的,我还使用我自己的 PC 同时作为服务器和客户端——如果您能找到两台 PC 并让它们互相通信,会更有趣。

DATE: 07/23/04 - 01:04:22                    syslog.log

system started ...
initialize the winsock library ... successful

Retrieve the local host name and address:
        ==> Name: liyang
        ==> Address: 209.206.17.197

Summary of socket settings:
   Socket Id:     1936
   port #:        1200
   debug:         false
   reuse addr:    false
   keep alive:    false
   send buf size: 8192
   recv bug size: 8192
   blocking:      true
   linger on:     false
   linger seconds: 0

server finishes binding process... 
server is waiting for client calls ... 

-----------------------------------------------------------------
server (name:liyang) status report:
   the following clients have successfully connected with server: 
   the following clients have shutdown the connection: 
-----------------------------------------------------------------


==> A client from [liyang-A] is connected!
==> A client from [liyang-B] is connected!
==> A client from [liyang-C] is connected!

[RECV fr liyang-A]: hi, this is client A.
[SEND to liyang-A]: hello, A
[RECV fr liyang-B]: this is client B
[SEND to liyang-B]: hello, B
[RECV fr liyang-C]: this is C1
[SEND to liyang-C]: no, you are C!

-----------------------------------------------------------------
server (name:liyang) status report:
   the following clients have successfully connected with server: 
         liyang-A
         liyang-B
         liyang-C
   the following clients have shutdown the connection: 
-----------------------------------------------------------------

[RECV fr liyang-C]: yes, it was a typo!
[SEND to liyang-C]: okay!
[RECV fr liyang-A]: so, who has connected with you?
[SEND to liyang-A]: you, B and C.
[RECV fr liyang-B]: any news?
[SEND to liyang-B]: no, everything is cool.

-----------------------------------------------------------------
server (name:liyang) status report:
   the following clients have successfully connected with server: 
         liyang-A
         liyang-B
         liyang-C
   the following clients have shutdown the connection: 
-----------------------------------------------------------------

[RECV fr liyang-C]: in that case, I am leaving.
[SEND to liyang-C]: bye!
[RECV fr liyang-C]: quit
[RECV fr liyang-A]: what about now?
[SEND to liyang-A]: you and B.

-----------------------------------------------------------------
server (name:liyang) status report:
   the following clients have successfully connected with server: 
         liyang-A
         liyang-B
         liyang-C
   the following clients have shutdown the connection: 
         liyang-C
-----------------------------------------------------------------

[RECV fr liyang-B]: I am also leaving.
[SEND to liyang-B]: okay, come back!

==> A client from [liyang-D] is connected!

[RECV fr liyang-D]: hello, are you there?
[SEND to liyang-D]: yes!

-----------------------------------------------------------------
server (name:liyang) status report:
   the following clients have successfully connected with server: 
         liyang-A
         liyang-B
         liyang-C
         liyang-D
   the following clients have shutdown the connection: 
         liyang-B
         liyang-C
-----------------------------------------------------------------

[RECV fr liyang-A]: anything new?
[SEND to liyang-A]: client D is also connected!
[RECV fr liyang-A]: good! ask D if anything is new.
[SEND to liyang-A]: okay.
[RECV fr liyang-D]: nothing is new!
[SEND to liyang-D]: okay.

-----------------------------------------------------------------
server (name:liyang) status report:
   the following clients have successfully connected with server: 
         liyang-A
         liyang-B
         liyang-C
         liyang-D
   the following clients have shutdown the connection: 
         liyang-B
         liyang-C
-----------------------------------------------------------------

7. 结论

本文介绍了客户端/服务器结构的实现,其中服务器可以同时处理多个客户端。该应用程序开发了几个关键类,这些类也足够通用,可以在其他应用程序中使用。希望这对您的开发工作有所帮助,我当然欢迎任何建议和评论。

许可证

本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。

作者可能使用的许可证列表可以在此处找到。

© . All rights reserved.