Select模型---轉自javaeye

2007-01-19

Select模型

來自:http://blog.csdn.net/dylgsy/archive/2007/01/18/

查找了很多資料都找不到select模型的詳細用法,《Windows網絡編程》這本書上也只是寫了一個簡單的迴應服務器,就連writefds的用法都沒講,也不知道什麼時候利用“可寫”來發文件。這些都是我的疑問,相信很多研究網絡編程的同路人也碰到了我的這些問題。這些疑問在這篇文章中都解決了!耗費了偶很多的精力去猜測去思考! 

感覺一些已經得道的高人都不肯把這些問題說透徹點,唉,只能靠自己去摸索了,希望這篇文章能對你有用,也希望一些高人能出來指點指點!

SOCKET模型的出現是爲了解決“一個客戶端一線程”的問題,爲了WINDOWS的線程切換不要太頻繁,我們可以使用WINDOWS的SOCKET模型。但在論壇裏又看到了一些文章說現在的計算機硬件相當發達,成萬的線程切換根本不是什麼問題。無論怎麼樣,既然這些模型被MS發明了出來,就有它的道理,我們還是好好來學習一下吧。

對於select模型,大家都知道用select函數,然後判斷讀集、寫集。但如何使用select,最終寫好的Server又是一個怎麼樣的結構呢?

select可以這樣用,寫成兩個函數,SelectSend和SelectRecv,這兩個函數和一般的send/recv不同的地方在於它是有超時值的,這樣就不會把程序完全阻塞了。這兩個函數大概如下(參考《PC網絡遊戲編程》):

SelectRecv(...)

 

