心跳的原理和对应的设计

Socket心跳包机制总结

 
跳包之所以叫心跳包是因为:它像心跳一样每隔固定时间发一次,以此来告诉服务器,这个客户端还活着。事实上这是为了保持长连接,至于这个包的内容,是没有什么特别规定的,不过一般都是很小的包,或者只包含包头的一个空包。
在TCP的机制里面,本身是存在有心跳包的机制的,也就是TCP的选项:SO_KEEPALIVE。系统默认是设置的2小时的心跳频率。但是它检查不到机器断电、网线拔出、防火墙这些断线。而且逻辑层处理断线可能也不是那么好处理。一般,如果只是用于保活还是可以的。
心跳包一般来说都是在逻辑层发送空的echo包来实现的。下一个定时器,在一定时间间隔下发送一个空包给客户端,然后客户端反馈一个同样的空包回来,服务器如果在一定时间内收不到客户端发送过来的反馈包,那就只有认定说掉线了。
其实,要判定掉线,只需要send或者recv一下,如果结果为零,则为掉线。但是,在长连接下,有可能很长一段时间都没有数据往来。理论上说,这个连接是一直保持连接的,但是实际情况中,如果中间节点出现什么故障是难以知道的。更要命的是,有的节点(防火墙)会自动把一定时间之内没有数据交互的连接给断掉。在这个时候,就需要我们的心跳包了,用于维持长连接,保活。
在获知了断线之后,服务器逻辑可能需要做一些事情,比如断线后的数据清理呀,重新连接呀……当然,这个自然是要由逻辑层根据需求去做了。
总的来说,心跳包主要也就是用于长连接的保活和断线处理。一般的应用下,判定时间在30-40秒比较不错。如果实在要求高,那就在6-9秒。

心跳检测步骤
1 客户端每隔一个时间间隔发生一个探测包给服务器
2 客户端发包时启动一个超时定时器
3 服务器端接收到检测包,应该回应一个包
4 如果客户机收到服务器的应答包,则说明服务器正常,删除超时定时器
5 如果客户端的超时定时器超时,依然没有收到应答包,则说明服务器挂了
 
 
 
TCP连接简介
当网络通信时采用TCP协议时,在真正的读写操作之前,server与client之间必须建立一个连接,
当读写操作完成后,双方不再需要这个连接时它们可以释放这个连接,
连接的建立是需要三次握手的,而释放则需要4次握手,
所以说每个连接的建立都是需要资源消耗和时间消耗的

 

所谓的心跳包就是客户端定时放送简单的信息给服务器端,告诉它我还在而已。代码就是每隔几分钟发送一个固定信息给服务器端,服务器端回复一个固定信息。如果服务器端几分钟后没有收到客户端信息则视客户端断开。比如有些通信软件长时间不适用,要想知道它的状态是在线还是离线,就需要心跳包,定时发包收包。

心跳包之所以叫心跳包是因为:它像心跳一样每隔固定时间发一次,以此来告诉服务器,这个客户端还活在。事实上这是为了保持长连接,至于这个包的内容,是没有什么特别规定的,不过一般都是很小的包,活着只包含包头的一个空包。

在TCP机制里面,本身是存在有心跳包机制的,也就是TCP选项:SO_KEEPALIVE. 系统默认是设置的2小时的心跳频率。

 


经典的三次握手示意图:


经典的四次握手关闭图:


一、长连接与短连接
长连接: 指在一个TCP连接上可以连续发送多个数据包,
在TCP连接保持期间,如果没有数据包发送,需要双方发检测包以维持此连接;
一般需要自己做在线维持。
短连接: 指通信双方有数据交互时,就建立一个TCP连接,数据发送完成后,则断开此TCP连接;
一般银行都使用短连接。
它的优点是:管理起来比较简单,存在的连接都是有用的连接,不需要额外的控制手段

比如http的,只是连接、请求、关闭,过程时间较短,服务器若是一段时间内没有收到请求即可关闭连接。
其实长连接是相对于通常的短连接而说的,也就是长时间保持客户端与服务端的连接状态。

长连接与短连接的操作过程
通常的短连接操作步骤是:
连接→数据传输→关闭连接;

而长连接通常就是:
连接→数据传输→保持连接(心跳)→数据传输→保持连接(心跳)→……→关闭连接;

这就要求长连接在没有数据通信时,定时发送数据包(心跳),以维持连接状态,
短连接在没有数据传输时直接关闭就行了

什么时候用长连接,短连接?
长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况。
每个TCP连接都需要三步握手,这需要时间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多,
所以每个操作完后都不断开,下次次处理时直接发送数据包就OK了,不用建立TCP连接。

