使用SOCKET實現TCP/IP協議的通訊
一、原理:
首先要理解基本的原理,2臺電腦間實現TCP通訊,首先要建立起連線,在這裡要提到伺服器端與客戶端,兩個的區別通俗講就是主動與被動的關係,兩個人對話,肯定是先有人先發起會話,要不然誰都不講,談什麼話題,呵呵!一樣,TCPIP下建立連線首先要有一個伺服器,它是被動的,它只能等待別人跟它建立連線,自己不會去主動連線,那客戶端如何去連線它呢,這裡提到2個東西,IP地址和埠號,通俗來講就是你去拜訪某人,知道了他的地址是一號大街2號樓,這個是IP地址,那麼1號樓這麼多門牌號怎麼區分,嗯!門牌號就是埠(這裡提到一點,我們訪問網頁的時候也是IP地址和埠號,IE預設的埠號是80),一個伺服器可以接受多個客戶端的連線,但是一個客戶端只能連線一臺伺服器,在連線後,伺服器自動劃分記憶體區域以分配各個客戶端的通訊,那麼,那麼多的客戶端伺服器如何區分,你可能會說,根據IP麼,不是很完整,很簡單的例子,你一臺計算機開3個QQ,伺服器怎麼區分?所以準確的說是IP和埠號,但是客戶端的埠號不是由你自己定的,是由計算機自動分配的,要不然就出現埠衝突了,說的這麼多,看下面的這張圖就簡單明瞭了。
在上面這張圖中,你可以理解為程式A和程式B是2個SOCKET程式,伺服器端程式A設定埠為81,已接受到3個客戶端的連線,計算機C開了2個程式,分別連線到E和D,而他的埠是計算機自動分配的,連線到E的埠為789,連線到D的為790。
瞭解了TCPIP通訊的基本結構後,接下來講解建立的流程,首先宣告一下我用的開發環境是Visual Studio2008版的,語言C#,元件System.Net.Sockets,流程的建立包括伺服器端的建立和客戶端的建立,如圖所示:
二、實現:
1.客戶端:
第一步,要建立一個客戶端物件TcpClient(名稱空間在System.Net.Sockets),接著,呼叫物件下的方法BeginConnect進行嘗試連線,入口引數有4個,address(目標IP地址),port(目標埠號),requestCallback(連線成功後的返調函式),state(傳遞引數,是一個物件,隨便什麼都行,我建議是將TcpClient自己傳遞過去),呼叫完畢這個函式,系統將進行嘗試連線伺服器。
第二步,在第一步講過一個入口引數requestCallback(連線成功後的返調函式),比如我們定義一個函式void Connected(IAsyncResult result),在連線伺服器成功後,系統會呼叫此函式,在函式裡,我們要獲取到系統分配的資料流傳輸物件(NetworkStream),這個物件是用來處理客戶端與伺服器端資料傳輸的,此物件由TcpClient獲得,在第一步講過入口引數state,如果我們傳遞了TcpClient進去,那麼,在函式裡我們可以根據入口引數state獲得,將其進行強制轉換TcpClient tcpclt = (TcpClient)result.AsyncState,接著獲取資料流傳輸物件NetworkStream ns = tcpclt.GetStream(),此物件我建議弄成全域性變數,以便於其他函式呼叫,接著我們將掛起資料接收等待,呼叫ns下的方法BeginRead,入口引數有5個,buff(資料緩衝),offset(緩衝起始序號),size(緩衝長度),callback(接收到資料後的返調函式),state(傳遞引數,一樣,隨便什麼都可以,建議將buff傳遞過去),呼叫完畢函式後,就可以進行資料接收等待了,在這裡因為已經建立了NetworkStream物件,所以也可以進行向伺服器傳送資料的操作了,呼叫ns下的方法Write就可以向伺服器傳送資料了,入口引數3個,buff(資料緩衝),offset(緩衝起始序號),size(緩衝長度)。
第三步,在第二步講過呼叫了BeginRead函式時的一個入口引數callback(接收到資料後的返調函式),比如我們定義了一個函式void DataRec(IAsyncResult result),在伺服器向客戶端傳送資料後,系統會呼叫此函式,在函式裡我們要獲得資料流(byte陣列),在上一步講解BeginRead函式的時候還有一個入口引數state,如果我們傳遞了buff進去,那麼,在這裡我們要強制轉換成byte[]型別byte[] data= (byte[])result.AsyncState,轉換完畢後,我們還要獲取緩衝區的大小int length = ns.EndRead(result),ns為上一步建立的NetworkStream全域性物件,接著我們就可以對資料進行處理了,如果獲取的length為0表示客戶端已經斷開連線。
具體實現程式碼,在這裡我建立了一個名稱為Test的類:
usingSystem;
using System.Collections.Generic;
using System.Net.Sockets;
namespace test
{
public class Test
{
protected TcpClient tcpclient = null; //全域性客戶端物件
protected NetworkStream networkstream = null;//全域性資料流傳輸物件
/// <summary>
/// 進行遠端伺服器的連線
/// </summary>
/// <param name="ip">ip地址</param>
/// <param name="port">埠</param>
public Test(string ip, int port)
{
networkstream = null;
tcpclient = new TcpClient(); //物件轉換成實體
tcpclient.BeginConnect(System.Net.IPAddress.Parse(ip), port, new AsyncCallback(Connected), tcpclient); //開始進行嘗試連線
}
/// <summary>
/// 傳送資料
/// </summary>
/// <param name="data">資料</param>
public void SendData(byte[] data)
{
if (networkstream != null)
networkstream.Write(data, 0, data.Length); //向伺服器傳送資料
}
/// <summary>
/// 關閉
/// </summary>
public void Close()
{
networkstream.Dispose(); //釋放資料流傳輸物件
tcpclient.Close(); //關閉連線
}
/// <summary>
/// 關閉
/// </summary>
/// <param name="result">傳入引數</param>
protected void Connected(IAsyncResult result)
{
TcpClient tcpclt = (TcpClient)result.AsyncState; //將傳遞的引數強制轉換成TcpClient
networkstream = tcpclt.GetStream(); //獲取資料流傳輸物件
byte[] data = new byte[1000]; //新建傳輸的緩衝
networkstream.BeginRead(data, 0, 1000, new AsyncCallback(DataRec), data); //掛起資料的接收等待
}
/// <summary>
/// 資料接收委託函式
/// </summary>
/// <param name="result">傳入引數</param>
protected void DataRec(IAsyncResult result)
{
int length = networkstream.EndRead(result); //獲取接收資料的長度
List<byte> data = new List<byte>(); //新建byte陣列
data.AddRange((byte[])result.AsyncState); //獲取資料
data.RemoveRange(length, data.Count - length); //根據長度移除無效的資料
byte[] data2 = new byte[1000]; //重新定義接收緩衝
networkstream.BeginRead(data2, 0, 1000, new AsyncCallback(DataRec), data2); //重新掛起資料的接收等待
//自定義程式碼區域,處理資料data
if (length == 0)
{
//連線已經關閉
}
}
}
}
2.伺服器端:
相對於客戶端的實現,伺服器端的實現稍複雜一點,因為前面講過,一個伺服器端可以接受N個客戶端的連線,因此,在伺服器端,有必要對每個連線上來的客戶端進行登記,因此伺服器端的程式結構包括了2個程式結構,第一個程式結構主要負責啟動伺服器、對來訪的客戶端進行登記和撤銷,因此我們需要建立2個類。
第一個程式結構負責伺服器的啟動與客戶端連線的登記,首先建立TcpListener網路偵聽類,建立的時候建構函式分別包括localaddr和port2個引數,localaddr指的是本地地址,也就是伺服器的IP地址,有人會問為什麼它自己不去自動獲得本機的地址?關於這個舉個很簡單的例子,伺服器安裝了2個網絡卡,也就有了2個IP地址,那建立伺服器的時候就可以選擇偵聽的使用的是哪個網路埠了,不過一般的電腦只有一個網路埠,你可以懶點直接寫個固定的函式直接獲取IP地址System.Net.Dns.GetHostAddresses(System.Net.Dns.GetHostName())[0],GetHostAddresses函式就是獲取本機的IP地址,預設選擇第一個埠於是後面加個[0],第2個引數port是真偵聽的埠,這個簡單,自己決定,如果出現埠衝突,函式自己會提醒錯誤的。第二步,啟動伺服器,TcpListener.Start()。第三步,啟動客戶端的嘗試連線,TcpListener.BeginAcceptTcpClient,入口2個引數,callback(客戶端連線上後的返調函式),state(傳遞引數,跟第二節介紹的一樣,隨便什麼都可以,建立把TcpListener自身傳遞過去),第四步,建立客戶端連線上來後的返調函式,比如我們建立個名為void ClientAccept(IAsyncResult result)的函式,函式裡,我們要獲取客戶端的物件,第三步裡講過我們傳遞TcpListener引數進去,在這裡,我們通過入口引數獲取它TcpListener tcplst = (TcpListener)result.AsyncState,獲取客戶端物件TcpClient bak_tcpclient = tcplst.EndAcceptTcpClient(result),這個bak_tcpclient我建議在類裡面建立個列表,然後把它加進去,因為下一個客戶端連線上來後此物件就會被沖刷掉了,客戶端處理完畢後,接下來我們要啟動下一個客戶端的連線tcplst.BeginAcceptTcpClient(new AsyncCallback(sub_ClientAccept), tcplst),這個和第三步是一樣的,我就不重複了。
第二個程式結構主要負責單個客戶端與伺服器端的處理程式,主要負責資料的通訊,方法很類似客戶端的程式碼,基本大同,除了不需要啟動連線的函式,因此這個程式結構主要啟動下資料的偵聽的功能、判斷斷開的功能、資料傳送的功能即可,在第一個程式第四步我們獲取了客戶端的物件bak_tcpclient,在這裡,我們首先啟動資料偵聽功能NetworkStream ns= bak_tcpclient.GetStream();ns.BeginRead(data, 0, 1024, new AsyncCallback(DataRec), data);這個跟我在第二節裡介紹的是一模一樣的(第二節第10行),還有資料的處理函式,資料傳送函式,判斷連線已斷開的程式碼與第二節也是一模一樣的,不過在這裡我們需要額外的新增一段程式碼,當判斷出連線已斷開的時候,我們要將客戶端告知第一個程式結構進行刪除客戶端操作,這個方法我的實現方法是在建立第二個程式結構的時候,將第一個程式結構當引數傳遞進來,判斷連線斷開後,呼叫第一個程式結構的公開方法去刪除,即從客戶端列表下刪除此物件。
第一個程式結構我們定義一個TSever的類,第二個程式結構我們一個TClient的類,程式碼如下:
public class TSever
{
public List<TClient> Clients = new List<TClient>(); //客戶端列表
private TcpListener tcplistener = null; //偵聽物件
/// <summary>
/// 建構函式
/// </summary>
/// <param name="port">偵聽埠</param>
public TSever(int port)
{
tcplistener = new TcpListener(System.Net.Dns.GetHostAddresses(System.Net.Dns.GetHostName())[0], port); //啟動偵聽
tcplistener.Start(); //啟動偵聽
tcplistener.BeginAcceptTcpClient(new AsyncCallback(ClientAccept), tcplistener); //開始嘗試客戶端的連線
}
private void ClientAccept(IAsyncResult result)
{
TcpListener tcplst = (TcpListener)result.AsyncState;
TcpClient bak_tcpclient = tcplst.EndAcceptTcpClient(result);
TClient bak_client = new TClient(bak_tcpclient, this);
Clients.Add(bak_client);
tcplst.BeginAcceptTcpClient(new AsyncCallback(ClientAccept), tcplst);
}
}
public class TClient
{
private TcpClient tcpclient = null; //客戶端物件
private NetworkStream networkstream = null; //資料傳送物件
private TSever m_Parent=null; //父級類
/// <summary>
/// 建構函式
/// </summary>
/// <param name="tcpclt">客戶端物件</param>
/// <param name="parent">父級</param>
public TClient(TcpClient tcpclt, TSever parent)
{
this.tcpclient = tcpclt;
this.m_Parent = parent;
string ip = ((IPEndPoint)tcpclient.Client.RemoteEndPoint).Address.ToString(); //獲取客戶端IP
string port = ((IPEndPoint)tcpclient.Client.RemoteEndPoint).Port.ToString(); //獲取客戶端埠
this.networkstream = tcpclt.GetStream(); //獲取資料傳輸物件
byte[] data = new byte[1024];
this.networkstream.BeginRead(data, 0, 1024, new AsyncCallback(DataRec), data);//啟動資料偵聽
}
/// <summary>
/// 資料接收
/// </summary>
/// <param name="result"></param>
private void DataRec(IAsyncResult result)
{
int length = networkstream.EndRead(result);
List<byte> data = new List<byte>();
data.AddRange((byte[])result.AsyncState);
byte[] data2 = new byte[1024];
networkstream.BeginRead(data2, 0, MaxRec, new AsyncCallback(DataRec), data2);
if (length == 0)
{
m_Parent.Clients.Remove(this); //告知父類刪除此客戶端
}
else
{
data.RemoveRange(length, data.Count - length);
//資料處理程式碼data
}
}
/// <summary>
/// 傳送資料
/// </summary>
/// <param name="data">資料</param>
/// <returns></returns>
public bool SendData(byte[] data)
{
networkstream.Write(data, 0, data.Length);
return (true);
}
}
摘自:http://hi.baidu.com/des_sky/item/a12969c83801acbc0d0a7bb2