天天看點

Visual C#網絡程式設計之TCP

注:不是原創!

前一篇《Visual C#.Net網絡程式開發之Socket》中說到:支援Http、Tcp和Udp的類組成了TCP/IP三層模型(請求響應層、應用協定層、傳輸層)的中間層-應用協定層,該層的類比位于最底層的Socket類提供了更高層次的抽象,它們封裝 TCP 和 UDP 套接字的建立,不需要處理連接配接的細節,這使得我們在編寫套接字級别的協定時,可以更多地嘗試使用

TCPClient 、 UDPClient和TcpListener,而不是直接向 Socket 中寫。它們之間的這種層次關系示意如下:

  可見,TcpClient 類基于 Socket 類建構,這是它能夠以更高的抽象程度提供 TCP 服務的基礎。正因為這樣,許多應用層上的通訊協定,比如FTP(File Transfers Protocol)檔案傳輸協定、HTTP(Hypertext Transfers Protocol)超文本傳輸協定等都直接建立在TcpClient等類之上。

  TCPClient 類使用 TCP 從 Internet 資源請求資料。TCP 協定建立與遠端終結點的連接配接,然後使用此連接配接發送和接收資料包。TCP 負責確定将資料包發送到終結點并在資料包到達時以正确的順序對其進行組合。

  從名字上就可以看出,TcpClient類專為用戶端設計,它為 TCP 網絡服務提供用戶端連接配接。TcpClient 提供了通過網絡連接配接、發送和接收資料的簡單方法。

  若要建立 TCP 連接配接,必須知道承載所需服務的網絡裝置的位址(IPAddress)以及該服務用于通訊的 TCP 端口 (Port)。Internet 配置設定号碼機構 (Internet Assigned Numbers Authority, IANA) 定義公共服務的端口号(你可以通路 http://www.iana.org/assignments/port-numbers獲得這方面更詳細的資料)。IANA 清單中所沒有的服務可使用 1,024 到 65,535 這一範圍中的端口号。要建立這種連接配接,你可以選用TcpClient類的三種構造函數之一:

  1、public TcpClient()當使用這種不帶任何參數的構造函數時,将使用本機預設的ip位址并将使用預設的通信端口号0。這樣情況下,如果本機不止一個ip位址,将無法選擇使用。以下語句示例了如何使用預設構造函數來建立新的 TcpClient:

TcpClient tcpClientC = new TcpClient();

  2、public TcpClient(IPEndPoint)使用本機IPEndPoint建立TcpClient的執行個體對象。上一篇介紹過了,IPEndPoint将網絡端點表示為IP位址和端口号,在這裡它用于指定在建立遠端主機連接配接時所使用的本地網絡接口(IP 位址)和端口号,這個構造方法為使用本機IPAddress和Port提供了選擇餘地。下面的語句示例了如何使用本地終結點建立 TcpClient 類的執行個體:

IPHostEntry ipInfo=Dns.GetHostByName("www.tuha.net");//主機資訊

IPAddressList[] ipList=ipInfo.AddressList;//IP位址數組

IPAddress ip=ipList[0];//多IP位址時一般用第一個

IPEndPoint ipEP=new IPEndPoint(ip,4088);//得到網絡終結點

try{

TcpClient tcpClientA = new TcpClient(ipLocalEndPoint);

}

catch (Exception e ) {

Console.WriteLine(e.ToString());

}

  到這裡,你可能會感到困惑,用戶端要和服務端建立連接配接,所指定的IP位址及通信端口号應該是遠端伺服器的呀!事實上的确如此,使用以上兩種構造函數,你所實作的隻是TcpClient執行個體對象與IP位址和Port端口的綁定,要完成連接配接,你還需要顯式指定與遠端主機的連接配接,這可以通過TcpClient類的Connect方法來實作, Connet方法使用指定的主機名和端口号将用戶端連接配接到 遠端主機:

  1)、public void Connect(IPEndPoint); 使用指定的遠端網絡終結點将用戶端連接配接到遠端 TCP 主機。

