第三步功能实现了,第四步功能就是实现悄悄话的功能。
这个功能比较复杂,因此分两部分实现,第一部分实现用户登录退出时列表框中能同步显示当前用户,第二部分再实现点击列表框中某个用户时实现悄悄话功能
第一部分
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
这样,第四步功能“实现悄悄话”也完成了。
附件是源代码和生成的服务器端程序、客户端程序。

上传的附件 4实现悄悄话.rar
服务器和客户端可执行文件.rar