3 新建项目
4.8 QQ 聊天程序的网络通信原理及编程
蒋 智 随着计算机网络的普及,网上聊天已成了和电话一
样重要且更经济的一种通信方式。QQ 是人们网上聊天 的首选,在许多人的联系方式中,QQ 号是其中之一。
若想自己也设计一个类似 QQ 的聊天程序,这也许是一 件很难的事情,但其实并不如你所想象的复杂,可以尝 试一下。
1 原理
QQ 聊天程序采用的是 C/S 通信模式,即客户/服务 模式,它把一个应用划分成功能不同的两个部分,分别 在不同的计算机上运行,其中一个为服务端程序,用来 响应和提供固定的服务,一个为客户端程序,用来向服 务端提出请求和要求某种服务。在数量关系上,通常有 一对一的(即一个服务端程序和一个客户端程序之间通 信),也有一对多的(即一个服务端程序和多个客户端程 序之间通信),也有多对多的(即多个服务端程序和多个 客户端程序之间通信)。所谓服务端程序、客户端程序也 是相对的概念,有时在一个程序中既有服务端又有客户 端的功能。QQ 聊天程序分成了两个程序,一个安装在腾 讯公司的服务器上,称之为服务端程序,一个安装在 QQ 用户的计算机上,称之为客户端程序。在许多介绍网络 通信编程的书籍中有关网络聊天的例子,当一个客户要 与另一个客户聊天时,第一个客户先把聊天数据发送给 服务器,然后服务器再把聊天数据转发给第二个客户,
服务器好像一个中转站,这在客户数量比较少时,服务 器还能承受,在客户数量比较多时,服务器肯定要瘫痪。
何况客户之间还要传送文件、语音聊天、视频聊天等等,
为了减少服务器的压力,各客户端之间需要直接通信。
1.1 在服务端和客户端之间(一对多)
用于客户端程序登录,验证用户密码,获取其他在 线好友信息等。
1.2 在客户端和客户端之间(多对多)
用于在线好友间直接通信聊天。此时每个客户端程序 上既有实现服务端功能的部分,又有实现客户端功能的部 分,前者用于接收聊天数据,后者用于发送聊天数据。
在用 C/S 模式进行通信时,作为客户端在请求与服 务端连接时需要知道服务端的 IP 地址,腾讯公司的服 务器具有固定的公网地址,这没有问题。但是在客户端 和客户端之间通信时,每个用户的 IP 地址都是 ISP 所给 的临时地址,无法固定,那么某个用户要与另一个用户 连接时,怎么知道对方的 IP 地址呢?当一个用户连接 到 Internet 后,获得了一个临时的公网地址,当登录到 QQ 时,QQ 服务端会获得该上线用户的 IP 地址,然后 告知其他要与该上线用户聊天的用户,其他用户就可以 连接该用户与之直接通信了。
在 Win32 平台上,对于众多的基层网络协议,
Winsock 是访问它们的首选接口。如果想从头开发一个 网络通信应用程序,TCP/IP 就是首选协议之一。为了保 证可靠的数据传输,宜选择 TCP 协议。在开发工具方面,
VC 是我的首选,但 VB 可能拥有更多的使用者,所以 用 VB 6.0 作为开发工具编程实现,以便更多的读者能 够理解。喜欢 VC 的读者要将其转化成 VC 下的代码也 很容易。在 VB 中有一个 Winsock 控件,它为 VB 网络 编程提供了一条便捷的途径。
本程序中用到的 Winsock 控件如表 1 所示。
表 1
控件名 是否为数组 作用 在哪个程序中
WinsockServer 是 用于和客户端通信 服务端程序
WinsockClient 否 用于和服务端通信 客户端程序
WinsockClientServer 是 用于接收在线好友的聊天信息 客户端程序 WinsockClientClient 是 用于向在线好友发送聊天信息 客户端程序
本程序中所涉及的一些其他功能如数据库访问等的 实现,限于篇幅不进行阐述,有兴趣的读者可以参考源 程序。为了阐述方便,本程序中服务端只用了一个窗体,
客户端只用了两个窗体,分别是登录窗体和聊天窗体。
接下来按照 QQ 聊天程序的所需实现的功能一步步 编程实现,其中服务端程序和客户端程序没有分开阐 述,而是按照所应实现功能的先后次序一并阐述,便于 读者理解及按此顺序编程调试。
2 编程
2.1 登录
使用 QQ 的人都知道,用 QQ 聊天第一步是登录,
这一步看似简单,其实程序做了不少事情。当用户启动 QQ 聊天客户端程序,出现登录界面,用户输入用户名和 密码,单击“登录”按钮后,在用户机上的客户端程序 首先请求与服务端程序建立连接,服务端程序接受客户 端程序连接请求。客户端程序在确保已和服务端程序建 立连接后发送登录时用户输入的用户名和密码,服务端 程序收到后,根据用户信息数据库中数据验证。如果验 证为合法用户,客户端程序则显示聊天窗体,并从服务 端获取在线好友信息,以便直接与各在线好友逐一建立 新的通信机制,实现好友间直接通信。具体实现步骤如下:
2.1.1 监听
先 在 窗 体 上 加 一 个 Winsock 控 件 , 名 称 设 为
“WinsockServer”。由于服务端需要和所有客户端通信,
需要很多 Winsock 控件,因此把刚加入的 Winsock 控件 WinsockServer 改为数组,只要将 Winsock 控件的 Index 属性设为 0 即可,此时 WinsockServer 数组只有一个元 素,以后可根据需要动态增减。当窗体载入时,只会创 建 WinsockServer(0)这一个 Winsock 控件实例。作为服 务端程序,首先要有一个 Winsock 用于监听客户端的连 接请求,自然就用 WinsockServer(0)作为监听 Winsock,
所以先要设置其通信协议为 TCP,然后设置本地端口,
以便客户端通过该端口与服务端连接,最后通过 Listen 方法监听。
Private Sub Form_Load()
ConDB '自定义函数 ConDB 用于和存储用户信息的数 '据库连接,具体实现见源程序。
WinsockServer(0).Protocol = sckTCPProtocol '设置通信协议为 TCP
WinsockServer(0).LocalPort = 8888 '设置本地端口 WinsockServer(0).Listen '监听
End Sub
2.1.2 请求与服务端程序建立连接
当用户输入用户名和密码单击“登录”按钮后,在
用户机上的客户端程序所做的事情是先请求与服务端 程序建立连接,这需要先在客户端程序的登录窗体上加 一个 Winsock 控件,名称设为“WinsockClient”。然后 设置通信协议,要连接的服务器 IP 地址,远程端口号 要与服务端监听所设置的本地端口一致,最后通过 Connect 方法请求与服务端程序建立连接。
Private Sub Command1_Click() '单击登录按钮事件 WinsockClient.Protocol = sckTCPProtocol '设置通信协议为 TCP
WinsockClient.RemoteHost = "127.0.0.1"
'要设置正确的服务器 IP 地址
WinsockClient.RemotePort = 8888 '远程端口 WinsockClient.Connect '请求与服务端程序建立连接 End Sub
2.1.3 接受客户端程序连接请求
当客户端请求与服务端程序建立连接时,处于监听 状态的服务端会收到消息触发 ConnectionRequest 事件,
所以服务端程序接受客户端程序连接请求的工作在 ConnectionRequest 事件方法中完成。由于 WinsockServer(0) 专用于监听,所以要新加载一个 Winsock 控件与该客户 端通信,为了保留每个在线客户的信息,我定义了一个 OnlineUserInformation 结构类型的数组 OnlineUserInfo,
它和 Winsock 控件数组相对应,比如 OnlineUserInfo(i) 中存放的是与 WinsockServer (i)通信客户端的信息,其 中 OnlineUserInfo(i).bUsed 表示 WinsockServer (i)控件是 否已被载入在与某个客户端通信使用,如未使用,则通 过 Load 方法将 WinsockServer (i)控件动态载入,随后调 用 Accept 方 法 接 受 客 户 端 程 序 连 接 请 求 。 其 中 MaxOnlineUser 是一个常量,表示最大在线用户数量。
Type OnlineUserInformation bUsed As Boolean
'对应下标的 Winsock 控件是否已被载入在用 UserID As String '对应的用户 ID IPAddr As String '对应的客户机 IP 地址 bLogined As Boolean '对应的用户是否登录 End Type
Public OnlineUserInfo(1 To MaxOnlineUser) As OnlineUserInformation
Private Sub WinsockServer_ConnectionRequest(Index As Integer, ByVal requestID As Long)
If Index = 0 Then
For i = 1 To MaxOnlineUser
If Not OnlineUserInfo(i).bUsed Then Load WinsockServer(i)
WinsockServer(i).Accept requestID OnlineUserInfo(i).bUsed = True
Exit For End If Next i End If End Sub
2.1.4 发送登录的用户名和密码
Public Const OnlineFriendIPID = "FriendIPID" '表示本数据 中包含在线好友的 IP 地址和 ID
Public Const Login = "User Login"
'表示本数据中包含登录用户的用户 ID 和密码
Public Const HeadLength = 10
关于分割标记符和结束标记符,我采用数值 1 和 2
Private Sub WinsockClient_Connect()
WinsockClient.SendData Login & TextUsername.Text &
Chr(1) & TextPassword.Text & Chr(2) End Sub
2.1.5 接收收据
当有数据到达时,程序会收到消息触发 DataArrival 事件,所以不论服务端还是客户端程序接收数据的工作 在 DataArrival 事件方法中完成。具体接收数据使用 GetData 方法进行,为了防止客户端发送来的数据太多,
服务端来不及处理,定义了一个字符串数组 BufferRecv,
该数组与 WinsockServer 控件数组对应,WinsockServer (Index) 控件收到的数据添加在 BufferRecv(Index)中,然 后通过结束标记符在 BufferRecv(Index)中取出一条条数
Private Sub WinsockServer_DataArrival(Index As Integer, ByVal bytesTotal As Long)
Dim tmpstr As String
Dim EndFlagLoc As Integer '结束标记所处位置变量 If Index = 0 Then Exit Sub
WinsockServer(Index).GetData tmpstr, , bytesTotal '接收收据存入 tmpstr 中
BufferRecv(Index) = BufferRecv(Index) & tmpstr '将收到的数据添加在 BufferRecv(Index)中 EndFlagLoc = InStr(BufferRecv(Index), Chr(2)) '在 BufferRecv(Index)中寻找第一个结束标记符的位置 While EndFlagLoc > 0
'在 BufferRecv(Index)中取第一个结束标记符左边的数据,
'即第一条数据
strMsg = Left$(BufferRecv(Index), EndFlagLoc - 1) '获取本条数据前面的数据含义标记字符串以分析本条数
If GetFriend(tmpUserID, AllFriendID, FriendCount) Then '然后根据在线用户信息数组用 InStr 函数逐一判断在线用 '户是否是该用户的好友
For i = 1 To MaxOnlineUser If OnlineUserInfo(i).bLogined Then
If InStr(AllFriendID, OnlineUserInfo(i).UserID) > 0 Then '如果是,则将在线好友的 ID 及 IP 地址发送给刚登录的 '客户。发送数据的格式为:数据含义标记字符串+IP 地址
'+分割标记符+用户 ID+结束标记符
If WinsockServer(Index).State = sckConnected Then WinsockServer(Index).SendData OnlineFriendIPID &
OnlineUserInfo(i).IPAddr & Chr(1) &
If WinsockServer(i).State = sckConnected Then WinsockServer(i).SendData OnlineFriendIPID &
OnlineUserInfo(Index).IPAddr & Chr(1) &
OnlineUserInfo(Index).UserID & Chr(2) DoEvents
End If
ToldFriendCount = ToldFriendCount + 1 End If
End If
If ToldFriendCount = FriendCount Then Exit For Next i
End If
'如果验证为非法用户,则发送含义为“不是合法用户”
'的数据标记,并将与该客户通信用的 Winsock 控件卸载 Else
WinsockServer(Index).SendData NotUser & Chr(2) DoEvents
OnlineUserInfo(Index).bUsed = False WinsockServer(Index).Close Unload WinsockServer(Index) End If
'然后从 BufferRecv(Index)中删除已分析的这条数据,继续 '分析下一条数据
BufferRecv(Index) = Mid(BufferRecv(Index), EndFlagLoc + 1) 后通过自定义过程 AddConnectOnlineFriend 将该在线好 友信息保存,并与该在线好友建立连接。
Private Sub WinsockClient_DataArrival(ByVal bytesTotal As Long)
Dim tmpstr As String Dim EndFlagLoc As Integer
WinsockClient.GetData tmpstr, , bytesTotal BufferCmd = BufferCmd & tmpstr EndFlagLoc = InStr(BufferCmd, Chr(2)) While EndFlagLoc > 0
strMsg = Left$(BufferCmd, EndFlagLoc - 1) Select Case Left$(strMsg, HeadLength) Case NotUser
自定义过程 AddConnectOnlineFriend 定义在聊天窗 体中,首先将服务端发来的在线好友的 IP 地址和 ID 等
自定义过程 AddConnectOnlineFriend 定义在聊天窗 体中,首先将服务端发来的在线好友的 IP 地址和 ID 等