学习版本:3.0.5.v20180626
描述:
自带读写锁的对象
代码:
package org.tio.utils.lock;
import java.io.Serializable;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
/**
* 自带读写锁的对象.
*
* @author tanyaowu
*/
public class ObjWithLock<T> implements Serializable {
/**
*
*/
private T obj = null;
/**
*
*/
private ReentrantReadWriteLock lock = null;
/**
*
* @param obj
* @author tanyaowu
*/
public ObjWithLock(T obj) {
this(obj, new ReentrantReadWriteLock());
}
/**
*
* @param obj
* @param lock
* @author tanyaowu
*/
public ObjWithLock(T obj, ReentrantReadWriteLock lock) {
super();
this.obj = obj;
this.lock = lock;
}
/**
*
* @return
* @author tanyaowu
*/
public ReentrantReadWriteLock getLock() {
return lock;
}
/**
* 获取写锁
* @return
*/
public WriteLock writeLock() {
return lock.writeLock();
}
/**
* 获取读锁
* @return
*/
public ReadLock readLock() {
return lock.readLock();
}
/**
*
* @return
* @author tanyaowu
*/
public T getObj() {
return obj;
}
/**
*
* @param obj
* @author tanyaowu
*/
public void setObj(T obj) {
this.obj = obj;
}
private static final long serialVersionUID = -3048283373239453901L;
}
学习小结:
/**
* 编码:把业务消息包编码为可以发送的ByteBuffer
* 总的消息结构:消息头 + 消息体
* 消息头结构: 4个字节,存储消息体的长度
* 消息体结构: 对象的json串的byte[]
*/
@Override
public ByteBuffer encode(Packet packet, GroupContext groupContext, ChannelContext channelContext) {
HelloPacket helloPacket = (HelloPacket) packet;
byte[] body = helloPacket.getBody();
int bodyLen = 0;
if (body != null) {
bodyLen = body.length;
}
//bytebuffer的总长度是 = 消息头的长度 + 消息体的长度
int allLen = HelloPacket.HEADER_LENGHT + bodyLen;
//创建一个新的bytebuffer
ByteBuffer buffer = ByteBuffer.allocate(allLen);
//设置字节序
buffer.order(groupContext.getByteOrder());
//写入消息头----消息头的内容就是消息体的长度
buffer.putInt(bodyLen);
//写入消息体
if (body != null) {
buffer.put(body);
}
return buffer;
}
然后,创建一个新的ByteBuffer,分配的长度就是消息的总长度,然后在消息头中定义消息体的长度,然后写入消息体,这样client解码的时候可以按照消息头中的消息体长度去解析消息体,获取到完整的包。
解码
/**
* 解码:把接收到的ByteBuffer,解码成应用可以识别的业务消息包
* 总的消息结构:消息头 + 消息体
* 消息头结构: 4个字节,存储消息体的长度
* 消息体结构: 对象的json串的byte[]
*/
@Override
public HelloPacket decode(ByteBuffer buffer, int limit, int position, int readableLength, ChannelContext channelContext) throws AioDecodeException {
//提醒:buffer的开始位置并不一定是0,应用需要从buffer.position()开始读取数据
//收到的数据组不了业务包,则返回null以告诉框架数据不够
if (readableLength < HelloPacket.HEADER_LENGHT) {
return null;
}
//读取消息体的长度
int bodyLength = buffer.getInt();
//数据不正确,则抛出AioDecodeException异常
if (bodyLength < 0) {
throw new AioDecodeException("bodyLength [" + bodyLength + "] is not right, remote:" + channelContext.getClientNode());
}
//计算本次需要的数据长度
int neededLength = HelloPacket.HEADER_LENGHT + bodyLength;
//收到的数据是否足够组包
int isDataEnough = readableLength - neededLength;
// 不够消息体长度(剩下的buffe组不了消息体)
if (isDataEnough < 0) {
return null;
} else //组包成功
{
HelloPacket imPacket = new HelloPacket();
if (bodyLength > 0) {
byte[] dst = new byte[bodyLength];
buffer.get(dst);
imPacket.setBody(dst);
}
return imPacket;
}
}
然后,将HelloPacket协议的消息头长度+接收到的实际消息体长度得到需要读取的数组长度,然后和可读长度比较,如果可读长度不够,那么是一个半包,返回null。如果长度足够,那么从ByteBuffer中获取消息体长度的数据并置入新的HelloPacket中返回。
处理消息
/**
* 处理消息
*/
@Override
public void handler(Packet packet, ChannelContext channelContext) throws Exception {
HelloPacket helloPacket = (HelloPacket) packet;
byte[] body = helloPacket.getBody();
if (body != null) {
String str = new String(body, HelloPacket.CHARSET);
System.out.println("收到消息:" + str);
HelloPacket resppacket = new HelloPacket();
resppacket.setBody(("收到了你的消息,你的消息是:" + str).getBytes(HelloPacket.CHARSET));
Tio.send(channelContext, resppacket);
}
return;
}
其次,将byte[]消息体数据根据指定的字符集进行转换,得到可以真正处理的字符串格式。这个数据可以进行后续的任何处理,然后将这个处理的结果重新封装成HelloPacket,返回给client。这样,服务端的处理就结束了。
心跳检测
/**
* 此方法如果返回null,框架层面则不会发心跳;如果返回非null,框架层面会定时发本方法返回的消息包
*/
@Override
public HelloPacket heartbeatPacket() {
return heartbeatPacket;
}
描述:
这是一个上下文抽象类,定义了t-io框架的上下文所需的信息,它继承了MapWithLockPropSupport类,所以这个类是一个可以进行安全属性读写的类。它的主要子类就是两个,服务端的ServerGroupContext和客户端的ClientGroupContext。这两个后续再学习,先分析它们的共同特性的抽象父类GroupContext。
代码:
package org.tio.core;
import java.nio.ByteOrder;
import java.util.HashMap;
import java.util.HashSet;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicInteger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tio.client.ReconnConf;
import org.tio.core.cluster.DefaultMessageListener;
import org.tio.core.cluster.TioClusterConfig;
import org.tio.core.intf.AioHandler;
import org.tio.core.intf.AioListener;
import org.tio.core.intf.ChannelTraceHandler;
import org.tio.core.intf.GroupListener;
import org.tio.core.intf.Packet;
import org.tio.core.intf.TioUuid;
import org.tio.core.maintain.BsIds;
import org.tio.core.maintain.ClientNodeMap;
import org.tio.core.maintain.Groups;
import org.tio.core.maintain.Ids;
import org.tio.core.maintain.IpBlacklist;
import org.tio.core.maintain.IpStats;
import org.tio.core.maintain.Ips;
import org.tio.core.maintain.Tokens;
import org.tio.core.maintain.Users;
import org.tio.core.ssl.SslConfig;
import org.tio.core.stat.DefaultIpStatListener;
import org.tio.core.stat.GroupStat;
import org.tio.core.stat.IpStatListener;
import org.tio.utils.Threads;
import org.tio.utils.lock.MapWithLock;
import org.tio.utils.lock.SetWithLock;
import org.tio.utils.prop.MapWithLockPropSupport;
import org.tio.utils.thread.pool.SynThreadPoolExecutor;
/**
*
* @author tanyaowu
* 2016年10月10日 下午5:25:43
*/
public abstract class GroupContext extends MapWithLockPropSupport {
static Logger log = LoggerFactory.getLogger(GroupContext.class);
/**
* 默认的接收数据的buffer size
*/
public static final int READ_BUFFER_SIZE = Integer.getInteger("tio.default.read.buffer.size", 2048);
private final static AtomicInteger ID_ATOMIC = new AtomicInteger();
private ByteOrder byteOrder = ByteOrder.BIG_ENDIAN;
public boolean isShortConnection = false;
public SslConfig sslConfig = null;
public boolean debug = false;
/**
* 心跳超时时间(单位: 毫秒),如果用户不希望框架层面做心跳相关工作,请把此值设为0或负数
*/
public long heartbeatTimeout = 1000 * 120;
public PacketHandlerMode packetHandlerMode = PacketHandlerMode.SINGLE_THREAD;//.queue;
/**
* 接收数据的buffer size
*/
public int readBufferSize = READ_BUFFER_SIZE;
protected ReconnConf reconnConf;//重连配置
public final ChannelTraceHandler clientTraceHandler = new DefaultChannelTraceHandler();
private GroupListener groupListener = null;
private TioUuid tioUuid = new DefaultTioUuid();
public SynThreadPoolExecutor tioExecutor = null;
public SynThreadPoolExecutor tioCloseExecutor = null;
public ThreadPoolExecutor groupExecutor = null;
public final ClientNodeMap clientNodeMap = new ClientNodeMap();
public final SetWithLock connections = new SetWithLock(new HashSet());
public final Groups groups = new Groups();
public final Users users = new Users();
public final Tokens tokens = new Tokens();
public final Ids ids = new Ids();
public final BsIds bsIds = new BsIds();
public final Ips ips = new Ips();
public IpStats ipStats = null;
/**
* ip黑名单
*/
public IpBlacklist ipBlacklist = null;//new IpBlacklist();
public final MapWithLock waitingResps = new MapWithLock(new HashMap());
/**
* packet编码成bytebuffer时,是否与ChannelContext相关,false: packet编码与ChannelContext无关
*/
// private boolean isEncodeCareWithChannelContext = true;
protected String id;
/**
* 解码异常多少次就把ip拉黑
*/
protected int maxDecodeErrorCountForIp = 10;
protected String name = "未命名GroupContext";
private IpStatListener ipStatListener = DefaultIpStatListener.me;
private boolean isStopped = false;
/**
* 如果此值不为null,就表示要集群
*/
private TioClusterConfig tioClusterConfig = null;
public GroupContext() {
this(null, null);
}
/**
*
* @param tioExecutor
* @param groupExecutor
* @author: tanyaowu
*/
public GroupContext(SynThreadPoolExecutor tioExecutor, ThreadPoolExecutor groupExecutor) {
this(null, tioExecutor, groupExecutor);
}
/**
*
* @param tioClusterConfig
* @param tioExecutor
* @param groupExecutor
* @author: tanyaowu
*/
public GroupContext(TioClusterConfig tioClusterConfig, SynThreadPoolExecutor tioExecutor, ThreadPoolExecutor groupExecutor) {
this(tioClusterConfig, tioExecutor, null, groupExecutor);
}
/**
*
* @param tioClusterConfig
* @param tioExecutor
* @param groupExecutor
* @author: tanyaowu
*/
public GroupContext(TioClusterConfig tioClusterConfig, SynThreadPoolExecutor tioExecutor, SynThreadPoolExecutor tioCloseExecutor, ThreadPoolExecutor groupExecutor) {
super();
this.id = ID_ATOMIC.incrementAndGet() + "";
this.ipBlacklist = new IpBlacklist(id, this);
this.ipStats = new IpStats(this, null);
// 调整rtopicMessageListener设定位置
// 原来在TioClusterConfig内,这样不太友好,
// 因为TioClusterConfig做为GroupContext的构造形参同时GroupContext又是TioClusterConfig的初始化参数,无解…
this.setTioClusterConfig(tioClusterConfig);
this.tioExecutor = tioExecutor;
if (this.tioExecutor == null) {
this.tioExecutor = Threads.tioExecutor;
}
this.groupExecutor = groupExecutor;
if (this.groupExecutor == null) {
this.groupExecutor = Threads.groupExecutor;
}
this.tioCloseExecutor = tioCloseExecutor;
if (this.tioCloseExecutor == null) {
this.tioCloseExecutor = Threads.tioCloseExecutor;
}
}
/**
* 获取AioHandler对象
* @return
* @author: tanyaowu
*/
public abstract AioHandler getAioHandler();
/**
* 获取AioListener对象
* @return
* @author: tanyaowu
*/
public abstract AioListener getAioListener();
/**
* 是否是集群
* @return true: 是集群
* @author: tanyaowu
*/
public boolean isCluster() {
return tioClusterConfig != null;
}
/**
*
* @return
* @author tanyaowu
*/
public ByteOrder getByteOrder() {
return byteOrder;
}
/**
* @return the groupListener
*/
public GroupListener getGroupListener() {
return groupListener;
}
/**
* 获取GroupStat对象
* @return
* @author: tanyaowu
*/
public abstract GroupStat getGroupStat();
/**
*
* @return
* @author tanyaowu
*/
public String getId() {
return id;
}
public String getName() {
return name;
}
/**
* @return the reconnConf
*/
public ReconnConf getReconnConf() {
return reconnConf;
}
/**
* @return the tioUuid
*/
public TioUuid getTioUuid() {
return tioUuid;
}
/**
* @return the syns
*/
public MapWithLock getWaitingResps() {
return waitingResps;
}
/**
* @return the isEncodeCareWithChannelContext
*/
// public boolean isEncodeCareWithChannelContext() {
// return isEncodeCareWithChannelContext;
// }
// /**
// * @return the isShortConnection
// */
// public boolean isShortConnection {
// return isShortConnection;
// }
/**
* @return the isStop
*/
public boolean isStopped() {
return isStopped;
}
/**
*
* @param byteOrder
* @author tanyaowu
*/
public void setByteOrder(ByteOrder byteOrder) {
this.byteOrder = byteOrder;
}
/**
* @param isEncodeCareWithChannelContext the isEncodeCareWithChannelContext to set
*/
// public void setEncodeCareWithChannelContext(boolean isEncodeCareWithChannelContext) {
// this.isEncodeCareWithChannelContext = isEncodeCareWithChannelContext;
// }
/**
* @param groupListener the groupListener to set
*/
public void setGroupListener(GroupListener groupListener) {
this.groupListener = groupListener;
}
/**
* @param heartbeatTimeout the heartbeatTimeout to set
*/
public void setHeartbeatTimeout(long heartbeatTimeout) {
this.heartbeatTimeout = heartbeatTimeout;
}
public void setName(String name) {
this.name = name;
}
/**
* @param packetHandlerMode the packetHandlerMode to set
*/
public void setPacketHandlerMode(PacketHandlerMode packetHandlerMode) {
this.packetHandlerMode = packetHandlerMode;
}
/**
* @param readBufferSize the readBufferSize to set
*/
public void setReadBufferSize(int readBufferSize) {
this.readBufferSize = Math.min(readBufferSize, TcpConst.MAX_DATA_LENGTH);
}
/**
* @param isShortConnection the isShortConnection to set
*/
public void setShortConnection(boolean isShortConnection) {
this.isShortConnection = isShortConnection;
}
/**
* @param isStop the isStop to set
*/
public void setStopped(boolean isStopped) {
this.isStopped = isStopped;
}
/**
* @param tioUuid the tioUuid to set
*/
public void setTioUuid(TioUuid tioUuid) {
this.tioUuid = tioUuid;
}
public TioClusterConfig getTioClusterConfig() {
return tioClusterConfig;
}
public void setTioClusterConfig(TioClusterConfig tioClusterConfig) {
this.tioClusterConfig = tioClusterConfig;
if (this.tioClusterConfig != null) {
this.tioClusterConfig.addMessageListener(new DefaultMessageListener(this));
}
}
public void setSslConfig(SslConfig sslConfig) {
this.sslConfig = sslConfig;
}
public IpStatListener getIpStatListener() {
return ipStatListener;
}
public void setIpStatListener(IpStatListener ipStatListener) {
this.ipStatListener = ipStatListener;
// this.ipStats.setIpStatListener(ipStatListener);
}
/**
* 是服务器端还是客户端
* @return
* @author tanyaowu
*/
public abstract boolean isServer();
}
解析:
编码和解码的总结:
完整的消息包括消息头和消息体,而消息头存的是消息体的长度,消息头的长度是编码和解码双方都达成一致的协议。解码方先个根据协议中消息头的长度读取消息头中消息体的长度,然后通过消息体的长度去判断剩下的字节数组是否有足够长的消息体,如果没有那么是半包,如果大于等于了这个长度,那么读取消息体的长度还原成原来的包进行后续的处理。
public void start(String serverIp, int serverPort) throws IOException {
this.serverNode = new Node(serverIp, serverPort);
channelGroup = AsynchronousChannelGroup.withThreadPool(serverGroupContext.groupExecutor);
serverSocketChannel = AsynchronousServerSocketChannel.open(channelGroup);
serverSocketChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
serverSocketChannel.setOption(StandardSocketOptions.SO_RCVBUF, 64 * 1024);
InetSocketAddress listenAddress = null;
if (StringUtils.isBlank(serverIp)) {
listenAddress = new InetSocketAddress(serverPort);
} else {
listenAddress = new InetSocketAddress(serverIp, serverPort);
}
serverSocketChannel.bind(listenAddress, 0);
AcceptCompletionHandler acceptCompletionHandler = serverGroupContext.getAcceptCompletionHandler();
serverSocketChannel.accept(this, acceptCompletionHandler);
log.warn("{} started, listen on {}", serverGroupContext.getName(), this.serverNode);
}
看了源码,就是把原生的nio中启动server的方式封装了一下。和netty的方式也蛮相似的,之前联系netty的时候也是差不多这个流程走下来,或许下次也可以把netty的设置也用这样的方式封装起来,这样启动节点就简单明了。
服务端的启动很简单,就是实例化一个TioServer,然后将服务端上下文参数设置好传入,最后启动即可。如:
//handler, 包括编码、解码、消息处理
public static ServerAioHandler aioHandler = new HelloServerAioHandler();
//事件监听器,可以为null,但建议自己实现该接口,可以参考showcase了解些接口
public static ServerAioListener aioListener = null;
//一组连接共用的上下文对象
public static ServerGroupContext serverGroupContext = new ServerGroupContext("hello-tio-server", aioHandler, aioListener);
//tioServer对象
public static TioServer tioServer = new TioServer(serverGroupContext);
//有时候需要绑定ip,不需要则null
public static String serverIp = null;
//监听的端口
public static int serverPort = Const.PORT;
/**
* 启动程序入口
*/
public static void main(String[] args) throws IOException {
serverGroupContext.setHeartbeatTimeout(org.tio.examples.helloworld.common.Const.TIMEOUT);
tioServer.start(serverIp, serverPort);
}
服务端上下文实例化需要传入的是服务端的name,自定义的服务端handler和自定义的服务端listener。
/**
* @see java.lang.Runnable#run()
*
* @author tanyaowu
* 2017年3月21日 下午4:26:39
*
*/
@Override
public void run() {
ByteBuffer byteBuffer = newByteBuffer;
if (byteBuffer != null) {
if (lastByteBuffer != null) {
byteBuffer = ByteBufferUtils.composite(lastByteBuffer, byteBuffer);
lastByteBuffer = null;
}
} else {
return;
}
label_2: while (true) {
try {
int initPosition = byteBuffer.position();
int limit = byteBuffer.limit();
int readableLength = limit - initPosition;
Packet packet = null;
if (channelContext.packetNeededLength != null) {
log.info("{}, 解码所需长度:{}", channelContext, channelContext.packetNeededLength);
if (readableLength >= channelContext.packetNeededLength) {
packet = groupContext.getAioHandler().decode(byteBuffer, limit, initPosition, readableLength, channelContext);
}
} else {
packet = groupContext.getAioHandler().decode(byteBuffer, limit, initPosition, readableLength, channelContext);
}
if (packet == null)// 数据不够,解不了码
{
lastByteBuffer = ByteBufferUtils.copy(byteBuffer, initPosition, limit);
ChannelStat channelStat = channelContext.stat;
channelStat.decodeFailCount++;
// int len = byteBuffer.limit() - initPosition;
log.debug("{} 本次解码失败, 已经连续{}次解码失败,参与解码的数据长度共{}字节", channelContext, channelStat.decodeFailCount, readableLength);
if (channelStat.decodeFailCount > 5) {
if (channelContext.packetNeededLength == null) {
log.info("{} 本次解码失败, 已经连续{}次解码失败,参与解码的数据长度共{}字节", channelContext, channelStat.decodeFailCount, readableLength);
}
//检查慢包攻击(只有自用版才有)
if (channelStat.decodeFailCount > 10) {
int capacity = lastByteBuffer.capacity();
int per = capacity / channelStat.decodeFailCount;
if (per < Math.min(groupContext.readBufferSize / 2, 256)) {
throw new AioDecodeException("连续解码" + channelStat.decodeFailCount + "次都不成功,并且平均每次接收到的数据为" + per + "字节,有慢攻击的嫌疑");
}
}
}
return;
} else //解码成功
{
channelContext.setPacketNeededLength(null);
channelContext.stat.latestTimeOfReceivedPacket = SystemTimer.currentTimeMillis();
ChannelStat channelStat = channelContext.stat;
channelStat.decodeFailCount = 0;
int afterDecodePosition = byteBuffer.position();
int len = afterDecodePosition - initPosition;
packet.setByteCount(len);
groupContext.getGroupStat().receivedPackets.incrementAndGet();
channelContext.stat.receivedPackets.incrementAndGet();
if (groupContext.ipStats.durationList != null && groupContext.ipStats.durationList.size() > 0) {
try {
for (Long v : groupContext.ipStats.durationList) {
IpStat ipStat = groupContext.ipStats.get(v, channelContext.getClientNode().getIp());
ipStat.getReceivedPackets().incrementAndGet();
groupContext.getIpStatListener().onAfterDecoded(channelContext, packet, len, ipStat);
}
} catch (Exception e1) {
log.error(packet.logstr(), e1);
}
}
channelContext.traceClient(ChannelAction.RECEIVED, packet, null);
AioListener aioListener = groupContext.getAioListener();
try {
if (log.isDebugEnabled()) {
log.debug("{} 收到消息 {}", channelContext, packet.logstr());
}
aioListener.onAfterDecoded(channelContext, packet, len);
} catch (Throwable e) {
log.error(e.toString(), e);
}
if (log.isDebugEnabled()) {
log.debug("{}, 解包获得一个packet:{}", channelContext, packet.logstr());
}
handler(packet, len);
int remainingLength = byteBuffer.limit() - byteBuffer.position();
if (remainingLength > 0)//组包后,还剩有数据
{
if (log.isDebugEnabled()) {
log.debug("{},组包后,还剩有数据:{}", channelContext, remainingLength);
}
continue label_2;
} else//组包后,数据刚好用完
{
lastByteBuffer = null;
log.debug("{},组包后,数据刚好用完", channelContext);
return;
}
}
} catch (Throwable e) {
channelContext.setPacketNeededLength(null);
log.error(channelContext + ", " + byteBuffer + ", 解码异常:" + e.toString(), e);
if (e instanceof AioDecodeException) {
List list = groupContext.ipStats.durationList;
if (list != null && list.size() > 0) {
try {
for (Long v : list) {
IpStat ipStat = groupContext.ipStats.get(v, channelContext.getClientNode().getIp());
ipStat.getDecodeErrorCount().incrementAndGet();
groupContext.getIpStatListener().onDecodeError(channelContext, ipStat);
}
} catch (Exception e1) {
log.error(e1.toString(), e1);
}
}
}
Tio.close(channelContext, e, "解码异常:" + e.getMessage());
return;
}
}
}