第三步功能实现了,第四步功能就是实现悄悄话的功能。
这个功能比较复杂,因此分两部分实现,第一部分实现用户登录退出时列表框中能同步显示当前用户,第二部分再实现点击列表框中某个用户时实现悄悄话功能
第一部分
1. 在Message.inc中,定义一个SEND_ONLINE_USER结构体,里面包含了当前登录的的所有用户名称,用户个数等信息。
①
;************************************************************** ;canmeng萌4
; 发送所有用户名字语句(服务器端->客户端):不等长数据包 ;canmeng萌4
;*********************************************************** ;canmeng萌4
MSG_ONLINE_USER struct ;canmeng萌4
sznumber dd ? ;当前在线用户个数 ;canmeng萌4
dwlength dd ? ;后面内容字段的长度 ;canmeng萌4
szcontent db 48 dup (?) ;所有用户的名字,最多可容纳4个用户 ;canmeng萌4
MSG_ONLINE_USER ends ;canmeng萌4
②
增加用于这个数据包的数据包头命令ID
CMD_ONLINE_USER equ 85h ; 服务器端 -> 客户端, canmeng萌4
③
在MSG_STRUCT结构体中也增加一个MSG_ONLINE_USER类型的字段
MSG_STRUCT struct
MsgHead MSG_HEAD <>
union
Login MSG_LOGIN <>
LoginResp MSG_LOGIN_RESP <>
MsgUp MSG_UP <>
MsgUpResp MSG_UP_RESP <> ;canmeng萌2
MsgDown MSG_DOWN <>
MsgOnlineUser MSG_ONLINE_USER <> ;canmeng萌4
ends
MSG_STRUCT ends
2. 在Server.asm中,增加一个子程序,实现的功能是把所有在线的用户的名字发送到本线程对应的客户端(实际上时发送MSG_ONLINE_USER数据包)。
① 这个子程序我就添加在_LinkCheck子程序之后了。
;************************************************************** ;canmeng萌4
; 把在线的用户名字发给在线的客户端 ;canmeng萌4
;************************************************************** ;canmeng萌4
_SendOnlineUser proc uses esi edi _hSocket
pushad ;canmeng萌4
invoke WaitForSingleObject,hEvent,INFINITE ;canmeng萌4,等待事件对象置位
mov [esi].MsgHead.dwCmdId,CMD_ONLINE_USER ;命令类型
mov ecx,dwThreadCounter ;canmeng萌4
mov eax,12 ;canmeng萌4
mul ecx ;canmeng萌4
inc eax ;canmeng萌4
mov ecx,eax ;;canmeng萌4,如果有两个线程,此时ecx应该为25
push ecx
mov edx,offset szusernames ;canmeng萌4
lea ebx,[esi].MsgOnlineUser.szcontent ;canmeng萌4
.while TRUE ;canmeng萌4
sub ecx,1 ;canmeng萌4
mov al,[edx] ;canmeng萌4
mov [ebx],al ;canmeng萌4
add edx,1 ;canmeng萌4
add ebx,1 ;canmeng萌4
.break .if ecx==0 ;canmeng萌4
.endw ;canmeng萌4 所有用户名称
pop eax
mov [esi].MsgOnlineUser.dwlength,eax ;所有用户名称的长度
add eax,sizeof MSG_HEAD+MSG_ONLINE_USER.szcontent ;canmeng萌4
mov [esi].MsgHead.dwLength,eax ;总长度
mov eax,dwThreadCounter ;canmeng萌4
mov [esi].MsgOnlineUser.sznumber,eax ;用户个数
invoke send,_hSocket,esi,[esi].MsgHead.dwLength,0 ;canmeng萌4
invoke SetEvent,hEvent ;canmeng萌4,置位事件对象
popad ;canmeng萌4
ret ;canmeng萌4
_SendOnlineUser endp ;canmeng萌4
注意:在填充数据包中所有用户名称即填写[esi].MsgOnlineUser.szcontent时,我是一个一个字节的填写过去的,这是为什么呢?我们知道每一个用户的名字占12个字节,以字符0作为结尾字符。如果直接用lstrcpy函数,只能拷贝一个用户名,所以要一个一个字节的传送进去。
② 在_SerciceThread这个线程中添加一个局部变量dwThreadCounter1,用来记录在线的用户人数。每当有用户登录或者退出时,全局变量dwThreadCounter就会发生变化,在线程的循环处理消息中,如果发现自己的dwThreadCounter1和全局变量dwThreadCounter不一致,那么就会把dwThreadCounter的值赋给本线程内的局部变量dwThreadCounter1并且向对应的客户端发送一个MSG_ONLINE_USER数据包(这是通过调用_SendOnlineUser子程序实现的)。
Local dwThreadCounter1
③
;****************************************************************
; 循环处理消息
;****************************************************************
.while ! (dwFlag & F_STOP)
invoke _SendMsgQueue,_hSocket,esi,edi
.break .if eax
mov ebx,dwThreadCounter ;canmeng萌4
cmp dwThreadCounter1,ebx ;canmeng萌4
jz @F ;canmeng萌4
mov ebx,dwThreadCounter ;canmeng萌4
mov dwThreadCounter1,ebx ;canmeng萌4
invoke _SendOnlineUser,_hSocket ;canmeng萌4
@@: ;canmeng萌4
invoke _LinkCheck,_hSocket,esi,edi
.break .if eax
.break .if dwFlag & F_STOP
3. 在Client.rc中进行客户端界面的修改,增加一个列表框,一个复位按钮(用于不选择用户名)等。
#define IDC_COUNT1 2008
#define IDC_LISTBOX 2009
#define IDC_RESET 2010
…………
LTEXT "当前在线人数:", -1, 245, 7, 60, 8 ;canmeng萌4
LTEXT "0", IDC_COUNT1, 300, 7, 37, 8 ;canmeng萌4
LISTBOX IDC_LISTBOX,250,22,50,120,LBS_STANDARD ;canmeng萌4
PUSHBUTTON "复位",IDC_RESET,250,150,30,14 ;canmeng萌4
;注意:在define哪一行的后面不能加注释啊,
4. 在Client1.asm中,首先把在Client.rc中定义的常量写出来。
①
IDC_COUNT1 equ 2008 ;canmeng萌4
IDC_LISTBOX equ 2009 ;canmeng萌4
IDC_RESET equ 2010 ;canmeng萌4
②定义一个全局变量,用来存储所有在线的用户名。
.data?
szUserBuffer db 48 dup (?) ;canmeng萌4,暂存所有的用户名字
③在循环接收消息中,对接收的消息头部命令ID进行判断,如果为CMD_ONLINE_USER,则说明数据包中包含所有的用户名称,于是把这些用户名称存到全局变量szUserBuffdr中,然后再一个个的显示在列表框中。
;********************************************************************
; 循环接收消息
;********************************************************************
……
.if @stMsg.MsgHead.dwCmdId == CMD_ONLINE_USER ;canmeng萌4
mov ecx,@stMsg.MsgOnlineUser.dwlength ;canmeng萌4
mov ebx,offset szUserBuffer ;canmeng萌4
lea edx,@stMsg.MsgOnlineUser.szcontent ;canmeng萌4
.while TRUE ;canmeng萌4
sub ecx,1 ;canmeng萌4
mov al,[edx] ;canmeng萌4
mov [ebx],al ;canmeng萌4
add edx,1 ;canmeng萌4
add ebx,1 ;canmeng萌4
.break .if ecx==0 ;canmeng萌4
.endw ;canmeng萌4
;注意:上面几句代码不能用lstrcpy代替
invoke SendDlgItemMessage,hWinMain,IDC_LISTBOX,LB_RESETCONTENT,0,0
mov ecx,@stMsg.MsgOnlineUser.sznumber ;canmeng萌4
.while TRUE ;canmeng萌4
sub ecx,1 ;canmeng萌4
mov eax,12 ;canmeng萌4
mul ecx ;注意,这一步会影响edx的值,非常非常重要
mov edx,offset szUserBuffer ;canmeng萌4
add edx,eax ;canmeng萌4
push edx ;canmeng萌4
push ecx ;canmeng萌4
invoke SendDlgItemMessage,hWinMain,IDC_LISTBOX,LB_ADDSTRING,0,edx ;canmeng萌4
pop ecx ;canmeng萌4
pop edx ;canmeng萌4
.break .if ecx==0 ;canmeng萌4
.endw ;canmeng萌4
Invoke SetDlgItemInt,hWinMain,IDC_COUNT1,@stMsg.MsgOnlineUser.sznumber,FALSE ;canmeng萌4
@@: ;canmeng萌4
.elseif @stMsg.MsgHead.dwCmdId == CMD_MSG_DOWN
这样,每当有用户登录或者退出时,每个客户端都能及时的显示当前的用户了。
第二部分
5. 从此步骤开始,就正式实现悄悄话的功能了。在Message.inc中,MSG_UP和MSG_DOWN结构体中都增加爱“本消息的接收对象”字段。
①MSG_UP struct
szreceiver db 12 dup (?) ; ;canmeng萌4
dwLength dd ? ;后面内容字段的长度
szContent db 256 dup (?) ;内容,不等长,长度由dwLength指定
MSG_UP ends
②MSG_DOWN struct
szTime db 20 dup (?) ;canmeng萌1
szSender db 12 dup (?) ;消息发送者
szreceiver db 12 dup (?) ;消息接收者 ;canmeng萌4
dwLength dd ? ;后面内容字段的长度
szContent db 256 dup (?) ;内容,不等长,长度由dwLength指定
MSG_DOWN ends
6. 在_MsgQueue.asm中,
①也要改变消息的结构:增加消息的接收对象
MSG_QUEUE_ITEM struct ;队列中单条消息的格式定义
dwMessageId dd ? ;消息编号
szSender db 12 dup (?) ;发送者
szreceiver db 12 dup (?) ;接收者 ;canmeng萌4
szContent db 256 dup (?) ;聊天内容
szTime db 20 dup (?) ;canmeng萌1
MSG_QUEUE_ITEM ends
②在_InsertMsgQueue中,需要增加一个参数_lpszreceiver并添加相应的语句。
_InsertMsgQueue proc _lpszSender,_lpszContent,_lpszreceiver ;canmeng萌4
……
invoke lstrcpy,addr [esi].szreceiver,_lpszreceiver ;canmeng萌4
③在_GetMsgFromQueue中,需要再增加一个参数_lpszreceiver并添加相应的语句。
_GetMsgFromQueue proc _dwMessageId,_lpszSender,_lpszContent,_lpszTime,_lpszreceiver;canmeng萌1,canmeng萌4
……
invoke lstrcpy,_lpszreceiver,addr [esi].szreceiver ;canmeng萌4
7. 在服务器端即Server.asm中,当用户发来一个登录数据包或者一个退出数据包,那么此消息的接收者当然是全部的客户端,我把此时的接收对象定义为“Allusers”。所以我在.const段中定义了一个全局变量dwreceiver。
①
.const
dwreceiver db 'Allusers',0 ;canmeng萌4
② 这样当服务器收到客户端的登录数据包时,就把“Allusers”作为消息的接收对象插入到消息队列中去。
;********************************************************************
; 广播:xxx 进入了聊天室
;********************************************************************
invoke lstrcpy,esi,addr [edi].szUserName
invoke lstrcat,esi,addr szUserLogin
invoke _InsertMsgQueue,addr szSysInfo,esi,addr dwreceiver ;canmeng萌4
③ 相似的,当服务器收到客户端的推出数据包时,也把“Allusers”作为消息的接收对象插入到消息队列中。
;********************************************************************
; 广播:xxx 退出了聊天室
;********************************************************************
invoke lstrcpy,esi,addr [edi].szUserName
invoke lstrcat,esi,addr szUserLogout
invoke _InsertMsgQueue,addr szSysInfo,addr @szBuffer,addr dwreceiver ;canmeng萌4
④ 当收到用户的聊天语句数据包时,直接把数据包中所包含的接收对象字段提取出来就行了。
;********************************************************************
; 循环处理消息
;********************************************************************
invoke _InsertMsgQueue,addr [edi].szUserName,addr [esi].MsgUp.szContent,\
addr [esi].MsgUp.szreceiver ;canmeng萌4
⑤ 另外,由于Message.inc中的_GetMsgFromQueue函数已经多了一个参数,所以也要修改_SendMsgFromQueue中的调用语句。
在_SendMsgFromQueue中,
invoke _GetMsgFromQueue,ecx,addr[esi].MsgDown.szSender,addr [esi].MsgDown.szContent,\
addr [esi].MsgDown.szTime,addr [esi].MsgDown.szreceiver ;canmeng萌1,canmeng萌4
8. 在Client1.asm中,客户端收到消息即MSG_DOWN数据包后,先进行判断。如果接收对象为“Allusers”,就把聊天语句发送到聊天室;如果接收对象是自己登录时使用的用户名,就显示一个消息框,消息框的内容是聊天语句;如果接受对象是别的,就直接略过。
①
在.const段中定义一个全局变量dwreceiver
.const
dwreceiver db 'Allusers',0 ;canmeng萌4
②
;********************************************************************
; 循环接收消息
;********************************************************************
.elseif @stMsg.MsgHead.dwCmdId == CMD_MSG_DOWN
invoke lstrcmp,addr dwreceiver,addr @stMsg.MsgDown.szreceiver ;canmeng萌4
cmp eax,0 ;canmeng萌4
jz @F ;canmeng萌4
invoke lstrcmp,addr szUserName,addr @stMsg.MsgDown.szreceiver ;canmeng萌4
cmp eax,0
jz @next ;canmeng萌4
jmp xiayige1 ;canmeng萌4
@@: ;canmeng萌4
invoke lstrcpy,addr @szBuffer,addr @stMsg.MsgDown.szContent ;canmeng萌1,先显示内容
invoke SendDlgItemMessage,hWinMain,IDC_INFO,LB_INSERTSTRING,0,addr @szBuffer
invoke lstrcpy,addr @szBuffer,addr @stMsg.MsgDown.szSender
invoke lstrcat,addr @szBuffer,addr szSpar
invoke lstrcat,addr @szBuffer,addr @stMsg.MsgDown.szTime ;canmeng萌1
invoke SendDlgItemMessage,hWinMain,IDC_INFO,LB_INSERTSTRING,0,addr @szBuffer;canmeng萌1
jmp xiayige1 ;canmeng萌4
@next: ;canmeng萌4
invoke MessageBox,hWinMain,addr @stMsg.MsgDown.szContent,\
addr @stMsg.MsgDown.szSender,MB_OK ;canmeng萌4
xiayige1: ;canmeng萌4
.elseif @stMsg.MsgHead.dwCmdId == CMD_MSG_UP_RESP ;canmeng萌2
③
另外,在列表框中,当点击“发送”按钮时,需要知道哪一个用户被选中了。因此要修改主窗口程序。
.elseif ax == IDOK
invoke SendDlgItemMessage,hWinMain,IDC_LISTBOX,LB_GETCURSEL,0,0 ;canmeng萌4
mov ecx,offset szreceiver ;canmeng萌4
invoke SendDlgItemMessage,hWinMain,IDC_LISTBOX,LB_GETTEXT,eax,ecx ;canmeng萌4
invoke lstrlen,addr szreceiver ;canmeng萌4
.if eax==0 ;canmeng萌4,如果没选择列表框中项目,就把szreceiver设为'Allusers'
invoke lstrcpy,addr szreceiver,addr dwreceiver ;canmeng萌4
.endif ;canmeng萌4
invoke lstrcpy,addr @stMsg.MsgUp.szreceiver,addr szreceiver ;canmeng萌4
……
其实这里的这个szreceiver是在.data中定义的全局变量。
.data
szreceiver db 12 dup (?)
④
添加一个复位按钮,用于不选择列表框中的任何一个项目。
;***************************************************************;canmeng萌4
.elseif ax == IDC_RESET ;canmeng萌4
invoke SendDlgItemMessage,hWnd,IDC_LISTBOX,LB_SETCURSEL,-1,0 ;canmeng萌4
invoke lstrcpy,addr szreceiver,addr dwreceiver ;canmeng萌4
⑤
再添加一个功能:当用户点击“注销”时,把列表框中的用户名清除掉,而且把在线人数改为0.
.elseif ax == IDC_LOGOUT
@@:
.if hSocket
invoke closesocket,hSocket
xor eax,eax
mov hSocket,eax
invoke SendDlgItemMessage,hWinMain,IDC_LISTBOX,LB_RESETCONTENT,0,0;canmeng萌4
invoke SetDlgItemInt,hWinMain,IDC_COUNT1,0,FALSE ;canmeng萌4
.endif
这样,第四步功能“实现悄悄话”也完成了。
附件是源代码和生成的服务器端程序、客户端程序。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)