心跳是为了保证客户端和服务端的通信可用。因为各种原因客户端和服务端不能及时响应和接收信息。比如网络断开,停电 或者是客户端/服务端 高负载。
所以每隔一段时间 客户端发送心跳包到客户端 服务端做出心跳的响应;
1.如果客户端在指定时间没有向服务端发送心跳包。则表示客户端的通信出现了问题。
2.如果客户端发送心跳包到服务端没有收到响应 则表示服务端的通信出现了问题。
netty提供IdleStateHandle 在监听距离上一次写的时间和距离上一次读的时间 如果超时则调用
源码:
public class IdleStateHandler extends ChannelDuplexHandler @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { // This method will be invoked only if this handler was added // before channelActive() event is fired. If a user adds this handler // after the channelActive() event, initialize() will be called by beforeAdd(). initialize(ctx); super.channelActive(ctx); } }
private void initialize(ChannelHandlerContext ctx) { // Avoid the case where destroy() is called before scheduling timeouts. // See: https://github.com/netty/netty/issues/143 switch (state) { case 1: case 2: return; } state = 1; initOutputChanged(ctx); lastReadTime = lastWriteTime = ticksInNanos(); if (readerIdleTimeNanos > 0) { readerIdleTimeout = schedule(ctx, new ReaderIdleTimeoutTask(ctx),//监听read的task readerIdleTimeNanos, TimeUnit.NANOSECONDS); } if (writerIdleTimeNanos > 0) { writerIdleTimeout = schedule(ctx, new WriterIdleTimeoutTask(ctx),//监听写的task writerIdleTimeNanos, TimeUnit.NANOSECONDS); } if (allIdleTimeNanos > 0) { allIdleTimeout = schedule(ctx, new AllIdleTimeoutTask(ctx),//监听读写的task allIdleTimeNanos, TimeUnit.NANOSECONDS); } }
private final class ReaderIdleTimeoutTask extends AbstractIdleTask { ReaderIdleTimeoutTask(ChannelHandlerContext ctx) { super(ctx); } @Override protected void run(ChannelHandlerContext ctx) { long nextDelay = readerIdleTimeNanos; if (!reading) { nextDelay -= ticksInNanos() - lastReadTime; } if (nextDelay <= 0) { // Reader is idle - set a new timeout and notify the callback. readerIdleTimeout = schedule(ctx, this, readerIdleTimeNanos, TimeUnit.NANOSECONDS); boolean first = firstReaderIdleEvent; firstReaderIdleEvent = false; try { IdleStateEvent event = newIdleStateEvent(IdleState.READER_IDLE, first); channelIdle(ctx, event); } catch (Throwable t) { ctx.fireExceptionCaught(t); } } else { // Read occurred before the timeout - set a new timeout with shorter delay. readerIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS); } } } private final class WriterIdleTimeoutTask extends AbstractIdleTask { WriterIdleTimeoutTask(ChannelHandlerContext ctx) { super(ctx); } @Override protected void run(ChannelHandlerContext ctx) { long lastWriteTime = IdleStateHandler.this.lastWriteTime; long nextDelay = writerIdleTimeNanos - (ticksInNanos() - lastWriteTime); if (nextDelay <= 0) { // Writer is idle - set a new timeout and notify the callback. writerIdleTimeout = schedule(ctx, this, writerIdleTimeNanos, TimeUnit.NANOSECONDS); boolean first = firstWriterIdleEvent; firstWriterIdleEvent = false; try { if (hasOutputChanged(ctx, first)) { return; } IdleStateEvent event = newIdleStateEvent(IdleState.WRITER_IDLE, first); channelIdle(ctx, event); } catch (Throwable t) { ctx.fireExceptionCaught(t); } } else { // Write occurred before the timeout - set a new timeout with shorter delay. writerIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS); } } } private final class AllIdleTimeoutTask extends AbstractIdleTask { AllIdleTimeoutTask(ChannelHandlerContext ctx) { super(ctx); } @Override protected void run(ChannelHandlerContext ctx) { long nextDelay = allIdleTimeNanos; if (!reading) { nextDelay -= ticksInNanos() - Math.max(lastReadTime, lastWriteTime); } if (nextDelay <= 0) { // Both reader and writer are idle - set a new timeout and // notify the callback. allIdleTimeout = schedule(ctx, this, allIdleTimeNanos, TimeUnit.NANOSECONDS); boolean first = firstAllIdleEvent; firstAllIdleEvent = false; try { if (hasOutputChanged(ctx, first)) { return; } IdleStateEvent event = newIdleStateEvent(IdleState.ALL_IDLE, first); channelIdle(ctx, event); } catch (Throwable t) { ctx.fireExceptionCaught(t); } } else { // Either read or write occurred before the timeout - set a new // timeout with shorter delay. allIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS); } } }
三个内部类是IdleSateHandle的内部类 可以看到内部是通过另起一个线程进行监听上一次对应事件的触发 如果超时则调用对应的事件
基于三的代码进行修改
首先是MessageHead消息头增加消息类型
public class MessageHead { private int headData=0X76;//协议开始标志 private int length;//包的长度 private String token; private Date createDate; private String type;//消息类型 ping表示心跳包 public int getHeadData() { return headData; } public void setHeadData(int headData) { this.headData = headData; } public int getLength() { return length; } public void setLength(int length) { this.length = length; } public String getToken() { return token; } public void setToken(String token) { this.token = token; } public Date getCreateDate() { return createDate; } public void setCreateDate(Date createDate) { this.createDate = createDate; } public String getType() { return type; } public void setType(String type) { this.type = type; } @Override public String toString() { SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // TODO Auto-generated method stub return "headData:"+headData+",length:"+length+",token:"+token+",createDate:"+ simpleDateFormat.format(createDate); } }
MessageDecode
package com.liqiang.SimpeEcode; import java.text.SimpleDateFormat; import java.util.List; import com.liqiang.nettyTest2.nettyMain; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageDecoder; import io.netty.handler.codec.MessageToByteEncoder; import io.netty.handler.codec.MessageToMessageDecoder; public class MessageDecode extends ByteToMessageDecoder{ private final int BASE_LENGTH=4+4+50+50+50;//协议头 类型 int+length 4个字节+消息类型加令牌和 令牌生成时间50个字节 private int headData=0X76;//协议开始标志 @Override protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out) throws Exception { // 刻度长度必须大于基本长度 if(buffer.readableBytes()>=BASE_LENGTH) { /** * 粘包 发送频繁 可能多次发送黏在一起 需要考虑 不过一个客户端发送太频繁也可以推断是否是攻击 */ //防止soket流攻击。客户端传过来的数据太大不合理 if(buffer.readableBytes()>2048) { //buffer.skipBytes(buffer.readableBytes()); } } int beginIndex;//记录包开始位置 while(true) { // 获取包头开始的index beginIndex = buffer.readerIndex(); //如果读到开始标记位置 结束读取避免拆包和粘包 if(buffer.readInt()==headData) { break; } //初始化读的index为0 buffer.resetReaderIndex(); // 当略过,一个字节之后, //如果当前buffer数据小于基础数据 返回等待下一次读取 if (buffer.readableBytes() < BASE_LENGTH) { return; } } // 消息的长度 int length = buffer.readInt(); // 判断请求数据包数据是否到齐 -150是消息头的长度。 if ((buffer.readableBytes()-150) < length) { //没有到齐 返回读的指针 等待下一次数据到期再读 buffer.readerIndex(beginIndex); return; } //读取消息类型 byte[] typeByte=new byte[50]; buffer.readBytes(typeByte); //读取令牌 byte[] tokenByte=new byte[50]; buffer.readBytes(tokenByte); //读取令牌生成时间 byte[]createDateByte=new byte[50]; buffer.readBytes(createDateByte); //读取content byte[] data = new byte[length]; buffer.readBytes(data); MessageHead head=new MessageHead(); head.setHeadData(headData); head.setToken(new String(tokenByte).trim()); SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); head.setCreateDate( simpleDateFormat.parse(new String(createDateByte).trim())); head.setLength(length); head.setType(new String(typeByte).trim()); Message message=new Message(head, data); //认证不通过 if(!message.authorization(message.buidToken())) { ctx.close(); return; } out.add(message); buffer.discardReadBytes();//回收已读字节 } }
MessageEncoder
package com.liqiang.SimpeEcode; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Calendar; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToByteEncoder; public class MessageEncoder extends MessageToByteEncoder<Message> { @Override protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception { // TODO Auto-generated method stub // 写入开头的标志 out.writeInt(msg.getHead().getHeadData()); // 写入包的的长度 out.writeInt(msg.getContent().length); byte[] typeByte = new byte[50]; /** * type定长50个字节 * 第一个参数 原数组 * 第二个参数 原数组位置 * 第三个参数 目标数组 * 第四个参数 目标数组位置 * 第五个参数 copy多少个长度 */ byte[] indexByte=msg.getHead().getType().getBytes(); try { System.arraycopy(indexByte, 0, typeByte, 0,indexByte.length>typeByte.length?typeByte.length:indexByte.length); } catch (Exception e) { // TODO: handle exception e.printStackTrace(); } //写入消息类型 out.writeBytes(typeByte); byte[] tokenByte = new byte[50]; /** * token定长50个字节 * 第一个参数 原数组 * 第二个参数 原数组位置 * 第三个参数 目标数组 * 第四个参数 目标数组位置 * 第五个参数 copy多少个长度 */ indexByte=msg.getHead().getToken().getBytes(); try { System.arraycopy(indexByte, 0, tokenByte, 0,indexByte.length>tokenByte.length?tokenByte.length:indexByte.length); } catch (Exception e) { // TODO: handle exception e.printStackTrace(); } //写入令牌 out.writeBytes(tokenByte); byte[] createTimeByte = new byte[50]; SimpleDateFormat format0 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String time = format0.format(msg.getHead().getCreateDate()); indexByte=time.getBytes(); System.arraycopy(indexByte, 0, createTimeByte, 0,indexByte.length>createTimeByte.length?createTimeByte.length:indexByte.length); //写入令牌生成时间 out.writeBytes(createTimeByte); // 写入消息主体 out.writeBytes(msg.getContent()); } }
红色部分为改动部分
ClientChannelInitializer
package com.liqiang.nettyTest2; import java.util.concurrent.TimeUnit; import com.liqiang.SimpeEcode.MessageDecode; import com.liqiang.SimpeEcode.MessageEncoder; import io.netty.channel.ChannelInitializer; import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.string.StringDecoder; import io.netty.handler.codec.string.StringEncoder; import io.netty.handler.timeout.IdleStateHandler; public class ClientChannelInitializer extends ChannelInitializer<SocketChannel> { private Client client; public ClientChannelInitializer(Client client) { // TODO Auto-generated constructor stub this.client=client; } @Override protected void initChannel(SocketChannel socketChannel) throws Exception { // TODO Auto-generated method stub socketChannel.pipeline() //表示5秒向服务器发送一次心跳包 10秒没接收到服务器端信息表示服务器端通信异常 则会触发clientHandle userEventTriggered事件 .addLast("ping",new IdleStateHandler(10, 5, 0, TimeUnit.SECONDS)) .addLast("decoder",new MessageEncoder()) .addLast("encoder",new MessageDecode()) .addLast(new ClientHandle(client));//注册处理器 } }
ClientHandle修改
package com.liqiang.nettyTest2; import java.util.Date; import com.liqiang.SimpeEcode.Message; import com.liqiang.SimpeEcode.MessageHead; import io.netty.channel.ChannelHandlerAdapter; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.http.cors.CorsHandler; import io.netty.handler.timeout.IdleState; import io.netty.handler.timeout.IdleStateEvent; public class ClientHandle extends ChannelInboundHandlerAdapter { Client client; public ClientHandle(Client client) { // TODO Auto-generated constructor stub this.client=client; } /** * 读写超时事事件 * IdleStateHandle配置的 如果5秒没有触发writer事件 则会触发 userEventTrigerd方法 我们则写一次心跳 * 如果10秒没有触发read事件则表示服务器通信异常 因为我们每次发送一次心跳包 服务器都会做出对应的心跳反应 * @throws Exception */ @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if(evt instanceof IdleStateEvent) { IdleStateEvent idleStateEvent=((IdleStateEvent) evt); /** * 如果没有收到服务端的写 则表示服务器超时 判断是否断开连接 */ if(idleStateEvent.state()==IdleState.READER_IDLE) { System.out.println("服务器无响应"); if(!ctx.channel().isOpen()) { System.out.println("正在重连"); client.connection(); System.out.println("重连成功"); } }else if(idleStateEvent.state()==IdleState.WRITER_IDLE) { //如果没有触发写事件则向服务器发送一次心跳包 System.out.println("正在向服务端发送心跳包"); MessageHead head=new MessageHead(); byte[]content="".getBytes(); head.setCreateDate(new Date()); head.setType("ping"); head.setLength(content.length); Message pingMessage=new Message(head,content); head.setToken(pingMessage.buidToken()); ctx.writeAndFlush(pingMessage); } }else { super.userEventTriggered(ctx, evt); } } //建立连接时回调 @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { // TODO Auto-generated method stub //System.out.println("与服务器建立连接成功"); client.setServerChannel(ctx); client.setConnection(true); //ctx.fireChannelActive();//如果注册多个handle 下一个handel的事件需要触发需要调用这个方法 } //读取服务器发送信息时回调 @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { Message message=(Message) msg; if(message.getHead().getType().equals("ping")) { //表示是心跳包 不做任何业务处理 }else { // TODO Auto-generated method stub System.out.println(msg.toString()); } } //发生异常时回调 @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { // TODO Auto-generated method stub System.out.println("发生异常 与服务器断开连接"); ctx.close();//关闭连接 } }
ServerChannelInitializer
package com.liqiang.nettyTest2; import java.util.concurrent.TimeUnit; import com.liqiang.SimpeEcode.MessageDecode; import com.liqiang.SimpeEcode.MessageEncoder; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelInitializer; import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.LineBasedFrameDecoder; import io.netty.handler.codec.AsciiHeadersEncoder.NewlineType; import io.netty.handler.codec.DelimiterBasedFrameDecoder; import io.netty.handler.codec.string.StringDecoder; import io.netty.handler.codec.string.StringEncoder; import io.netty.handler.timeout.IdleStateHandler; public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> { private Server server; public ServerChannelInitializer(Server server) { this.server=server; } @Override protected void initChannel(SocketChannel channel) throws Exception { // TODO Auto-generated method stub channel.pipeline() //7秒没收到客户端信息 则表示客户端因为网络等原因异常关闭 .addLast("ping",new IdleStateHandler(7, 0, 0,TimeUnit.SECONDS)) .addLast("decoder",new MessageDecode()) .addLast("encoder",new MessageEncoder()) .addLast(new ServerHandle(server)); } }
ServerHandle
package com.liqiang.nettyTest2; import java.util.Date; import com.liqiang.SimpeEcode.Message; import com.liqiang.SimpeEcode.MessageHead; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.timeout.IdleState; import io.netty.handler.timeout.IdleStateEvent; import io.netty.handler.timeout.IdleStateHandler; public class ServerHandle extends ChannelInboundHandlerAdapter { private Server server; public ServerHandle(Server server) { // TODO Auto-generated constructor stub this.server = server; } /** * 读写超时事事件 */ @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if(evt instanceof IdleStateEvent) { IdleStateEvent event=(IdleStateEvent)evt; //如果读超时 if(event.state()==IdleState.READER_IDLE) { System.out.println("有客户端超时了"); ctx.channel().close();//关闭连接 } }else { super.userEventTriggered(ctx, evt); } } // 建立连接时回调 @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { // TODO Auto-generated method stub System.out.println("有客户端建立连接了"); server.addClient(ctx); // ctx.fireChannelActive();//pipeline可以注册多个handle 这里可以理解为是否通知下一个Handle继续处理 } // 接收到客户端发送消息时回调 @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { Message message=(Message)msg; if(message.getHead().getType().equals("ping")) { //表示心跳包 服务端响应心跳包 而不做相关业务处理 MessageHead head=new MessageHead(); byte[] content="".getBytes(); head.setCreateDate(new Date()); head.setType("ping"); head.setLength(content.length); Message pingMessage=new Message(head,content); head.setToken(pingMessage.buidToken()); ctx.writeAndFlush(pingMessage); }else { System.out.println("server接收到客户端发送信息:" + msg.toString()); } // TODO Auto-generated method stub // ctx.fireChannelRead(msg);pipeline可以注册多个handle 这里可以理解为是否通知下一个Handle继续处理 } // 通信过程中发生异常回调 @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { // TODO Auto-generated method stub // super.exceptionCaught(ctx, cause); ctx.close();// 发生异常关闭通信通道 System.out.println("发生异常与客户端失去连接"); cause.printStackTrace(); // ctx.fireExceptionCaught(cause);pipeline可以注册多个handle 这里可以理解为是否通知下一个Handle继续处理 } }
client
package com.liqiang.nettyTest2; import com.liqiang.SimpeEcode.Message; import io.netty.bootstrap.Bootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.string.StringDecoder; import io.netty.handler.codec.string.StringEncoder; import io.netty.util.concurrent.EventExecutorGroup; public class Client implements Runnable{ private String ip;// ip private int port;// 端口 private boolean isConnection = false; private ChannelHandlerContext serverChannel; public Client(String ip, int port) { this.ip = ip; this.port = port; } // 与服务器建立连接 public void connection() { new Thread(this).start(); } @Override public void run() { // TODO Auto-generated method stub EventLoopGroup group = new NioEventLoopGroup();// 服务器监听服务器发送信息 Bootstrap bootstrap = new Bootstrap(); bootstrap.group(group).channel(NioSocketChannel.class).option(ChannelOption.TCP_NODELAY, true) .handler(new ClientChannelInitializer(this));// 基于NIO编程模型通信 try { ChannelFuture channelFuture = bootstrap.connect(ip, port).sync(); channelFuture.channel().closeFuture().sync(); } catch (InterruptedException e) { // TODO Auto-generated catch block System.out.println("连接服务器失败"); }finally { //尝试重连 System.out.println("正在重连"); run(); } } public void close() { serverChannel.close(); } public boolean isConnection() { return isConnection; } public void setConnection(boolean isConnection) { this.isConnection = isConnection; } public void sendMsg(Message msg) { while(isConnection) { serverChannel.writeAndFlush(msg); } } public ChannelHandlerContext getServerChannel() { return serverChannel; } public void setServerChannel(ChannelHandlerContext serverChannel) { this.serverChannel = serverChannel; } }
Server
package com.liqiang.nettyTest2; import java.net.InetSocketAddress; import java.util.List; import java.util.Vector; import com.liqiang.SimpeEcode.Message; import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.string.StringDecoder; import io.netty.handler.codec.string.StringEncoder; public class Server implements Runnable { private int port;// 监听端口 private Vector<ChannelHandlerContext> clients;// 保存在线客户端信息 public Server(int port) { clients = new Vector<ChannelHandlerContext>(); this.port = port; } // 广播 public void sendAll(Message msg) { clients.forEach(c -> { c.writeAndFlush(msg); }); } public void addClient(ChannelHandlerContext client) { clients.add(client); } @Override public void run() { /** * NioEventLoopGroup 内部维护一个线程池 如果构造函数没有指定线程池数量 则默认为系统core*2 */ EventLoopGroup acceptor = new NioEventLoopGroup();// acceptor负责监客户端连接请求 EventLoopGroup worker = new NioEventLoopGroup();// worker负责io读写(监听注册channel的 read/writer事件) ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(acceptor, worker).channel(NioServerSocketChannel.class) .localAddress(new InetSocketAddress(port)).childHandler(new ServerChannelInitializer(this)) .option(ChannelOption.SO_BACKLOG, 128).childOption(ChannelOption.SO_KEEPALIVE, true); try { ChannelFuture channelFuture = bootstrap.bind(port).sync(); System.out.println("服务器已启动"); // 将阻塞 直到服务器端关闭或者手动调用 channelFuture.channel().closeFuture().sync(); // 释放资源 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); }finally { acceptor.shutdownGracefully(); worker.shutdownGracefully(); } } public void startServer() { new Thread(this).start(); } }
测试
package com.liqiang.nettyTest2; import java.util.Date; import com.liqiang.SimpeEcode.Message; import com.liqiang.SimpeEcode.MessageHead; public class nettyClientMain { public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { // TODO Auto-generated method stub Client client1 = new Client("127.0.0.1", 8081); client1.connection(); String content = "哈哈哈哈!"; byte[] bts = content.getBytes(); MessageHead head = new MessageHead(); // 令牌生成时间 head.setCreateDate(new Date()); head.setType("message"); head.setLength(bts.length); Message message = new Message(head, bts); message.getHead().setToken(message.buidToken()); client1.sendMsg(message); } }).start(); } }
package com.liqiang.nettyTest2; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import javax.management.StringValueExp; import javax.swing.text.StringContent; import com.liqiang.SimpeEcode.Message; import com.liqiang.SimpeEcode.MessageHead; public class nettyMain { public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { // TODO Auto-generated method stub Server server = new Server(8081); server.startServer(); } }).start(); } }
1.先开启服务端
2.再开启客户端
3.关闭服务端
然后我们再重新启动服务端 打印
正在重连 正在重连 正在重连 正在重连 正在重连 正在重连 正在重连 正在重连 正在向服务端发送心跳包 正在向服务端发送心跳包 正在向服务端发送心跳包 正在向服务端发送心跳包
原文地址:https://www.cnblogs.com/LQBlog/p/9163424.html
时间: 2024-11-10 14:28:55