例如:数据库的连接用长连接,
如果用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费。

二、发送接收方式
1、异步
报文发送和接收是分开的,相互独立的,互不影响。这种方式又分两种情况:
(1)异步双工:接收和发送在同一个程序中,由两个不同的子进程分别负责发送和接收
(2)异步单工:接收和发送是用两个不同的程序来完成。

2、同步
报文发送和接收是同步进行,既报文发送后等待接收返回报文。
同步方式一般需要考虑超时问题,即报文发出去后不能无限等待,需要设定超时时间,
超过该时间发送方不再等待读返回报文,直接通知超时返回。

在长连接中一般是没有条件能够判断读写什么时候结束,所以必须要加长度报文头。
读函数先是读取报文头的长度,再根据这个长度去读相应长度的报文。

三. 单工、半双工和全双工
根据通信双方的分工和信号传输方向可将通信分为三种方式:
单工、
半双工、
全双工。

在计算机网络中主要采用双工方式,其中:
局域网采用半双工方式,
城域网和广域网采用全双年方式。

1. 单工(Simplex)方式:
通信双方设备中发送器与接收器分工明确,只能在由发送器向接收器的单一固定方向上传送数据。
采用单工通信的典型发送设备如早期计算机的读卡器,典型的接收设备如打印机。

2. 半双工(Half Duplex)方式:
通信双方设备既是发送器,也是接收器,两台设备可以相互传送数据,但某一时刻则只能向一个方向传送数据。
例如,步话机是半双工设备,因为在一个时刻只能有一方说话。

3. 全双工(Full Duplex)方式:
通信双方设备既是发送器,也是接收器,两台设备可以同时在两个方向上传送数据。
例如,电话是全双工设备,因为双方可同时说话。

而像WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,
而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源,
如果用长连接,而且同时有成千上万的用户,如果每个用户都占用一个连接的话,那可想而知吧。
所以并发量大,但每个用户无需频繁操作情况下需用短连好。

总之,长连接和短连接的选择要视情况而定。
 
面来看看代码:

首先创建一个实体对象,对应配置信息

Java代码 收藏代码
  1. package com.socket;
  2. /**
  3. * @说明 连接的公共属性
  4. * @author cuisuqiang
  5. * @version 1.0
  6. * @since
  7. */
  8. public class SocketEntity {
  9. /**
  10. * 连接的名字,以名字作为Key
  11. */
  12. private String name;
  13. /**
  14. * 连接的IP
  15. */
  16. private String ip;
  17. /**
  18. * 连接的端口
  19. */
  20. private int port;
  21. /**
  22. * 是否保持连接
  23. */
  24. private boolean keepConn;
  25. public String getName() {
  26. return name;
  27. }
  28. public void setName(String name) {
  29. this.name = name;
  30. }
  31. public String getIp() {
  32. return ip;
  33. }
  34. public void setIp(String ip) {
  35. this.ip = ip;
  36. }
  37. public int getPort() {
  38. return port;
  39. }
  40. public void setPort(int port) {
  41. this.port = port;
  42. }
  43. public boolean isKeepConn() {
  44. return keepConn;
  45. }
  46. public void setKeepConn(boolean keepConn) {
  47. this.keepConn = keepConn;
  48. }
  49. }

创建配置文件socket.properties

socket1_isKeep属性相当于Spring中的单例还是每次创建

Java代码 收藏代码
  1. # @author cuisuqiang
  2. socket1=socket1
  3. socket1_ip=127.0.0.1
  4. socket1_port=8001
  5. socket1_isKeep=1
  6. socket2=socket2
  7. socket2_ip=127.0.0.1
  8. socket2_port=8001
  9. socket2_isKeep=0
  10. # 连接的数量,一定要和实际配置的数量匹配
  11. socketConnCount=2
  12. # 公共的检测间隔 秒
  13. commonCheckTime=5

然后初始化配置信息和连接,同时该类中有静态变量来保持连接对象

