1. 程式人生 > >TCP 理論詳解

TCP 理論詳解

TCP 簡 介

  • TCP(Transmission Control Protocol) 是 socket 上的一種提供可靠的資料傳輸的通訊協議——傳輸控制協議
  • TCP 只是一個協議棧,就像作業系統的執行機制一樣,必須要具體實現,同時還要提供對外的操作介面。就像作業系統會提供標準的程式設計介面,比如 Win32 程式設計介面一樣,TCP 也必須對外提供程式設計介面,這就是 Socket 程式設計介面 

  • TCP/IP 協議是一個協議簇,包括應用層,傳輸層,網路層,網路訪問層,之所以命名為TCP/IP協議,因為TCP、IP協議是兩個很重要的協議。TCP 
    協議只是 TCP/IP 協議簇下的其中一個。

 

  • 通過 IP 協議並不能清楚地瞭解到資料包是否順利地傳送給目標計算機,而使用 TCP 協議,它將資料包成功傳送給目標計算機後,會要求傳送一個確認,如果在某個時間內沒有收到確認,TCP將重新發送資料包。
  • Socket(套接字) 實際上是對 TCP 協議的封裝,Socket 本身並不是協議,而是一個呼叫介面(API),通過 Socket 才能使用 TCP協議
  • Socket程式設計基本就是listen,accept以及send,write等幾個基本的操作 ,Java JDK 中 Socket 程式設計的 API 都在 java
    .net
     
    包中

TCP VS UDP

  • TCP(Transmission Control Protocol) 可以保證資料的正確性和可靠性,UDP 則允許資料丟失
  • TCP 和 UDP(User Datagram Protocol   使用者資料報協議)同屬於傳輸層,共同架設在IP層(網路層)之上
  • IP 層主要負責節點之間(End to End)的資料包傳送,這裡的節點是一臺網路裝置,比如計算機,只負責把資料送到節點,而不能區分上面的不同應用,所以 TCP 和 UDP 協議在其基礎上加入了埠資訊,埠於是標識的是一個節點上的一個應用
  • 除了增加埠資訊,UPD 協議基本就沒有對 IP 層的資料進行任何的處理了,而TCP協議還加入了更加複雜的傳輸控制,比如滑動的資料傳送視窗(Slice Window),以及接收確認和重發機制以達到資料的可靠傳送
  • 採用 UDP 的 QQ 比採用 TCP 傳輸協議的 MSN 傳輸檔案要快,但並不能說 QQ 的通訊是不安全的,因為程式設計師可以把確認、驗證的工作交給應用程式來做
  • TCP 協議提供了可靠的資料傳輸,但是其擁塞控制、資料校驗、重傳機制的網路開銷很大,不適合實時通訊,所以選擇開銷很小的 UDP 協議來傳輸資料

對比項

TCP

UDP

說明

對系統要求

較多

 

程式結構

較複雜

簡單

UDP是流模式、資料報模式

資料準確性

UDP可能丟包

資料順序

保證順序

不保證順序

 

資料大小

較大

短訊息

 

客戶端數目

較少

大量

 

響應速度

較慢

 

網路負擔

較大

 

  • UDP 就像發簡訊,只管發出去,至於對方是不是空號(網路不可到達)能不能收到(丟包)等並不關心;TCP 就像打電話,雙方要通話,首先,要確定對方是不是開機(網路可以到達),然後要確定是不是沒有訊號(網路可以到達),然後還需要對方接聽(通訊連結)。

TCP VS HTTP

  • TPC 協議是傳輸層協議,主要解決資料如何在網路中傳輸而 HTTP 是應用層協議,主要解決如何包裝資料
  • 傳輸資料時,可以只使用(傳輸層)TCP 協議,但是這樣就沒有應用層,便無法識別資料內容,如果想要使傳輸的資料有意義,則必須使用到應用層協議,應用層協議有很多,比如 HTTP、FTP、TELNET 等,也可以自己定義應用層協議。WEB 使用 HTTP 協議作應用層協議,以封裝 HTTP 文字資訊,然後使用 TCP 做傳輸層協議將它發到網路上。
  • 套接字(Socket)是通訊的基石,是支援 TCP/IP 協議的網路通訊的基本操作單元。它是網路通訊過程中端點的抽象表示,包含進行網路通訊必須的五種資訊

