[已完结]2014小学期OpenQQ开发记录文档




 项目要求
       4周内以MFC为框架使用C/C++完成一个类似于QQ的局域网聊天工具(群聊,私聊,文件传输,窗口抖动)(服务端、客户端)-- OpenQQ。
 开发环境
      Visual Studio 2010(LOCAL MSDN)  + Windows 8
 源码地址
  Github
 开发过程记录
    
14.8.13    完成OpenQQ的登陆主界面主要包括:头像栏、背景、账号、密码、注册账号、找回密码、记住密码、自动登陆、简单的账号注册弹出页面、完成关于信息、修改图标等。
通过ps一张背景图,然后使用如下代码将其放到OnPaint函数的else中修改背景图:
    
01  void COpenQQDlg::OnPaint()
02  {
03      if (IsIconic())
04      {
05          CPaintDC dc(this); // 用于绘制的设备上下文
06 
07          SendMessage(WM_ICONERASEBKGND, reinterpret_cast<WPARAM>(dc.GetSafeHdc()), 0);
08 
09          // 使图标在工作区矩形中居中
10          int cxIcon = GetSystemMetrics(SM_CXICON);
11          int cyIcon = GetSystemMetrics(SM_CYICON);
12          CRect rect;
13          GetClientRect(&rect);
14          int x = (rect.Width() - cxIcon + 1) / 2;
15          int y = (rect.Height() - cyIcon + 1) / 2;
16 
17          // 绘制图标
18          dc.DrawIcon(x, y, m_hIcon);
19      }
20      else
21      {
22          //CDialogEx::OnPaint();
23 
24          CPaintDC dc(this);
25          CRect rc;
26          GetClientRect(&rc);
27          CDC dcMem;
28          dcMem.CreateCompatibleDC(&dc);
29          CBitmap bmpBackground;
30          bmpBackground.LoadBitmap(IDB_BTBK);
31 
32          BITMAP bitmap;
33          bmpBackground.GetBitmap(&bitmap);
34          CBitmap* pbmpPri = dcMem.SelectObject(&bmpBackground);
35          dc.StretchBlt(0,0,rc.Width(), rc.Height(), &dcMem,0,0,bitmap.bmWidth, bitmap.bmHeight, SRCCOPY);
36      }
37  }
    
    代码也很简单,这里就不注释了。 
    成果截图:
              
     14.8.14    将图片控件换成按钮控件,实现简单的点击头像换头像的功能。
实现简单的服务器界面。实现记住密码功能(通过Get/WritePrivateProfileString()方法 写入ini文件字段)有小部分问题所以暂时未添加进去这个功能。
    
WritePrivateProfileString函数声明如下:

01  BOOL WINAPI WritePrivateProfileString(
02  __in LPCTSTR lpAppName,
03  __in LPCTSTR lpKeyName,
04  __in LPCTSTR lpString,
05  __in LPCTSTR lpFileName
06  );
 
    GetPrivateProfileString函数声明如下:
01  DWORD WINAPI GetPrivateProfileString(
02  __in LPCTSTR lpAppName,
03  __in LPCTSTR lpKeyName,
04  __in LPCTSTR lpDefault,
05  __out LPTSTR lpReturnedString,
06  __in DWORD nSize,
07  __in LPCTSTR lpFileName
08  );
 
    14.8.15    实现基本的群聊功能。修复一个小bug。
    简单的使用WSAAsyncSelect通过自定义消息响应各种操作,加载winsock2.2库。 
    在InitInstance()中使用WSAStartup加载套接字库,由于使用的winsock2.2,所以不能直接使用MFC提供的AfxInitSocket()来初始化,在App类的析构函数终止套接字库的使用。客户端通过connect连接到服务器,服务器端通过自定义消息响应到lParam的低字节位,在FD_ACCEPT中判断客户端连接上来。然后在FD_READ接收来自客户端的UserInfo结构信息,用来验证客户的信息(目前没有实现这个功能,打算后面实现验证这个功能。尽量不去使用数据库。)然后就是通过send、recv等socket函数进行消息的广播以及信息的传递。
    基本socket函数的函数原型如下:

01  SOCKET PASCAL FAR socket( int af, int type, int protocol);
02  int PASCAL FAR connect( SOCKET s, const struct sockaddr FAR* name, int namelen);
03  int PASCAL FAR bind( SOCKET s, const struct sockaddr FAR* name,int namelen);
04  SOCKET PASCAL accept( SOCKET s, struct sockaddr * addr,int * addrlen);
05  int PASCAL FAR send( SOCKET s, const char FAR* buf, int len, int flags);
06  int PASCAL FAR recv( SOCKET s, char FAR* buf, int len, int flags);
        
    自定义UserInfo结构定义如下:

01  //用户信息结构
02  struct UserInfo
03  {
04      UINT    ID;
05      TCHAR    userName[20];
06      TCHAR    passWord[20];
07      BOOL    bOnLine;
08      BOOL    accFlag;
09      TCHAR    userIP[15];
10      SOCKET    userSocket;
11      SOCKET    friendSocket;
12      BOOL    isFriendInfo;//来自服务器消息是好友的信息还是 真正我该接收的消息
13  };

