TCP與UDP傳輸協議


目錄結構:

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不會聽到“嗎你好”,這就是可靠地數據傳輸。

雙全工的傳輸協議:全雙工,這個理解起來也很簡單,A打電話給B,B接聽電話,那么A可以說話給B聽,同樣B也可以給A說話,不可能只允許一個人說話.。

點對點的傳輸協議:點對點,這個看了上面的舉例相比大家都知道了,還要說一點的是,如果在A和B打電話過程中,B又來了一個緊急電話,那么B就要將與A的通話進行通話保持,所以不管怎么講同一個連接只能是點對點的,不能一對多。

 

1.2 UDP協議

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

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

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構造輸入流和輸出流進行通信
  • 關閉相關資源

下面這個例子

 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類中采用循環相應多個客戶端的連接,

 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

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

 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#通信示例

因特網中端口


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM