菜鳥學網路之 —— 長連線和短連線

Rymn發表於2018-05-22

本文參考文章:

目錄

菜鳥學網路之 —— 長連線和短連線

1. Http協議與TCP/IP 協議的關係

HTTP的長連線和短連線本質上是TCP長連線和短連線。HTTP屬於應用層協議,在傳輸層使用TCP協議,在網路層使用IP協議。IP協議主要解決網路路由和定址問題,TCP協議主要解決如何在IP層之上可靠的傳遞資料包,使在網路上的另一端收到發端發出的所有包,並且順序與發出順序一致。TCP有可靠,面向連線的特點。

2. Http/TCP/Socket連線

2.1 Http連線

Http協議,即超文字傳輸協議,是Web聯網的基礎。Http協議是建立在TCP協議之上的一種應用。Http協議負責如何包裝資料,而TCP協議負責如何傳輸資料。因此,如果只有TCP協議,那麼將無法解析傳輸過來的資料。

HTTP連線最顯著的特點是客戶端傳送的每次請求都需要伺服器回送響應,在請求結束後,會主動釋放連線。從建立連線到關閉連線的過程稱為“一次連線”。

1)在HTTP 1.0中,客戶端的每次請求都要求建立一次單獨的連線,在處理完本次請求後,就自動釋放連線,這是一種“短連線”。

2)在HTTP 1.1中則可以在一次連線中處理多個請求,並且多個請求可以重疊進行,不需要等待一個請求結束後再傳送下一個請求,這是一種“長連線”。在Http 1.1 中只需要在請求頭配置keep-alive : true即可實現長連線。此時,服務端返回的請求頭中會有 connection : keep-alive 表明這是一個長連線。

2.2 TCP連線

手機能夠使用聯網功能是因為手機底層實現了TCP/IP協議,可以使手機終端通過無線網路建立TCP連線。TCP協議可以對上層網路提供介面,使上層網路資料的傳輸建立在“無差別”的網路之上。

TCP連線需要經過“三次握手”,斷開連線需要經過“四次揮手”。

菜鳥學網路之 —— 長連線和短連線

2.3 Socket連線

2.3.1 Socket的定義

Socket,即套接字,是支援TCP/IP協議的網路通訊的基本操作單元。它是網路通訊過程中端點的抽象表示,包含進行網路通訊必須的五種資訊:連線使用的協議,本地主機的IP地址,本地程式的協議埠,遠地主機的IP地址,遠地程式的協議埠。

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

2.3.2 Socket連線

建立Socket連線至少需要一對套接字,其中一個執行於客戶端,稱為ClientSocket ,另一個執行於伺服器端,稱為ServerSocket 。

套接字之間的連線過程分為三個步驟:伺服器監聽,客戶端請求,連線確認。

  • 伺服器監聽:伺服器端套接字並不定位具體的客戶端套接字,而是處於等待連線的狀態,實時監控網路狀態,等待客戶端的連線請求。

  • 客戶端請求:指客戶端的套接字提出連線請求,要連線的目標是伺服器端的套接字。為此,客戶端的套接字必須首先描述它要連線的伺服器的套接字,指出伺服器端套接字的地址和埠號,然後就向伺服器端套接字提出連線請求。

  • 連線確認:當伺服器端套接字監聽到或者說接收到客戶端套接字的連線請求時,就響應客戶端套接字的請求,建立一個新的執行緒,把伺服器端套接字的描述發給客戶端,一旦客戶端確認了此描述,雙方就正式建立連線。而伺服器端套接字繼續處於監聽狀態,繼續接收其他客戶端套接字的連線請求。

2.4 Socket連線和TCP連線的關係

建立Socket連線時,可以指定使用的傳輸層協議,Socket可以支援不同的傳輸層協議(TCP或UDP),當使用TCP協議進行連線時,該Socket連線就是一個TCP連線

總結:socket是對TCP/IP協議的封裝和應用(程式設計師層面上),它提供了一組基本的函式介面(比如:create、listen、accept等),使得程式設計師更方便地使用TCP/IP協議棧。

TCP/IP只是一個協議棧,就像作業系統的執行機制一樣,必須要具體實現,同時還要提供對外的操作介面。

2.5 Socket連線和Http連線的關係