这里介绍一个技巧:由于socket的基本操作函数只支持传送char *的数据,自定义结构体我们可以传一个他的地址,然后强制转成char*
由于指针变量都是占用4个字节,大大简化了编程。 
     成果截图:  
    14.8.16    实现简单的设置界面功能,扩展设置选项的对话框以及收缩其对话框。服务器端实现客户在线信息的显示。聊天界面加入用户名的显示。实现单对单的单击相应,打开双人聊天的对话框。
    对于扩展设置,参考孙鑫的代码,但是这里需要说明的是本身初始化应该是小对话框,点击按钮之后才是大对话框,所以这里提供一个思路,可以在对话框初始化的时候获取其大对话框的信息就保存来,使用成员变量。然后该怎么做你懂的。
通过以下两个循环来使客户端收到在线好友的信息(注:按理说自己的好友里面不能有自己。)。
    更新信息关键代码:

01  //向以前上线的老客户发送刚上线的新客户的信息
02  for (int i=0; i<m_clientCnt; i++)
03  {
04      send(m_user[i].userSocket, (char *)&m_recvMsg, sizeof(m_recvMsg), 0);
05  }
06 
07  //向上线的新客户发送老客户的信息
08  for (int i=0; i<m_clientCnt; i++)
09  {
10      _tcscpy(m_recvMsg.userName,m_user[i].userName);
11      send(m_user[m_clientCnt].userSocket, (char *)&m_recvMsg, sizeof(m_recvMsg), 0);
12  }

    使用如下的代码来测试响应ListCtrl控件点击好友的消息。通过pNMListView->iItem就可以获得该项的索引,然后获取其socket信息就能向其发消息了。

01  void CFriDlg::OnNMClickListFriend(NMHDR *pNMHDR, LRESULT *pResult)
02  {
03      LPNMITEMACTIVATE pNMItemActivate = reinterpret_cast<LPNMITEMACTIVATE>(pNMHDR);
04 
05      NM_LISTVIEW* pNMListView = (NM_LISTVIEW*)pNMHDR;
06      if(pNMListView->iItem != -1)
07      {
08          CString strtemp;
09          strtemp.Format(_T("单击的是第%d行第%d列"),
10          pNMListView->iItem, pNMListView->iSubItem);
11          AfxMessageBox(strtemp);
12      }
13 
14      *pResult = 0;
15  }
 
    想法:加入私聊功能,加入SendMsg结构体。通过将发送的信息和姓名以及一个BOOL值绑定到SendMsg结构体的对象。让服务器来判断是群聊还是私聊。 (目前还有点bug,争取明天修正。)
    
SendMsg结构体定义如下:

01  struct SendMsg
02  {
03      TCHAR recvUser[20];
04      TCHAR sendMsg[250];
05      BOOL isSingleSend;
06  };

    成果截图: 
 
    14.8.17    首先简单实现下通过每个好友列表对话框能识别出自己是谁,免得搞混了(MFC中是使用SetWindowText(),并不存在SetWindowTitle()将自己的用户名显示到标题栏)。简单实现了在聊天对话框的标题栏上显示“与**聊天”的标识信息。需要自己注意的是GetDlgItem()这个函数是我用的最多的函数,我居然以为它的平台sdk的函是也是返回CWnd *,结果才是返回该窗口该控件的句柄。顺便学到每个控件基本都有一个FromHandle函数,多熟悉就好。   
    想法:在UserInfo结构同 添加一项 BOOLisFriendInfo;//来自服务器消息是好友的信息还是 真正我该接收的消息
    然后,好吧代码被我改的我都不知道成什么样子了。

    最后初步实现了私聊功能,存在一个bug就是每个用户必须都最多只打开一个聊天对话框,这样就没得问题,但是当每个用户都打开多个聊天对话框就会导致消息送达的对话框不对。(已经尝试着新加一个SubWindowPar结构使用WM_SUBWINDOW自定义消息在创建非模态聊天对话框的时候就传递这个句柄,想着在接收数据,显示数据的时候通过这个句柄来显示到正确的地方,但是却没有解决。 )
    SubWindowPar结构定义如下: 

01  struct SubWindowPar
02  {
03      HWND hwnd; //新建的非模态对话框的句柄
04      CString userName;
05  };

    成果截图:

    14.8.18    首先看了下有关非模态对话框的文档,对对象和窗口的销毁做了优化(将自定义的“取消”按钮的ID改为IDCANCEL,重载OnCancel函数,调用DestroyWindow函数(突然发现点击对话框的关闭按钮也是调用了OnCancel函数))。
 
01  void CChatDlg::OnCancel()
02  {
03      //CDialogEx::OnCancel();
04      DestroyWindow();
05  }
    
    在屏幕上一个窗口被删除后,框架会调用CWnd::PostNcDestroy,所以重载下这个虚函数,然后加入一行delete this;语句,析构窗口对象。
 

