概述
在C/S 模式中,服务器端往往是设计的重点。一般来说,服务器的能够承受的连接数量是衡量一个服务器性能好坏的重要标准,为了测试服务器能够承受的连接数,我们必须使用多台客户机来测试他的性能.可是,很多情况下,我们没有那么多的机器,同时使用多台机器进行测试也是浪费资源,为此,我们设计了模拟多用户客户端程序来解决这个问题。
本文采用MFC的CSocket类在.NET平台上进行设计.所谓的模拟多用户就是用一个客户端程序来建立多个与服务器的连接,就好像多个客户端与服务器进行连接一样。设计的重点是:
○ 程序能够生成用户指定的数目的连接;
○ 用户可以在建立的连接中任意指定某个连接进行通信;
○ 用户可以随意更换连接进行通信测试,每个连接不会互相混淆,尤其是在读写数据的时候,不能张冠李戴;
○ 用户可以随意指定断开某个连接,而不会影响其他连接。
那么这么多的连接究竟如何管理呢?
首先,我们要有一种数据结构来描述每个连接的详细情况。本文采用了结构体。
自定义结构体struct socket_info
{
CSocket* s_client; //保存用户的SOCKET值
u_long client_addr; //保存用户网络地址
CString username; //用户昵称
int id;//连接号
} ;
然后,使用C++的模板类CList来管理这些连接。以后所做的所有事情就是对这个链表的操作。
设计步骤
1、创建一个基于对话框的工程CClientDlg.在MFC应用程序向导中选中windows 套接字.
2、给对话框添加菜单,并添加菜单项,包括配置服务器、用户登陆、退出、通信、断开连接。
3、添加"配置服务器"响应函数,如图1所示:设置m_Port=1111,m_strIpaddress="192.168.0.88"。

4、添加"用户登陆"响应函数,如图2所示:

系统根据用户输入的想要建立的连接数,自动生产连接并将其放入链表中。
5、添加"通信"响应函数,如图3所示:

