TCP/IP文键传输系统
一、设计原理
1.1服务器
1.需求分析
(1)自定义密钥数字,自定义端口,创建套接字字
(2)监听,断开监听
(3)显示接入的用户
(4)收文件
自动接受并将文件保存到当前的文档中,并将文件名、大小、发送者的IP存入目录中
(5)发文件
收到客户机请求后,自动发送文件,无需人工操作
2.逻辑设计
1.先监听。收到用户的连接后,将客户的信息存到客户目录中
2.收到客户的文件发送请求后,准备接收文件。收到文件后把文件信息存到文件目录中。文件存到当前文档中
3.收到客户的目录发送请求后,发送文件数量,循环发送文件目录中的文件信息。
4.收到客户的文件下载请求后,准备接收文件信息。收到文件信息后,找到相应文件发送过去。
3.类和关键函数的设计
-
Page1和Page2是用来存储文件信息表和客户信息表的两个对话框,在服务器对话框的Tab Control控件里。
-
MySocket就是类似于第十章的MySocket.
OnAccept()函数作用就是当连接了客户后,将客户信息载入到客户信息里。令客户端的choice为0,表示客户端刚连接上
3.,CClientSocket就是类似于第十章的NewSocket,主要用于存客户机信息。
choice(int)用来存储客户机的请求信息。0:客户端连接上了,准备发送密钥数字1:客户端发送文件 2:客户端请求文件目录 3:客户端请求发送文件。
Askingwfd (WIN32_FIND_DATA) 用来存储客户机需要下载的文件信息。
OnReceive() 先来接收客户机的请求信息choice 。客户端发送文件时,先接收文件的信息。然后分批接受存储。如果成功接收,就将信息存储到文件目录。请求文件目录时,就转到OnSend()。客户端请求文件下载时,就接收文件信息Askingwfd然后转到OnSend()
OnSend(),判断客户机的请求信息choice。客户端刚连接上,发送密钥数字。客户端请求文件目录,就发送文件数量,然后循环发送文件信息。
客户端请求下载文件时,根据收到的文件信息Askingwfd来找到文件并发送
1.2客户机
1.需求分析
1.输入服务器的IP地址和端口号后连接。
2.登录,登出
3.发送文件
4.登录后可以浏览服务器的文件信息
5.得到服务器文件信息后,可以选择下载。
2.逻辑设计
1.先连接,连接后收到密钥数字。
2.发送文件。先发送发送文件的请求,然后发送文件信息。最后发送文件
3.登录。如果密码和收到的密钥数字一样,则登录成功,然后登出、浏览文件、下载这些 按钮才可用,文件目录才可见。
4.浏览服务器文件。发送浏览服务器文件目录的请求后,准备接收文件数量,然后循环接收文件信息,并存储文件信息。把文件信息显示在目录中。
5.下载。从文件目录中选择文件后,发送文件下载请求。然后将对应的文件信息发送过去,然后准备接收文件。文件存储再当前目录。
3.类和关键函数的设计
FileListDlg是存储服务器文件信息的。LoginDlg是登陆对话框。MySocket,是CSocket的子类,类似于第十章的MySocket
1.C客户端Dlg
OnInitDialog()初始化,只有连接服务器按钮可用。套接字的choice(in)表示,服务种类。0:初始化,收到登陆密钥 1:发送文件 2:请求服务器文件目录 3:请求下载。
(按钮)连接服务器:OnBnClickedconnectbtn()。令套接字的choice为0,表示初始化。成功连接后。才可以使用浏览、发送、断开连接、登录按钮。
(按钮)登录:OnBnClickedLogin()。显示登录对话框,只用密码和服务器设置的密钥相同,才登陆成功,然后可以使用浏览服务器文件、下载、登出按钮。显示文件目录/同时登录按钮设置为不可用。
(按钮)下载:OnBnClickedDownload()。choice=3.当选择了文件目录的某一条后,才可以下载。
(按钮)登出:OnBnClickedLogin()。文件目录不可见。设置登录按钮可用,浏览服务器文件、发送、登出按钮不可用。
2.MySocket。和第十章的MySockey类似,客户端套接字
deFile(deque< WIN32_FIND_DATA>) :存储服务器发来的文件目录星系
choice(int):表示服务类型,上文提到了。
OnReceive:choice=0。表示初始化,接收服务器发来的密钥数字,用来判断登录密码的正确性。choice=2时,先清空当前的文件目录信息和目录显示,然后接收服务器发来的文件信息,并显示。choice=3时,接收服务器发来的文件。
二、设计步骤
2.1服务器
2.1.1. 初始化 C服务器Dlg::OnInitDialog()
初始化两个目录。文件目录和客户端目录
BOOL C服务器Dlg::OnInitDialog()
{
CDialogEx::OnInitDialog();
// 设置此对话框的图标。 当应用程序主窗口不是对话框时,框架将自动
// 执行此操作
SetIcon(m_hIcon, TRUE); // 设置大图标
SetIcon(m_hIcon, FALSE); // 设置小图标
// TODO: 在此添加额外的初始化代码
//为Tab Control增加两个页面
//----------------------初始化两个目录-------------------------------//
MyTab.InsertItem(0, _T("文件"));
MyTab.InsertItem(1, _T("客户端"));
m_pSocket = new MySocket;
//m_pSocket->ps = new CClientSocket;
//关联对话框,并且将IDCTABTEST控件设为父窗口
mPage1.Create(IDD_PAGE1, GetDlgItem(IDC_TAB1));
mPage2.Create(IDD_PAGE2, GetDlgItem(IDC_TAB1));
//获得IDC_TABTEST客户区大小
CRect rs;
MyTab.GetClientRect(&rs);
//调整子对话框在父窗口中的位置
rs.top += 30;
rs.bottom -= 60;
rs.left += 1;
rs.right -= 2;
//设置子对话框尺寸并移动到指定位置
mPage1.MoveWindow(&rs);
mPage2.MoveWindow(&rs);
//分别设置隐藏和显示
mPage1.ShowWindow(true);
mPage2.ShowWindow(false);
//设置默认的选项卡
MyTab.SetCurSel(0);
mPage2.m_ClientList.InsertColumn(0, _T("IP地址"), LVCFMT_LEFT,200);
mPage2.m_ClientList.InsertColumn(1, _T("端口"), LVCFMT_LEFT,150);
mPage1.m_FileList.InsertColumn(0, _T("文件名"), LVCFMT_LEFT, 100);
mPage1.m_FileList.InsertColumn(1, _T("文件大小"), LVCFMT_LEFT, 100);
mPage1.m_FileList.InsertColumn(2, _T("发送方IP地址"), LVCFMT_LEFT, 200);
//GetDlgItem(IDC_BtnConnect)->EnableWindow(false);
GetDlgItem(IDC_disBtn)->EnableWindow(false);
return TRUE; // 除非将焦点设置到控件,否则返回 TRUE
}
2.1.2. 监听按钮 C服务器Dlg::OnBnClickedBtnconnect()
1.创建套接字
编辑框输入,设置为值 iPort。但是由于文本框的刷新问题,文本框的值很难传递进来,所以我用的是直接去文本框内值的办法。IDC_EDIT1是编辑框的ID。
int i = GetDlgItemInt(IDC_EDIT1);
创建的CSocket子类MySocket。又MySocket* m_pSocket来连接套接字。本来是准备用第十章讲的CAsyncSocket来创建,后来看网上的教学视频是用的CSocket,查了一下好像是封装后的,更简单。
if (FALSE == m_pSocket->Create(i, SOCK_STREAM))
{
MessageBox(_T("套接字创立失败,换一个端口"));
return;
}
2.监听,监听失败
if (FALSE == m_pSocket->Listen())
{
MessageBox(_T("监听失败"));c++
return;
}
3.传入密钥数字。为了防止误操,设置密钥数字编辑框和监听按钮不可用。
KeyWord = GetDlgItemInt(IDC_KeyWordEdt);
GetDlgItem(IDC_KeyWordEdt)->EnableWindow(false);
GetDlgItem(IDC_BtnConnect)->EnableWindow(false);
GetDlgItem(IDC_disBtn)->EnableWindow(true);
2.1.3 断开监听 C服务器Dlg::OnBnClickeddisbtn()
断开监听。并设置密钥编辑框和监听按钮可用。
closesocket(sListen);
WSACleanup();c+
GetDlgItem(IDC_BtnConnect)->EnableWindow(true);c++
GetDlgItem(IDC_disBtn)->EnableWindow(false);
GetDlgItem(IDC_KeyWordEdt)->EnableWindow(true);
2.1.4连接上客户端 MySocket::OnReceive(int nErrorCode)
1.连接后,将对应客户端的choice初始化为0,准备发送密钥数字
ps = new CClientSocket;
Accept(*ps);
ps->GetPeerName(IpAddr,uPort);
ps->choice = 0;
ps->AsyncSelect(FD_WRITE);
2.记录客户端信息到客户目录里。
C服务器Dlg* pMainDlg = (C服务器Dlg*)AfxGetMainWnd();
pMainDlg->mPage2.m_ClientList.InsertItem(0, IpAddr);
CString str;
str.Format(_T("%d"), uPort);
pMainDlg->mPage2.m_ClientList.SetItemText(0,1, str);
pMainDlg->mPage2.m_ClientList.SetItemData(0,(DWORD)ps);
2.1.4 接收客户端的内容 CClientSocket::OnReceive(int nErrorCode)
1.接收客户端的请求
Receive(&choice, sizeof(choice));
2.choice==1。客户端请求上传文件到服务器
接收文件信息,并且在当前文件夹创造文件,准备接受。
if (choice == 1)//客户端上传文件
{
C服务器Dlg* pMainDlg = (C服务器Dlg*)AfxGetMainWnd();
WIN32_FIND_DATA wfd;
Receive(&wfd, sizeof(wfd),0);
CString str;
CFile file;
if (FALSE == file.Open(wfd.cFileName, CFile::modeCreate | CFile::modeWrite | CFile::typeBinary))
{
OutputDebugString(_T("文件打开失败"));
return;
}
循环接收文件内容,直到接收完成。当接受内容等于文件大小时,表示接受完成
DWORD dwReadCount = 0;
//循环收文件内容
while (dwReadCount < wfd.nFileSizeLow)
{
char* buffer = new char[1024];
int n;
n=Receive(buffer, 1023);
buffer[n] = '\0';
dwReadCount += n;
OutputDebugString(_T("2"));
file.Write(buffer, n);
delete[] buffer;
}
记录文件信息到文件目录内
pMainDlg->mPage1.m_FileList.InsertItem(0, wfd.cFileName);
str.Format(_T("%d"), wfd.nFileSizeLow);
pMainDlg->mPage1.m_FileList.SetItemText(0, 1, str);
pMainDlg->mPage1.m_FileList.SetItemText(0, 2, IpAddr);
}
3.choice==2.准备发送对应内容
else if (choice == 2)//客户端请求文件目录
{
AsyncSelect(FD_WRITE);
}
4.choice==3.接收客户端需要下载的文件信息Askingwfd,准备发送对应内容
else if (choice == 3)//客户端请求文件
{
Receive(&Askingwfd, sizeof(Askingwfd), 0);
AsyncSelect(FD_WRITE);
}
2.1.5 发送内容到客户端 CClientSocket::OnSend(int nErrorCode)
通过choice判断客户端的请求
choice为0,刚连接上客户端,发送密钥数字。准备接收服务请求
if (choice == 0)
{
C服务器Dlg* pMainDlg = (C服务器Dlg*)AfxGetMainWnd();
CString str;
int kWord = pMainDlg->KeyWord;
Send(&kWord, sizeof(kWord));
str.Format(_T("%d"), pMainDlg->KeyWord);
OutputDebugString(str);
AsyncSelect(FD_READ);
}
choice为2。客户端请求文件目录。先发送目录大小,然后循环找到对应的文件,发送文件信息。
if (choice == 2)//客户端请求文件目录
{
C服务器Dlg* pMainDlg = (C服务器Dlg*)AfxGetMainWnd();
//OutputDebugString(_T("有人请求目录"));
WIN32_FIND_DATA wfd;
int sum = pMainDlg->mPage1.m_FileList.GetItemCount();
Send(&sum, sizeof(sum));
for (int i = 0; i < sum; i++)
{
HANDLE hFinder = FindFirstFile(pMainDlg->mPage1.m_FileList.GetItemText(i, 0), &wfd);
FindClose(hFinder);
Send(&wfd, sizeof(wfd),0);
}
OutputDebugString(_T("发送完成"));
AsyncSelect(FD_READ);
}
choice为3。客户端请求文件下载。通过接收到的文件信息Askingwfd,找到对应文件并发送
if (choice == 3)
{
CFile file;
if (FALSE == file.Open(Askingwfd.cFileName, CFile::modeRead | CFile::typeBinary))
{
//MessageBox("文件打开失败");
return;
}
DWORD dwReadCount = 0;
char* buffer = new char[1024];
int n = 0;
while (dwReadCount < Askingwfd.nFileSizeLow)
{
memset(buffer, 0, sizeof(buffer));
n = file.Read(buffer, 1023);
buffer[n] = '\0';
Send(buffer, n);
dwReadCount += n;
}
delete[]buffer;
AsyncSelect(FD_READ);
}
2.2客户端
2.2.1初始化 C客户端Dlg::OnInitDialog()
设置按钮,只有连接按钮可用
GetDlgItem(IDC_connectBTN)->EnableWindow(true);
GetDlgItem(IDC_Send)->EnableWindow(false);
GetDlgItem(IDC_search)->EnableWindow(false);
GetDlgItem(IDC_Login)->EnableWindow(false);
GetDlgItem(IDC_List)->EnableWindow(false);
GetDlgItem(IDC_Download)->EnableWindow(false);
GetDlgItem(IDC_logout)->EnableWindow(false);
GetDlgItem(IDC_BtnDisC)->EnableWindow(false);
2.2.2连接 & 断开连接 C客户端Dlg::OnBnClickedconnectbtn() &C客户端Dlg::OnBnClickedBtndisc()
先初始化choice为0,表示准备接受服务器密钥。创建套接字并连接
int i = GetDlgItemInt(IDC_Port);
CString str;
GetDlgItemText(IDC_Addr, str);
m_pSocket = new MySocket;
m_pSocket->choice = 0;
choice = 0;
if (FALSE == m_pSocket->Create())
{
MessageBox(_T("套接字创建失败"));
return;
}
if (FALSE == m_pSocket->Connect(str,i))
{
MessageBox(_T("服务器连接失败"));
return;
}
MessageBox("成功连接服务器");
设置按钮属性。因为连接上了,所以可以上传文件和登陆了,也可以断开连接。
GetDlgItem(IDC_Send)->EnableWindow(true);
GetDlgItem(IDC_search)->EnableWindow(true);
GetDlgItem(IDC_Login)->EnableWindow(true);
GetDlgItem(IDC_BtnDisC)->EnableWindow(true);
GetDlgItem(IDC_connectBTN)->EnableWindow(false);
初始化文件目录,我的文件目录不小心建在m_pSocket里面的,但是显示没有问题。所以我也没有改回来。逻辑没有问题,就是和服务器的设计有点不统一而已。因为还没有登陆,文件目录不显示。
tab.InsertItem(0, _T("文件"));
m_pSocket->mPage.Create(IDD_contentDlg, GetDlgItem(1019));
//获得IDC_TABTEST客户区大小
CRect rs;
tab.GetClientRect(&rs);
//调整子对话框在父窗口中的位置
rs.top += 30;
rs.bottom -= 60;
rs.left += 1;
rs.right -= 2;
//设置子对话框尺寸并移动到指定位置
m_pSocket->mPage.MoveWindow(&rs);
//分别设置隐藏和显示
m_pSocket->mPage.ShowWindow(false);
//设置默认的选项卡
tab.SetCurSel(0);
m_pSocket->mPage.m_FileList.SetExtendedStyle(LVS_EX_FULLROWSELECT);
m_pSocket->mPage.m_FileList.InsertColumn(0, _T("文件名"), LVCFMT_LEFT, 200);
m_pSocket->mPage.m_FileList.InsertColumn(1, _T("文件大小"), LVCFMT_LEFT, 100);
断开连接。就是关一下套接字。然后把按钮还原成初始化的样子
void C客户端Dlg::OnBnClickedBtndisc()
{
// TODO: 在此添加控件通知处理程序代码
//MessageBox("断开啦");
GetDlgItem(IDC_connectBTN)->EnableWindow(true);
GetDlgItem(IDC_Send)->EnableWindow(false);
GetDlgItem(IDC_search)->EnableWindow(false);
GetDlgItem(IDC_Login)->EnableWindow(false);
GetDlgItem(IDC_List)->EnableWindow(false);
GetDlgItem(IDC_Download)->EnableWindow(false);
GetDlgItem(IDC_logout)->EnableWindow(false);
GetDlgItem(IDC_BtnDisC)->EnableWindow(false);
closesocket(sClient);
WSACleanup();
}
2.2.3浏览文件 &发送文件 C客户端Dlg::OnBnClickedsearch() & C客户端Dlg::OnBnClickedSend()
浏览主机里的文件,把路径保存到filePass (后来发现拼错了,但是问题不大)里面。
void C客户端Dlg::OnBnClickedsearch()
{
// TODO: 在此添加控件通知处理程序代码
CFileDialog dlg(TRUE);
if (IDOK == dlg.DoModal())
{
filePass = dlg.GetPathName();
UpdateData(FALSE);
}
}
通过filePass找到文件。先发送 发送文件的请求:choice==1 ,提醒服务器要接受文件了,然后发送文件信息
WIN32_FIND_DATA wfd;
HANDLE hFinder = FindFirstFile(filePass, &wfd);
FindClose(hFinder);
choice = 1;
m_pSocket->Send(&choice, sizeof(choice));
m_pSocket->Send(&wfd, sizeof(wfd));
先打开文件,然后用字节流的方式循环发送文件。
//发送文件内容
CFile file;
if (FALSE == file.Open(filePass, CFile::modeRead|CFile::typeBinary))
{
MessageBox("文件打开失败");
return;
}
DWORD dwReadCount = 0;
char* buffer = new char[1024];
int n = 0;
while (dwReadCount < wfd.nFileSizeLow)
{
memset(buffer, 0, sizeof(buffer));
n=file.Read(buffer, 1023);
buffer[n] = '\0';
m_pSocket->Send(buffer, n);
dwReadCount += n;
}
delete[]buffer;
MessageBox(_T("发布成功"));
2.2.4 登录 & 登出 C客户端Dlg::OnBnClickedLogin() & C客户端Dlg::OnBnClickedlogout()
登录。如果输入的数值等于之前服务器发送的密钥数字,则登陆成功。成功后显示文件目录,登出、浏览文件目录、下载按钮才可用,同时登录按钮不可用。
void C客户端Dlg::OnBnClickedLogin()
{
m_pLogin = new LoginDlg;
if (IDOK == m_pLogin->DoModal())
{
/*CString str;
str.Format(_T("% d"), KeyWord);
OutputDebugString(str);*/
if (m_pLogin->KeyWord == KeyWord)
{
ShowList();
GetDlgItem(IDC_List)->EnableWindow(true);
GetDlgItem(IDC_Download)->EnableWindow(true);
GetDlgItem(IDC_logout)->EnableWindow(true);
GetDlgItem(IDC_Login)->EnableWindow(false);
}
else
MessageBox("密钥错误哦");
}
}
void C客户端Dlg::ShowList()
{
//显示目录
m_pSocket->mPage.ShowWindow(true);
}
登出。就是隐藏文件目录,对按钮再设置一下
void C客户端Dlg::OnBnClickedlogout()
{
HideList();
GetDlgItem(IDC_List)->EnableWindow(false);
GetDlgItem(IDC_Download)->EnableWindow(false);
GetDlgItem(IDC_logout)->EnableWindow(false);
GetDlgItem(IDC_Login)->EnableWindow(true);
}
void C客户端Dlg::HideList()
{
m_pSocket->mPage.ShowWindow(false);
}
2.2.5 浏览服务器文件 C客户端Dlg::OnBnClickedList()
发送一下浏览的请求,choice=2表示浏览服务器的请求。然后套接字自动接收。会得到文件目录信息。接收在后面写。
void C客户端Dlg::OnBnClickedList()
{
choice = 2;
m_pSocket->choice = 2;
m_pSocket->Send(&choice, sizeof(choice);
}
2.2.6 下载文件 C客户端Dlg::OnBnClickedDownload()
选中文件,找到对应的文件信息。
POSITION ps;
int nIndex;
ps = m_pSocket->mPage.m_FileList.GetFirstSelectedItemPosition();
nIndex = m_pSocket->mPage.m_FileList.GetNextSelectedItem(ps);
//TODO:添加多选的操作。
if (nIndex == -1)
{
MessageBox("你还没有选择文件哦");
return;
}
设置服务类型Choice==3,表示请求下载文件。发送请求,然后再发送文件信息。套接字自动接收
choice = 3;
m_pSocket->choice = 3;
m_pSocket->Send(&choice, sizeof(choice));
m_pSocket->Send(&(m_pSocket->deFile.at(nIndex)), sizeof(m_pSocket->deFile.at(nIndex)));
2.2.7接收文件 MySocket::OnReceive(int nErrorCode)
通过Choice判断接收内容。
choice==1,接收密码。存到主对话框的KeyWord里面,用于登录时的判断
//得到密钥数字
if (choice == 0)
{
C客户端Dlg* pMainDlg=(C客户端Dlg*)AfxGetMainWnd();
int kWord;
Receive(&pMainDlg->KeyWord, sizeof(pMainDlg->KeyWord));
CString str;
str.Format(_T("% d"), pMainDlg->KeyWord);
OutputDebugString(str);
}
choice==2.接收目录信息。先初始化(清空当前的目录)。再得到文件数量,循环获得文件信息,信息存到deFile里面,再把信息插入到文件目录里
if (choice == 2)
{
//清空当前目录文件信息
while (!deFile.empty())
{
deFile.pop_back();
}
mPage.m_FileList.DeleteAllItems();
//得到文件总数
int sum;
Receive(&sum, sizeof(sum));
CString str;
WIN32_FIND_DATA wfd;
//循环得到文件信息
for (int i = 0; i < sum; i++)
{
//记录文件信息到deFile
Receive(&wfd, sizeof(wfd), 0);
deFile.push_front(wfd);
//显示文件信息到目录中
mPage.m_FileList.InsertItem(0, wfd.cFileName);
str.Format(_T("%d"), wfd.nFileSizeLow);
mPage.m_FileList.SetItemText(0, 1, str);
}
OutputDebugString("收到所有啦啦");
}
Choice==3,接收文件
if (choice == 3)
{
//循环接受文件
WIN32_FIND_DATA wfd;
Receive(&wfd, sizeof(wfd), 0);
CString str;
CFile file;
if (FALSE == file.Open(wfd.cFileName, CFile::modeCreate | CFile::modeWrite | CFile::typeBinary))
{
AfxMessageBox(_T("文件打开失败"));
return;
}
DWORD dwReadCount = 0;
while (dwReadCount < wfd.nFileSizeLow)
{
char* buffer = new char[1024];
int n;
n = Receive(buffer, 1023);
buffer[n] = '\0';
dwReadCount += n;
OutputDebugString(_T("2"));
file.Write(buffer, n);
delete[] buffer;
}
AfxMessageBox(_T("收到啦"));
}
三、实验结果及结果分析
3.1.监听
服务器监听,监听成功后,监听按钮不可用,密钥数字不可再编辑。
客户机和服务器deBug输出发送/得到的密钥
3.2.客户端连接
客户端请求连接。成功连接后,按钮属性发生变化
服务器的客户目录显示客户端信息
客户机和服务器deBug输出发送/得到的密钥
第十章的那个代码设置了OnTimer,所以就是循环连接。我的没有设置。有时候会连不上。就要重新连接。然后如果ip或者端口好出错了,它要等好久,然后返回没有连接上。
3.3浏览文件并发送
发送成功后显示发布成功,文件目录显示。
服务器在接受文件时是创造文件,如果有的话就直接接受。如果一个文件发了两次,目录会显示两个,但是其实文件只有一个。
3.4.登录
密码不一致,显示提示
登陆成功后可用其他的功能
3.5.浏览服务器文件
3.6.下载
没有选取文件显示提示
因为服务器对文件不进行操作,所以没有考虑文件不存在的问题。所以如果文件不存在的话,服务器那边显示提示,客户端这边可能会报错。
四、参考文献
b站的视频 https://www.bilibili.com/video/BV1Xt411K7fp?t=5903
还有一些函数和控件的用法都是随查随用的。很多很杂,不记得了。
这是我的代码
链接:https://pan.baidu.com/s/1hcC9CISOefLhbxhJE_i5_Q
提取码:xmrw