01  void CChatDlg::PostNcDestroy()
02  {
03      //CDialogEx::PostNcDestroy();
04      delete this;
05  }
    
    但是这样却出现了一个问题,关闭聊天的对话框就出现了客户下线的情况。原因是这个私聊是在之前群聊的基础上演变而来的,当非模态窗口析构的时候
会调用closesocket关闭主窗口的套接字。通过注视掉聊天对话框析构函数的closesocket函数的调用,但是多个窗口的聊天问题依然存在。新的想法,去掉聊天对话框对套接字的调用,通过windows消息机制,让好友对话框来收消息,然后通过消息传递给聊天对话框显示出来。结果实现起来太绕了,直接被绞晕了,果断给放弃了。
    实现非模态对话框与同一好友聊天的对话框只能打开一次:
 
01  public:
02      CChatDlg *m_chatDlg[20];
03      m_chatDlg[列表控件的序号] == NULL ? Create : Don't Create;
   
    在子对话框调用DestroyWindow之前通过以下的循环置NULL:

01  for (int i=0; i<20;)
02  {
03      if (((CFriDlg *)GetParent())->m_chatDlg[i] == NULL)
04      {
05          i++;
06      }
07      else
08      {
09          if (((CFriDlg *)GetParent())->m_chatDlg[i]->m_hWnd == m_hWnd)
10          {
11              ((CFriDlg *)GetParent())->m_chatDlg[i] = NULL;
12              break;
13          }
14          else
15          {
16              i++;
17          }
18      }
19  }
 
    实现群聊和私聊的整合
    成果截图: 
    突然发现私聊的时候发送方的名字显示的是接收方的名字,在服务器端修正此bug。
    修正后截图: 

    14.8.19    终于修复多窗口bug,聊天核心功能基本完成!
    起始尝试在每个子窗口的发送按钮响应函数中添加一个WSAAsyncSelect选择函数,表示是从当前对话框发送出去,就不会发送到另外的对话框了。但是同时需要保留在InitSocket中的WSAAsyncSelect选择函数,因为对方接收消息不可能还去点一下发送才接收(但是这招不起作用,已经换方法了)。
    接收方核心代码:

