初学者使用 C++ 编写 Windows TCP 套接字程序






4.65/5 (189投票s)
帮助初学者使用 Winsock 编写 C++ TCP 套接字程序
引言
(我猜)关于这个主题的材料并不多,能够充分解释 Windows 套接字的使用,让初学者能够理解并开始编程。我仍然记得我曾多么费力地寻找一个合适的教程,但当我开始自己编程时,这些教程却留下了许多疑问。
那已经是很久以前的事了,而且对我来说,编写第一个能够通过互联网与其他计算机通信的应用程序是一项巨大的挑战——尽管我最初接触套接字是通过 Visual Basic;这是一个高级且非常用户友好的编程语言。现在我早已转向更强大的 C++,我很快发现,我在 VB 中编写套接字所付出的劳动与等待我的相比,简直微不足道!
幸运的是,经过在互联网上搜索各种网页数小时后,我能够收集到所有零散的信息,并最终用 C++ 编译我的第一个 telnet 程序。我的目标是将所有必要的数据集中在一个地方;就在这里,这样读者就不必在互联网上重新收集所有数据。因此,我提供本教程,希望能将其作为开始编程的足够信息。
在我们开始之前,您需要 `include` winsock.h 并将 libws2_32.a 链接到您的项目中,以便使用 TCP/IP 所需的 API。如果这不可能,可以使用 `LoadLibrary()` 在运行时加载 ws2_32.dll,或类似的方法。
本文中的所有代码都是使用“Bloodshed Dev-C++ 4.9.8.0”编写和测试的;但通常情况下,它应该能与任何编译器兼容,只需少量修改。
线程、端口和套接字究竟是什么?
实际上,我们可以用“套接字”这个名字所呈现的形象比喻来类似地说明它们是什么以及它们如何工作。您可能还记得,在实际的机械套筒中,它是连接的阴端,即“接收”端。“线程”是您的计算机与远程计算机之间连接的符号名称,而线程连接到套接字。
如果我用这些专业术语让您感到困惑,您可以将线程想象成一条从一台计算机延伸到另一台计算机的实际的、物理的缝纫线,就像常见的类比一样。但是,为了将线连接到每台计算机,必须有一个接收对象来连接线,这些对象被称为套接字。
套接字可以打开在任何“端口”上;端口只是一个唯一的数字,用于将其与其他线程区分开,因为同一台计算机可以建立不止一个连接。其中一些端口已被预留用于特定目的。除了这些端口之外,还有大量其他端口可用于任何事情:实际上超过 6,000 个。下面列出了一些常用端口及其相应的服务
端口 | Service |
7 | Ping |
13 | 时间 |
15 | Netstat |
22 | SSH |
23 | Telnet(默认) |
25 | SMTP(发送邮件) |
43 | Whois(查询信息) |
79 | Finger(查询服务器信息) |
80 | HTTP(网页) |
110 | POP(接收邮件) |
119 | NNTP |
513 | CLOGIN(用于 IP 欺骗) |
这里还有许多用于特定目的的端口未显示。但通常情况下,如果您想使用一个没有特定分配服务的端口,1000 到 6535 之间的任何端口都可以。当然,如果您想监听发送和接收服务端口的消息,也可以这样做。
您现在连接到互联网了吗?假设您连接了,并且您正在运行 Internet Explorer 或其他网页服务,以及 AOL 或其他聊天程序。除此之外(好像连接已经够慢了),您还试图发送和接收电子邮件。您认为哪些端口已打开,正在发送和接收数据?
- Internet Explorer(等)通过端口 80 发送和接收数据
- AOL 和其他即时消息程序通常喜欢使用未分配的、数千以上的端口,以避免干扰。每个聊天程序都有所不同,因为没有特定的“聊天”服务,并且可以同时运行多个消息程序。
- 当您发送电子邮件时,您和远程邮件服务器正在使用端口 25 进行通信。
- 当您接收电子邮件时,您的邮件客户端(如 Microsoft Outlook)使用端口 110 从邮件服务器检索您的邮件。
列表还将继续。
显然,仅知道我们正在使用的端口号是不够的;我们需要知道我们正在连接到哪个远程计算机/服务器。就像我们在拜访别人之前要弄清楚他们的家庭地址一样,如果我们正在连接而不是仅仅监听(聊天程序需要能够做到这两点),我们就必须知道我们正在连接的主机的“IP 地址”。
IP 地址是分配给网络上的每台计算机的标识号,由四个由句点分隔的数字组组成。您可以通过在 MSDOS 提示符下运行 ipconfig.exe 来查看您的 IP 地址。
在本教程的示例中,我们将使用所谓的“环回地址”来测试我们的聊天程序,而无需连接到互联网。该地址是 **127.0.0.1**。每当您尝试连接到此 IP 时,计算机都会将请求回送到您的计算机,并尝试在指定端口上查找服务器。这样,您就可以在同一台计算机上运行服务器和客户端。一旦您决定连接到其他远程计算机,并且调试好了您的聊天程序,您就需要获取每台计算机的唯一 IP 地址才能在互联网上与它们通信。
由于我们人类很容易忘记事情,而且我们不可能记住我们访问的每个网站的一串数字,所以一些聪明人想出了“域名”这个绝妙的主意。现在,我们有像 www.yahoo.com 和 www.cia.gov 这样简洁的名称,它们代表的 IP 地址比笨拙的数字串容易记住得多。当您在浏览器窗口中键入这些名称之一时,该域名的 IP 地址会通过“路由器”进行查找,一旦获得(或者主机被“解析”),浏览器就可以联系位于该地址的服务器。
例如,假设我打电话给接线员,因为我记不住我女朋友的电话号码(可能性很小)。所以,我只需要告诉接线员她的名字(以及一些其他细节,但那不重要),她就会愉快地告诉我那些数字。当请求任何域名的 IP 地址时,发生的事情大致就是这样。
我们有两个 API 可以完成此任务。最好检查一下使用您程序的用户是键入了域名还是 IP 地址,以便您的程序可以在继续之前查找正确的 IP 地址。大多数人 anyway, 不会想记住任何 IP 地址,所以您很可能需要在建立连接之前将域名转换为 IP 地址——这需要计算机连接到互联网。然后,一旦您有了地址,就可以连接了。
//Return the IP address of a domain name
DECLARE_STDCALL_P(struct hostent *) gethostbyname(const char*);
//Convert a string address (i.e., "127.0.0.1") to an IP address. Note that
//this function returns the address into the correct byte order for us so
//that we do not need to do any conversions (see next section)
unsigned long PASCAL inet_addr(const char*);
字节序
您刚以为所有这些线程-套接字的东西都会很简单容易,我们就得开始讨论字节序了。这是因为 Intel 计算机和网络协议使用不同的字节序,在我们发送每个端口和 IP 地址之前,我们必须将其转换为网络字节序;否则就会一团糟。端口 25,如果不反转,实际上不会成为端口 25。所以,当我们尝试与服务器通信时,我们必须确保我们说的是与服务器相同的语言。
幸运的是,我们不必手动编写所有转换函数;Microsoft 好心地为我们提供了几个 API 来完成此任务。用于更改 IP 或端口号字节序的四个函数如下所示
u_long PASCAL htonl(u_long); //Host to network long
u_long PASCAL ntohl(u_long); //Network to host long
u_short PASCAL htons(u_short); //Host to network short
u_short PASCAL ntohs(u_short); //Network to host short
**记住!**“主机”计算机是指侦听并接受连接的计算机,而“网络”计算机是连接到主机的访问者。
因此,例如,在我们指定要侦听或连接的端口之前,我们必须使用 `htons()` 函数将数字转换为网络字节序。请注意,在使用 `inet_addr()` 将字符串 IP 地址转换为所需格式后,我们将以正确的网络顺序返回地址,从而无需调用 `htonl()`。区分 `htons()` 和 `htonl()` 的一个简单方法是,将端口号视为较短的数字,将 IP 视为较长的数字(这是正确的——IP 地址由四组最多三位数字组成,用句点分隔,而端口号只有一个)。
启动 Winsock
好的,现在我们终于涵盖了基础知识,希望您开始看到隧道的尽头,我们可以继续前进。如果您不理解程序的每一个方面,请不要担心,因为随着我们的进展,许多补充事实将得以揭示。
使用 Windows 套接字(又名“Winsock”)编程的第一步是启动 Winsock API。Winsock 有两个版本;版本一是旧的、有限的版本;版本二是最新版本,因此我们倾向于指定它。
#define SCK_VERSION1 0x0101
#define SCK_VERSION2 0x0202
int PASCAL WSAStartup(WORD,LPWSADATA);
int PASCAL WSACleanup(void);
//This typedef will be filled out when the function returns
//with information about the Winsock version
typedef struct WSAData
{
WORD wVersion;
WORD wHighVersion;
char szDescription[WSADESCRIPTION_LEN+1];
char szSystemStatus[WSASYS_STATUS_LEN+1];
unsigned short iMaxSockets;
unsigned short iMaxUdpDg;
char * lpVendorInfo;
}
WSADATA;
typedef WSADATA *LPWSADATA;
您只需要分别调用一次这些函数,前者在初始化 Winsock 时调用,后者在完成时调用。但是,在您完成之前不要关闭 Winsock,因为这样做会取消您的程序发起的任何连接或您正在侦听的任何端口。
初始化套接字
我们现在应该理解套接字的工作原理了,但直到现在我们还不知道如何初始化它们。必须填写正确的参数并将其传递给一个方便的 API 调用,该调用会(希望)开始套接字。在这种情况下,我们将返回我们创建的套接字的句柄。这个句柄非常“方便”,我们必须随身携带它来操作套接字活动。
当您完成所有肮脏的工作后,在程序退出之前关闭所有打开的套接字被认为是良好的编程实践。当然,当程序退出时,它所有的连接都会被强制关闭,包括任何套接字,但最好通过 `closesocket()` 以优雅的方式关闭它们。调用此 API 时,您需要将套接字句柄传递给它。
//There are many more options than the ones defined here, to see them
//browse the winsock2.h header file
#define SOCK_STREAM 1
#define SOCK_DGRAM 2
#define SOCK_RAW 3
#define AF_INET 2
#define IPPROTO_TCP 6
SOCKET PASCAL socket(int,int,int);
int PASCAL closesocket(SOCKET);
创建套接字时,您需要传递“地址族”、套接字“类型”和“协议类型”。除非您进行一些特殊的(或奇怪的)工作,这超出了本报告的范围,否则通常应该只传递 `AF_INET` 作为默认地址族。此参数指定如何解释计算机地址。
套接字不仅仅只有一种类型;实际上,有很多种。最常见的三个包括:原始套接字、流套接字和数据报套接字。然而,在本教程中,我们使用的是流套接字,因为我们处理的是 TCP 协议,因此我们将 `SOCK_STREAM` 指定为 `socket()` 的第二个参数。
我们很接近了,非常接近!我们已经完成了“细节”部分,现在让我们继续进行更有趣的 Winsock 编程部分。
连接到远程主机(作为客户端)
让我们用一个可以连接到远程计算机的简单程序来尝试我们学过的内容。这样做将有助于您更好地理解一切如何工作,并有助于防止信息过载!
您需要填写有关您要连接到的远程主机的信息,然后将此结构的指针传递给神奇的函数 `connect()`。该结构和 API 如下所示。请注意,`sin_zero` 参数是不需要的,因此留空。
struct sockaddr_in
{
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
int PASCAL connect(SOCKET,const struct sockaddr*,int);
我强烈建议您亲手输入本报告中的所有示例,而不是将其复制粘贴到您的编译器中。虽然我知道这样做会减慢您的速度,但我有信心并从经验中知道,您通过这种方式学习过程会比复制粘贴代码好得多。
//CONNECT TO REMOTE HOST (CLIENT APPLICATION)
//Include the needed header files.
//Don't forget to link libws2_32.a to your program as well
#include <winsock.h>
SOCKET s; //Socket handle
//CONNECTTOHOST – Connects to a remote host
bool ConnectToHost(int PortNo, char* IPAddress)
{
//Start up Winsock…
WSADATA wsadata;
int error = WSAStartup(0x0202, &wsadata);
//Did something happen?
if (error)
return false;
//Did we get the right Winsock version?
If (wssadata.wVersion != 0x0202)
{
WSACleanup(); //Clean up Winsock
return false;
}
//Fill out the information needed to initialize a socket…
SOCKADDR_IN target; //Socket address information
target.sin_family = AF_INET; // address family Internet
target.sin_port = htons (PortNo); //Port to connect on
target.sin_addr.s_addr = inet_addr (IPAddress); //Target IP
s = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP); //Create socket
if (s == INVALID_SOCKET)
{
return false; //Couldn't create the socket
}
//Try connecting...
if (connect(s, (SOCKADDR *)&target, sizeof(target)) == SOCKET_ERROR)
{
return false; //Couldn't connect
}
else
return true; //Success
}
//CLOSECONNECTION – shuts down the socket and closes any connection on it
void CloseConnection ()
{
//Close the socket if it exists
if (s)
closesocket(s);
WSACleanup(); //Clean up Winsock
}
在继续之前,请键入此代码并尝试一下。
接收连接——充当服务器
现在您已经体验过连接到远程计算机的感受了,是时候扮演“服务器”角色了;这样远程计算机就可以连接到您。要做到这一点,我们可以在任何端口上“侦听”并等待传入的连接。一如既往,我们使用一些方便的 API 调用
int PASCAL bind(SOCKET,const struct sockaddr*,int); //bind to a socket
int PASCAL listen(SOCKET,int); //Listen for an incoming connection
//Accept a connection request
SOCKET PASCAL accept(SOCKET,struct sockaddr*,int*);
当您充当服务器时,您可以接收您正在侦听的端口上的连接请求:例如,假设一台远程计算机想与您的计算机聊天,它会先询问您的服务器是否愿意建立连接。为了建立连接,您的服务器必须 `accept()` 连接请求。请注意,“服务器”决定是否建立连接。最后,两台计算机都已连接,可以交换数据。
虽然 `listen()` 函数是在端口上侦听并充当服务器的最简单方法,但它不是最理想的。当您尝试使用它时,您会很快发现您的程序将冻结,直到收到传入连接,因为 `listen()` 是一个“阻塞”函数——它一次只能执行一项任务,并且在连接挂起之前不会返回。
这绝对是个问题,但有一些解决方案。首先,如果您熟悉多线程应用程序(请注意,我们这里*不是*指 TCP 线程),那么您可以将服务器代码放在一个单独的线程上,该线程启动后不会冻结整个程序,从而不会影响父程序的效率。这确实比它需要的要麻烦得多;因为您可以直接用“异步”套接字替换 `listen()` 函数。如果这个听起来很重要的名字引起了您的注意,您可以跳到下一节,但建议您留在这里学习基础知识。我们稍后会优化我们的代码;但现在,让我们专注于基本要素。
在您考虑侦听端口之前,您必须
- 初始化 Winsock(我们之前讨论过,还记得吗)
- 启动一个套接字并确保它返回一个非零值,这表示成功,并且是套接字的句柄
- 用必要的数据填充 `SOCKADDR_IN` 结构,包括地址族、端口和 IP 地址。
- 使用 `bind()` 将套接字绑定到特定的 IP 地址(如果您在 `SOCKADDR_IN` 的 `sin_addr` 部分指定了 **inet_addr("0.0.0.0")** 或 `htonl(INADDR_ANY)`,您可以绑定到任何 IP 地址)
此时,如果一切顺利,您就可以调用 `listen()` 并随意收听了。
`listen()` 的第一个参数必须是您之前初始化的套接字的句柄。当然,此套接字附加到的任何端口都将是您正在侦听的端口。然后,您可以使用下一个也是最后一个参数指定可以同时与您的服务器通信的远程计算机数量。然而,通常情况下,除非您想排除除一个或几个连接之外的所有连接,否则我们只将 `SOMAXCONN`(SOcket MAX CONNection)作为 `listen()` 的最后一个参数传递。如果套接字运行正常,一切都会顺利,当收到连接请求时,`listen()` 将返回。如果您希望建立连接,这就是调用 `accept()` 的信号。
#include <windows.h>
#include <winsock.h>
SOCKET s;
WSADATA w;
//LISTENONPORT – Listens on a specified port for incoming connections
//or data
int ListenOnPort(int portno)
{
int error = WSAStartup (0x0202, &w); // Fill in WSA info
if (error)
{
return false; //For some reason we couldn't start Winsock
}
if (w.wVersion != 0x0202) //Wrong Winsock version?
{
WSACleanup ();
return false;
}
SOCKADDR_IN addr; // The address structure for a TCP socket
addr.sin_family = AF_INET; // Address family
addr.sin_port = htons (portno); // Assign port to this socket
//Accept a connection from any IP using INADDR_ANY
//You could pass inet_addr("0.0.0.0") instead to accomplish the
//same thing. If you want only to watch for a connection from a
//specific IP, specify that //instead.
addr.sin_addr.s_addr = htonl (INADDR_ANY);
s = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP); // Create socket
if (s == INVALID_SOCKET)
{
return false; //Don't continue if we couldn't create a //socket!!
}
if (bind(s, (LPSOCKADDR)&addr, sizeof(addr)) == SOCKET_ERROR)
{
//We couldn't bind (this will happen if you try to bind to the same
//socket more than once)
return false;
}
//Now we can start listening (allowing as many connections as possible to
//be made at the same time using SOMAXCONN). You could specify any
//integer value equal to or lesser than SOMAXCONN instead for custom
//purposes). The function will not //return until a connection request is
//made
listen(s, SOMAXCONN);
//Don't forget to clean up with CloseConnection()!
}
如果您编译并运行此代码,如前所述,您的程序将冻结直到收到连接请求。例如,您可以通过尝试“`telnet`”连接来引起此连接请求。连接最终会失败,当然,因为连接不会被接受,但您会触发 `listen()` 返回,您的程序将从死亡的国度复活。您可以通过在 MSDOS 命令提示符下键入 telnet 127.0.0.1 “`port_number`”(将“`port_number`”替换为您的服务器正在侦听的端口)来尝试此操作。
异步套接字
由于使用 `listen()` 等阻塞函数非常不切实际且令人头疼,让我们在继续之前讨论一下“异步”套接字。我之前提到过它们,并答应过您会向您展示它们是如何工作的。
C++ 在这方面为我们提供了一个大多数高级编程语言所没有的优势;即,我们不必花费额外的精力“子类化”父窗口就可以使用异步套接字。它已经为我们做好了,所以我们实际上要做的就是将处理代码添加到消息处理程序中。这是因为异步套接字,正如您将看到的,依赖于在收到连接请求、接收到数据等时向您的程序发送消息的能力。这使得它可以在后台静默等待,而不会干扰父程序或影响生产力,因为它只在必要时进行通信。代价也相对较小,因为它确实不需要太多额外的编码。理解它是如何工作的可能需要一些时间,但您一定会为花时间理解异步套接字而感到高兴。长远来看,这将为您节省很多麻烦。
与重做和修改我们到目前为止编写的所有代码不同,使套接字异步只需在 `listen()` 函数后添加一行代码。当然,您的消息处理程序需要准备好接收以下消息
FD_ACCEPT
:如果您的应用程序充当客户端(即,您正在尝试使用 `connect()` 连接到远程主机),则在收到连接请求时会收到此消息。如果您选择这样做,将发送以下消息FD_CONNECT
:表示连接已成功建立FD_READ
:我们从远程计算机收到了传入数据。稍后我们将学习如何处理它。FD_CLOSE
:远程主机已断开连接,因此我们失去了连接。
这些值将发送到您消息处理程序的 `lParam` 参数。我将在几分钟内向您展示确切的放置位置;但首先,我们需要理解用于将我们的套接字设置为异步模式的 API 参数
//Switch the socket to a non-blocking asynchronous one
int PASCAL WSAAsyncSelect(SOCKET,HWND,u_int,long);
第一个参数,显然,要求我们套接字的句柄,第二个参数需要我们父窗口的句柄。这是必要的,以便它将消息发送到正确的窗口!第三个参数,如您所见,接受一个整数值,您将为此指定一个唯一的通知编号。当任何消息发送到您程序的“消息处理程序”时,您指定的任何编号也将一并发送。因此,您应该对您的消息处理程序进行编码,使其等待识别编号,*然后*确定发送的是哪种类型的通知。我知道这很令人困惑,所以希望看一下下面的源代码能有所启发。
#define MY_MESSAGE_NOTIFICATION 1048 //Custom notification message
//This is our message handler/window procedure
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message) //handle the messages
{
case MY_MESSAGE_NOTIFICATION: //Is a message being sent?
{
switch (lParam) //If so, which one is it?
{
case FD_ACCEPT:
//Connection request was made
break;
case FD_CONNECT:
//Connection was made successfully
break;
case FD_READ:
//Incoming data; get ready to receive
break;
case FD_CLOSE:
//Lost the connection
break;
}
}
break;
//Other normal window messages here…
default: //The message doesn't concern us
return DefWindowProc(hwnd, message, wParam, lParam);
}
break;
}
这并不算太糟糕,对吧?现在我们的处理程序已准备就绪,我们应该将以下代码行添加到 `ListenOnPort()` 函数中,在 `listen()` 之后
//The socket has been created
//IP address has been bound
//Function listen() has just been called…
//Set the socket to non-blocking asynchronous mode
//hwnd is a valid handle to the program's parent window
//Make sure you OR all the needed flags
WSAAsyncSelect (s, hwnd, MY_MESSAGE_NOTIFICATION, (FD_ACCEPT | FD_CONNECT |
FD_READ | FD_CLOSE);
//And so forth…
C:\Documents and Settings\Cam>netstat -an
活动连接
协议 | 本地地址 | 远程地址 | 状态 |
TCP | 0.0.0.0:135 | 0.0.0.0:0 | 侦听中 |
TCP | 0.0.0.0:445 | 0.0.0.0:0 | 侦听中 |
TCP | 0.0.0.0:5225 | 0.0.0.0:0 | 侦听中 |
TCP | 0.0.0.0:5226 | 0.0.0.0:0 | 侦听中 |
TCP | 0.0.0.0:8008 | 0.0.0.0:0 | 侦听中 |
TCP | 127.0.0.1:1025 | 0.0.0.0:0 | 侦听中 |
TCP | 127.0.0.1:1035 | 127.0.0.1:5226 | 已建立 |
TCP | 127.0.0.1:5226 | 127.0.0.1:1035 | 已建立 |
TCP | 127.0.0.1:8005 | 0.0.0.0:0 | 侦听中 |
UDP | 0.0.0.0:445 | *:* | |
UDP | 0.0.0.0:500 | *:* | |
UDP | 0.0.0.0:4500 | *:* | |
UDP | 127.0.0.1:123 | *:* | |
UDP | 127.0.0.1:1031 | *:* | |
UDP | 127.0.0.1:1032 | *:* | |
UDP | 127.0.0.1:1900 | *:* |
C:\Documents and Settings\Cam>
如果您的服务器工作正常,您应该在“**本地地址**”下看到类似“`0.0.0.0:Port#,`”的内容,其中 `Port#` 是您正在侦听的端口,处于`LISTENING`状态。顺便说一句,如果您忘记使用 `htons()` 转换端口号,您可能会发现一个新的端口已打开,但它将位于一个与您预期的端口完全不同的端口上。
如果需要几次尝试才能让一切正常工作,请不要担心;我们都会遇到这种情况。您会成功的。(当然,如果您尝试了几周都失败了,那就烧掉这份报告,忘了是谁写的吧!)
发送和接收数据
到目前为止,您的服务器就像一个又聋又哑的人!这毫不奇怪,在现实世界中对您并没有多大用处。所以,让我们看看如何与任何决定与我们聊天的计算机进行有效且恰当的通信。一如既往,当我们不知所措时,一些 API 调用就会派上用场。
//Send text data to a remote computer
int PASCAL send(SOCKET,const char*,int,int);
//Receive incoming text from a remote computer
int PASCAL recv(SOCKET,char*,int,int);
//Advanced functions that allow you to communicate exclusively with a
//certain computer when multiple computers are connected to the same server
int PASCAL sendto(SOCKET,const char*,int,int,const struct sockaddr*,int);
int PASCAL recvfrom(SOCKET,char*,int,int,struct sockaddr*,int*);
如果您*不*使用异步服务器,那么您必须将 `recv()` 函数放在一个计时器函数中,该函数不断检查传入数据——这至少算不上优雅的解决方案。另一方面,如果您做得很明智,并设置了一个异步服务器,那么您所要做的就是在您的消息处理程序中的 `FD_READ` 中放入您的 `recv()` 代码。当有传入数据时,您将收到通知。还能比这更简单吗!
当我们检测到活动时,必须创建一个缓冲区来存储它,然后将指向缓冲区的指针传递给 `recv()`。函数返回后,文本应该已经恭敬地放入我们的缓冲区中,迫不及待地等待显示。请查看源代码。
case FD_READ:
{
char buffer[80];
memset(buffer, 0, sizeof(buffer)); //Clear the buffer
//Put the incoming text into our buffer
recv (s, buffer, sizeof(buffer)-1, 0);
//Do something smart with the text in buffer
//You could display it in a textbox, or use:
//MessageBox(hwnd, Buffer, "Captured Text…", MB_OK);
}
break;
既然您已经能够从远程计算机或服务器接收传入文本,那么我们的服务器还缺少什么呢?那就是回复,或者将数据“发送”到远程计算机的能力。这可能是 Winsock 编程中最简单、最显而易见的过程,但如果您像我一样喜欢每个步骤都得到详细说明,那么这里是如何正确使用 `send()` 的方法。
char *szpText;
//Allocate memory for the text in your Text Edit, retrieve the text,
//(see the source code for this) and then pass a pointer to it…
send(s, szpText, len_of_text, 0);
为了简洁起见,上面的代码片段只是一个骨架,为您提供 `send()` 用法的大致思路。要查看完整代码,请下载本教程附带的示例源代码。
在更高级的方面,有时简单的 `send()` 和 `receive()` 函数不足以满足您的需求。当您同时处理来自不同源的多个连接时(还记得我们调用 `listen()` 时,传递了 `SOMAXCONN` 以允许最大数量的连接吗),您需要将数据发送到*一个*特定的计算机,而不是*所有*计算机。如果您非常聪明,您可能会注意到 `send()` 和 `receive()` 下面有两个额外的 API(如果您注意到了,这是加分项!);`sendto()` 和 `receivefrom()`。
这两个 API 允许您与任何一个远程计算机通信,而不会让连接到您计算机的所有其他人都知道。在这些高级函数中,有一个额外的参数接受指向 `sockaddr_in` 结构的指针,您可以使用它来指定您希望专门与之通信的任何远程计算机的 IP 地址。如果您正在构建一个功能齐全的聊天程序或类似的程序,这是一项重要的技能,但除了向您介绍这些函数如何工作的基础知识之外,我将让您自己弄清楚。(您是否讨厌作者这样说?通常,这是因为我们自己也没有最起码的线索……但说真的,如果您决定需要它们,实现它们应该不会花费太多精力。)
一些最后的说明
好吧,到目前为止,您应该对 Windows 套接字有了相当的了解——或者对其深恶痛绝——但无论如何,如果您正在寻找比我能在此提供的更好的解释,请查看本文提供的示例源代码。练习对您的帮助将远远大于阅读任何文章。
此外,我发现如果您尝试复制粘贴代码,或者只是编译您在互联网上找到的别人的代码,您将无法获得通过自己动手输入所有示例所能获得的理解程度。我知道这很麻烦!但如果您花时间去做,长远来看您将省去很多麻烦。
玩得开心,请在发帖反馈中告诉我您对本文的看法。
本文(不包括附带的源代码)版权归作者所有 © 2006 年,未经作者事先明确许可,不得为个人牟利而修改、出售和重新分发。它免费提供,旨在造福公众。但是,您可以在不以任何方式修改原始内容的前提下,随意制作和分发副本。谢谢!