public void Connect(IPAddress, int); 使用指定的 IP 位址和端口号将用戶端連接配接到 TCP 主機。

public void Connect(string, int); 将用戶端連接配接到指定主機上的指定端口。

  需要指出的是,Connect方法的所有重載形式中的參數IPEndPoint網絡終結點、IPAddress以及表現為string的Dns主機名和int指出的Port端口均指的是遠端伺服器。

  以下示例語句使用主機預設IP和Port端口号0與遠端主機建立連接配接:

TcpClient tcpClient = new TcpClient();//建立TcpClient對象執行個體

try{

tcpClient.Connect("www.contoso.com",11002);//建立連接配接

}

catch (Exception e ){

Console.WriteLine(e.ToString());

}

3、public TcpClient(string, int);初始化 TcpClient 類的新執行個體并連接配接到指定主機上的指定端口。與前兩個構造函數不一樣,這個構造函數将自動建立連接配接,你不再需要額外調用Connect方法,其中string類型的參數表示遠端主機的Dns名,如:www.tuha.net。

  以下示例語句調用這一方法實作與指定主機名和端口号的主機相連:

try{

TcpClient tcpClientB = new TcpClient("www.tuha.net", 4088);

}

catch (Exception e ) {

Console.WriteLine(e.ToString());

}

  前面我們說,TcpClient類建立在Socket之上,在Tcp服務方面提供了更高層次的抽象,展現在網絡資料的發送和接受方面,是TcpClient使用标準的Stream流處理技術,使得它讀寫資料更加友善直覺,同時,.Net架構負責提供更豐富的結構來處理流,貫穿于整個.Net架構中的流具有更廣泛的相容性,建構在更一般化的流操作上的通用方法使我們不再需要困惑于檔案的實際内容(HTML、XML 或其他任何内容),應用程式都将使用一緻的方法(Stream.Write、Stream.Read) 發送和接收資料。另外,流在資料從 Internet 下載下傳的過程中提供對資料的即時通路,可以在部分資料到達時立即開始處理,而不需要等待應用程式下載下傳完整個資料集。.Net中通過NetworkStream類實作了這些處理技術。

  NetworkStream 類包含在.Net架構的System.Net.Sockets 命名空間裡,該類專門提供用于網絡通路的基礎資料流。NetworkStream 實作通過網絡套接字發送和接收資料的标準.Net 架構流機制。NetworkStream 支援對網絡資料流的同步和異步通路。NetworkStream 從 Stream 繼承,後者提供了一組豐富的用于友善網絡通訊的方法和屬性。

  同其它繼承自抽象基類Stream的所有流一樣,NetworkStream網絡流也可以被視為一個資料通道,架設在資料來源端(客戶Client)和接收端(服務Server)之間,而後的資料讀取及寫入均針對這個通道來進行。

  .Net架構中,NetworkStream流支援兩方面的操作:

  1、 寫入流。寫入是從資料結構到流的資料傳輸。

  2、讀取流。讀取是從流到資料結構(如位元組數組)的資料傳輸。

  與普通流Stream不同的是,網絡流沒有目前位置的統一概念,是以不支援查找和對資料流的随機通路。相應屬性CanSeek 始終傳回 false,而 Seek 和 Position 方法也将引發 NotSupportedException。

  基于Socket上的應用協定方面,你可以通過以下兩種方式擷取NetworkStream網絡資料流:

  1、使用NetworkStream構造函數:public NetworkStream(Socket, FileAccess, bool);(有重載方法),它用指定的通路權限和指定的 Socket 所屬權為指定的 Socket 建立 NetworkStream 類的新執行個體,使用前你需要建立Socket對象執行個體,并通過Socket.Connect方法建立與遠端服務端的連接配接,而後才可以使用該方法得到網絡傳輸流。示例如下:

Socket s=new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);//建立用戶端Socket對象執行個體

try{

s.Connect("www.tuha.net",4088);//建立與遠端主機的連接配接

}

catch(Exception e){

MessageBox.show("連接配接錯誤:" +e.Message);

}

