轉自:http://www.cnblogs.com/jsjkandy/archive/2008/08/12/1266345.html
一、前言
在前篇《GPRS開發(fā)系列文章之進階篇》里,我主要詳細講解了客戶端進行GPRS連接的常用API,并對GPRSdemo測試程序中的連接類ConnManager中的一些重要函數做了說明,最后稍微提及了下服務器端要用到的一些類庫。今天,在這篇實戰(zhàn)篇中,我將在理解前兩篇的基礎上,結合客戶端與服務器端,向大家介紹這篇GPRS開發(fā)之實戰(zhàn)篇,向大家演示如何利用GPRS開發(fā)一個客戶端與服務器端互相通信的程序,主要介紹SOCKET編程的原理和SOCKET應用API,并在最后提供本實戰(zhàn)篇的源代碼下載。最后還是那句老話,歡迎指點,共同提高!
二、實戰(zhàn)系列篇詳解
1. 開發(fā)環(huán)境
a) 客戶端:EVC4;
b) 服務器端:VS2005(C#);
2. 運行環(huán)境
a) 客戶端:ppc 2003(winCE4.2) for mobile或以上版本;
b) 服務器端:WINXP/SERVER 2003等
3. 客戶端和服務器端通信詳解 本文章的主要目的是利用GPRS連接編寫一個利用TCP協(xié)議進行通信的程序,而在上篇文章已解決了GPRS連接的問題,因此剩下的主要就是我們都比較熟悉的SOCKET編程了,由于客戶端和服務器端進行SOCKET通信的原理相同,所以我將他們放到一起進行講解,主要介紹SOCKET編程的一些原理及要點,然后貼出部分比較重要的代碼供大家參考。 首先,介紹些要了解SOCKET編程的一些核心概念: 我們知道在這種通信程序中,一般客戶端和服務器端是分開的(本機通信可以看作是一種特例),客戶端一旦和服務器端建立連接成功后就可以透明的傳輸數據和接收數據了。那么我們的程序在建立了GPRS連接到Internet后是如何訪問到我們指定的服務器的呢?通信過程又是怎么控制的呢?那么首先看第一個概念,進程通信。 進程通信:這里的進程通信包括兩種情況,一種是同一機器的不同進程之間的通信,另一種是在同一網絡中(不同網絡通過路由進行連接還是可以看成同一網絡)的不同機器的不同進程之間的通信。在同一臺機器中的進程通信問題,由于每個進程都在自己的地址范圍內運行,為保證兩個相互通信的進程之間既互不干擾又協(xié)調一致工作,操作系統(tǒng)為進程通信提供了相應設施,如管道(pipe)、命名管道(named pipe)和信號量(semaphore)等。各個進程要進行通信首先要解決進程的標識問題,在同一機器中,可用process ID來唯一標識每個單獨的進程,我們可以在任務管理器中進行查看,每個進行都有自己唯一的標志符。如果沒有看到的,可以在任務管理器中點擊“查看”,然后點擊“選擇列”,在出現(xiàn)的對話框中選中“PID(進程標志符)”這一欄,確定后我們就可以看到每個進程的PID了,。而在網絡中的不同電腦要進行通信,首先要經過網絡間的協(xié)議轉換然后再尋址找到我們的目的機器,最后根據特定標志符找到特定的進程,于是我們的客戶端進程就可以和服務器進程進行網間進程通信了,在這一過程中扮演著重要角色的就是TCP/IP協(xié)議 TCP/IP協(xié)議:TCP/IP是一個協(xié)議簇,它包括網絡接口層,網絡層、傳輸層和應用層,網絡層中有負責因特網地址(IP地址)與底層網絡地址之間進行轉換的地址解析協(xié)議ARP和反向地址解析協(xié)議RARP。同時也包括對主機和網關進行差錯報告、控制和進行請求/應答的IGMP協(xié)議和網絡層的核心協(xié)議IP協(xié)議。在TCP/IP協(xié)議簇中的傳輸層中,提供了進程間的通信的TCP和UDP協(xié)議,這兩個協(xié)議分別提供了了可靠的面向連接的傳輸服務和簡單高效的無連接傳輸服務,我們最需要了解的就是傳輸層中的這兩個協(xié)議。 IP地址:因特網的IP協(xié)議提供了一種整個互聯(lián)網中通用的地址格式,并在同一管理下進行IP地址的分配并保證其唯一性,以確保每臺因特網主機(路由器)對應一個IP地址。 端口:網絡中可以被命名和尋址的通信端口,是操作系統(tǒng)可分配的一種資源。按照OSI七層協(xié)議的描述,傳輸層與網絡層在功能上的最大區(qū)別是傳輸層提供進程通信能力。從這個意義上講,網絡通信的最終地址就不僅僅是主機地址了,還包括可以描述進程的某種標識符。為此,TCP/IP協(xié)議提出了協(xié)議端口(protocol port,簡稱端口)的概念,用于標識通信的進程。 端口是一種抽象的軟件結構(包括一些數據結構和I/O緩沖區(qū))。應用程序(即進程)通過系統(tǒng)調用與某端口建立連接(binding)后,傳輸層傳給該端口的數據都被相應進程所接收,相應進程發(fā)給傳輸層的數據都通過該端口輸出。在TCP/IP協(xié)議的實現(xiàn)中,端口類似于一般的I/O操作,進程獲取一個端口,相當于獲取本地唯一的I/O文件,可以用一般的讀寫原語訪問之,如我們通過指定端口讀取GPS信息等。 類似于文件描述符,每個端口都擁有一個叫端口號(port number)的整數型標識符,用于區(qū)別不同端口。由于TCP/IP傳輸層的兩個協(xié)議TCP和UDP是完全獨立的兩個軟件模塊,因此各自的端口號也相互獨立,如TCP有一個255號端口,UDP也可以有一個255號端口,二者并不沖突。因此當我們通過指定的IP地址和端口號就可以找到唯一標志我們的進程了。 在了解了上述基礎知識后,我們可以簡單做個回顧,總結下整個連接的過程。本文介紹的客戶端與服務器端通信是典型的C/S模式,客戶端在請求服務器端提供特定服務后,服務器端接收請求并提供相應服務。在TCP/IP網絡應用中,C/S模式中服務器端是采取主動的方式,首先啟動,并根據請求提供相應服務。 服務器端: 1. 打開一通信通道并告知本地主機,它愿意在某一公認地址上接收客戶請求; 2. 等待客戶請求到達該端口; 3. 接收到服務請求,處理該請求并發(fā)送應答信號 4. 返回第二步,等待另一客戶請求。 5. 關閉服務器 客戶端: 1. 打開一通信通道,并連接到服務器所在主機的特定端口; 2. 向服務器發(fā)服務請求報文,等待并接收應答;繼續(xù)提出請求...... 3. 請求結束后關閉通信通道并終止。 客戶端主界面如圖所示:
 【代碼部分】 客戶端主要功能為建立服務器的連接,和服務器互相通信(發(fā)送數據和接收數據),其中用到的關鍵的核心類為CConnectionManager類和CTCPClient_CE類,而服務器端主要負責偵聽同時也發(fā)送數據給客戶端,用到的核心類為ConnectionManager,客戶端和服務器利用socket通信步驟如下: 第一步:實例化套接字。 用WINSOCK API方式如下(客戶端):
