概述可承遇到,不知什么原因,一个夜晚,机房中,大片的远程调用连接断开。

第二天早上,用户访问高峰,大部分服务器都在获取连接,造成大片网络阻塞。

服务崩溃,惨不忍睹的景象。

本文将从长连接和短连接的概念切入,再到长连接与短连接的区别,以及应用场景,引出心跳机制和断线重连,给出代码实现。

从原理到实践杜绝此类现象。

短连接概念

client与server通过三次握手建立连接,client发送请求消息,server返回响应,一次连接就完成了。

这时候双方任意都可以发起close操作,不过一般都是client先发起close操作。上述可知,短连接一般只会在 client/server间传递一次请求操作。

短连接的优缺点

管理起来比较简单,存在的连接都是有用的连接,不需要额外的控制手段。

使用场景

通常浏览器访问服务器的时候就是短连接。

对于服务端来说,长连接会耗费服务端的资源,而且用户用浏览器访问服务端相对而言不是很频繁的

如果有几十万,上百万的连接,服务端的压力会非常大,甚至会崩溃。

所以对于并发量大,请求频率低的,建议使用短连接。

长连接什么是长连接

client向server发起连接,server接受client连接,双方建立连接。

Client与server完成一次读写之后,它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。

长连接的生命周期

正常情况下,一条TCP长连接建立后,只要双不提出关闭请求并且不出现异常情况,这条连接是一直存在的.

操作系统不会自动去关闭它,甚至经过物理网络拓扑的改变之后仍然可以使用。

所以一条连接保持几天、几个月、几年或者更长时间都有可能,只要不出现异常情况或由用户(应用层)主动关闭。

客户端和服务单可一直使用该连接进行数据通信。

长连接的优点

长连接可以省去较多的TCP建立和关闭的操作,减少网络阻塞的影响,

当发生错误时,可以在不关闭连接的情况下进行提示,

减少CPU及内存的使用,因为不需要经常的建立及关闭连接。

长连接的缺点

连接数过多时,影响服务端的性能和并发数量。

使用场景

数据库的连接就是采用TCP长连接.

RPC,远程服务调用,在服务器,一个服务进程频繁调用另一个服务进程,可使用长连接,减少连接花费的时间。

总结

1.对于长连接和短连接的使用是需要根据应用场景来判断的

2.长连接并不是万能的,也是需要维护的,

长连接的实现心跳机制应用层协议大多都有HeartBeat机制,通常是客户端每隔一小段时间向服务器发送一个数据包,通知服务器自己仍然在线。

并传输一些可能必要的数据。使用心跳包的典型协议是IM,比如QQ/MSN/飞信等协议。

在TCP的机制里面,本身是存在有心跳包的机制的,也就是TCP的选项:SO_KEEPALIVE。

系统默认是设置的2小时的心跳频率。但是它检查不到机器断电、网线拔出、防火墙这些断线。

而且逻辑层处理断线可能也不是那么好处理。一般,如果只是用于保活还是可以的。

为什么需要心跳机制?因为网络的不可靠性, 有可能在 TCP 保持长连接的过程中, 由于某些突发情况, 例如网线被拔出, 突然掉电等,

会造成服务器和客户端的连接中断. 在这些突发情况下, 如果恰好服务器和客户端之间没有交互的话, 那么它们是不能在短时间内发现对方已经掉线的.

心跳机制即可解决此类问题。

TCP协议的KeepAlive机制默认KeepAlive状态是不打开的。

需要将setsockopt将SOL_SOCKET.SO_KEEPALIVE设置为1才是打开KeepAlive状态,

并且可以设置三个参数:

tcp_keepalive_time ,tcp_keepalive_probes , tcp_keepalive_intvl,

分别表示:连接闲置多久开始发keepalive的ack包、发几个ack包不回复才当对方已断线、两个ack包之间的间隔。

很多网络设备,尤其是NAT路由器,由于其硬件的限制(例如内存、CPU处理能力),无法保持其上的所有连接,因此在必要的时候,会在连接池中选择一些不活跃的连接踢掉。

典型做法是LRU,把最久没有数据的连接给T掉。

通过使用TCP的KeepAlive机制(修改那个time参数),可以让连接每隔一小段时间就产生一些ack包,以降低被踢掉的风险,当然,这样的代价是额外的网络和CPU负担。

如何实现心跳机制?两种方式实现心跳机制:

使用 TCP 协议层面的 keepalive 机制.

在应用层上实现自定义的心跳机制.

虽然在 TCP 协议层面上, 提供了 keepalive 保活机制, 但是使用它有几个缺点:

它不是 TCP 的标准协议, 并且是默认关闭的.

TCP keepalive 机制依赖于操作系统的实现, 默认的 keepalive 心跳时间是 两个小时, 并且对 keepalive 的修改需要系统调用(或者修改系统配置), 灵活性不够.

TCP keepalive 与 TCP 协议绑定, 因此如果需要更换为 UDP 协议时, keepalive 机制就失效了.

使用 TCP 层面的 keepalive 机制比自定义的应用层心跳机制节省流量,

本文的主要介绍应用层方面实现心跳机制,使用netty实现心跳和断线重连。

netty实现心跳机制netty对心跳机制提供了机制,实现的关键是IdleStateHandler先来看一下他的构造函数

代码语言:javascript复制 public IdleStateHandler(

long readerIdleTime, long writerIdleTime, long allIdleTime,

TimeUnit unit) {

this(false, readerIdleTime, writerIdleTime, allIdleTime, unit);

}实例化一个 IdleStateHandler 需要提供三个参数:

readerIdleTimeSeconds, 读超时. 即当在指定的时间间隔内没有从 Channel 读取到数据时, 会触发一个 READER_IDLE 的 IdleStateEvent 事件.

writerIdleTimeSeconds, 写超时. 即当在指定的时间间隔内没有数据写入到 Channel 时, 会触发一个 WRITER_IDLE 的 IdleStateEvent 事件.

allIdleTimeSeconds, 读和写都超时. 即当在指定的时间间隔内没有读并且写操作时, 会触发一个 ALL_IDLE 的 IdleStateEvent 事件.

netty心跳流程1. 客户端成功连接服务端。

2.在客户端中的ChannelPipeline中加入IdleStateHandler,设置写事件触发事件为5s.

3.客户端超过5s未写数据,触发写事件,向服务端发送心跳包,

4.同样,服务端要对心跳包做出响应,其实给客户端最好的回复就是“不回复”,减轻服务端的压力

5.超过三次,1过0s服务端都会收到来自客户端的心跳信息,服务端可以认为客户端挂了,可以close链路。

6.客户端恢复正常,发现链路已断,重新连接服务端。

代码实现服务端handler:

代码语言:javascript复制package com.heartbreak.server;

import io.netty.channel.ChannelHandlerContext;

import io.netty.channel.SimpleChannelInboundHandler;

import io.netty.handler.timeout.IdleState;

import io.netty.handler.timeout.IdleStateEvent;

import java.util.Random;

/**

* @author janti

* @date 2018/6/10 12:21

*/

public class HeartbeatServerHandler extends SimpleChannelInboundHandler {

// 失败计数器:未收到client端发送的ping请求

private int unRecPingTimes = 0;

// 定义服务端没有收到心跳消息的最大次数

private static final int MAX_UN_REC_PING_TIMES = 3;

private Random random = new Random(System.currentTimeMillis());

@Override

protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {

if (msg!=null && msg.equals("Heartbeat")){

System.out.println("客户端"+ctx.channel().remoteAddress()+"--心跳信息--");

}else {

System.out.println("客户端----请求消息----:"+msg);

String resp = "商品的价格是:"+random.nextInt(1000);

ctx.writeAndFlush(resp);

}

}

@Override

public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {

if (evt instanceof IdleStateEvent) {

IdleStateEvent event = (IdleStateEvent) evt;

if (event.state()==IdleState.READER_IDLE){

System.out.println("===服务端===(READER_IDLE 读超时)");

// 失败计数器次数大于等于3次的时候,关闭链接,等待client重连

if (unRecPingTimes >= MAX_UN_REC_PING_TIMES) {

System.out.println("===服务端===(读超时,关闭chanel)");

// 连续超过N次未收到client的ping消息,那么关闭该通道,等待client重连

ctx.close();

} else {

// 失败计数器加1

unRecPingTimes++;

}

}else {

super.userEventTriggered(ctx,evt);

}

}

}

@Override

public void channelActive(ChannelHandlerContext ctx) throws Exception {

super.channelActive(ctx);

System.out.println("一个客户端已连接");

}

@Override

public void channelInactive(ChannelHandlerContext ctx) throws Exception {

super.channelInactive(ctx);

System.out.println("一个客户端已断开连接");

}

}服务端server:

