本文參考文章:
- HTTP長連線和短連線
- TCP(HTTP)長連線和短連線區別和怎樣維護長連線
- http、TCP/IP協議與socket之間的區別(推薦閱讀)
- 通俗大白話來理解TCP協議的三次握手和四次分手
- 為什麼說基於TCP的移動端IM仍然需要心跳保活?
目錄
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長連線,使用固定心跳。
心跳典型值為: