需求
假如现在项目业务中需要有个简单的通讯来发送通知消息的需求,需要客户端和服务端进行通讯,但是只需要一个客户端和服务端建立长连接即可。客户端与服务端建立长连接过程中,由于网络问题、服务端重启等问题,可能会导致客户端连接被中断,所以客户端必须具备连接中断后重新恢复的通讯的功能。
方案
因为本人应用中只需要简单的消息发送,所以使用socket在客户端与服务端之间建立简单的长连接,使用socket进行通讯即可满足需求。下面是基于socket的消息通讯进行展开,其中包括了心跳检测、连接中断恢复效果。
思路
socker服务端
服务端开启服务监听后,存在两种情况。
当服务端还没有跟客户端建立连接。客户端连进来,单独为其开启接收、发送线程。其中发送线程是从阻塞消息队列中获取消息,进行发送;队列为空,则阻塞等待发送。服务端发送消息时,把消息放到阻塞队列即可。
当新客户端进行连接时判断是否已经跟任一存储的客户端建立起正常通讯。当建立起通讯的时候,则不允许新的客户端进行建立长连接通讯,返回相应的提示信息告知客户端不被允许。由客户端监听接收消息类型情况,以便结束其开启的接收线程,并释放资源,关闭客户端socket。此时,服务端新连接进来的客户端开启的接收线程,由于监听到socket客户端流已关闭,自然会发生读取信息流异常,针对异常处理释放新客户端连接进来所开启的资源(包括释放socket、结束新开启的接收监听线程等)。
建立后长连接发生异常。新的连进来,需要关闭发送线程、接收线程(这个自动检测到异常会进行资源释放操作处理)。发送线程由于消费队列的时候存在阻塞,不好摧毁线程,所以需要手动向旧队列发送一条消息,唤醒线程,检测到流关闭的时候,自然也会抛出异常,处理异常释放资源即可。
socker客户端
需要包括心跳检测线程、接收线程、发送线程(本人项目中不需要,不做讨论)。
心跳线程:定时发送消息检测是否还与服务端保持长连接。若发送消息异常或服务端接收读取心跳消息超时(即newSocket.setSoTimeout(60000)),则证明连接异常,释放连接资源,摧毁心跳线程;心跳线程释放连接资源的时候,必然引起接收线程接收监听异常。处理接收线程异常,释放相应的资源即可。
实现
socker服务端代码:
1、开启socket服务
/**
* 启动服务
*/
public void start() {
Thread socketServiceThread = new Thread(() -> {
ServerSocket serverSocket = null;
try {
//serverSocket = new ServerSocket(8000);
serverSocket = new ServerSocket(port);
log.info("服务端 socket 在[{}]启动正常", port);
//记录已开启的线程,便于管理
while (true) {
Socket newSocket = serverSocket.accept();
//设定输入流读取阻塞超时时间(60秒收不到客户端消息判定断线;与客户端心跳检测结合一起使用的)
newSocket.setSoTimeout(60000); //注意:读取时间等待超时时间,必须比心跳检测消息发送时间大;否则就不断在中断连接的循环之中
if (clientSocket != null && !clientSocket.isClosed()) {
//如果已有一个连接上客户端且没有关闭,则丢弃新连进来的
OutputStream outputStream = newSocket.getOutputStream();
DataOutputStream dataOutputStream = new DataOutputStream(outputStream);
SocketMsgDataVo msgDataVo = new SocketMsgDataVo();
//开启这个接收线程,纯属关闭资源用
ServerRecvThread otherRecvThread = new ServerRecvThread(newSocket);
new Thread(otherRecvThread).start();
//发送个无用的消息,告知具体情况
msgDataVo.setType(SocketMsgTypeEnum.SERVER_NOT_ALLOW.getType());
msgDataVo.setBody("from server: one is connected and other is not allowed at present");
SocketUtil.writeMsgData(dataOutputStream, msgDataVo);
log.warn("one is connected and new is not allowed at present");
//继续监听
continue;
}
//1、关闭已开启的线程
this.closeOpenedThreads();
//2、重建新的socket服务
clientSocket = newSocket;
ServerRecvThread newRecvThread = new ServerRecvThread(clientSocket);
threadMap.put(clientSocket, newRecvThread);
new Thread(newRecvThread).start();
ServerSendThread newServerSendThread = new ServerSendThread(clientSocket);
sendThread = newServerSendThread;
new Thread(newServerSendThread).start();
}
} catch (IOException e) {
log.error("socket服务端发生异常");
e.printStackTrace();
//释放资源
//关闭已开启的线程
this.closeOpenedThreads();
if (serverSocket != null && !serverSocket.isClosed()) {
try {
serverSocket.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
});
socketServiceThread.setName("socket server main thread");
socketServiceThread.start();
}
/**
*关闭已开启的线程
*/
private void closeOpenedThreads() {
if (clientSocket != null) {
log.info("删除旧的无效连接及其接收、发送线程");
ServerRecvThread oldRecvThread = threadMap.remove(clientSocket);
oldRecvThread.setStop(true);
sendThread.setStop(true);
SocketMsgDataVo msgDataVo = new SocketMsgDataVo();
//发送个无用的消息,唤醒线程(可能处于阻塞),以便结束旧的发送线程
msgDataVo.setType(SocketMsgTypeEnum.HEART_BEAT.getType());
msgDataVo.setBody("from server: null message");
sendThread.addMsgToQueue(msgDataVo);
log.info("旧的无效连接及其接收、发送线程已回收");
}
}
2、接收线程
@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);
if (msgDataVo.getType() == SocketMsgTypeEnum.HEART_BEAT.getType()) {
//客户端心跳监测不用处理
log.info("收到客户端心跳消息");
}
}
} catch (IOException e) {
log.error("服务端接收消息发生异常");
e.printStackTrace();
} finally {
log.info("服务端旧接收线程已摧毁");
StreamUtil.closeInputStream(dataInputStream);
StreamUtil.closeInputStream(inputStream);
SocketUtil.closeSocket(socket);
}
}
3、阻塞发送线程
//阻塞安全队列,设置队列容量,否则为无限大
private final BlockingQueue<SocketMsgDataVo> msgQueue = new LinkedBlockingQueue<>(100);
private Socket socket;
private volatile boolean isStop = false;
public ServerSendThread(Socket socket) {
this.socket = socket;
}
public void addMsgToQueue(SocketMsgDataVo msgDataVo) {
try {
//队列已满,阻塞直到未满放进元素(这里一直阻塞不太行,建议使用offer()来设置入队列超时时间)
msgQueue.put(msgDataVo);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@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 = null;
try {
msgDataVo = msgQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (msgDataVo != null && msgDataVo.getBody() != null) { //正文内容不能为空,否则不发
SocketUtil.writeMsgData(dataOutputStream, msgDataVo);
log.info("服务端消息已发送!");
}
}
} catch (Exception e) {
log.error("服务端消息发送异常");
e.printStackTrace();
} finally {
log.info("服务端旧消息发送线程已摧毁");
//释放资源
msgQueue.clear();
StreamUtil.closeOutputStream(dataOutputStream);
StreamUtil.closeOutputStream(outputStream);
SocketUtil.closeSocket(socket);
}
}
socker客户端代码:
1、开启客户端socket服务
/**
* 启动服务
*/
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, mongoUpdateRealTimeInterfaceDataUrl);
new Thread(recvThread).start();
ClientHeartBeatThread heartBeatThread = new ClientHeartBeatThread(socket);
new Thread(heartBeatThread).start();
//1、连接成功后,心跳异常检测、随后重连
//可以不用轮询,避免减少线程频繁切换,花费更多的cpu资源:先wait()阻塞线程,使用心跳线程finally操作中notify()唤醒重建连接
while (!heartBeatThread.isStop()) {
//进行空循环, 掉线休眠,防止损耗过大, 随即重连
try {
Thread.sleep(ClientSocketService.THREAD_SLEEP_MILLS);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
//recvThread.setStop(true);
//旧的、接收线程、心跳线程摧毁,准备重建连接、接收线程、心跳线程
} 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();
}
2、心跳线程
@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(SocketMsgTypeEnum.HEART_BEAT.getType());
msgDataVo.setBody("from client:Is connect ok ?");
if (msgDataVo != null && msgDataVo.getBody() != null) { //正文内容不能为空,否则不发)
SocketUtil.writeMsgData(dataOutputStream, msgDataVo);
}
try {
Thread.sleep(ClientHeartBeatThread.CHECK_MILLS);
} 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);
//这里可以使用对象锁:notify() 通知唤醒等待线程重建socket连接; 避免socket服务轮询等待重建连接操作
}
}
3、接收线程
@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() == "某某类型") {
} 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 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;
}
不足
连接中断过程中,原消息队列中所有的消息都会丢失掉;客户端重连后重新建立消息队列,以便接收新的消息。即不保证所有消息都能被客户端接收到,重连过程中存在消息丢失、发送失败。
最后附上github项目地址