所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口。
TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/网际协议)是指能够在多个不同网络间实现信息传输的协议簇。TCP/IP协议不仅仅指的是TCP 和IP两个协议,而是指一个由FTP、SMTP、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;
}
}