1. 程式人生 > >Unix和Windows跨系統通訊程式設計

Unix和Windows跨系統通訊程式設計

前言

  隨著Internet的不斷髮展,客戶機/伺服器模型得到了廣泛的應用。客戶應用程式向伺服器程式請求服務。一個服務程式通常在一個眾所周知的埠監聽對服務的請求,也就是說,服務程序一直處於休眠狀態,直到一個客戶對這個服務的地址提出了連線請求。在這個時刻,服務程式被“驚醒”並且為客戶提供服務或對客戶的請求作出適當的反應。   Unix最早是由美國貝爾實驗室發明的一種多使用者、多工的通用作業系統,由於 Unix具有技術成熟、可靠性高、網路和資料庫功能強、伸縮性突出和開發性好的特色,可滿足各行各業的實際需要,特別是企業重要業務的需要,已經成為主要的工作平臺和重要的企業操作平臺,而微軟公司的 Windows作業系統使用者介面友好,安裝、使用也比較方便,應用軟體豐富,在個人PC機上成為主流作業系統。因此服務端使用 Unix,客戶端使用 Windows能充分利用它們各自的優點,這也是今後發展的一個趨勢。

  同時,金融行業的中間業務發展迅速,銀行主機採用 Unix系統,而各代收代付單位多采用 Windows或 WindowsNT系統,在它們之間傳輸資料檔案也涉及跨系統通訊的問題,通過套接字 Socket能方便地實現 Unix和 Windows的跨系統通訊,本文擬就這一問題作一探討。

  二、 SOCKET簡介

  TCP/IP是計算機互連最常使用的網路通訊協議, TCP/IP的核心部分由網路作業系統的核心實現,應用程式通過程式設計介面來訪問 TCP/IP,見下圖:

圖1 應用程式與Windows Socket關係圖

  七十年代中,美國國防部高研署(DARPA)將TCP/IP的軟體提供給加利福尼亞大學Berkeley分校後,TCP/IP很快被整合到Unix中,同時出現了許多成熟的TCP/IP應用程式介面(API)。這個API稱為Socket介面。今天,SOCKET介面是TCP/IP網路最為通用的API,也是在INTERNET上進行應用開發最為通用的API。

  九十年代初,由Microsoft聯合了其他幾家公司共同制定了一套WINDOWS下的網路程式設計介面,即Windows Sockets規範。它是Berkeley Sockets的重要擴充,主要是增加了一些非同步函式,並增加了符合 Windows 訊息驅動特性的網路事件非同步選擇機制。 Windows Sockets規範是一套開放的、支援多種協議的 Windows下的網路程式設計介面。目前,在實際應用中的Windows Sockets規範主要有1.1版和2.0版。兩者的最重要區別是1.1版只支援TCP/IP協議,而2.0版可以支援多協議,2.0版有良好的向後相容性,目前,Windows下的Internet軟體都是基於 WinSock開發的。

  Socket實際在計算機中提供了一個通訊埠,可以通過這個埠與任何一個具有Socket介面的計算機通訊。應用程式在網路上傳輸,接收的資訊都通過這個Socket介面來實現。在應用開發中就像使用檔案控制代碼一樣,可以對 Socket控制代碼進行讀、寫操作。我們將 Socket翻譯為套接字,套接字分為以下三種類型:

  位元組流套接字(Stream Socket) 是最常用的套接字型別,TCP/IP協議族中的 TCP 協議使用此類介面。位元組流套介面提供面向連線的(建立虛電路)、無差錯的、傳送先後順序一致的、無記錄邊界和非重複的網路信包傳輸。

  資料報套接字 (Datagram Socket) TCP/IP協議族中的UDP協議使用此類介面,它是無連線的服務,它以獨立的信包進行網路傳輸,信包最大長度為32KB,傳輸不保證順序性、可靠性和無重複性,它通常用於單個報文傳輸或可靠性不重要的場合。資料報套介面的一個重要特點是它保留了記錄邊界。對於這一特點。資料報套介面採用了與現在許多包交換網路(例如乙太網)非常類似的模型。

  原始資料報套接字(Raw Socket) 提供對網路下層通訊協議(如IP協議)的直接訪問,它一般不是提供給普通使用者的,主要用於開發新的協議或用於提取協議較隱蔽的功能。

  三、 基於SOCKET的應用開發

