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