一个轻量级的 C++ 客户端/服务器套接字类






4.43/5 (67投票s)
2004年5月20日
12分钟阅读

690004

31948
一个多平台 C++ 客户端/服务器套接字。
背景
本文介绍了一个通用的、轻量级的 C++ 客户端/服务器套接字类,以及一个使用该类构建的迷你信使。互联网上关于套接字编程的信息非常丰富,下载相关代码也并不耗时,因此,首先讨论这个类的优点是很有必要的。
首先,这个类是轻量级的。你可以在 MFC、C#、.NET 框架中找到套接字 API,但显然,要利用这些 API,你需要将应用程序与 MFC、.NET 框架绑定。在某些情况下,MFC、C# 或 .NET 可能不是应用程序需求的最佳解决方案。此外,如果应用程序要在 Unix/Linux 平台开发,那么这些 API 甚至不可用。另一方面,本文介绍的类可以轻松地用于控制台应用程序;无需为该类提供任何窗口句柄即可使用。此外,通过简单地在头文件中添加 #define UNIX
,该类可以直接集成到在 Unix/Linux 平台上开发的应用程序中。
其次,该类通过使用最通用和最底层的构建块提供了高性能的客户端/服务器结构。这为开发人员带来了极大的灵活性,例如,由开发人员决定添加适当的自定义安全系统。在其他优点中,使用该类构建的服务器可以在 Windows 机器上运行,客户端可以在同一台机器上(通过进程间通信),或者更常见的是在运行 Windows 或 Unix 系统的不同机器上。其他通信方可以是本地网络上的站点,也可以是通过 Internet 进行通信的机器。
引言
本文的其余部分将按照建立通信的直观流程进行组织
- 通信之前;
- 通信期间;
- 通信之后;
通信之前,我们需要了解通信双方的基本信息:本地机器的名称是什么,本地机器的 IP 地址是什么?如果我知道远程主机的名称是 www.yuchen.net,那么对应的 IP 地址是什么?反之呢?通过使用我们的 myHostInfo
类,可以轻松检索这些基本信息——虽然它不是本文的“明星”类,但许多开发人员可能会发现它很方便使用。
为了实现通信,mySocket
类及其子类 myTcpSocket
是明星类。为了确保性能,需要能够控制套接字的行为。基类 mySocket
提供了控制套接字行为的方法,例如阻塞或非阻塞、延迟开启/关闭、接收缓冲区大小、发送缓冲区大小等。myTcpSocket
进一步将套接字定义为 TCP 套接字,其方法确保通信以可靠的方式进行。
通信之后,上述明星类负责完成善后工作。另外两个我们开发的用于协助通信的类也值得一提。由于通信的复杂性,防范潜在的故障非常重要。提供了一个非常简单的异常处理类 myException
,其 response()
方法被定义为 virtual
。该方法的默认实现只是将错误消息输出到日志文件(见下文)和标准输出。但是,可以通过覆盖此方法来决定任何所需的特定处理。
另一个简单的类是系统日志类 myLog
,它在记录系统采取的操作时非常方便。通常,系统生成的日志文件是查找错误的第一站。
本文的最后一节介绍了一个使用上述类开发的应用程序。这个应用程序叫做迷你信使,两个人可以用它来互相交谈。它本质上是一个 Yahoo! Messenger,但使用它时,屏幕上不会显示 Yahoo! 标志,所以你不必担心老板可能会在背后监视你。这个小型应用程序的主要目的当然是展示我们开发的类的一种可能的用法。
myHostInfo 类
构建通信的第一步是了解对方:它的 域名,它对应的 IP 地址。提供了 myHostInfo
类来查询主机的网络信息。
构造函数/析构函数摘要
myHostInfo()
默认构造函数。在未给出/未知域名或 IP 地址的情况下,可以使用此构造函数,在这种情况下,将使用当前处理器的标准名称作为本地主机名。
myHostInfo(string& sHostName, hostType type)
sHostName
可以是主机名,例如 www.yuchen.net,此时type
必须使用NAME
值。sHostName
也可以是 IP 地址,例如“128.34.67.23”,此时type
必须使用ADDRESS
值。~myHostInfo()
方法摘要
const string& getHostIPAddress()
返回主机机的 IP 地址。
const string& getHostName()
返回主机机的 域名。
以下示例显示了此类用法。
string serverName = "www.yuchen.net"; myHostInfo myServer(serverName,NAME); string serverName = myServer.getHostName(); string serverAddr = myServer.getHostIPAddress(); cout << "Server name: " << serverName << endl; cout << "Server IP address: " << serverAddr << endl;
上述代码的输出如下(我无法获取截图,我将直接复制粘贴)
Server name: www.yuchen.net
Server IP address: 66.218.85.169
Press any key to continue
mySocket 类
套接字是所有 Internet 通信最基本的构建块。mySocket
类被提供为该构建块的基类。TCP 和 UDP 套接字将从继承此基类构建(在本文中,仅开发了 TCP 套接字)。
构造函数/析构函数摘要
mySocket(int portNumber)
端口号用于构造套接字。
virtual ~mySocket()
一旦创建了套接字,应用程序就可以查询或重置各种设置。
mySocket
类中的方法主要是这些设置的修改器。
方法摘要
我们只展示 set 方法,get 方法省略,详细请参考代码
void setDebug(int)
启用调试信息的记录。此选项允许内核维护发送和接收的消息历史。1 为开启,0 为关闭。
void setReuseAddr(int)
在正常情况下,两个或多个应用程序不能使用
bind()
来竞标同一个端口和地址。通过开启此选项,此规则将被忽略。1 为开启,0 为关闭。void setKeepAlive(int)
通过启用周期性的消息传输来保持已连接套接字处于活动状态。如果消息发送和接收失败,则认为已连接套接字已损坏。1 为开启,0 为关闭。
void setLingerOnOff(bool)
开启此选项会导致套接字在关闭时延迟。通常,当应用程序尝试关闭套接字时,系统会尝试传递未发送的数据。如果此选项已开启,则进程会阻塞直到所有数据都传输完毕或直到延迟时间到期。
true
表示开启,false
表示关闭。void setLingerSeconds(int)
设置系统用于发送所有未发送数据的时间。
int
单位为秒。如果设置为 0,则在关闭套接字时丢弃剩余未发送的数据。请参见setLingerOnOff(bool)
。void setSocketBlocking(int)
为给定套接字开启和关闭阻塞模式会控制在执行以下操作时进程的挂起
- 向已连接的 TCP 套接字写入数据。
- 从已连接的 TCP/UDP 套接字接收数据。
- 使用 TCP 套接字接受来自对等应用程序的连接。
默认情况下,这些操作会阻塞执行进程。使用 1 表示开启以强制阻塞,使用 0 表示关闭以关闭阻塞。
void setSendBufSize(int)
设置发送数据的缓冲区大小。仅当默认发送缓冲区大小不足时才有用。另请参阅
setReceiveBufSize(int)
。void setReceiveBufSize(int)
设置接收数据的缓冲区大小。仅当默认接收缓冲区大小不足时才有用。另请参阅
setSendBufSize(int)
。friend ostream& operator<<(ostream&, mySocket&)
用于将套接字记录到标准输出或任何其他 IO 流。
以下示例显示了套接字的设置(请注意,我们使用了 myHostInfo
来获取本地机器的信息)
cout << "Retrieve the local host name and address:" << endl; myHostInfo myLocalMachine; // use the default constructor string localHostName = myLocalHost.getHostName(); string localHostAddr = myLocalHost.getHostIPAddress(); cout << "Name: " << localHostName << endl; cout << "Address: " << localHostAddr << endl; mySocket localSocket(1000); // 1000 is the port number cout << localSocket; // show this socket // change some setting: set linger on localSocket.setLingerOnOff(true); localSocket.setLingerSeconds(10); // show it again cout << endl << "After changing the socket settings ... " << endl; cout << localSocket;
这是上述代码的输出
Name: liyang
Address: 209.206.17.121
--------------- Summary of socket settings -------------------
Socket Id: 1960
port #: 1000
debug: false
reuse addr: false
keep alive: false
send buf size: 8192
recv bug size: 8192
blocking: true
linger on: false
linger seconds: 0
----------- End of Summary of socket settings ----------------
After changing the socket settings ...
--------------- Summary of socket settings -------------------
Socket Id: 1960
port #: 1000
debug: false
reuse addr: false
keep alive: false
send buf size: 8192
recv bug size: 8192
blocking: true
linger on: true
linger seconds: 10
----------- End of Summary of socket settings ----------------
Press any key to continue
myTcpSocket 类
myTcpSocket
类用于构建基于 TCP 套接字通信。我们首先描述此类中的重要方法,然后讨论构建客户端和服务器之间通信的流程。
构造函数/析构函数摘要
myTcpSocket(int)
应用程序使用的构造函数。
int
是将与此套接字关联的端口号。~myTcpSocket()
创建 TCP 套接字后,可以使用以下方法来构建两个套接字之间的通信。
方法摘要
int sendMessage(string&)
将消息
string&
发送给对方。返回发送的字节数。这可以是服务器调用或客户端调用。void bindSocket()
此调用用于将套接字文件描述符与套接字端口号以及将来客户端将使用的地址相关联。在任何通信发生之前,这是一个必需的调用。这仅为服务器端调用,不是客户端调用。
myTcpSocket* acceptClient(string&)
调用此函数以接受连接的客户端。这仍然是服务器调用,不是客户端调用。它还会阻塞线程直到检测到传入的客户端调用。一旦检测到传入的客户端调用,就会返回一个新的套接字。对于服务器来说,这个新套接字是传入客户端的唯一表示:与此连接客户端的任何后续通信都通过此套接字进行。
传入客户端的主机名,例如 www.yuchen.net,将保存在
string&
中以便于访问。void listenToClient(int numPorts = 5)
服务器端调用,将服务器置于“侦听”状态。
numPorts
指定服务器总共可以处理多少个传入调用。virtual void connectToServer(string&,hostType)
客户端调用,用于客户端连接服务器。
string&
是客户端尝试连接的服务器的名称或 IP 地址。hostType
的定义如下enum hostType {NAME, ADDRESS};
典型调用通常采用以下两种形式之一
myClient.connectToServer(www.yuchen.net, NAME);
myClient.connectToServer(“128.23.36.89”, ADDRESS);
int recieveMessage(string&)
从对方接收消息。这可以是服务器调用或客户端调用。
string&
保存缓冲区,在成功返回后将包含消息,返回的int
值表示收到的字节数。
构建通信
现在,是时候描述服务器和客户端之间构建通信的流程了(我们不深入讨论 myException
类和 myLog
类,您可以阅读文档/代码来理解它们)。由于本文无意成为教程,因此此描述仅涵盖故事的重要部分。
服务器必须先启动
myTcpSocket myServer(PORTNUM); // create the socket and everything cout << myServer; // show the server configuration
这将创建一个名为 myServer
的服务器。在该类内部,使用 PORTNUM
给定的端口号构建了一个套接字。这很像为你的房子申请电话线:你已经打电话给,比如说,BellSouth,所以你已经获得了你房子的电话号码。
接下来要做的就是将你的电话与你获得的电话号码绑定,通过将电话连接到墙上的电话插孔来完成。墙上的电话插孔就是刚刚创建的套接字,而电话本身就像 PORTNUM
。
myServer.bindSocket();
在此之后,你就可以接听朋友(以及不幸的账单公司)的电话了。这可以通过等待传入电话来完成。在我们的模型中,通过以下方式完成:
myServer.listenToClient();
当电话铃响时,您可以通过拿起电话来接听来电
/* wait to accept a client connection. processing is suspended until the client connects */ cout << "server is waiting for client connection ... " << endl; // connection dedicated for client communication myTcpSocket* client; string clientHost; // to hold the client’s name client = myServer.acceptClient(clientHost); // pick up the call!
请注意,服务器通过创建新套接字来接受传入的客户端调用,它实际上将继续在其自己的套接字上等待其他传入调用——这可以理解为电信公司提供的“呼叫等待”功能。
一旦我们到达这一点,服务器所做的唯一事情就是通过调用 sendMessage()
和 receiveMessage()
函数与客户端来回传递信息。下一节的迷你信使会展示详细信息。
另一方面,客户端很简单。但是,在我们启动客户端之前,我们需要知道我们要打电话给哪个服务器。假设我们知道服务器的 IP 地址(如果不知道,我们应该知道服务器的名称,然后可以使用 myHostInfo
类来查询服务器的 IP 地址),我们需要将此 IP 地址写入 serverConfig.txt 文件,并且我们需要读取此文件以获取服务器的 IP 地址,以便我们可以调用它。
首先,让我们构建客户端
myTcpSocket myClient(PORTNUM); // build the client myClient.setLingerOnOff(true); myClient.setLingerSeconds(10); cout << myClient; // show the client configuration
假设服务器的 IP 地址保存在 serverAddr
中,我们现在可以连接到服务器
// connect to the server.
myClient.connectToServer(serverAddr,ADDRESS);
接下来是使用 sendMessage()
和 receiveMessage()
方法与服务器通信。
迷你信使
为了演示到目前为止讨论的类的用法,开发了一个小型应用程序。这个应用程序允许您通过 Internet 与您的朋友交谈,而无需使用 Yahoo! Messenger 等工具。通过上述讨论,现在应该可以轻松理解服务器和客户端的双方了。
要使用此信使,假设您的朋友是服务器,他必须在其一端启动服务器,然后他会告知您其服务器的 IP 地址,例如,通过打电话(我们仍然需要低技术)。在您的 serverConfig.txt 文件中写入 IP 地址后,您就可以启动您的客户端,准备开始交谈了。
服务器端
#include "..\mySocket\mySocket.h" #include "..\myLog\myLog.h" #include "..\myException\myException.h" #include "..\myHostInfo\myHostInfo.h" myLog winLog; int main() { #ifdef WINDOWS_XP // Initialize the winsock library WSADATA wsaData; winLog << "system started ..." << endl; winLog << endl << "initialize the winsock library ... "; try { if (WSAStartup(0x101, &wsaData)) { myException* initializationException = new myException(0,"Error: calling WSAStartup()"); throw initializationException; } } catch(myException* excp) { excp->response(); delete excp; exit(1); } winLog << "successful" << endl; #endif // get local information for the server winLog << endl; winLog << "Retrieve the local host name and address:" << endl; myHostInfo uHostAddress; string localHostName = uHostAddress.getHostName(); string localHostAddr = uHostAddress.getHostIPAddress(); cout << "----------------------------------------" << endl; cout << " My local host information:" << endl; cout << " Name: " << localHostName << endl; cout << " Address: " << localHostAddr << endl; cout << "----------------------------------------" << endl; winLog << " ==> Name: " << localHostName << endl; winLog << " ==> Address: " << localHostAddr << endl; // open socket on the local host myTcpSocket myServer(PORTNUM); cout << myServer; winLog << "server configuation: " << endl; winLog << myServer; myServer.bindSocket(); cout << endl << "server finishes binding process... " << endl; winLog << endl << "server finishes binding process... " << endl; myServer.listenToClient(); cout << "server is listening to the port ... " << endl; winLog << "server is listening to the port ... " << endl; // wait to accept a client connection. // processing is suspended until the client connects cout << "server is waiting for client connecction ... " << endl; winLog << "server is waiting for client connnection ... " << endl; // connection dedicated for client communication myTcpSocket* client; string clientHost; // client name etc. client = myServer.acceptClient(clientHost); cout << endl << "==> A client from [" << clientHost << "] is connected!" << endl << endl; winLog << endl << "==> A client from [" << clientHost << "] is connected!" << endl << endl; while(1) { string clientMessageIn = ""; // receive from the client int numBytes = client->recieveMessage(clientMessageIn); if ( numBytes == -99 ) break; cout << "[RECV:" << clientHost << "]: " << clientMessageIn << endl; winLog << "[RECV:" << clientHost << "]: " << clientMessageIn << endl; // send to the clien char sendmsg[MAX_MSG_LEN+1]; memset(sendmsg,0,sizeof(sendmsg)); cout << "[" << localHostName << ":SEND] "; cin.getline(sendmsg,MAX_MSG_LEN); if ( numBytes == -99 ) break; string sendMsg(sendmsg); if ( sendMsg.compare("Bye") == 0 || sendMsg.compare("bye") == 0 ) break; winLog << "[" << localHostName << ": SEND] " << sendMsg << endl; client->sendMessage(sendMsg); } #ifdef WINDOWS_XP // Close the winsock library winLog << endl << "system shut down ..."; try { if (WSACleanup()) { myException* cleanupException = new myException(0,"Error: calling WSACleanup()"); throw cleanupException; } } catch(myException* excp) { excp->response(); delete excp; exit(1); } winLog << "successful" << endl; #endif return 1; }
客户端
#include "..\mySocket\mySocket.h" #include "..\myLog\myLog.h" #include "..\myException\myException.h" #include "..\myHostInfo\myHostInfo.h" myLog winLog; string serverIPAddress = ""; void readServerConfig(); void checkFileExistence(const string&); int main() { #ifdef WINDOWS_XP // Initialize the winsock library WSADATA wsaData; winLog << "system started ..." << endl; winLog << endl << "initialize the winsock library ... "; try { if (WSAStartup(0x101, &wsaData)) { myException* initializationException = new myException(0,"Error: calling WSAStartup()"); throw initializationException; } } catch(myException* excp) { excp->response(); delete excp; exit(1); } winLog << "successful" << endl; #endif // get local information for the client winLog << endl; winLog << "Retrieve the localHost [CLIENT] name and address:" << endl; myHostInfo uHostAddress; string localHostName = uHostAddress.getHostName(); string localHostAddr = uHostAddress.getHostIPAddress(); cout << "Name: " << localHostName << endl; cout << "Address: " << localHostAddr << endl; winLog << " ==> Name: " << localHostName << endl; winLog << " ==> Address: " << localHostAddr << endl; // get server's information readServerConfig(); winLog << endl; winLog << "Retrieve the remoteHost [SERVER] name and address:" << endl; winLog << " ==> the given address is " << serverIPAddress << endl; myHostInfo serverInfo(serverIPAddress,ADDRESS); string serverName = serverInfo.getHostName(); string serverAddr = serverInfo.getHostIPAddress(); cout << "Name: " << serverName << endl; cout << "Address: " << serverAddr << endl; winLog << " ==> Name: " << serverName << endl; winLog << " ==> Address: " << serverAddr << endl; // an instance of the myTcpSocket is created. At this point a TCP // socket has been created and a port has been defined. myTcpSocket myClient(PORTNUM); myClient.setLingerOnOff(true); myClient.setLingerSeconds(10); cout << myClient; winLog << "client configuation: " << endl; winLog << myClient; // connect to the server. cout << "connecting to the server [" << serverName << "] ... " << endl; winLog << "connecting to the server [" << serverName << "] ... " << endl; myClient.connectToServer(serverAddr,ADDRESS); int recvBytes = 0; while (1) { // send message to server char sendmsg[MAX_MSG_LEN+1]; memset(sendmsg,0,sizeof(sendmsg)); cout << "[" << localHostName << ":SEND] "; cin.getline(sendmsg,MAX_MSG_LEN); string sendMsg(sendmsg); if ( sendMsg.compare("Bye") == 0 || sendMsg.compare("bye") == 0 ) break; winLog << "[" << localHostName << ": SEND] " << sendMsg << endl; myClient.sendMessage(sendMsg); // receive message from server string clientMessageIn = ""; recvBytes = myClient.recieveMessage(clientMessageIn); if ( recvBytes == -99 ) break; cout << "[RECV:" << serverName << "]: " << clientMessageIn << endl; winLog << "[RECV:" << serverName << "]: " << clientMessageIn << endl; } #ifdef WINDOWS_XP // Close the winsock library winLog << endl << "system shut down ..."; try { if (WSACleanup()) { myException* cleanupException = new myException(0,"Error: calling WSACleanup()"); throw cleanupException; } } catch(myException* excp) { excp->response(); delete excp; exit(1); } winLog << "successful" << endl; #endif return 1; } void readServerConfig() { string serverConfigFile = "serverConfig.txt"; checkFileExistence(serverConfigFile); ifstream serverConfig(serverConfigFile.c_str()); // read server's IP address getline(serverConfig,serverIPAddress); serverConfig.close(); } void checkFileExistence(const string& fileName) { ifstream file(fileName.c_str()); if (!file) { cout << "Cannot continue:" << fileName << " does NOT exist!" << endl; exit(1); } file.close(); }
这是一个示例输出屏幕(请注意,我使用的是同一台 PC 进行通话,但仍然通过 Internet)
服务器
------------------------------------------------------
My local host information:
Name: liyang
Address: 209.206.17.121
------------------------------------------------------
--------------- Summary of socket settings -------------------
Socket Id: 1960
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
----------- End of Summary of socket settings ----------------
server finishes binding process...
server is listening to the port ...
server is waiting for client connecction ...
==> A client from [liyang] is connected!
[RECV:liyang]: hello?
[liyang:SEND] yes?
[RECV:liyang]: so we can talk...
[liyang:SEND] looks like so.
!! your party has shut down the connection...
Press any key to continue
客户端
Name: liyang
Address: 209.206.17.121
Name: liyang
Address: 209.206.17.121
--------------- Summary of socket settings -------------------
Socket Id: 1944
port #: 1200
debug: false
reuse addr: false
keep alive: false
send buf size: 8192
recv bug size: 8192
blocking: true
linger on: true
linger seconds: 10
----------- End of Summary of socket settings ----------------
connecting to the server [liyang] ...
[liyang:SEND] hello?
[RECV:liyang]: yes?
[liyang:SEND] so we can talk...
[RECV:liyang]: looks like so.
[liyang:SEND] bye
Press any key to continue
结论
本文介绍了 C++ 中的一个轻量级服务器/客户端类,我希望它能在您的开发工作中有所帮助,当然还有更多需要考虑的事项才能使其正常工作。我欢迎任何评论/建议。