圖2 面向連線協議的SOCKET程式設計模型

  面向連線協議的SOCKET程式設計模型應用最為廣泛,因為面向連線協議提供了一系列的資料糾錯功能,可以保證在網路上傳輸的資料及時、無誤地到達對方。基於連線協議(位元組流套接字)的服務是設計客戶機/伺服器應用程式時的標準,其程式設計模型如下:

  儘管Windows Sockets和Berkeley Sockets都是TCP/IP應用程式的程式設計介面,但二者由於分屬不同的系統,在某些環節仍有一些差別。Windows Sockets API沒有嚴格地堅持Berkeley傳統風格,通常這麼做是因為在Windows環境中實現的難度。

  1.套介面資料型別和錯誤數值

   Windows Sockets規範中定義了一個新的資料型別 SOCKET,這一型別的定義對於將來Windows Sockets規範的升級是必要的。這一型別的定義保證了應用程式向Win32 環境的可移植性。因為這一型別會自動地從16位升級到32位。

  在 UNIX中,所有控制代碼包括套介面控制代碼,都是非負的短整數。 Windows Sockets 控制代碼則沒有這一限制,除了INVALID_SOCKET 不是一個有效的套介面外,套介面可以取從0到 INVALID_SOCKET-1 之間的任意值。因為 SOCKET 型別是unsigned ,所以編譯已經存在於UNIX 環境中的應用程式的原始碼可能會導致 signed/unsigned 資料型別不匹配的警告。

  因此,在socket() 例程和accept() 例程返回時,檢查是否有錯誤發生就不應該再使用把返回值和-1比較的方法,或判斷返回值是否為負(這兩種方法在BSD 中都是很普通,很合法的途徑)。取而代之的是,一個應用程式應該使用常量INVALID_SOCKET ,該常量已在WINSOCK.H 中定義。

  例如:
  BDS 風格
  m_hSocket=socket(…);
  if(m_hSocket=-1)   /*or m_hSocket<0*/
    {…}
  Windows風格:
  m_hSocket=socket(…);
  if(m_hSocket=INVALID_SOCKET)
    {…}

  2.select() 函式和FD_*巨集

  由於一個套介面不再表示為非負的整數,select() 函式在Windows Sockets API中的實現有一些變化:每一組套介面仍然用 fd_set 型別來代表,但是它並不是一個位掩碼。

  typedef struct fd_set{
   u_int fd_count;
   SOCKET fd_array;
  } fd_set;

  整個組的套介面是用了一個套介面的陣列來實現的。為了避免潛在的危險,應用程式應該堅持用FD_XXX 巨集(FD_SET,FD_ZERO,FD_CLR,FD_ISSET)來設定,初始化,清除和檢查fd_set 結構。

  3.錯誤程式碼-errno,h_errno,WSAGetLastError()

   Windows Sockets 實現所設定的錯誤程式碼是無法通過 errno 變數得到的。另外對於 getXbyY() 這一類的函式,錯誤程式碼無法從h _errno 變數得到,錯誤程式碼可以使用 WSAGetLastError()呼叫得到,這種改進是為了適應多執行緒程式設計的需要。WSAGetLastError()允許程式設計師能夠得到對應於每一執行緒的最近的錯誤程式碼。

  為了保持與 BSD 的相容性,應用程式可以加入以下一行程式碼:

  #define errno WSAGetLastError()

  這就保證了用全程的 errno 變數所寫的網路程式程式碼在單執行緒環境中可以正確使用。當然,這樣做有許多明顯的缺點:如果一個源程式對套介面和非套介面函式都用 errno 變數來檢查錯誤,那麼這種機制將無法工作。此外,一個應用程式不可能為 errno 賦一個新的值 (在Windows Sockets 中,WSAGetLastError()函式可以做到這一點)。

  例如:
  BSD風格:
  retcode=recv(…);
  if(retcode=-1 && errno=EWOULDBLOCK)
  {…}
  Windows風格:
  retcode=recv(…);
  if(retcode=-1 && WSAGetLastError ()=EWOULDBLOCK)
  {…}

  雖然為了相容性原因,錯誤常量與4.3BSD 所提供的一致:應用程式應該儘可能地使用“WSA”系列錯誤程式碼定義。且常量 SOCKET_ERROR是被用來檢查API 呼叫失敗的。雖然對這一常量的使用並不是強制性的,但這是推薦的。例如,上面程式更準確應該是:

  retcode=recv(…)
  if(retcode=SOCKET_ERROR&&WSAGetLastError()=
  WSAEWOULDBLOCK)
  {…}

  4.重新命名的函式

  有兩種原因Berkeley 套介面中的函式必須重新命名,以避免與其他的 API 衝突:

  ① close() 和closesocket()
  在Berkeley套介面中,套接口出現的形式與標準檔案描述字相同,所以 close() 函式可以用來象關閉檔案一樣關閉套介面。在Windows Sockets API中,套接字和正常檔案控制代碼不是等同的,例如read(),write() 和close() 在應用於套介面後不能保證正確工作。套介面必須使用 closesocket()例程來關閉,用close() 例程來關閉套介面是不正確的。

  ② ioctl()和iooctlsocket()
  Windows Sockets定義ioctlsocket() 例程,用於實現 BSD中ioctl() 和fcntl() 的功能。

  5.阻塞例程和 EINPROGRESS 巨集

  雖然Windows Sockets 支援關於套介面的阻塞操作,但是這種應用是被強烈反對的,如果程式設計師被迫使用阻塞模式(例如一個準備移植的已有的程式,BSD 包含了大量的阻塞函式,且其預設的工作方式都是阻塞的),那麼他應該清楚地知道Windows Sockets中阻塞操作的語義。

  6.指標

  所有應用程式與Windows Sockets 使用的指標都必須是FAR 指標,為了方便應用程式開發者作用,Windows Sockets規範定義了資料型別LPHOSTENT 。

  7. Windows Sockets支援的最大套介面數目

  一個Windows Sockets 應用程式可以使用的套介面的最大數目是在編譯時由常量 FD_SETSIZE 決定的。這個常量在 select() 函式中被用來組建fd_set 結構。在WINSOCK.H 中預設值是64。如果一個應用程式希望能夠使用超過64個套介面,則程式設計人員必須在每一個原始檔包含 WINSOCK.H 前定義確切的FD_SETSIZE值。有一種方法可以完成這項工作,就是在工程專案檔案中的編譯器選項上加入這一定義。 FD_SETSIZE定義的值對Windows Sockets實現所支援的套介面的數目並無任何影響。

  8.標頭檔案

  為了方便基於 Berkeley 套介面的已有的原始碼的移植, Windows Sockets支援許多 Berkeley標頭檔案。這些 Berkeley標頭檔案被包含在WINSOCK.H中。所以一個 Windows Sockets應用程式只需簡單的包含 WINSOCK.H就足夠了(這也是一種被推薦使用的方法)。

 四、 跨系統通訊程式設計例項

  下面通過一個例項來具體說明 Socket 在Unix 和Windows 跨系統通訊程式設計中的應用。

  Internet 上可以提供一種叫 IRC(Internet Relay Chatting,Internet 線上聊天系統)的服務。使用者通過客戶端的程式登入到 IRC 伺服器上,就可以與登入在同一 IRC 伺服器上的客戶進行交談,這也就是平常所說的聊天室。在這裡,給出了一個在執行 TCP/IP 協議的網路上實現 IRC 服務的程式。其中,伺服器執行在 SCO Open Server 5.0.5上,客戶端執行在Windows98 或 Windows NT 上。

  在一臺計算機上執行服務端程式,同一網路的其他計算機上執行客戶端程式,登入到伺服器上,各個客戶之間就可以聊天了。

  1.服務端

  服務端用名為 client 的整型陣列記錄每個客戶的已連線套介面描述字,此陣列中的所有元素都初始化為-1。同時伺服器既要處理監聽套介面,又要處理所有已連線套介面,因此需要用到 I/O 複用。通過 select 函式核心等待多個事件中的任一個發生,並僅在一個或多個事件發生或經過某指定時間後才喚醒程序。維護一個讀描述字集 rset ,當有客戶到達時,在陣列 client中的第一個可用條目(即值為-1的第一個條目)記錄其已連線套介面的描述字。同時把這個已連線描述字加到讀描述字集rset 中,變數maxi 是當前使用的陣列 client 的最大下標,而變數 maxfd+1 是函式 select第一個引數的當前值。如下圖:(假設有兩個客戶與伺服器建立連線)

圖3 第二客戶建立連線後的TCP伺服器

圖4 第二客戶建立連線後的資料結構

  伺服器在指定的埠上監聽,每當一個套介面接收到資訊,都將會把接收到的資訊傳送給每一個Client 。其主要源程式如下:

  int main(int argc,char **argv)
  {
  int   i,maxi,maxfd,listenfd,connfd,sockfd;
  int   nready,client;
  ssize_t n;
  fd_set rset,allset;
  char line[MAXLNE];
  socklen_t clilen;
  struct sockaddr_in cliaddr,servaddr;
  const int on=1;
  listenfd=Socket(AF_INET,SOCK_STREAM,0);
  if(setsockopt (listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))=-1);    err_ret(″setsockopt error″);
  bzero(&servaddr,sizeof(servaddr));
  servaddr.sin_family =AF_INET;
  servaddr.sin_addr.s_addr =htonl(INADDR_ANY);
  servaddr.sin_port =htons(SERV_PORT);
  Bind(listenfd,(SA*)&servaddr,sizeof(servaddr));
  Listen(listenfd,LISTENQ);
  maxfd =listenfd; //初始化
  maxi = -1; //最大下標
  for(i=0;i<FD_SETSIZE;i++)
    client[i]=-1; //-1表示可用
  FD_ZERO(&allset);
  FD_SET(listenfd,&allset);
  for(;;){
   rset=allset; //結構賦值
    nready=Select(maxfd+1,&rset,NULL,NULL,NULL);
   if (FD_ISSET(listenfd,&rset)){ //有使用者連線
    clilen=sizeof(cliaddr);
    connfd=Accept(listenfd,(SA*)&cliaddr,&clilen);
    for(i=0;i<FD_SETSIZE;i++)
      if(client[i]< 0 {
       client[i]=connfd;//儲存套接字
       break;
      }
    if(i=FD_SETSIZE);
     err_quit(″too many clients″);
    FD_SET (connfd,&allset); //增加套接字到讀描述字集
    if(connfd>maxfd);
      maxfd=connfd;
    if(i>maxi);
      maxi=i;
    if(--nready<=0)
     continue;
  }
  for (i=0;i<=maxi;i++){ //檢查所有使用者連線
    if((sockfd=client[i])<0)
     continue;
    if(FD_ISSET(sockfd,&rset))
{
     if((n=Readline(sockfd,line,MAXLINE))=0)
{               //使用者關閉連線
       Close(sockfd);
       FD_CLR(sockfd,&allset);
       client[i]=-1
      }else
        printf(″%s″,line);
       Broadcast(client,maxi,line,n);
       if (--nready<=0)
         break;
        }
      }
    }
  }
  //將聊天內容傳送到所有已連線的使用者
  Broadcast(int client,int maxi,char*str,size_t n)
  {
      int i;
      int sockfd;
      for(i=0;i<maxi,i++){
       if((sockfd=client[i])<0)
       continue;
      writen(sockfd,str,n);
    }
  }

  2.客戶端

  為了實現非阻塞通訊,利用非同步選擇函式 WSAAsynSelect() 將網路事件與 WinSock 訊息聯絡起來,由該函式註冊一些使用者感興趣的網路事件(如接收緩衝區滿,允許傳送資料,請求連線等)。當這些被註冊的網路事件發生時,應用程式的相應函式將接收到有關訊息。

  應用程式在使用Windows Sockets DLL 之前必須先呼叫啟動函式WSAStartup() ,該函式的功能有兩點:一是由應用程式指定所要求Windows Sockets DLL 版本;二是獲得系統 Windows Sockets DLL的一些技術細節。每一個WSAStartup()函式必須和一個WSACleanup()函式對應,當應用程式終止時,必須呼叫 WSACleanup()將自己從OLL 中登出。

  客戶端程式用 VC++6.0在Windows98 作業系統下設計,程式主框架由 AppWizard 生成,客戶端核心程式碼在 CTalkDialog類中。只有一個 socket 變數m_hSocket 與服務端進行連線。連線建立好後,通過此 SOCKET傳送和接收資訊。

  手工加入CTalkDialog::OnSockConnect() ,完成基本套接字程式設計,為客戶程式申請一個套接字,並將該套接字與指定伺服器繫結,然後向伺服器發出連線請求,啟動非同步選擇函式等待伺服器的響應。

  void CTalkDialog:″OnSockConnect()
{
   struct sockaddr_in servaddr;
   WSADATA wsaData;
   if(WSAStartup(WINSOCK_VERSION,&wsaData));
   {
    MessageBox(″Could not load Windows Sockets DLL,″,NULL,MB_Ok);
    return;
  }
m_hSocket=socket(AF_INET,SOCK_STREAM,0);
   memset(&servaddr,0,sizeof(servaddr));
   servaddr.sin_family=AF_INET;
   //m_iPort,m_csIP 為通過註冊對話方塊返回的埠號和IP地址
  servaddr.sin_port=htons(m_iport);
  servaddr.sin_addr.S_un.S_addr=inet_addr(m_csIP);
  if (connect(m_hSocket,(SA*)& servaddr,sizeof (servaddr))!=0){
     AfxMessageBox(″連線伺服器失敗!″);
   GetDlgItem(IDC_BUTTON_OUT)->EnableWindow(FALSE);
     GetDlgItem(IDC_BUTTON_IN)->EnableWindowTRUE
     GetDlgItem(IDC_EDIT-SEND)->EnableWindow(FALSE);
     UpdateData(FALSE);
  }
  else{
     GetDlgItem(IDC_BUTTON_IN)->EnableWindow(FALSE);
   GetDlgItem(IDC_BUTTON_OUT)->EnableWindow(TRUE);
   GetDlgItem(IDC_EDIT-SEND)->EnableWindow(TRUE);
     int iErrorCode=WSAAsyncSelect(m_hSocket,m_hWnd,WM_SOCKET_READ,   FD_READ);
     if(iErrorCode=SOCKET_ERROR)
      MessageBox(″WSAAsyncSelect failed on socket″,NULL,MB_OK);
    }
  }  手工加入CTalkDialog::OnSockRead(), 響應WinSock 發來的訊息
  LRESULT CTalkDialog::OnSockRead(WPARAM wParamLPARAM 1Param)
  {
    int iRead;
    int iBufferLength;
    int iEnd;
    int iSpaceRemain;
    char chIncomingData[100];
    iBufferLength=iSpaceRemain=sizeof (chIncomingData);
   iEnd=0;
   iSpaceRemain-=iEnd;
   iRead=recv(m_hSocket,(LPSTR)(chIncomingData+iEnd),iSpaceRemain,0);
   iEnd+=iRead;
   if (iRead=SOCKET_ERROR)
    AfxMessageBox(″接收資料錯誤!″);
   chIncomingData[iEnd]=‘/0’;
   if(lstrlen(chIncomingData)!=0)
{
  m_csRecv=m_csRecv+chIncomingData;
  GetDlgItem(IDC_EDIT_RECV)->SetWindowText((LPCSTR)m_csRecv);
  CEdit*pEdit;
  pEdit=(CEdit*)GetDlgItem(IDC_EDIT_RECV);
  int i=pEdit->GetLineCount();
     pEdit->LineScroll(i,0);
    }
    return(OL);
  }
  手工加入CTalkDialog::OnSend(),將指定緩衝區中的資料傳送出去。
  void CTalkDialog::OnSend()
   {
    UpdateData();
    m_csSend.TrimLeft();
    m_csSend.TrimRight();
    if(!m_csSend.IsEmpty())
   {
m_csSend=m_csName+″:″+m_csSend+″/r/n″;
   int nCharSend=send(m_hSocket,m_csSend,m_csSend.GetLength(),0);
   if(nCharSend=SOCKET_ERROR)
    MessageBox(″ 傳送資料錯誤!″,NULL,MB_OK);
   }
   m_csSend=″″;
   UpdateData(FALSE);
   CWnd *pEdit=GetDlgItem(IDC_EDIT_SEND);
   pEdit->SetFocus();
   return;
  }
  通過ClassWizard 增加 virtual function PreTranslateMessage(), 控制當按回車鍵時呼叫OnSend(),而不是執行預設按鈕的動作。
  BOOL CTalkDialog::PreTranslateMessage(MSG*pMsg)
  {
if(pMsg-> message==WM_KEYDOWN && pMsg-> wParam=VK_RETURN){
     OnSend();
    }
    return CDialog::PreTranslateMessage(pMsg);
  }

  為了簡化設計,使用者名稱在客戶端控制,服務端只進行簡單的接收資訊和“廣播”此資訊,不進行名字校驗,也就是說,可以有同名客戶登入到服務端。這個程式設計雖然簡單,但是已經具備了聊天室的最基本的功能。服務端程式在SCO OpenServer 5.0.5 下編譯通過,客戶端程式在VC++6.0 下編譯通過,在使用TCP/IP 協議的區域網上執行良好。

  上述例項僅用於說明通過 Socket 程式設計介面能方便地實現跨系統通訊,而這種跨系統通訊在金融行業中有著越來越廣泛的應用,利用它可以實現各種多媒體查詢,資料傳輸,網路通訊等功能。因此對跨系統通訊的探討是非常必要和有意義的,希望上述探討對大家能有所啟發。