学习,c++有2个星期了。本来,本人是做php出身的。做php快2年了,最近身边多了很多高手。让自己对c开始感兴趣了,就开始学习c++了。首先接触的就是mfc。前几天,看到了一个博文,是有关,mfc网络编程的。可对方,的实例只能是多对多,出于兴趣,自己改写了下它的程序,实现了点对点的聊天。所以,本实例并非纯原创的。这个还请大家见谅,尤其是作者。我在他程序基础上,增加了1对1的聊天,同时还保留了群聊。而且,最关键的是,我增加了很多备注。很适合新手学习。。。本人也是新手,还请各位高手提出宝贵建议。。。先谢谢大家了。
如果要转载请注明原地址:http://blog.csdn.net/open520yin/article/details/8222279
实例下载地址:http://download.csdn.net/detail/open520yin/4808903(为了自己能有点下载积分,客户端和服务端一起打包5个积分不算贵吧。。呵呵。。。)
大家要是想看懂这个可能还需要先了解一下mfc的socket的一些基本使用规则我也有一篇博文写了
c++/MFC 极为简单的socket实例:http://blog.csdn.net/open520yin/article/details/8202465
MFC的CSocket编程,利用CSocket实现一个基于TCP实现一个QQ聊天程序。
///////////////////////////////////////////////////////////////////////// 服务端 start ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
先讲讲服务端,一切先从服务端开始:
首先就是要使用AfxSocketInit初始化winsocket,
//初始化winSock库,成功则返回非0否则返回0
WSAData wsData;
if(!AfxSocketInit(&wsData))
{
AfxMessageBox(_T("Socket 库初始化出错!"));
return false;
}
m_iSocket 是一个 CServerSocket*的 指针 ,CServerSocket类是一个我们自己的类我会在后面给出相应代码,他继承于CSocket类。
//创建服务器端Socket、采用TCP
m_iSocket = new CServerSocket();
if(!m_iSocket)
{
AfxMessageBox(_T("动态创建服务器套接字出错!"));
return false;
}
实例socket好了,就要创建套接字了。。这里的端口要和客户端连接的端口一致,不然就连接不上。而且,这个端口,要和服务器的其他软件端口不能冲突,怎么去判断冲突,可以自己谷歌一下,很简单的。我这里就直接写死了,这个端口一般不会被占用的。。
//端口使用8989
if(!m_iSocket->Create(8989))
{
AfxMessageBox(_T("创建套接字错误!"));
m_iSocket->Close();
return false;
}
创建好了就要,开始监听这个端口了。这个是一般的,socket必须建立的几个过程。。
if(!m_iSocket->Listen())
{
AfxMessageBox(_T("监听失败!"));
m_iSocket->Close();
return false;
}
然后重载ExitInstance,退出时对进行清理
int CNetChatServerApp::ExitInstance()
{
if(m_iSocket)
{
delete m_iSocket;
m_iSocket = NULL;
}
return CWinApp::ExitInstance();
}
我再去看看上面用到的CServerSocket类,这个是用来,服务端接收消息用的。开启了监听这里的OnAccept()方法就会一直被循环调用。这个方法其实是重写CSocket类的OnAccept()方法。只要socket开启了监听,OnAccept就会被循环调用,我那个简单的socket实例,是开启一个while进行死循环来达到这个目的。大家不要介意,我也是新手。这里OnAccept方法为什么能一直被循环执行,我到现在也没弄明白,如果有高手知道请告诉我下。但是我知道,这里就是如果服务器有收到消息就会调用这里,提示ClientSocket接受消息。
void CServerSocket::OnAccept(int nErrorCode)
{
//接受到一个连接请求
CClientSocket* theClientSock(0);
//初始化在初始化里把m_listSockets赋值到m_pList里
theClientSock = new CClientSocket(&m_listSockets);
if(!theClientSock)
{
AfxMessageBox(_T("内存不足,客户连接服务器失败!"));
return;
}
Accept(*theClientSock); //接受
//加入list中便于管理,这个很关键
m_listSockets.AddTail(theClientSock);
CSocket::OnAccept(nErrorCode);
}
OnAccept收到消息,就会实例CClientSocket类,这里其实主要是,服务端发送消息和接受消息的主要部分。也是服务端,最核心的部分。OnAccept收到消息后,就会通知CClientSocket来接受消息。注意了,CserverSocket是接收消息而CClientSocket是接收消息。接收,接受还是有区别的。这个关系我们要理解清楚。这个是我自己的理解,不时候是否有错误。还请高手赐教。。我学c++最多不过半个月,有些东西,都真是靠自己的理解。下面的接受消息OnReceive方法怎么调用的,我也有点模糊,这个方法好像是重写Socket的。就有数据来,他就会自动调用。m_listSockets.AddTail(theClientSock);这个很关键,m_listSockets是CPtrList类型,我对这个也还不太了解,经过我一些认识,这个是存放socket连接,成功一个就会加入这个,是一个链表。用来存放所有连接到服务器的socket连接的,这个后面会经常用到。
下面的HEADER是一个结构体,定义如下
typedef struct tagHeader{
int type ;//协议类型
int nContentLen; //将要发送内容的长度
char to_user[20];
char from_user[20];
}HEADER ,*LPHEADER;
这个结构体,要和客户端保持一致,不然我担心会有问题。就算没有问题,我估计转换也麻烦。尽量保持一直吧,这个也算是一种协议吧。客户端传输的时候,也传递这样的结构体。下面的方法,具体就看看备注吧。我在备注里讲解了。但是注意的是,我们客户端发送消息,是一次发送2个消息,先发送一个头部消息,这个头部消息是一个结构体,是服务端和客户端一种自定义的协议。这样的好处是,能节约资源,并且提前知道内容的长度进行申请内存空间。能先知道对应的消息类型,然后再进行转换和读取。这个头部接受好了,然后再去接受正式的数据。这个,可能你去看看。服务端可能会更容易了解。
void CClientSocket::OnReceive(int nErrorCode)
{
//有消息接收
//先得到信息头
HEADER head; //定义客户端发送的过来的一样的结构体
int nlen = sizeof HEADER; //计算结构体大小
char *pHead = NULL; //用于接受的结构体
pHead = new char[nlen]; //申请和结构体一样大小的内存空间
if(!pHead)
{
TRACE0("CClientSocket::OnReceive 内存不足!");
return;
}
memset(pHead,0, sizeof(char)*nlen ); //初始化
Receive(pHead,nlen); //收到内容,并赋值到pHead中,指定接受的空间大小
//以下是将接收大结构体进行强制转换成我们的结构体,
head.type = ((LPHEADER)pHead)->type;
head.nContentLen = ((LPHEADER)pHead)->nContentLen;
//head.to_user 是char[]类型,如果不进行初始化,可能会有乱码出现
memset(head.to_user,0,sizeof(head.to_user));
//讲接受的数据转换过后并赋值到head.to_user,以下同
strcpy(head.to_user,((LPHEADER)pHead)->to_user);
memset(head.from_user,0,sizeof(head.from_user));
strcpy(head.from_user,((LPHEADER)pHead)->from_user);
delete pHead; //使用完毕,指针变量的清除
pHead = NULL;
//再次接收,这次是接受正式数据内容
//这个就是,头部接受到的内容长度,这样能对应的申请内容空间
pHead = new char[head.nContentLen];
if(!pHead)
{
TRACE0("CClientSocket::OnRecive 内存不足!");
return;
}
//这里是一个验证,防止内存错误。和申请的空间进行对比,如果接受到数据大小
//和头部的内容大小不一样,则数据有问题,不给予接受
if( Receive(pHead, head.nContentLen)!=head.nContentLen)
{
AfxMessageBox(_T("接收数据有误!"));
delete pHead;
return;
}
////////////根据消息类型,处理数据,这个也是和客户端进行对应的。。下面的MSG_LOGOIN,MSG_SEND是定义好的常量,可以F12看看////////////////////
switch(head.type)
{
case MSG_LOGOIN: //登陆消息
OnLogoIN(pHead, head.nContentLen,head.from_user);
break;
case MSG_SEND: //发送消息
OnMSGTranslate(pHead, head.nContentLen,head.to_user,head.from_user);
break;
default : break;
}
delete pHead;
CSocket::OnReceive(nErrorCode);
}
这上面的注释应该非常清楚了。我建议要看懂这个程序,新手的多看几遍,尤其是先看看客服端怎么发送数据的,然后再来看服务端怎么接受的。这样可能更清楚点。
假如,上面的head.type 是 MSG_LOGOIN 也就是,客户端发送的是一个,登陆的消息过来。就会调用OnLogoIN
//登录
void CClientSocket::OnLogoIN(char* buff, int nlen,char from_user[20])
{
//对得接收到的用户信息进行验证
//... (为了简化这步省略)
//登录成功
CTime time;
time = CTime::GetCurrentTime(); //获取现在时间
CString strTime = time.Format("%Y-%m-%d %H:%M:%S ");
CString strTemp(buff);
strTime = strTime + strTemp + _T(" 登录...\r\n");
//记录日志
//将内容在NetChatServerDlg里的控件显示
((CNetChatServerDlg*)theApp.GetMainWnd())->DisplayLog(strTime);
m_strName = strTemp;
//更新服务列表,这个是更新服务器端的在线名单
//str1 返回的是所有用户字符串
CString str1 = this->UpdateServerLog();
//更新在线所有客服端,from_user 是为了不更新自己的在线列表,
//自己跟自己聊天没多大意思吧,其实更自己聊也问题不大,我只是为了学习,加了这么一个工程
this->UpdateAllUser(str1,from_user);
}
这里主要做的是接受的用户参数整理,并且尤这里去更新一些操作。
下面这句话,我虽然知道什么意思,就是调用CNetChatServerDlg的DisplayLog方法,这样调用,我以前还真没看到。有高手能解释下吗?
((CNetChatServerDlg*)theApp.GetMainWnd())->DisplayLog(strTime);
UpdateServerLog更新服务端在线列表UpdateAllUser更新用户在线列表
//跟新服务器在线名单
// 返回在线用户列表的String
CString CClientSocket::UpdateServerLog()
{
CString strUserInfo = _T("");
POSITION ps = m_pList->GetHeadPosition(); //返回的是链表头元素的位置
while(ps!=NULL)
{
CClientSocket* pTemp = (CClientSocket*)m_pList->GetNext(ps); //指向下一个元素
strUserInfo += pTemp->m_strName + _T("#"); //每一次用#结束
}
((CNetChatServerDlg*)theApp.GetMainWnd())->UpdateUserInfo(strUserInfo); //更新服务器显示
return strUserInfo;
}
这个很简单,唯一让我费事的是。m_pList这个变量,和个类型我很不熟悉。但是经过几天的探索这个程序,知道这个是存放所有socket连接的,发送消息到对应的人那里,也是靠这个连接、、、
他通过,while循环,把所有的用户准换成Cstring类型,并且用#隔开,方便下一次读取在UpdateUserInfo就是更新,界面显示。UpdateUserInfo方法,在代码里注释很清楚,我这里就不多说了。
void CNetChatServerDlg::UpdateUserInfo( CString strUserInfo) const
{
CString strTemp;
//CStringArray strArray;
CListBox* pList = (CListBox*)GetDlgItem(IDC_LT_ONLINE);
pList->ResetContent(); //清除所有的数据
while(!strUserInfo.IsEmpty())
{
int n = strUserInfo.Find('#'); //查找第一次出现#的位置,读取第一次数据
strTemp = strUserInfo.Left(n);
//strArray.Add(strTemp);
pList->AddString(strTemp); //加入数据
strUserInfo = strUserInfo.Right(strUserInfo.GetLength()-n-1); //减去第一个的位置
}
}
而且,这次发送的主要内容是,所有用户的字符串。
//跟新所有在线用户
void CClientSocket::UpdateAllUser(CString strUserInfo,char from_user[20])
{
HEADER _head;
_head.type = MSG_UPDATE;
_head.nContentLen = strUserInfo.GetLength()+1;
memset(_head.from_user, 0, sizeof(_head.from_user));
strcpy(_head.from_user,from_user);
char *pSend = new char[_head.nContentLen];
memset(pSend, 0, _head.nContentLen*sizeof(char));
//因为我用的是vs2010 所以是用unicode字符,转换的时候很多
//网站上转换方法不行,必须用WideCharToMultiByte准换
if( !WChar2MByte(strUserInfo.GetBuffer(0), pSend, _head.nContentLen))
{
AfxMessageBox(_T("字符转换失败"));
delete pSend;
return;
}
POSITION ps = m_pList->GetHeadPosition(); //循环对客户端发送消息
while(ps!=NULL)
{
CClientSocket* pTemp = (CClientSocket*)m_pList->GetNext(ps);
//发送协议头
pTemp->Send((char*)&_head, sizeof(_head));
pTemp->Send(pSend,_head.nContentLen );//发送主要内容
}
delete pSend;
}
这里又要回到 CClientSocket::OnReceive 这里接收到客户端的头部,的head.type如果是MSG_SEND那么就是,服务端接收到一个,客户端发送给别人的聊天消息。
这个switch就是CClientSocket::OnReceive里的
switch(head.type)
{
case MSG_LOGOIN: //登陆消息
OnLogoIN(pHead, head.nContentLen,head.from_user);
break;
case MSG_SEND: //发送消息
OnMSGTranslate(pHead, head.nContentLen,head.to_user,head.from_user);
break;
default : break;
}
//转发消息
void CClientSocket::OnMSGTranslate(char* buff, int nlen,char to_user[20],char from_user[20])
{
//建立头部信息,准备发送
HEADER head;
head.type = MSG_SEND;
head.nContentLen = nlen;
strcpy(head.to_user,to_user);
strcpy(head.from_user,from_user);
POSITION ps = m_pList->GetHeadPosition(); //取得,所有用户的队列
CString str(buff);
int i = strcmp(head.to_user,"群聊");
while(ps!=NULL)
{
CClientSocket* pTemp = (CClientSocket*)m_pList->GetNext(ps);
//只发送2个人, 一个是发送聊天消息的人和接收聊天消息的人。
//如果,接收聊天消息的人是“群聊”那么就发送所有用户,实现群聊和一对一关键就在于此
if(pTemp->m_strName==head.to_user || pTemp->m_strName==head.from_user || i==0 )
{
pTemp->Send(&head,sizeof(HEADER)); //先发送头部
pTemp->Send(buff, nlen); //然后发布内容
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////客户端 start////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
客户端在最开始的工作和服务端差不多,就是建立socket连接。但是和服务端相比就没那么复杂了。
//初始化Socket
if(!AfxSocketInit())
{
AfxMessageBox(_T("初始化Socket库失败!"));
return false;
}
m_pSocket = new CClientSocket();
if(!m_pSocket)
{
AfxMessageBox(_T("内存不足!"));
return false;
}
if(!m_pSocket->Create())
{
AfxMessageBox(_T("创建套接字失败!"));
return false;
}
void CLogoInDlg::OnBnClickedBtnLogoin()
{
//登录
UpdateData();
if(m_strUser.IsEmpty())
{
AfxMessageBox(_T("用户名不能为空!"));
return;
}
if(m_dwIP==0)
{
AfxMessageBox(_T("无效IP地址"));
return;
}
CClientSocket* pSock = theApp.GetMainSocket();
IN_ADDR addr ;
addr.S_un.S_addr = htonl(m_dwIP);
//inet_ntoa返回一个char *,而这个char *的空间是在inet_ntoa里面静态分配
CString strIP(inet_ntoa(addr));
//开始只是创建了,并没有连接,这里连接socket,这个8989端口要和服务端监听的端口一直,否则监听不到的。
if(!pSock->Connect(strIP.GetBuffer(0),8989))
{
AfxMessageBox(_T("连接服务器失败!"));
return ;
}
CString Cm_strUser = m_strUser;
char from_user[20];
memset(from_user,0,sizeof(from_user));
WideCharToMultiByte(CP_OEMCP,0,(LPCTSTR)m_strUser,-1,from_user,260,0,false);
pSock->m_strUserName = m_strUser; //将用户名字传递过去,用于第二个对话框读取
char* pBuff = new char[m_strUser.GetLength()+1];
memset(pBuff, 0, m_strUser.GetLength()); //开辟一个,存储用户名的内存空间
if(WChar2MByte(m_strUser.GetBuffer(0), pBuff, m_strUser.GetLength()+1))
//头部空间,和头部长度 用户名 用户名长度加1 发送者用户名
pSock->LogoIn(pBuff, m_strUser.GetLength()+1,from_user); delete pBuff;CDialogEx::OnCancel();}
上面是把,你登陆的用户名发送给服务端。。先调用CClientSocket的Logoin,
CClientSocket* pSock = theApp.GetMainSocket();
这句话,又出现了。大概知道是那个意思,但是不知道这个属于什么。注意看这个头部协议,一定要和服务端对应。
//用户登陆
BOOL CClientSocket::LogoIn(LPSTR lpBuff, int nlen,char from_user[20])
{
HEADER _head;
_head.type = MSG_LOGOIN; //头部类型
_head.nContentLen = nlen; //长度
memset(_head.to_user,0,20);
memset(_head.from_user,0,20);
strcpy(_head.from_user,from_user);
//_head.to_user = "";
int _nSnd= 0;
if((_nSnd = Send((char*)&_head, sizeof(_head)))==SOCKET_ERROR) //将头部发送过去
return false;
if((_nSnd = Send(lpBuff, nlen))==SOCKET_ERROR) //头部内存空间,和长度发送过去
return false;
return TRUE;
}
CLogoInDlg* pLogoinDlg;
pLogoinDlg = new CLogoInDlg();
CString m_strUser;
if(pLogoinDlg->DoModal()==IDOK)
{
//不登录
delete pLogoinDlg;
m_pSocket->Close();
return false;
}
else
{
m_strUser = pLogoinDlg->m_strUser; //读取用户名
delete pLogoinDlg;
}
CNetChatClientDlg dlg;
//将用户名传入下一个对话框
dlg.m_caption = m_strUser + _T(" 的聊天对话框");
m_pMainWnd = &dlg;
INT_PTR nResponse = dlg.DoModal();
点击登陆之后,先读取用户名,然后弹出我们主窗口。并讲用户名传入其中。
在CNetChatClientDlg 里 的OnInitDialog方法中,设置弹出框的标题。
SetWindowText(m_caption);
在来看看CClientSocket,这个建立以后,只要有消息过来,就会掉用我们重载的OnReceive方法,为什么,我也不知道。这里还是我的模糊地带,有清楚的高手能告诉我一下吗?我只知道是这样。
时间过去那么久了。我们登陆了,从上面的服务器端代码可以看出来,我们登陆后,服务器端会发送消息给我们。告诉我们什么用户上线了,要更新在线列表,先来看看重载的OnReceive方法,这个是客户端最核心的部分
void CClientSocket::OnReceive(int nErrorCode)
{
//首先接受head头
HEADER head ;
char* pHead = NULL;
pHead = new char[sizeof(head)];
memset(pHead, 0, sizeof(head));
Receive(pHead, sizeof(head)); //接受并赋值到pHead里
//强制转换
head.type =((LPHEADER)pHead)->type;
head.nContentLen = ((LPHEADER)pHead)->nContentLen;
strcpy(head.from_user,((LPHEADER)pHead)->from_user);
delete pHead;
pHead = NULL;
char* pBuff = NULL;
pBuff = new char[head.nContentLen];
if(!pBuff)
{
AfxMessageBox(_T("内存不足!"));
return;
}
memset(pBuff, 0 , sizeof(char)*head.nContentLen);
//接受主要参数,如果和头部定义的长度不一样,那就是有问题
if(head.nContentLen!=Receive(pBuff, head.nContentLen))
{
AfxMessageBox(_T("收到数据有误!"));
delete pBuff;
return;
}
CString strText(pBuff);
//这里和服务端类似,根据head.type判断是什么消息
switch(head.type)
{
case MSG_UPDATE:
{
CString strText(pBuff);
((CNetChatClientDlg*)(AfxGetApp()->GetMainWnd()))->UpdateUserInfo(strText);
}
break;
case MSG_SEND:
{
//显示接收到的消息
CString str(pBuff);
((CNetChatClientDlg*)(AfxGetApp()->GetMainWnd()))->UpdateText(str);
break;
}
default: break;
}
delete pBuff;
CSocket::OnReceive(nErrorCode);
}
这里和服务器端类似,如果服务端懂了。这里应该很容易明白。刚刚我们登陆了,,这次服务器端应该发送的是一个head.type为 MSG_UPDATE的。不信你看看服务端的CClientSocket::UpdateAllUser方法定义的head.type。
把接受到的数据,传递到CNetChatClientDlg::UpdateUserInfo进行更新界面
void CNetChatClientDlg::UpdateUserInfo(CString strInfo) //显示所有用户
{
CString strTmp;
CListBox* pBox = (CListBox*)GetDlgItem(IDC_LB_ONLINE);
pBox->ResetContent();//清除所有的数据
//获取自己的用户名,如果是自己的则不需要在自己的列表里显示
CString m_strUserName = theApp.GetMainSocket()->m_strUserName;
//发送消息
void CNetChatClientDlg::OnBnClickedBtnSend()
{
//发送消息
UpdateData();
if(m_strSend.IsEmpty())
{
AfxMessageBox(_T("发送类容不能为空!"));
return ;
}
//获取选中内容,并赋值to_user,下面长度加1上面有讲
CListBox* pList = (CListBox*)GetDlgItem(IDC_LB_ONLINE);
CString tep(_T(""));
INT nIndex = 0 ;
nIndex = pList->GetCurSel();
if(LB_ERR == nIndex)
{
AfxMessageBox(_T("请选择聊天对象!"));
return ;
}
pList->GetText( nIndex, tep ) ;
char* to_user = new char[tep.GetLength()*2+1];
memset(to_user, 0, tep.GetLength()*2+1);
WChar2MByte(tep.GetBuffer(0), to_user, tep.GetLength()*2+1);
CString m_strUserName = theApp.GetMainSocket()->m_strUserName;
char from_user[20];
memset(from_user,0,sizeof(from_user));
WChar2MByte(m_strUserName.GetBuffer(0), from_user, m_strUserName.GetLength()*2);
CString temp;
CTime time = CTime::GetCurrentTime();
temp = time.Format("%H:%M:%S");
//姓名 +_T("\n\t") 时间
m_strSend = theApp.GetMainSocket()->m_strUserName+_T(" 发送给 ") + to_user + _T(" ") + temp +_T("\r\n ") + m_strSend +_T("\r\n");
char* pBuff = new char[m_strSend.GetLength()*2];
memset(pBuff, 0, m_strSend.GetLength()*2);
WChar2MByte(m_strSend.GetBuffer(0), pBuff, m_strSend.GetLength()*2);
//发送
theApp.GetMainSocket()->SendMSG(pBuff, m_strSend.GetLength()*2,to_user,from_user);
delete pBuff;
m_strSend.Empty();
UpdateData(0);
}
这个过程,其实主要是准备数据,真正的发送在CClientSocket::SendMSG里。这个里面,定义一下头部,然后分2次发送。服务端的接受,你们可以再去看看
BOOL CClientSocket::SendMSG(LPSTR lpBuff, int nlen,char to_user[20],char from_user[20])
{
//生成协议头
HEADER head;
head.type = MSG_SEND;
head.nContentLen = nlen;
strcpy(head.to_user,to_user);
strcpy(head.from_user,from_user);
int i =Send(&head, sizeof(HEADER));
if(i==SOCKET_ERROR)
{
AfxMessageBox(_T("发送错误!"));
return FALSE;
};
if(Send(lpBuff, nlen)==SOCKET_ERROR)
{
AfxMessageBox(_T("发送错误!"));
return FALSE;
};
return TRUE;
}
服务端操作过来,,又到了。
CClientSocket::OnReceive来接受,同样,还是看head.type这次服务端发送的是MSG_SEND了。这次就更简单了。直接显示接受的内容就可以了。。。如果是MSG_SEND
case MSG_SEND:
{
//显示接收到的消息
CString str(pBuff);
((CNetChatClientDlg*)(AfxGetApp()->GetMainWnd()))->UpdateText(str);
break;
}
void CNetChatClientDlg::UpdateText(CString &strText)
{
((CEdit*)GetDlgItem(IDC_ET_TEXT))->ReplaceSel(strText);
}
而且,我还有一个奇怪问题,不是知道是我系统问题,还是怎么了。。。。我是win7系统,我在本机运行了一个服务端,多个客户端,这样,有时候会出现,有的客户端,只能发消息,不能收,有的只能收不能发。
尤其是通过vs运行出来的实例。。。如果不通过vs运行出来的,还没什么什么。我在虚拟机了,运行多个实例就没有问题了。不管多少都ok。我担心是不是vs运行出来的实例,占用的端口有问题。。。还是其他什么问题。
看下你们的测试,是否也有这样的问题。所以,建议你们,vs调试的时候,就出一个实例,其他的实例,在虚拟机里出来。。。。。。本人新手,如果写的不好,还请多给建议、。。。。。