bool CTCPClient_CE::Connect() { struct sockaddr_in addr; int err;
addr.sin_family = AF_INET; addr.sin_port = htons(m_port); //此處要將雙字節(jié)轉換成單字節(jié) char ansiRemoteHost[255]; ZeroMemory(ansiRemoteHost,255); WideCharToMultiByte(CP_ACP,WC_COMPOSITECHECK,m_remoteHost,wcslen(m_remoteHost) ,ansiRemoteHost,wcslen(m_remoteHost),NULL,NULL);
addr.sin_addr.s_addr=inet_addr(ansiRemoteHost);
//創(chuàng)建TCP套接字 m_socket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); if (m_socket == INVALID_SOCKET) { return FALSE; } //此時采用同步連接方式,connect直接返回成功或是失敗 err = connect(m_socket,(struct sockaddr *)&addr,sizeof(addr)); if (err == SOCKET_ERROR) { return FALSE; } //設置通訊模式為異步模式 DWORD ul= 1; ioctlsocket(m_socket,FIONBIO,&ul); return TRUE; }
服務器端用.net平臺如下:
private void StartToListen(object sender, DoWorkEventArgs e) { try { this.listenerSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); this.listenerSocket.Bind(new IPEndPoint(this.serverIP, this.serverPort)); this.listenerSocket.Listen(200); while (bListen) this.CreateNewClientManager(this.listenerSocket.Accept()); } catch(SocketException ex) { if (ex.ErrorCode == 10004) return; else { throw ex; } } }
第二步,進行偵聽。獲取數據,發(fā)送數據。 客戶端發(fā)送數據:
bool CTCPClient_CE::SendData(const char * buf , int len) { int nBytes = 0; int nSendBytes=0; while (nSendBytes < len) { nBytes = send(m_socket,buf+nSendBytes,len-nSendBytes,0); if (nBytes==SOCKET_ERROR ) { int iErrorCode = WSAGetLastError(); //觸發(fā)socket的Error事件 OnError(m_pOwnerWnd,iErrorCode); //觸發(fā)與服務器端斷開連接事件 OnDisConnect(m_pOwnerWnd); //關閉socket Close(); return FALSE; }
nSendBytes = nSendBytes + nBytes; if (nSendBytes < len) { Sleep(1000); } } return TRUE; }
服務器端發(fā)送數據:
private bool SendCommandToClient(Command cmd) { try { semaphor.WaitOne(); string strSentInfo = string.Empty; strSentInfo = string.Format("發(fā)送者:{0}{1}內容:{2}", cmd.SenderName, Environment.NewLine, cmd.MetaData);
byte[] buffer = new byte[256]; buffer = System.Text.Encoding.Default.GetBytes(strSentInfo); this.networkStream.Write(buffer, 0, buffer.GetLength(0)); this.networkStream.Flush(); semaphor.Release(); return true; } catch { semaphor.Release(); return false; } }
可以看出,雖然他們語法不相同,語義卻相同。在實例化一個套接字對象socket時,我們都要指定協(xié)議簇,套接字類型(有流式套接字、數據報套接字和原始套接字等類型)和傳輸協(xié)議,成功獲取套接字后服務器端要與指定端口綁定(Bind),然后進行監(jiān)聽(Listen),并調用accept ()方法。Accept()以同步方式從偵聽套接字的連接請求隊列中提取第一個掛起的連接請求,然后創(chuàng)建并返回新的 Socket,而客戶端完成套接字的實例化后,開始調用Select()函數判斷是否有讀事件發(fā)生,如果有則調用Recv()函數獲取從服務器端發(fā)來的數據或者調用Send()函數來向服務器發(fā)送數據。 客戶端主要函數為: bool Open(CWnd * pWnd); bool Connect(); bool SendData(const char * buf , int len); bool Close(); 服務器端主要函數為: void StartToListen(object sender, DoWorkEventArgs e); void CreateNewClientManager(Socket socket); void StartReceive(object sender, DoWorkEventArgs e); void SendCommandToClient(Command cmd);
三、引用(參考)文章
1. http://bbs./viewthread.php?tid=198859(socket編程原理-很不錯);
2.http://msdn.microsoft.com/zh-cn/library/system.net.sockets.socket_methods.aspx(msdn 開發(fā)中心socket部分)
3.實戰(zhàn)篇源代碼下載:客戶端 服務器端
|