第一次接触MFC的Socket网络编程.
参考了:MFC网络编程——简单的服务器/客户端-CSDN论坛
这里用的编译器是:Visual Studio 2019
服务器开发:
- 新建一个MFC项目 项目名称:MFCServer
应用程序类型:选择基于对话框
2.在高级功能中,勾选Windows套接字
其余的默认就行。
3.在对话框中加入3个Static Text,2个Edit Control,1个Button,下表是各个控件的参数:
注:添加变量可以右键对应的控件,点击添加变量即可
4.添加事件处理函数,可通过右键对话框,点击类向导
在类向导中添加一个新的CServerSocket类,基类为CSocket;在虚函数中添加三个函数,OnAccept()、OnClose()、OnReceive();
点击确定即可。
5.编写CServerSocket类,首先在资源管理器中打开CServerSocket.h头文件
修改CServerSocket.h 头文件
#pragma once
#include <afxsock.h>
#include"MFCServerDlg.h" //主对话框头文件
class CMFCServerDlg; //主对话框类
class CServerSocket :public CSocket
{
public:
CMFCServerDlg* m_pDlg; //主对话框类指针对象
CServerSocket() {};
virtual ~CServerSocket() {};
virtual void OnAccept(int nErrorCode);
virtual void OnClose(int nErrorCode);
virtual void OnReceive(int nErrorCode);
};
接着修改源文件 CServerSocket.cpp
#include "pch.h"
#include "CServerSocket.h"
void CServerSocket::OnAccept(int nErrorCode)
{
// TODO: 在此添加专用代码和/或调用基类
m_pDlg->AddClient(); //添加上线用户
CSocket::OnAccept(nErrorCode);
}
void CServerSocket::OnClose(int nErrorCode)
{
// TODO: 在此添加专用代码和/或调用基类
m_pDlg->RemoveClient(this); // 删除下线用户
CSocket::OnClose(nErrorCode);
}
void CServerSocket::OnReceive(int nErrorCode)
{
// TODO: 在此添加专用代码和/或调用基类
m_pDlg->RecvData(this); // 接收数据
CSocket::OnReceive(nErrorCode);
}
6.在头文件MFCServerDlg.h 中添加服务器类CServerSocket.h 头文件,以及CServerSocket类声明
#include"CServerSocket.h"
class CServerSocket;
7.在CMFCServerDlg类中添加函数声明与变量声明
CServerSocket* listenSocket; // 用于打开服务器
CPtrList m_clientList; // 链表用于存储用户
bool m_connect; // 用于标记服务器状态
void AddClient(); // 增加用户,响应用户请求
void RemoveClient(CServerSocket* pSocket); // 移除下线的用户
void RecvData(CServerSocket* pSocket); // 获取数据
void UpdateEvent(CString str); // 更新事件日志
void SendMSG(CString str); // 发送消息给各个客户端
8.在源文件MFCServerDlg.cpp 中添加函数实现
先在资源视图中打开
资源视图可以从顶部菜单栏——视图中打开
双击按钮自动生成响应函数
9.编写OnBnClickedStartserver()函数实现
void CMFCServerDlg::OnBnClickedStartserver()
{
// TODO: 在此添加控件通知处理程序代码
if (m_connect)
{
delete listenSocket;
listenSocket = NULL;
m_connect = false;
SetDlgItemText(IDC_StartServer, _T("打开服务器"));
UpdateEvent(_T("系统关闭服务器."));
return;
}
listenSocket = new CServerSocket();
listenSocket->m_pDlg = this;
// 指定对话框为主对话框,不能少了这句
UpdateData(true);
if (!listenSocket->Create(m_port))// 创建服务器的套接字,IP地址默认本机IP
{
AfxMessageBox(_T("创建套接字错误!"));
listenSocket->Close();
return;
}
if (!listenSocket->Listen())
{
AfxMessageBox(_T("监听失败!"));
listenSocket->Close();
return;
}
m_connect = true;
SetDlgItemText(IDC_StartServer, _T("关闭服务器"));
UpdateEvent(_T("系统打开服务器."));
}
说明:本函数用于打开或关闭服务器,主要用到Create函数和Listen函数用于创建服务器和监听客户端。其中端口号从编辑框获取,应用程序的可用端口范围是1024-65535。
10.编写AddClient() 函数,用于增加用户,响应用户请求
void CMFCServerDlg::AddClient()
{
CServerSocket* pSocket = new CServerSocket;
pSocket->m_pDlg = this;
listenSocket->Accept(*pSocket);
pSocket->AsyncSelect(FD_READ | FD_WRITE | FD_CLOSE);
m_clientList.AddTail(pSocket);
m_userCount = m_clientList.GetCount();
UpdateData(false);
UpdateEvent(_T("用户连接服务器."));
SendMSG(_T("有一位用户加入"));
}
说明:本函数在CServerSocket类中的OnAccept消息中调用,用于响应用户连接服务器的请求,主要函数为Accept,当连接成功后,通过链表m_clientList保存新用户,更新日志,向所有用户发送提醒“有一位用户加入”。
11.编写RemoveClient函数,用于移除下线的用户
void CMFCServerDlg::RemoveClient(CServerSocket* pSocket)
{
POSITION nPos = m_clientList.GetHeadPosition();
POSITION nTmpPos = nPos;
UpdateData();
if (m_userCount == 0)//如果所有用户退出,将聊天记录清空
{
m_chatData = _T("");
}
while (nPos)
{
CServerSocket* pSockItem = (CServerSocket*)m_clientList.GetNext(nPos);
if (pSockItem->m_hSocket == pSocket->m_hSocket)
{
pSockItem->Close();
delete pSockItem;
m_clientList.RemoveAt(nTmpPos);
m_userCount = m_clientList.GetCount();
UpdateData(false);
UpdateEvent(_T("用户离开."));
return;
}
nTmpPos = nPos;
}
}
说明:本函数在CServerSocket类中的OnClose消息中调用,用到POSITION结构,查找存储用户中哪位用户下线了,将下线用户释放,从链表中删除,并更新日志。以及判断用户是否全部下线,如果全部用户下线则将m_chatData聊天数据清除。
12.编写RecvData函数,用于接收来着客户端的数据,并立刻将数据发送给全部客户端
void CMFCServerDlg::RecvData(CServerSocket* pSocket)
{
char* pData = NULL;
pData = new char[1024];
memset(pData, 0, sizeof(char) * 1024);
CString str;
if (pSocket->Receive(pData, 1024, 0) != SOCKET_ERROR)
{
str.Format(_T("%s"), pData);
m_chatData += str + _T("\r\n"); //将收到的数据加到m_chatData,并换行
SendMSG(m_chatData); // 将m_chatData转发给所有用户,包括发送数据的用户 }
delete[]pData;
pData = NULL;
}
说明:本函数在CServerSocket类中的OnReceive消息中调用,用于处理接收到的数据并将数据转发给所有用户,通过CSocket类的GetPeerName函数可以获取用户的IP和端口号。
13.编写UpdateEvent函数,用于更新事件日志
void CMFCServerDlg::UpdateEvent(CString str)
{
CString string;
CTime time = CTime::GetCurrentTime();// 获取系统当前时间
str += _T("\r\n"); // 用于换行显示日志
string = time.Format(_T("%Y/%m/%d %H:%M:%S ")) + str;// 格式化当前时间
int lastLine = m_eventLog.LineIndex(m_eventLog.GetLineCount() - 1);//获取编辑框最后一行索引
m_eventLog.SetSel(lastLine + 1, lastLine + 2, 0); //选择编辑框最后一行
m_eventLog.ReplaceSel(string); //替换所选那一行的内容
}
14.编写SendMSG函数,用于发送数据给客户端
void CMFCServerDlg::SendMSG(CString str)
{
char* pSend = (char*)str.GetBuffer(0);
int nlen = str.GetLength();
POSITION nPos = m_clientList.GetHeadPosition();
while (nPos)
{
CServerSocket*pTemp(CServerSocket*)m_clientList.GetNext(nPos);
pTemp->Send(pSend, nlen * 2);//长度一定要乘2,不然发送中文数据可能会乱码
}
}
最后可以在初始化的时候设置一个默认的端口号,这里默认设置6000
最后的成品
客户端开发:
- 新建MFC项目,项目名称为:MFCClient
- 勾选Windows套接字
- 加入控件
客户端的控件会多一点
需要用到5个编辑框,2个按钮,3个静态文本
ID | 描述文字 | 变量名 | 变量类型 | 备注 | |
编辑框1 | IDC_IPAddress | m_address | CString | ||
编辑框2 | IDC_UserName | m_userName | CString | ||
编辑框3 | IDC_PORT | m_port | UINT | 将数字(Number)设为True | |
编辑框4 | IDC_DataReceive | 将多行,垂直滚动设为True | |||
编辑框5 | IDC_DataSend | m_DataSend | CString | ||
按钮1 | IDC_connect | 连接服务器 | m_ConPC | CButton | |
按钮2 | IDC_Send | 发送 |
4.类向导添加新的类CClientSocket 添加虚函数OnReceive();与写服务器时的步骤差不多
- 编写 CClientSocket.h 头文件
#pragma once
#include <afxsock.h>
class CClientSocket : public CSocket
{
public:
CClientSocket();
virtual ~CClientSocket();
virtual void OnReceive(int nErrorCode);
};
2.编写源文件 CClientSocket.cpp 实现OnReceive()函数
#include "pch.h"
#include "CClientSocket.h"
#include "MFCClientDlg.h"
#include "MFCClient.h"
CClientSocket::CClientSocket() {}
CClientSocket::~CClientSocket() {}
void CClientSocket::OnReceive(int nErrorCode)
{
// TODO: 在此添加专用代码和/或调用基类
char* pData = NULL;
pData = new char[1024];
memset(pData, 0, sizeof(char) * 1024);
UCHAR leng = 0;
CString str;
leng = Receive(pData, 1024, 0);
str.Format(_T("% s"), pData);
// 在编辑框中显示接收到的数据
((CMFCClientDlg*)theApp.GetMainWnd())->SetDlgItemTextW(IDC_DataReceive, str);
delete[]pData;
pData = NULL;
CSocket::OnReceive(nErrorCode);
}
3.编写MFCClinentDlg.h 头文件,在里面加入头文件
#include"CClientSocket.h"
以及在类内部声明
bool m_connect; //连接状态
CClientSocket* pSock; // 客户端套接字指针对象
接下来的代码均在CMFCClientDlg.cpp 中编写
4.编写“打开服务器”按钮的响应函数(双击按钮生成)
void CMFCClientDlg::OnBnClickedconnect()
{
// TODO: 在此添加控件通知处理程序代码
if (m_connect) // 如果已经连接,则断开服务器
{
m_connect = false;
pSock->Close();
delete pSock;
pSock = NULL;
m_ConPC.SetWindowTextW(_T("连接服务器"));
GetDlgItem(IDC_IPAddress)->EnableWindow(TRUE);
GetDlgItem(IDC_PORT)->EnableWindow(TRUE);
GetDlgItem(IDC_Username)->EnableWindow(TRUE);
UpdateData(false);
return;
}
else // 未连接,则连接服务器
{
pSock = new CClientSocket();
if (!pSock->Create()) //创建套接字
{
AfxMessageBox(_T("创建套接字失败!"));
return;
}
}
UpdateData();
if (m_address.GetLength() == 0)
{
AfxMessageBox(_T("IP地址必须填写!"));
return;
}
if (!pSock->Connect(m_address, m_port)) //连接服务器
{
AfxMessageBox(_T("连接服务器失败!"));
return;
}
else
{
m_connect = true;
m_ConPC.SetWindowTextW(_T("断开服务器"));
//成功连接后将IP、端口、昵称 这三个编辑框控件状态设为只读
GetDlgItem(IDC_IPAddress)->EnableWindow(FALSE);
GetDlgItem(IDC_PORT)->EnableWindow(FALSE);
GetDlgItem(IDC_Username)->EnableWindow(FALSE);
UpdateData(false);
}
}
说明:点击连接服务器按钮后,本函数通过Create和Connect与服务器建立连接,并在连接成功后,将IP、端口、昵称 这三个编辑框控件状态设为只读,再次按下该按钮时,会断开连接
5.编写OnBnClickedSend()函数
void CMFCClientDlg::OnBnClickedSend()
{
// TODO: 在此添加控件通知处理程序代码
if (!m_connect)return; //未连接服务器则不执行
UpdateData(true); //获取界面数据
CString SendText = _T("");
SendText += m_userName + _T(":") + m_DataSend;
if (SendText != "")
{
char* pBuff = (char*)SendText.GetBuffer(0);
int nlen = SendText.GetLength();
pSock->Send(pBuff, nlen * 2);
}
GetDlgItem(IDC_DataSend)->SetWindowText(_T(""));
}
说明:该函数会将昵称+发送内容一并发给服务器
6.在初始化的时候设置IP地址的默认值,以及端口的默认参数
其实也可以不用,就是如果没有就得每次打开都得输入一次;
大家可以按下win+R ,输入cmd
在cmd中输入 ipconfig 查找自己的ip地址
7.防止在输入发送内容时,按下回车键导致程序关闭
可以在类向导中,添加PreTranslateMessage()函数(也是在虚函数中找)
点击确定
编写PreTranslateMessage()函数
BOOL CMFCClientDlg::PreTranslateMessage(MSG* pMsg)
{
// TODO: 在此添加专用代码和/或调用基类
switch (pMsg->wParam)
{
case WM_KEYDOWN:OnBnClickedSend();//回车按下后将消息发送出去
case VK_ESCAPE:
return true; break;
}
return CDialogEx::PreTranslateMessage(pMsg);
}
说明:回车发送这个功能还不太完善,按下回车后会发送两次,原因应该是按下回车会触发一次,松开回车会触发一次。暂时还没有比较好的解决方法。
最后最后,这个程序只能在同一局域网才能连接,如果是在两台或者多台电脑上运行,起服务器的那台电脑需要关闭Windows防火墙,不然客户端是连接不到服务器的