1)連線使用的協議

2)本地主機的IP地址

3)本地程序的協議埠

4)遠地主機的IP地址

5)遠地程序的協議埠

  •  應用層通過傳輸層進行資料通訊時,TCP 會遇到同時為多個應用程式程序提供併發服務的問題,為了區別不同的應用程式程序和連線,許多計算機作業系統為應用程式與 TCP/IP 協議互動提供了套接字(Socket)介面。應用層可以和傳輸層通過Socket 介面,區分來自不同應用程式程序或網路連線的通訊,實現資料傳輸的併發服務。

TCP 三次握手

  • TCP 是面向連線的,雖然說網路的不安全不穩定特性決定了多少次握手都不能保證連線的可靠性,但 TCP 的三次握手在很大程度上保證了連線的可靠性。
  • 建立起一個 TCP 連線需要經過“三次握手”

第一次握手:客戶端傳送 syn (Synchronization:同步) 包 (syn=j) 到伺服器,並進入SYN_SEND 狀態,等待伺服器確認

第二次握手:伺服器收到 syn 包,必須確認客戶的 SYN (ack=j+1),同時自己也傳送一個 SYN包(syn=k),即SYN+ACK (Acknowledge:承認)包,此時伺服器進入SYN_RECV(Receive:接收)狀態;

第三次握手:客戶端收到伺服器的 SYN+ACK 包,向伺服器傳送確認包 ACK(ack=k+1),此包傳送完畢,客戶端和伺服器進入ESTABLISHED(Established:建立、確立) 狀態,完成三次握手。

  • 握手過程中傳送的包裡不包含資料,三次握手完畢後,客戶端與伺服器才正式開始傳送資料。理想狀態下,TCP 連線一旦建立,在通訊雙方中的任何一方主動關閉連線之前,TCP 連線都將被一直保持下去
  • 伺服器和客戶端均可以主動發起斷開TCP連線的請求,斷開過程需要經過“四次握手” 。

TCP 與 Java

  • 協議相當於相互通訊的程式間達成的一種約定,它規定了分組報文的結構、交換方式、包含的意義以及怎樣對報文所包含的資訊進行解析
  • TCP/IP 協議族有 IP 協議、TCP 協議和 UDP 協議
  • 現在 TCP/IP 協議族中的主要 socket 型別為流套接字(使用TCP協議)和資料報套接字(使用UDP協議)
  • TCP 協議提供面向連線的服務,通過它建立的是可靠地連線Java   為 TCP 協議提供了兩個類:Socket 類和 ServerSocket
  • 一個 Socket 例項代表了 TCP 連線的一個客戶端,而一個 ServerSocket 例項代表了 TCP 連線的一個伺服器端
  • 一般在 TCP Socket 程式設計中,客戶端有多個,而伺服器端只有一個,客戶端 TCP 向伺服器端 TCP 傳送連線請求,伺服器端的 ServerSocket 例項則監聽來自客戶端的 TCP 連線請求,併為每個請求建立新的 Socket 例項,由於服務端在呼叫 accept() 等待客戶端的連線請求時會阻塞,直到收到客戶端傳送的連線請求才會繼續往下執行程式碼,因此要為每個Socket連線開啟一個執行緒
  • 伺服器端要同時處理 ServerSocket 例項和 Socket 例項,而客戶端只需要使用 Socket 例項。另外每個 Socket 例項會關聯一個 InputStream 和 OutputStream 物件,通過將位元組寫入套接字的 OutputStream 來發送資料,並通過從 InputStream 來接收資料。
  • 套接字之間的連線過程分為三個步驟:1、伺服器監聽;2、客戶端請求;3、連線確認,4)收發訊息。

1)伺服器監聽

  • 伺服器端套接字並不定位具體的客戶端套接字,而是處於等待連線的狀態,實時監控網路狀態,等待客戶端的連線請求
    /**
     * 建立繫結到特定埠的 TCP 服務端例項
     * ServerSocket(int port):指定繫結的埠,預設的tcp佇列大小為50,預設監聽本地所有的ip地址(如果有多個網絡卡)
     * ServerSocket(int port, int backlog, InetAddress bindAddr)
     *      port)繫結的埠
     *      backlog)TCP連線佇列大小
     *      bindAddr)多網絡卡時指定繫結哪個 IP 地址
     * 如果被繫結的埠已經被其它應用繫結,如 Mysql 的 3306,Tomcat 的 8080 等,
     * 則此時繫結會丟擲異常:java.net.BindException: Address already in use:
     */
    ServerSocket serverSocket = new ServerSocket(8080);

    /**
     * 偵聽 TCP 客戶端的連線
     * 該方法會從全連線佇列中獲取一個客戶端Socket請求。
     * 該方法是阻塞方法,如果當前沒有請求的連線,則會一直阻塞,直到有客戶端連線請求為止
     * 伺服器端的 ServerSocket 例項則監聽來自客戶端的 TCP 連線請求,併為每個請求建立新的 Socket 例項
     */
    Socket socket = serverSocket.accept();

    System.out.println("客戶端連線成功:" + socket.getRemoteSocketAddress());

2)客戶端請求

  • 客戶端的套接字( Socket )需要知道伺服器端套接字(ServerSocket)的地址和埠號,然後向伺服器端套接字提出連線請求
    /**
     * Socket(String host, int port):
     *      host)被連線的伺服器 IP 地址
     *      port)被連線的伺服器監聽的埠
     * Socket(InetAddress address, int port)
     *      address)用於設定 ip 地址的物件
     * 此時如果 TCP 伺服器未開放,或者其它原因導致連線失敗,則丟擲異常:
     * java.net.ConnectException: Connection refused: connect
     */
    Socket socket = new Socket("127.0.0.1", 8080);
    System.out.println("連線成功..........");

3)連線確認

  • 當伺服器端套接字( ServerSocket )監聽到或者說接收到客戶端套接字( Socket) 的連線請求時,就響應客戶端套接字的請求,並把伺服器端套接字的描述返回給客戶端,一旦客戶端確認了此描述,雙方就正式建立連線。
  • 注意,應該使用多執行緒,每監聽到一個,就新開一個執行緒來處理,使伺服器端套接字一直處於監聽狀態,繼續接收其他客戶端套接字的連線請求

4)收發訊息

  • TCP 伺服器端編碼:
package com.lct.tcp;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * Created by Administrator on 2018/10/14 0014.
 * TCP 服務端
 */
public class TcpServer {

    public static void main(String[] args) {
        tcpAccept();
    }

    /**
     * Tcp 服務端介紹連線
     */
    public static void tcpAccept() {
        ServerSocket serverSocket = null;
        try {
            /**
             * 建立繫結到特定埠的 TCP 服務端例項
             * ServerSocket(int port):指定繫結的埠,預設的tcp佇列大小為50,預設監聽本地所有的ip地址(如果有多個網絡卡)
             * ServerSocket(int port, int backlog, InetAddress bindAddr)
             *      port)繫結的埠
             *      backlog)TCP連線佇列大小
             *      bindAddr)多網絡卡時指定繫結哪個 IP 地址
             * 如果被繫結的埠已經被其它應用繫結,如 Mysql 的 3306,Tomcat 的 8080 等,
             * 則此時繫結會丟擲異常:java.net.BindException: Address already in use:
             */
            serverSocket = new ServerSocket(8080);

            /**
             * 偵聽 TCP 客戶端的連線,TCP 是典型的 BIO 模型,即同步則塞式網路程式設計,必須保證迴圈不間斷的監聽客戶端的連線
             * 對於每一個 TCP 的客戶端連線都要新開執行緒進行資料處理
             * accept() 方法會從全連線佇列中獲取一個客戶端Socket請求。
             * accept() 方法是阻塞方法,如果當前沒有請求的連線,則會一直阻塞,直到有客戶端連線請求為止
             * 伺服器端的 ServerSocket 例項則監聽來自客戶端的 TCP 連線請求,併為每個請求建立新的 Socket 例項
             */
            while (true) {
                System.out.println("等待客戶端連線......" + Thread.currentThread().getName());
                final Socket socket = serverSocket.accept();

                /** 設定輸入流讀取資料的超時時間為 10 秒*/
                /*socket.setSoTimeout(10 * 1000);*/
                System.out.println("客戶端連線成功......" + Thread.currentThread().getName());
                new Thread() {
                    @Override
                    public void run() {
                        try {
                            InputStream inputStream = socket.getInputStream();
                            InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "UTF-8");
                            /** 使用 BufferedReader 逐行讀取更方便*/
                            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
                            StringBuffer message = new StringBuffer();
                            String readline = null;

                            /**
                             * TCP 連線成功之後,輸入流 InputStream 讀取資料的方法也會一直阻塞等待接收對方的資料
                             * 當使用了 Socket 的 setSoTimeout(int timeout) 方法設定超時時間後,則在指定的時間內沒有
                             * 接收倒是資料時,則拋異常:java.net.SocketTimeoutException: Read timed out
                             */
                            while ((readline = bufferedReader.readLine()) != null) {
                                message.append(readline + "\n");
                            }
                            System.out.println(Thread.currentThread().getName() + " 收到訊息:" + message.toString());
                            bufferedReader.close();
                            inputStreamReader.close();
                            inputStream.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        } finally {
                            /**操作完畢,關閉 Socket 連線*/
                            try {
                                if (!socket.isClosed()) {
                                    socket.close();
                                }
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            /**
             * 如果丟擲異常,則關閉 Tcp 伺服器
             */
            if (serverSocket != null && !serverSocket.isClosed()) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
  • TCP 客戶端端編碼:
package com.lct.tcp;

import java.io.*;
import java.net.Socket;

/**
 * Created by Administrator on 2018/10/14 0014.
 * TCP 客戶端
 */
public class TcpClient {
    public static void main(String[] args) {
        /**
         * 使用三個執行緒模擬 3 個 TCP 客戶端進行連線
         * 連線成功後,給伺服器傳送一條訊息
         */
        for (int i = 0; i < 3; i++) {
            new Thread() {
                @Override
                public void run() {
                    tcpSendMessage();
                }
            }.start();
        }
    }

    /**
     * Tcp 客戶端連線伺服器併發送訊息
     */
    public static void tcpSendMessage() {
        Socket socket = null;
        try {
            /**
             * Socket(String host, int port):
             *      host)被連線的伺服器 IP 地址
             *      port)被連線的伺服器監聽的埠
             * Socket(InetAddress address, int port)
             *      address)用於設定 ip 地址的物件
             * 此時如果 TCP 伺服器未開放,或者其它原因導致連線失敗,則丟擲異常:
             * java.net.ConnectException: Connection refused: connect
             */
            socket = new Socket("127.0.0.1", 8080);
            System.out.println("連線成功.........." + Thread.currentThread().getName());

            /** 往服務端傳送一條訊息,指定字元編碼為 UTF-8*/
            OutputStream outputStream = socket.getOutputStream();
            OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream, "UTF-8");
            outputStreamWriter.write("修長城的民族!,客戶端=" + Thread.currentThread().getName());
            outputStreamWriter.flush();
            outputStreamWriter.close();
            outputStream.flush();
            outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            /** 操作完畢關閉 socket*/
            if (socket != null && !socket.isClosed()) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

執行結果,服務端如下:

等待客戶端連線......main
客戶端連線成功......main
等待客戶端連線......main
客戶端連線成功......main
等待客戶端連線......main
客戶端連線成功......main
等待客戶端連線......main
Thread-0 收到訊息:修長城的民族!,客戶端=Thread-2

Thread-1 收到訊息:修長城的民族!,客戶端=Thread-0

Thread-2 收到訊息:修長城的民族!,客戶端=Thread-1

執行結果,客戶端如下:

連線成功..........Thread-1
連線成功..........Thread-2
連線成功..........Thread-0

網路程式設計

  • 技術日新月異,目前網路程式設計最為流行的當屬 Netty,Java 網路程式設計發展歷程:

JDK 1.4 以前:java.net + java.io——即平時所使用的簡單的 TCP 、UDP 程式設計

JDK 1.4 及以後:java.nio

當下流行:JBoos 的 Netty 庫、Apache 的  Mina 等