Socket Client 长连接及心跳检测

简介:

所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口。

TCP/IP协议:

TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/网际协议)是指能够在多个不同网络间实现信息传输的协议簇。TCP/IP协议不仅仅指的是TCP 和IP两个协议,而是指一个由FTPSMTP、TCP、UDP、IP等协议构成的协议簇, 只是因为在TCP/IP协议中TCP协议和IP协议最具代表性,所以被称为TCP/IP协议。

说明: springBoot下socket一对一建立长连接通讯,包括客户端心跳检测、掉线重连、收发信息等

目录结构:

 

 1、ClientHeartBeatThread(客户端心跳检测、保持长连接状态)

import com.ws.common.enums.SocketMsgTypeEnum;
import com.ws.common.util.SocketUtil;
import com.ws.common.util.StreamUtil;
import com.ws.common.vo.SocketMsgDataVo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.DataOutputStream;
import java.io.OutputStream;
import java.net.Socket;

/**
 * 客户端心跳检测、保持长连接状态
 */
public class ClientHeartBeatThread implements Runnable {

    private final static Logger log = LoggerFactory.getLogger(ClientHeartBeatThread.class);

    private Socket socket;
    private Object lockObject = new Object(); //锁对象,用于线程通讯,唤醒重试线程

    //3间隔多长时间发送一次心跳检测
    private int socketHeartIntervalTime;

    private volatile boolean isStop = false;

    public ClientHeartBeatThread(Socket socket, int socketHeartIntervalTime, Object lockObject) {
        this.socket = socket;
        this.socketHeartIntervalTime = socketHeartIntervalTime;
        this.lockObject = lockObject;
    }

    @Override
    public void run() {
        OutputStream outputStream = null;
        DataOutputStream dataOutputStream = null;
        try {
            outputStream = socket.getOutputStream();
            dataOutputStream = new DataOutputStream(outputStream);
            //客户端心跳检测
            while (!this.isStop && !socket.isClosed()) {
                SocketMsgDataVo msgDataVo = new SocketMsgDataVo();
                msgDataVo.setType((byte) 1);
                msgDataVo.setBody("from client:Is connect ok ?");
                if (msgDataVo != null && msgDataVo.getBody() != null) { //正文内容不能为空,否则不发)
                    SocketUtil.writeMsgData(dataOutputStream, msgDataVo);
                }
                try {
                    Thread.sleep(socketHeartIntervalTime * 1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } catch (Exception e) {
            log.error("客户端心跳消息发送异常");
            e.printStackTrace();
        } finally {
            this.isStop = true;
            log.info("客户端旧心跳线程已摧毁");
            StreamUtil.closeOutputStream(dataOutputStream);
            StreamUtil.closeOutputStream(outputStream);
            SocketUtil.closeSocket(socket);
            //最后唤醒线程、重建连接
            synchronized (lockObject) {
                lockObject.notify();
            }
        }
    }

    public boolean isStop() {
        return isStop;
    }

    public void setStop(boolean stop) {
        isStop = stop;
    }
}

2、ClientRecvThread(客户端发送,服务端消息接收线程)

import com.ws.common.enums.SocketMsgTypeEnum;
import com.ws.common.util.SocketUtil;
import com.ws.common.util.StreamUtil;
import com.ws.common.vo.SocketMsgDataVo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;

/**
 * 客户端发送,服务端消息接收线程
 */
public class ClientRecvThread implements Runnable {

    private final static Logger log = LoggerFactory.getLogger(ClientRecvThread.class);

    private Socket socket;

    private volatile boolean isStop = false;

    public ClientRecvThread(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        //线程终止条件: 设置标志位为 true or socket 已关闭
        InputStream inputStream = null;
        DataInputStream dataInputStream = null;
        try {
            inputStream = socket.getInputStream();
            dataInputStream = new DataInputStream(inputStream);
            while (!isStop && !socket.isClosed()) {
                SocketMsgDataVo msgDataVo = SocketUtil.readMsgData(dataInputStream);
                log.info("客户端收到消息:{}",msgDataVo.toString());
                //相对耗时,可以开线程来处理消息,否则影响后续消息接收处理速率
                if (msgDataVo.getType() == (byte) 1) {
                    //------------------此处业务逻辑处理-----------------------

                    //根据通讯协议,解析正文json数据
                    /*if (msgDataVo.getBody() != null) {
                        JSONObject jsonObject = JSONObject.fromObject(msgDataVo.getBody());
                        RealTimeMsgEntity realTimeMsgEntity = (RealTimeMsgEntity) JSONObject.toBean(jsonObject, RealTimeMsgEntity.class);
                    }*/
                } else if (msgDataVo.getType() == (byte) 2){
                    log.error("已有客户端跟服务端建立连接, 暂时不被允许");
                    //结束、释放资源
                    break;
                } else {
                    //其它消息类型不处理
                }
            }
        } catch (IOException e) {
            log.error("客户端接收消息发生异常");
            e.printStackTrace();
        } finally {
            this.isStop = true;
            log.info("客户端旧接收线程已摧毁");
            StreamUtil.closeInputStream(dataInputStream);
            StreamUtil.closeInputStream(inputStream);
            SocketUtil.closeSocket(socket);
            /*if (socket.isClosed()) {
                System.out.println("socket.isClosed");
            }*/
        }

    }

    public boolean getStop() {
        return isStop;
    }

    public void setStop(boolean stop) {
        isStop = stop;
    }
}

3、ClientSocketService(socket客户端服务)

import com.ws.common.util.SocketUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.net.Socket;

/**
 * @Desc socket客户端服务
 * @Notice 服务端仅允许一个socket客户端连接,先来后到
 * @Author ws
 * @Time 2020/2/21
 */
@Component
public class ClientSocketService implements InitializingBean {

    private final static Logger log = LoggerFactory.getLogger(ClientSocketService.class);

    private Socket socket;
    private final Object lockObject = new Object(); //锁对象,用于线程通讯,唤醒重试线程

    private final static int THREAD_SLEEP_MILLS = 30000;

    @Value("${qxts.socket.host}")
    private String host;

    @Value("${qxts.socket.port}")
    private int port;

    //30s 间隔多少秒发送一次心跳检测
    @Value("${qxts.socket.heart.interval.time}")
    private int socketHeartIntervalTime;

    //在该类的依赖注入完毕之后,会自动调用afterPropertiesSet方法,否则外部tomcat部署会无法正常启动socket

    //jar包的启动时直接由项目的主函数开始启动,此时会先初始化IOC容器,然后才会进行内置Servlet环境(一般为Tomcat)的启动。
    //war包通常使用Tomcat进行部署启动,在tomcat启动war应用时,会先进行Servlet环境的初始化,之后才会进行到IOC容器的初始化,也就是说,在servlet初始化过程中是不能使用IOC依赖注入的
    @Override
    public void afterPropertiesSet() throws Exception {
        start();
    }

    /**
     * 启动服务
     */
    public void start(){
        Thread socketServiceThread = new Thread(() -> {
            while (true) {
                try {
                    //尝试重新建立连接
                    //socket = SocketUtil.createClientSocket("127.0.0.1", 9999);
                    socket = SocketUtil.createClientSocket(host, port);
                    log.info("客户端 socket 在[{}]连接正常", port);
                    ClientRecvThread recvThread = new ClientRecvThread(socket);
                    new Thread(recvThread).start();
                    ClientHeartBeatThread heartBeatThread = new ClientHeartBeatThread(socket, socketHeartIntervalTime, lockObject);
                    new Thread(heartBeatThread).start();
                    //1、连接成功后阻塞,由心跳检测异常唤醒
                    //方式1
                    synchronized (lockObject) {
                        try {
                            lockObject.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }

                    //方式2
                    /*while (!heartBeatThread.isStop()) {
                        //进行空循环, 掉线休眠,防止损耗过大, 随即重连
                        try {
                            Thread.sleep(ClientSocketService.THREAD_SLEEP_MILLS);
                        } catch (InterruptedException e1) {
                            e1.printStackTrace();
                        }
                    }*/
                    //旧的、接收线程、心跳线程摧毁,准备重建连接、接收线程、心跳线程
                } catch (IOException e) {
                    log.error("socket客户端进行连接发生异常");
                    e.printStackTrace();
                    //2、第一次启动时连接异常发生,休眠, 重建连接
                    try {
                        Thread.sleep(ClientSocketService.THREAD_SLEEP_MILLS);
                    } catch (InterruptedException e1) {
                        e1.printStackTrace();
                    }
                }
            }
        });
        socketServiceThread.setName("socket client main thread");
        socketServiceThread.start();
    }

    public Socket getSocket() {
        if (socket != null && !socket.isClosed()) {
            return socket;
        }
        return null;
    }

4、配置文件 yml

#socket
qxts.socket.host=127.0.0.1
qxts.socket.port=3306
#间隔多长时间发送一次心跳检测(单位:秒)
qxts.socket.heart.interval.time=30

5、测试

    /**
     * 测试
     * @param args
     */
    public static void main(String[] args) {
        ClientSocketService clientSocket = new ClientSocketService();
        clientSocket.start();
    }

6、工具类

import com.ws.common.vo.SocketMsgDataVo;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;

public class SocketUtil {

    private static final int  BLANK_SPACE_COUNT = 5;

    public static Socket createClientSocket(String host, int port) throws IOException {
        Socket socket = new Socket(host,port);
        return socket;
    }

    public static void closeSocket(Socket socket) {
        if (socket != null && !socket.isClosed()) {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void writeMsgData(DataOutputStream dataOutputStream, SocketMsgDataVo msgDataVo) throws IOException {
        byte[] data = msgDataVo.getBody().getBytes();
        int len = data.length + SocketUtil.BLANK_SPACE_COUNT;
        dataOutputStream.writeByte(msgDataVo.getType());
        dataOutputStream.writeInt(len);
        dataOutputStream.write(data);
        dataOutputStream.flush();
    }

    public static SocketMsgDataVo readMsgData(DataInputStream dataInputStream) throws IOException {
        byte type = dataInputStream.readByte();
        int len = dataInputStream.readInt();
        byte[] data = new byte[len - SocketUtil.BLANK_SPACE_COUNT];
        dataInputStream.readFully(data);
        String str = new String(data);
        System.out.println("获取的数据类型为:" + type);
        System.out.println("获取的数据长度为:" + len);
        System.out.println("获取的数据内容为:" + str);
        SocketMsgDataVo msgDataVo = new SocketMsgDataVo();
        msgDataVo.setType(type);
        msgDataVo.setBody(str);
        return msgDataVo;
    }
}

7、输入、输出流工具类

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * 输入、输出流操作工具类
 */
public class StreamUtil {

    public static void closeInputStream(InputStream is) {
        if (is != null) {
            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void closeOutputStream(OutputStream os) {
        if (os != null) {
            try {
                os.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

8、实体类

package com.tiktok.test;

/**
 * @ClassName SocketMsgDataVo
 * @Description
 * @Author liulianjia
 * @Date 12:28 2022/7/11
 * @Version 1.0
 **/
public class SocketMsgDataVo {
    private int type;

    private String body;

    public int getType() {
        return type;
    }

    public void setType(byte type) {
        this.type = type;
    }

    public String getBody() {
        return body;
    }

    public void setBody(String body) {
        this.body = body;
    }
}

  • 2
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
以下是一个简单的 Java Socket 长连接实现心跳检测的代码示例: ```java import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; public class SocketClient { private static final int HEARTBEAT_INTERVAL = 5000; // 心跳间隔时间,单位毫秒 private Socket socket; public SocketClient(String ip, int port) throws IOException { socket = new Socket(ip, port); } public void start() { new Thread(new Runnable() { @Override public void run() { try { InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream(); // 发送心跳包 byte[] heartBeat = "ping".getBytes(); while (true) { outputStream.write(heartBeat); Thread.sleep(HEARTBEAT_INTERVAL); } } catch (Exception e) { e.printStackTrace(); } } }).start(); new Thread(new Runnable() { @Override public void run() { try { while (true) { InputStream inputStream = socket.getInputStream(); byte[] buffer = new byte[1024]; int length = inputStream.read(buffer); if (length > 0) { String message = new String(buffer, 0, length); System.out.println("收到消息:" + message); } } } catch (Exception e) { e.printStackTrace(); } } }).start(); } public static void main(String[] args) throws IOException { SocketClient client = new SocketClient("127.0.0.1", 8888); client.start(); } } ``` 在这个示例中,我们使用了两个线程,一个用于发送心跳包,另一个用于接收服务器发送的消息。发送心跳包的线程会在每个固定的时间间隔内发送一个固定的字符串,例如 "ping",而接收消息的线程则会不断地从 Socket 的输入流中读取数据,并将其打印出来。 当然,这只是一个简单的示例,实际情况下可能需要更复杂的逻辑来处理各种异常情况和错误。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

才疏学浅的浅~

谢谢老板,老板大气^_^

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值