01  //是好友更新的信息
02  if (m_recvMsg.isFriendInfo == TRUE)
03  {
04      m_friendList.InsertItem(m_friendList.GetItemCount(), m_recvMsg.userName);
05      _tcscpy(m_user[m_clientCnt++].userName, m_recvMsg.userName);
06  }
07  else
08  {
09      if (m_recvMsg.isSingleSend == TRUE)
10      {
11          //让哪个窗口显示消息
12          int i;
13          for (i=0; i<20; i++)
14          {
15              if (!_tcscmp(m_whichWnd[i].recvUser, m_recvMsg.sendUser)
16                  && !_tcscmp(m_whichWnd[i].sendUser, m_recvMsg.recvUser))
17              {
18                  ((CListBox *)m_chatDlg[m_whichWnd[i].wndNum]->GetDlgItem(IDC_LIST_RECV))->InsertString(-1, m_recvMsg.sendMsg);
19                  break;
20              }
21          }
22 
23      if (i>=20)
24      {
25          //没有创建这个窗口
26          //通过弹出框将该信息弹出来
27          MessageBox(m_recvMsg.sendMsg, _T("你有新的消息,请注意查收"), MB_OK);
28      }
29      else
30      {
31          ((CListBox *)m_chatDlg[0]->GetDlgItem(IDC_LIST_RECV))->InsertString(-1, m_recvMsg.sendMsg);
32      }
33  }
 
    新的尝试,硬着头皮直接删掉了聊天对话框对套接字的调用,然后直接在其父窗口进行调用,将收到的消息发送到指定的聊天子对话框中。
    成果截图:图片

    加入
了未打开聊天对话框时,新消息到来时的提示功能。
       成果截图:
 


    14.8.20    优化服务器端显示在线、离线客户信息,修正部分bug。使用线性表删除操作实现客户端下线删除客户端的操作。实现客户端的好友上下线信息更新。简单修改客户端和服务器的界面。
    去掉标题栏,通过ps两张图片,添加两个图片按钮来响应是最小化还是关闭,想让窗口随着鼠标移动,可以通过响应WM_LBUTTONDOW消息,在其中添加如下代码:

01  void COpenQQDlg::OnLButtonDown(UINT nFlags, CPoint point)
02  {
03      SendMessage(WM_SYSCOMMAND,0xF012,0);
04      CDialogEx::OnLButtonDown(nFlags, point);
05  }

成果截图: 






    14.8.21    首先简单的实现对IP控件的使用,端口号编辑框默认使用服务器端的6600端口,且禁止修改。简单实现服务器端按钮的灵活禁用与启用。通过自己的乱七八糟的想法,初步实现了多用户名和密码的保存,对Combo box控件的事件响应实现了切换用户名切换到对应的密码。初步实现了自动登陆的功能。一共创建了三个配置文件,分别为:RememberPassword.ini、AutoLogin.ini、RemPassCnt.ini,
三个配置文件分别存储的信息项为:

01  RememberPassword.ini(保存的用户名和密码):
02  [0] //用户名的序号
03  UserName=154836158
04  [154836158] //用户名
05  Password=21212 //该用户名对应的密码
06 
[1] //用户名的序号
07  UserName=41905107
08  [41905107] //用户名
09  Password=*ptwww //该用户名对应的密码
10  [2] //同理
11  UserName=908222
12  [908222]
13  Password=0000000000000000
14 
15  RemPassCnt.ini(保存用户的个数):
16  [保存用户名的个数]
17  profileAppnameCnt=3
18  AutoLogin.ini(保存是否自动登陆):
19  [自动登陆]
20  bAutoLogin=TRUE
 
    关键代码如下:

01  void COpenQQDlg::OnBnClickedAutologin()
02  {
03      // TODO: 在此添加控件通知处理程序代码
04 
05      //自动登陆选中,将key写入ini文件
06      if (BST_CHECKED == IsDlgButtonChecked(IDC_AUTOLOGIN))
07      {
08          ::WritePrivateProfileString(_T("自动登陆"), _T("bAutoLogin"),
09                  _T("TRUE"), _T("..\\AutoLogin.ini"));
10      }
11      else
12      {
13          ::WritePrivateProfileString(_T("自动登陆"), _T("bAutoLogin"),
14                  _T("FALSE"), _T("..\\AutoLogin.ini"));
15      }
16  }
17 
18 
19  void COpenQQDlg::OnBnClickedRempasswd()
20  {
21      // TODO: 在此添加控件通知处理程序代码
22 
23      if (BST_CHECKED == IsDlgButtonChecked(IDC_REMPASSWD))
24      {
25          CString lpPath = _T("..\\RememberPassword.ini");
26 
27          CString userName;
28          GetDlgItemText(IDC_USERNAME, userName);
29 
30          CString passWord;
31          GetDlgItemText(IDC_PASSWD, passWord);
32 
33          CString szClientCnt;
34          szClientCnt.Format(_T("%d"), m_profileAppnameCnt);
35 
36          ::WritePrivateProfileString(szClientCnt, _T("UserName"), userName, lpPath);
37          ::WritePrivateProfileString(userName, _T("Password"), passWord, lpPath);
38          m_profileAppnameCnt++;
39 
40          //将这个计数也写入一个配置文件?
41          lpPath = _T("..\\RemPassCnt.ini");
42          szClientCnt.Format(_T("%d"), m_profileAppnameCnt);
43          ::WritePrivateProfileString(_T("保存用户名的个数"), _T("profileAppnameCnt"),
44                      szClientCnt, lpPath);
45      }
46  }
47 
48 
49  void COpenQQDlg::LoadProfiles(void)
50  {
51      //加载密码保护 多少个用户
52      CString lpPath = _T("..\\RemPassCnt.ini");
53      TCHAR szClientCnt[3];
54      char chClientCnt[3];
55      //szClientCnt.Format(_T("%d"), m_profileAppnameCnt);
56      ::GetPrivateProfileString(_T("保存用户名的个数"), _T("profileAppnameCnt"),
57                  NULL, szClientCnt, 3, lpPath);
58      WideCharToMultiByte(CP_OEMCP, 0, szClientCnt, -1, chClientCnt, 3, 0, FALSE);
59      m_profileAppnameCnt = atoi(chClientCnt);
60      MessageBox(szClientCnt);
61 
62      //加载保存密码的配置文件
63      lpPath = _T("..\\RememberPassword.ini");
64      CFileFind finder;
65      BOOL ifFind = finder.FindFile(lpPath);
66 
67      if (ifFind)
68      {
69          //MessageBox(_T("文件存在。"));
70          //自动从ini文件载入保存的用户名和密码
71          TCHAR userName[20];
72          TCHAR passWord[20];
73          for (int i=0; i<m_profileAppnameCnt; i++)
74          {
75              CString szCnt;
76              szCnt.Format(_T("%d"), i);
77              ::GetPrivateProfileString(szCnt, _T("UserName"), NULL, userName,
78                      20, lpPath);
79              ::GetPrivateProfileString(userName, _T("Password"), NULL, passWord,
80                      20, lpPath);
81              //MessageBox(passWord, userName);
82              ((CComboBox *)GetDlgItem(IDC_USERNAME))->InsertString(-1, userName);
83              SetDlgItemText(IDC_USERNAME, userName);
84              SetDlgItemText(IDC_PASSWD, passWord);
85          }
86 
87          //将checkbox标记为勾选
88          ((CButton *)GetDlgItem(IDC_REMPASSWD))->SetCheck(1);
89      }
90 
91      //加载自动登陆的配置文件
92      lpPath = _T("..\\AutoLogin.ini");
93      ifFind = finder.FindFile(lpPath);
94      TCHAR bAutoLogin[6];
95      ::GetPrivateProfileString(_T("自动登陆"), _T("bAutoLogin"),
96              _T("FALSE"), bAutoLogin,
97                  6, _T("..\\AutoLogin.ini"));
98      if (ifFind)
99      {
100        if (!_tcscmp(bAutoLogin, _T("TRUE")))
101        {
102            ((CButton *)GetDlgItem(IDC_AUTOLOGIN))->SetCheck(1);
103            OnBnClickedBtnlogin();
104        }
105    }
106}
 
 
注:保存密码实现的方法绝对有更简单的方法,为了原创,我也不晓得为啥我就这么想的去保存,但是好在还是成功了。此方案仅供参考。
成果截图:
 
 
    虚拟机测试截图:
 

    14.8.22    修复各种上下线信息更新bug,这里贴出OnSock函数(注:全是凭自己感觉写的,总觉得写复杂了,仅供参考。)。优化一些细节,在服务器端控制了相同账号的登陆限制,但是没有在客户端登陆界面给予提示。美化了界面,虽然感觉挺丑的,但是没办法还是先这样吧。在SubWindowPar中添加了CString windowTitle,不知道为什么在非模态对话框中的InitDialog中获取不到它的标题内容,所以还是顺便通过这个消息从父窗口传递过来吧。
    服务器端OnSock代码如下:

01  LRESULT COpenQQSerDlg::OnSock(WPARAM wParam, LPARAM lParam)
02  {
03      SOCKET s = wParam;
04 
05      char chRecvMsg[512];
06      memset(chRecvMsg, 0, 512);
07      CString logInfo;
08      CString logTempInfo;
09      //char tempName[30];
10      CString fromatMsg;
11      char sendMsg[256];
12      BOOL isOnce = TRUE;
13      CTime currTime = CTime::GetCurrentTime();
14      logTempInfo = currTime.Format("%H:%M:%S ");
15      int i, j, k;
16      switch (LOWORD(lParam))
17      {
18      case FD_READ:
19          //来判断是否第一次链接
20          //m_clientCnt 为所有的客户(包括在线和离线的用户)
21          for (int i=0; i<m_clientCnt; i++)
22          {
23              if (m_user[i].userSocket == s)
24              {
25                  //存在这个socket说明不是第一次
26                  //要么是聊天 要么是将其的状态更新为在线
27 
28                  isOnce = FALSE;
29                  break;
30              }
31          }
32 
33          //如果是第一次,那么就更新其信息到对应的listctrl
34          if (isOnce)
35          {
36              //第一次连接的时候收取用户发来的信息
37              if (/*m_user[m_clientCnt].bOnLine == FALSE && */m_user[m_clientCnt].accFlag == TRUE)
38              {
39                  recv(s, (char *)&m_user[m_clientCnt], sizeof(m_user[m_clientCnt]), 0);
40 
41                  //接收了姓名之后来判断是不是老客户,是老客户直接替换套接字
42                  int x;
43                  for (x=0; x<m_clientCnt; x++)
44                  {
45                      if (!_tcscmp(m_user[x].userName, m_user[m_clientCnt].userName))
46                      {
47                          //老客户
48                          if (m_user[x].bOnLine == TRUE)
49                          {
50                              //MessageBox(_T("已经登陆!"));
51                              //向该登陆界面发送消息。
52 
53                              //send(s, "the same", strlen("the same")+1, 0);
54                              return 0;
55                          }
56                          else
57                          {
58                              //send(s, "no the same", strlen("no the same")+1, 0);
59                              m_user[x].userSocket = s;
60                          }
61                          break;
62                      }
63                  }
64 
65                  //新客户
66                  if (x == m_clientCnt)
67                  {
68                      m_user[m_clientCnt].userSocket = s;
69                      m_user[m_clientCnt].bOnLine = TRUE;
70                 
71                      logInfo.Format(_T("%s 上线了!"), m_user[m_clientCnt].userName);
72                      logTempInfo += logInfo;
73 
74                      m_userList.InsertItem(m_userList.GetItemCount(), m_user[m_clientCnt].userName);//主项
75                      m_userList.SetItemText(m_userList.GetItemCount()-1, 1, _T("在线"));//子项
76 
77                      //暂时先加一个循环来将所发送的信息都设置为好友更新的信息
78                      m_recvMsg.isFriendInfo = TRUE;
79                      m_recvMsg.isOnLine = TRUE;
80                      _tcscpy(m_recvMsg.userName,m_user[m_clientCnt].userName);
81 
82                      //向以前上线的老客户发送刚上线的新客户的信息
83                      for (int i=0; i<m_clientCnt; i++)
84                      {
85                          if (m_user[i].bOnLine == TRUE)
86                              send(m_user[i].userSocket, (char *)&m_recvMsg, sizeof(m_recvMsg), 0);
87                      }
88 
89                      //向上线的新客户发送老客户的信息
90                      for (int i=0; i<m_clientCnt; i++)
91                      {
92                          if (m_user[i].bOnLine == TRUE)
93                          {
94                              _tcscpy(m_recvMsg.userName,m_user[i].userName);
95                              send(m_user[m_clientCnt].userSocket, (char *)&m_recvMsg, sizeof(m_recvMsg), 0);
96                          }
97                      }
98 
99                      m_clientCnt++;
100                    ((CListBox *)GetDlgItem(IDC_LIST_LOG))->InsertString(-1, logTempInfo);
101                }
102                else
103                {
104                    //老客户
105                    //将该老客户 设置为在线
106                    m_user[x].bOnLine = TRUE;
107
108                    //将老客户的标志设置为在线
109                    for (int n=0; n<m_userList.GetItemCount(); n++)
110                    {
111                        if (!_tcscmp(m_user[x].userName, m_userList.GetItemText(n,0)))
112                        {
113                            m_userList.SetItemText(n, 1, _T("在线"));
114
115                            logInfo.Format(_T("%s 上线了!"), m_user[m_clientCnt].userName);
116                            logTempInfo += logInfo;
117                            ((CListBox *)GetDlgItem(IDC_LIST_LOG))->InsertString(-1, logTempInfo);
118                            break;
119                        }
120                    }
121
122                    m_recvMsg.isFriendInfo = TRUE;
123                    m_recvMsg.isOnLine = TRUE;
124                    _tcscpy(m_recvMsg.userName,m_user[m_clientCnt].userName);
125
126                    //向以前上线的老客户发送刚上线的新客户的信息
127                    for (int i=0; i<m_clientCnt; i++)
128                    {
129                        if (m_user[i].bOnLine == TRUE)
130                            send(m_user[i].userSocket, (char *)&m_recvMsg, sizeof(m_recvMsg), 0);
131                    }
132
133                    //向上线的新客户发送老客户的信息
134                    for (int i=0; i<m_clientCnt; i++)
135                    {
136                        if (m_user[i].bOnLine == TRUE)
137                        {
138                            _tcscpy(m_recvMsg.userName,m_user[i].userName);
139                            send(m_user[x].userSocket, (char *)&m_recvMsg, sizeof(m_recvMsg), 0);
140                        }
141                    }
142                }
143            }
144        }
145        else
146        {
147            //可能是老客户再次上线,也有可能是聊天
148
149            //聊天 将所有用户的标志位设置为FALSE
150            //判断是群聊还是私聊           
151            recv(s, (char *)&m_recvMsg, sizeof(m_recvMsg), 0);
152            m_recvMsg.isFriendInfo = FALSE;
153
154
155            //取出消息并转化为char*字符串
156            WideCharToMultiByte(CP_OEMCP, 0, m_recvMsg.sendMsg, -1, sendMsg, 256, 0, FALSE);
157
158            //私聊
159            fromatMsg = m_recvMsg.sendUser;
160            fromatMsg += _T(" ");
161            fromatMsg += currTime.Format("%H:%M:%S ");
162            fromatMsg += m_recvMsg.sendMsg;
163
164            WideCharToMultiByte(CP_OEMCP, 0, fromatMsg.GetBuffer(), -1, chRecvMsg, 512, 0, FALSE);
165            _tcscpy(m_recvMsg.sendMsg, fromatMsg.GetBuffer());
166
167            SendMsg tempSendMsg;
168            _tcscpy(tempSendMsg.sendUser,m_recvMsg.recvUser);
169            _tcscpy(tempSendMsg.recvUser,m_recvMsg.sendUser);
170            _tcscpy(tempSendMsg.sendMsg, m_recvMsg.sendMsg);
171            tempSendMsg.isFriendInfo = FALSE;
172            tempSendMsg.isSingleSend = TRUE;
173
174            if (m_recvMsg.isSingleSend == TRUE)
175            {
176
177                send(s, (char *)&tempSendMsg, sizeof(tempSendMsg), 0);
178                for (int i=0; i<m_clientCnt; i++)
179                {
180                    if (!_tcscmp(m_recvMsg.recvUser, m_user[i].userName))
181                    {   
182                        //给对方发送
183                        send(m_user[i].userSocket, (char *)&m_recvMsg, sizeof(m_recvMsg), 0);                   
184                        break;
185                    }
186                }
187            }
188            else //群聊
189           
{
190                for (int i=0; i<m_clientCnt; i++)
191                {
192                    send(m_user[i].userSocket, (char *)&m_recvMsg, sizeof(m_recvMsg), 0);
193                }
194            }
195        }
196        break;
197
198    case FD_ACCEPT://有客户端上线
199        accept(s, NULL, NULL);
200        m_user[m_clientCnt].accFlag = TRUE;
201
202        break;
203
204    case FD_CLOSE://有客户端下线
205        for (i=0; i<m_clientCnt; i++)
206        {
207            if (m_user[i].userSocket == s)
208            {
209                m_user[i].bOnLine = FALSE;
210                logInfo.Format(_T("%s 下线了!"), m_user[i].userName);
211                logTempInfo += logInfo;
212                ((CListBox *)GetDlgItem(IDC_LIST_LOG))->InsertString(-1, logTempInfo);
213
214                //m_userList.DeleteItem(i);
215                for (int m=0; m<m_userList.GetItemCount(); m++)
216                {
217                    if (!_tcscmp(m_user[i].userName, m_userList.GetItemText(m, 0)))
218                    {
219                        //m_userList.DeleteItem(m);
220                        m_userList.SetItemText(m, 1, _T("离线"));
221                        break;
222                    }
223                }
224               
225                //向所有客户端发送下线信息
226                SendMsg outOfLineMsg;
227                outOfLineMsg.isFriendInfo = TRUE;
228                _tcscpy(outOfLineMsg.userName, m_user[i].userName);
229                outOfLineMsg.isOnLine = FALSE;
230
231                for (k=0; k<m_clientCnt; k++)
232                {
233                    if (k!=i)
234                        send(m_user[k].userSocket, (char *)&outOfLineMsg, sizeof(outOfLineMsg), 0);
235                }
236                break;
237            }
238        }
239    }
240    return 0;
241}

  
    成果截图:
图片
     通过CSDN的一篇文章,
添加了登陆界面的托盘显示(貌似作用不大,后面考虑取消,或者在好友对话框中添加这个功能)。在这里代码就不贴了,直接百度吧,这些功能用处也不是很大。
    结果截图:

     
    14.8.23    加入窗口抖动功能(参考自CSDN某人的)(群聊中禁用窗口抖动功能),初步实现文件传输。
    窗口抖动核心功能代码如下:

01  //OnSock中实现 ,参考自CSDN某人的
02  //判断是否是窗口抖动
03  if (!_tcscmp(m_recvMsg.sendMsg, _T("~w&i*n(d)o$w#s@h^a_k+e~")))
04  {
05      CRect rect;
06      m_chatDlg[m_whichWnd[i].wndNum]->GetWindowRect(&rect);
07      int move=10;
08      PlaySound(_T("..\\shake.wav"),NULL,SND_FILENAME | SND_ASYNC);
09      for(int z=1;z<9;z++)
10      {
11          rect.OffsetRect(0,move);
12          m_chatDlg[m_whichWnd[i].wndNum]->MoveWindow(&rect);
13          Sleep(50);
14          rect.OffsetRect(move,0);
15          m_chatDlg[m_whichWnd[i].wndNum]->MoveWindow(&rect);
16          Sleep(50);
17          if (10==move)
18          {
19              move=-10;
20          }
21          else
22          {
23              move=10;
24          }
25      }
26  }

    窗口抖动截图:

    14.8.24     算是比较完美的实现了文件传输功能。刚开始图片传输错误的原因在于读取代码有问题,后经独立验证,实现了较为正确的文件读取。
    关键代码如下:

01  void CChatDlg::OnBnClickedFile()
02  {
03      // TODO: 在此添加控件通知处理程序代码
04      //这里需要弹出一个选择文件的窗口
05      CFileDialog fileDlg(TRUE);
06      fileDlg.m_ofn.lpstrTitle = _T("请选择你要发送的文件");
07      fileDlg.m_ofn.lpstrFilter = _T("All Files(*.*)\0*.*\0\0");
08 
09      if (IDOK == fileDlg.DoModal())
10      {
11          CFile file;
12          file.Open(fileDlg.GetPathName(), CFile::modeRead);
13          int bytesRead = 0;
14          long fileOffset = 0;
15 
16          ULONGLONG fileLen = file.GetLength();
17          m_sendMsg.fileLen = fileLen;
18          ((CFriDlg *)GetParent())->GetWindowText(m_sendMsg.sendUser, 20);
19          CString recvUser;
20          GetWindowText(recvUser);
21          int begin = recvUser.Find(_T("聊"));
22          _tcscpy(m_sendMsg.recvUser, recvUser.Mid(2, begin-3));
23          m_sendMsg.isFile = TRUE;
24          _tcscpy(m_sendMsg.fileName, fileDlg.GetFileName().GetBuffer());
25 
26          CString sendInfo;
27 
28          while (fileLen > MAX_FILE_SIZE)
29          {
30              m_sendMsg.isFinished = FALSE;
31 
32              file.Seek(fileOffset, CFile::begin);
33             
34              bytesRead = MAX_FILE_SIZE;
35 
36              file.Read(m_sendMsg.sendFile, bytesRead);
37             
38              //发送到主窗口
39              //WM_SUBTOMAIN    WM_USER+5
40              m_sendMsg.fileOffset = fileOffset;
41              m_sendMsg.transSize = bytesRead;
42              ((CFriDlg *)GetParent())->SendMessage(WM_SUBTOMAIN, (WPARAM)&m_sendMsg);
43             
44              sendInfo.Format(_T("文件发送进度:%d%%"), int(((double)fileOffset/m_sendMsg.fileLen)*100));
45              m_transFilePro.SetPos(int(((double)fileOffset/m_sendMsg.fileLen)*100));
46              GetDlgItem(IDC_SEND)->SetWindowText(sendInfo);
47 
48              fileOffset += bytesRead;
49              fileLen -= MAX_FILE_SIZE;
50 
51              Sleep(70);
52          }
53         
54          file.Seek(fileOffset, CFile::begin);
55         
56          if (fileLen <= MAX_FILE_SIZE)
57          {
58              m_sendMsg.isFinished = TRUE;
59 
60              file.Read(m_sendMsg.sendFile, fileLen);
61     
62              m_sendMsg.fileOffset = fileOffset;
63              m_sendMsg.transSize = bytesRead;
64              ((CFriDlg *)GetParent())->SendMessage(WM_SUBTOMAIN, (WPARAM)&m_sendMsg);
65              sendInfo.Format(_T("文件发送进度:%d%%"), 100);
66              m_transFilePro.SetPos(100);
67              GetDlgItem(IDC_SEND)->SetWindowText(sendInfo);
68          }
69 
70          file.Close();               
71      }
72  }

     文件传输截图:







 

    14.8.25修复发送文件之后不能发送消息,修复传输文件和窗口抖动的冲突,实现收到消息自动弹出窗口并显示收到的消息,实现收到窗口抖动自动弹出窗口并抖动,实现收到文件自动弹出窗口并更新进度。修复实现过程中的bug。优化代码,将窗口抖动、文件传输代码分别封装起来。加入标志位标记是否为窗口抖动,废除之前的通过发送和接收windowshake字符串的方法。直接隐藏群聊中的窗口中抖动和文件传输的控件。
 

    14.8.26    完成用户名、密码的验证。通过在服务器端添加两个配置文件,当客户端第一次将用户名发送过来的时候,我就进行遍历配置文件的内容进行验证。比较完美的实现了注册功能,以及限制同账号多次注册。对connect函数的返回值进行处理,实现当服务器端连接不上给出提示。加入各种注册验证。基本完成了概要设计文档的撰写。

    服务器端写入注册的用户信息到配置文件代码如下:

01  BOOL COpenQQSerDlg::WriteRegInfoToProfile(const CString &wUserName, const CString &wPasswd)
02  {
03 
04      CString lpPath = _T("..\\userNameAndPasswd.ini");
05 
06      CString szClientCnt;
07      szClientCnt.Format(_T("%d"), m_registerUserCnt);
08 
09      //先循环遍历整个userNameAndPasswd文件,看该用户名存在不存在
10      for (int i=0; i<m_registerUserCnt; i++)
11      {
12          TCHAR userName[20];
13          CString szCnt;
14          szCnt.Format(_T("%d"), i);
15          ::GetPrivateProfileString(szCnt, _T("UserName"), NULL, userName,
16              20, lpPath);
17          //该用户名存在,写入失败
18          if (!_tcscmp(userName, wUserName))
19          {
20              return FALSE;
21          }
22      }
23 
24      ::WritePrivateProfileString(szClientCnt, _T("UserName"), wUserName, lpPath);
25      ::WritePrivateProfileString(wUserName, _T("Password"), wPasswd, lpPath);
26      m_registerUserCnt++;
27 
28      //将这个计数也写入一个配置文件
29      lpPath = _T("..\\UserCnt.ini");
30      szClientCnt.Format(_T("%d"), m_registerUserCnt);
31      ::WritePrivateProfileString(_T("注册用户的个数"), _T("RegisterUserCnt"),
32          szClientCnt, lpPath);
33 
34      return TRUE;
35  }

     服务器端校正用户信息代码如下:

01  BOOL COpenQQSerDlg::VerifyUser(const CString& vUserName, const CString& vUserPasswd)
02  {
03      //载入配置文件
04      //加载保存密码的配置文件
05      CString lpPath = _T("..\\userNameAndPasswd.ini");
06      CFileFind finder;
07      BOOL ifFind = finder.FindFile(lpPath);
08 
09      if (ifFind)
10      {
11          //自动从ini文件载入保存的用户名和密码
12          TCHAR userName[20];
13          TCHAR passWord[20];
14          for (int i=0; i<m_registerUserCnt; i++)
15          {
16              CString szCnt;
17              szCnt.Format(_T("%d"), i);
18              ::GetPrivateProfileString(szCnt, _T("UserName"), NULL, userName,
19                      20, lpPath);
20 
21              if (!_tcscmp(vUserName, userName))
22              {
23                  ::GetPrivateProfileString(userName, _T("Password"), NULL, passWord,
24                          20, lpPath);
25 
26                  if (!_tcscmp(vUserPasswd, passWord))
27                  {
28                      //用户名,密码验证通过
29                      return TRUE;
30                  }
31              }
32          }
33      }
34      return FALSE;
35  }
 
     
成果截图:



 

    14.8.27     今天由于花了时间在帮别人写代码和自己写文档上,所以也没怎么写代码。貌似都不怎么想继续写了。晚上闲着无聊就在好友列表实现了显示QQ头像的功能,顺便加入了 相同账号不能多次重复登陆的限制,并在客户端进行警告提示。最后优化了代码,删掉了冗余的创建对话框的代码。

     成果截图:




 
    14.8.28     实现自己头像的修改并同步至服务器。

    14.8.29     实现好友头像同步并显示。
    成果截图:
 

 
就这样吧(存在的局域网bug已经让我心碎了,不想去解决了,最终只是一个失败的产品)。。。
                                        新生开学了,我们明年也该滚了,好好珍惜在校的时光吧。











 

posted @ 2014-11-22 20:20  IFPELSET  阅读(393)  评论(2)    收藏  举报