Java代码 收藏代码
  1. package com.socket;
  2. import java.util.ArrayList;
  3. import java.util.LinkedHashMap;
  4. import java.util.List;
  5. import java.util.Map;
  6. import java.util.Properties;
  7. import java.util.concurrent.ExecutorService;
  8. import java.util.concurrent.Executors;
  9. import org.apache.commons.logging.Log;
  10. import org.apache.commons.logging.LogFactory;
  11. /**
  12. * @说明 变量保持
  13. * @author cuisuqiang
  14. * @version 1.0
  15. * @since
  16. */
  17. public class SocketKeep {
  18. private static Log logger = LogFactory.getLog(SocketKeep.class);
  19. /**
  20. * 配置信息加载
  21. */
  22. public static List<SocketEntity> socketEntityList = new ArrayList<SocketEntity>();
  23. /**
  24. * 连接对象保持,只保持需要系统保持的连接
  25. */
  26. public static Map<String, SocketCui> socketMap = new LinkedHashMap<String, SocketCui>();
  27. /**
  28. * 连接对象是否锁定 1:锁定,其他未锁定
  29. */
  30. public static Map<String, String> socketIsLock = new LinkedHashMap<String, String>();
  31. /**
  32. * 共用连接检测间隔
  33. */
  34. public static int commonCheckTime = 2;
  35. /**
  36. * 连接的数量,一定要和实际配置的数量匹配
  37. */
  38. public static int socketConnCount = 0;
  39. public static ExecutorService executorService = null;// 线程池
  40. /**
  41. * 初始化所有连接信息
  42. */
  43. public static void initSocketKeep() {
  44. Properties properties = null;
  45. try {
  46. properties = new Properties();
  47. properties.load(SocketKeep.class.getClassLoader().getResourceAsStream("socket.properties"));
  48. logger.warn("加载socket.properties文件成功!");
  49. } catch (Exception e) {
  50. logger.error("加载socket.properties文件失败!", e);
  51. properties = null;
  52. }
  53. if (null != properties) {
  54. try {
  55. commonCheckTime = Integer.parseInt(properties.getProperty("commonCheckTime"));
  56. socketConnCount = Integer.parseInt(properties.getProperty("socketConnCount"));
  57. executorService = Executors.newFixedThreadPool(socketConnCount + 1);
  58. } catch (Exception e) {
  59. executorService = Executors.newFixedThreadPool(1);
  60. logger.error("解析共用信息时错误!", e);
  61. // 系统忽略这两个属性的加载异常
  62. }
  63. SocketEntity socketEntity = null;
  64. for (int i = 1; i <= socketConnCount; i++) {
  65. String name = properties.getProperty("socket" + i);
  66. if(null != name){
  67. socketEntity = new SocketEntity();
  68. String ip = properties.getProperty("socket" + i + "_ip");
  69. String port = properties.getProperty("socket" + i + "_port");
  70. String isKeep = properties.getProperty("socket" + i + "_isKeep");
  71. socketEntity.setName(name);
  72. socketEntity.setIp(ip);
  73. socketEntity.setPort(Integer.parseInt(port));
  74. boolean keepConn = false;
  75. if(null != isKeep && "1".equals(isKeep)){
  76. keepConn = true;
  77. }
  78. socketEntity.setKeepConn(keepConn);
  79. socketEntityList.add(socketEntity);
  80. }
  81. }
  82. }
  83. logger.warn("加载Socket连接配置信息结束!");
  84. logger.warn("开始初始化Socket连接!");
  85. SocketCui socket = null;
  86. for(SocketEntity socketEntity : socketEntityList){
  87. if(null != socketEntity && socketEntity.isKeepConn()){
  88. try {
  89. socket = new SocketCui(socketEntity.getIp(),socketEntity.getPort());
  90. socket.setSoTimeout(0);
  91. socket.setKeepAlive(true);
  92. socket.setName(socketEntity.getName());
  93. } catch (Exception e) {
  94. logger.error("初始化某个连接时错误!错误的连接将放弃!资源名称:" + socketEntity.getName(), e);
  95. socket = null;
  96. }
  97. if(null != socket){
  98. socketMap.put(socketEntity.getName(), socket);
  99. }else{
  100. socketMap.put(socketEntity.getName(), new SocketCui());
  101. }
  102. socketIsLock.put(socketEntity.getName(), "0");
  103. }
  104. }
  105. // 开始执行检查
  106. executorService.execute(new CheckThread());
  107. logger.warn("初始化Socket连接结束!");
  108. }
  109. }

启动的线程是用于检查连接的

Java代码 收藏代码
  1. package com.socket;
  2. import org.apache.commons.logging.Log;
  3. import org.apache.commons.logging.LogFactory;
  4. /**
  5. * @说明 轮询检测某个连接当前是否可用
  6. * @author cuisuqiang
  7. * @version 1.0
  8. * @since 当遇到一个错误的连接,将会启动重连,同时挂起该连接的使用
  9. */
  10. public class CheckThread implements Runnable {
  11. private static Log logger = LogFactory.getLog(CheckThread.class);
  12. public void run() {
  13. while(true){
  14. SocketCui socket = null;
  15. for(SocketEntity socketEntity : SocketKeep.socketEntityList){
  16. if(null != socketEntity && socketEntity.isKeepConn()){
  17. String isLock = SocketKeep.socketIsLock.get(socketEntity.getName());
  18. // 如果当前未被使用
  19. if(!"1".equals(isLock)){
  20. // 锁定引用
  21. SocketKeep.socketIsLock.put(socketEntity.getName(), "1");
  22. socket = SocketKeep.socketMap.get(socketEntity.getName());
  23. try {
  24. // 发送一个心跳包
  25. socket.sendUrgentData(0xFF);
  26. // 释放资源
  27. SocketKeep.socketIsLock.put(socketEntity.getName(), "0");
  28. } catch (Exception e) {
  29. logger.error("检查连接时异常!启动重连!资源名称:" + socketEntity.getName(), e);
  30. // 如果异常,应该建立一个线程去初始化该连接
  31. InitSocket initS = new InitSocket(socketEntity.getName());
  32. new Thread(initS).start();
  33. }
  34. }
  35. }
  36. }
  37. // 执行间隔
  38. try {
  39. logger.error("本次检测结束!");
  40. Thread.sleep(SocketKeep.commonCheckTime * 1000);
  41. } catch (Exception e) {
  42. }
  43. }
  44. }
  45. }

当检查线程发现无效的连接时会启动新的线程初始化该连接

Java代码 收藏代码
  1. package com.socket;
  2. import org.apache.commons.logging.Log;
  3. import org.apache.commons.logging.LogFactory;
  4. /**
  5. * @说明 负责初始化失效的连接
  6. * @author cuisuqiang
  7. * @version 1.0
  8. * @since
  9. */
  10. public class InitSocket implements Runnable{
  11. private static Log logger = LogFactory.getLog(InitSocket.class);
  12. /**
  13. * 是否有某个连接的配置信息,只有有配置信息才能建立连接
  14. */
  15. private static boolean isHave = false;
  16. private SocketEntity socketEntity = null;
  17. private String name;
  18. public InitSocket(String name){
  19. this.name = name;
  20. // 检测是否有某个连接的配置信息
  21. for(SocketEntity socketEntity : SocketKeep.socketEntityList){
  22. if(null != socketEntity && socketEntity.isKeepConn()){
  23. if(socketEntity.getName().equals(name)){
  24. this.setSocketEntity(socketEntity);
  25. isHave = true;
  26. }
  27. }
  28. }
  29. }
  30. public void run() {
  31. boolean isError = true;
  32. SocketCui socket = null;
  33. if(isHave){
  34. while(isError){
  35. try {
  36. socket = new SocketCui(this.getSocketEntity().getIp(),this.getSocketEntity().getPort());
  37. socket.setSoTimeout(0);
  38. socket.setKeepAlive(true);
  39. socket.setName(this.name);
  40. // 发送一个心跳包
  41. socket.sendUrgentData(0xFF);
  42. } catch (Exception e) {
  43. logger.error("建立资源连接时错误!资源:" + this.name, e);
  44. socket = null;
  45. }
  46. if(null != socket){
  47. SocketKeep.socketMap.put(this.getSocketEntity().getName(), socket);
  48. // 设置连接当前可用
  49. SocketKeep.socketIsLock.put(this.getSocketEntity().getName(), "0");
  50. logger.warn("建立资源连接成功!资源名称:" + this.name);
  51. isError = false;
  52. }
  53. try {
  54. Thread.sleep(2 * 1000);
  55. } catch (Exception e) {
  56. }
  57. }
  58. }else{
  59. logger.error("没有发现指定资源的配置信息!资源名称:" + this.name);
  60. }
  61. logger.warn("初始化资源执行结束!资源名称:" + this.name);
  62. }
  63. public SocketEntity getSocketEntity() {
  64. return socketEntity;
  65. }
  66. public void setSocketEntity(SocketEntity socketEntity) {
  67. this.socketEntity = socketEntity;
  68. }
  69. }

同时注意,用户在使用连接后会调用关闭方法。我们是不能让连接关闭的,要保持常连接。所以如果用户指定的是保持这个连接,那么返回的连接对象就不是原来的Socket对象了,我们要重写这个对象