try{

NetworkStream stream=new NetworkStream(s,FileAccess.ReadWrite,false);//取得網絡傳輸流

}

  2、通過TcpClient.GetStream方法:public NetworkStream etStream();它傳回用于發送和接收資料的基礎網絡流NetworkStream。GetStream 通過将基礎 Socket 用作它的構造函數參數來建立 NetworkStream 類的執行個體。使用前你需要先創TcpClient對象執行個體并建立與遠端主機的連接配接,示例如下:

TcpClient tcpClient = new TcpClient();//建立TcpClient對象執行個體

Try{

tcpClient.Connect("www.tuha.net",4088);//嘗試與遠端主機相連

}

catch(Exception e){

MessageBox.Show("連接配接錯誤:"+e.Message);

}

try{

NetworkStream stream=tcpClient.GetStream();//擷取網絡傳輸流

}

catch(Exception e)

{

MessageBox.Show("TcpClient錯誤:"+e.Message);

}

  通過以上方法得到NetworkStream網絡流之後,你就可以使用标準流讀寫方法Write和Read來發送和接受資料了。

  以上是.Net下使用TcpClient類實作用戶端程式設計的技術資料,為了向用戶端提供這些服務,我們還需要編制相應的服務端程式,前一篇《Visual C#.Net網絡程式開發-Socket篇》上曾經提到, Socket作為其他網絡協定的基礎,既可以面向用戶端開發,也可以面向服務端開發,在傳輸層面上使用較多,而在應用協定層面上,用戶端我們采用建構于Socket類之上的TcpClient取代Socket;相應地,建構于Socket之上的TcpListener提供了更高理念級别的 TCP 服務,使得我們能更友善地編寫服務端應用程式。正是因為這樣的原因,像FTP 和 HTTP 這樣的應用層協定都是在 TcpListener 類的基礎上建立的。

  .Net中的TCPListener 用于監視TCP 端口上的傳入請求,通過綁定本機IP位址和相應端口(這兩者應與用戶端的請求一緻)建立TcpListener對象執行個體,并由Start方法啟動偵聽;當TcpListener偵聽到使用者端的連接配接後,視用戶端的不同請求方式,通過AcceptTcpClient 方法接受傳入的連接配接請求并建立 TcpClient 以處理請求,或者通過AcceptSocket 方法接受傳入的連接配接請求并建立 Socket 以處理請求。最後,你需要使用 Stop 關閉用于偵聽傳入連接配接的 Socket,你必須也關閉從 AcceptSocket 或 AcceptTcpClient 傳回的任何執行個體。這個過程詳細解說如下:

  首先,建立TcpListener對象執行個體,這通過TcpListener類的構造方法來實作:

public TcpListener(port);//指定本機端口

public TcpListener(IPEndPoint)//指定本機終結點

public TcpListener(IPAddress,port)//指定本機IP位址及端口

  以上方法中的參數在前面多次提到,這裡不再細述,唯一需要提醒的是,這些參數均針對服務端主機。下面的示例示範建立 TcpListener 類的執行個體:

IPHostEntry ipInfo=Dns.Resolve("127.0.0.1");//主機資訊

IPAddressList[] ipList=ipInfo.IPAddressList;//IP數組

IPAddress ip=ipList[0];//IP

try{

TcpListener tcpListener = new TcpListener(ipAddress, 4088);//建立TcpListener對象執行個體以偵聽使用者端連接配接

}

catch ( Exception e){

MessageBox.Show("TcpListener錯誤:"+e.Message);

}

  随後,你需要調用Start方法啟動偵聽:

public void Start();

  其次,當偵聽到有使用者端連接配接時,需要接受挂起的連接配接請求,這通過調用以下兩方法之一來完成連接配接:

public Socket AcceptSocket();

