最新FTP服务器与客户端设计与开发.doc
Four short words sum up what has lifted most successful individuals above the crowd: a little bit more.-author-dateFTP服务器与客户端设计与开发详细设计 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 CWndfriend 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;/ 开始创建socketif (m_ListenSocket.Create(m_nPort)/ start listeningif (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 windowif (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 timerKillTimer(1);m_bRunning = FALSE;m_ListenSocket.Close();CConnectThread* pThread = NULL;/ close all running threadsdom_CriticalSection.Lock();POSITION pos = m_ThreadList.GetHeadPosition();if (pos != NULL)pThread = (CConnectThread *)m_ThreadList.GetAt(pos);m_CriticalSection.Unlock();/ save thread membersint nThreadID = pThread->m_nThreadID;HANDLE hThread = pThread->m_hThread;AddTraceLine(0, "%d Shutting down thread.", nThreadID);/ tell thread to stoppThread->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 stoppedAddTraceLine(0, "%d Problem while killing thread.", nThreadID);/ don't try again, so removem_CriticalSection.Lock();POSITION rmPos = m_ThreadList.Find(pThread);if (rmPos != NULL)m_ThreadList.RemoveAt(rmPos);m_CriticalSection.Unlock();elseAddTraceLine(0, "%d Thread successfully stopped.", nThreadID);elsem_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 establishedCSocket 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 threadn");return;CFTPServer *pWnd = (CFTPServer *)m_pWndServer; / since everything is successful, add the thread to our listpWnd->m_CriticalSection.Lock(); pWnd->m_ThreadList.AddTail(pThread);pWnd->m_CriticalSection.Unlock();/ save pointerpThread->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 connectionm_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);elseif (!(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 clientCString 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;trypWnd->m_CriticalSection.Lock();/ delete this thread from the linked listPOSITION pos = pWnd->m_ThreadList.Find(this);if(pos != NULL)pWnd->m_ThreadList.RemoveAt(pos);pWnd->m_CriticalSection.Unlock(); / notify service main looppWnd->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 classm_pWndServer->PostMessage(WM_THREADMSG, (WPARAM)0, (LPARAM)nBytes);void CConnectThread:IncReceivedBytes(int nBytes)m_LastDataTransferTime = CTime:GetCurrentTime();m_nReceivedBytes += nBytes;/ notify server classm_pWndServer->PostMessage(WM_THREADMSG, (WPARAM)1, (LPARAM)nBytes);CConnectSocket类每个线程都是通过一个CConnectSocket对象m_ConnectSocket来完成数据的接受和发送。当线程创建成功以后,m_ConnectSocket对象通过OnReceive函数获得数据,然后利用ParseCommand函数来解析其中FTP命令void CConnectSocket:OnReceive(int nErrorCode) TCHAR buffBUFFERSIZE;int nRead = Receive(buff, BUFFERSIZE);switch (nRead)case 0:Close();break;case SOCKET_ERROR:if (GetLastError() != WSAEWOULDBLOCK) TCHAR szError256;wsprintf(szError, "OnReceive error: %d", GetLastError();AfxMessageBox (szError);break;default:if (nRead != SOCKET_ERROR && nRead != 0)(CConnectThread *)AfxGetThread()->IncReceivedBytes(nRead);/ terminate the stringbuffnRead = 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 commandCString strCommand, strArguments;if (!GetRxCommand(strCommand, strArguments)return;int nCommand;/查找命令for (nCommand = TOK_USER; nCommand < TOK_ERROR; nCommand+)/ found command ?if (strCommand = commandListnCommand.m_pszName)/ did we expect an argument ?if (commandListnCommand.m_bHasArguments && (strArguments = "")SendResponse("501 Syntax error");return;break;if (nCommand = TOK_ERROR)/ command is not in our listSendResponse("500 Syntax error, command unrecognized.");return;/ no commands are excepted before successfull logged onif (nCommand > TOK_PASS && !m_bLoggedon)SendResponse("530 Please log in with USER and PASS first.");return;/ proces commandswitch(nCommand)/ specify usernamecase TOK_USER:strArguments.MakeLower();m_bLoggedon = FALSE;m_strUserName = strArguments;CString strPeerAddress;UINT nPeerPort;GetPeerName(strPeerAddress, nPeerPort);/ tell FTP server a new user has connectedCConnectThread *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 passwordcase TOK_PASS:/ already logged on ?if (m_bLoggedon)SendResponse("503 Bad sequence of commands.");else/ check user and passwordCUser 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 directorycase 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 directorycase 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:/ a1m_TransferStatus.m_strRemoteHost = strSub;m_TransferStatus.m_strRemoteHost += "."break;case 2:/ a2m_TransferStatus.m_strRemoteHost += strSub;m_TransferStatus.m_strRemoteHost += "."break;case 3:/ a3m_TransferStatus.m_strRemoteHost += strSub;m_TransferStatus.m_strRemoteHost += "."break;case 4:/ a4m_TransferStatus.m_strRemoteHost += strSub;break;case 5:/ p1m_TransferStatus.m_nRemotePort = 256*atoi(strSub);break;case 6:/ p2m_TransferStatus.m_nRemotePort += atoi(strSub);break;m_TransferStatus.m_bPassiveMode = FALSE;SendResponse("200 Port command successful");break;/ switch to passive modecase TOK_PASV:/ delete existing datasocketDestroyDataSocket();/ create new data socketm_TransferStatus.m_pDataSocket = new CDataSocket(this, -1);if (!m_TransferStatus.m_pDataSocket->Create()DestroyDataSocket();SendResponse("421 Can't create socket");break;/ start listeningm_TransferStatus.m_pDataSocket->Listen();m_TransferStatus.m_pDataSocket->AsyncSelect();CString strIP, strTmp;UINT nPort;/ get our ip addressGetSockName(strIP, nPort);/ Now retrieve the portm_TransferStatus.m_pDataSocket->GetSockName(strTmp, nPort);/ Reformat the ipstrIP.Replace(".",",");/ tell the client which address/port to connect toCString 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 directorycase 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 dirif (strArguments = "")strArguments = m_strCurrentDir;