菜鳥學網絡之 —— 長鏈接和短鏈接

本文參考文章:php

目錄

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

HTTP的長鏈接和短鏈接本質上是TCP長鏈接和短鏈接。HTTP屬於應用層協議,在傳輸層使用TCP協議,在網絡層使用IP協議。IP協議主要解決網絡路由和尋址問題,TCP協議主要解決如何在IP層之上可靠的傳遞數據包,使在網絡上的另外一端收到發端發出的全部包,而且順序與發出順序一致。TCP有可靠,面向鏈接的特色。html

2. Http/TCP/Socket鏈接

2.1 Http鏈接

Http協議,即超文本傳輸協議,是Web聯網的基礎。Http協議是創建在TCP協議之上的一種應用。Http協議負責如何包裝數據,而TCP協議負責如何傳輸數據。所以,若是隻有TCP協議,那麼將沒法解析傳輸過來的數據。git

HTTP鏈接最顯著的特色是客戶端發送的每次請求都須要服務器回送響應,在請求結束後,會主動釋放鏈接。從創建鏈接到關閉鏈接的過程稱爲「一次鏈接」。程序員

1)在HTTP 1.0中,客戶端的每次請求都要求創建一次單獨的鏈接,在處理完本次請求後,就自動釋放鏈接,這是一種「短鏈接」。github

2)在HTTP 1.1中則能夠在一次鏈接中處理多個請求,而且多個請求能夠重疊進行,不須要等待一個請求結束後再發送下一個請求,這是一種「長鏈接」。在Http 1.1 中只須要在請求頭配置keep-alive : true便可實現長鏈接。此時,服務端返回的請求頭中會有 connection : keep-alive 代表這是一個長鏈接。bash

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 {
        //一、建立客戶端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鏈接壽命的因素

一、NAT超時

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

二、DHCP的租期(lease time)

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

三、網絡狀態變化

手機網絡和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長鏈接,使用固定心跳。

心跳典型值爲:

相關文章
相關標籤/搜索