public TcpClient AcceptTcpClient();

  前一個方法傳回代表用戶端的Socket對象,随後可以通過Socket 類的 Send 和 Receive 方法與遠端計算機通訊;後一個方法傳回代表用戶端的TcpClient對象,随後使用上面介紹的 TcpClient.GetStream 方法擷取 TcpClient 的基礎網絡流 NetworkStream,并使用流讀寫Read/Write方法與遠端計算機通訊。

  最後,請記住關閉偵聽器:public void Stop();

  同時關閉其他連接配接執行個體:public void Close();

下面的示例完整展現了上面的過程:

bool done = false;

TcpListener listener = new TcpListener(13);// 建立TcpListener對象執行個體(13号端口提供時間服務)

listener.Start();//啟動偵聽

while (!done) {//進入無限循環以偵聽使用者連接配接

TcpClient client = listener.AcceptTcpClient();//偵聽到連接配接後建立用戶端連接配接TcpClient

NetworkStream ns = client.GetStream();//得到網絡傳輸流

byte[] byteTime = Encoding.ASCII.GetBytes(DateTime.Now.ToString());//預發送的内容(此為服務端時間)轉換為位元組數組以便寫入流

try {

ns.Write(byteTime, 0, byteTime.Length);//寫入流

ns.Close();//關閉流

client.Close();//關閉用戶端連接配接

}

catch (Exception e) {

MessageBox.Show("流錯誤:"+e.Message)

}

  綜合運用上面的知識,下面的執行個體實作了簡單的網絡通訊-雙機互連,針對用戶端和服務端分别編制了應用程式。用戶端建立到服務端的連接配接,向遠端主機發送連接配接請求連接配接信号,并發送交談内容;遠端主機端接收來自客戶的連接配接,向用戶端發回确認連接配接的信号,同時接收并顯示用戶端的交談内容。在這個基礎上,發揮你的創造力,你完全可以開發出一個基于程式語言(C#)級的聊天室!

  用戶端主要源代碼:

public void SendMeg()//發送資訊

{

try

{

int port=Int32.Parse(textBox3.Text.ToString());//遠端主機端口

try

{

tcpClient=new TcpClient(textBox1.Text,port);//建立TcpClient對象執行個體 }

catch(Exception le)

{

MessageBox.Show("TcpClient Error:"+le.Message);

}

string strDateLine=DateTime.Now.ToShortDateString()+" "+DateTime.Now.ToLongTimeString();//得到發送時用戶端時間

netStream=tcpClient.GetStream();//得到網絡流

sw=new StreamWriter(netStream);//建立TextWriter,向流中寫字元

string words=textBox4.Text;//待發送的話

string content=strDateLine+words;//待發送内容

sw.Write(content);//寫入流

sw.Close();//關閉流寫入器

netStream.Close();//關閉網絡流

tcpClient.Close();//關閉用戶端連接配接

}

catch(Exception ex)

{

MessageBox.Show("Sending Message Failed!"+ex.Message);

}

textBox4.Text="";//清空

}

  伺服器端主要源代碼:

public void StartListen()//偵聽特定端口的使用者請求

{

//ReceiveMeg();

isLinked=false; //連接配接标志

try

{

int port=Int32.Parse(textBox1.Text.ToString());//本地待偵聽端口

serverListener=new TcpListener(port);//建立TcpListener對象執行個體

serverListener.Start(); //啟動偵聽

}

catch(Exception ex)

{

MessageBox.Show("Can‘t Start Server"+ex.Message);

return;

}

isLinked=true;

while(true)//進入無限循環等待使用者端連接配接

{

try

{

tcpClient=serverListener.AcceptTcpClient();//建立用戶端連接配接對象

netStream=tcpClient.GetStream();//得到網絡流

sr=new StreamReader(netStream);//流讀寫器

}

catch(Exception re)

{

MessageBox.Show(re.Message);

}

string buffer="";

string received="";

received+=sr.ReadLine();//讀流中一行

while(received.Length!=0)

{

buffer+=received;

buffer+="/r/n";

//received="";

received=sr.ReadLine();

}

listBox1.Items.Add(buffer);//顯示

//關閉

sr.Close();

netStream.Close();

tcpClient.Close();

}

}