点击"发送",发送数据;点击"接收",接收数据;点击"断开该连接",关闭socket,并从链表中删除该连接。
具体代码
// Client.h : PROJECT_NAME 应用程序的主头文件
#include "afxtempl.h" //使用模板类必须加入该头文件
//声明全局变量
struct socket_info
{……} ;//如前所示
extern CList<socket_info,socket_info&> s_info; //链表类
extern CClientSocket* lSocket;
extern int id;//用于指示用户选择的要进行通信的连接
extern char pBuf[100];//接收缓冲区
// ClientDlg.cpp : 实现文件
#include "ConfigureDlg.h"
#include "LoginDlg.h"
#include "CommunicationDlg.h"
void CClientDlg::OnServerConfserver()//“服务器配置”菜单响应函数
{ CConfigureDlg dlg;
if(dlg.DoModal()==IDOK)
{m_Port=dlg.m_Port;//m_port是CClientDlg类的成员变量
m_strIpaddress=dlg.m_strIpaddress;//m_strIpaddress是CClientDlg类的成员变量
}
}
void CClientDlg::OnServerConflogin()//“用户登陆”菜单响应函数
{ CLoginDlg dlg;
int t=0;
int size=s_info.GetSize();//查看当前连接链表的长度
if(dlg.DoModal()==IDOK)
{ socket_info* pInfo;//声明结构体
for(int i=0;i<dlg.m_nUserCount;i++)
{ pInfo = new socket_info;
pInfo->s_client=new CClientSocket();//创建socket
if(!(pInfo->s_client->Create()))
{ delete pInfo->s_client;
pInfo->s_client=NULL;
}
if(!(pInfo->s_client->Connect(m_strIpaddress,m_Port)))
{ t++;
delete pInfo->s_client;
pInfo->s_client=NULL;
}
else{
pInfo->id =size+i; //设置当前连接的id
pInfo->username=dlg.m_strUsername;
s_info.AddTail(*pInfo); //将成功的连接加入链表
}
}
int c=dlg.m_nUserCount-t; //得到成功的连接数
char message[10];
::sprintf(message,"%d",c);//将数字转换成字符串
strcat(message,"succeed");
if(c>=0) AfxMessageBox(message);
}
}
void CClientDlg::OnCommunication()//“通讯”菜单响应函数
{ CCommunicationDlg dlg;
dlg.DoModal();
}
void CClientDlg::OnCommunicationDisconall()//“退出”菜单响应函数
{ if(lSocket) lSocket=NULL;
if(!s_info.IsEmpty())//如果链表不为空
{ POSITION pos;
socket_info info=s_info.GetHead();
for(pos=s_info.GetHeadPosition();pos!=NULL;)
{ info=s_info.GetNext(pos);
if(!info.s_client==NULL)
{ info.s_client->ShutDown(2);
info.s_client->Close();
delete info.s_client; //删除socket
info.s_client=NULL;
}
}s_info.RemoveAll();//清除列表
}AfxMessageBox("Disconnect succeed");
}
// CommunicationDlg.cpp : 实现文件
void CCommunicationDlg::OnBnClickedQuery()//"发送"按钮的响应函数
{ lSocket=NULL;
UpdateData();
CString pB="hello";
id=atoi(m_strQueryId); //获得用户输入
POSITION pos;
if(!s_info.IsEmpty())
{ socket_info info=s_info.GetHead();
if(id>=s_info.GetCount()) //可选择的id必须小于链表的大小
MessageBox("the data is larger than the count of the list","Alert",MB_OK);
else{
for(pos=s_info.GetHeadPosition();;)
{ if(info.id==id&&!info.s_client==NULL)
{ lSocket=info.s_client;//将用户指定的连接的socket赋予lSocket lSocket->Send(m_strSendData,m_strSendData.GetLength());//发送发送文本框中的文本
break;
}
if(pos==NULL) break;
else info =s_info.GetNext(pos);
}
}
}else AfxMessageBox("the queer is empty!");//链表为空
}
void CCommunicationDlg::OnBnClickedAdd()//"接收"按钮的响应函数
{ UpdateData();
BOOL MsgEnd=TRUE;
int iRecv;
if(!lSocket==NULL)
{ memset(pBuf,0,100);//清空缓冲区
do{ iRecv=lSocket->Receive(pBuf,100);
if(iRecv<10&&iRecv>0) { MsgEnd=TRUE;}
pBuf[iRecv]=0;
}while(!MsgEnd);
m_ReceData.SetSel(0,-1);
m_ReceData.ReplaceSel(pBuf);// 显示接收的字符串
} else AfxMessageBox("the socket was disconnected");
}
void CCommunicationDlg::OnBnClickedCancel()//"断开该连接"按钮的响应函数
{ if(lSocket) {lSocket=NULL;}
if(s_info.IsEmpty()) AfxMessageBox("链表为空");
else {socket_info info=s_info.GetAt(s_info.FindIndex(id));//找到id对应的结构体
if(!info.s_client==NULL)
{ if(info.s_client->ShutDown(2))
{ info.s_client->Close();//关闭socket
delete info.s_client;
info.s_client=NULL;
s_info.RemoveAt(s_info.FindIndex(id));//从链表中删除该结构体
AfxMessageBox("Disconnect successfully!");
}
}
}OnCancel();
}
设计技巧
在设计中,我们要注意几个问题,这些问题的解决直接影响到程序的性能。
1、对于一个基于对话框的应用程序,Visual MFC应用程序向导不会给对话框创建菜单。如果要在对话框中显示菜单,必须把它作为一个资源,并连接到对话框窗口。具体步骤:
○ 右击资源试图的"菜单"选项,创建一个菜单IDR_MENU1,添加菜单项;
○ 打开资源试图的"对话框"选项,右击对话框(IDD_CLIENT_DIALOG),选择"属性", 在弹出的属性表中找到"Menu",将它的值设为IDR_MENU1;
2、用户要建立连接,就要指定连接数,问题是,用户不一定一次指定所有的连接。比如说,第一次,用户指定了50个连接,程序将50个连接加入到连接队列中。经过测试,用户发现50个连接运行情况良好,于是,用户想要测试100个连接的运行情况,这时,我们不能要求用户退出并重新运行程序,然后指定100个连接重新进行测试。我们要做的就是让用户能够再次指定50个连接,并且将这50个连接加入到前50个连接的后面。所以,在设计时,每次建立指定数目的连接前,必须查询队列的长度,然后将建立的连接加入到队列中正确的位置上。正如“用户登陆”菜单响应函数所示:
int size=s_info.GetSize();//查看当前连接链表的长度
......
pInfo->id =size+i; //设置当前连接的id
3、每次用户指定某个连接进行测试时,程序都要自动搜索连接队列找到指定的连接,并发送信息。问题是,我们的发送和接收是不同的响应函数,如何保证接收信息的连接是用户指定的连接呢?例如,用户建立了100个连接,指定Id为59的连接进行通信,当发送数据时,程序自动找到Id=59的socket发送数据,可是,当接收数据时,程序怎么知道是哪个连接负责接收数据呢?我们可以在接收响应函数中再次查找队列,但是,这样不但增加了系统资源的消耗,而且增加了系统的延迟。我们采用全局变量来解决这个问题。
extern CClientSocket* lSocket;
lSocket=info.s_client;//将用户指定的连接的socket赋予lSocket
lSocket->Send(m_strSendData,m_strSendData.GetLength());//发送发送文本框中的文本
......
iRecv=lSocket->Receive(pBuf,100);
4、每次用户退出程序之前,必须断开所有连接,并清空队列。 如“退出”菜单响应函数所示。
结束语
经过实际验证,该程序能够很好的测试服务器的连接承受能力,从理论上来说,用户可以指定任意多的连接,实际上,连接数受到计算机资源的限制。 在通信过程中,各个连接能够良好的进行,不会互相干扰。