Socket連線一般情況下都是TCP連線,因此Socket連線一旦建立,通訊雙方就可以進行互相傳送內容。但在實際網路應用中,客戶端到伺服器之間的通訊往往需要穿越多箇中間節點,例如路由器、閘道器、防火牆等,大部分防火牆預設會關閉長時間處於非活躍狀態的連線而導致 Socket 連線斷連,因此需要通過輪詢告訴網路,該連線處於活躍狀態。(這也就是常說的“心跳策略”

Http連線是**“請求-響應”**的方式,不僅在請求時需要先建立連線,而且需要客戶端向伺服器發出請求後,伺服器端才能回覆資料。

總結:如果建立的是Socket連線,伺服器可以直接將資料傳送給客戶端;如果方建立的是HTTP連線,則伺服器需要等到客戶端傳送一次請求後才能將資料傳回給客戶端。

3. Http長連線和短連線

長連線: 客戶端和服務端建立連線後不進行斷開,之後客戶端再次訪問這個伺服器上的內容時,繼續使用這一條連線通道。

短連線: 客戶端和服務端建立連線,傳送完資料後立馬斷開連線。下次要取資料,需要再次建立連線。

在HTTP/1.0中,預設使用的是短連線。但從 HTTP/1.1起,預設使用長連線。

4 Http長連線和TCP長連線的區別

Http長連線 和 TCP長連線的區別在於: TCP 的長連線需要自己去維護一套心跳策略。,而Http只需要在請求頭加入keep-alive:true即可實現長連線。

5 手寫一次TCP長連線

思路: (1) 服務端就只需要編寫一個讀執行緒,不斷讀取來自客戶端的訊息,並列印出來即可 (2) 客戶端需要開啟兩個定時器,一個是用來模擬傳送普通訊息,一個用來模擬傳送心跳包 (3) 服務端和客戶端之間有協議,用來標識什麼情況下,這個資料表示的是普通訊息,什麼情況下,這個資料表示的是心跳訊息。

步驟一:定義協議,表明什麼情況下表示普通訊息,什麼情況下表示心跳訊息,在這裡,我們用前四位用來區分普通訊息和心跳訊息

菜鳥學網路之 —— 長連線和短連線

菜鳥學網路之 —— 長連線和短連線

菜鳥學網路之 —— 長連線和短連線

步驟二:定義一個方法,按照協議內容包裝內容

菜鳥學網路之 —— 長連線和短連線

現在給出完整的協議類的程式碼: BasicProtocol:

public abstract class BasicProtocol {

    static final int TYPE_LEN = 4;//表示業務型別;1111 -> 心跳包   1234 -> 傳送普通文字訊息
    static final int CONTEXT_LEN = 4;

    /**
     * 獲取正文文字
     * @return
     */
    public abstract String getContext();

    /**
     * 獲取包裝好的byte[]
     * @return
     */
    public byte[] getData() {
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            baos.write(getType().getBytes(),0,TYPE_LEN);
            byte[] bytes = getContext().getBytes();
            baos.write(ProtocolUtil.int2ByteArrays(bytes.length),0,CONTEXT_LEN);
            baos.write(bytes,0,bytes.length);
            return baos.toByteArray();
        }catch (Exception e){
            return null;
        }
    }

    /**
     * 獲取業務型別
     * @return
     */
    public abstract  String getType();

    /**
     * 解析資料
     * @param bytes
     */
    public abstract void parseBinary(byte[] bytes);
}

複製程式碼

HeartBeatProtocol:

public class HeartBeatProtocol extends BasicProtocol {
    static final String TYPE = "1111";
    @Override
    public String getContext() {
        return "兄弟,我還在,你不要擔心";
    }

    @Override
    public String getType() {
        return TYPE;
    }

    @Override
    public void parseBinary(byte[] bytes) {

    }
}

複製程式碼

MessageProtocol:

public class MessageProtocol extends BasicProtocol {

    private String context;
    static final String TYPE = "1234";

    public void setContext(String context){
        this.context = context;
    }

    @Override
    public String getContext() {
        return context;
    }

    @Override
    public String getType() {
        return TYPE;
    }

    @Override
    public void parseBinary(byte[] bytes) {
        setContext(new String(bytes));
    }
}
複製程式碼

步驟三:編寫服務端程式碼:啟動一個讀執行緒,讀取客戶端資料

public class LongServer implements Runnable {

    private ReadTask readTask;//讀資料的執行緒
    private Socket socket;

    public LongServer(Socket socket){
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            readTask = new ReadTask();
            readTask.inputStream = new DataInputStream(socket.getInputStream());
            readTask.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 負責讀取資料
     */
    public class ReadTask extends Thread{
        private DataInputStream inputStream;
        private boolean isCancle = false;//是否取消迴圈
        @Override
        public void run() {
         //   try {
                while (!isCancle){
                    try {
                       // inputStream = new DataInputStream (socket.getInputStream());
                        BasicProtocol protocol = ProtocolUtil.readInputStream(inputStream);
                        if(protocol != null){
                            System.out.println("================:"+protocol.getContext());
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
//            } catch (IOException e) {
//                e.printStackTrace();
//                stops();//捕獲到io異常,可能原因是連線斷開了,所以我們停掉所有操作
//            }
        }
    }

    /**
     * 停止掉所有活動
     */
    public void stops(){
        if (readTask!=null){
            readTask.isCancle=true;
            readTask.interrupt();
            readTask=null;
        }
}
複製程式碼

步驟四:客戶端程式碼

public class Client {

    private Socket socket;
    private WriteTask writeTask;

    public static void main(String[] args) throws IOException{
         Client client = new Client();
         client.start();
    }
    String[] string = {"使用者名稱:admin;密碼:admin", "身無綵鳳雙飛翼,心有靈犀一點通。", "兩情若是久長時,又豈在朝朝暮暮。"
            , "沾衣欲溼杏花雨,吹面不寒楊柳風。", "何須淺碧輕紅色,自是花中第一流。", "更無柳絮因風起,唯有葵花向日傾。"
            , "海上生明月,天涯共此時。", "一寸丹心圖報國,兩行清淚為思親。", "清香傳得天心在,未話尋常草木知。",
            "和風和雨點苔紋,漠漠殘香靜裡聞。"};

    public Client() throws IOException {
        //1、建立客戶端Socket,指定伺服器地址和埠
        socket = new Socket("127.0.0.1", 9013);
    }

    public void start(){
        try {
            writeTask = new WriteTask();
            writeTask.outputStream = new DataOutputStream(socket.getOutputStream());//預設初始化發給自己
            writeTask.start();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static byte[] int2ByteArrays(int i) {
        byte[] result = new byte[4];
        result[0] = (byte) ((i >> 24) & 0xFF);
        result[1] = (byte) ((i >> 16) & 0xFF);
        result[2] = (byte) ((i >> 8) & 0xFF);
        result[3] = (byte) (i & 0xFF);
        return result;
    }
    //訊息佇列
    private volatile ConcurrentLinkedQueue<BasicProtocol> reciverData= new ConcurrentLinkedQueue<BasicProtocol>();
    /**
     * 負責寫入資料
     */
    public class WriteTask extends Thread{
        private DataOutputStream outputStream;
        private boolean isCancle = false;
        private Timer heart = new Timer();//傳送心跳包的定時任務
        private Timer message = new Timer();//模擬傳送普通資料
        @Override
        public void run() {
            //每隔20s傳送一次心跳包
            heart.schedule(new TimerTask() {
                @Override
                public void run() {
                    reciverData.add(new HeartBeatProtocol());
                }
            },0,1000*20);

            //先延時2s,然後每隔6s傳送一次普通資料
            Random random = new Random();
            message.schedule(new TimerTask() {
                @Override
                public void run() {
                    MessageProtocol bp = new MessageProtocol();
                    bp.setContext(string[random.nextInt(string.length)]);
                    reciverData.add(bp);
                }
            },1000*2,1000*6);


            while (!isCancle){
                BasicProtocol bp = reciverData.poll();
                if(bp!=null){
                    System.out.println("------:"+bp.getContext());
                    ProtocolUtil.writeOutputStream(bp,outputStream);
                }
            }
        }
    }

    /**
     * 停止掉所有活動
     */
    public void stops(){
//        if (readTask!=null){
//            readTask.isCancle=true;
//            readTask.interrupt();
//            readTask=null;
//        }

        if (writeTask!=null) {
            writeTask.isCancle = true;
            //取消傳送心跳包的定時任務
            writeTask.heart.cancel();
            //取消傳送普通訊息的定時任務
            writeTask.message.cancel();
            writeTask.interrupt();
            writeTask=null;
        }
    }
}
複製程式碼

以上程式碼參考:socket實現長連線

6 影響TCP連線壽命的因素

1、NAT超時

大部分移動無線網路運營商都在鏈路一段時間沒有資料通訊時,會淘汰 NAT 表中的對應項,造成鏈路中斷(NAT超時的更多描述見附錄6.1)。NAT超時是影響TCP連線壽命的一個重要因素(尤其是國內),所以客戶端自動測算NAT超時時間,來動態調整心跳間隔,是一個重要的優化點。

2、DHCP的租期(lease time)

目前測試發現安卓系統對DHCP的處理有Bug,DHCP租期到了不會主動續約並且會繼續使用過期IP,這個問題會造成TCP長連線偶然的斷連。(租期問題的具體描述見附錄6.2)。

3、網路狀態變化

手機網路和WIFI網路切換、網路斷開和連上等情況有網路狀態的變化,也會使長連線變為無效連線,需要監聽響應的網路狀態變化事件,重新建立Push長連線。

7. 不同網路狀態下的心跳策略

穩定的網路狀態下:

菜鳥學網路之 —— 長連線和短連線

其中:

  • [MinHeart,MaxHeart]——心跳可選區間。

  • successHeart——當前成功心跳,初始為MinHeart

  • curHeart——當前心跳初始值為successHeart

  • heartStep——心跳增加步長

  • successStep——穩定期後的探測步長

如何判斷網路狀態穩定?

答:使用 短心跳連續成功三次,此時認為網路相對穩定。

8. 各平臺Push策略研究

以下內容來自於微信分享的關於心跳策略的文章

8.1 WhatsApp的Push策略

菜鳥學網路之 —— 長連線和短連線

8.2 Line的Push策略

菜鳥學網路之 —— 長連線和短連線

8.3 微信的Push策略

微信沒有使用GCM,自己維護TCP長連線,使用固定心跳。

心跳典型值為:

菜鳥學網路之 —— 長連線和短連線

相關文章