代码语言:javascript复制package com.heartbreak.server;

import io.netty.bootstrap.ServerBootstrap;

import io.netty.channel.*;

import io.netty.channel.nio.NioEventLoopGroup;

import io.netty.channel.socket.SocketChannel;

import io.netty.channel.socket.nio.NioServerSocketChannel;

import io.netty.handler.codec.string.StringDecoder;

import io.netty.handler.codec.string.StringEncoder;

import io.netty.handler.timeout.IdleStateHandler;

import java.util.concurrent.TimeUnit;

/**

* @author tangj

* @date 2018/6/10 10:46

*/

public class HeartBeatServer {

private static int port = 9817;

public HeartBeatServer(int port) {

this.port = port;

}

ServerBootstrap bootstrap = null;

ChannelFuture f;

// 检测chanel是否接受过心跳数据时间间隔(单位秒)

private static final int READ_WAIT_SECONDS = 10;

public static void main(String args[]) {

HeartBeatServer heartBeatServer = new HeartBeatServer(port);

heartBeatServer.startServer();

}

public void startServer() {

EventLoopGroup bossgroup = new NioEventLoopGroup();

EventLoopGroup workergroup = new NioEventLoopGroup();

try {

bootstrap = new ServerBootstrap();

bootstrap.group(bossgroup, workergroup)

.channel(NioServerSocketChannel.class)

.childHandler(new HeartBeatServerInitializer());

// 服务器绑定端口监听

f = bootstrap.bind(port).sync();

System.out.println("server start ,port: "+port);

// 监听服务器关闭监听,此方法会阻塞

f.channel().closeFuture().sync();

} catch (Exception e) {

e.printStackTrace();

} finally {

bossgroup.shutdownGracefully();

workergroup.shutdownGracefully();

}

}

private class HeartBeatServerInitializer extends ChannelInitializer {

@Override

protected void initChannel(SocketChannel ch) throws Exception {

ChannelPipeline pipeline = ch.pipeline();

// 监听读操作,读超时时间为5秒,超过5秒关闭channel;

pipeline.addLast("ping", new IdleStateHandler(READ_WAIT_SECONDS, 0, 0, TimeUnit.SECONDS));

pipeline.addLast("decoder", new StringDecoder());

pipeline.addLast("encoder", new StringEncoder());

pipeline.addLast("handler", new HeartbeatServerHandler());

}

}

} 客户端handler

代码语言:javascript复制package com.heartbreak.client;

import io.netty.buffer.ByteBuf;

import io.netty.buffer.Unpooled;

import io.netty.channel.ChannelHandlerContext;

import io.netty.channel.EventLoop;

import io.netty.channel.SimpleChannelInboundHandler;

import io.netty.handler.timeout.IdleState;

import io.netty.handler.timeout.IdleStateEvent;

import io.netty.util.CharsetUtil;

import io.netty.util.ReferenceCountUtil;

import java.text.SimpleDateFormat;

import java.util.Date;

import java.util.concurrent.TimeUnit;

/**

* @author tangj

* @date 2018/6/11 22:55

*/

public class HeartBeatClientHandler extends SimpleChannelInboundHandler{

private HeartBeatClient client;

private SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:dd");

private static final ByteBuf HEARTBEAT_SEQUENCE = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("Heartbeat",

CharsetUtil.UTF_8));

public HeartBeatClientHandler(HeartBeatClient client) {

this.client = client;

}

@Override

protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {

System.out.println("收到服务端回复:"+msg);

if (msg.equals("Heartbeat")) {

ctx.write("has read message from server");

ctx.flush();

}

ReferenceCountUtil.release(msg);

}

@Override

public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {

if (evt instanceof IdleStateEvent) {

IdleState state = ((IdleStateEvent) evt).state();

if (state == IdleState.WRITER_IDLE) {

ctx.writeAndFlush(HEARTBEAT_SEQUENCE.duplicate());

}

} else {

super.userEventTriggered(ctx, evt);

}

}

@Override

