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