Winsock 编程入门 - 带客户端的多线程 TCP 服务器






4.90/5 (64投票s)
2002年3月6日
7分钟阅读

625027

28264
介绍多线程 TCP 文件服务器、自定义 TCP 聊天协议和自定义 TCP 客户端
引言
在本系列之前的文章中,我们学习了编写一个简单的 TCP 服务器,该服务器一次只能接受一个连接;还学习了编写一个简单的 TCP 客户端,该客户端可以通过 HTTP 下载文件。但对于大多数人来说,显而易见的是,服务器程序肯定需要同时处理多个客户端连接。否则,如果一个客户端当前已连接,其他客户端将觉得该服务器没什么用。
在本文中,我们将编写一个多线程 TCP 服务器。此外,我们还将创建自己的自定义 TCP 聊天协议,尽管它非常简单。然后,我们还将编写一个客户端,该客户端将连接到此服务器并使用此协议进行聊天。您可以尝试同时运行客户端的多个实例,以测试服务器是否真的可以接受多个连接。服务器只是将请求的文件发送给客户端,然后客户端将其保存在客户端计算机上。
此处用于处理多个客户端的方法是经典的“每个客户端连接一个线程”的方法。还有其他更有效的处理多个连接的机制,例如 IO 完成端口。但是,如果聊天协议很简单,并且每个客户端连接的处理器负载不高,那么这是一种相当合理的方法,除非同时连接的客户端数量异常大。
编写多线程服务器
那些读过我关于编写简单 TCP 服务器的文章的人可能知道如何创建 TCP 服务器。其他人最好先阅读那篇文章。其他人可以继续。我们的 `main()` 函数与之前没有太大区别。我们启动服务器线程,然后无限循环等待 `_getch()`,直到有人按下 ESC 键,然后我们关闭服务器套接字并退出。
int _tmain(int argc, TCHAR* argv[], TCHAR* envp[]) { int nRetCode = 0; cout << "Press ESCAPE to terminate program\r\n"; AfxBeginThread(MTServerThread,0); while(_getch()!=27); closesocket(server); WSACleanup(); return nRetCode; }
现在让我们看看服务器线程。直到我们调用 `listen()` 的部分,代码本质上是相同的。在 `accept()` 部分,我们做了一些小的更改。
UINT MTServerThread(LPVOID pParam) { /* Code has been snipped off. Refer zipped source for actual code */ if(listen(server,10)!=0) { return 0; } SOCKET client; sockaddr_in from; int fromlen=sizeof(from); while(true) { client=accept(server, (struct sockaddr*)&from,&fromlen); AfxBeginThread(ClientThread,(LPVOID)client); } return 0; }
所发生的一切都很简单。我们接受一个连接,一旦接受,我们就启动一个新线程,并将客户端 `SOCKET` 传递给该线程。然后我们返回到 `accept()`。因此,一旦客户端连接,就会启动一个新线程来处理该客户端,从而使下一个客户端也能连接,并启动又一个线程,以此类推。天哪!而且,有些人实际上认为这会比这更难,对吧?
我们自己的自定义协议
让我们定义我们的自定义 TCP 聊天协议中允许的命令。我们显然需要一个用于关闭会话的命令。“QUIT”怎么样?这似乎是一个不错的、显而易见的单词。现在我们不希望任何人都能下载文件。因此,让我们添加一个“AUTH”命令,它带有一个指定密码的参数。如果密码正确,我们将用户置于授权状态。AUTH 和 QUIT 都可以在未授权状态下使用。但是“FILE”命令用于检索文件,只允许在授权状态下使用。好了,这是三个命令,其中两个随时可用,一个只允许在授权状态下使用。
QUIT
:- 这将关闭连接AUTH [密码]
:- 这会将用户登录到授权模式FILE [带完整路径的文件名]
:- 检索文件 [仅在授权模式下有效]
想象一下!我们自己的可爱小协议。让我们使用 # 来表示成功消息,使用 ! 来表示错误消息。在我实际向您展示代码实现之前,我现在将向您展示一个简单的 TCP 聊天会话到我们的服务器会是什么样子。
Trying 192.168.1.44...
Connected to 192.168.1.44.
Escape character is '^]'.
#Server Ready.
file c:\config.sys
!You are not logged in.
auth yellow
!Bad password.
auth passwd
#You are logged in.
file c:\config.sys
DEVICE=C:\WINDOWS\HIMEM.SYS
DEVICE=C:\WINDOWS\EMM386.EXE
#File c:\config.sys sent successfully.
file c:\setup.log
[InstallShield Silent]
Version=v6.00.000
File=Log File
[ResponseResult]
ResultCode=0
[Application]
Name=Intel Ultra ATA Storage Driver
Version=6.03.007
Company=Intel
Lang=0009
#File c:\setup.log sent successfully.
file d:\g5.doc
!File d:\g5.doc could not be opened.
quit
Connection closed by foreign host.
如您所见,一旦用户登录,他就可以请求任意数量的文件。我这里只显示了文本文件,但他也可能要求二进制文件。在我们稍后编写的客户端程序中,我们也可以这样做。现在让我们看看客户端线程如何处理此聊天协议。
UINT ClientThread(LPVOID pParam) { char buff[512]; CString cmd; CString params; int n; int x; BOOL auth=false; SOCKET client=(SOCKET)pParam; strcpy(buff,"#Server Ready.\r\n"); send(client,buff,strlen(buff),0); while(true) { n=recv(client,buff,512,0); if(n==SOCKET_ERROR ) break; buff[n]=0; if(ParseCmd(buff,cmd,params)) { if(cmd=="QUIT") break; if(cmd=="AUTH") { if(params=="passwd") { auth=true; strcpy(buff,"#You are logged in.\r\n"); } else { strcpy(buff,"!Bad password.\r\n"); } send(client,buff,strlen(buff),0); } if(cmd=="FILE") { if(auth) { if(SendFile(client,params)) sprintf(buff, "#File %s sent successfully.\r\n", params); else sprintf(buff, "!File %s could not be opened.\r\n", params); x = send(client, buff, strlen(buff),0); } else { strcpy(buff,"!You are not logged in.\r\n"); send(client,buff,strlen(buff),0); } } } else { strcpy(buff,"!Invalid command.\r\n"); send(client,buff,strlen(buff),0); } } closesocket(client); return 0; }
希望代码是自解释的。我将简要介绍一下。我们首先像 TCP 服务器一样发送服务器问候。现在我们循环并接受命令。我们使用 `ParseCmd` 函数将输入的命令字符串解析为两个 `CString` 对象,一个包含命令,另一个包含任何参数。如果 `ParseCmd` 返回 `true`,则表示发送了未知命令,我们会返回错误消息。
如果命令是 QUIT,我们则跳出 while 循环,关闭客户端套接字并退出线程。我们还有一个名为 auth 的布尔标志,只有在客户端授权后,我们才将其设置为 true。在此之前,如果我们收到 FILE 命令,我们会发送一条消息,说明客户端未登录。目前,我将“passwd”硬编码为我们的密码,但在实际情况下,用户名/密码将从数据库或配置文件中获取。
使用 AUTH 命令,用户可以登录。我们将其与密码进行比较,如果匹配,我们会发送一条消息告诉他们已登录并将 auth 标志设置为 true;否则,我们会给他们一个登录失败的错误消息。一旦他们获得授权,他们就可以请求文件。我们使用一个名为 `SendFile` 的函数通过 TCP 连接发送文件。我希望事情现在都清楚了。
现在让我们看看 `ParseCmd` 函数
BOOL ParseCmd(char *str, CString& cmd, CString& params) { int n; CString tmp=str; tmp.TrimLeft(); tmp.TrimRight(); if((n=tmp.Find(' '))==-1) { tmp.MakeUpper(); if(tmp!="QUIT") return false; cmd=tmp; return true; } cmd=tmp.Left(n); params=tmp.Mid(n+1); cmd.MakeUpper(); if((cmd!="AUTH") && (cmd!="FILE")) return false; return true; }
嗯,如您所见,该函数非常直观。它拆分字符串,如果遇到有效命令则返回 `true`。否则,它返回 `false`,表示错误。
现在让我们看看 `SendFile` 函数。
BOOL SendFile(SOCKET s, CString fname) { CFile f; BOOL p=f.Open(fname,CFile::modeRead); char buff[1024]; int y; int x; if(!p) return false; while(true) { y=f.Read(buff,1024); x=send(s,buff,y,0); if(y<1024) { f.Close(); break; } } return true; }
这也很简单。如果文件发送成功,则返回 `true`;如果文件未找到,则返回 `false`。有点令人困惑,不是吗。我在 `ParseCmd` 中使用 `true` 来表示错误,而在这里我使用 `false` 来表示错误。我想在编码标准方面我还有很长的路要走。我希望各位女士先生们能原谅我性格中的这一严重缺陷。
好了,现在我们已经编写了带有自定义聊天协议的多线程 TCP 服务器。我想这很好。但是,我敢打赌,有些人现在可能在想,也许编写一个漂亮的客户端程序来连接服务器,并使用我们的协议与其聊天,并检索一些文件会很好。嗯。巧合的是,我也有同样的想法。因此,我们将继续编写一个客户端程序。
自定义客户端
这次我不会详细介绍源代码。那些阅读过我简单 TCP 客户端文章的人,在下载和理解源代码方面不会有任何问题。只有几点。请勿对 `recv()` 返回的缓冲区使用 `strcpy()` 和 `strchr()` 等字符串操作函数。这些字节缓冲区可能不是空终止的,调用 `strcpy()` 等函数将直接破坏您的程序。您可以打开源代码并查看我在缓冲区中搜索特定字符集的两个函数。我完全没有使用字符串操作函数。
这是使用自定义客户端的方法。顺便说一句,我在客户端源代码中硬编码了 127.0.0.01 作为服务器 IP。如果您在不同的机器上运行服务器和客户端,您可能需要更改它。运行前请确保服务器已启动并正在运行。
E:\work\MTSClient\Debug>mtsclient
Usage :- mtsclient [file1] [file2] [file3] ....
E:\work\MTSClient\Debug>mtsclient c:\cu.gif c:\cp.gif c:\g.gif c:\ddd
File c:\cu.gif not found on server
cp.gif has been saved.
g.gif has been saved.
File c:\ddd not found on server
E:\work\MTSClient\Debug>
结论
这可能是我的 TCP 编程入门系列(共 3 部分)的最后一篇文章。到目前为止,您应该能够编写基本的 TCP 客户端和 TCP 服务器 [多线程]。我建议您尝试编写一个连接到 POP 服务器、登录并检查是否有邮件的简单程序。或者,您可以尝试编写一个使用 SMTP 聊天发送电子邮件的小程序。要测试您的服务器编码技能,您可以尝试编写一个简单的 HTTP 服务器。或者,构思一些内容并编写您自己的自定义服务器和自定义协议。祝您玩得开心……