Java代码 收藏代码
  1. package com.socket;
  2. import java.io.IOException;
  3. import java.net.Socket;
  4. import java.net.UnknownHostException;
  5. /**
  6. * @说明 被重新定义的连接对象,增加了名字这个属性,重写了关闭的方法
  7. * @author cuisuqiang
  8. * @version 1.0
  9. * @since
  10. */
  11. public class SocketCui extends Socket{
  12. /**
  13. * 为对象增加名称属性
  14. */
  15. private String name;
  16. public SocketCui() {
  17. }
  18. public SocketCui(String ip,int port) throws UnknownHostException, IOException{
  19. super(ip, port);
  20. }
  21. /**
  22. * 覆盖关闭的方法
  23. */
  24. @Override
  25. public synchronized void close() throws IOException {
  26. SocketKeep.socketIsLock.put(this.name, "0");
  27. }
  28. public String getName() {
  29. return name;
  30. }
  31. public void setName(String name) {
  32. this.name = name;
  33. }
  34. }

这样,关闭时只会解除其占用,而不会真正关闭该连接。

我们来写一个服务端,这个服务端一直接受连接,并检查连接的有效性,当失效时不处理

同时打印接收到的连接信息

Java代码 收藏代码
  1. package com.test;
  2. import java.net.*;
  3. import java.text.SimpleDateFormat;
  4. import java.util.Date;
  5. /**
  6. * @说明 服务端,始终接受连接
  7. * @author cuisuqiang
  8. * @version 1.0
  9. * @since
  10. */
  11. public class ServiceTest {
  12. public static void main(String[] args) {
  13. try {
  14. ServerSocket ss1 = new ServerSocket(8001);
  15. Runnable accumelatora1 = new Accumulatort(ss1);
  16. Thread threada = new Thread(accumelatora1, "ThreadA");
  17. threada.start();
  18. System.out.println("服务启动完毕!");
  19. } catch (Exception e) {
  20. e.printStackTrace();
  21. }
  22. }
  23. }
  24. class Accumulatort implements Runnable {
  25. ServerSocket ss = null;
  26. public Accumulatort(ServerSocket s) {
  27. this.ss = s;
  28. }
  29. @SuppressWarnings("unchecked")
  30. public void run() {
  31. try {
  32. SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  33. while (true) {
  34. Socket s = ss.accept();
  35. System.out.println(format.format(new Date()) + " " + "---------收到请求!");
  36. new Thread(new ServiceImpl(s)).start();
  37. }
  38. } catch (Exception e) {
  39. e.printStackTrace();
  40. }
  41. }
  42. }

服务端处理连接的实现类

Java代码 收藏代码
  1. package com.test;
  2. import java.net.Socket;
  3. /**
  4. * @说明 循环发送心跳包保持连接属性
  5. * @author cuisuqiang
  6. * @version 1.0
  7. * @since
  8. */
  9. public class ServiceImpl implements Runnable {
  10. Socket socket = null;
  11. public ServiceImpl(Socket s) {
  12. this.socket = s;
  13. }
  14. public void run() {
  15. boolean isKeep = true;
  16. try {
  17. while (isKeep) {
  18. socket.sendUrgentData(0xFF);
  19. Thread.sleep(1 * 1000);
  20. }
  21. } catch (Exception e) {
  22. isKeep = false;
  23. }
  24. }
  25. }

先启动服务端,然后我们再写一个测试端,这个测试类会不断去管理器中获取相应的连接,同时打印连接信息

通过打印的连接信息,我们可以知道获取的是不是同一个对象

同时如果你一直获取单例的对象,那么可能出现该连接被检查线程占用的情况

Java代码 收藏代码
  1. package com.test;
  2. import java.net.Socket;
  3. import java.text.SimpleDateFormat;
  4. import java.util.Date;
  5. import com.socket.CommonSocket;
  6. import com.socket.SocketKeep;
  7. /**
  8. * @说明 循环去请求获得相应的连接然后打印连接地址
  9. * @author cuisuqiang
  10. * @version 1.0
  11. * @since
  12. */
  13. public class GetSocketTest {
  14. public static void main(String[] args) {
  15. SocketKeep.initSocketKeep();
  16. while(true){
  17. SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  18. try {
  19. Socket socket1 = CommonSocket.getSocketByName("socket1");
  20. if(null != socket1){
  21. System.out.println(format.format(new Date()) + " " + socket1.toString());
  22. socket1.close();
  23. }
  24. } catch (Exception e) {
  25. e.printStackTrace();
  26. }
  27. try {
  28. Socket socket2 = CommonSocket.getSocketByName("socket2");
  29. if(null != socket2){
  30. System.out.println(format.format(new Date()) + " " + socket2.toString());
  31. socket2.close();
  32. }
  33. } catch (Exception e) {
  34. e.printStackTrace();
  35. }
  36. try {
  37. Thread.sleep(1000);
  38. } catch (Exception e) {
  39. }
  40. }
  41. }
  42. }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值