资源描述
FTP服务器与客户端设计与开发
详细设计
程序包括5个主要功能:
1. 服务器的运行:启动和停止FTP服务
2. 用户管理:添加用户,删除用户和设置用户权限
3. 服务器配置:设置服务器开放端口,最大连接数等
4. 运行统计:统计当前服务器运行时期上传下载的流量等等
5. 安全设置:允许连接服务器的IP列表,以及禁止访问的IP
服务器的运行模块
功能:负责FTP服务器的运行。
使用类:CFTPServer类,CApplicationDlg类,CListenSocket类,CConnectThread类,CConnectSocket类
各种类的功能:
1. CFTPServer类:是CWnd的子类,作为程序的顶层类,负责实现或者调用各个成员函数
2. CApplicationDlg类:CDialog类的子类,实现程序主窗口。
3. CListenSocket类:负责监听FTP客户端连接,并实现有效连接
4. CConnectThread类:负责实现并保证多个连接的有效性。
5. CConnectSocket类:实现FTP命令的解析,数据的发送和接收
CFTPServer类
作为服务器的顶层类,实现服务器开始运行时的所有成员函数
申明如下:
class CFTPServer : public CWnd
{
friend CConnectSocket;//CConnectSocket作为其友元类,可以访问内部私有数据成员
public:
void SetGoodbyeMessage(LPCTSTR lpszText);//发送退出信息
void SetWelcomeMessage(LPCTSTR lpszText);//发送欢迎信息
void SetTimeout(int nValue);//设置暂停时间
void SetPort(int nValue);//设置端口
void SetMaxUsers(int nValue);//设置最大连接数
void SetStatisticsInterval(int nValue);//统计时间间隔
BOOL IsActive();//是否有效
void Stop();
BOOL Start();
CFTPServer();
virtual ~CFTPServer();
CUserManager m_UserManager;//用户管理对象
CSecurityManager m_SecurityManager;//安全策略
CFTPServer类
最主要的成员函数是start()和stop(),分别负责ftp服务器的开始运行和结束运行
函数声明如下:
/********************************************************************/
/* */
/* Function name : Start */
/* Description : Start listining on port 21 and accept new */
/* connections. */
/* */
/********************************************************************/
BOOL CFTPServer::Start()
{
if (m_bRunning)
return FALSE;//如果运行,返回错误标志
// create dummy window for message routing
if (!CWnd::CreateEx(0, AfxRegisterWndClass(0), "FTP Server Notification Sink", WS_POPUP, 0,0,0,0, NULL, 0))
{
AddTraceLine(0, "Failed to create notification window.");
return FALSE;
}
// 开始创建socket
if (m_ListenSocket.Create(m_nPort))
{
// start listening
if (m_ListenSocket.Listen())
{
m_ListenSocket.m_pWndServer = this;
m_bRunning = TRUE;
SetTimer(1, m_nStatisticsInterval, NULL);
AddTraceLine(0, "FTP Server started on port %d.", m_nPort);
return TRUE;
}
}
AddTraceLine(0, "FTP Server failed to listen on port %d.", m_nPort);
// destroy notification window
if (IsWindow(m_hWnd))
DestroyWindow();
m_hWnd = NULL;
return FALSE;
}
/********************************************************************/
/* */
/* Function name : Stop */
/* Description : Stop FTP server. */
/* */
/********************************************************************/
void CFTPServer::Stop()
{
if (!m_bRunning)
return;
// stop statistics timer
KillTimer(1);
m_bRunning = FALSE;
m_ListenSocket.Close();
CConnectThread* pThread = NULL;
// close all running threads
do
{
m_CriticalSection.Lock();
POSITION pos = m_ThreadList.GetHeadPosition();
if (pos != NULL)
{
pThread = (CConnectThread *)m_ThreadList.GetAt(pos);
m_CriticalSection.Unlock();
// save thread members
int nThreadID = pThread->m_nThreadID;
HANDLE hThread = pThread->m_hThread;
AddTraceLine(0, "[%d] Shutting down thread...", nThreadID);
// tell thread to stop
pThread->SetThreadPriority(THREAD_PRIORITY_HIGHEST);
pThread->PostThreadMessage(WM_QUIT,0,0);
// wait for thread to end, while keeping the messages pumping (max 5 seconds)
if (WaitWithMessageLoop(hThread, 5000) == FALSE)
{
// thread doesn't want to stopped
AddTraceLine(0, "[%d] Problem while killing thread.", nThreadID);
// don't try again, so remove
m_CriticalSection.Lock();
POSITION rmPos = m_ThreadList.Find(pThread);
if (rmPos != NULL)
m_ThreadList.RemoveAt(rmPos);
m_CriticalSection.Unlock();
}
else
{
AddTraceLine(0, "[%d] Thread successfully stopped.", nThreadID);
}
}
else
{
m_CriticalSection.Unlock();
pThread = NULL;
}
}
while (pThread != NULL);
AddTraceLine(0, "FTP Server stopped.");
if (IsWindow(m_hWnd))
DestroyWindow();
m_hWnd = NULL;
}
CListenSocket类
用于监听每个客户的连接,CListenSocket类是CAsyncSocket的子类,其成员函数listen监听来自客户端的连接,当监听到可以接收的socket的时候通过OnAccept函数准备创建有效连接的进程。
函数如下:
void CListenSocket::OnAccept(int nErrorCode)
{
// New connection is being established
CSocket sockit;
// Accept the connection using a temp CSocket object.
Accept(sockit);
// Create a thread to handle the connection. The thread is created suspended so that we can
// set variables in CConnectThread before it starts executing.
CConnectThread* pThread = (CConnectThread*)AfxBeginThread(RUNTIME_CLASS(CConnectThread), THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED);
if (!pThread)
{
sockit.Close();
TRACE("Could not create thread\n");
return;
}
CFTPServer *pWnd = (CFTPServer *)m_pWndServer;
// since everything is successful, add the thread to our list
pWnd->m_CriticalSection.Lock();
pWnd->m_ThreadList.AddTail(pThread);
pWnd->m_CriticalSection.Unlock();
// save pointer
pThread->m_pWndServer = m_pWndServer;
// Pass the socket to the thread by passing the socket handle. You cannot pass
// a CSocket object across threads.
pThread->m_hSocket = sockit.Detach();
// Now start the thread.
pThread->ResumeThread();
}
CConnectThread类
CConnectThread类负责为每个有效进程创建一个线程,每个进程完成数据传输的所有任务,穿件县城后通过InitInstance完成线程的初始化
BOOL CConnectThread::InitInstance()
{
try
{
// Attach the socket handle to a CSocket object.
// This makes sure that the socket notifications are sent to this thread.
m_ConnectSocket.Attach(m_hSocket);
m_ConnectSocket.m_pThread = this;
CString strIPAddress;
UINT nPort;
m_ConnectSocket.GetPeerName(strIPAddress, nPort);
// notify server that there's a new connection
m_pWndServer->SendMessage(WM_THREADSTART, (WPARAM)this, 0);
if (((CFTPServer *)m_pWndServer)->CheckMaxUsers())
{
m_ConnectSocket.SendResponse("421 Too many users are connected, please try again later.");
PostThreadMessage(WM_QUIT,0,0);
}
else
if (!((CFTPServer *)m_pWndServer)->IsIPAddressAllowed(strIPAddress))
{
m_ConnectSocket.SendResponse("421 Access denied, IP address was rejected by the server.");
PostThreadMessage(WM_QUIT,0,0);
}
else
{
// send welcome message to client
CString strText = ((CFTPServer *)m_pWndServer)->GetWelcomeMessage();
m_ConnectSocket.SendResponse("220 " + strText);
m_nTimerID = ::SetTimer(NULL, 0, 1000, TimerProc);
}
}
catch(CException *e)
{
e->Delete();
}
return TRUE;
}
线程结束以后,通过ExitInstance函数实现资源的释放代码如下:
int CConnectThread::ExitInstance()
{
CFTPServer *pWnd = (CFTPServer *)m_pWndServer;
try
{
pWnd->m_CriticalSection.Lock();
// delete this thread from the linked list
POSITION pos = pWnd->m_ThreadList.Find(this);
if(pos != NULL)
{
pWnd->m_ThreadList.RemoveAt(pos);
}
pWnd->m_CriticalSection.Unlock();
// notify service main loop
pWnd->SendMessage(WM_THREADCLOSE, (WPARAM)this, 0);
}
catch(CException *e)
{
pWnd->m_CriticalSection.Unlock();
e->Delete();
}
return CWinThread::ExitInstance();
}
为了了解传输过程中接收和发送的字节数,使用IncReceivedBytes和IncSentBytes来计算。这两个函数在CConnectSocket类中调用,代码如下:
void CConnectThread::IncSentBytes(int nBytes)
{
m_LastDataTransferTime = CTime::GetCurrentTime();
m_nSentBytes += nBytes;
// notify server class
m_pWndServer->PostMessage(WM_THREADMSG, (WPARAM)0, (LPARAM)nBytes);
}
void CConnectThread::IncReceivedBytes(int nBytes)
{
m_LastDataTransferTime = CTime::GetCurrentTime();
m_nReceivedBytes += nBytes;
// notify server class
m_pWndServer->PostMessage(WM_THREADMSG, (WPARAM)1, (LPARAM)nBytes);
}
CConnectSocket类
每个线程都是通过一个CConnectSocket对象m_ConnectSocket来完成数据的接受和发送。当线程创建成功以后,m_ConnectSocket对象通过OnReceive函数获得数据,然后利用ParseCommand函数来解析其中FTP命令
void CConnectSocket::OnReceive(int nErrorCode)
{
TCHAR buff[BUFFERSIZE];
int nRead = Receive(buff, BUFFERSIZE);
switch (nRead)
{
case 0:
Close();
break;
case SOCKET_ERROR:
if (GetLastError() != WSAEWOULDBLOCK)
{
TCHAR szError[256];
wsprintf(szError, "OnReceive error: %d", GetLastError());
AfxMessageBox (szError);
}
break;
default:
if (nRead != SOCKET_ERROR && nRead != 0)
{
((CConnectThread *)AfxGetThread())->IncReceivedBytes(nRead);
// terminate the string
buff[nRead] = 0;
m_RxBuffer += CString(buff);
GetRxLine();
}
break;
}
CSocket::OnReceive(nErrorCode);
}
ParseCommand函数
是当前程序最重要的一个部分,它根据客户端提交的各种命令进行相应的操作代码如下
void CConnectSocket::ParseCommand()
{
static CFTPCommand commandList[] =
{
{TOK_USER, "USER", TRUE},
{TOK_PASS, "PASS", TRUE},
{TOK_CWD, "CWD", TRUE},
{TOK_PWD, "PWD", FALSE},
{TOK_PORT, "PORT", TRUE},
{TOK_PASV, "PASV", FALSE},
{TOK_TYPE, "TYPE", TRUE},
{TOK_LIST, "LIST", FALSE},
{TOK_REST, "REST", TRUE},
{TOK_CDUP, "CDUP", FALSE},
{TOK_RETR, "RETR", TRUE},
{TOK_STOR, "STOR", TRUE},
{TOK_SIZE, "SIZE", TRUE},
{TOK_DELE, "DELE", TRUE},
{TOK_RMD, "RMD", TRUE},
{TOK_MKD, "MKD", TRUE},
{TOK_RNFR, "RNFR", TRUE},
{TOK_RNTO, "RNTO", TRUE},
{TOK_ABOR, "ABOR", FALSE},
{TOK_SYST, "SYST", FALSE},
{TOK_NOOP, "NOOP", FALSE},
{TOK_BYE, "BYE", FALSE},
{TOK_QUIT, "QUIT", FALSE},
{TOK_ERROR, "", FALSE},
};
// parse command
CString strCommand, strArguments;
if (!GetRxCommand(strCommand, strArguments))
{
return;
}
int nCommand;
//查找命令
for (nCommand = TOK_USER; nCommand < TOK_ERROR; nCommand++)
{
// found command ?
if (strCommand == commandList[nCommand].m_pszName)
{
// did we expect an argument ?
if (commandList[nCommand].m_bHasArguments && (strArguments == ""))
{
SendResponse("501 Syntax error");
return;
}
break;
}
}
if (nCommand == TOK_ERROR)
{
// command is not in our list
SendResponse("500 Syntax error, command unrecognized.");
return;
}
// no commands are excepted before successfull logged on
if (nCommand > TOK_PASS && !m_bLoggedon)
{
SendResponse("530 Please log in with USER and PASS first.");
return;
}
// proces command
switch(nCommand)
{
// specify username
case TOK_USER:
{
strArguments.MakeLower();
m_bLoggedon = FALSE;
m_strUserName = strArguments;
CString strPeerAddress;
UINT nPeerPort;
GetPeerName(strPeerAddress, nPeerPort);
// tell FTP server a new user has connected
CConnectThread *pThread = (CConnectThread *)m_pThread;
((CFTPServer *)pThread->m_pWndServer)->m_pEventSink->OnFTPUserConnected(m_pThread->m_nThreadID, m_strUserName, strPeerAddress);
SendResponse("331 Password required for " + strArguments);
}
break;
// specify password
case TOK_PASS:
{
// already logged on ?
if (m_bLoggedon)
{
SendResponse("503 Bad sequence of commands.");
}
else
{
// check user and password
CUser user;
if (theServer.m_UserManager.CheckUser(m_strUserName, strArguments, user))
{
//设置用户主目录
m_strCurrentDir = "/";
// 成功登录提示
m_bLoggedon = TRUE;
SendResponse("230 Logged on");
}
else
SendResponse("530 Login or password incorrect!");
}
}
break;
// change current directory
case TOK_CWD:
{
int nResult = theServer.m_UserManager.ChangeDirectory(m_strUserName, m_strCurrentDir, strArguments);
CString str;
switch(nResult)
{
case 0:
str.Format("250 CWD successful. \"%s\" is current directory.", m_strCurrentDir);
SendResponse(str);
break;
case 1:
str.Format("550 CWD failed. \"%s\": Permission denied.", strArguments);
SendResponse(str);
break;
default:
str.Format("550 CWD failed. \"%s\": directory not found.", strArguments);
SendResponse(str);
break;
}
}
break;
// print current directory
case TOK_PWD:
{
CString str;
str.Format("257 \"%s\" is current directory.", m_strCurrentDir);
SendResponse(str);
}
break;
// specify IP and port (PORT a1,a2,a3,a4,p1,p2) -> IP address a1.a2.a3.a4, port p1*256+p2.
case TOK_PORT:
{
CString strSub;
int nCount=0;
while (AfxExtractSubString(strSub, strArguments, nCount++, ','))
{
switch(nCount)
{
case 1: // a1
m_TransferStatus.m_strRemoteHost = strSub;
m_TransferStatus.m_strRemoteHost += ".";
break;
case 2: // a2
m_TransferStatus.m_strRemoteHost += strSub;
m_TransferStatus.m_strRemoteHost += ".";
break;
case 3: // a3
m_TransferStatus.m_strRemoteHost += strSub;
m_TransferStatus.m_strRemoteHost += ".";
break;
case 4: // a4
m_TransferStatus.m_strRemoteHost += strSub;
break;
case 5: // p1
m_TransferStatus.m_nRemotePort = 256*atoi(strSub);
break;
case 6: // p2
m_TransferStatus.m_nRemotePort += atoi(strSub);
break;
}
}
m_TransferStatus.m_bPassiveMode = FALSE;
SendResponse("200 Port command successful");
break;
}
// switch to passive mode
case TOK_PASV:
{
// delete existing datasocket
DestroyDataSocket();
// create new data socket
m_TransferStatus.m_pDataSocket = new CDataSocket(this, -1);
if (!m_TransferStatus.m_pDataSocket->Create())
{
DestroyDataSocket();
SendResponse("421 Can't create socket");
break;
}
// start listening
m_TransferStatus.m_pDataSocket->Listen();
m_TransferStatus.m_pDataSocket->AsyncSelect();
CString strIP, strTmp;
UINT nPort;
// get our ip address
GetSockName(strIP, nPort);
// Now retrieve the port
m_TransferStatus.m_pDataSocket->GetSockName(strTmp, nPort);
// Reformat the ip
strIP.Replace(".",",");
// tell the client which address/port to connect to
CString str;
str.Format("227 Entering Passive Mode (%s,%d,%d)", strIP, nPort/256, nPort%256);
SendResponse(str);
m_TransferStatus.m_bPassiveMode = TRUE;
break;
}
case TOK_TYPE:
{
SendResponse("200 Type set to " + strArguments);
}
break;
// list current directory
case TOK_LIST:
{
if(!m_TransferStatus.m_bPassiveMode && (m_TransferStatus.m_strRemoteHost == "" || m_TransferStatus.m_nRemotePort == -1))
{
SendResponse("503 Bad sequence of commands.");
}
else
{
// if client did not specify a directory use current dir
if (strArguments == "")
{
strArguments = m_strCurrentDir;
}
else
{
// check if argument is file or directory
CString strResult;
int nResult = theServer.m_UserManager.GetFileName(m_strUserName, strArguments, m_strCurrentDir, FTP_LIST, strResult);
if (nRes
展开阅读全文