本文参考文章: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长链接,使用固定心跳。
心跳典型值为: