TCP與UDP傳輸協議

目錄結構:html

contents structure [-]

TCP和UDP協議都是運行在傳輸層的協議,在OSI網絡的七層傳輸模型中,若是咱們把應用層、表示層、傳輸層統稱爲應用層(其實在TCP/IP模型中就是把應用層、表示層、傳輸層統稱爲應用層的),那麼咱們平時編寫的程序就屬於應用層。應用層位於傳輸層之上,當咱們須要使用TCP/UDP協議的時候,直接調用傳輸層留下的TCP/UDP協議接口就能夠了。在下面的示例中,咱們把發送發送數據的一方稱爲客戶端,接受數據的一方稱爲服務端。下面TCP和UDP的實現都採用了C#和Java代碼,這個筆者還須要提一點,就是在Android中使用鏈接的時候,因爲爲了防止網速太卡,因此Android中使用TCP鏈接的時候,應該在一個新建的線程中進行。編程

1 TCP協議和UDP協議的比較

1.1 TCP協議

TCP的全稱是Transmission Control Protocol (傳輸控制協議)

  • 傳輸控制協議,是一種面向鏈接的協議,相似打電話
  • 在通訊的整個過程當中保持鏈接
  • 保證了數據傳遞的可靠性和有序性
  • 是一種全雙工的字節流通訊方式
  • 服務器壓力比較大,資源消耗比較快,發送數據效率比較低
  • 點對點的傳輸協議

 

接下來筆者解釋一下上面的幾個概念:服務器

面向鏈接的傳輸協議:面向鏈接,好比A打電話給B,若是B接聽了,那麼A和B之間就的通話,就是面向鏈接的。網絡

可靠的傳輸協議:可靠的,一旦創建了鏈接,數據的發送必定可以到達,而且若是A說「你好嗎?」 B不會聽到「嗎你好」,這就是可靠地數據傳輸。socket

雙全工的傳輸協議:全雙工,這個理解起來也很簡單,A打電話給B,B接聽電話,那麼A能夠說話給B聽,一樣B也能夠給A說話,不可能只容許一我的說話.。ide

點對點的傳輸協議:點對點,這個看了上面的舉例相比你們都知道了,還要說一點的是,若是在A和B打電話過程當中,B又來了一個緊急電話,那麼B就要將與A的通話進行通話保持,因此無論怎麼講同一個鏈接只能是點對點的,不能一對多。ui

 

1.2 UDP協議

UDP是User Datagram Protocol(用戶數據報協議)

  • 用戶數據報協議,是一種非面向鏈接的協議,相似寫信
  • 在通訊的整個過程當中不須要保持鏈接
  • 不保證數據傳輸的可靠性和有序性
  • 是一種雙全工的數據報通訊方式
  • 服務器壓力比較小,資源比較低,發送效率比較高
  • 能夠一對1、一對多、多對1、多對多

2 基於TCP的網絡編程模型

2.1 使用Java代碼實現TCP

服務端:

  • 建立ServerSocket的對象而且提供端口號, public ServerSocket(int port) 
  • 等待客戶端的請求鏈接,使用accept()方法, public Socket accept()  
  • 鏈接成功後,使用Socket獲得輸入流和輸入流,進行通訊
  • 關閉相關資源

客戶端:

  • 建立Socket類型的對象,而且提供IP地址和端口號, public Socket(String host, int port) 
  • 使用Socket構造輸入流和輸出流進行通訊
  • 關閉相關資源

下面這個例子this

 1 /*
 2  * 在提供端口號的時候應該注意:最好定義在1024~49151。
 3  */
 4 public class TestServerString {
 5 
 6     public static void main(String[] args) {
 7         try{
 8             //1.建立ServerSocket類型的對象,並提供端口號
 9             ServerSocket ss = new ServerSocket(8888);
10             //2.等待客戶端的鏈接請求,使用accept()方法,保持阻塞狀態
11             while(true){
12                 System.out.println("等待客戶端的鏈接請求...");
13                 Socket s = ss.accept();
14                 new ServerThread(s).start();
15                 System.out.println("客戶端鏈接成功!");
16             }
17             
18         }catch(Exception e){
19             e.printStackTrace();
20         }
21 
22     }
23 
24 }
TestServerString類

在TestServerString類中採用循環相應多個客戶端的鏈接,url

 1 public class ServerThread extends Thread{
 2     
 3     private Socket s;
 4     
 5     public ServerThread(Socket s){
 6         this.s=s;
 7     }
 8     
 9     @Override
10     public void run(){
11         try{
12             BufferedReader br = new BufferedReader(
13                     new InputStreamReader(s.getInputStream()));
14             PrintStream ps = new PrintStream(s.getOutputStream());
15             //編程實現服務器能夠不斷地客戶端進行通訊
16             while(true){
17                 //服務器接收客戶端發來的消息並打印
18                 String str = br.readLine();
19                 //當客戶端發來"bye"時,結束循環
20                 if("bye".equalsIgnoreCase(str)) break;
21                 System.out.println(s.getLocalAddress()+":"+ str); 
22                 //向客戶端回發消息「I received!」
23                 ps.println("server received!");
24             }
25             //4.關閉相關的套接字
26             ps.close();
27             br.close();
28             s.close();
29         }catch(Exception e){
30             e.printStackTrace();
31         }
32     }
33 }
ServerThread

在輸入流和輸出流中採用循環可客戶端傳輸信息spa

 1 public class TestClientString {
 2 
 3     public static void main(String[] args) {
 4         
 5         try{
 6             //1.建立Socket類型的對象,並指定IP地址和端口號
 7             Socket s = new Socket("127.0.0.1", 8888);
 8             //2.使用輸入輸出流進行通訊
 9             BufferedReader br = new BufferedReader(
10                     new InputStreamReader(System.in));
11             PrintStream ps = new PrintStream(s.getOutputStream());
12             BufferedReader br2 = new BufferedReader(
13                     new InputStreamReader(s.getInputStream()));
14             //編程實現客戶端不斷地和服務器進行通訊
15             while(true){
16                 //提示用戶輸入要發送的內容
17                 System.out.println("請輸入要發送的內容:");
18                 String msg = br.readLine();
19                 ps.println(msg);
20                 //當客戶端發送"bye"時,結束循環
21                 if("bye".equalsIgnoreCase(msg)){ 
22                     break;
23                 };
24                 //等待接收服務器的回覆,並打印回覆的結果
25                 String str2 = br2.readLine();
26                 System.out.println("服務器發來的消息是:" + str2);
27             }
28             //3.關閉Socket對象
29             br2.close();
30             br.close();
31             ps.close();
32             s.close();
33         }catch(Exception e){
34             e.printStackTrace();
35         }
36 
37     }
38 
39 }
TestClientString

在客戶端中採用循環,可讓客戶端與服務器創建一次鏈接,實現屢次通訊。

在socket中有兩個構造方法,值得提一下:

Socket(InetAddress address, int port)

使用這個構造方法,程序會自動綁定一個本地地址,而且在之後的鏈接中不會改變,若是須要在本地模擬多個客戶端,那麼就不可用了。

下面這個構造方法,在鏈接到遠程地址中能夠指定本地地址和端口:

Socket(String host, int port, InetAddress localAddr, int localPort)

若是本地端口指定爲0,那麼系統將會自動選擇一個空閒的端口綁定。

2.2 使用C#代碼實現TCP

服務端:

  • 指定須要監聽的地址
  • 指定須要監聽的端口
  • 開始監聽
  • 獲取TcpClient實例
  • 獲取NetworkStream實例
  • 傳輸數據
  • 關閉流
  • 關閉鏈接

客戶端:

  • 指明目的地的地址
  • 指明目的地的端口
  • 鏈接
  • 獲取NetworkStream對象
  • 傳輸數據
  • 關閉流
  • 關閉鏈接

下面筆者給出一個用戶服務端和客戶端互發一條消息的示例:

服務端代碼:

    class Server
    {
        static void Main(string[] args)
        {
            IPAddress ip = IPAddress.Parse("127.0.0.1");
            TcpListener server = new TcpListener(ip,8005);
            server.Start();
            TcpClient client = server.AcceptTcpClient();

            NetworkStream dataStream = client.GetStream();
            //讀數據
            byte[] buffer = new byte[8192];
            int dataSize = dataStream.Read(buffer, 0, 8192);
            Console.WriteLine("server讀取到數據:"+Encoding.Default.GetString(buffer,0,dataSize));

            //寫數據
            string msg = "你好 client";
            byte[] writebuffer = Encoding.Default.GetBytes(msg);
            dataStream.Write(writebuffer, 0, writebuffer.Length);

            dataStream.Close();
            client.Close();
            
            Console.ReadLine();
        }
    }
Server.cs

客戶端代碼:

    class Client
    {
        static void Main(string[] args)
        {
            IPAddress ip = IPAddress.Parse("127.0.0.1");
            TcpClient client = new TcpClient();
            client.Connect(ip, 8005);

            //寫數據
            NetworkStream dataStream = client.GetStream();
            string msg = "你好 server";
            byte[] buffer = Encoding.Default.GetBytes(msg);
            dataStream.Write(buffer, 0, buffer.Length);

            //讀數據
            byte[] readbuffer = new byte[8192];
            int dataSize = dataStream.Read(readbuffer, 0, 8192);
            Console.WriteLine("Client讀取到數據:" + Encoding.Default.GetString(readbuffer, 0, dataSize));

            dataStream.Close();
            client.Close();
            Console.ReadLine();
        }
    }
Client

 

3 基於UDP的網絡編程模型

3.1 使用Java代碼實現UDP

客戶端:

  • 建立DatagramSocket類型的對象,不須要提供任何信息, public DatagramSocket()  
  • 建立DatagramPacket類型的對象,指定發送的內容、IP地址、端口號, public DatagramPacket(byte[] buf, int length, InetAddress address, int port)
  • 發送數據,使用send()方法, public void send(DatagramPacket p) 
  • 關閉相關的資源

服務端:

  • 建立DatagramSocket類型的對象,而且指定端口, public DatagramSocket(int port) 
  • 建立DatagramPacket類型的對象,用於接收發來的數據, public DatagramPacket(byte[] buf, int length) 
  • 接收數據,使用receive()方法, public void receive(DatagramPacket p) 
  • 關閉相關資源

例:

發送方:

 1 public class UDPSender {
 2 
 3     public static void main(String[] args) {
 4         try{
 5         /*
 6          * create DatagramSocket instance
 7          */
 8         DatagramSocket ds=new DatagramSocket();
 9         //create DatagramPackage instance and specify the content to send ,ip address,port
10         InetAddress ia=InetAddress.getLocalHost();
11         System.out.println(ia.toString());
12         String str="吳興國";
13         byte[] data=str.getBytes();
14         DatagramPacket dp=new DatagramPacket(data,data.length,ia,8888);
15         //send data use send()
16         ds.send(dp);
17         //create DatagramPacket instance for receive
18         byte []b2=new byte[1024];
19         DatagramPacket dp2=new DatagramPacket(b2,b2.length);
20         ds.receive(dp2);
21         System.out.println("result:"+new String(data));
22         //close resorce
23         ds.close();
24         }catch(IOException e){
25             e.printStackTrace();
26         }
27     }
28 
29 }
UDPSender

接收方:

 1 public class UDPReceiver {
 2 
 3     public static void main(String[] args) {
 4         try{
 5         /*
 6          * create DatagramSocket instance,and support port 
 7          */
 8         DatagramSocket ds=new DatagramSocket(8888);
 9         /*
10          * create DatagramPackage instance for receive data
11          */
12         byte []data=new byte[1024];
13         DatagramPacket dp=new DatagramPacket(data,data.length);
14         /*
15          * receive source
16          */
17         ds.receive(dp);
18         System.out.println("contents are:"+new String(data,0,dp.getLength()));
19         /*
20          * send data
21          */
22         String str="I received!";
23         byte[] b2=str.getBytes();
24         DatagramPacket dp2=
25                 new DatagramPacket(b2,b2.length,dp.getAddress(),dp.getPort());
26         ds.send(dp2);
27         System.out.println("發送成功,ip:"+dp.getAddress());
28         
29         /*
30          * close resource
31          */
32         }catch(SocketException e){
33             e.printStackTrace();
34         }catch(IOException e){
35             e.printStackTrace();
36         }
37     }
38 
39 }
UDPReceiver

3.2 使用C#代碼實現UDP

客戶端:

  • 實例化一個客戶端的IpEndPoint對象
  • 實例化一個客戶端的UdpClient對象
  • 實例化服務端的IpEndPoint對象
  • 使用客戶端的UdpClient發送數據到服務端

服務端:

  • 實例化一個服務端的IpEndPoint對象
  • 實例化一個服務端的IpUdpClient對象
  • 實例化客戶端的的IpEndPoint
  • 使用服務端的IpUdpClient接受數據

下面是一個案例,實現客戶端向服務端發送一條信息,而後服務端接收信息而且打印出來:

服務端:

    class Server
    {
        static void Main(string[] args)
        {
            IPEndPoint udpPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 5500);
            UdpClient udpClient = new UdpClient(udpPoint);
            //IPEndPoint senderPoint = new IPEndPoint(IPAddress.Parse("14.55.36.2"), 0);
            IPEndPoint senderPoint = new IPEndPoint(IPAddress.Any, 0);
            byte[] recvData = udpClient.Receive(ref senderPoint);
            Console.WriteLine("Receive Message:{0}", Encoding.Default.GetString(recvData));
            Console.Read();
        }
    }
Server.cs

客戶端:

    class Client
    {
        static void Main(string[] args)
        {
            IPEndPoint udpPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 4505);//實例化本地IPEndPoint
            UdpClient udpClient = new UdpClient(udpPoint);//實例化本地UpdClient
            //UdpClient udpClient = new UdpClient();
            string sendMsg = "Hello UDP Server.";
            byte[] sendData = Encoding.Default.GetBytes(sendMsg);
            IPEndPoint targetPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 5500);//服務端的IPEndPoint對象
            udpClient.Send(sendData, sendData.Length, targetPoint);
            Console.WriteLine("Send Message:{0}", sendMsg);
            Console.Read();
        }
    }
Client.cs

 

4 TCP的長鏈接和短鏈接

CP協議中有長鏈接和短鏈接之分。短鏈接在數據包發送完成後就會本身斷開,長鏈接在發包完畢後,會在必定的時間內保持鏈接,即咱們一般所說的Keepalive(存活定時器)功能。

默認的Keepalive超時須要7,200,000 milliseconds,即2小時,探測次數爲5次。它的功效和用戶本身實現的心跳機制是同樣的。開啓Keepalive功能須要消耗額外的寬帶和流量,儘管這微不足道,但在按流量計費的環境下增長了費用,另外一方面,Keepalive設置不合理時可能會由於短暫的網絡波動而斷開健康的TCP鏈接。

 

keepalive並非TCP規範的一部分。在Host Requirements RFC羅列有不使用它的三個理由:
(1)在短暫的故障期間,它們可能引發一個良好鏈接(good connection)被釋放(dropped),
(2)它們消費了沒必要要的寬帶,
(3)在以數據包計費的互聯網上它們(額外)花費金錢。然而,在許多的實現中提供了存活定時器。

一些服務器應用程序可能表明客戶端佔用資源,它們須要知道客戶端主機是否崩潰。存活定時器能夠爲這些應用程序提供探測服務。Telnet服務器和Rlogin服務器的許多版本都默認提供存活選項。
我的計算機用戶使用TCP/IP協議經過Telnet登陸一臺主機,這是可以說明須要使用存活定時器的一個經常使用例子。若是某個用戶在使用結束時只是關掉了電源,而沒有註銷(log off),那麼他就留下了一個半打開(half-open)的鏈接。若是客戶端消失,留給了服務器端半打開的鏈接,而且服務器又在等待客戶端的數據,那麼等待將永遠持續下去。存活特徵的目的就是在服務器端檢測這種半打開鏈接。
也能夠在客戶端設置存活器選項,且沒有不容許這樣作的理由,但一般設置在服務器。若是鏈接兩端都須要探測對方是否消失,那麼就能夠在兩端同時設置(好比NFS)。

 

keepalive工做原理:
若在一個給定鏈接上,兩小時以內無任何活動,服務器便向客戶端發送一個探測段。(咱們將在下面的例子中看到探測段的樣子。)客戶端主機必須是下列四種狀態之一:
1) 客戶端主機依舊活躍(up)運行,而且從服務器可到達。從客戶端TCP的正常響應,服務器知道對方仍然活躍。服務器的TCP爲接下來的兩小時復位存活定時器,若是在這兩個小時到期以前,鏈接上發生應用程序的通訊,則定時器從新爲往下的兩小時復位,而且接着交換數據。
2) 客戶端已經崩潰,或者已經關閉(down),或者正在重啓過程當中。在這兩種狀況下,它的TCP都不會響應。服務器沒有收到對其發出探測的響應,而且在75秒以後超時。服務器將總共發送10個這樣的探測,每一個探測75秒。若是沒有收到一個響應,它就認爲客戶端主機已經關閉並終止鏈接。
3) 客戶端曾經崩潰,但已經重啓。這種狀況下,服務器將會收到對其存活探測的響應,但該響應是一個復位,從而引發服務器對鏈接的終止。
4) 客戶端主機活躍運行,但從服務器不可到達。這與狀態2相似,由於TCP沒法區別它們兩個。它所能代表的僅是未收到對其探測的回覆。

服務器沒必要擔憂客戶端主機被關閉而後重啓的狀況(這裏指的是操做員執行的正常關閉,而不是主機的崩潰)。
當系統被操做員關閉時,全部的應用程序進程(也就是客戶端進程)都將被終止,客戶端TCP會在鏈接上發送一個FIN。收到這個FIN後,服務器TCP向服務器進程報告一個文件結束,以容許服務器檢測這種狀態。
在第一種狀態下,服務器應用程序不知道存活探測是否發生。凡事都是由TCP層處理的,存活探測對應用程序透明,直到後面2,3,4三種狀態發生。在這三種狀態下,經過服務器的TCP,返回給服務器應用程序錯誤信息。(一般服務器向網絡發出一個讀請求,等待客戶端的數據。若是存活特徵返回一個錯誤信息,則將該信息做爲讀操做的返回值返回給服務器。)在狀態2,錯誤信息相似於「鏈接超時」。狀態3則爲「鏈接被對方復位」。第四種狀態看起來像鏈接超時,或者根據是否收到與該鏈接相關的ICMP錯誤信息,而可能返回其它的錯誤信息。

在TCP程序中,我常常須要確認客戶端和服務端是否還保持者鏈接,這個時候有以下兩種方案:
1.TCP鏈接雙方定時發握手消息,而且在後面的程序中單獨啓線程,發送心跳信息。

2.利用TCP協議棧中的KeepAlive探測,也就是對TCP的鏈接的Socket設置KeepAlive。

在Java中利用下面的方法設置長鏈接:

setKeepAlive(boolean)

在C#能夠按照以下方式設置長鏈接:

SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true)

 

5 網絡編程中定義端口應注意事項

互聯網中的協議被分爲三種,

  • 衆所周知(Well Known Ports)端口:編號0~1023,一般由操做系統分配,用於標識一些衆所周知的服務。衆所周知的端口編號一般又IANA統一分配。它們緊密綁定(binding)於一些服務。一般這些端口的通信明確代表了某種服務的協議。例如:80端口實際上老是HTTP通信。
  • 註冊(Registered Ports)端口:編號1024~49151,能夠動態的分配給不一樣的網絡應用進程。
  • 動態和/或私有端口(Dynamic and/or Private Ports):編號49152~65535,理論上,不該爲服務分配這些端口。實際上,機器一般從1024起分配動態端口。但也有例外:SUN的RPC端口從32768開始。

6 參考文章:

C#通訊示例

因特網中端口

相關文章
相關標籤/搜索