int SelectRecv(SOCKET hSocket, char *pszBuffer, int nBufferSize, 
        DWORD dwTimeout)
{
    ASSERT(hSocket 
!= NULL);
    
if(hSocket==NULL)
        
return ( SOCKET_ERROR );
    FD_SET fd 
= {1, hSocket};
    TIMEVAL tv 
= {dwTimeout, 0};
    
int nBytesReceived=0;
    
if(select(0&fd, NULL, NULL, &tv) == 0
        
goto CLEAR;
    
if((nBytesReceived = recv(hSocket, pszBuffer, nBufferSize, 0)) == SOCKET_ERROR)
        
goto CLEAR;
    
return nBytesReceived;

CLEAR:
    SetLastError(WSAGetLastError());
//超時
    return(SOCKET_ERROR);
}

 

SelectSend(...)

 

int SelectSend(SOCKET hSocket,char const * pszBuffer, 
        
int nBufferSize, DWORD dwTimeout)

{
    
if(hSocket==NULL)
        
return(SOCKET_ERROR);
    
int nBytesSent = 0;
    
int nBytesThisTime;
    
const char* pszTemp = pszBuffer;
    
do {
        nBytesThisTime 
= Send_Block(hSocket,pszTemp, nBufferSize-nBytesSent, dwTimeout);
        
if(nBytesThisTime<0)
            
return(SOCKET_ERROR);
        
//如果一次沒有發送成功
        nBytesSent += nBytesThisTime;
        
//改變當前字符指針
        pszTemp += nBytesThisTime;
    }
 while(nBytesSent < nBufferSize);
    
return nBytesSent;
}

 

這樣就可以利用上面兩個函數寫發送和接收程序了!我們下面來看看如何使用select模型建立我們的Server,下面分了4個文件,大家可以拷貝下來實際編譯一下。首先有兩個公用的文件:CommonSocket.h、CommonCmd.h。他們都是放在 Common Include 目錄中的!然後就是SelectServer.cpp和SocketClient.cpp這兩個文件,可以分別建立兩個工程把他們加進去編譯!有些關鍵的地方我都寫在文件註釋裏了,可以自己看看,這些是我寫的。爲了方便大家拷貝,我就不用“代碼插入”的方式了。直接貼到下面!

/*/
文件:SelectServer.cpp
說明:

 此文件演示瞭如何使用select模型來建立服務器,難點是select的writefds在什麼時候使用。
 好好看看代碼就能很明白的了,可以說我寫這些代碼就是爲了探索這個問題的!找了很多資料都找不到!!

 在這裏我懷疑是否可以同時讀寫同一個SOCKET,結果發現是可以的,但是最好別這樣做。因爲會導致包的順序不一致。

    這裏說一下SELECT模型的邏輯:
 我們如果不使用select模型,在調用recv或者send時候會導致程序阻塞。如果使用了select
 就給我們增加了一層保護,就是說在調用了select函數之後,對處於讀集合的socket進行recv操作
 是一定會成功的(這是操作系統給我們保證的)。對於判斷SOCKET是否可寫時也一樣。
 而且就算不可讀或不可寫,使用了select也不會鎖 死!因爲 select 函數提供了超時!利用這個特性還可以
 做異步connect,也就是可以掃描主機,看哪個主機開了服務(遠程控制軟件經常這樣幹哦!)

 我們如何利用這種邏輯來設計我們的server呢?
 這裏用的方法是建立一個SocketInfo,這個SocketInfo包括了對Socket當前進行的操作,我把它分爲:
 {RecvCmd, RecvData, ExecCmd} 一開始socket是處於一個RecvCmd的狀態,
 然後取到了CMD(也就是取到了指令,可想象一下CPU得到了指令後幹什麼),然後就要取數據了,取得指令
 知道要幹什麼,取得了數據就可以實際開始幹了。實際開始幹就是ExecCmd,在這個狀態之後都是需要
 發送數據的了,所以把他們都放在判斷SOCKET可寫下面<就是 if(FD_ISSET(vecSocketInfo[i].sock, &fdWrite)) >,
 即當Socket可寫就可以發送信息給客戶端了。

 發送的根本協議是這樣的:先發一個SCommand的結構體過去,這個結構體說明了指令和數據的長度。
 然後就根據這個長度接收數據。最後再給客戶端做出相應的響應!

    根據這種代碼結構,可以很方便的添加新的功能。

   錯誤處理做得不太好,以後再補充了。

 其他的如註釋,結構,命名等的編碼規範都用了個人比較喜歡的方式。

輸出:
 ../Bin/SelectServer.exe

用法:
 直接啓動就可以了

Todo:
 下一步首先完成各個SOCKET的模型,然後公開自己的研究代碼。
 功能方面就是:
 1、服務器可以指定共享文件夾
 2、客戶端可以列出服務器共享了哪些文件
 3、客戶端可以列出哪些用戶在線,並可以發命令和其他用戶聊天
 4、加上界面
/*/

#include <winsock2.h>
#pragma comment(lib, "WS2_32")

#include <windows.h>

#pragma warning(disable: 4786)
#include <iostream>
#include <vector>
#include <map>
#include <string>
#include <algorithm>
using namespace std;

#include "../Common Include/CommonSocket.h"
#include "../Common Include/CommonCmd.h"

typedef struct tagSocketInfo
{
 SOCKET sock;
 ECurOp eCurOp;
 SCommand cmd;
 char *data;
}SSocketInfo;

// 登錄用戶的列表
map<string, SOCKET> g_LoginUsers;

// 註冊用戶的列表(用戶名,密碼)
map<string, string> g_RegUSers;

// 用於退出服務器
bool g_bExit = false;

void DoRecvCmd(vector<SSocketInfo> &vecSockInfo, int idx);
void DoRecvData(vector<SSocketInfo> &vecSockInfo, int idx);
void DoExecCmd(vector<SSocketInfo> &vecSockInfo, int idx);

bool DoAuthen(SOCKET sock, char *data, DWORD len);
bool DoGetFile(SOCKET sock, char *data, DWORD len);
bool DoRegister(SOCKET sock, char *data, DWORD len);

void GetRegUsers();

///////////////////////////////////////////////////////////////////////
//
// 函數名       : RemoveByIndex
// 功能描述     : 根據 index 來刪除 VECTOR 裏的元素
// 參數         : vector<T> &vec [in]
// 參數         : int nIdx   [in]
// 返回值       : void 
//
///////////////////////////////////////////////////////////////////////
template<class T>
void EraseByIndex(vector<T> &vec, int nIdx)
{
 vector<T>::iterator it;
 it = vec.begin() + nIdx;
 vec.erase(it);
}

void main()
{
 InitWinsock();

 vector<SSocketInfo> vecSocketInfo;

 SOCKET sockListen = BindServer(PORT);
 ULONG NonBlock = 1;
 ioctlsocket(sockListen, FIONBIO, &NonBlock);
 
 SOCKET sockClient;

 GetRegUsers();

 FD_SET fdRead;
 FD_SET fdWrite;
 
 while(!g_bExit)
 {
  // 每次調用select之前都要把讀集和寫集清空
  FD_ZERO(&fdRead);
  FD_ZERO(&fdWrite);
  
  // 設置好讀集和寫集
  FD_SET(sockListen, &fdRead);
  for(int i = 0; i < vecSocketInfo.size(); i++)
  {
   FD_SET(vecSocketInfo[i].sock, &fdRead);
   FD_SET(vecSocketInfo[i].sock, &fdWrite);
  }

  // 調用select函數
  if(select(0, &fdRead, &fdWrite, NULL, NULL) == SOCKET_ERROR)
  {
   OutErr("select() Failed!");
   break;
  }

  // 說明可以接受連接了
  if(FD_ISSET(sockListen, &fdRead))
  {
   char szClientIP[50];
   sockClient = AcceptClient(sockListen, szClientIP);
   cout << szClientIP << " 連接上來" << endl;

   ioctlsocket(sockClient, FIONBIO, &NonBlock);

   SSocketInfo sockInfo;
   sockInfo.sock = sockClient;
   sockInfo.eCurOp = RecvCmd;
   // 把接收到的這個socket加入自己的隊列中
   vecSocketInfo.push_back(sockInfo);
  }

  for(i = 0; i < vecSocketInfo.size(); i++)
  {
   // 如果可讀
   if(FD_ISSET(vecSocketInfo[i].sock, &fdRead))
   {
    switch(vecSocketInfo[i].eCurOp)
    {
    case RecvCmd:
     DoRecvCmd(vecSocketInfo, i);
     break;

    case RecvData:
     DoRecvData(vecSocketInfo, i);
     break;
     
    default:
     break;
    }
   }

   // 如果可寫
   if(FD_ISSET(vecSocketInfo[i].sock, &fdWrite))
   {
    switch(vecSocketInfo[i].eCurOp)
    {
    case ExecCmd:
     DoExecCmd(vecSocketInfo, i);
     break;
    
    default:
     break;
    }
   }
  }
 }
}


///////////////////////////////////////////////////////////////////////
//
// 函數名       : DoRecvCmd
// 功能描述     : 獲取客戶端傳過來的cmd
// 參數         : vector<SSocketInfo> &vecSockInfo
// 參數         : int idx
// 返回值       : void 
//
///////////////////////////////////////////////////////////////////////
void DoRecvCmd(vector<SSocketInfo> &vecSockInfo, int idx)
{
 SSocketInfo *sockInfo = &vecSockInfo[idx];
 int nRet = RecvFix(sockInfo->sock, (char *)&(sockInfo->cmd), sizeof(sockInfo->cmd));

 // 如果用戶正常登錄上來再用 closesocket 關閉 socket 會返回0 
 // 如果用戶直接關閉程序會返回 SOCKET_ERROR,強行關閉
 if(nRet == SOCKET_ERROR || nRet == 0)
 {
  OutMsg("客戶端已退出。");
  closesocket(sockInfo->sock);
  sockInfo->sock = INVALID_SOCKET;     
  EraseByIndex(vecSockInfo, idx);
  return;
 }
 sockInfo->eCurOp = RecvData;
}


///////////////////////////////////////////////////////////////////////
//
// 函數名       : DoRecvData
// 功能描述     : DoRecvCmd 已經獲得了指令,接下來就要獲得執行指令所需要的數據
// 參數         : vector<SSocketInfo> &vecSockInfo
// 參數         : int idx
// 返回值       : void 
//
///////////////////////////////////////////////////////////////////////
void DoRecvData(vector<SSocketInfo> &vecSockInfo, int idx)
{
 SSocketInfo *sockInfo = &vecSockInfo[idx];
 // 爲數據分配空間,分配多一位用來放最後的0
 sockInfo->data = new char[sockInfo->cmd.DataSize + 1];
 memset(sockInfo->data, 0, sockInfo->cmd.DataSize + 1);
 
 // 接收數據
 int nRet = RecvFix(sockInfo->sock, sockInfo->data, sockInfo->cmd.DataSize);
 if(nRet == SOCKET_ERROR || nRet == 0)
 {
  OutMsg("客戶端已退出。");
  closesocket(sockInfo->sock);
  sockInfo->sock = INVALID_SOCKET;     
  EraseByIndex(vecSockInfo, idx);
  return;
 }
  
 sockInfo->eCurOp = ExecCmd;
}


///////////////////////////////////////////////////////////////////////
//
// 函數名       : DoExecCmd
// 功能描述     : 指令和執行指令所需數據都已經準備好了,接下來就可以執行命令
// 參數         : vector<SSocketInfo> &vecSockInfo
// 參數         : int idx
// 返回值       : void 
//
///////////////////////////////////////////////////////////////////////
void DoExecCmd(vector<SSocketInfo> &vecSockInfo, int idx)
{
 SSocketInfo *sockInfo = &vecSockInfo[idx];
 switch(sockInfo->cmd.CommandID) 
 {
 case CMD_AUTHEN:
  DoAuthen(sockInfo->sock, sockInfo->data, sockInfo->cmd.DataSize);
   break;
 case CMD_GETFILE:
  DoGetFile(sockInfo->sock, sockInfo->data, sockInfo->cmd.DataSize);
  break;
 case CMD_REGISTER:
  DoRegister(sockInfo->sock, sockInfo->data, sockInfo->cmd.DataSize);
  break;
 default:
  break;
 }

 // 執行完命令後就設置回接收指令狀態
 sockInfo->eCurOp = RecvCmd;
}

///////////////////////////////////////////////////////////////////////
//
// 函數名       : DoAuthen
// 功能描述     : 對用戶名和密碼做驗證
// 參數         : SOCKET sock
// 參數         : char *data
// 參數         : DWORD len
// 返回值       : bool 
//
///////////////////////////////////////////////////////////////////////
bool DoAuthen(SOCKET sock, char *data, DWORD len)
{
 // 取得用戶名和密碼的字符串
 // 格式爲 "dyl 123"

 char *pBuf = data;
 int nIdx = 0;
 char szName[10];
 memset(szName, 0, 10);
 char szPass[10];
 memset(szPass, 0, 10);
 
 while (*pBuf != ' ')
 {
  szName[nIdx++] = *pBuf++;
 }
 szName[nIdx] = '/0';

 *pBuf++;

 nIdx = 0;
 while (*pBuf != '/0')
 {
  szPass[nIdx++] = *pBuf++;
 }
 szPass[nIdx] = '/0';


 char szSend[30];
 memset(szSend, 0, 30);
 bool bUserExist = false;

 if( g_RegUSers.find(string(szName)) != g_RegUSers.end() )
 {
  if(strcmp(g_RegUSers[szName].c_str(), szPass) == 0)
  {
   strcpy(szSend, "UP OK!");
   g_LoginUsers[szName] = sock;
  }
  else
  {
   strcpy(szSend, "P Err!");
  }  
 }
 else
 {
 // 不存在這個用戶
  strcpy(szSend, "U Err!");
 }
 
 int nRet = SendFix(sock, szSend, strlen(szSend));

 if(nRet == SOCKET_ERROR)
  return false;

 // 執行完了就釋放data
 delete []data;

 return true;
}


///////////////////////////////////////////////////////////////////////
//
// 函數名       : DoGetFile
// 功能描述     : 爲用戶提供文件
// 參數         : SOCKET sock
// 參數         : char *data
// 參數         : DWORD len
// 返回值       : bool 
//
///////////////////////////////////////////////////////////////////////
bool DoGetFile(SOCKET sock, char *data, DWORD len)
{
 // 打開文件,判斷文件是否存在
 HANDLE hFile = CreateFile(data, GENERIC_READ, FILE_SHARE_READ, 
  NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
 
 if(hFile == INVALID_HANDLE_VALUE)
 {
  OutMsg("文件不存在!");
  DWORD dwSize = 0;
  SendFix(sock, (char *)&dwSize, sizeof(dwSize));
  return false;
 }
 else
 {// 發送文件信息

  // 發送文件大小,發送過去
  DWORD dwFileSize = GetFileSize(hFile, NULL);
  int nRet = SendFix(sock, (char *)&dwFileSize, sizeof(dwFileSize));
  if(nRet == SOCKET_ERROR)
   return false;
  
  // 讀文件記錄併發送
  DWORD nLeft = dwFileSize;
  char szBuf[1024];
  DWORD nCurrRead = 0;
  while(nLeft > 0)
  {
   if(!ReadFile(hFile, szBuf, 1024, &nCurrRead, NULL))
   {
    OutErr("ReadFile failed!");
    return false;
   }
   SendFix(sock, szBuf, nCurrRead);
   nLeft -= nCurrRead;
  }
  
  CloseHandle(hFile);
 }
 
 delete []data;
 return true;
}

bool DoRegister(SOCKET sock, char *data, DWORD len)
{
 // 取得用戶名和密碼的字符串
 // 格式爲 "dyl 123"

 bool bReturn = true;
 char *pBuf = data;
 int nIdx = 0;
 char szName[10];
 memset(szName, 0, 10);
 char szPass[20];
 memset(szPass, 0, 20);
 
 while (*pBuf != ' ')
 {
  szName[nIdx++] = *pBuf++;
 }
 szName[nIdx] = '/0';

 *pBuf++;

 nIdx = 0;
 while (*pBuf != '/0')
 {
  szPass[nIdx++] = *pBuf++;
 }
 szPass[nIdx] = '/0';

 char szSend[30];
 memset(szSend, 0, 30); 

 HANDLE hFile = CreateFile("Users.lst", GENERIC_WRITE, FILE_SHARE_READ, NULL, 
  OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
 if(hFile == INVALID_HANDLE_VALUE)
 {
  hFile = CreateFile("Users.lst", GENERIC_WRITE, FILE_SHARE_READ, NULL, 
   CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL);
  if(hFile == INVALID_HANDLE_VALUE) 
  {
   OutMsg("創建文件Users.lst失敗!");
   strcpy(szSend, "REG ERR!");
   bReturn = false;
  }
  else
  {
   // 在開始加
   SetFilePointer(hFile, 0, 0, FILE_BEGIN);
   DWORD dwWritten = 0;
   if(!WriteFile(hFile, szName, 10, &dwWritten, NULL))
   {
    OutMsg("WriteFile failed!");
    strcpy(szSend, "REG ERR!");
    bReturn = false;
   }
   if(!WriteFile(hFile, szPass, 20, &dwWritten, NULL))
   {
    OutMsg("WriteFile failed!");
    strcpy(szSend, "REG ERR!");
    bReturn = false;
   }
   
   CloseHandle(hFile);

   // 讀回到已註冊用戶列表中
   GetRegUsers();

   strcpy(szSend, "REG OK!");
  }
 }
 else
 {
  // 移動到最後追加
  SetEndOfFile(hFile);
  DWORD dwWritten = 0;
  if(!WriteFile(hFile, szName, 10, &dwWritten, NULL))
  {
   OutMsg("WriteFile failed!");
   strcpy(szSend, "REG ERR!");
   bReturn = false;
  }
  if(!WriteFile(hFile, szPass, 20, &dwWritten, NULL))
  {
   OutMsg("WriteFile failed!");
   strcpy(szSend, "REG ERR!");
   bReturn = false;
  }

  CloseHandle(hFile);

  // 讀回到已註冊用戶列表中
  GetRegUsers();

  strcpy(szSend, "REG OK!");  
 }
 int nRet = SendFix(sock, szSend, strlen(szSend));
 if(nRet == SOCKET_ERROR)
  bReturn = false;

 // 執行完了就釋放data
 delete []data; 

 return bReturn;
}

void GetRegUsers()
{
 g_RegUSers.clear();

 char szName[10];
 char szPwd[20];
 
 HANDLE hFile = CreateFile("Users.lst", GENERIC_READ, FILE_SHARE_READ, NULL, 
  OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
 if(hFile == INVALID_HANDLE_VALUE)
 {
  OutMsg("用戶列表不存在!");
 }
 else
 {
  DWORD dwFileSize = 0;
  dwFileSize = GetFileSize(hFile, NULL);
  
  SetFilePointer(hFile, 0, 0, FILE_BEGIN);

  DWORD dwRead = 0;
  
  
  DWORD dwLeft = dwFileSize;
  while(dwLeft > 0)
  {
   memset(szName, 0, 10);
   memset(szPwd, 0, 20);
   if(!ReadFile(hFile, szName, 10, &dwRead, NULL))
   {
    DWORD dwErr = GetLastError();
    OutMsg("ReadFile failed!");
   }
   dwLeft -= dwRead;
   if(!ReadFile(hFile, szPwd, 20, &dwRead, NULL))
   {
    DWORD dwErr = GetLastError();
    OutMsg("ReadFile failed!");
   }
   dwLeft -= dwRead;
   g_RegUSers[szName] = szPwd;
  } 
 }

 CloseHandle(hFile);
}

/**********************************************************************************************************************/

第二個文件

/*/
文件:SocketClient.cpp

說明:
 此文件是作爲測試的客戶端,實現了登錄和取文件的功能。
 和服務端的交互就是採用了發送命令、數據長度,然後發送具體的數據這樣的順序。
 詳細可看服務端的說明。

 基本邏輯是這樣的,客戶端要先登錄服務端,然後登錄成功之後,才能進行相應的操作。

 錯誤處理做得不太好,以後再補充了。

 其他的如註釋,結構,命名等的編碼規範都用了個人比較喜歡的方式。

輸出:
 ../Bin/SocketClient.exe

用法:
 可以 SocketClient Server_IP
 或者直接啓動SocketClient,會提示你輸入服務端的IP

Todo:
 下一步首先完成各個SOCKET的模型,然後公開自己的研究代碼。
 功能方面就是:
 1、服務器可以指定共享文件夾
 2、客戶端可以列出服務器共享了哪些文件
 3、客戶端可以列出哪些用戶在線,並可以發命令和其他用戶聊天
/*/

#include <winsock2.h>
#pragma comment(lib, "WS2_32")

#include <iostream>
using namespace std;

#include <stdlib.h>

#include "../Common Include/CommonSocket.h"
#include "../Common Include/CommonCmd.h"

bool g_bAuth = false;

void GetFile(SOCKET sock);
bool Auth(SOCKET sock, char *szName, char *szPwd);
bool RegisterUser(SOCKET sock, char *szName, char *szPwd);


///////////////////////////////////////////////////////////////////////
//
// 函數名       : Usage
// 功能描述     : 提示程序用法
// 返回值       : void 
//
///////////////////////////////////////////////////////////////////////
void Usage() 

 printf("*******************************************/n"); 
 printf("Socket Client                            /n"); 
 printf("Written by DYL                         /n"); 
 printf("Email: [email protected]                 /n"); 
 printf("Usage: SocketClient.exe Server_IP          /n"); 
 printf("*******************************************/n"); 
}


///////////////////////////////////////////////////////////////////////
//
// 函數名       : Menu
// 功能描述     : 選擇服務的界面
// 返回值       : void 
//
///////////////////////////////////////////////////////////////////////
void Menu()
{
 system("cls");
 printf("********************************************/n"); 
 printf("請選擇操作:        /n/n"); 
 printf("1、取得文件         /n"); 
 printf("2、退出          /n"); 
 printf("********************************************/n"); 
}


///////////////////////////////////////////////////////////////////////
//
// 函數名       : LoginMenu
// 功能描述     : 用戶登錄的界面
// 返回值       : void 
//
///////////////////////////////////////////////////////////////////////
void LoginMenu()
{
 cout << "請按任意鍵繼續操作." <<endl;
 getchar();
 system("cls");
 printf("********************************************/n"); 
 printf("請選擇操作:        /n/n"); 
 printf("1、登錄          /n"); 
 printf("2、註冊          /n"); 
 printf("3、退出          /n"); 
 printf("********************************************/n"); 
}


///////////////////////////////////////////////////////////////////////
//
// 函數名       : Login
// 功能描述     : 用戶登錄的界面邏輯
// 參數         : SOCKET sock
// 返回值       : bool 
//
///////////////////////////////////////////////////////////////////////
bool Login(SOCKET sock)
{
 bool bGoOn = true;
 while(bGoOn)
 {
  LoginMenu();
  int nChoose = 0;
  cin >> nChoose;

  char szName[10];
  char szPwd[20];
  char szConfirmPwd[20];
  memset(szName, 0, 10);
  memset(szPwd, 0, 20);
  memset(szConfirmPwd, 0, 20);

  bool bGoOnLogin = true;

  switch(nChoose) 
  {
  case 1:
   while(bGoOnLogin)
   {
    cout << "請輸入你的用戶名:";
    cin >> szName;
    cout << "請輸入你的密碼:";
    cin >> szPwd;
    if(Auth(sock, szName, szPwd))
    {
     return true; 
    }
    else
    {
     char c;
     cout << "繼續登錄?y/n" << endl;
     cin >> c;
     switch(c) 
     {
     case 'y':
      bGoOnLogin = true;
      break;
     case 'n':
      bGoOnLogin = false;
      break;
     default:
      break;
     }
    }
   }
   break;
   
  case 2:
   cout << "請輸入你的用戶名:";
   cin >> szName;
   cout << "請輸入你的密碼:";
   cin >> szPwd;
   cout << "請再次輸入你的密碼:";
   cin >> szConfirmPwd;
   if(strcmp(szPwd, szConfirmPwd) != 0)
   {
    cout << "前後密碼不一致" << endl;
   }
   else
   {
    if(!RegisterUser(sock, szName, szPwd))
    {
     cout << "註冊用戶失敗!" << endl;
    }
   }
   break;

  case 3:
   bGoOn = false;
   return false;
  default:
   break;
  }
 }

 return false;
}

void main(int argc, char *argv[])
{
 system("cls");
 char szServerIP[20];
 memset(szServerIP, 0, 20);

 if(argc != 2)
 {
  cout << "請輸入服務器IP:";
  cin >> szServerIP;
 }
 else
 {
  strcpy(szServerIP, argv[1]);
 }
 InitWinsock();
 SOCKET sockServer;
 sockServer = ConnectServer(szServerIP, PORT, 1);
 if(sockServer == NULL)
 {
  OutErr("連接服務器失敗!");
  return;
 }
 else
 {
  OutMsg("已和服務器建立連接!");
 }

 // 要求用戶登錄
 if(!Login(sockServer))
  return;

 // 登錄成功,讓用戶選擇服務
 int nChoose = 0;
 bool bExit = false;
 while(!bExit)
 {
  Menu();
  cin >> nChoose;
  switch(nChoose)
  {
  case 1:  // 獲取文件
   GetFile(sockServer);
   break;
  case 2:
   bExit = true;
   break;   
  default:
   break;
  }
 }
 shutdown(sockServer, SD_BOTH);
 closesocket(sockServer);
 
}


///////////////////////////////////////////////////////////////////////
//
// 函數名       : Auth
// 功能描述     : 用戶登錄認證
// 參數         : SOCKET sock
// 參數         : char *szName
// 參數         : char *szPwd
// 返回值       : bool 
//
///////////////////////////////////////////////////////////////////////
bool Auth(SOCKET sock, char *szName, char *szPwd)
{
 char szCmd[50];
 memset(szCmd, 0, 50);
 strcpy(szCmd, szName);
 strcat(szCmd, " ");
 strcat(szCmd, szPwd);
 
 SCommand cmd;
 cmd.CommandID = CMD_AUTHEN;
 cmd.DataSize = strlen(szCmd);

 int nRet;
 nRet = SendFix(sock, (char *)&cmd, sizeof(cmd));
 if(nRet == SOCKET_ERROR)
 {
  OutErr("SendFix() failed!");
  return false;
 }
 else
 {
  SendFix(sock, szCmd, strlen(szCmd));
  char szBuf[10];
  memset(szBuf, 0, 10);
  recv(sock, szBuf, 10, 0);
  if(strcmp(szBuf, "UP OK!") == 0)
  {
   cout << "登錄成功。" << endl;
   g_bAuth = true;
  }
  else if(strcmp(szBuf, "U Err!") == 0)
  {
   cout << "此用戶不存在。" << endl;
   g_bAuth = false;
  }
  else if(strcmp(szBuf, "P Err!") == 0)
  {
   cout << "密碼錯誤。" << endl;
   g_bAuth = false;
  }
 }
 return g_bAuth;
}


///////////////////////////////////////////////////////////////////////
//
// 函數名       : GetFile
// 功能描述     : 取得服務器的文件
// 參數         : SOCKET sock
// 返回值       : void 
//
///////////////////////////////////////////////////////////////////////
void GetFile(SOCKET sock)
{
 if(!g_bAuth)
 {
  OutMsg("用戶還沒登錄!請先登錄");
  return;
 }
 
 char szSrcFile[MAX_PATH];
 char szDstFile[MAX_PATH];
 memset(szSrcFile, 0, MAX_PATH);
 memset(szDstFile, 0, MAX_PATH);

 cout << "你要取得Server上的文件:";
 cin >> szSrcFile;

 cout << "你要把文件存在哪裏:";
 cin >> szDstFile;

 SCommand cmd;
 cmd.CommandID = CMD_GETFILE;
 cmd.DataSize = strlen(szSrcFile);

 // 發送命令
 SendFix(sock, (char *)&cmd, sizeof(cmd));
 
 // 發送文件名
 SendFix(sock, szSrcFile, strlen(szSrcFile));

 // 接收文件長度
 DWORD dwFileSize = 0;
 RecvFix(sock, (char*)&dwFileSize, sizeof(dwFileSize));
 
 if(dwFileSize == 0)
 {
  OutMsg("文件不存在");
  return;
 }

 // 接收文件內容
 DWORD dwLeft = dwFileSize;
 char szBuf[1024];
 HANDLE hFile = CreateFile(szDstFile, GENERIC_WRITE, FILE_SHARE_READ, 
  NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
 if(hFile == INVALID_HANDLE_VALUE)
 {
  hFile = CreateFile(szDstFile, GENERIC_WRITE, FILE_SHARE_READ, 
   NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL);
  if(hFile == INVALID_HANDLE_VALUE)
  {
   OutErr("CreateFile failed!");
   return;
  }
 }
 while(dwLeft > 0)
 {
  memset(szBuf, 0, 1024);
  // 這裏是不確定文件內容的,所以要用recv,不能用RecvFix
  int nRead = recv(sock, szBuf, 1024, 0);
  if(nRead == SOCKET_ERROR)
   OutErr("RecvFix Error!");

  DWORD dwWritten = 0;
  if(!WriteFile(hFile, szBuf, nRead, &dwWritten, NULL))
  {
   OutErr("WriteFile error!");
   return;
  }
  dwLeft -= dwWritten;
 }

 CloseHandle(hFile);

 OutMsg("接收文件成功!");
}


///////////////////////////////////////////////////////////////////////
//
// 函數名       : RegisterUser
// 功能描述     : 註冊新用戶
// 參數         : SOCKET sock
// 參數         : char *szName
// 參數         : char *szPwd
// 返回值       : bool 
//
///////////////////////////////////////////////////////////////////////
bool RegisterUser(SOCKET sock, char *szName, char *szPwd)
{
 char szCmd[50];
 memset(szCmd, 0, 50);
 strcpy(szCmd, szName);
 strcat(szCmd, " ");
 strcat(szCmd, szPwd);
 
 SCommand cmd;
 cmd.CommandID = CMD_REGISTER;
 cmd.DataSize = strlen(szCmd);

 // 發送命令
 int nRet = SendFix(sock, (char *)&cmd, sizeof(cmd));
 if(nRet == SOCKET_ERROR)
 {
  OutErr("SendFix() failed!");
  return false;
 }
 else
 {
  // 發送用戶名和密碼串
  SendFix(sock, szCmd, strlen(szCmd));
  char szBuf[10];
  memset(szBuf, 0, 10);
  
  recv(sock, szBuf, 10, 0);
  if(strcmp(szBuf, "REG OK!") == 0)
  {
   cout << "註冊成功。" << endl;
   return true;
  }
  else if(strcmp(szBuf, "REG ERR!") == 0)
  {
   cout << "註冊失敗." << endl;
   return false;
  }
 }
 
 return false;
}

/**********************************************************************************************************************/

第三個文件,公用的

/*/
文件: CommonSocket.h
說明: 
 實現了服務端和客戶端一些公用的函數!
/*/

#ifndef __COMMONSOCKET_H__
#define __COMMONSOCKET_H__

#include <iostream>
using namespace std;

#define OutErr(a) cout << (a) << endl /
      << "出錯代碼:" << WSAGetLastError() << endl /
      << "出錯文件:" << __FILE__ << endl  /
      << "出錯行數:" << __LINE__ << endl /

#define OutMsg(a) cout << (a) << endl;

///////////////////////////////////////////////////////////////////////
//
// 函數名       : InitWinsock
// 功能描述     : 初始化WINSOCK
// 返回值       : void 
//
///////////////////////////////////////////////////////////////////////
void InitWinsock()
{
 // 初始化WINSOCK
 WSADATA wsd;
 if( WSAStartup(MAKEWORD(2, 2), &wsd) != 0)
 {
  OutErr("WSAStartup()");
 }
}

///////////////////////////////////////////////////////////////////////
//
// 函數名       : ConnectServer
// 功能描述     : 連接SERVER
// 參數         : char *lpszServerIP IP地址
// 參數         : int nPort    端口
// 返回值       : SOCKET    SERVER 的 Socket
//
///////////////////////////////////////////////////////////////////////
SOCKET ConnectServer(char *lpszServerIP, int nPort, ULONG NonBlock)
{
 SOCKET sServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
 //ioctlsocket(sServer, FIONBIO, &NonBlock);
 
 struct hostent *pHost = NULL;
 struct sockaddr_in servAddr;
 servAddr.sin_family = AF_INET;
 servAddr.sin_port = htons(nPort);
 servAddr.sin_addr.s_addr = inet_addr(lpszServerIP);

 
 // 如果給的是主機的名字而不是IP地址
 if(servAddr.sin_addr.s_addr == INADDR_NONE)
 {
  pHost = gethostbyname( lpszServerIP );
  if(pHost == NULL)
  {
   OutErr("gethostbyname Failed!");
   return NULL;
  }
  memcpy(&servAddr.sin_addr, pHost->h_addr_list[0], pHost->h_length);
 }

 int nRet = 0;
 nRet = connect(sServer, (struct sockaddr *)&servAddr, sizeof(servAddr));
 if( nRet == SOCKET_ERRO

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章