本文參考文章:php
HTTP的長鏈接和短鏈接本質上是TCP長鏈接和短鏈接。HTTP屬於應用層協議,在傳輸層使用TCP協議,在網絡層使用IP協議。IP協議主要解決網絡路由和尋址問題,TCP協議主要解決如何在IP層之上可靠的傳遞數據包,使在網絡上的另外一端收到發端發出的全部包,而且順序與發出順序一致。TCP有可靠,面向鏈接的特色。html
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
手機可以使用聯網功能是由於手機底層實現了TCP/IP協議,可使手機終端經過無線網絡創建TCP鏈接。TCP協議能夠對上層網絡提供接口,使上層網絡數據的傳輸創建在「無差異」的網絡之上。服務器
TCP鏈接須要通過「三次握手」,斷開鏈接須要通過「四次揮手」。微信
2.3.1 Socket的定義網絡
Socket,即套接字,是支持TCP/IP協議的網絡通訊的基本操做單元。它是網絡通訊過程當中端點的抽象表示,包含進行網絡通訊必須的五種信息:鏈接使用的協議,本地主機的IP地址,本地進程的協議端口,遠地主機的IP地址,遠地進程的協議端口。併發
應用層經過傳輸層進行數據通訊時,TCP會遇到同時爲多個應用程序進程提供併發服務的問題。多個TCP鏈接或多個應用程序進程可能須要經過同一個 TCP協議端口傳輸數據。爲了區別不一樣的應用程序進程和鏈接,許多計算機操做系統爲應用程序與TCP/IP協議交互提供了套接字(Socket)接口。應用層能夠和傳輸層經過Socket接口,區分來自不一樣應用程序進程或網絡鏈接的通訊,實現數據傳輸的併發服務。
2.3.2 Socket鏈接
創建Socket鏈接至少須要一對套接字,其中一個運行於客戶端,稱爲ClientSocket ,另外一個運行於服務器端,稱爲ServerSocket 。
套接字之間的鏈接過程分爲三個步驟:服務器監聽,客戶端請求,鏈接確認。
服務器監聽:服務器端套接字並不定位具體的客戶端套接字,而是處於等待鏈接的狀態,實時監控網絡狀態,等待客戶端的鏈接請求。
客戶端請求:指客戶端的套接字提出鏈接請求,要鏈接的目標是服務器端的套接字。爲此,客戶端的套接字必須首先描述它要鏈接的服務器的套接字,指出服務器端套接字的地址和端口號,而後就向服務器端套接字提出鏈接請求。
鏈接確認:當服務器端套接字監聽到或者說接收到客戶端套接字的鏈接請求時,就響應客戶端套接字的請求,創建一個新的線程,把服務器端套接字的描述發給客戶端,一旦客戶端確認了此描述,雙方就正式創建鏈接。而服務器端套接字繼續處於監聽狀態,繼續接收其餘客戶端套接字的鏈接請求。
建立Socket鏈接時,能夠指定使用的傳輸層協議,Socket能夠支持不一樣的傳輸層協議(TCP或UDP),當使用TCP協議進行鏈接時,該Socket鏈接就是一個TCP鏈接。
總結:socket是對TCP/IP協議的封裝和應用(程序員層面上),它提供了一組基本的函數接口(好比:create、listen、accept等),使得程序員更方便地使用TCP/IP協議棧。
TCP/IP只是一個協議棧,就像操做系統的運行機制同樣,必需要具體實現,同時還要提供對外的操做接口。
Socket鏈接通常狀況下都是TCP鏈接,所以Socket鏈接一旦創建,通訊雙方就能夠進行互相發送內容。但在實際網絡應用中,客戶端到服務器之間的通訊每每須要穿越多箇中間節點,例如路由器、網關、防火牆等,大部分防火牆默認會關閉長時間處於非活躍狀態的鏈接而致使 Socket 鏈接斷連,所以須要經過輪詢告訴網絡,該鏈接處於活躍狀態。(這也就是常說的「心跳策略」)
Http鏈接是**「請求-響應」**的方式,不只在請求時須要先創建鏈接,並且須要客戶端向服務器發出請求後,服務器端才能回覆數據。
總結:若是創建的是Socket鏈接,服務器能夠直接將數據傳送給客戶端;若是方創建的是HTTP鏈接,則服務器須要等到客戶端發送一次請求後才能將數據傳回給客戶端。
長鏈接: 客戶端和服務端創建鏈接後不進行斷開,以後客戶端再次訪問這個服務器上的內容時,繼續使用這一條鏈接通道。
短鏈接: 客戶端和服務端創建鏈接,發送完數據後立馬斷開鏈接。下次要取數據,須要再次創建鏈接。
在HTTP/1.0中,默認使用的是短鏈接。但從 HTTP/1.1起,默認使用長鏈接。
Http長鏈接 和 TCP長鏈接的區別在於: TCP 的長鏈接須要本身去維護一套心跳策略。,而Http只須要在請求頭加入keep-alive:true
便可實現長鏈接。
思路: (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實現長鏈接
一、NAT超時
大部分移動無線網絡運營商都在鏈路一段時間沒有數據通信時,會淘汰 NAT 表中的對應項,形成鏈路中斷(NAT超時的更多描述見附錄6.1)。NAT超時是影響TCP鏈接壽命的一個重要因素(尤爲是國內),因此客戶端自動測算NAT超時時間,來動態調整心跳間隔,是一個重要的優化點。
二、DHCP的租期(lease time)
目前測試發現安卓系統對DHCP的處理有Bug,DHCP租期到了不會主動續約而且會繼續使用過時IP,這個問題會形成TCP長鏈接偶然的斷連。(租期問題的具體描述見附錄6.2)。
三、網絡狀態變化
手機網絡和WIFI網絡切換、網絡斷開和連上等狀況有網絡狀態的變化,也會使長鏈接變爲無效鏈接,須要監聽響應的網絡狀態變化事件,從新創建Push長鏈接。
穩定的網絡狀態下:
其中:
[MinHeart,MaxHeart]——心跳可選區間。
successHeart——當前成功心跳,初始爲MinHeart
curHeart——當前心跳初始值爲successHeart
heartStep——心跳增長步長
successStep——穩按期後的探測步長
如何判斷網絡狀態穩定?
答:使用 短心跳連續成功三次,此時認爲網絡相對穩定。
如下內容來自於微信分享的關於心跳策略的文章。
微信沒有使用GCM,本身維護TCP長鏈接,使用固定心跳。
心跳典型值爲: