Github源碼下載:https://github.com/chenxingxing6/sourcecode/tree/master/study-net
一、前言
1.1 什麼是心跳檢測
在分佈式系統中,分佈在不同主機上的節點需要檢測其他節點的狀態,如服務器節點需要檢測從節點是否失效。爲了檢測對方節點的有效性,每隔固定時間就發送一個固定信息給對方,對方回覆一個固定信息,如果長時間沒有收到對方的回覆,則斷開與對方的連接。發包方既可以是服務端,也可以是客戶端。因爲是每隔固定時間發送一次,類似心跳,所以發送的固定信息稱爲心跳包。心跳包一般爲比較小的包,可根據具體實現。一般而言,應該客戶端主動向服務器發送心跳包,因爲服務器向客戶端發送心跳包會影響服務器的性能。
所有保持長連接的地方都要用到心跳包,心跳包就是在客戶端和服務器間定時通知對方自己狀態的一個自己定義的命令字,按照一定的時間間隔發送,類似於心跳,所以叫做心跳包。
1.2 實現方式
二、自己在應用層實現心跳檢測
客戶端:
1.Client通過持有Socket的對象,可以發送Massage Object(消息)給服務端。
2.如果keepAliveDelay 2秒內未發送任何數據,則自動發送一個KeepAlive(心跳)給服務端,用於維持連接。
服務端:
1.由於客戶端會定時(keepAliveDelay毫秒)發送維持連接的信息過來,所以,服務端要有一個檢測機制。
2.當服務端receiveTimeDelay毫秒(程序中是3秒)內未接收任何數據,則自動斷開與客戶端的連接。
2.1 Client.java
package com.demo.longconn.client;
import com.demo.longconn.KeepAlive;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.util.concurrent.TimeUnit;
/**
* User: lanxinghua
* Date: 2019/11/4 18:49
* Desc: 客戶端,定時向服務端程序,發送一個維持連接包的
*/
public class Client {
private String ip;
private int port;
private String name;
private Socket socket;
// 連接狀態
private volatile boolean isConn = false;
// 最後一次發送數據時間
private long lastSendTime;
public Client(String ip, int port, String name) {
this.ip = ip;
this.port = port;
this.name = name;
}
public void start(){
if (isConn)return;
try {
System.out.println(name + "已啓動.....");
socket = new Socket(ip, port);
isConn = true;
// 保持長連接的線程,每隔2秒項服務器發一個一個保持連接的心跳消息
lastSendTime = System.currentTimeMillis();
new Thread(new KeepAliveWatchDog()).start();
// 接受消息的線程,處理消息
// new Thread(new ReceiveWatchDog()).start();
}catch (Exception e){
e.printStackTrace();
stop();
}
}
public void stop(){
if (isConn){
isConn = false;
}
}
class KeepAliveWatchDog implements Runnable{
// 連接推遲時間2s
long keepAliveDelay = 2000;
// 檢測推遲時間
long checkDelay = 10;
public void run() {
if (isConn == false){
System.out.println(isConn);
}
while (isConn){
long tt = (System.currentTimeMillis() - lastSendTime);
// 到了時間需要去發送心跳檢測
if (tt > keepAliveDelay){
try {
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
oos.writeObject(new KeepAlive(name));
oos.flush();
lastSendTime = System.currentTimeMillis();
// 假設客戶端宕機
if ("客戶端3".equals(name)){
System.out.println(name + "出現故障,將在1s後宕機");
TimeUnit.SECONDS.sleep(1);
stop();
}
if ("客戶端2".equals(name)){
System.out.println(name + "出現故障,將在2s後宕機");
TimeUnit.SECONDS.sleep(2);
stop();
}
}catch (Exception e){
e.printStackTrace();
stop();
}
}else {
try {
TimeUnit.MILLISECONDS.sleep(checkDelay);
}catch (Exception e){
e.printStackTrace();
stop();
}
}
}
}
}
class ReceiveWatchDog implements Runnable{
public void run() {
while (isConn){
try {
InputStream in = socket.getInputStream();
// 判斷流裏面的字節數
if (in.available() > 0){
ObjectInputStream ois = new ObjectInputStream(in);
Object o = ois.readObject();
System.out.println(name + "check result:" + o.toString());
}else {
TimeUnit.MILLISECONDS.sleep(10);
}
}catch (Exception e){
e.printStackTrace();
}
}
}
}
}
2.2 Server.java
package com.demo.longconn.server;
import com.demo.longconn.KeepAlive;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* User: lanxinghua
* Date: 2019/11/4 18:49
* Desc: 服務端,服務端檢測receiveTimeDelay內沒接收到任何數據,自動和客戶端斷開連接。
*/
public class Server {
private int port;
private ServerSocket serverSocket;
private Socket socket;
// 連接狀態
private volatile boolean isConn = false;
// 檢測心跳時間 3s
private long receiveTimeDelay = 3000;
// 最後一次接收時間
long lastReceiveTime;
// 有效客戶端
private static Map<String/*clientName*/, ClientState> map = new HashMap<String, ClientState>(10);
private Object lock = new Object();
public Server(int port) {
this.port = port;
}
public void start(){
if (isConn) return;
try {
System.out.println("服務端啓動......");
isConn = true;
// 檢測客戶端心跳
new Thread(new ConnectWatchDog()).start();
}catch (Exception e){
e.printStackTrace();
stop();
}
}
public void stop(){
if (isConn) isConn = false;
}
/**
* 連接監控
*/
class ConnectWatchDog implements Runnable{
public void run() {
try {
serverSocket = new ServerSocket(port);
while (isConn){
socket = serverSocket.accept();
lastReceiveTime = System.currentTimeMillis();
new Thread(new SocketAction(socket)).start();
}
}catch (Exception e){
e.printStackTrace();
stop();
}
}
}
/**
* 監控客戶端宕機的機器
*/
class CleanScan implements Runnable{
public void run() {
while (true) {
synchronized (lock) {
if (map.isEmpty()) {
return;
}
for (Map.Entry<String, ClientState> client : map.entrySet()) {
ClientState clientState = client.getValue();
if (clientState == null) {
map.remove(client.getKey());
return;
}
if (clientState.getIsValid() == 0) {
continue;
}
if (System.currentTimeMillis() - clientState.getLastReTime() > receiveTimeDelay) {
clientState.setIsValid(0);
System.out.println("有效客戶端" + getAliveClientCount() + " 客戶端宕機" + clientState.toString());
}
}
}
try {
TimeUnit.SECONDS.sleep(3);
}catch (Exception e){
e.printStackTrace();
}
}
}
/**
* 獲取有效客戶端數量
* @return
*/
private long getAliveClientCount(){
return map.values().stream().filter(e -> e.getIsValid() == 1).count();
}
}
class SocketAction implements Runnable{
boolean isRun = true;
Socket s;
ClientState clientState;
public SocketAction(Socket s) {
this.s = s;
clientState = new ClientState();
}
public void run() {
try {
while (isConn && isRun){
new Thread(new CleanScan()).start();
if (System.currentTimeMillis() - lastReceiveTime > receiveTimeDelay){
close();
}else {
InputStream in = s.getInputStream();
if (in.available() > 0){
ObjectInputStream ois = new ObjectInputStream(in);
Object o = ois.readObject();
lastReceiveTime = System.currentTimeMillis();
if (o instanceof KeepAlive){
KeepAlive alive = (KeepAlive) o;
System.out.println("客戶端數量:" + getAliveClientCount() + " "+alive.getClientName() + " 心跳檢查ok:" + o.toString());
clientState.setClientName(alive.getClientName());
clientState.setIsValid(1);
clientState.setLastReTime(lastReceiveTime);
map.put(alive.getClientName(), clientState);
}
}else {
TimeUnit.MILLISECONDS.sleep(10);
}
}
}
}catch (Exception e){
e.printStackTrace();
close();
}
}
private long getAliveClientCount(){
return map.values().stream().filter(e -> e.getIsValid() == 1).count();
}
private void close(){
if (isRun) isRun = false;
if (socket!=null){
try {
socket.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
}
}
2.3 ClientState.java
package com.demo.longconn.server;
/**
* @Author: cxx
* @Date: 2019/11/4 22:34
* 客戶端服務器狀態
*/
public class ClientState {
private int isValid = 0;
private String clientName;
private long lastReTime;
public int getIsValid() {
return isValid;
}
public void setIsValid(int isValid) {
this.isValid = isValid;
}
public String getClientName() {
return clientName;
}
public void setClientName(String clientName) {
this.clientName = clientName;
}
public long getLastReTime() {
return lastReTime;
}
public void setLastReTime(long lastReTime) {
this.lastReTime = lastReTime;
}
@Override
public String toString() {
return "ClientState{" +
"isValid=" + isValid +
", clientName='" + clientName + '\'' +
", lastReTime=" + lastReTime +
'}';
}
}
2.4 KeepAlive.java
package com.demo.longconn;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* User: lanxinghua
* Date: 2019/11/4 18:45
* Desc: 維持連接的消息對象,心跳對象
*/
public class KeepAlive implements Serializable {
private String clientName;
public KeepAlive(String clientName) {
this.clientName = clientName;
}
@Override
public String toString() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "\t維持連接包";
}
public String getClientName() {
return clientName;
}
public void setClientName(String clientName) {
this.clientName = clientName;
}
}
2.5 ServerTest.java
package com.demo.longconn;
import com.demo.longconn.server.Server;
/**
* User: lanxinghua
* Date: 2019/11/4 19:32
* Desc:
*/
public class ServerTest {
public static void main(String[] args) {
new Server(9999).start();
}
}
2.6 ClientTest.java
package com.demo.longconn;
import com.demo.longconn.client.Client;
/**
* User: lanxinghua
* Date: 2019/11/4 19:32
* Desc:
*/
public class ClientTest {
public static void main(String[] args) {
for (int i = 1; i <=3; i++) {
new Client("localhost", 9999, "客戶端" + i).start();
}
}
}
三、測試
3.1 開啓服務端
服務端啓動…
3.2 開啓客戶端
客戶端1已啓動…
客戶端2已啓動…
客戶端3已啓動…
測試計劃:假設客戶端宕機
// 假設客戶端宕機
if ("客戶端3".equals(name)){
System.out.println(name + "出現故障,將在1s後宕機");
TimeUnit.SECONDS.sleep(1);
stop();
}
if ("客戶端2".equals(name)){
System.out.println(name + "出現故障,將在2s後宕機");
TimeUnit.SECONDS.sleep(2);
stop();
}