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