Netty学习篇④-心跳机制及断线重连
心跳检测
前言
客户端和服务端的连接属于socket连接,也属于长连接,往往会存在客户端在连接了服务端之后就没有任何操作了,但还是占用了一个连接;当越来越多类似的客户端出现就会浪费很多连接,netty中可以通过心跳检测来找出一定程度(自定义规则判断哪些连接是无效链接)的无效链接并断开连接,保存真正活跃的连接。
什么叫心跳检测
我理解的心跳检测应该是客户端/服务端定时发送一个数据包给服务端/客户端,检测对方是否有响应;
如果是存活的连接,在一定的时间内应该会收到响应回来的数据包;
如果在一定时间内还是收不到接收方的响应的话,就可以当做是挂机,可以断开此连接;
如果检测到了掉线之后还可以进行重连;
心跳检测的实现
- TCP自带心跳检测,协议层采用Keeplive机制默认2小时频率触发一次检测,但是它存在缺陷:检测不出网线拔出、防火墙、使用起来不灵活、依赖操作系统等
- Netty可以通过IdleStateHandler来实现心跳检测,使用起来也非常方便清晰
IdleStateHandler原理
idleStateHandler在通道注册之后会开启一个定时任务,定时去检测通道中后续是否还有进行数据传输,如果在规定的时间内没有进行数据传输则会触发对应的超时事件,使用者可以根据对应的事件自定义规则来判别当前连接是否是活跃,是否需要关闭连接等来进行操作。
一般idleStateHandler触发的事件IdleStateEvent会在心跳handler中的userEventTriggered方法中捕获到对应的超时事件。
IdleStateHandler的继承关系:通过ChannelDuplexHandler类继承ChannelInboundHandler和实现ChannelOutboundHandler来实现对入站和出站的重写和监控
源码分析
IdleStateHandler初始化:为0代表不监控
/**
* @observeOutput 观察输出
* @readerIdleTime 读超时时间 自定义时间内检测Channel通道有没有读取到数据,为0代表不监控
* @writerIdleTime 写超时时间 自定义时间内检测Channel通道有没有write数据,为0代表不监控
* @allIdleTime 总超时时间 自定义时间内检测Channel通道有没有读/写数据,为0代表不监控
* @unit 时间单位
*/
public IdleStateHandler(boolean observeOutput,
long readerIdleTime, long writerIdleTime, long allIdleTime,
TimeUnit unit) {
if (unit == null) {
throw new NullPointerException("unit");
} this.observeOutput = observeOutput; // 初始化读取空闲时间,最小值为0
if (readerIdleTime <= 0) {
readerIdleTimeNanos = 0;
} else {
// 定义读取超时时间为自定义设置时间
readerIdleTimeNanos = Math.max(unit.toNanos(readerIdleTime), MIN_TIMEOUT_NANOS);
}
if (writerIdleTime <= 0) {
writerIdleTimeNanos = 0;
} else {
// 设置写超时时间
writerIdleTimeNanos = Math.max(unit.toNanos(writerIdleTime), MIN_TIMEOUT_NANOS);
}
if (allIdleTime <= 0) {
allIdleTimeNanos = 0;
} else {
// 设置总超时时间
allIdleTimeNanos = Math.max(unit.toNanos(allIdleTime), MIN_TIMEOUT_NANOS);
}
}
IdleStateHandler和channel通道的关联:通过类图可以得知,idleStateHandler可以重写入站和出站的方法,不过只是通过channelRead和write方法来记录阅读的时间等不做其他操作
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 初始化检测器,开启定时任务
initialize(ctx);
super.channelActive(ctx);
} @Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
// 在通道非活跃状态的时候销毁定时任务
destroy();
super.channelInactive(ctx);
} @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 如果设置的读超时时间大于0则设置是否读操作为true
if (readerIdleTimeNanos > 0 || allIdleTimeNanos > 0) {
reading = true;
firstReaderIdleEvent = firstAllIdleEvent = true;
}
// 记录时间和标识标志之后就直接fire当前read到下一个ChannelHandler处理类中
ctx.fireChannelRead(msg);
} @Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// 当读取完毕时,设置是否正在读取为false,设置最后读取时间为系统当前时间
if ((readerIdleTimeNanos > 0 || allIdleTimeNanos > 0) && reading) {
lastReadTime = ticksInNanos();
reading = false;
}
// 同样直接fire掉,跳入到下一个handler中
ctx.fireChannelReadComplete();
} // idleStateHandler重写的write方法
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
// Allow writing with void promise if handler is only configured for read timeout events.
if (writerIdleTimeNanos > 0 || allIdleTimeNanos > 0) {
ctx.write(msg, promise.unvoid()).addListener(writeListener);
} else {
ctx.write(msg, promise);
}
} // ChannelOutboundHandler提供的write接口方法
/**
* Called once a write operation is made. The write operation will write the messages through the
* {@link ChannelPipeline}. Those are then ready to be flushed to the actual {@link Channel} once
* {@link Channel#flush()} is called
* 执行一次写操作;写操作通过ChannelPipeline来传输信息;最后通过channel的flush()方法来刷新
*
* @param ctx the {@link ChannelHandlerContext} for which the write operation is made 实际写操作者
* @param msg the message to write 写的消息
* @param promise the {@link ChannelPromise} to notify once the operation completes 在操作完成时立即通知(类似future异步通知)
* @throws Exception thrown if an error occurs
*/
void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception;
定时器初始化/销毁:将使用者自定义的超时时间设置为延迟定时任务
初始化: private void initialize(ChannelHandlerContext ctx) {
// Avoid the case where destroy() is called before scheduling timeouts.
// See: https://github.com/netty/netty/issues/143
// state; // 0 - none, 1 - 初始化, 2 - 销毁
switch (state) {
case 1:
case 2:
return;
} state = 1;
initOutputChanged(ctx);
// 最后读取时间
lastReadTime = lastWriteTime = ticksInNanos();
// 如果设置的空闲时间大于0则开启定时任务进行监控
if (readerIdleTimeNanos > 0) {
readerIdleTimeout = schedule(ctx, new ReaderIdleTimeoutTask(ctx),
readerIdleTimeNanos, TimeUnit.NANOSECONDS);
}
// 写超时时间
if (writerIdleTimeNanos > 0) {
writerIdleTimeout = schedule(ctx, new WriterIdleTimeoutTask(ctx),
writerIdleTimeNanos, TimeUnit.NANOSECONDS);
}
// 总超时时间
if (allIdleTimeNanos > 0) {
allIdleTimeout = schedule(ctx, new AllIdleTimeoutTask(ctx),
allIdleTimeNanos, TimeUnit.NANOSECONDS);
}
}销毁: /**
* @readerIdleTimeout ==> ScheduledFuture类
* @writerIdleTimeout ==> ScheduledFuture类
*/
private void destroy() {
// 设置销毁状态
state = 2;
// 销毁线程
// ScheduledFuture
if (readerIdleTimeout != null) {
readerIdleTimeout.cancel(false);
readerIdleTimeout = null;
}
if (writerIdleTimeout != null) {
writerIdleTimeout.cancel(false);
writerIdleTimeout = null;
}
if (allIdleTimeout != null) {
allIdleTimeout.cancel(false);
allIdleTimeout = null;
}
}
开启定时任务
ScheduledFuture<?> schedule(ChannelHandlerContext ctx, Runnable task, long delay, TimeUnit unit) {
return ctx.executor().schedule(task, delay, unit);
}
读/写定时任务:定时任务启动的时候,通过设置的超时时间和上一次触发channelRead的时间进行相减比较来判断是否超时了
// 仅分析读取超时时间定时任务,写超时差不多就是触发的时间不一样,比对的变量换成了设置的写超时时间
private final class ReaderIdleTimeoutTask extends AbstractIdleTask { ReaderIdleTimeoutTask(ChannelHandlerContext ctx) {
super(ctx);
} @Override
protected void run(ChannelHandlerContext ctx) {
// readerIdleTimeNanos:初始化IdleStateHandler设置的读取超时时间
long nextDelay = readerIdleTimeNanos;
// 如果没有任何读取操作
if (!reading) {
// 判断是否有超时
// nextDelay = nextDelay-(ticksInNanos() - lastReadTime)
// 即设置的超时时间减去距离上一次读取的时间
nextDelay -= ticksInNanos() - lastReadTime;
}
// 如果小于等于0 则触发读取超时事件,设置新的延迟时间
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);
}
}
}
项目实战(主要代码,项目以服务端作为心跳监控,也可以在客户端进行心跳监控)
项目结构
├─src
│ ├─main
│ │ ├─java
│ │ │ └─com
│ │ │ └─hetangyuese
│ │ │ └─netty
│ │ │ ├─client
│ │ │ │ MyChannelFutureListener.java
│ │ │ │ MyClient05.java
│ │ │ │ MyClientChannelHandler.java
│ │ │ │ MyClientChannelInitializer.java
│ │ │ │
│ │ │ └─server
│ │ │ │ MyServer05.java
│ │ │ │ MyServerChannelInitializer.java
│ │ │ │ MyServerHandler.java
│ │ │ │
│ │ │ └─decoder
在ChannelPipeline中注册IdleStateHandler
public class MyServerChannelInitializer extends ChannelInitializer<SocketChannel> { @Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().
addLast(new StringEncoder(Charset.forName("GBK")))
.addLast(new StringDecoder(Charset.forName("GBK")))
.addLast(new LoggingHandler(LogLevel.INFO))
// 设置读取超时时间为5秒,写超时和总超时为0即不做监控
.addLast(new IdleStateHandler(5, 0, 0))
.addLast(new MyServerHandler());
}
}
服务端处理handler(其中userEventTriggered为接收心跳任务触发的事件,这次做了计数三次触发读空闲超时则断开连接)
public class MyServerHandler extends ChannelInboundHandlerAdapter { private AtomicInteger count = new AtomicInteger(1); /**
* 心跳检测机制会进入
* @param ctx
* @param evt
* @throws Exception
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
System.out.println("心跳检测触发了事件, object: , time: " + evt + new Date().toLocaleString());
super.userEventTriggered(ctx, evt);
if (evt instanceof IdleStateEvent) {
IdleStateEvent e = (IdleStateEvent) evt;
// 客户端连接应该是请求 write
if (e.state() == IdleState.READER_IDLE) {
System.out.println("服务端监测到了读取超时");
count.incrementAndGet();
if (count.get() > 3) {
System.out.println("客户端还在?? 已经3次检测没有访问了,我要断开了哦!!!");
ctx.channel().close();
}
} else if (e.state() == IdleState.WRITER_IDLE) {
// 如果一直有交互则会发送writer_idle
System.out.println("服务端收到了写入超时");
} else {
System.out.println("服务端收到了All_idle");
}
} else {
super.userEventTriggered(ctx,evt);
}
} @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("myServerHandler is active, time: " + new Date().toLocaleString());
ctx.writeAndFlush("成功连接服务端, 当前时间:" + new Date().toLocaleString());
} @Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("服务端与客户端断开了连接, time: " + new Date().toLocaleString());
} @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("myServerHandler 收到了客户端的信息 msg:" + msg + ", time: " + new Date().toLocaleString());
ctx.writeAndFlush("您好,客户端,我是服务端");
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
运行结果
myServer is start time: 2019-11-8 14:47:16
myServerHandler is active, time: 2019-11-8 14:47:18
十一月 08, 2019 2:47:18 下午 io.netty.handler.logging.LoggingHandler channelRegistered
信息: [id: 0x8d924d06, L:/127.0.0.1:9001 - R:/127.0.0.1:62195] REGISTERED
十一月 08, 2019 2:47:18 下午 io.netty.handler.logging.LoggingHandler channelActive
信息: [id: 0x8d924d06, L:/127.0.0.1:9001 - R:/127.0.0.1:62195] ACTIVE
十一月 08, 2019 2:47:18 下午 io.netty.handler.logging.LoggingHandler write
信息: [id: 0x8d924d06, L:/127.0.0.1:9001 - R:/127.0.0.1:62195] WRITE: 成功连接服务端, 当前时间:2019-11-8 14:47:18
十一月 08, 2019 2:47:18 下午 io.netty.handler.logging.LoggingHandler flush
信息: [id: 0x8d924d06, L:/127.0.0.1:9001 - R:/127.0.0.1:62195] FLUSH
心跳检测触发了事件, object: , time: io.netty.handler.timeout.IdleStateEvent@a3a0212019-11-8 14:47:23
服务端监测到了读取超时
心跳检测触发了事件, object: , time: io.netty.handler.timeout.IdleStateEvent@18692702019-11-8 14:47:28
服务端监测到了读取超时
心跳检测触发了事件, object: , time: io.netty.handler.timeout.IdleStateEvent@18692702019-11-8 14:47:33
服务端监测到了读取超时
客户端还在?? 已经3次检测没有访问了,我要断开了哦!!!
十一月 08, 2019 2:47:33 下午 io.netty.handler.logging.LoggingHandler close
信息: [id: 0x8d924d06, L:/127.0.0.1:9001 - R:/127.0.0.1:62195] CLOSE
十一月 08, 2019 2:47:33 下午 io.netty.handler.logging.LoggingHandler channelInactive
信息: [id: 0x8d924d06, L:/127.0.0.1:9001 ! R:/127.0.0.1:62195] INACTIVE
服务端与客户端断开了连接, time: 2019-11-8 14:47:33
十一月 08, 2019 2:47:33 下午 io.netty.handler.logging.LoggingHandler channelUnregistered
信息: [id: 0x8d924d06, L:/127.0.0.1:9001 ! R:/127.0.0.1:62195] UNREGISTERED
以上就是心跳监控的所有流程了,合理的利用Netty的心跳机制可以有效的剔除一些无用的连接释放些资源
Netty断线重连: 在长连接中有时候出现断开的时候可以重新连接出现断线重连的情况:
- 首次连接但是连接不上,通过ChannelFutureListener增加监控进行重连
- 由于网络原因等、自动断开等 通过在channelInactive中进行重连即可
ChannelFutureListener进行重连
public class MyChannelFutureListener implements ChannelFutureListener {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isSuccess()) {
System.out.println("当前已连接");
return;
}
System.out.println("启动连接客户端失败,开始重连");
final EventLoop loop = future.channel().eventLoop();
loop.schedule(new Runnable() {
@Override
public void run() {
try {
MyClient05.reConnection();
System.out.println("客户端重连成功");
} catch (Exception e){
e.printStackTrace();
}
}
}, 1L, TimeUnit.SECONDS);
}
}
**client启动 **不知道为啥想要测试listener的效果时,connect的sync()不能带,是由于同步阻塞的原因?
public void start() {
EventLoopGroup group = new NioEventLoopGroup();
try {
bootstrap = getBootstrap();
bootstrap.group(group)
.option(ChannelOption.AUTO_READ, true)
.option(ChannelOption.TCP_NODELAY, true)
.channel(NioSocketChannel.class)
.handler(new MyClientChannelInitializer());
// ChannelFuture future = bootstrap.connect(new InetSocketAddress(ip, port)).sync();
ChannelFuture future = bootstrap.connect(new InetSocketAddress(ip, port));
// 增加监听
future.addListener(new MyChannelFutureListener());
future.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
直接启动客户端,可以看到listener输出:启动连接客户端失败,开始重连
channelInactive进行重连(我是直接new了一个线程去重连)
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("客户端断开了连接, time: " + new Date().toLocaleString());
new Thread(new Runnable() {
@Override
public void run() {
try {
new MyClient05("127.0.0.1", 9001).start();
System.out.println("客户端重新连接了服务端 time:" + new Date().toLocaleString());
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
测试方法:
1.可以直接通过上面的心跳机制断开连接后,客户端的channelInactive检测到断开会自动执行重连
测试结果:
服务端:
------
心跳检测触发了事件, object: , time: io.netty.handler.timeout.IdleStateEvent@ab81552019-11-8 16:43:12
服务端监测到了读取超时
心跳检测触发了事件, object: , time: io.netty.handler.timeout.IdleStateEvent@15192622019-11-8 16:43:17
服务端监测到了读取超时
心跳检测触发了事件, object: , time: io.netty.handler.timeout.IdleStateEvent@15192622019-11-8 16:43:22
服务端监测到了读取超时
客户端还在?? 已经3次检测没有访问了,我要断开了哦!!!
十一月 08, 2019 4:43:22 下午 io.netty.handler.logging.LoggingHandler close
信息: [id: 0x6bfc0d90, L:/192.168.0.118:9001 - R:/192.168.0.118:51031] CLOSE
十一月 08, 2019 4:43:22 下午 io.netty.handler.logging.LoggingHandler channelInactive
信息: [id: 0x6bfc0d90, L:/192.168.0.118:9001 ! R:/192.168.0.118:51031] INACTIVE
十一月 08, 2019 4:43:22 下午 io.netty.handler.logging.LoggingHandler channelUnregistered
信息: [id: 0x6bfc0d90, L:/192.168.0.118:9001 ! R:/192.168.0.118:51031] UNREGISTERED
服务端与客户端断开了连接, time: 2019-11-8 16:43:22
十一月 08, 2019 4:43:22 下午 io.netty.handler.logging.LoggingHandler channelRegistered
信息: [id: 0xbcb2ec62, L:/127.0.0.1:9001 - R:/127.0.0.1:51061] REGISTERED
十一月 08, 2019 4:43:22 下午 io.netty.handler.logging.LoggingHandler channelActive
信息: [id: 0xbcb2ec62, L:/127.0.0.1:9001 - R:/127.0.0.1:51061] ACTIVE
myServerHandler is active, time: 2019-11-8 16:43:22
十一月 08, 2019 4:43:22 下午 io.netty.handler.logging.LoggingHandler write
信息: [id: 0xbcb2ec62, L:/127.0.0.1:9001 - R:/127.0.0.1:51061] WRITE: 成功连接服务端, 当前时间:2019-11-8 16:43:22
十一月 08, 2019 4:43:22 下午 io.netty.handler.logging.LoggingHandler flush
信息: [id: 0xbcb2ec62, L:/127.0.0.1:9001 - R:/127.0.0.1:51061] FLUSH
十一月 08, 2019 4:43:26 下午 io.netty.handler.logging.LoggingHandler channelReadComplete
-------------------------------------------------------------------------------------
客户端:
-------
当前已连接
客户端与服务端建立了连接 time: 2019-11-8 16:43:07
客户端接收到了服务的响应的数据 msg: 成功连接服务端, 当前时间:2019-11-8 16:43:07, time: 2019-11-8 16:43:07
客户端断开了连接, time: 2019-11-8 16:43:22
当前已连接
客户端与服务端建立了连接 time: 2019-11-8 16:43:22
客户端接收到了服务的响应的数据 msg: 成功连接服务端, 当前时间:2019-11-8 16:43:22, time: 2019-11-8 16:43:22
Netty学习篇④-心跳机制及断线重连的更多相关文章
- Netty 如何实现心跳机制与断线重连?
作者:sprinkle_liz www.jianshu.com/p/1a28e48edd92 心跳机制 何为心跳 所谓心跳, 即在 TCP 长连接中, 客户端和服务器之间定期发送的一种特殊的数据包, ...
- Netty学习摘记 —— 心跳机制 / 基于分隔符和长度的协议
本文参考 本篇文章是对<Netty In Action>一书第十一章"预置的ChannelHandler和编解码器"的学习摘记,主要内容为通过 SSL/TLS 保护 N ...
- netty心跳机制和断线重连(四)
心跳是为了保证客户端和服务端的通信可用.因为各种原因客户端和服务端不能及时响应和接收信息.比如网络断开,停电 或者是客户端/服务端 高负载. 所以每隔一段时间 客户端发送心跳包到客户端 服务端做出心 ...
- Netty — 心跳检测和断线重连
一.前言 由于在通信层的网络连接的不可靠性,比如:网络闪断,网络抖动等,经常会出现连接断开.这样对于使用长连接的应用而言,当突然高流量冲击势必会造成进行网络连接,从而产生网络堵塞,应用响应速度下降,延 ...
- Netty 中的心跳机制
在TCP长连接或者WebSocket长连接中一般我们都会使用心跳机制–即发送特殊的数据包来通告对方自己的业务还没有办完,不要关闭链接. 网络的传输是不可靠的,当我们发起一个链接请求的过程之中会发生什么 ...
- webSocket使用心跳包实现断线重连
首先new一个webscoket的连接 let noticeSocketLink = new WebSocket(‘webSocket的地址’) 这里是连接成功之后的操作 linkNoticeWebs ...
- Netty学习篇③--整合springboot
经过前面的netty学习,大概了解了netty各个组件的概念和作用,开始自己瞎鼓捣netty和我们常用的项目的整合(很简单的整合) 项目准备 工具:IDEA2017 jar包导入:maven 项目框架 ...
- Netty学习篇⑤--编、解码
前言 学习Netty也有一段时间了,Netty作为一个高性能的异步框架,很多RPC框架也运用到了Netty中的知识,在rpc框架中丰富的数据协议及编解码可以让使用者更加青睐: Netty支持丰富的编解 ...
- Netty学习篇⑥--ByteBuf源码分析
什么是ByteBuf? ByteBuf在Netty中充当着非常重要的角色:它是在数据传输中负责装载字节数据的一个容器;其内部结构和数组类似,初始化默认长度为256,默认最大长度为Integer.MAX ...
随机推荐
- Zeppelin 学习笔记之 Zeppelin安装和elasticsearch整合
Zeppelin安装: Apache Zeppelin提供了web版的类似ipython的notebook,用于做数据分析和可视化.背后可以接入不同的数据处理引擎,包括spark, hive, taj ...
- MongoDB 学习笔记之 group聚合
group聚合: key: 分组字段 cond:过滤条件 reduce: curr是当前行 result是每组的结果集 initial : 组变量初始值 finalize: 统计一组后的回调函数 用g ...
- Android NDK(一) ndk-build构建工具进行NDK开发
本文目录 一.androidstudio环境 二.快捷键配置 三.新建项目 四.NDK开发 五.so文件编译 一. androidstudio的环境 在SDK Tools中安装NDK开发环境(File ...
- 链表二:链表中倒数第k个结点
题目:链表中倒数第k个结点描述:输入一个链表,输出该链表中倒数第k个结点.解决方案:思路: 根据规律得出倒数第k个节点是 n-k+1个节点 方法一:先计算出链表的长度,在循环走到n-k+1步.(相当于 ...
- KafkaStream简介
Kafka Streams 1 概述 Kafka Streams是一个客户端程序库,用于处理和分析存储在Kafka中的数据,并将得到的数据写回Kafka或发送到外部系统.Kafka Stream基于一 ...
- JZOJ 3875 星球联盟
[问题描述] 在遥远的 S 星系中一共有 N 个星球,编号为 1…N.其中的一些星球决定组成联盟, 以方便相互间的交流. 但是,组成联盟的首要条件就是交通条件.初始时,在这 N 个星球间有 M 条太空 ...
- 引入jar包到本地仓库方法
1. 将jar放到本地某个位置,然后cmd到目标位置:2. 执行mvn install:install-file -DgroupId=com.alipay -DartifactId=alipay-tr ...
- 第3章(2) Linux下C编程风格
Linux内核编码风格在内核源代码的Documentation/CodingStyle目录下(新版本内核在Documentation/process/coding-style.rst). 变量命名采用 ...
- [洛谷P1062/NOIP2006普及组] 数列
首先题面是这样的: 给定一个正整数 k(3≤k≤15) ,把所有k的方幂及所有有限个互不相等的k的方幂之和构成一个递增的序列,例如,当 k=3 时,这个序列是: 1,3,4,9,10,12,13,- ...
- 防止CSRF跨站请求伪造
CSRF(Cross-site request forgery)跨站请求伪造,也被称为“One Click Attack”或者Session Riding,通常缩写为CSRF或者XSRF,是一种对网站 ...