public void channelInactive(ChannelHandlerContext ctx) throws Exception {

super.channelInactive(ctx);

System.err.println("客户端与服务端断开连接,断开的时间为:"+format.format(new Date()));

// 定时线程 断线重连

final EventLoop eventLoop = ctx.channel().eventLoop();

eventLoop.schedule(new Runnable() {

@Override

public void run() {

client.doConncet();

}

}, 10, TimeUnit.SECONDS);

}

}客户端启动:

代码语言:javascript复制package com.heartbreak.client;

import io.netty.bootstrap.Bootstrap;

import io.netty.buffer.ByteBuf;

import io.netty.channel.*;

import io.netty.channel.nio.NioEventLoopGroup;

import io.netty.channel.socket.SocketChannel;

import io.netty.channel.socket.nio.NioSocketChannel;

import io.netty.handler.codec.string.StringDecoder;

import io.netty.handler.codec.string.StringEncoder;

import io.netty.handler.timeout.IdleStateHandler;

import java.io.BufferedReader;

import java.io.InputStreamReader;

import java.util.Random;

import java.util.concurrent.TimeUnit;

/**

* @author tangj

* @date 2018/6/10 16:18

*/

public class HeartBeatClient {

private Random random = new Random();

public Channel channel;

public Bootstrap bootstrap;

protected String host = "127.0.0.1";

protected int port = 9817;

public static void main(String args[]) throws Exception {

HeartBeatClient client = new HeartBeatClient();

client.run();

client.sendData();

}

public void run() throws Exception {

EventLoopGroup group = new NioEventLoopGroup();

try {

bootstrap = new Bootstrap();

bootstrap.group(group)

.channel(NioSocketChannel.class)

.handler(new SimpleClientInitializer(HeartBeatClient.this));

doConncet();

} catch (Exception e) {

e.printStackTrace();

}

}

/**

* 发送数据

* @throws Exception

*/

public void sendData() throws Exception {

BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

while (true){

String cmd = in.readLine();

switch (cmd){

case "close" :

channel.close();

break;

default:

channel.writeAndFlush(in.readLine());

break;

}

}

}

/**

* 连接服务端

*/

public void doConncet() {

if (channel != null && channel.isActive()) {

return;

}

ChannelFuture channelFuture = bootstrap.connect(host, port);

channelFuture.addListener(new ChannelFutureListener() {

@Override

public void operationComplete(ChannelFuture futureListener) throws Exception {

if (channelFuture.isSuccess()) {

channel = futureListener.channel();

System.out.println("connect server successfully");

} else {

System.out.println("Failed to connect to server, try connect after 10s");

futureListener.channel().eventLoop().schedule(new Runnable() {

@Override

public void run() {

doConncet();

}

}, 10, TimeUnit.SECONDS);

}

}

});

}

private class SimpleClientInitializer extends ChannelInitializer {

private HeartBeatClient client;

public SimpleClientInitializer(HeartBeatClient client) {

this.client = client;

}

@Override

protected void initChannel(SocketChannel socketChannel) throws Exception {

ChannelPipeline pipeline = socketChannel.pipeline();

pipeline.addLast(new IdleStateHandler(0, 5, 0));

pipeline.addLast("encoder", new StringEncoder());

pipeline.addLast("decoder", new StringDecoder());

pipeline.addLast("handler", new HeartBeatClientHandler(client));

}

}

}运行结果:

1.客户端长时间未发送心跳包,服务端关闭连接

代码语言:javascript复制server start ,port: 9817

一个客户端已连接

===服务端===(READER_IDLE 读超时)

===服务端===(READER_IDLE 读超时)

===服务端===(READER_IDLE 读超时)

===服务端===(READER_IDLE 读超时)

===服务端===(读超时,关闭chanel)

一个客户端已断开连接2.客户端发送心跳包,服务端和客户端保持心跳信息

代码语言:javascript复制一个客户端已连接

客户端/127.0.0.1:55436--心跳信息--

客户端/127.0.0.1:55436--心跳信息--

客户端/127.0.0.1:55436--心跳信息--

客户端/127.0.0.1:55436--心跳信息--3.服务单宕机,断开连接,客户端进行重连

代码语言:javascript复制客户端与服务端断开连接,断开的时间为:2018-06-12 23:47:12

Failed to connect to server, try connect after 10s

Failed to connect to server, try connect after 10s

Failed to connect to server, try connect after 10s

connect server successfully代码地址:

LearnTCP

参考:

TCP长连接与短连接、心跳机制Socket的长连接和短连接.