第一次提交

This commit is contained in:
wyw
2024-08-08 00:31:26 +08:00
commit c202e2b63d
1819 changed files with 221890 additions and 0 deletions

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>fastbee-server</artifactId>
<groupId>com.fastbee</groupId>
<version>3.8.5</version>
</parent>
<artifactId>base-server</artifactId>
</project>

View File

@ -0,0 +1,29 @@
package com.fastbee.base.codec;
/**
* 分隔符报文处理器
* 处理分割符报文tcp粘包处理
* @author bill
*/
public class Delimiter {
public final byte[] value;
public final boolean strip;
public Delimiter(byte[] value) {
this(value, true);
}
public Delimiter(byte[] value, boolean strip) {
this.value = value;
this.strip = strip;
}
public byte[] getValue() {
return value;
}
public boolean isStrip() {
return strip;
}
}

View File

@ -0,0 +1,76 @@
package com.fastbee.base.codec;
import static io.netty.util.internal.ObjectUtil.checkPositive;
import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
/**
* 固定长度报文处理器
* 处理固定长度报文tcp粘包处理
* @author bill
*/
public class LengthField {
public final byte[] prefix;
/*最大帧长度*/
public final int lengthFieldMaxFrameLength;
/*偏移量*/
public final int lengthFieldOffset;
/*字段长度*/
public final int lengthFieldLength;
/*结尾偏移量*/
public final int lengthFieldEndOffset;
/*报文调整 默认0不调整*/
public final int lengthAdjustment;
/*默认0*/
public final int initialBytesToStrip;
/**构造固定长度处理器*/
public LengthField(byte[] prefix, int maxFrameLength, int lengthFieldOffset, int lengthFieldLength) {
this(prefix, maxFrameLength, lengthFieldOffset, lengthFieldLength, 0, 0);
}
public LengthField(byte[] prefix, int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip) {
checkPositive(maxFrameLength, "maxFrameLength_LengthField");
checkPositiveOrZero(lengthFieldOffset, "lengthFieldOffset");
checkPositiveOrZero(initialBytesToStrip, "initialBytesToStrip");
if (lengthFieldOffset > maxFrameLength - lengthFieldLength) {
throw new IllegalArgumentException("maxFrameLength_LengthField (" + maxFrameLength + ") must be equal to or greater than lengthFieldOffset (" + lengthFieldOffset + ") + lengthFieldLength (" + lengthFieldLength + ").");
} else {
this.prefix = prefix;
this.lengthFieldMaxFrameLength = maxFrameLength;
this.lengthFieldOffset = lengthFieldOffset;
this.lengthFieldLength = lengthFieldLength;
this.lengthAdjustment = lengthAdjustment;
this.lengthFieldEndOffset = lengthFieldOffset + lengthFieldLength;
this.initialBytesToStrip = initialBytesToStrip;
}
}
public byte[] getPrefix() {
return prefix;
}
public int getLengthFieldMaxFrameLength() {
return lengthFieldMaxFrameLength;
}
public int getLengthFieldOffset() {
return lengthFieldOffset;
}
public int getLengthFieldLength() {
return lengthFieldLength;
}
public int getLengthFieldEndOffset() {
return lengthFieldEndOffset;
}
public int getLengthAdjustment() {
return lengthAdjustment;
}
public int getInitialBytesToStrip() {
return initialBytesToStrip;
}
}

View File

@ -0,0 +1,18 @@
package com.fastbee.base.codec;
import com.fastbee.common.core.mq.DeviceReport;
import io.netty.buffer.ByteBuf;
/**
* 基础消息解码类
*
* @author bill
*/
public interface MessageDecoder {
/**
* TCP3.进站消息解码方法
*/
DeviceReport decode(ByteBuf buf, String clientId);
}

View File

@ -0,0 +1,15 @@
package com.fastbee.base.codec;
import com.fastbee.common.core.protocol.Message;
import com.fastbee.base.session.Session;
import io.netty.buffer.ByteBuf;
/**
* 基础消息编码类
*
* @author bill
*/
public interface MessageEncoder{
ByteBuf encode(Message message, String clientId);
}

View File

@ -0,0 +1,61 @@
package com.fastbee.base.core;
import com.fastbee.base.core.annotation.Async;
import com.fastbee.base.core.annotation.AsyncBatch;
import com.fastbee.base.core.annotation.PakMapping;
import com.fastbee.base.core.hanler.AsyncBatchHandler;
import com.fastbee.base.core.hanler.BaseHandler;
import com.fastbee.base.core.hanler.SyncHandler;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
/**
* 消息处理映射
*
* @author bill
*/
public abstract class AbstractHandlerMapping implements HandlerMapping {
private final Map<Object, BaseHandler> handlerMap = new HashMap<>(64);
/**
* 将node中被@Column标记的方法注册到映射表
*/
protected synchronized void registerHandlers(Object bean) {
Class<?> beanClass = bean.getClass();
Method[] methods = beanClass.getDeclaredMethods();
for (Method method : methods) {
PakMapping annotation = method.getAnnotation(PakMapping.class);
if (annotation != null) {
String desc = annotation.desc();
int[] types = annotation.types();
AsyncBatch asyncBatch = method.getAnnotation(AsyncBatch.class);
BaseHandler baseHandler;
// 异步处理
if (asyncBatch != null) {
baseHandler = new AsyncBatchHandler(bean, method, desc, asyncBatch.poolSize(), asyncBatch.maxMessageSize(), asyncBatch.maxWaitTime());
} else {
baseHandler = new SyncHandler(bean, method, desc, method.isAnnotationPresent(Async.class));
}
for (int type : types) {
handlerMap.put(type, baseHandler);
}
}
}
}
/**
* 根据消息类型获取handler
*/
@Override
public BaseHandler getHandler(int messageId) {
return handlerMap.get(messageId);
}
}

View File

@ -0,0 +1,27 @@
package com.fastbee.base.core;
import com.fastbee.base.core.annotation.Node;
import com.fastbee.base.util.ClassUtils;
import java.util.List;
/**
* 默认消息映射处理类
* @author bill
*/
public class DefaultHandlerMapping extends AbstractHandlerMapping {
public DefaultHandlerMapping(String endpointPackage) {
List<Class> endpointClasses = ClassUtils.getClassList(endpointPackage, Node.class);
for (Class endpointClass : endpointClasses) {
try {
Object bean = endpointClass.getDeclaredConstructor((Class[]) null).newInstance((Object[]) null);
super.registerHandlers(bean);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}

View File

@ -0,0 +1,37 @@
package com.fastbee.base.core;
import com.fastbee.common.core.protocol.Message;
import com.fastbee.base.session.Session;
/**
* 消息拦截器
* @author bill
*/
public interface HandlerInterceptor<T extends Message> {
/**
* 未匹配到对应的Handle消息处理
*/
T notSupported(T request, Session session);
/**
* 调用之前
* 处理消息类型匹配
*/
boolean beforeHandle(T request, Session session);
/**
* 需要应答设备,在这里执行
* 调用之后返回值为void的 */
T successful(T request, Session session);
/** 调用之后,有返回值的 */
void afterHandle(T request, T response, Session session);
/**
* 报错应答方法
* 调用之后抛出异常的
*/
T exceptional(T request, Session session, Exception e);
}

View File

@ -0,0 +1,12 @@
package com.fastbee.base.core;
import com.fastbee.base.core.hanler.BaseHandler;
/**
* 消息处理接口
* @author bill
*/
public interface HandlerMapping {
BaseHandler getHandler(int messageId);
}

View File

@ -0,0 +1,23 @@
package com.fastbee.base.core;
import com.fastbee.base.core.annotation.Node;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import java.util.Map;
/**
*
* @author bill
*/
public class SpringHandlerMapping extends AbstractHandlerMapping implements ApplicationContextAware {
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Map<String, Object> endpoints = applicationContext.getBeansWithAnnotation(Node.class);
for (Object bean : endpoints.values()) {
super.registerHandlers(bean);
}
}
}

View File

@ -0,0 +1,15 @@
package com.fastbee.base.core.annotation;
import java.lang.annotation.*;
/**
* 异步处理设备数据
*
* @author bill
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Async {
}

View File

@ -0,0 +1,27 @@
package com.fastbee.base.core.annotation;
import java.lang.annotation.*;
/**
* 多线程异步处理设备数据,新建线程组处理
*
* @author bill
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AsyncBatch {
/*批量处理的最大消息数*/
int maxMessageSize() default 5000;
/*线程数*/
int poolSize() default 2;
/*最大等待时间*/
int maxWaitTime() default 1000;
/*最小处理消息数*/
int minMessageSize() default 100;
}

View File

@ -0,0 +1,14 @@
package com.fastbee.base.core.annotation;
import java.lang.annotation.*;
/**
* 消息节点
* @author bill
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Node {
}

View File

@ -0,0 +1,17 @@
package com.fastbee.base.core.annotation;
import java.lang.annotation.*;
/**
* 字段映射
* @author bill
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PakMapping {
int[] types();
String desc() default "";
}

View File

@ -0,0 +1,128 @@
package com.fastbee.base.core.hanler;
import com.fastbee.common.core.protocol.Message;
import com.fastbee.base.session.Session;
import com.fastbee.base.util.VirtualList;
import com.fastbee.common.exception.ServiceException;
import io.netty.util.concurrent.DefaultThreadFactory;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Method;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 异步批量处理报文
* @author bill
*/
@Slf4j
public class AsyncBatchHandler extends BaseHandler{
/*消息处理队列*/
private final ConcurrentLinkedQueue<Message> queue;
/*线程池*/
private final ThreadPoolExecutor executor;
private final int poolSize;
private final int maxEventSize;
private final int maxWait;
private final int warningLines;
public AsyncBatchHandler(Object target, Method targetMethod, String desc, int poolSize, int maxEventSize, int maxWait) {
super(target, targetMethod, desc);
Class<?>[] parameterTypes = targetMethod.getParameterTypes();
if (parameterTypes.length >1){
throw new ServiceException("参数列表过长");
}
if (!parameterTypes[0].isAssignableFrom(List.class)){
throw new ServiceException("参数不是List类型");
}
this.poolSize = poolSize;
this.maxEventSize = maxEventSize;
this.maxWait = maxWait;
this.warningLines = maxEventSize * poolSize * 50;
this.queue = new ConcurrentLinkedQueue<>();
this.executor = new ThreadPoolExecutor(this.poolSize,this.poolSize,1000L, TimeUnit.MILLISECONDS
,new LinkedBlockingQueue<>(500),new DefaultThreadFactory(targetMethod.getName()));
for (int i = 0; i < poolSize; i++) {
boolean start = i == 0;
executor.execute(()->{
try {
startInternal(start);
}catch (Exception e){
log.error("线程池处理数据出错",e);
}
});
}
}
@Override
public <T extends Message> T invoke(T request, Session session) throws Exception {
queue.offer(request);
return null;
}
public void startInternal(boolean master) {
Message[] array = new Message[maxEventSize];
long logtime = 0;
long starttime = 0;
for (; ; ) {
Message temp;
int i = 0;
while ((temp = queue.poll()) != null) {
array[i++] = temp;
if (i >= maxEventSize) {
break;
}
}
if (i > 0) {
starttime = System.currentTimeMillis();
try {
targetMethod.invoke(targetObject, new VirtualList<>(array, i));
} catch (Exception e) {
log.warn(targetMethod.getName(), e);
}
long time = System.currentTimeMillis() - starttime;
if (time > 1000L) {
log.warn("线程池处理数据耗时:{}ms,共{}条记录", time, i);
}
}
if (i < maxEventSize) {
try {
for (int j = 0; j < i; j++) {
array[j] = null;
}
Thread.sleep(maxWait);
} catch (InterruptedException e) {
log.error("sleep error!");
}
} else if (master) {
if (logtime < starttime) {
logtime = starttime + 5000L;
int size = queue.size();
if (size > warningLines) {
log.warn("线程池队列已满, size:{}", size);
}
}
}
}
}
}

View File

@ -0,0 +1,86 @@
package com.fastbee.base.core.hanler;
import com.fastbee.common.core.protocol.Message;
import com.fastbee.base.session.Session;
import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
/**
* 基础处理类
*
* @author bill
*/
public abstract class BaseHandler {
public static final int MESSAGE = 0;
public static final int SESSION = 1;
public final Object targetObject;
public final Method targetMethod;
public final int[] parameterTypes;
public final boolean returnVoid;
public final boolean async;
public final String desc;
public BaseHandler(Object target, Method targetMethod, String desc) {
this(target, targetMethod, desc, false);
}
public BaseHandler(Object targetObject, Method targetMethod, String desc, boolean async) {
this.targetObject = targetObject;
this.targetMethod = targetMethod;
this.returnVoid = targetMethod.getReturnType().isAssignableFrom(Void.TYPE);
this.async = async;
if (desc == null || desc.isEmpty())
desc = targetMethod.getName();
this.desc = desc;
Type[] types = targetMethod.getGenericParameterTypes();
int[] parameterTypes = new int[types.length];
try {
for (int i = 0; i < types.length; i++) {
Type type = types[i];
Class<?> clazz;
if (type instanceof ParameterizedTypeImpl) {
clazz = (Class<?>) ((ParameterizedTypeImpl) type).getActualTypeArguments()[0];
} else {
clazz = (Class<?>) type;
}
if (Message.class.isAssignableFrom(clazz)) {
parameterTypes[i] = MESSAGE;
} else if (Session.class.isAssignableFrom(clazz)) {
parameterTypes[i] = SESSION;
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
this.parameterTypes = parameterTypes;
}
public <T extends Message> T invoke(T request, Session session) throws Exception {
Object[] args = new Object[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
int type = parameterTypes[i];
switch (type) {
case BaseHandler.MESSAGE:
args[i] = request;
break;
case BaseHandler.SESSION:
args[i] = session;
break;
}
}
return (T) targetMethod.invoke(targetObject, args);
}
@Override
public String toString() {
return desc;
}
}

View File

@ -0,0 +1,23 @@
package com.fastbee.base.core.hanler;
import com.fastbee.common.core.protocol.Message;
import com.fastbee.base.session.Session;
import java.lang.reflect.Method;
/**
* 同步处理报文
* @author bill
*/
public class SyncHandler extends BaseHandler{
public SyncHandler(Object target, Method targetMethod, String desc,boolean async) {
super(target, targetMethod, desc, async);
}
@Override
public <T extends Message> T invoke(T request, Session session) throws Exception {
return super.invoke(request, session);
}
}

View File

@ -0,0 +1,12 @@
package com.fastbee.base.core.model;
/**
* 消息流水号响应
* @author gsb
* @date 2022/11/7 10:19
*/
public interface Response {
/**应答消息流水号*/
int getResponseSerialNo();
}

View File

@ -0,0 +1,19 @@
package com.fastbee.base.model;
import lombok.Data;
/**
* @author gsb
* @date 2023/3/9 10:07
*/
@Data
public class DeviceMsg {
protected String clientId;
protected Long deviceId;
private int protocolVersion;
private Long productId;
}

View File

@ -0,0 +1,16 @@
package com.fastbee.base.model;
import com.fastbee.base.session.Session;
/**
* @author gsb
* @date 2023/3/9 10:03
*/
public enum SessionKey {
DeviceMsg;
public static DeviceMsg getDeviceMsg(Session session){
return (DeviceMsg)session.getAttribute(SessionKey.DeviceMsg);
}
}

View File

@ -0,0 +1,60 @@
package com.fastbee.base.service;
import com.fastbee.base.session.Session;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 服务会话存储接口
* @author gsb
* @date 2022/10/14 14:16
*/
public interface ISessionStore {
/**
* session 会话存储
*
* @param clientId: 客户端标识
* @param session: session会话
*/
void storeSession(String clientId, Session session);
/**
* 根据客户端标识获取相应会话
*
* @param clientId: 客户端标识
*/
Session getSession(String clientId);
/**
* 清除历史会话状态
*
* @param clientId: 客户端标识
*/
void cleanSession(String clientId);
/**
* 根据客户端标识查看是否存在该会话
*
* @param clientId:
*/
boolean containsKey(String clientId);
/**
* 获取集合
* @return MAP
*/
ConcurrentHashMap<String, Session> getSessionMap();
/**
* map分页从1开始
*
* @param sourceMap 分页数据
* @param pageSize 页面大小
* @param currentPage 当前页面
*/
public Map<String, Session> listPage(Map<String, Session> sourceMap, int pageSize, int currentPage);
}

View File

@ -0,0 +1,104 @@
package com.fastbee.base.service.impl;
import com.fastbee.base.service.ISessionStore;
import com.fastbee.base.session.Session;
import org.springframework.stereotype.Service;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 内存存储服务会话
*
* @author gsb
* @date 2022/10/14 14:18
*/
@Service
public class SessionStoreImpl implements ISessionStore {
/*session存储集合*/
private final ConcurrentHashMap<String, Session> sessionMap = new ConcurrentHashMap<>();
/**
* MQTT会话存储
*
* @param clientId: 客户端标识
* @param session: MQTT会话
*/
@Override
public void storeSession(String clientId, Session session) {
sessionMap.put(clientId, session);
}
/**
* 根据客户端标识获取相应会话
*
* @param clientId: 客户端标识
*/
@Override
public Session getSession(String clientId) {
return sessionMap.get(clientId);
}
/**
* 清除历史会话状态
*
* @param clientId: 客户端标识
*/
@Override
public void cleanSession(String clientId) {
sessionMap.remove(clientId);
}
/**
* 根据客户端标识查看是否存在该会话
*
* @param clientId:
*/
@Override
public boolean containsKey(String clientId) {
return sessionMap.containsKey(clientId);
}
/**
* 获取集合
* @return MAP
*/
@Override
public ConcurrentHashMap<String, Session> getSessionMap(){
return sessionMap;
}
/**
* map分页从1开始
*
* @param sourceMap 分页数据
* @param pageSize 页面大小
* @param currentPage 当前页面
*/
@Override
public Map<String, Session> listPage(Map<String, Session> sourceMap, int pageSize, int currentPage) {
Map<String, Session> map = new LinkedHashMap<>();
if (sourceMap.size() > 0) {
AtomicInteger flag = new AtomicInteger(0);
AtomicInteger size = new AtomicInteger(0);
int currIdx = (currentPage > 1 ? (currentPage - 1) * pageSize : 0);
sourceMap.forEach((ass, list_km) -> {
if (flag.get() >= currIdx) {
if (size.get() < pageSize) {
map.put(ass, list_km);
} else {
return;
}
size.getAndIncrement();
}
flag.getAndIncrement();
});
}
return map;
}
}

View File

@ -0,0 +1,74 @@
package com.fastbee.base.session;
import com.fastbee.common.core.protocol.Message;
import io.netty.buffer.ByteBuf;
import io.netty.channel.socket.DatagramPacket;
/**
* 报文消息的包装
* @author bill
*/
public abstract class Packet {
/*session*/
public final Session session;
/*基础消息*/
public Message message;
/*报文缓存buf*/
public ByteBuf byteBuf;
public static Packet of(Session session, Message message) {
if (session.isUdp()) {
return new UDP(session, message, null);
}
return new TCP(session, message, message.getPayload());
}
public static Packet of(Session session, ByteBuf message) {
if (session.isUdp()) {
return new UDP(session, null, message);
}
return new TCP(session, null, message);
}
private Packet(Session session, Message message, ByteBuf byteBuf) {
this.session = session;
this.message = message;
this.byteBuf = byteBuf;
}
public Packet replace(Message message) {
this.message = message;
return this;
}
public ByteBuf take() {
ByteBuf temp = this.byteBuf;
this.byteBuf = null;
return temp;
}
public abstract Object wrap(ByteBuf byteBuf);
private static class TCP extends Packet {
private TCP(Session session, Message message, ByteBuf byteBuf) {
super(session, message, byteBuf);
}
@Override
public Object wrap(ByteBuf byteBuf) {
return byteBuf;
}
}
private static class UDP extends Packet {
private UDP(Session session, Message message, ByteBuf byteBuf) {
super(session, message, byteBuf);
}
@Override
public Object wrap(ByteBuf byteBuf) {
return new DatagramPacket(byteBuf, session.remoteAddress());
}
}
}

View File

@ -0,0 +1,286 @@
package com.fastbee.base.session;
import com.fastbee.common.core.protocol.Message;
import com.fastbee.common.enums.ServerType;
import com.fastbee.common.exception.ServiceException;
import com.fastbee.common.utils.DateUtils;
import com.fastbee.common.utils.gateway.mq.Topics;
import com.fastbee.base.core.model.Response;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.mqtt.MqttMessageType;
import io.netty.handler.codec.mqtt.MqttVersion;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoSink;
import java.net.InetSocketAddress;
import java.util.*;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
/**
* 会话
*
* @Author guanshubiao
* @Date 2022/9/12 20:22
*/
@Data
@Slf4j
public class Session {
private boolean udp;
private Function<Session, Boolean> remover;
protected Channel channel;
/**
* 客户端id
*/
private String clientId;
private String productId;
private SessionManager sessionManager;
private InetSocketAddress remoteAddress;
private String remoteAddressStr;
private long creationTime;
private long lastAccessTime;
private Map<Object, Object> attributes;
private String sessionId;
//原子计数器,报文没有消息流水号的,充当流水号
private AtomicInteger serialNo = new AtomicInteger(0);
private int keepAlive = 60;
/*mqtt版本号*/
private MqttVersion version;
/*是否清楚会话*/
private Boolean cleanSession = false;
/*mqtt账号*/
private String username;
/*是否链接*/
private Boolean connected = false;
/*mqtt消息类型*/
private MqttMessageType mqttMessageType;
private int keepAliveMax = 120;
/*主题*/
private String topicName;
/*Channel处理类上下文*/
private ChannelHandlerContext handlerContext;
private List<Topics> topics;
private Integer topicCount;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date connected_at;
private String ip;
/*服务协议类型 MQTTTCP UDP COAP*/
private ServerType serverType;
public Session(){
}
public Session(SessionManager sessionManager,
Channel channel,
InetSocketAddress remoteAddress,
Function<Session, Boolean> remover,
boolean udp,
ServerType serverType) {
this.channel = channel;
this.creationTime = DateUtils.getTimestamp();
this.lastAccessTime = creationTime;
this.sessionManager = sessionManager;
this.remoteAddress = remoteAddress;
this.remoteAddressStr = remoteAddress.toString();
this.remover = remover;
this.udp = udp;
this.serverType = serverType;
this.ip = remoteAddress.getHostName();
this.connected = true;
this.connected_at = DateUtils.getNowDate();
if (sessionManager != null && sessionManager.getSessionKeys() != null) {
this.attributes = new EnumMap(sessionManager.getSessionKeys());
} else {
this.attributes = new TreeMap<>();
}
}
/**
* 判断设备是否已经注册上线
*/
public boolean isRegistered() {
return clientId != null;
}
/*设备端注册*/
public void register(Message message) {
register(message.getClientId(), message);
}
public void register(String clientId, Message message) {
//已经注册,不再发送注册包数据
if (isRegistered()){
return;
}
if (clientId == null) {
throw new ServiceException("客户端注册clientId不能为空");
}
this.clientId = clientId.toUpperCase();
if (sessionManager != null) {
sessionManager.add(this);
}
}
public Object getAttribute(Object name) {
return attributes.get(name);
}
public void setAttribute(Object name, Object value) {
attributes.put(name, value);
}
/**
* 获取最后上线时间
*/
public long access() {
return lastAccessTime = DateUtils.getTimestamp();
}
/**
* 获取远程端口
*/
public InetSocketAddress remoteAddress() {
return remoteAddress;
}
/**
* 获取流水号
*/
public int nextSerialNO() {
int current;
int next;
do {
current = serialNo.get();
next = current > 0xffff ? 0 : current;
} while (!serialNo.compareAndSet(current, next + 1));
return next;
}
/**
* 处理离线方法
*/
public void invalidate() {
if (isRegistered() && sessionManager != null) {
sessionManager.remove(this);
}
remover.apply(this);
}
public boolean isUdp() {
return udp;
}
private final Map<String, MonoSink> topicSubscribers = new HashMap<>();
private static final Mono rejected = Mono.error(new RejectedExecutionException("设备端暂无响应"));
/**
* 异步发送通知类消息
* 同步发送 mono.block()
* 订阅回调 mono.doOnSuccess(处理成功).doOnError(处理异常).subscribe(开始订阅)
*/
public Mono<Void> notify(Message message) {
return mono(channel.writeAndFlush(Packet.of(this, message)));
}
public Mono<Void> notify(ByteBuf message) {
return mono(channel.writeAndFlush(Packet.of(this, message)));
}
public static Mono<Void> mono(ChannelFuture channelFuture) {
return Mono.create(sink -> channelFuture.addListener(future -> {
if (future.isSuccess()) {
sink.success();
} else {
sink.error(future.cause());
}
}));
}
/**
* 异步发送消息,接收响应
* 同步接收 mono.block()
* 订阅回调 mono.doOnSuccess(处理成功).doOnError(处理异常).subscribe(开始订阅)
*/
public <T> Mono<T> request(Message message, Class<T> responseClass) {
String key = requestKey(message, responseClass);
Mono<T> subscribe = this.subscribe(key);
if (subscribe == null) {
return rejected;
}
ChannelFuture channelFuture = channel.writeAndFlush(Packet.of(this, message));
return Mono.create(sink -> channelFuture.addListener(future -> {
if (future.isSuccess()) {
sink.success(future);
} else {
sink.error(future.cause());
}
})).then(subscribe).doFinally(signal -> unsubscribe(key));
}
/**
* 消息响应
*/
public boolean response(Message message){
MonoSink monoSink = topicSubscribers.get(responseKey(message));
if (monoSink != null){
monoSink.success(message);
return true;
}
return false;
}
/**
* 订阅
*/
private Mono subscribe(String key) {
synchronized (topicSubscribers) {
if (!topicSubscribers.containsKey(key)) {
return Mono.create(sink -> topicSubscribers.put(key, sink));
}
}
return null;
}
/**
* 取消订阅
*/
private void unsubscribe(String key) {
topicSubscribers.remove(key);
}
/*生成流水号*/
private static String requestKey(Message request, Class responseClass) {
String className = responseClass.getName();
if (Response.class.isAssignableFrom(responseClass)) {
String serNo = request.getSerNo();
return new StringBuffer(34).append(className).append('.').append(serNo).toString();
}
return className;
}
/*返回流水号*/
private static String responseKey(Object response) {
String className = response.getClass().getName();
if (response instanceof Response) {
int serialNo = ((Response) response).getResponseSerialNo();
return new StringBuffer(34).append(className).append('.').append(serialNo).toString();
}
return className;
}
}

View File

@ -0,0 +1,26 @@
package com.fastbee.base.session;
/**
* session监听
* @author gsb
* @date 2022/11/7 8:57
*/
public interface SessionListener {
/** 客户端建立连接 */
default void sessionCreated(Session session) {
}
/** 客户端完成注册或鉴权 */
default void sessionRegistered(Session session) {
}
/**
* 客户端注销或离线
*/
default void sessionDestroyed(Session session) {
}
}

View File

@ -0,0 +1,129 @@
package com.fastbee.base.session;
import com.fastbee.common.enums.ServerType;
import com.fastbee.common.utils.spring.SpringUtils;
import com.fastbee.base.service.ISessionStore;
import io.netty.channel.Channel;
import lombok.extern.slf4j.Slf4j;
import java.net.InetSocketAddress;
import java.util.Collection;
import java.util.Locale;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
/**
* session管理
* @author gsb
* @date 2022/11/7 8:55
*/
@Slf4j
public class SessionManager {
private final Class<? extends Enum> sessionKeys;
private final SessionListener sessionListener;
/*Session会话存储*/
private static ISessionStore sessionStore = SpringUtils.getBean(ISessionStore.class);
public SessionManager(){
this(null,null);
}
public SessionManager(SessionListener sessionListener){
this(null,sessionListener);
}
public SessionManager(Class<? extends Enum> sessionKeys, SessionListener sessionListener){
this.sessionKeys = sessionKeys;
this.sessionListener = sessionListener;
}
/**
* 获取Session
*/
public Session getSession(String clientId) {
return sessionStore.getSession(clientId);
}
public boolean containKey(String clientId){
return sessionStore.containsKey(clientId);
}
/**
* 获取所有session
*/
public Collection<Session> all(){
return sessionStore.getSessionMap().values();
}
/**
* 新建session TCP
* @return
*/
public Session newInstance(Channel channel){
InetSocketAddress sender = (InetSocketAddress) channel.remoteAddress();
Session session = new Session(this, channel, sender, s -> {
channel.close();
return true;
}, false, ServerType.TCP);
if (sessionListener != null) {
try {
sessionListener.sessionCreated(session);
} catch (Exception e) {
log.error("sessionCreated", e);
}
}
return session;
}
/**
* 新建session UDP
*/
public Session newInstance(Channel channel, InetSocketAddress sender, Function<Session, Boolean> remover) {
Session session = new Session(this, channel, sender, remover, true,ServerType.UDP);
if (sessionListener != null) {
try {
sessionListener.sessionCreated(session);
} catch (Exception e) {
log.error("sessionCreated", e);
}
}
return session;
}
/**
* 设备端离线
*/
protected void remove(Session session){
sessionStore.cleanSession(session.getClientId());
if (null != sessionListener){
try {
//设备状态业务处理
sessionListener.sessionDestroyed(session);
}catch (Exception e){
log.error("设备端离线异常",e);
}
}
}
/**
* 设备端上线
*/
public void add(Session session){
sessionStore.storeSession(session.getClientId().toUpperCase(),session);
if (null != sessionListener){
try {
sessionListener.sessionRegistered(session);
}catch (Exception e){
log.error("设备端注册",e);
}
}
}
public Class<? extends Enum> getSessionKeys(){
return sessionKeys;
}
}

View File

@ -0,0 +1,42 @@
package com.fastbee.base.util;
import com.fastbee.base.session.Session;
import io.netty.channel.Channel;
import io.netty.util.AttributeKey;
/**
* channel和pipeline桥梁Attribute信息构建
* @author gsb
* @date 2022/10/7 18:40
*/
public class AttributeUtils {
/*存储客户端连接信息*/
private static final AttributeKey<Object> SESSION_KEY = AttributeKey.valueOf("session");
/*存储客户端id*/
private static final AttributeKey<Object> CLIENT_ID_KEY = AttributeKey.valueOf("clientId");
/*添加session*/
public static void setSession(Channel channel, Session session){
channel.attr(SESSION_KEY).set(session);
}
/*获取session*/
public static Session getSession(Channel channel){
return (Session) channel.attr(SESSION_KEY).get();
}
/*添加clientId*/
public static void setClientId(Channel channel, String clientId){
channel.attr(CLIENT_ID_KEY).set(clientId);
}
/*获取clientId*/
public static String getClientId(Channel channel){
return (String) channel.attr(CLIENT_ID_KEY).get();
}
}

View File

@ -0,0 +1,43 @@
package com.fastbee.base.util;
import io.netty.buffer.ByteBuf;
/**
* byteBuf操作工具
* @author bill
*/
public class ByteBufUtils {
/**
* 返回报文的readerIndex和报文中找到的第一个指针之间的字节数-如果在报文中找不到针则返回1
*/
public static int indexOf(ByteBuf haystack, byte[] needle) {
for (int i = haystack.readerIndex(); i < haystack.writerIndex(); i++) {
int haystackIndex = i;
int needleIndex;
for (needleIndex = 0; needleIndex < needle.length; needleIndex++) {
if (haystack.getByte(haystackIndex) != needle[needleIndex]) {
break;
} else {
haystackIndex++;
if (haystackIndex == haystack.writerIndex() && needleIndex != needle.length - 1) {
return -1;
}
}
}
if (needleIndex == needle.length) {
// 找到读取的index
return i - haystack.readerIndex();
}
}
return -1;
}
public static boolean startsWith(ByteBuf haystack, byte[] prefix) {
for (int i = 0, j = haystack.readerIndex(); i < prefix.length; )
if (prefix[i++] != haystack.getByte(j++))
return false;
return true;
}
}

View File

@ -0,0 +1,106 @@
package com.fastbee.base.util;
import java.io.File;
import java.lang.annotation.Annotation;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* 类操作工具
* @author bill
*/
public class ClassUtils {
public static List<Class> getClassList(String packageName, Class<? extends Annotation> annotationClass) {
List<Class> classList = getClassList(packageName);
classList.removeIf(next -> !next.isAnnotationPresent(annotationClass));
return classList;
}
public static List<Class> getClassList(String packageName) {
List<Class> classList = new LinkedList<>();
String path = packageName.replace(".", "/");
try {
Enumeration<URL> urls = ClassUtils.getClassLoader().getResources(path);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
if (url != null) {
String protocol = url.getProtocol();
if (protocol.equals("file")) {
addClass(classList, url.toURI().getPath(), packageName);
} else if (protocol.equals("jar")) {
JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection();
JarFile jarFile = jarURLConnection.getJarFile();
Enumeration<JarEntry> jarEntries = jarFile.entries();
while (jarEntries.hasMoreElements()) {
JarEntry jarEntry = jarEntries.nextElement();
String entryName = jarEntry.getName();
if (entryName.startsWith(path) && entryName.endsWith(".class")) {
String className = entryName.substring(0, entryName.lastIndexOf(".")).replaceAll("/", ".");
addClass(classList, className);
}
}
}
}
}
} catch (Exception e) {
throw new RuntimeException("Initial class error!");
}
return classList;
}
private static void addClass(List<Class> classList, String packagePath, String packageName) {
try {
File[] files = new File(packagePath).listFiles(file -> (file.isDirectory() || file.getName().endsWith(".class")));
if (files != null)
for (File file : files) {
String fileName = file.getName();
if (file.isFile()) {
String className = fileName.substring(0, fileName.lastIndexOf("."));
if (packageName != null) {
className = packageName + "." + className;
}
addClass(classList, className);
} else {
String subPackagePath = fileName;
if (packageName != null) {
subPackagePath = packagePath + "/" + subPackagePath;
}
String subPackageName = fileName;
if (packageName != null) {
subPackageName = packageName + "." + subPackageName;
}
addClass(classList, subPackagePath, subPackageName);
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static void addClass(List<Class> classList, String className) {
classList.add(loadClass(className, false));
}
public static Class loadClass(String className, boolean isInitialized) {
try {
return Class.forName(className, isInitialized, getClassLoader());
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
public static ClassLoader getClassLoader() {
return Thread.currentThread().getContextClassLoader();
}
}

View File

@ -0,0 +1,51 @@
package com.fastbee.base.util;
import lombok.Data;
import java.util.concurrent.ConcurrentHashMap;
/**
* concurrentMap存储类
* @author bill
*/
@Data
public abstract class ConcurrentStorage<K,V> implements Storage<K,V>{
private volatile ConcurrentHashMap<K,V> map;
public ConcurrentStorage(){
this(new ConcurrentHashMap<K,V>());
}
public ConcurrentStorage(ConcurrentHashMap<K,V> map){
this.map = map;
}
@Override
public V push(K key, V value) {
return map.put(key,value);
}
@Override
public V pop(K key) {
return map.get(key);
}
@Override
public V remove(K key) {
return map.remove(key);
}
@Override
public boolean isContains(Object key) {
return map.containsKey(key);
}
@Override
public Object getStorage() {
return this.map;
}
}

View File

@ -0,0 +1,30 @@
package com.fastbee.base.util;
import com.fastbee.common.core.mq.DeviceStatusBo;
import com.fastbee.common.enums.DeviceStatus;
import com.fastbee.common.utils.DateUtils;
import io.netty.channel.Channel;
import java.net.InetSocketAddress;
/**
* 设备信息工具类
* @author bill
*/
public class DeviceUtils {
/*构造返回MQ的设备状态model*/
public static DeviceStatusBo buildStatusMsg(Channel channel, String clientId, DeviceStatus status, String ip){
InetSocketAddress address = (InetSocketAddress) channel.remoteAddress();
return DeviceStatusBo.builder()
.serialNumber(clientId)
.status(status)
.ip(ip)
.hostName(address.getHostName())
.port(address.getPort())
.timestamp(DateUtils.getNowDate()).build();
}
}

View File

@ -0,0 +1,48 @@
package com.fastbee.base.util;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author bill
*/
public class Stopwatch {
private final AtomicInteger count = new AtomicInteger();
private final Thread thread;
public Stopwatch start() {
this.thread.start();
return this;
}
public int increment() {
return count.incrementAndGet();
}
public Stopwatch() {
thread = new Thread(() -> {
long start;
while (true) {
if (count.get() > 0) {
start = System.currentTimeMillis();
break;
}
try {
Thread.sleep(1L);
} catch (Exception e) {
}
}
while (true) {
try {
Thread.sleep(2000L);
} catch (Exception e) {
}
int num = count.get();
long time = (System.currentTimeMillis() - start) / 1000;
System.out.println(time + "\t" + num + "\t" + num / time);
}
});
thread.setName(Thread.currentThread().getName() + "-c");
thread.setPriority(Thread.MIN_PRIORITY);
thread.setDaemon(true);
}
}

View File

@ -0,0 +1,18 @@
package com.fastbee.base.util;
/**
* 存储管理
* @author bill
*/
public interface Storage<K,V> {
V push(K key, V value);
V pop(K key);
V remove(K key);
boolean isContains(K key);
Object getStorage();
}

View File

@ -0,0 +1,101 @@
package com.fastbee.base.util;
import java.io.Serializable;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.UnaryOperator;
/**
* @author bill
*/
public class VirtualList<E> extends AbstractList<E> implements RandomAccess, Serializable {
private final E[] elementData;
private final int size;
public VirtualList(E[] array, int length) {
this.elementData = array;
this.size = length;
}
@Override
public int size() {
return size;
}
@Override
public Object[] toArray() {
return elementData.clone();
}
@Override
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
if (a.length < size)
return Arrays.copyOf(this.elementData, size,
(Class<? extends T[]>) a.getClass());
System.arraycopy(this.elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
@Override
public E get(int index) {
return elementData[index];
}
@Override
public E set(int index, E element) {
E oldValue = elementData[index];
elementData[index] = element;
return oldValue;
}
@Override
public int indexOf(Object o) {
E[] a = this.elementData;
if (o == null) {
for (int i = 0; i < size; i++)
if (a[i] == null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(a[i]))
return i;
}
return -1;
}
@Override
public boolean contains(Object o) {
return indexOf(o) != -1;
}
@Override
public Spliterator<E> spliterator() {
return Spliterators.spliterator(elementData, 0, size, Spliterator.ORDERED);
}
@Override
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
for (int i = 0; i < size; i++) {
action.accept(elementData[i]);
}
}
@Override
public void replaceAll(UnaryOperator<E> operator) {
Objects.requireNonNull(operator);
E[] a = this.elementData;
for (int i = 0; i < size; i++) {
a[i] = operator.apply(a[i]);
}
}
@Override
public void sort(Comparator<? super E> c) {
Arrays.sort(elementData, 0, size, c);
}
}

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>fastbee-server</artifactId>
<groupId>com.fastbee</groupId>
<version>3.8.5</version>
</parent>
<artifactId>boot-strap</artifactId>
<description>网关服务启动模块</description>
<dependencies>
<dependency>
<groupId>com.fastbee</groupId>
<artifactId>mqtt-broker</artifactId>
</dependency>
<dependency>
<groupId>com.fastbee</groupId>
<artifactId>http-server</artifactId>
</dependency>
<dependency>
<groupId>com.fastbee</groupId>
<artifactId>fastbee-protocol-collect</artifactId>
</dependency>
<dependency>
<groupId>com.fastbee</groupId>
<artifactId>base-server</artifactId>
<version>3.8.5</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fastbee</groupId>
<artifactId>coap-server</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,33 @@
package com.fastbee.bootstrap.coap;
import com.fastbee.coap.Coapserver;
import com.fastbee.common.enums.ServerType;
import com.fastbee.server.Server;
import com.fastbee.server.config.NettyConfig;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
@Order(13)
@Configuration
@ConditionalOnProperty(name = "server.coap.enabled", havingValue = "true")//设置是否启动
@ConfigurationProperties(value = "server.coap")
@Data
public class CoapBootStrap {
@Autowired
private Coapserver coapserver;
private int port;
@Bean(initMethod = "start",destroyMethod = "stop")
public Server coapServerBoot(){
return NettyConfig.custom()
.setPort(port)
.setName(ServerType.COAP.getDes())
.setType(ServerType.COAP)
.setServer(coapserver)
.build();
}
}

View File

@ -0,0 +1,34 @@
package com.fastbee.bootstrap.http;
import com.fastbee.common.enums.ServerType;
import com.fastbee.http.server.HttpServer;
import com.fastbee.server.Server;
import com.fastbee.server.config.NettyConfig;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
@Order(13)
@Configuration
@ConditionalOnProperty(name = "server.http.enabled", havingValue = "true")//设置是否启动
@ConfigurationProperties(value = "server.http")
@Data
public class HttpBootStrap {
@Autowired
private HttpServer httpServer;
private int port;
@Bean(initMethod = "start",destroyMethod = "stop")
public Server httpServerBoot(){
return NettyConfig.custom()
.setPort(port)
.setName(ServerType.HTTP.getDes())
.setType(ServerType.HTTP)
.setServer(httpServer)
.build();
}
}

View File

@ -0,0 +1,70 @@
package com.fastbee.bootstrap.mqtt;
import com.fastbee.mqtt.server.MqttServer;
import com.fastbee.mqtt.server.WebSocketServer;
import com.fastbee.server.Server;
import com.fastbee.server.config.NettyConfig;
import com.fastbee.common.enums.ServerType;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
/**
* MQTT-BROKER启动
* @author gsb
* @date 2022/9/17 17:25
*/
@Order(10)
@Configuration
@ConfigurationProperties(value = "server.broker")
@Data
public class MQTTBootStrap {
@Autowired
private MqttServer mqttServer;
@Autowired
private WebSocketServer webSocketServer;
/*服务器集群节点*/
private String brokerNode;
/*端口*/
private int port;
/*心跳时间*/
private int keepAlive;
/*webSocket端口*/
private int websocketPort;
/*webSocket路由*/
private String websocketPath;
/**
* 启动mqttBroker
* @return server
*/
@ConditionalOnProperty(value = "server.broker.enabled", havingValue = "true")
@Bean(initMethod = "start", destroyMethod = "stop")
public Server mqttBroker() {
return NettyConfig.custom()
.setIdleStateTime(0,0,keepAlive)
.setName(ServerType.MQTT.getDes())
.setType(ServerType.MQTT)
.setPort(port)
.setServer(mqttServer)
.build();
}
@ConditionalOnProperty(value = "server.broker.openws", havingValue = "true")
@Bean(initMethod = "start",destroyMethod = "stop")
public Server webSocket(){
return NettyConfig.custom()
.setIdleStateTime(0,0,keepAlive)
.setName(ServerType.WEBSOCKET.getDes())
.setType(ServerType.WEBSOCKET)
.setPort(websocketPort)
.setServer(webSocketServer)
.build();
}
}

View File

@ -0,0 +1,62 @@
package com.fastbee.bootstrap.tcp;
import com.fastbee.bootstrap.tcp.config.TcpHandlerInterceptor;
import com.fastbee.common.enums.ServerType;
import com.fastbee.modbus.codec.MessageAdapter;
import com.fastbee.server.Server;
import com.fastbee.base.codec.Delimiter;
import com.fastbee.server.config.NettyConfig;
import com.fastbee.base.core.HandlerMapping;
import com.fastbee.base.session.SessionManager;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
/**
* TCP服务端启动
* @author bill
*/
@Order(11)
@Configuration
@ConditionalOnProperty(value = "server.tcp.enabled", havingValue = "true")//设置是否启动
@ConfigurationProperties(value = "server.tcp")
@Data
public class TCPBootStrap {
private int port;
private int keepAlive;
private byte delimiter;
@Autowired
private MessageAdapter messageAdapter;
private HandlerMapping handlerMapping;
private TcpHandlerInterceptor handlerInterpolator;
private SessionManager sessionManager;
public TCPBootStrap(HandlerMapping handlerMapping, TcpHandlerInterceptor interpolator, SessionManager sessionManager){
this.handlerMapping = handlerMapping;
this.handlerInterpolator = interpolator;
this.sessionManager = sessionManager;
}
@Bean(initMethod = "start",destroyMethod = "stop")
public Server TCPServer(){
return NettyConfig.custom()
.setIdleStateTime(keepAlive,0,0)
.setPort(port)
//设置报文最大长度 modbus-rtu
.setMaxFrameLength(100)
.setDelimiters(new Delimiter(new byte[]{0x7e}, true))
.setDecoder(messageAdapter)
.setEncoder(messageAdapter)
.setHandlerMapping(handlerMapping)
.setHandlerInterceptor(handlerInterpolator)
.setSessionManager(sessionManager)
.setName(ServerType.TCP.getDes())
.setType(ServerType.TCP)
.build();
}
}

View File

@ -0,0 +1,43 @@
package com.fastbee.bootstrap.tcp.config;
import com.fastbee.protocol.WModelManager;
import com.fastbee.base.core.HandlerMapping;
import com.fastbee.base.core.SpringHandlerMapping;
import com.fastbee.base.session.SessionListener;
import com.fastbee.base.session.SessionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author bill
*/
@Configuration
public class TcpBeanConfig {
@Bean
public HandlerMapping handlerMapping(){
return new SpringHandlerMapping();
}
@Bean
public TcpHandlerInterceptor handlerInterceptor(){
return new TcpHandlerInterceptor();
}
@Bean
public SessionListener sessionListener(){
return new TcpSessionListener();
}
@Bean
public SessionManager sessionManager(SessionListener sessionListener){
return new SessionManager(sessionListener);
}
@Bean
public WModelManager wModelManager(){
return new WModelManager("com.fastbee.modbus");
}
}

View File

@ -0,0 +1,52 @@
package com.fastbee.bootstrap.tcp.config;
import com.fastbee.common.core.protocol.Message;
import com.fastbee.modbus.pak.TcpDtu;
import com.fastbee.base.core.HandlerInterceptor;
import com.fastbee.base.session.Session;
import lombok.extern.slf4j.Slf4j;
/**
* 消息拦截应答
* @author bill
*/
@Slf4j
public class TcpHandlerInterceptor implements HandlerInterceptor<Message> {
@Override
public Message notSupported(Message request, Session session) {
return null;
}
@Override
public boolean beforeHandle(Message request, Session session) {
int messageId = Integer.parseInt(request.getMessageId());
if (messageId == TcpDtu.注册报文 || messageId == TcpDtu.心跳包 || messageId == TcpDtu.整包消息){
return true;
}
if (!session.isRegistered()){
log.warn("设备未注册,session={}",session);
return true;
}
return false;
}
@Override
public Message successful(Message request, Session session) {
return null;
}
/**
* 调用之后
*/
@Override
public void afterHandle(Message request, Message response, Session session) {
if (response != null){
//response.setSerialNo(session.nextSerialNO());
}
}
@Override
public Message exceptional(Message request, Session session, Exception e) {
return null;
}
}

View File

@ -0,0 +1,55 @@
package com.fastbee.bootstrap.tcp.config;
import com.fastbee.base.session.Session;
import com.fastbee.base.session.SessionListener;
import com.fastbee.common.core.mq.DeviceStatusBo;
import com.fastbee.common.enums.DeviceStatus;
import com.fastbee.mq.redischannel.consumer.DeviceStatusConsumer;
import com.fastbee.base.util.DeviceUtils;
import com.fastbee.mqtt.manager.MqttRemoteManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* @author bill
*/
@Slf4j
@Service
public class TcpSessionListener implements SessionListener {
@Resource
private MqttRemoteManager remoteManager;
@Resource
private DeviceStatusConsumer statusConsumer;
/** 客户端建立连接 */
@Override
public void sessionCreated(Session session) {
}
/** 客户端完成注册或鉴权 */
@Override
public void sessionRegistered(Session session) {
DeviceStatusBo statusBo = DeviceUtils.buildStatusMsg(session.getChannel(), session.getClientId(),
DeviceStatus.ONLINE, session.getIp());
statusConsumer.consume(statusBo);
remoteManager.pushDeviceStatus(-1L,statusBo.getSerialNumber(),statusBo.getStatus());
log.info("TCP客户端:[{}],注册上线",session.getClientId());
}
/** 客户端注销或离线 */
@Override
public void sessionDestroyed(Session session) {
/*推送离线消息到mq处理*/
DeviceStatusBo statusBo = DeviceUtils.buildStatusMsg(session.getChannel(),
session.getClientId(), DeviceStatus.OFFLINE, session.getIp());
statusConsumer.consume(statusBo);
remoteManager.pushDeviceStatus(-1L,statusBo.getSerialNumber(),statusBo.getStatus());
log.info("TCP客户端:[{}],离线",session.getClientId());
}
}

View File

@ -0,0 +1,58 @@
package com.fastbee.bootstrap.udp;
import com.fastbee.bootstrap.tcp.config.TcpHandlerInterceptor;
import com.fastbee.common.enums.ServerType;
import com.fastbee.modbus.codec.MessageAdapter;
import com.fastbee.server.Server;
import com.fastbee.server.config.NettyConfig;
import com.fastbee.base.core.HandlerMapping;
import com.fastbee.base.session.SessionManager;
import lombok.Data;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
/**
* UDP服务端启动
* @author bill
*/
@Order(12)
@Configuration
@ConditionalOnProperty(value = "server.udp.enabled", havingValue = "true")//设置是否启动
@ConfigurationProperties(value = "server.udp")
@Data
public class UDPBootStrap {
private int port;
private byte delimiter;
private MessageAdapter messageAdapter;
private HandlerMapping handlerMapping;
private TcpHandlerInterceptor handlerInterpolator;
private SessionManager sessionManager;
public UDPBootStrap(MessageAdapter messageAdapter, HandlerMapping handlerMapping, TcpHandlerInterceptor interpolator, SessionManager sessionManager){
this.messageAdapter = messageAdapter;
this.handlerMapping = handlerMapping;
this.handlerInterpolator = interpolator;
this.sessionManager = sessionManager;
}
@Bean(initMethod = "start",destroyMethod = "stop")
public Server UDPServer(){
return NettyConfig.custom()
.setPort(port)
//.setDelimiters(new Delimiter(new byte[]{0x0D},false)) //分隔符配置
.setDecoder(messageAdapter)
.setEncoder(messageAdapter)
.setHandlerMapping(handlerMapping)
.setHandlerInterceptor(handlerInterpolator)
.setSessionManager(sessionManager)
.setName(ServerType.UDP.getDes())
.setType(ServerType.UDP)
.build();
}
}

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>fastbee-server</artifactId>
<groupId>com.fastbee</groupId>
<version>3.8.5</version>
</parent>
<artifactId>coap-server</artifactId>
<name>coap-server</name>
<dependencies>
<dependency>
<groupId>com.fastbee</groupId>
<artifactId>iot-server-core</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,40 @@
package com.fastbee.coap;
import com.fastbee.coap.handler.TimeResourceHandler;
import com.fastbee.coap.server.CoapServerChannelInitializer;
import com.fastbee.coap.server.ResourceRegistry;
import com.fastbee.server.Server;
import com.fastbee.server.config.NettyConfig;
import io.netty.bootstrap.AbstractBootstrap;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioDatagramChannel;
import io.netty.util.concurrent.DefaultThreadFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
public class Coapserver extends Server {
@Override
protected AbstractBootstrap initialize() {
bossGroup = new NioEventLoopGroup(1, new DefaultThreadFactory(config.name, Thread.MAX_PRIORITY));
if (config.businessCore > 0) {
businessService = new ThreadPoolExecutor(config.businessCore, config.businessCore, 1L,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new DefaultThreadFactory(config.name, true, Thread.NORM_PRIORITY));
}
//注册数据路由
ResourceRegistry resourceRegistry = new ResourceRegistry();
resourceRegistry.register(new TimeResourceHandler("/utc-time"));
return new Bootstrap()
.group(bossGroup)
.channel(NioDatagramChannel.class)
.option(ChannelOption.SO_BROADCAST, true)
.handler(new CoapServerChannelInitializer(resourceRegistry));
}
}

View File

@ -0,0 +1,35 @@
## Coap
## Coap Roadmap
- Coap消息编解码 消息头 可选项 消息内容
- Coap数据路由注册
- Coap观察机制
- Coap消息块处理
- Coap认证处理 Token Messageid
- Coap content上下文
- Coap TransportService北向接口
## Coap南向接口
- .well-known/core 查询服务端支持的uri资源
- /utc-time 查询服务端utc-time
- 其他拓展(设备注册,设备上下线,设备属性上报,设备)
## Coap北向接口 TransportService 开放给业务层的接口类)
- 设备命令下发
- 设备属性设置
- 设备升级
- 设备重启
## Coap TransportContext 上下文
- 转发器 http转发,mqtt broker转发
- 消息格式适配器(物模型编码,json,字节编码,裸数据)
- 调度器
- 执行线程池
- 请求消息
- 响应消息
- 会话管理
## Coap TransportHandler 处理函数
- ReqDispatcher 消息分发处理

View File

@ -0,0 +1,222 @@
package com.fastbee.coap.codec;
import com.fastbee.coap.model.*;
import com.fastbee.coap.model.options.*;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.socket.DatagramPacket;
import io.netty.handler.codec.MessageToMessageDecoder;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import static com.fastbee.coap.model.MessageCode.EMPTY;
import static com.fastbee.coap.model.MessageType.*;
@ChannelHandler.Sharable
@Slf4j
public class CoapMessageDecoder extends MessageToMessageDecoder<DatagramPacket> {
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, DatagramPacket datagramPacket, List<Object> list) throws Exception {
ByteBuf buffer = datagramPacket.content();
log.debug("Incoming message to be decoded (length: {})", buffer.readableBytes());
//Decode the header values
int encodedHeader = buffer.readInt();
int version = (encodedHeader >>> 30) & 0x03;
int messageType = (encodedHeader >>> 28) & 0x03;
int tokenLength = (encodedHeader >>> 24) & 0x0F;
int messageCode = (encodedHeader >>> 16) & 0xFF;
int messageID = (encodedHeader) & 0xFFFF;
log.debug("Decoded Header: (T) {}, (TKL) {}, (C) {}, (ID) {}",
messageType, tokenLength, messageCode, messageID);
//Check whether the protocol version is supported (=1)
if (version != CoapMessage.PROTOCOL_VERSION) {
String message = "CoAP version (" + version + ") is other than \"1\"!";
throw new HeaderDecodingException(messageID, datagramPacket.sender(), message);
}
//Check whether TKL indicates a not allowed token length
if (tokenLength > CoapMessage.MAX_TOKEN_LENGTH) {
String message = "TKL value (" + tokenLength + ") is larger than 8!";
throw new HeaderDecodingException(messageID, datagramPacket.sender(), message);
}
//Check whether there are enough unread bytes left to read the token
if (buffer.readableBytes() < tokenLength) {
String message = "TKL value is " + tokenLength + " but only " + buffer.readableBytes() + " bytes left!";
throw new HeaderDecodingException(messageID, datagramPacket.sender(), message);
}
//Handle empty message (ignore everything but the first 4 bytes)
if (messageCode == EMPTY) {
if (messageType == ACK) {
log.info("ACK Message");
list.add(CoapMessage.createEmptyAcknowledgement(messageID));
return ;
} else if (messageType == RST) {
log.info("RST Message");
list.add(CoapMessage.createEmptyReset(messageID));
return;
} else if (messageType == CON) {
log.info("CON Message");
list.add(CoapMessage.createPing(messageID));
return;
} else {
//There is no empty NON message defined, so send a RST
throw new HeaderDecodingException(messageID, datagramPacket.sender(), "Empty NON messages are invalid!");
}
}
//Read the token
byte[] token = new byte[tokenLength];
buffer.readBytes(token);
//Handle non-empty messages (CON, NON or ACK)
CoapMessage coapMessage;
if (MessageCode.isRequest(messageCode)) {
coapMessage = new CoapRequest(messageType, messageCode);
} else {
coapMessage = new CoapResponse(messageType, messageCode);
coapMessage.setMessageType(messageType);
}
coapMessage.setMessageID(messageID);
coapMessage.setToken(new Token(token));
//Decode and set the options
if (buffer.readableBytes() > 0) {
try {
setOptions(coapMessage, buffer);
} catch (OptionCodecException ex) {
ex.setMessageID(messageID);
ex.setToken(new Token(token));
//ex.setRemoteSocket(remoteSocket);
ex.setMessageType(messageType);
throw ex;
}
}
//The remaining bytes (if any) are the messages payload. If there is no payload, reader and writer index are
//at the same position (buf.readableBytes() == 0).
buffer.discardReadBytes();
try {
coapMessage.setContent(buffer);
} catch (IllegalArgumentException e) {
String warning = "Message code {} does not allow content. Ignore {} bytes.";
log.warn(warning, coapMessage.getMessageCode(), buffer.readableBytes());
}
log.info("Decoded Message: {}", coapMessage);
coapMessage.setSender(datagramPacket.sender());
list.add(coapMessage);
}
private void setOptions(CoapMessage coapMessage, ByteBuf buffer) throws OptionCodecException {
//Decode the options
int previousOptionNumber = 0;
int firstByte = buffer.readByte() & 0xFF;
while(firstByte != 0xFF && buffer.readableBytes() >= 0) {
log.debug("First byte: {} ({})", toBinaryString(firstByte), firstByte);
int optionDelta = (firstByte & 0xF0) >>> 4;
int optionLength = firstByte & 0x0F;
log.debug("temp. delta: {}, temp. length {}", optionDelta, optionLength);
if (optionDelta == 13) {
optionDelta += buffer.readByte() & 0xFF;
} else if (optionDelta == 14) {
optionDelta = 269 + ((buffer.readByte() & 0xFF) << 8) + (buffer.readByte() & 0xFF);
}
if (optionLength == 13) {
optionLength += buffer.readByte() & 0xFF;
} else if (optionLength == 14) {
optionLength = 269 + ((buffer.readByte() & 0xFF) << 8) + (buffer.readByte() & 0xFF);
}
log.info("Previous option: {}, Option delta: {}", previousOptionNumber, optionDelta);
int actualOptionNumber = previousOptionNumber + optionDelta;
log.info("Decode option no. {} with length of {} bytes.", actualOptionNumber, optionLength);
try {
byte[] optionValue = new byte[optionLength];
buffer.readBytes(optionValue);
switch(OptionValue.getType(actualOptionNumber)) {
case EMPTY: {
EmptyOptionValue value = new EmptyOptionValue(actualOptionNumber);
coapMessage.addOption(actualOptionNumber, value);
break;
}
case OPAQUE: {
OpaqueOptionValue value = new OpaqueOptionValue(actualOptionNumber, optionValue);
coapMessage.addOption(actualOptionNumber, value);
break;
}
case STRING: {
StringOptionValue value = new StringOptionValue(actualOptionNumber, optionValue, true);
coapMessage.addOption(actualOptionNumber, value);
break;
}
case UINT: {
UintOptionValue value = new UintOptionValue(actualOptionNumber, optionValue, true);
coapMessage.addOption(actualOptionNumber, value);
break;
}
default: {
log.error("This should never happen!");
throw new RuntimeException("This should never happen!");
}
}
} catch (IllegalArgumentException e) {
//failed option creation leads to an illegal argument exception
log.warn("Exception while decoding option!", e);
if (MessageCode.isResponse(coapMessage.getMessageCode())) {
//Malformed options in responses are silently ignored...
log.warn("Silently ignore malformed option no. {} in inbound response.", actualOptionNumber);
} else if (Option.isCritical(actualOptionNumber)) {
//Critical malformed options in requests cause an exception
throw new OptionCodecException(actualOptionNumber);
} else {
//Not critical malformed options in requests are silently ignored...
log.warn("Silently ignore elective option no. {} in inbound request.", actualOptionNumber);
}
}
previousOptionNumber = actualOptionNumber;
if (buffer.readableBytes() > 0) {
firstByte = buffer.readByte() & 0xFF;
} else {
// this is necessary if there is no payload and the last option is empty (e.g. UintOption with value 0)
firstByte = 0xFF;
}
log.debug("{} readable bytes remaining.", buffer.readableBytes());
}
}
private static String toBinaryString(int byteValue) {
StringBuilder buffer = new StringBuilder(8);
for(int i = 7; i >= 0; i--) {
if ((byteValue & (int) Math.pow(2, i)) > 0) {
buffer.append("1");
} else {
buffer.append("0");
}
}
return buffer.toString();
}
}

View File

@ -0,0 +1,189 @@
package com.fastbee.coap.codec;
import com.google.common.primitives.Ints;
import com.fastbee.coap.model.CoapMessage;
import com.fastbee.coap.model.MessageCode;
import com.fastbee.coap.model.options.OptionValue;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.socket.DatagramPacket;
import io.netty.handler.codec.MessageToMessageEncoder;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
@ChannelHandler.Sharable
@Slf4j
public class CoapMessageEncoder extends MessageToMessageEncoder<CoapMessage> {
/**
* The maximum option delta (65804)
*/
public static final int MAX_OPTION_DELTA = 65804;
/**
* The maximum option length (65804)
*/
public static final int MAX_OPTION_LENGTH = 65804;
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, CoapMessage coapMessage, List<Object> list) throws Exception {
log.info("CoapMessage to be encoded: {}", coapMessage);
// start encoding
ByteBuf encodedMessage = ByteBufAllocator.DEFAULT.buffer();
// encode HEADER and TOKEN
encodeHeader(encodedMessage, coapMessage);
log.debug("Encoded length of message (after HEADER + TOKEN): {}", encodedMessage.readableBytes());
if (coapMessage.getMessageCode() == MessageCode.EMPTY) {
encodedMessage = Unpooled.wrappedBuffer(Ints.toByteArray(encodedMessage.getInt(0) & 0xF0FFFFFF));
DatagramPacket datagramPacket = new DatagramPacket(encodedMessage, coapMessage.getSender());
list.add(datagramPacket);
return;
}
if (coapMessage.getAllOptions().size() == 0 && coapMessage.getContent().readableBytes() == 0) {
DatagramPacket datagramPacket = new DatagramPacket(encodedMessage, coapMessage.getSender());
list.add(datagramPacket);
return;
}
// encode OPTIONS (if any)
encodeOptions(encodedMessage, coapMessage);
log.debug("Encoded length of message (after OPTIONS): {}", encodedMessage.readableBytes());
// encode payload (if any)
if (coapMessage.getContent().readableBytes() > 0) {
// add END-OF-OPTIONS marker only if there is payload
encodedMessage.writeByte(255);
// add payload
encodedMessage = Unpooled.wrappedBuffer(encodedMessage, coapMessage.getContent());
log.debug("Encoded length of message (after CONTENT): {}", encodedMessage.readableBytes());
}
//发送响应
DatagramPacket datagramPacket = new DatagramPacket(encodedMessage, coapMessage.getSender());
list.add(datagramPacket);
}
protected void encodeHeader(ByteBuf buffer, CoapMessage coapMessage) {
byte[] token = coapMessage.getToken().getBytes();
int encodedHeader = ((coapMessage.getProtocolVersion() & 0x03) << 30)
| ((coapMessage.getMessageType() & 0x03) << 28)
| ((token.length & 0x0F) << 24)
| ((coapMessage.getMessageCode() & 0xFF) << 16)
| ((coapMessage.getMessageID() & 0xFFFF));
buffer.writeInt(encodedHeader);
if (log.isDebugEnabled()) {
StringBuilder binary = new StringBuilder(Integer.toBinaryString(encodedHeader));
while(binary.length() < 32) {
binary.insert(0, "0");
}
log.debug("Encoded Header: {}", binary.toString());
}
//Write token
if (token.length > 0) {
buffer.writeBytes(token);
}
}
protected void encodeOptions(ByteBuf buffer, CoapMessage coapMessage) throws OptionCodecException {
//Encode options one after the other and append buf option to the buf
int previousOptionNumber = 0;
for(int optionNumber : coapMessage.getAllOptions().keySet()) {
for(OptionValue optionValue : coapMessage.getOptions(optionNumber)) {
encodeOption(buffer, optionNumber, optionValue, previousOptionNumber);
previousOptionNumber = optionNumber;
}
}
}
protected void encodeOption(ByteBuf buffer, int optionNumber, OptionValue optionValue, int prevNumber)
throws OptionCodecException {
//The previous option number must be smaller or equal to the actual one
if (prevNumber > optionNumber) {
log.error("Previous option no. ({}) must not be larger then current option no ({})",
prevNumber, optionNumber);
throw new OptionCodecException(optionNumber);
}
int optionDelta = optionNumber - prevNumber;
int optionLength = optionValue.getValue().length;
if (optionLength > MAX_OPTION_LENGTH) {
log.error("Option no. {} exceeds maximum option length (actual: {}, max: {}).",
optionNumber, optionLength, MAX_OPTION_LENGTH);
throw new OptionCodecException(optionNumber);
}
if (optionDelta > MAX_OPTION_DELTA) {
log.error("Option delta exceeds maximum option delta (actual: {}, max: {})", optionDelta, MAX_OPTION_DELTA);
throw new OptionCodecException(optionNumber);
}
if (optionDelta < 13) {
//option delta < 13
if (optionLength < 13) {
buffer.writeByte(((optionDelta & 0xFF) << 4) | (optionLength & 0xFF));
} else if (optionLength < 269) {
buffer.writeByte(((optionDelta << 4) & 0xFF) | (13 & 0xFF));
buffer.writeByte((optionLength - 13) & 0xFF);
} else {
buffer.writeByte(((optionDelta << 4) & 0xFF) | (14 & 0xFF));
buffer.writeByte(((optionLength - 269) & 0xFF00) >>> 8);
buffer.writeByte((optionLength - 269) & 0xFF);
}
} else if (optionDelta < 269) {
//13 <= option delta < 269
if (optionLength < 13) {
buffer.writeByte(((13 & 0xFF) << 4) | (optionLength & 0xFF));
buffer.writeByte((optionDelta - 13) & 0xFF);
} else if (optionLength < 269) {
buffer.writeByte(((13 & 0xFF) << 4) | (13 & 0xFF));
buffer.writeByte((optionDelta - 13) & 0xFF);
buffer.writeByte((optionLength - 13) & 0xFF);
} else {
buffer.writeByte((13 & 0xFF) << 4 | (14 & 0xFF));
buffer.writeByte((optionDelta - 13) & 0xFF);
buffer.writeByte(((optionLength - 269) & 0xFF00) >>> 8);
buffer.writeByte((optionLength - 269) & 0xFF);
}
} else {
//269 <= option delta < 65805
if (optionLength < 13) {
buffer.writeByte(((14 & 0xFF) << 4) | (optionLength & 0xFF));
buffer.writeByte(((optionDelta - 269) & 0xFF00) >>> 8);
buffer.writeByte((optionDelta - 269) & 0xFF);
} else if (optionLength < 269) {
buffer.writeByte(((14 & 0xFF) << 4) | (13 & 0xFF));
buffer.writeByte(((optionDelta - 269) & 0xFF00) >>> 8);
buffer.writeByte((optionDelta - 269) & 0xFF);
buffer.writeByte((optionLength - 13) & 0xFF);
} else {
buffer.writeByte(((14 & 0xFF) << 4) | (14 & 0xFF));
buffer.writeByte(((optionDelta - 269) & 0xFF00) >>> 8);
buffer.writeByte((optionDelta - 269) & 0xFF);
buffer.writeByte(((optionLength - 269) & 0xFF00) >>> 8);
buffer.writeByte((optionLength - 269) & 0xFF);
}
}
//Write option value
buffer.writeBytes(optionValue.getValue());
log.debug("Encoded option no {} with value {}", optionNumber, optionValue.getDecodedValue());
log.debug("Encoded message length is now: {}", buffer.readableBytes());
}
}

View File

@ -0,0 +1,21 @@
package com.fastbee.coap.codec;
import lombok.Getter;
import lombok.Setter;
import java.net.InetSocketAddress;
@Getter
@Setter
public class HeaderDecodingException extends Exception{
private int messageID;
private InetSocketAddress remoteSocket;
public HeaderDecodingException(int messageID, InetSocketAddress remoteSocket, String message) {
super(message);
this.messageID = messageID;
this.remoteSocket = remoteSocket;
}
}

View File

@ -0,0 +1,29 @@
package com.fastbee.coap.codec;
import com.fastbee.coap.model.Token;
import com.fastbee.coap.model.options.Option;
import lombok.Getter;
import lombok.Setter;
import java.net.InetSocketAddress;
@Getter
@Setter
public class OptionCodecException extends Exception {
private static final String message = "Unsupported or misplaced critical option %s";
private int optionNumber;
private int messageID;
private Token token;
private InetSocketAddress remoteSocket;
private int messageType;
public OptionCodecException(int optionNumber) {
super();
this.optionNumber = optionNumber;
}
@Override
public String getMessage() {
return String.format(message, Option.asString(this.optionNumber));
}
}

View File

@ -0,0 +1,18 @@
package com.fastbee.coap.handler;
public abstract class AbstractResourceHandler implements ResourceHandler{
@Override
public String getTitle() {
return null;
}
@Override
public String getInterface() {
return null;
}
@Override
public String getResourceType() {
return null;
}
}

View File

@ -0,0 +1,37 @@
package com.fastbee.coap.handler;
import com.fastbee.coap.model.CoapMessage;
import com.fastbee.coap.model.CoapRequest;
import com.fastbee.coap.server.ResourceRegistry;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
@ChannelHandler.Sharable
@Slf4j
@Setter
@Getter
public class ReqDispatcher extends SimpleChannelInboundHandler<CoapMessage> {
private ResourceRegistry resourceRegistry;
private ChannelHandlerContext context;
public ReqDispatcher(ResourceRegistry resourceRegistry) {
this.resourceRegistry = resourceRegistry;
}
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, CoapMessage coapMessage) throws Exception {
log.debug("ReqDispatcher message: {}", coapMessage);
if (!(coapMessage instanceof CoapRequest)) {
log.info("remoteAddress:{}",coapMessage.getSender());
return ;
}
final CoapRequest coapRequest = (CoapRequest) coapMessage;
channelHandlerContext.writeAndFlush(resourceRegistry.respond(coapRequest));
}
}

View File

@ -0,0 +1,13 @@
package com.fastbee.coap.handler;
import com.google.common.util.concurrent.SettableFuture;
import com.fastbee.coap.model.CoapRequest;
import com.fastbee.coap.model.CoapResponse;
import java.net.InetSocketAddress;
public interface RequestConsumer {
public void processCoapRequest(SettableFuture<CoapResponse> responseFuture, CoapRequest coapRequest,
InetSocketAddress remoteSocket) throws Exception;
}

View File

@ -0,0 +1,36 @@
package com.fastbee.coap.handler;
import com.fastbee.coap.model.CoapMessage;
import com.fastbee.coap.model.CoapResponse;
public interface ResourceHandler {
/**
* The path served by this resource handler.
*/
public String getPath();
/**
* Detailed title for this path (or <code>null</code>). See http://datatracker.ietf.org/doc/rfc6690/
*/
public String getTitle();
/**
* Interface name of this resource (or <code>null</code>), can be an URL to a WADL file. See
* http://datatracker.ietf.org/doc/rfc6690/
*/
public String getInterface();
/**
* Resource type (or <code>null</code>). See http://datatracker.ietf.org/doc/rfc6690/
*/
public String getResourceType();
/**
* Generate the response for this request.
*
* @param request the request to serve
* @return the response
*/
public CoapResponse handle(CoapMessage request);
}

View File

@ -0,0 +1,46 @@
package com.fastbee.coap.handler;
import com.fastbee.coap.model.CoapMessage;
import com.fastbee.coap.model.CoapResponse;
import com.fastbee.coap.model.MessageCode;
import com.fastbee.coap.model.options.ContentFormat;
public class TimeResourceHandler extends AbstractResourceHandler{
private final String path;
public TimeResourceHandler(String path) {
this.path = path;
}
@Override
public String getPath() {
return this.path;
}
@Override
public CoapResponse handle(CoapMessage request) {
if (request.getMessageCode() == MessageCode.GET) {
CoapResponse coapResponse = new CoapResponse(request.getMessageType(),
MessageCode.CONTENT_205);
long time = System.currentTimeMillis() % 86400000;
long hours = time / 3600000;
long remainder = time % 3600000;
long minutes = remainder / 60000;
long seconds = (remainder % 60000) / 1000;
coapResponse.setContent(String.format("The current time is %02d:%02d:%02d", hours, minutes, seconds).getBytes(CoapMessage.CHARSET), ContentFormat.TEXT_PLAIN_UTF8);
coapResponse.setSender(request.getSender());
coapResponse.setMessageID(request.getMessageID());
coapResponse.setToken(request.getToken());
return coapResponse;
} else {
CoapResponse coapResponse = new CoapResponse(request.getMessageType(),
MessageCode.METHOD_NOT_ALLOWED_405);
String message = "Service does not allow " + request.getMessageCodeName() + " requests.";
coapResponse.setContent(message.getBytes(CoapMessage.CHARSET), ContentFormat.TEXT_PLAIN_UTF8);
coapResponse.setSender(request.getSender());
coapResponse.setMessageID(request.getMessageID());
coapResponse.setToken(request.getToken());
return coapResponse;
}
}
}

View File

@ -0,0 +1,74 @@
package com.fastbee.coap.model;
public enum BlockSize {
UNBOUND(-1, 65536),
SIZE_16(0, 16),
SIZE_32(1, 32),
SIZE_64(2, 64),
SIZE_128(3, 128),
SIZE_256(4, 256),
SIZE_512(5, 512),
SIZE_1024(6, 1024);
private final int encodedSize;
private final int decodedSize;
public static final int UNDEFINED = -1;
private static final int SZX_MIN = 0;
private static final int SZX_MAX = 6;
BlockSize(int encodedSize, int decodedSize) {
this.encodedSize = encodedSize;
this.decodedSize = decodedSize;
}
public int getSzx() {
return this.encodedSize;
}
public int getSize() {
return this.decodedSize;
}
public static int getSize(long szx) {
return getBlockSize(szx).getSize();
}
public static boolean isValid(long szx) {
return !(szx < SZX_MIN) && !(szx > SZX_MAX);
}
public static long min(long szx1, long szx2) throws IllegalArgumentException {
if (szx1 < UNDEFINED || szx1 > SZX_MAX || szx2 < UNDEFINED || szx2 > SZX_MAX) {
throw new IllegalArgumentException("SZX value out of allowed range.");
} else if (szx1 == BlockSize.UNDEFINED) {
return szx2;
} else if (szx2 == BlockSize.UNDEFINED) {
return szx1;
} else {
return Math.min(szx1, szx2);
}
}
public static BlockSize getBlockSize(long szx) throws IllegalArgumentException{
if (szx == BlockSize.UNDEFINED) {
return BlockSize.UNBOUND;
} else if (szx == 0) {
return SIZE_16;
} else if (szx == 1) {
return SIZE_32;
} else if (szx == 2) {
return SIZE_64;
} else if (szx == 3) {
return SIZE_128;
} else if (szx == 4) {
return SIZE_256;
} else if (szx == 5) {
return SIZE_512;
} else if (szx == 6) {
return SIZE_1024;
} else {
throw new IllegalArgumentException("Unsupported SZX value (Block Option): " + szx);
}
}
}

View File

@ -0,0 +1,494 @@
package com.fastbee.coap.model;
import com.google.common.base.Supplier;
import com.google.common.collect.Multimaps;
import com.google.common.collect.SetMultimap;
import com.google.common.primitives.Longs;
import com.fastbee.coap.model.options.*;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.Unpooled;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.net.InetSocketAddress;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.*;
import static com.fastbee.coap.model.MessageCode.EMPTY;
import static com.fastbee.coap.model.MessageType.*;
@Slf4j
@Data
public abstract class CoapMessage {
/**
* The CoAP protocol version (1)
*/
public static final int PROTOCOL_VERSION = 1;
/**
* The default character set for {@link CoapMessage}s (UTF-8)
*/
public static final Charset CHARSET = StandardCharsets.UTF_8;
/**
* Internal constant to indicate that the message ID was not yet set (-1)
*/
public static final int UNDEFINED_MESSAGE_ID = -1;
/**
* The maximum length of the byte array that backs the {@link Token} of {@link CoapMessage} (8)
*/
public static final int MAX_TOKEN_LENGTH = 8;
private static final String WRONG_OPTION_TYPE = "Option no. %d is no option of type %s";
private static final String OPTION_NOT_ALLOWED_WITH_MESSAGE_TYPE = "Option no. %d (%s) is not allowed with " +
"message type %s";
private static final String OPTION_ALREADY_SET = "Option no. %d is already set and is only allowed once per " +
"message";
private static final String DOES_NOT_ALLOW_CONTENT = "CoAP messages with code %s do not allow payload.";
private static final String EXCLUDES = "Already contained option no. %d excludes option no. %d";
private int messageType;
private int messageCode;
private int messageID;
private Token token;
InetSocketAddress sender;
protected SetMultimap<Integer, OptionValue> options;
private ByteBuf content;
protected CoapMessage(int messageType, int messageCode, int messageID, Token token)
throws IllegalArgumentException {
if (!MessageType.isMessageType(messageType))
throw new IllegalArgumentException("No. " + messageType + " is not corresponding to any message type.");
if (!MessageCode.isMessageCode(messageCode))
throw new IllegalArgumentException("No. " + messageCode + " is not corresponding to any message code.");
this.setMessageType(messageType);
this.setMessageCode(messageCode);
log.debug("Set Message Code to {} ({}).", MessageCode.asString(messageCode), messageCode);
this.setMessageID(messageID);
this.setToken(token);
this.options = Multimaps.newSetMultimap(new TreeMap<>(),
LinkedHashSetSupplier.getInstance());
this.content = ByteBufAllocator.DEFAULT.buffer();
log.debug("Created CoAP message: {}", this);
}
protected CoapMessage(int messageType, int messageCode) throws IllegalArgumentException {
this(messageType, messageCode, UNDEFINED_MESSAGE_ID, new Token(new byte[0]));
}
public static CoapMessage createEmptyReset(int messageID) throws IllegalArgumentException {
return new CoapMessage(RST, EMPTY, messageID, new Token(new byte[0])) {};
}
public static CoapMessage createEmptyAcknowledgement(int messageID) throws IllegalArgumentException {
return new CoapMessage(ACK, EMPTY, messageID, new Token(new byte[0])) {};
}
public static CoapMessage createPing(int messageID) throws IllegalArgumentException{
return new CoapMessage(CON, EMPTY, messageID, new Token(new byte[0])) {};
}
public void setMessageType(int messageType) throws IllegalArgumentException {
if (!MessageType.isMessageType(messageType))
throw new IllegalArgumentException("Invalid message type (" + messageType +
"). Only numbers 0-3 are allowed.");
this.messageType = messageType;
}
public boolean isPing() {
return this.messageCode == EMPTY && this.messageType == CON;
}
public boolean isRequest() {
return MessageCode.isRequest(this.getMessageCode());
}
public boolean isResponse() {
return MessageCode.isResponse(this.getMessageCode());
}
public void addOption(int optionNumber, OptionValue optionValue) throws IllegalArgumentException {
this.checkOptionPermission(optionNumber);
for(int containedOption : options.keySet()) {
if (Option.mutuallyExcludes(containedOption, optionNumber))
throw new IllegalArgumentException(String.format(EXCLUDES, containedOption, optionNumber));
}
options.put(optionNumber, optionValue);
log.debug("Added option (number: {}, value: {})", optionNumber, optionValue.toString());
}
protected void addStringOption(int optionNumber, String value) throws IllegalArgumentException {
if (!(OptionValue.getType(optionNumber) == OptionValue.Type.STRING))
throw new IllegalArgumentException(String.format(WRONG_OPTION_TYPE, optionNumber, OptionValue.Type.STRING));
//Add new option to option list
StringOptionValue option = new StringOptionValue(optionNumber, value);
addOption(optionNumber, option);
}
protected void addUintOption(int optionNumber, long value) throws IllegalArgumentException {
if (!(OptionValue.getType(optionNumber) == OptionValue.Type.UINT))
throw new IllegalArgumentException(String.format(WRONG_OPTION_TYPE, optionNumber, OptionValue.Type.STRING));
//Add new option to option list
byte[] byteValue = Longs.toByteArray(value);
int index = 0;
while(index < byteValue.length && byteValue[index] == 0) {
index++;
}
UintOptionValue option = new UintOptionValue(optionNumber, Arrays.copyOfRange(byteValue, index, byteValue.length));
addOption(optionNumber, option);
}
protected void addOpaqueOption(int optionNumber, byte[] value) throws IllegalArgumentException {
if (!(OptionValue.getType(optionNumber) == OptionValue.Type.OPAQUE))
throw new IllegalArgumentException(String.format(WRONG_OPTION_TYPE, optionNumber, OptionValue.Type.OPAQUE));
//Add new option to option list
OpaqueOptionValue option = new OpaqueOptionValue(optionNumber, value);
addOption(optionNumber, option);
}
protected void addEmptyOption(int optionNumber) throws IllegalArgumentException {
if (!(OptionValue.getType(optionNumber) == OptionValue.Type.EMPTY))
throw new IllegalArgumentException(String.format(WRONG_OPTION_TYPE, optionNumber, OptionValue.Type.EMPTY));
//Add new option to option list
options.put(optionNumber, new EmptyOptionValue(optionNumber));
log.debug("Added empty option (number: {})", optionNumber);
}
public void removeOptions(int optionNumber) {
int result = options.removeAll(optionNumber).size();
log.debug("Removed {} options with number {}.", result, optionNumber);
}
private void checkOptionPermission(int optionNumber) throws IllegalArgumentException {
Option.Occurence permittedOccurence = Option.getPermittedOccurrence(optionNumber, this.messageCode);
if (permittedOccurence == Option.Occurence.NONE) {
throw new IllegalArgumentException(String.format(OPTION_NOT_ALLOWED_WITH_MESSAGE_TYPE,
optionNumber, Option.asString(optionNumber), this.getMessageCodeName()));
} else if (options.containsKey(optionNumber) && permittedOccurence == Option.Occurence.ONCE) {
throw new IllegalArgumentException(String.format(OPTION_ALREADY_SET, optionNumber));
}
}
private static long extractBits(final long value, final int bits, final int offset) {
final long shifted = value >>> offset;
final long masked = (1L << bits) - 1L;
return shifted & masked;
}
public int getProtocolVersion() {
return PROTOCOL_VERSION;
}
public String getMessageTypeName() {
return MessageType.asString(this.messageType);
}
public String getMessageCodeName() {
return MessageCode.asString(this.messageCode);
}
public long getContentFormat() {
if (options.containsKey(Option.CONTENT_FORMAT)) {
return ((UintOptionValue) options.get(Option.CONTENT_FORMAT).iterator().next()).getDecodedValue();
} else {
return ContentFormat.UNDEFINED;
}
}
public void setObserve(long value) {
try {
this.removeOptions(Option.OBSERVE);
value = value & 0xFFFFFF;
this.addUintOption(Option.OBSERVE, value);
}
catch (IllegalArgumentException e) {
this.removeOptions(Option.OBSERVE);
log.error("This should never happen.", e);
}
}
public long getObserve() {
if (!options.containsKey(Option.OBSERVE)) {
return UintOptionValue.UNDEFINED;
} else {
return (long) options.get(Option.OBSERVE).iterator().next().getDecodedValue();
}
}
public void setSize2(long size2) throws IllegalArgumentException{
this.options.removeAll(Option.SIZE_2);
this.addUintOption(Option.SIZE_2, size2);
}
public long getSize2() {
if (options.containsKey(Option.SIZE_2)) {
return ((UintOptionValue) options.get(Option.SIZE_2).iterator().next()).getDecodedValue();
} else {
return UintOptionValue.UNDEFINED;
}
}
public void setSize1(long size1) throws IllegalArgumentException{
this.options.removeAll(Option.SIZE_1);
this.addUintOption(Option.SIZE_1, size1);
}
public long getSize1() {
if (options.containsKey(Option.SIZE_1)) {
return ((UintOptionValue) options.get(Option.SIZE_1).iterator().next()).getDecodedValue();
} else {
return UintOptionValue.UNDEFINED;
}
}
public byte[] getEndpointID1() {
Set<OptionValue> values = getOptions(Option.ENDPOINT_ID_1);
if (values.isEmpty()) {
return null;
} else {
return values.iterator().next().getValue();
}
}
public void setEndpointID1() {
this.setEndpointID1(new byte[0]);
}
public void setEndpointID1(byte[] value) {
try {
this.removeOptions(Option.ENDPOINT_ID_1);
this.addOpaqueOption(Option.ENDPOINT_ID_1, value);
} catch (IllegalArgumentException e) {
this.removeOptions(Option.ENDPOINT_ID_1);
log.error("This should never happen.", e);
}
}
public byte[] getEndpointID2() {
Set<OptionValue> values = getOptions(Option.ENDPOINT_ID_2);
if (values.isEmpty()) {
return null;
} else {
return values.iterator().next().getValue();
}
}
public void setEndpointID2(byte[] value) {
try {
this.removeOptions(Option.ENDPOINT_ID_2);
this.addOpaqueOption(Option.ENDPOINT_ID_2, value);
} catch (IllegalArgumentException e) {
this.removeOptions(Option.ENDPOINT_ID_2);
log.error("This should never happen.", e);
}
}
public void setContent(ByteBuf content) throws IllegalArgumentException {
if (!(MessageCode.allowsContent(this.messageCode)) && content.readableBytes() > 0) {
throw new IllegalArgumentException(String.format(DOES_NOT_ALLOW_CONTENT, this.getMessageCodeName()));
}
this.content = content;
}
public void setContent(ByteBuf content, long contentFormat) throws IllegalArgumentException {
try {
this.addUintOption(Option.CONTENT_FORMAT, contentFormat);
setContent(content);
} catch (IllegalArgumentException e) {
this.content = ByteBufAllocator.DEFAULT.buffer();
this.removeOptions(Option.CONTENT_FORMAT);
throw e;
}
}
public void setContent(byte[] content) throws IllegalArgumentException {
setContent(Unpooled.wrappedBuffer(content));
}
public void setContent(byte[] content, long contentFormat) throws IllegalArgumentException {
setContent(Unpooled.wrappedBuffer(content), contentFormat);
}
public ByteBuf getContent() {
return this.content;
}
public byte[] getContentAsByteArray() {
byte[] result = new byte[this.getContentLength()];
this.getContent().readBytes(result, 0, this.getContentLength());
return result;
}
public int getContentLength() {
return this.content.readableBytes();
}
public SetMultimap<Integer, OptionValue> getAllOptions() {
return this.options;
}
public void setAllOptions (SetMultimap<Integer, OptionValue> options) {
this.options = options;
}
public Set<OptionValue> getOptions(int optionNumber) {
return this.options.get(optionNumber);
}
public boolean containsOption(int optionNumber) {
return !getOptions(optionNumber).isEmpty();
}
@Override
public int hashCode() {
return toString().hashCode() + content.hashCode();
}
@Override
public boolean equals(Object object) {
if (!(object instanceof CoapMessage)) {
log.error("Different type");
return false;
}
CoapMessage other = (CoapMessage) object;
//Check header fields
if (this.getProtocolVersion() != other.getProtocolVersion())
return false;
if (this.getMessageType() != other.getMessageType())
return false;
if (this.getMessageCode() != other.getMessageCode())
return false;
if (this.getMessageID() != other.getMessageID())
return false;
if (!this.getToken().equals(other.getToken()))
return false;
//Iterators iterate over the contained options
Iterator<Map.Entry<Integer, OptionValue>> iterator1 = this.getAllOptions().entries().iterator();
Iterator<Map.Entry<Integer, OptionValue>> iterator2 = other.getAllOptions().entries().iterator();
//Check if both CoAP Messages contain the same options in the same order
while(iterator1.hasNext()) {
//Check if iterator2 has no more options while iterator1 has at least one more
if (!iterator2.hasNext())
return false;
Map.Entry<Integer, OptionValue> entry1 = iterator1.next();
Map.Entry<Integer, OptionValue> entry2 = iterator2.next();
if (!entry1.getKey().equals(entry2.getKey()))
return false;
if (!entry1.getValue().equals(entry2.getValue()))
return false;
}
//Check if iterator2 has at least one more option while iterator1 has no more
if (iterator2.hasNext())
return false;
//Check content
return this.getContent().equals(other.getContent());
}
@Override
public String toString() {
StringBuilder result = new StringBuilder();
//Header + Token
result.append("[Header: (V) ").append(getProtocolVersion())
.append(", (T) ").append(getMessageTypeName())
.append(", (TKL) ").append(token.getBytes().length)
.append(", (C) ").append(getMessageCodeName())
.append(", (ID) ").append(getMessageID())
.append(" | (Token) ").append(token).append(" | ");
//Options
result.append("Options:");
for(int optionNumber : getAllOptions().keySet()) {
result.append(" (No. ").append(optionNumber).append(") ");
Iterator<OptionValue> iterator = this.getOptions(optionNumber).iterator();
OptionValue<?> optionValue = iterator.next();
result.append(optionValue.toString());
while(iterator.hasNext())
result.append(" / ").append(iterator.next().toString());
}
result.append(" | ");
//Content
result.append("Content: ");
long payloadLength = getContent().readableBytes();
if (payloadLength == 0)
result.append("<no content>]");
else
result.append(getContent().toString(0, Math.min(getContent().readableBytes(), 20), CoapMessage.CHARSET)).append("... ( ").append(payloadLength).append(" bytes)]");
return result.toString();
}
public void setMessageCode(int messageCode) throws IllegalArgumentException {
if (!MessageCode.isMessageCode(messageCode))
throw new IllegalArgumentException("Invalid message code no. " + messageCode);
this.messageCode = messageCode;
}
private final static class LinkedHashSetSupplier implements Supplier<LinkedHashSet<OptionValue>> {
public static LinkedHashSetSupplier instance = new LinkedHashSetSupplier();
private LinkedHashSetSupplier() {}
public static LinkedHashSetSupplier getInstance() {
return instance;
}
@Override
public LinkedHashSet<OptionValue> get() {
return new LinkedHashSet<>();
}
}
}

View File

@ -0,0 +1,322 @@
package com.fastbee.coap.model;
import com.fastbee.coap.model.options.*;
import lombok.extern.slf4j.Slf4j;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Locale;
import java.util.Set;
import static com.fastbee.coap.model.MessageType.CON;
import static com.fastbee.coap.model.MessageType.NON;
@Slf4j
public class CoapRequest extends CoapMessage {
private static final String NO_REQUEST_TYPE = "Message type %d is not a suitable type for requests (only CON and NON)!";
private static final String NO_REQUEST_CODE = "Message code %d is not a request code!";
private static final String URI_SCHEME = "URI scheme must be set to \"coap\" (but given URI is: %s)!";
private static final String URI_FRAGMENT = "URI must not have a fragment (but given URI is: %s)!";
public CoapRequest(int messageType, int messageCode, URI targetUri) throws IllegalArgumentException {
this(messageType, messageCode, targetUri, false);
}
public CoapRequest(int messageType, int messageCode, URI targetUri, boolean useProxy)
throws IllegalArgumentException {
this(messageType, messageCode);
if (useProxy) {
setProxyURIOption(targetUri);
} else {
setTargetUriOptions(targetUri);
}
log.debug("New request created: {}.", this);
}
public CoapRequest(int messageType, int messageCode) throws IllegalArgumentException {
super(messageType, messageCode);
if (messageType < CON || messageType > NON) {
throw new IllegalArgumentException(String.format(NO_REQUEST_TYPE, messageType));
}
if (!MessageCode.isRequest(messageCode)) {
throw new IllegalArgumentException(String.format(NO_REQUEST_CODE, messageCode));
}
}
private void setProxyURIOption(URI targetUri) throws IllegalArgumentException {
this.addStringOption(Option.PROXY_URI, targetUri.toString());
}
private void setTargetUriOptions(URI targetUri) throws IllegalArgumentException {
targetUri = targetUri.normalize();
//URI must be absolute and thus contain a scheme part (must be one of "coap" or "coaps")
String scheme = targetUri.getScheme();
if (scheme == null) {
throw new IllegalArgumentException(String.format(URI_SCHEME, targetUri.toString()));
}
scheme = scheme.toLowerCase(Locale.ENGLISH);
if (!(scheme.equals("coap"))) {
throw new IllegalArgumentException(String.format(URI_SCHEME, targetUri.toString()));
}
//Target URI must not have fragment part
if (targetUri.getFragment() != null) {
throw new IllegalArgumentException(String.format(URI_FRAGMENT, targetUri.toString()));
}
//Create target URI options
if (!(OptionValue.isDefaultValue(Option.URI_HOST, targetUri.getHost().getBytes(CoapMessage.CHARSET)))) {
addUriHostOption(targetUri.getHost());
}
if (targetUri.getPort() != -1 && targetUri.getPort() != OptionValue.URI_PORT_DEFAULT) {
addUriPortOption(targetUri.getPort());
}
addUriPathOptions(targetUri.getPath());
addUriQueryOptions(targetUri.getQuery());
}
private void addUriQueryOptions(String uriQuery) throws IllegalArgumentException {
if (uriQuery != null) {
for(String queryComponent : uriQuery.split("&")) {
this.addStringOption(Option.URI_QUERY, queryComponent);
log.debug("Added URI query option for {}", queryComponent);
}
}
}
private void addUriPathOptions(String uriPath) throws IllegalArgumentException {
if (uriPath != null) {
//Path must not start with "/" to be further processed
if (uriPath.startsWith("/")) {
uriPath = uriPath.substring(1);
}
if ("".equals(uriPath)) {
return;
}
for(String pathComponent : uriPath.split("/")) {
this.addStringOption(Option.URI_PATH, pathComponent);
log.debug("Added URI path option for {}", pathComponent);
}
}
}
private void addUriPortOption(int uriPort) throws IllegalArgumentException {
if (uriPort > 0 && uriPort != OptionValue.URI_PORT_DEFAULT) {
this.addUintOption(Option.URI_PORT, uriPort);
}
}
private void addUriHostOption(String uriHost) throws IllegalArgumentException {
addStringOption(Option.URI_HOST, uriHost);
}
public void setIfMatch(byte[]... etags) throws IllegalArgumentException {
setOpaqueOptions(Option.IF_MATCH, etags);
}
public Set<byte[]> getIfMatch() {
Set<OptionValue> ifMatchOptionValues = options.get(Option.IF_MATCH);
Set<byte[]> result = new HashSet<>(ifMatchOptionValues.size());
for (OptionValue ifMatchOptionValue : ifMatchOptionValues)
result.add(((OpaqueOptionValue) ifMatchOptionValue).getDecodedValue());
return result;
}
public String getUriHost() {
if (options.containsKey(Option.URI_HOST))
return ((StringOptionValue) options.get(Option.URI_HOST).iterator().next()).getDecodedValue();
return null;
}
public void setEtags(byte[]... etags) throws IllegalArgumentException {
setOpaqueOptions(Option.ETAG, etags);
}
private void setOpaqueOptions(int optionNumber, byte[]... etags) throws IllegalArgumentException {
this.removeOptions(optionNumber);
try{
for(byte[] etag : etags) {
this.addOpaqueOption(optionNumber, etag);
}
} catch(IllegalArgumentException e) {
this.removeOptions(optionNumber);
throw e;
}
}
public Set<byte[]> getEtags() {
Set<byte[]> result = new HashSet<>();
for (OptionValue optionValue : options.get(Option.ETAG))
result.add(((OpaqueOptionValue) optionValue).getDecodedValue());
return result;
}
public boolean setIfNonMatch() {
if (options.containsKey(Option.IF_NONE_MATCH))
return true;
try{
this.addEmptyOption(Option.IF_NONE_MATCH);
return true;
}
catch(IllegalArgumentException e) {
return false;
}
}
public boolean isIfNonMatchSet() {
return options.containsKey(Option.IF_NONE_MATCH);
}
public long getUriPort() {
if (options.containsKey(Option.URI_PORT))
return ((UintOptionValue) options.get(Option.URI_PORT).iterator().next()).getDecodedValue();
return OptionValue.URI_PORT_DEFAULT;
}
public String getUriPath() {
String result = "/";
Iterator<OptionValue> iterator = options.get(Option.URI_PATH).iterator();
if (iterator.hasNext())
result += ((StringOptionValue) iterator.next()).getDecodedValue();
while(iterator.hasNext())
result += ("/" + ((StringOptionValue) iterator.next()).getDecodedValue());
return result;
}
public String getUriQuery() {
String result = "";
if (options.containsKey(Option.URI_QUERY)) {
Iterator<OptionValue> iterator = options.get(Option.URI_QUERY).iterator();
result += (((StringOptionValue) iterator.next()).getDecodedValue());
while(iterator.hasNext())
result += ("&" + ((StringOptionValue) iterator.next()).getDecodedValue());
}
return result;
}
public String getUriQueryParameterValue(String parameter) {
if (!parameter.endsWith("="))
parameter += "=";
for(OptionValue optionValue : options.get(Option.URI_QUERY)) {
String value = ((StringOptionValue) optionValue).getDecodedValue();
if (value.startsWith(parameter))
return value.substring(parameter.length());
}
return null;
}
public void setAccept(long... contentFormatNumbers) throws IllegalArgumentException {
options.removeAll(Option.ACCEPT);
try{
for(long contentFormatNumber : contentFormatNumbers)
this.addUintOption(Option.ACCEPT, contentFormatNumber);
}
catch (IllegalArgumentException e) {
options.removeAll(Option.ACCEPT);
throw e;
}
}
public Set<Long> getAcceptedContentFormats() {
Set<Long> result = new HashSet<>();
for(OptionValue optionValue : options.get(Option.ACCEPT))
result.add(((UintOptionValue) optionValue).getDecodedValue());
return result;
}
public URI getProxyURI() throws URISyntaxException {
if (options.containsKey(Option.PROXY_URI)) {
OptionValue proxyUriOptionValue = options.get(Option.PROXY_URI).iterator().next();
return new URI(((StringOptionValue) proxyUriOptionValue).getDecodedValue());
}
if (options.get(Option.PROXY_SCHEME).size() == 1) {
OptionValue proxySchemeOptionValue = options.get(Option.PROXY_SCHEME).iterator().next();
String scheme = ((StringOptionValue) proxySchemeOptionValue).getDecodedValue();
String uriHost = getUriHost();
OptionValue uriPortOptionValue = options.get(Option.URI_PORT).iterator().next();
int uriPort = ((UintOptionValue) uriPortOptionValue).getDecodedValue().intValue();
String uriPath = getUriPath();
String uriQuery = getUriQuery();
return new URI(scheme, null, uriHost, uriPort == OptionValue.URI_PORT_DEFAULT ? -1 : uriPort, uriPath,
uriQuery, null);
}
return null;
}
public void setPreferredBlock2Size(BlockSize size) {
this.setBlock2(0, size.getSzx());
}
public void setBlock2(long number, long szx) throws IllegalArgumentException{
try {
this.removeOptions(Option.BLOCK_2);
if (number > 1048575 || !(BlockSize.isValid(szx))) {
String error = "Invalid value for BLOCK2 option (NUM: " + number + ", SZX: " + szx + ")";
throw new IllegalArgumentException(error);
}
this.addUintOption(Option.BLOCK_2, ((number & 0xFFFFF) << 4) + szx);
} catch (IllegalArgumentException e) {
log.error("This should never happen.", e);
}
}
public void setPreferredBlock1Size(BlockSize size) {
this.setBlock1(0, false, size.getSzx());
}
public void setBlock1(long number, boolean more, long szx) throws IllegalArgumentException{
try {
this.removeOptions(Option.BLOCK_1);
if (number > 1048575 || !(BlockSize.isValid(szx))) {
String error = "Invalid value for BLOCK1 option (NUM: " + number + ", SZX: " + szx + ")";
throw new IllegalArgumentException(error);
}
this.addUintOption(Option.BLOCK_1, ((number & 0xFFFFF) << 4) + ((more ? 1 : 0) << 3) + szx);
} catch (IllegalArgumentException e) {
log.error("This should never happen.", e);
}
}
public boolean isObservationRequest() {
return(!options.get(Option.OBSERVE).isEmpty());
}
}

View File

@ -0,0 +1,182 @@
package com.fastbee.coap.model;
import com.fastbee.coap.model.options.*;
import lombok.extern.slf4j.Slf4j;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Iterator;
@Slf4j
public class CoapResponse extends CoapMessage {
private static final String NO_ERRROR_CODE = "Code no. %s is no error code!";
public CoapResponse(int messageType, int messageCode) throws IllegalArgumentException {
super(messageType, messageCode);
if (!MessageCode.isResponse(messageCode))
throw new IllegalArgumentException("Message code no." + messageCode + " is no response code.");
}
public static CoapResponse createErrorResponse(int messageType, int messageCode, String content)
throws IllegalArgumentException{
if (!MessageCode.isErrorMessage(messageCode)) {
throw new IllegalArgumentException(String.format(NO_ERRROR_CODE, MessageCode.asString(messageCode)));
}
CoapResponse errorResponse = new CoapResponse(messageType, messageCode);
errorResponse.setContent(content.getBytes(CoapMessage.CHARSET), ContentFormat.TEXT_PLAIN_UTF8);
return errorResponse;
}
public static CoapResponse createErrorResponse(int messageType, int messageCode, Throwable throwable)
throws IllegalArgumentException{
StringWriter stringWriter = new StringWriter();
throwable.printStackTrace(new PrintWriter(stringWriter));
return createErrorResponse(messageType, messageCode, stringWriter.toString());
}
public boolean isErrorResponse() {
return MessageCode.isErrorMessage(this.getMessageCode());
}
public void setEtag(byte[] etag) throws IllegalArgumentException {
this.addOpaqueOption(Option.ETAG, etag);
}
public byte[] getEtag() {
if (options.containsKey(Option.ETAG)) {
return ((OpaqueOptionValue) options.get(Option.ETAG).iterator().next()).getDecodedValue();
} else {
return null;
}
}
public void setObserve() {
this.setObserve(System.currentTimeMillis() % ResourceStatusAge.MODULUS);
}
public void setPreferredBlock2Size(BlockSize block2Size) {
if (BlockSize.UNBOUND == block2Size || block2Size == null) {
this.removeOptions(Option.BLOCK_2);
} else {
this.setBlock2(0, false, block2Size.getSzx());
}
}
public void setBlock2(long number, boolean more, long szx) throws IllegalArgumentException{
try {
this.removeOptions(Option.BLOCK_2);
if (number > 1048575) {
throw new IllegalArgumentException("Max. BLOCK2NUM is 1048575");
}
//long more = ((more) ? 1 : 0) << 3;
this.addUintOption(Option.BLOCK_2, ((number & 0xFFFFF) << 4) + ((more ? 1 : 0) << 3) + szx);
} catch (IllegalArgumentException e) {
this.removeOptions(Option.BLOCK_2);
log.error("This should never happen.", e);
}
}
public void setBlock1(long number, long szx) throws IllegalArgumentException{
try {
this.removeOptions(Option.BLOCK_1);
if (number > 1048575) {
throw new IllegalArgumentException("Max. BLOCK1NUM is 1048575");
}
//long more = ((more) ? 1 : 0) << 3;
this.addUintOption(Option.BLOCK_1, ((number & 0xFFFFF) << 4) + (1 << 3) + szx);
} catch (IllegalArgumentException e) {
this.removeOptions(Option.BLOCK_1);
log.error("This should never happen.", e);
}
}
public boolean isUpdateNotification() {
return this.getObserve() != UintOptionValue.UNDEFINED;
}
public void setLocationURI(URI locationURI) throws IllegalArgumentException {
options.removeAll(Option.LOCATION_PATH);
options.removeAll(Option.LOCATION_QUERY);
String locationPath = locationURI.getRawPath();
String locationQuery = locationURI.getRawQuery();
try{
if (locationPath != null) {
//Path must not start with "/" to be further processed
if (locationPath.startsWith("/"))
locationPath = locationPath.substring(1);
for(String pathComponent : locationPath.split("/"))
this.addStringOption(Option.LOCATION_PATH, pathComponent);
}
if (locationQuery != null) {
for(String queryComponent : locationQuery.split("&"))
this.addStringOption(Option.LOCATION_QUERY, queryComponent);
}
} catch(IllegalArgumentException ex) {
options.removeAll(Option.LOCATION_PATH);
options.removeAll(Option.LOCATION_QUERY);
throw ex;
}
}
public URI getLocationURI() throws URISyntaxException {
//Reconstruct path
StringBuilder locationPath = new StringBuilder();
if (options.containsKey(Option.LOCATION_PATH)) {
for (OptionValue optionValue : options.get(Option.LOCATION_PATH))
locationPath.append("/").append(((StringOptionValue) optionValue).getDecodedValue());
}
//Reconstruct query
StringBuilder locationQuery = new StringBuilder();
if (options.containsKey(Option.LOCATION_QUERY)) {
Iterator<OptionValue> queryComponentIterator = options.get(Option.LOCATION_QUERY).iterator();
locationQuery.append(((StringOptionValue) queryComponentIterator.next()).getDecodedValue());
while(queryComponentIterator.hasNext())
locationQuery.append("&")
.append(((StringOptionValue) queryComponentIterator.next()).getDecodedValue());
}
if (locationPath.length() == 0 && locationQuery.length() == 0)
return null;
return new URI(null, null, null, (int) UintOptionValue.UNDEFINED, locationPath.toString(),
locationQuery.toString(), null);
}
public void setMaxAge(long maxAge) {
try {
this.options.removeAll(Option.MAX_AGE);
this.addUintOption(Option.MAX_AGE, maxAge);
} catch (IllegalArgumentException e) {
log.error("This should never happen.", e);
}
}
public long getMaxAge() {
if (options.containsKey(Option.MAX_AGE)) {
return ((UintOptionValue) options.get(Option.MAX_AGE).iterator().next()).getDecodedValue();
} else {
return OptionValue.MAX_AGE_DEFAULT;
}
}
}

View File

@ -0,0 +1,247 @@
package com.fastbee.coap.model;
import com.google.common.collect.ImmutableMap;
import java.util.HashMap;
public abstract class MessageCode {
/**
* Corresponds to Code 0
*/
public static final int EMPTY = 0;
/**
* Corresponds to Request Code 1
*/
public static final int GET = 1;
/**
* Corresponds to Request Code 2
*/
public static final int POST = 2;
/**
* Corresponds to Request Code 3
*/
public static final int PUT =3;
/**
* Corresponds to Request Code 4
*/
public static final int DELETE = 4;
/**
* Corresponds to Response Code 65
*/
public static final int CREATED_201 = 65;
/**
* Corresponds to Response Code 66
*/
public static final int DELETED_202 = 66;
/**
* Corresponds to Response Code 67
*/
public static final int VALID_203 = 67;
/**
* Corresponds to Response Code 68
*/
public static final int CHANGED_204 = 68;
/**
* Corresponds to Response Code 69
*/
public static final int CONTENT_205 = 69;
/**
* Corresponds to Response Code 95
*/
public static final int CONTINUE_231 = 95;
/**
* Corresponds to Response Code 128
*/
public static final int BAD_REQUEST_400 = 128;
/**
* Corresponds to Response Code 129
*/
public static final int UNAUTHORIZED_401 = 129;
/**
* Corresponds to Response Code 130
*/
public static final int BAD_OPTION_402 = 130;
/**
* Corresponds to Response Code 131
*/
public static final int FORBIDDEN_403 = 131;
/**
* Corresponds to Response Code 132
*/
public static final int NOT_FOUND_404 = 132;
/**
* Corresponds to Response Code 133
*/
public static final int METHOD_NOT_ALLOWED_405 = 133;
/**
* Corresponds to Response Code 134
*/
public static final int NOT_ACCEPTABLE_406 = 134;
/**
* Corresponds to Response Code 136
*/
public static final int REQUEST_ENTITY_INCOMPLETE_408 = 136;
/**
* Corresponds to Response Code 140
*/
public static final int PRECONDITION_FAILED_412 = 140;
/**
* Corresponds to Response Code 141
*/
public static final int REQUEST_ENTITY_TOO_LARGE_413 = 141;
/**
* Corresponds to Response Code 143
*/
public static final int UNSUPPORTED_CONTENT_FORMAT_415 = 143;
/**
* Corresponds to Response Code 160
*/
public static final int INTERNAL_SERVER_ERROR_500 = 160;
/**
* Corresponds to Response Code 161
*/
public static final int NOT_IMPLEMENTED_501 = 161;
/**
* Corresponds to Response Code 162
*/
public static final int BAD_GATEWAY_502 = 162;
/**
* Corresponds to Response Code 163
*/
public static final int SERVICE_UNAVAILABLE_503 = 163;
/**
* Corresponds to Response Code 164
*/
public static final int GATEWAY_TIMEOUT_504 = 164;
/**
* Corresponds to Response Code 165
*/
public static final int PROXYING_NOT_SUPPORTED_505 = 165;
private static final HashMap<Integer, String> MESSAGE_CODES = new HashMap<>();
static {
MESSAGE_CODES.putAll(ImmutableMap.<Integer, String>builder()
.put(EMPTY, "EMPTY (" + EMPTY + ")")
.put(GET, "GET (" + GET + ")")
.put(POST, "POST (" + POST + ")")
.put(PUT, "PUT (" + PUT + ")")
.put(DELETE, "DELETE (" + DELETE + ")")
.put(CREATED_201, "CREATED (" + CREATED_201 + ")")
.put(DELETED_202, "DELETED (" + DELETED_202 + ")")
.put(VALID_203, "VALID (" + VALID_203 + ")")
.put(CHANGED_204, "CHANGED (" + CHANGED_204 + ")")
.put(CONTENT_205, "CONTENT (" + CONTENT_205 + ")")
.put(CONTINUE_231, "CONTINUE (" + CONTINUE_231 + ")")
.put(BAD_REQUEST_400, "BAD REQUEST (" + BAD_REQUEST_400 + ")")
.put(UNAUTHORIZED_401, "UNAUTHORIZED (" + UNAUTHORIZED_401 + ")")
.put(BAD_OPTION_402, "BAD OPTION (" + BAD_OPTION_402 + ")")
.put(FORBIDDEN_403, "FORBIDDEN (" + FORBIDDEN_403 + ")")
.put(NOT_FOUND_404, "NOT FOUND (" + NOT_FOUND_404 + ")")
.put(METHOD_NOT_ALLOWED_405, "METHOD NOT ALLOWED (" + METHOD_NOT_ALLOWED_405 + ")")
.put(NOT_ACCEPTABLE_406, "NOT ACCEPTABLE (" + NOT_ACCEPTABLE_406 + ")")
.put(REQUEST_ENTITY_INCOMPLETE_408, "REQUEST ENTITY INCOMPLETE (" + REQUEST_ENTITY_INCOMPLETE_408 + ")")
.put(PRECONDITION_FAILED_412, "PRECONDITION FAILED (" + PRECONDITION_FAILED_412 + ")")
.put(REQUEST_ENTITY_TOO_LARGE_413, "REQUEST ENTITY TOO LARGE (" + REQUEST_ENTITY_TOO_LARGE_413 + ")")
.put(UNSUPPORTED_CONTENT_FORMAT_415, "UNSUPPORTED CONTENT FORMAT (" + UNSUPPORTED_CONTENT_FORMAT_415 + ")")
.put(INTERNAL_SERVER_ERROR_500, "INTERNAL SERVER ERROR (" + INTERNAL_SERVER_ERROR_500 + ")")
.put(NOT_IMPLEMENTED_501, "NOT IMPLEMENTED (" + NOT_IMPLEMENTED_501 + ")")
.put(BAD_GATEWAY_502, "BAD GATEWAY (" + BAD_GATEWAY_502 + ")")
.put(SERVICE_UNAVAILABLE_503, "SERVICE UNAVAILABLE (" + SERVICE_UNAVAILABLE_503 + ")")
.put(GATEWAY_TIMEOUT_504, "GATEWAY TIMEOUT (" + GATEWAY_TIMEOUT_504 + ")")
.put(PROXYING_NOT_SUPPORTED_505, "PROXYING NOT SUPPORTED (" + PROXYING_NOT_SUPPORTED_505 + ")")
.build()
);
}
public static String asString(int messageCode) {
String result = MESSAGE_CODES.get(messageCode);
return result == null ? "UNKOWN (" + messageCode + ")" : result;
}
/**
* Returns <code>true</code> if the given number corresponds to a valid
* {@link MessageCode} and <code>false</code> otherwise
*
* @param number the number to check for being a valid {@link MessageCode}
*
* @return <code>true</code> if the given number corresponds to a valid
* {@link MessageCode} and <code>false</code> otherwise
*/
public static boolean isMessageCode(int number) {
return MESSAGE_CODES.containsKey(number);
}
/**
* This method indicates whether the given number refers to a {@link MessageCode} for {@link CoapRequest}s.
*
* <b>Note:</b> Messages with {@link MessageCode#EMPTY} are considered neither a response nor a request
*
* @return <code>true</code> in case of a request code, <code>false</code> otherwise.
*
*/
public static boolean isRequest(int messageCode) {
return (messageCode > 0 && messageCode < 5);
}
/**
* This method indicates whether the given number refers to a {@link MessageCode} for {@link CoapResponse}s.
*
* <b>Note:</b> Messages with {@link MessageCode#EMPTY} are considered neither a response nor a request
*
* @return <code>true</code> in case of a response code, <code>false</code> otherwise.
*
*/
public static boolean isResponse(int messageCode) {
return messageCode >= 5;
}
/**
* This method indicates whether the given number refers to a {@link MessageCode} for {@link CoapResponse}s
* indicating an error.
*
* @return <code>true</code> in case of an error response code, <code>false</code> otherwise.
*
*/
public static boolean isErrorMessage(int codeNumber) {
return (codeNumber >= 128);
}
/**
* This method indicates whether a message may contain payload
* @return <code>true</code> if payload is allowed, <code>false</code> otherwise
*/
public static boolean allowsContent(int codeNumber) {
return !(codeNumber == GET || codeNumber == DELETE);
}
}

View File

@ -0,0 +1,49 @@
package com.fastbee.coap.model;
import com.google.common.collect.ImmutableMap;
import java.util.HashMap;
public abstract class MessageType {
/**
* Corresponds to Message EventType 0
*/
public static final int CON = 0;
/**
* Corresponds to Message EventType 1
*/
public static final int NON = 1;
/**
* Corresponds to Message EventType 2
*/
public static final int ACK = 2;
/**
* Corresponds to Message EventType 3
*/
public static final int RST = 3;
private static final HashMap<Integer, String> MESSAGE_TYPES = new HashMap<>();
static {
MESSAGE_TYPES.putAll(ImmutableMap.<Integer, String>builder()
.put(CON, "CON (" + CON + ")")
.put(NON, "NON (" + NON + ")")
.put(ACK, "ACK (" + ACK + ")")
.put(RST, "RST (" + RST + ")")
.build()
);
}
public static String asString(int messageType) {
String result = MESSAGE_TYPES.get(messageType);
return result == null ? "UNKOWN (" + messageType + ")" : result;
}
public static boolean isMessageType(int number) {
return MESSAGE_TYPES.containsKey(number);
}
}

View File

@ -0,0 +1,43 @@
package com.fastbee.coap.model;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ResourceStatusAge {
public static final long MODULUS = (long) Math.pow(2, 24);
private static final long THRESHOLD = (long) Math.pow(2, 23);
private final long sequenceNo;
private final long timestamp;
public ResourceStatusAge(long sequenceNo, long timestamp) {
this.sequenceNo = sequenceNo;
this.timestamp = timestamp;
}
public static boolean isReceivedStatusNewer(ResourceStatusAge latest, ResourceStatusAge received) {
if (latest.sequenceNo < received.sequenceNo && received.sequenceNo - latest.sequenceNo < THRESHOLD) {
log.debug("Criterion 1 matches: received ({}) is newer than latest ({}).", received, latest);
return true;
}
if (latest.sequenceNo > received.sequenceNo && latest.sequenceNo - received.sequenceNo > THRESHOLD) {
log.debug("Criterion 2 matches: received ({}) is newer than latest ({}).", received, latest);
return true;
}
if (received.timestamp > latest.timestamp + 128000L) {
log.debug("Criterion 3 matches: received ({}) is newer than latest ({}).", received, latest);
return true;
}
log.debug("No criterion matches: received({}) is older than latest ({}).", received, latest);
return false;
}
@Override
public String toString() {
return "STATUS AGE (Sequence No: " + this.sequenceNo + ", Reception Timestamp: " + this.timestamp + ")";
}
}

View File

@ -0,0 +1,75 @@
package com.fastbee.coap.model;
import com.google.common.primitives.Bytes;
import com.google.common.primitives.Longs;
import com.google.common.primitives.UnsignedLongs;
import java.util.Arrays;
public class Token implements Comparable<Token>{
public static int MAX_LENGTH = 8;
private final static char[] hexArray = "0123456789ABCDEF".toCharArray();
private final byte[] token;
public Token(byte[] token) {
if (token.length > 8)
throw new IllegalArgumentException("Maximum token length is 8 (but given length was " + token.length + ")");
this.token = token;
}
public byte[] getBytes() {
return this.token;
}
@Override
public String toString() {
String tmp = bytesToHex(getBytes());
if (tmp.length() == 0)
return "<EMPTY>";
else
return "0x" + tmp;
}
public static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for ( int j = 0; j < bytes.length; j++ ) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
@Override
public boolean equals(Object object) {
if ((!(object instanceof Token)))
return false;
Token other = (Token) object;
return Arrays.equals(this.getBytes(), other.getBytes());
}
@Override
public int hashCode() {
return Arrays.hashCode(token);
}
@Override
public int compareTo(Token other) {
if (other.equals(this))
return 0;
if (this.getBytes().length < other.getBytes().length)
return -1;
if (this.getBytes().length > other.getBytes().length)
return 1;
return UnsignedLongs.compare(Longs.fromByteArray(Bytes.concat(this.getBytes(), new byte[8])),
Longs.fromByteArray(Bytes.concat(other.getBytes(), new byte[8])));
}
}

View File

@ -0,0 +1,436 @@
package com.fastbee.coap.model.linkformat;
import com.fastbee.coap.model.CoapMessage;
import com.fastbee.coap.model.options.StringOptionValue;
import lombok.extern.slf4j.Slf4j;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
@Slf4j
public class LinkParam {
/**
* The enumeration {@link Key} contains all link-param-keys that are supported
*
* @author Oliver Kleine
*/
public enum Key {
/**
* Corresponds to link-param-key "rel"
*/
REL("rel", ValueType.RELATION_TYPE, ValueType.DQUOTED_RELATION_TYPES),
/**
* Corresponds to link-param-key "anchor"
*/
ANCHOR("anchor", ValueType.DQUOTED_URI_REFERENCE),
/**
* Corresponds to link-param-key "rev"
*/
REV("rev", ValueType.RELATION_TYPE),
/**
* Corresponds to link-param-key "hreflang"
*/
HREFLANG("hreflang", ValueType.LANGUAGE_TAG),
/**
* Corresponds to link-param-key "media"
*/
MEDIA("media", ValueType.MEDIA_DESC, ValueType.DQUOTED_MEDIA_DESC),
/**
* Corresponds to link-param-key "title"
*/
TITLE("title", ValueType.DQUOTED_STRING),
/**
* Corresponds to link-param-key "title*"
*/
TITLE_STAR("title*", ValueType.EXT_VALUE),
/**
* Corresponds to link-param-key "type"
*/
TYPE("type", ValueType.MEDIA_TYPE, ValueType.DQUOTED_MEDIA_TYPE),
/**
* Corresponds to link-param-key "rt"
*/
RT("rt", ValueType.RELATION_TYPE),
/**
* Corresponds to link-param-key "if"
*/
IF("if", ValueType.RELATION_TYPE),
/**
* Corresponds to link-param-key "sz"
*/
SZ("sz", ValueType.CARDINAL),
/**
* Corresponds to link-param-key "ct"
*/
CT("ct", ValueType.CARDINAL, ValueType.DQUOTED_CARDINALS),
/**
* Corresponds to link-param-key "obs"
*/
OBS("obs", ValueType.EMPTY),
/**
* Used internally for unknown link-param-keys
*/
UNKNOWN(null, ValueType.UNKNOWN);
private final String keyName;
private final Set<ValueType> valueTypes;
Key(String keyName, ValueType... valueType) {
this.keyName = keyName;
this.valueTypes = new HashSet<>(valueType.length);
this.valueTypes.addAll(Arrays.asList(valueType));
}
/**
* Returns the name of this link-param-key (i.e. "ct")
* @return the name of this link-param-key (i.e. "ct")
*/
public String getKeyName() {
return this.keyName;
}
/**
* Returns the {@link ValueType}s that are allowed for values of this key
* @return the {@link ValueType}s that are allowed for values of this key
*/
public Set<ValueType> getValueTypes() {
return this.valueTypes;
}
}
/**
* The enumeration {@link ValueType} contains all value types that are supported
*
* @author Oliver Kleine
*/
public enum ValueType {
/**
* Corresponds to the empty type, i.e. no value
*/
EMPTY (false, false),
/**
* Corresponds to a single value of type "relation-types"
*/
RELATION_TYPE (false, false),
/**
* Corresponds to one or more values of type "relation-types" enclosed in double-quotes (<code>DQUOTE</code>)
*/
DQUOTED_RELATION_TYPES(true, true),
/**
* Corresponds to a single value of type "URI reference"
*/
DQUOTED_URI_REFERENCE(true, false),
/**
* Corresponds to a single value of type "Language-Tag"
*/
LANGUAGE_TAG (false, false),
/**
* Corresponds to a single value of type "Media Desc"
*/
MEDIA_DESC (false, false),
/**
* Corresponds to a single value of type "Media Desc" enclosed in double-quotes (<code>DQUOTE</code>)
*/
DQUOTED_MEDIA_DESC(true, false),
/**
* Corresponds to a single value of type "quoted-string", i.e. a string value enclosed in double-quotes
* (<code>DQUOTE</code>)
*/
DQUOTED_STRING(true, false),
/**
* Corresponds to a single value of type "ext-value"
*/
EXT_VALUE (false, false),
/**
* Corresponds to a single value of type "media-type"
*/
MEDIA_TYPE (false, false),
/**
* Corresponds to a single value of type "media-type" enclosed in double-quotes (<code>DQUOTE</code>)
*/
DQUOTED_MEDIA_TYPE(true, false),
/**
* Corresponds to a single value of type "cardinal", i.e. digits
*/
CARDINAL (false, false),
/**
* Values of this type consist of multiple cardinal values, divided by white spaces and enclosed in
* double-quotes (<code>DQUOTE</code>)
*/
DQUOTED_CARDINALS(true, true),
/**
* Internally used to represent all other types
*/
UNKNOWN(false, false);
private final boolean doubleQuoted;
private final boolean multipleValues;
ValueType(boolean doubleQuoted, boolean multipleValues) {
this.doubleQuoted = doubleQuoted;
this.multipleValues = multipleValues;
}
/**
* Returns <code>true</code> if this {@link ValueType} allows multiple values divided by white spaces and
* <code>false</code> otherwise
*
* @return <code>true</code> if this {@link ValueType} allows multiple values divided by white spaces and
* <code>false</code> otherwise
*/
public boolean isMultipleValues() {
return this.multipleValues;
}
/**
* Returns <code>true</code> if values of this {@link ValueType} are enclosed in double-quotes
* (<code>DQUOTE</code>) and <code>false</code> otherwise
*
* @return <code>true</code> if values of this {@link ValueType} are enclosed in double-quotes
* (<code>DQUOTE</code>) and <code>false</code> otherwise
*/
public boolean isDoubleQuoted() {
return this.doubleQuoted;
}
}
/**
* Returns the {@link Key} corresponding to the given name or <code>null</code> if no such {@link Key} exists
*
* @param keyName the name of the {@link Key} to lookup
* @return the {@link Key} corresponding to the given name or <code>null</code> if no such {@link Key} exists
*/
public static Key getKey(String keyName) {
for(Key key : Key.values()) {
if (key.getKeyName().equals(keyName)) {
return key;
}
}
return null;
}
/**
* Returns the {@link ValueType} that corresponds to the given key-value-pair or <code>null</code> if no such
* {@link ValueType} exists.
*
* @param key the key
* @param value the value
*
* @return the {@link ValueType} that corresponds to the given key-value-pair or <code>null</code> if no such
* {@link ValueType} exists.
*/
public static ValueType getValueType(Key key, String value) {
// determine possible value types
Set<ValueType> valueTypes = key.getValueTypes();
// check if link param value is quoted and if there is quoted type
if (valueTypes.size() == 1) {
return valueTypes.iterator().next();
} else if (value.startsWith("\"") && value.endsWith("\"")) {
for (ValueType valueType : valueTypes) {
if(valueType.isDoubleQuoted()) {
return valueType;
}
}
} else {
for (ValueType valueType : valueTypes) {
if(!valueType.isDoubleQuoted()) {
return valueType;
}
}
}
return null;
}
/**
* Decodes the given (serialized) link param (e.g. <code>ct=40</code>)
* @param linkParam the serialized link param
* @return an instance of {@link LinkParam} according to the given parameter
*/
public static LinkParam decode(String linkParam) {
// remove percent encoding
byte[] tmp = StringOptionValue.convertToByteArrayWithoutPercentEncoding(linkParam);
linkParam = new String(tmp, CoapMessage.CHARSET);
// determine the key of this link param
String keyName = !linkParam.contains("=") ? linkParam : linkParam.substring(0, linkParam.indexOf("="));
LinkParam.Key key = LinkParam.getKey(keyName);
if(key == null) {
log.warn("Unsupported key name for link param: {}", keyName);
return null;
} else if (keyName.equals(linkParam)) {
// empty attribute
if(!key.getValueTypes().contains(ValueType.EMPTY)) {
log.debug("Key {} does not support empty values!", key.getKeyName());
return null;
} else {
return new LinkParam(key, ValueType.EMPTY, null);
}
} else {
// link param has non-empty value
String value = linkParam.substring(linkParam.indexOf("=") + 1, linkParam.length());
LinkParam.ValueType valueType = LinkParam.getValueType(key, value);
if(valueType == null) {
log.warn("Could not determine value type for key \"{}\" and value\"{}\".", keyName, value);
return null;
} else {
log.debug("Value: {}, Type: {}", value, valueType);
return new LinkParam(key, valueType, value);
}
}
}
/**
* <p>Creates a new instance of {@link LinkParam}</p>
*
* <p><b>Note:</b>For some kinds of link params the enclosing double quotes are part of the value (e.g. value "0 41"
* for {@link Key#CT} or "Some title" for {@link Key#TITLE}). Thus, the latter is created using
* <code>createLinkParam(Key.TITLE, "\"Some title\"")</code>
* </p>
*
* @param key the {@link Key} of the link param to be created
* @param value the value of the link param to be created (see note above)
*
* @return a new instance of {@link LinkParam} according to the given parameters (key and value)
*/
public static LinkParam createLinkParam(Key key, String value) {
ValueType valueType = getValueType(key, value);
if (valueType == null) {
log.warn("Could not determine value type for key \"{}\" and value\"{}\".", key.getKeyName(), value);
return null;
} else {
return new LinkParam(key, valueType, value);
}
}
//******************************************************************************************
// instance related fields and methods
//******************************************************************************************
private final Key key;
private final ValueType valueType;
private final String value;
private LinkParam(Key key, ValueType valueType, String value) {
this.key = key;
this.valueType = valueType;
// remove double quotes if existing
this.value = valueType.isDoubleQuoted() ? value.substring(1, value.length() - 1) : value;
log.debug("LinkParam created: {}", this.toString());
}
/**
* Returns the {@link Key} of this {@link LinkParam}
* @return the {@link Key} of this {@link LinkParam}
*/
public Key getKey() {
return key;
}
/**
* Shortcut for {@link #getKey()#getKeyName()}
* @return the name of the {@link Key} of this {@link LinkParam} (e.g. "ct" or "rt")
*/
public String getKeyName() {
return this.key.getKeyName();
}
/**
* Returns the {@link ValueType} of the value returned by {@link #getValue()}
* @return the {@link ValueType} of the value returned by {@link #getValue()}
*/
public ValueType getValueType() {
return this.valueType;
}
/**
* Returns the value of this {@link LinkParam}
* @return the value of this {@link LinkParam}
*/
public String getValue() {
if (this.valueType.isDoubleQuoted()) {
return "\"" + this.value + "\"";
} else {
return this.value;
}
}
/**
* <p>Returns <code>true</code> if the given value is contained in the value returned by {@link #getValue()} and
* <code>false</code> otherwise. The exact behaviour depends on whether there are multiple values allowed in a
* single param (see: {@link ValueType#isMultipleValues()}).</p>
*
* <p>Example: If the {@link LinkParam} corresponds to <code>ct="0 41"</code> then both, <code>contains("0")</code>
* and <code>contains("41")</code> return <code>true</code> but <code>contains("0 41")</code> returns
* <code>false</code>.</p>
*
* @param value the value to check
*
* @return <code>true</code> if the given value is contained in the value returned by {@link #getValue()} and
* <code>false</code> otherwise.
*/
public boolean contains(String value) {
if (this.valueType.isMultipleValues()){
return Arrays.asList(this.value.split(" ")).contains(value);
} else {
return this.value.equals(value);
}
}
/**
* Returns a string representation of this {@link LinkParam}
* @return a string representation of this {@link LinkParam}
*/
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append(this.key.getKeyName());
if (this.valueType != ValueType.EMPTY) {
builder.append("=");
if (this.valueType.doubleQuoted) {
builder.append("\"");
}
builder.append(this.value);
if (this.valueType.doubleQuoted) {
builder.append("\"");
}
}
return builder.toString();
}
}

View File

@ -0,0 +1,109 @@
package com.fastbee.coap.model.linkformat;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
@Slf4j
public class LinkValue {
//*******************************************************************************************
// static fields and methods
//*******************************************************************************************
static LinkValue decode(String linkValue) {
LinkValue result = new LinkValue(getUriReference(linkValue));
for (String linkParam : LinkValue.getLinkParams(linkValue)) {
result.addLinkParam(LinkParam.decode(linkParam));
}
return result;
}
private static String getUriReference(String linkValue) {
String uriReference = linkValue.substring(linkValue.indexOf("<") + 1, linkValue.indexOf(">"));
log.info("Found URI reference <{}>", uriReference);
return uriReference;
}
private static List<String> getLinkParams(String linkValue) {
String[] linkParams = linkValue.split(";");
return new ArrayList<>(Arrays.asList(linkParams).subList(1, linkParams.length));
}
//******************************************************************************************
// instance related fields and methods
//******************************************************************************************
private final String uriReference;
private final Collection<LinkParam> linkParams;
/**
* Creates a new instance of {@link LinkValue}
* @param uriReference the URI reference, i.e. the resource to be described
* @param linkParams the {@link LinkParam}s to describe the resource
*/
public LinkValue(String uriReference, Collection<LinkParam> linkParams) {
this.uriReference = uriReference;
this.linkParams = linkParams;
}
private LinkValue(String uriReference) {
this(uriReference, new ArrayList<LinkParam>());
}
private void addLinkParam(LinkParam linkParams) {
this.linkParams.add(linkParams);
}
/**
* Returns the URI reference of this {@link LinkValue}
* @return the URI reference of this {@link LinkValue}
*/
public String getUriReference() {
return this.uriReference;
}
/**
* Returns the {@link LinkParam}s describing the resource identified by the URI reference
* @return the {@link LinkParam}s describing this resource identified by the URI reference
*/
public Collection<LinkParam> getLinkParams() {
return this.linkParams;
}
/**
* Returns <code>true</code> if this {@link LinkValue} contains a {@link LinkParam} that matches the given
* criterion, i.e. the given key-value-pair and <code>false</code> otherwise.
*
* @param key the key of the criterion
* @param value the value of the criterion
*
* @return <code>true</code> if this {@link LinkValue} contains a {@link LinkParam} that matches the given
* criterion, i.e. the given key-value-pair and <code>false</code> otherwise.
*/
public boolean containsLinkParam(LinkParam.Key key, String value) {
for (LinkParam linkParam : this.linkParams) {
if (key.equals(linkParam.getKey())) {
return value == null || linkParam.contains(value);
}
}
return false;
}
/**
* Returns a string representation of this {@link LinkValue}.
* @return a string representation of this {@link LinkValue}.
*/
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("<").append(uriReference).append(">");
for (LinkParam linkParam : this.getLinkParams()) {
builder.append(";").append(linkParam.toString());
}
return builder.toString();
}
}

View File

@ -0,0 +1,189 @@
package com.fastbee.coap.model.linkformat;
import com.fastbee.coap.model.CoapMessage;
import com.fastbee.coap.model.options.ContentFormat;
import java.util.*;
public class LinkValueList {
//*******************************************************************************************
// static methods
//*******************************************************************************************
/**
* Decodes a serialized link-value-list, e.g. the payload (content) of a {@link CoapMessage} with content type
* {@link ContentFormat#APP_LINK_FORMAT} and returns a corresponding {@link LinkValueList} instance.
*
* @param linkValueList the serialized link-value-list
*
* @return A {@link LinkValueList} instance corresponsing to the given serialization
*/
public static LinkValueList decode(String linkValueList) {
LinkValueList result = new LinkValueList();
Collection<String> linkValues = getLinkValues(linkValueList);
for(String linkValue : linkValues) {
result.addLinkValue(LinkValue.decode(linkValue));
}
return result;
}
private static Collection<String> getLinkValues(String linkValueList) {
List<String> linkValues = new ArrayList<>();
Collections.addAll(linkValues, linkValueList.split(","));
return linkValues;
}
//******************************************************************************************
// instance related fields and methods
//******************************************************************************************
private Collection<LinkValue> linkValues;
private LinkValueList() {
this.linkValues = new TreeSet<>(new Comparator<LinkValue>() {
@Override
public int compare(LinkValue linkValue1, LinkValue linkValue2) {
return linkValue1.getUriReference().compareTo(linkValue2.getUriReference());
}
});
}
/**
* Creates a new instance of {@link LinkValueList}
* @param linkValues the {@link LinkValue}s to be contained in the {@link LinkValueList} to be created
*/
public LinkValueList(LinkValue... linkValues) {
this.linkValues = new ArrayList<>(Arrays.asList(linkValues));
}
/**
* Adds an instance of {@link LinkValue} to this {@link LinkValueList}.
* @param linkValue the {@link LinkValue} to be added
*/
public void addLinkValue(LinkValue linkValue) {
this.linkValues.add(linkValue);
}
public boolean removeLinkValue(String uriReference) {
for (LinkValue linkValue : this.linkValues) {
if (linkValue.getUriReference().equals(uriReference)) {
this.linkValues.remove(linkValue);
return true;
}
}
return false;
}
/**
* Returns all URI references contained in this {@link LinkValueList}
*
* @return all URI references contained in this {@link LinkValueList}
*/
public List<String> getUriReferences() {
List<String> result = new ArrayList<>(linkValues.size());
for (LinkValue linkValue : linkValues) {
result.add(linkValue.getUriReference());
}
return result;
}
/**
* Returns the URI references that match the given criterion, i.e. contain a {@link LinkParam} with the given
* pair of keyname and value.
*
* @param key the {@link LinkParam.Key} to match
* @param value the value to match
*
* @return the URI references that match the given criterion
*/
public Set<String> getUriReferences(LinkParam.Key key, String value) {
Set<String> result = new HashSet<>();
for (LinkValue linkValue : linkValues) {
if (linkValue.containsLinkParam(key, value)) {
result.add(linkValue.getUriReference());
}
}
return result;
}
/**
* Returns the {@link LinkParam}s for the given URI reference.
*
* @param uriReference the URI reference to lookup the {@link LinkParam}s for
*
* @return the {@link LinkParam}s for the given URI reference
*/
public Collection<LinkParam> getLinkParams(String uriReference) {
List<LinkParam> result = new ArrayList<>();
for (LinkValue linkValue : this.linkValues) {
if (linkValue.getUriReference().equals(uriReference)) {
return linkValue.getLinkParams();
}
}
return null;
}
public LinkValueList filter(LinkParam.Key key, String value) {
LinkValueList result = new LinkValueList();
for (LinkValue linkValue : this.linkValues) {
if (linkValue.containsLinkParam(key, value)) {
result.addLinkValue(linkValue);
}
}
return result;
}
public LinkValueList filter(String hrefValue) {
if (hrefValue.endsWith("*")) {
return filterByUriPrefix(hrefValue.substring(0, hrefValue.length() - 1));
} else {
return filterByUriReference(hrefValue);
}
}
private LinkValueList filterByUriPrefix(String prefix) {
LinkValueList result = new LinkValueList();
for (LinkValue linkValue : this.linkValues) {
if (linkValue.getUriReference().startsWith(prefix)) {
result.addLinkValue(linkValue);
}
}
return result;
}
private LinkValueList filterByUriReference(String uriReference) {
LinkValueList result = new LinkValueList();
for (LinkValue linkValue : this.linkValues) {
if (linkValue.getUriReference().endsWith(uriReference)) {
result.addLinkValue(linkValue);
return result;
}
}
return result;
}
/**
* Returns a string representation of this {@link LinkValueList}, i.e. the reversal of {@link #decode(String)}
* @return a string representation of this {@link LinkValueList}, i.e. the reversal of {@link #decode(String)}
*/
public String encode() {
StringBuilder builder = new StringBuilder();
for (LinkValue linkValue : this.linkValues) {
builder.append(linkValue.toString());
builder.append(",");
}
if (builder.length() > 0) {
builder.deleteCharAt(builder.length() - 1);
}
return builder.toString();
}
/**
* Returns a string representation of this {@link LinkValueList} (same as {@link #encode()}
* @return a string representation of this {@link LinkValueList} (same as {@link #encode()}
*/
@Override
public String toString() {
return this.encode();
}
}

View File

@ -0,0 +1,59 @@
package com.fastbee.coap.model.options;
public abstract class ContentFormat {
/**
* Corresponds to number -1
*/
public static final long UNDEFINED = -1;
/**
* Corresponds to number 0
*/
public static final long TEXT_PLAIN_UTF8 = 0;
/**
* Corresponds to number 40
*/
public static final long APP_LINK_FORMAT = 40;
/**
* Corresponds to number 41
*/
public static final long APP_XML = 41;
/**
* Corresponds to number 42
*/
public static final long APP_OCTET_STREAM = 42;
/**
* Corresponds to number 47
*/
public static final long APP_EXI = 47;
/**
* Corresponds to number 50
*/
public static final long APP_JSON = 50;
/**
* Corresponds to number 201 (no standard but defined for very selfish reasons)
*/
public static final long APP_RDF_XML = 201;
/**
* Corresponds to number 202 (no standard but defined for very selfish reasons)
*/
public static final long APP_TURTLE = 202;
/**
* Corresponds to number 203 (no standard but defined for very selfish reasons)
*/
public static final long APP_N3 = 203;
/**
* Corresponds to number 205 (no standard but defined for very selfish reasons)
*/
public static final long APP_SHDT = 205;
}

View File

@ -0,0 +1,56 @@
package com.fastbee.coap.model.options;
import lombok.extern.slf4j.Slf4j;
import java.util.Arrays;
@Slf4j
public final class EmptyOptionValue extends OptionValue<Void> {
/**
* @param optionNumber the option number of the {@link EmptyOptionValue} to be created
*
* @throws java.lang.IllegalArgumentException if the given option number does not refer to an empty option
*/
public EmptyOptionValue(int optionNumber) throws IllegalArgumentException {
super(optionNumber, new byte[0], false);
log.debug("Empty Option (#{}) created.", optionNumber);
}
/**
* Returns <code>null</code>
* @return <code>null</code>
*/
@Override
public Void getDecodedValue() {
return null;
}
/**
* Returns <code>0</code>
* @return <code>0</code>
*/
@Override
public int hashCode() {
return 0;
}
/**
* Checks if a given {@link Object} equals this {@link EmptyOptionValue} instance. A given {@link Object} equals
* this {@link EmptyOptionValue} if and only if the {@link Object} is an instance of {@link EmptyOptionValue}.
*
* @param object the object to check for equality with this instance of {@link EmptyOptionValue}
*
* @return <code>true</code> if the given {@link Object} is an instance of {@link EmptyOptionValue} and
* <code>false</code> otherwise.
*/
@Override
public boolean equals(Object object) {
if (!(object instanceof EmptyOptionValue))
return false;
EmptyOptionValue other = (EmptyOptionValue) object;
return Arrays.equals(this.getValue(), other.getValue());
}
}

View File

@ -0,0 +1,56 @@
package com.fastbee.coap.model.options;
import java.util.Arrays;
public class OpaqueOptionValue extends OptionValue<byte[]> {
private final static char[] hexArray = "0123456789ABCDEF".toCharArray();
public OpaqueOptionValue(int optionNumber, byte[] value) throws IllegalArgumentException {
super(optionNumber, value, false);
}
@Override
public byte[] getDecodedValue() {
return this.value;
}
@Override
public int hashCode() {
return Arrays.hashCode(getDecodedValue());
}
@Override
public boolean equals(Object object) {
if (!(object instanceof OpaqueOptionValue))
return false;
OpaqueOptionValue other = (OpaqueOptionValue) object;
return Arrays.equals(this.getValue(), other.getValue());
}
@Override
public String toString() {
return toHexString(this.value);
}
public static String toHexString(byte[] bytes) {
if (bytes.length == 0)
return "<empty>";
else
return "0x" + bytesToHex(bytes);
}
private static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for ( int j = 0; j < bytes.length; j++ ) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
}

View File

@ -0,0 +1,449 @@
package com.fastbee.coap.model.options;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableMap;
import java.util.HashMap;
import static com.fastbee.coap.model.options.Option.Occurence.*;
import static com.fastbee.coap.model.MessageCode.*;
public abstract class Option {
public enum Occurence {
NONE, ONCE, MULTIPLE
}
/**
* Corresponds to option number -1 = unknown)
*/
public static final int UNKNOWN = -1;
/**
* Corresponds to option number 1
*/
public static final int IF_MATCH = 1;
/**
* Corresponds to option number 3
*/
public static final int URI_HOST = 3;
/**
* Corresponds to option number 4
*/
public static final int ETAG = 4;
/**
* Corresponds to option number 5
*/
public static final int IF_NONE_MATCH = 5;
/**
* Corresponds to option number 6
*/
public static final int OBSERVE = 6;
/**
* Corresponds to option number 7
*/
public static final int URI_PORT = 7;
/**
* Corresponds to option number 8
*/
public static final int LOCATION_PATH = 8;
/**
* Corresponds to option number 11
*/
public static final int URI_PATH = 11;
/**
* Corresponds to option number 12
*/
public static final int CONTENT_FORMAT = 12;
/**
* Corresponds to option number 14
*/
public static final int MAX_AGE = 14;
/**
* Corresponds to option number 15
*/
public static final int URI_QUERY = 15;
/**
* Corresponds to option number 17
*/
public static final int ACCEPT = 17;
/**
* Corresponds to option number 20
*/
public static final int LOCATION_QUERY = 20;
/**
* Corresponds to option number 23
*/
public static final int BLOCK_2 = 23;
/**
* Corresponds to option number 27
*/
public static final int BLOCK_1 = 27;
/**
* Corresponds to option number 28
*/
public static final int SIZE_2 = 28;
/**
* Corresponds to option number 35
*/
public static final int PROXY_URI = 35;
/**
* Corresponds to option number 39
*/
public static final int PROXY_SCHEME = 39;
/**
* Corresponds to option number 60
*/
public static final int SIZE_1 = 60;
/**
* Corresponds to option number 124
*/
public static final int ENDPOINT_ID_1 = 124;
/**
* Corresponds to option number 189
*/
public static final int ENDPOINT_ID_2 = 189;
private static HashMap<Integer, String> OPTIONS = new HashMap<>();
static {
OPTIONS.putAll(ImmutableMap.<Integer, String>builder()
.put(IF_MATCH, "IF MATCH (" + IF_MATCH + ")")
.put(URI_HOST, "URI HOST (" + URI_HOST + ")")
.put(ETAG, "ETAG (" + ETAG + ")")
.put(IF_NONE_MATCH, "IF NONE MATCH (" + IF_NONE_MATCH + ")")
.put(OBSERVE, "OBSERVE (" + OBSERVE + ")")
.put(URI_PORT, "URI PORT (" + URI_PORT + ")")
.put(LOCATION_PATH, "LOCATION PATH (" + LOCATION_PATH + ")")
.put(URI_PATH, "URI PATH (" + URI_PATH + ")")
.put(CONTENT_FORMAT, "CONTENT FORMAT (" + CONTENT_FORMAT + ")")
.put(MAX_AGE, "MAX AGE (" + MAX_AGE + ")")
.put(URI_QUERY, "URI QUERY (" + URI_QUERY + ")")
.put(ACCEPT, "ACCEPT (" + ACCEPT + ")")
.put(LOCATION_QUERY, "LOCATION QUERY (" + LOCATION_QUERY + ")")
.put(BLOCK_2, "BLOCK 2 (" + BLOCK_2 + ")")
.put(BLOCK_1, "BLOCK 1 (" + BLOCK_1 + ")")
.put(SIZE_2, "SIZE 2 (" + SIZE_2 + ")")
.put(PROXY_URI, "PROXY URI (" + PROXY_URI + ")")
.put(PROXY_SCHEME, "PROXY SCHEME (" + PROXY_SCHEME + ")")
.put(SIZE_1, "SIZE 1 (" + SIZE_1 + ")")
.put(ENDPOINT_ID_1, "ENDPOINT ID 1 (" + ENDPOINT_ID_1 + ")")
.put(ENDPOINT_ID_2, "ENDPOINT ID 2 (" + ENDPOINT_ID_2 + ")")
.build()
);
}
public static String asString(int optionNumber) {
String result = OPTIONS.get(optionNumber);
return result == null ? "UNKOWN (" + optionNumber + ")" : result;
}
private static HashMultimap<Integer, Integer> MUTUAL_EXCLUSIONS = HashMultimap.create();
static {
MUTUAL_EXCLUSIONS.put(URI_HOST, PROXY_URI);
MUTUAL_EXCLUSIONS.put(PROXY_URI, URI_HOST);
MUTUAL_EXCLUSIONS.put(URI_PORT, PROXY_URI);
MUTUAL_EXCLUSIONS.put(PROXY_URI, URI_PORT);
MUTUAL_EXCLUSIONS.put(URI_PATH, PROXY_URI);
MUTUAL_EXCLUSIONS.put(PROXY_URI, URI_PATH);
MUTUAL_EXCLUSIONS.put(URI_QUERY, PROXY_URI);
MUTUAL_EXCLUSIONS.put(PROXY_URI, URI_QUERY);
MUTUAL_EXCLUSIONS.put(PROXY_SCHEME, PROXY_URI);
MUTUAL_EXCLUSIONS.put(PROXY_URI, PROXY_SCHEME);
}
public static boolean mutuallyExcludes(int firstOptionNumber, int secondOptionNumber) {
return MUTUAL_EXCLUSIONS.get(firstOptionNumber).contains(secondOptionNumber);
}
private static final HashBasedTable<Integer, Integer, Option.Occurence> OCCURENCE_CONSTRAINTS
= HashBasedTable.create();
static {
// GET Requests
OCCURENCE_CONSTRAINTS.row(GET).putAll(ImmutableMap.<Integer, Occurence>builder()
.put(URI_HOST, ONCE)
.put(URI_PORT, ONCE)
.put(URI_PATH, MULTIPLE)
.put(URI_QUERY, MULTIPLE)
.put(PROXY_URI, ONCE)
.put(PROXY_SCHEME, ONCE)
.put(ACCEPT, MULTIPLE)
.put(ETAG, MULTIPLE)
.put(OBSERVE, ONCE)
.put(BLOCK_2, ONCE)
.put(SIZE_2, ONCE)
.put(ENDPOINT_ID_1, ONCE)
.build()
);
// POST Requests
OCCURENCE_CONSTRAINTS.row(POST).putAll(ImmutableMap.<Integer, Occurence>builder()
.put(URI_HOST, ONCE)
.put(URI_PORT, ONCE)
.put(URI_PATH, MULTIPLE)
.put(URI_QUERY, MULTIPLE)
.put(ACCEPT, MULTIPLE)
.put(PROXY_URI, ONCE)
.put(PROXY_SCHEME, ONCE)
.put(CONTENT_FORMAT, ONCE)
.put(BLOCK_2, ONCE)
.put(BLOCK_1, ONCE)
.put(SIZE_2, ONCE)
.put(SIZE_1, ONCE)
.put(ENDPOINT_ID_1, ONCE)
.build()
);
// PUT Requests
OCCURENCE_CONSTRAINTS.row(PUT).putAll(ImmutableMap.<Integer, Occurence>builder()
.put(URI_HOST, ONCE)
.put(URI_PORT, ONCE)
.put(URI_PATH, MULTIPLE)
.put(URI_QUERY, MULTIPLE)
.put(ACCEPT, MULTIPLE)
.put(PROXY_URI, ONCE)
.put(PROXY_SCHEME, ONCE)
.put(CONTENT_FORMAT, ONCE)
.put(IF_MATCH, ONCE)
.put(IF_NONE_MATCH, ONCE)
.put(BLOCK_2, ONCE)
.put(BLOCK_1, ONCE)
.put(SIZE_2, ONCE)
.put(SIZE_1, ONCE)
.put(ENDPOINT_ID_1, ONCE)
.build()
);
// DELETE Requests
OCCURENCE_CONSTRAINTS.row(DELETE).putAll(ImmutableMap.<Integer, Occurence>builder()
.put(URI_HOST, ONCE)
.put(URI_PORT, ONCE)
.put(URI_PATH, MULTIPLE)
.put(URI_QUERY, MULTIPLE)
.put(PROXY_URI, ONCE)
.put(PROXY_SCHEME, ONCE)
.put(ENDPOINT_ID_1, ONCE)
.build()
);
//Response success (2.x)
OCCURENCE_CONSTRAINTS.row(CREATED_201).putAll(ImmutableMap.<Integer, Occurence>builder()
.put(ETAG, ONCE)
.put(OBSERVE, ONCE)
.put(LOCATION_PATH, MULTIPLE)
.put(LOCATION_QUERY, MULTIPLE)
.put(CONTENT_FORMAT, ONCE)
.put(BLOCK_2, ONCE)
.put(BLOCK_1, ONCE)
.put(SIZE_2, ONCE)
.put(ENDPOINT_ID_2, ONCE)
.build()
);
OCCURENCE_CONSTRAINTS.row(DELETED_202).putAll(ImmutableMap.<Integer, Occurence>builder()
.put(CONTENT_FORMAT, ONCE)
.put(BLOCK_2, ONCE)
.put(BLOCK_1, ONCE)
.put(SIZE_2, ONCE)
.put(ENDPOINT_ID_2, ONCE)
.build()
);
OCCURENCE_CONSTRAINTS.row(VALID_203).putAll(ImmutableMap.<Integer, Occurence>builder()
.put(OBSERVE, ONCE)
.put(ETAG, ONCE)
.put(MAX_AGE, ONCE)
.put(CONTENT_FORMAT, ONCE)
.put(ENDPOINT_ID_1, ONCE)
.put(ENDPOINT_ID_2, ONCE)
.build()
);
OCCURENCE_CONSTRAINTS.row(CHANGED_204).putAll(ImmutableMap.<Integer, Occurence>builder()
.put(ETAG, ONCE)
.put(CONTENT_FORMAT, ONCE)
.put(BLOCK_2, ONCE)
.put(BLOCK_1, ONCE)
.put(SIZE_2, ONCE)
.put(ENDPOINT_ID_2, ONCE)
.build()
);
OCCURENCE_CONSTRAINTS.row(CONTENT_205).putAll(ImmutableMap.<Integer, Occurence>builder()
.put(OBSERVE, ONCE)
.put(CONTENT_FORMAT, ONCE)
.put(MAX_AGE, ONCE)
.put(ETAG, ONCE)
.put(BLOCK_2, ONCE)
.put(BLOCK_1, ONCE)
.put(SIZE_2, ONCE)
.put(ENDPOINT_ID_1, ONCE)
.put(ENDPOINT_ID_2, ONCE)
.build()
);
OCCURENCE_CONSTRAINTS.row(CONTINUE_231).putAll(ImmutableMap.<Integer, Occurence>builder()
.put(BLOCK_1, ONCE)
.build()
);
// Client ERROR Responses (4.x)
OCCURENCE_CONSTRAINTS.row(BAD_REQUEST_400).putAll(ImmutableMap.<Integer, Occurence>builder()
.put(MAX_AGE, ONCE)
.put(CONTENT_FORMAT, ONCE)
.put(ENDPOINT_ID_2, ONCE)
.build()
);
OCCURENCE_CONSTRAINTS.row(UNAUTHORIZED_401).putAll(ImmutableMap.<Integer, Occurence>builder()
.put(MAX_AGE, ONCE)
.put(CONTENT_FORMAT, ONCE)
.put(ENDPOINT_ID_2, ONCE)
.build()
);
OCCURENCE_CONSTRAINTS.row(BAD_OPTION_402).putAll(ImmutableMap.<Integer, Occurence>builder()
.put(MAX_AGE, ONCE)
.put(CONTENT_FORMAT, ONCE)
.put(ENDPOINT_ID_2, ONCE)
.build()
);
OCCURENCE_CONSTRAINTS.row(FORBIDDEN_403).putAll(ImmutableMap.<Integer, Occurence>builder()
.put(MAX_AGE, ONCE)
.put(CONTENT_FORMAT, ONCE)
.put(ENDPOINT_ID_2, ONCE)
.build()
);
OCCURENCE_CONSTRAINTS.row(NOT_FOUND_404).putAll(ImmutableMap.<Integer, Occurence>builder()
.put(MAX_AGE, ONCE)
.put(CONTENT_FORMAT, ONCE)
.put(ENDPOINT_ID_2, ONCE)
.build()
);
OCCURENCE_CONSTRAINTS.row(METHOD_NOT_ALLOWED_405).putAll(ImmutableMap.<Integer, Occurence>builder()
.put(MAX_AGE, ONCE)
.put(CONTENT_FORMAT, ONCE)
.put(ENDPOINT_ID_2, ONCE)
.build()
);
OCCURENCE_CONSTRAINTS.row(NOT_ACCEPTABLE_406).putAll(ImmutableMap.<Integer, Occurence>builder()
.put(MAX_AGE, ONCE)
.put(CONTENT_FORMAT, ONCE)
.put(ENDPOINT_ID_2, ONCE)
.build()
);
OCCURENCE_CONSTRAINTS.row(REQUEST_ENTITY_INCOMPLETE_408).putAll(ImmutableMap.<Integer, Occurence>builder()
.put(CONTENT_FORMAT, ONCE)
.build()
);
OCCURENCE_CONSTRAINTS.row(PRECONDITION_FAILED_412).putAll(ImmutableMap.<Integer, Occurence>builder()
.put(MAX_AGE, ONCE)
.put(CONTENT_FORMAT, ONCE)
.put(ENDPOINT_ID_2, ONCE)
.build()
);
OCCURENCE_CONSTRAINTS.row(REQUEST_ENTITY_TOO_LARGE_413).putAll(ImmutableMap.<Integer, Occurence>builder()
.put(MAX_AGE, ONCE)
.put(CONTENT_FORMAT, ONCE)
.put(BLOCK_1, ONCE)
.put(SIZE_1, ONCE)
.put(ENDPOINT_ID_2, ONCE)
.build()
);
OCCURENCE_CONSTRAINTS.row(UNSUPPORTED_CONTENT_FORMAT_415).putAll(ImmutableMap.<Integer, Occurence>builder()
.put(MAX_AGE, ONCE)
.put(CONTENT_FORMAT, ONCE)
.put(ENDPOINT_ID_2, ONCE)
.build()
);
// Server ERROR Responses ( 5.x )
OCCURENCE_CONSTRAINTS.row(INTERNAL_SERVER_ERROR_500).putAll(ImmutableMap.<Integer, Occurence>builder()
.put(MAX_AGE, ONCE)
.put(CONTENT_FORMAT, ONCE)
.put(ENDPOINT_ID_2, ONCE)
.build()
);
OCCURENCE_CONSTRAINTS.row(NOT_IMPLEMENTED_501).putAll(ImmutableMap.<Integer, Occurence>builder()
.put(MAX_AGE, ONCE)
.put(CONTENT_FORMAT, ONCE)
.put(ENDPOINT_ID_2, ONCE)
.build()
);
OCCURENCE_CONSTRAINTS.row(BAD_GATEWAY_502).putAll(ImmutableMap.<Integer, Occurence>builder()
.put(MAX_AGE, ONCE)
.put(CONTENT_FORMAT, ONCE)
.put(ENDPOINT_ID_2, ONCE)
.build()
);
OCCURENCE_CONSTRAINTS.row(GATEWAY_TIMEOUT_504).putAll(ImmutableMap.<Integer, Occurence>builder()
.put(MAX_AGE, ONCE)
.put(CONTENT_FORMAT, ONCE)
.put(ENDPOINT_ID_2, ONCE)
.build()
);
OCCURENCE_CONSTRAINTS.row(PROXYING_NOT_SUPPORTED_505).putAll(ImmutableMap.<Integer, Occurence>builder()
.put(MAX_AGE, ONCE)
.put(CONTENT_FORMAT, ONCE)
.put(ENDPOINT_ID_2, ONCE)
.build()
);
}
public static Occurence getPermittedOccurrence(int optionNumber, int messageCode) {
Occurence result = OCCURENCE_CONSTRAINTS.get(messageCode, optionNumber);
return result == null ? NONE : result;
}
public static boolean isCritical(int optionNumber) {
return (optionNumber & 1) == 1;
}
public static boolean isSafe(int optionNumber) {
return !((optionNumber & 2) == 2);
}
public static boolean isCacheKey(int optionNumber) {
return !((optionNumber & 0x1e) == 0x1c);
}
}

View File

@ -0,0 +1,187 @@
package com.fastbee.coap.model.options;
import com.google.common.net.InetAddresses;
import com.google.common.primitives.Longs;
import com.fastbee.coap.model.CoapMessage;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.HashMap;
import static com.fastbee.coap.model.options.OptionValue.Type.*;
public abstract class OptionValue<T>{
private static final String UNKNOWN_OPTION = "Unknown option no. %d";
private static final String VALUE_IS_DEFAULT_VALUE = "Given value is default value for option no. %d.";
private static final String OUT_OF_ALLOWED_RANGE = "Given value length (%d) is out of allowed range " +
"for option no. %d (min: %d, max; %d).";
/**
* Provides names of available option types (basically for internal use)
*/
public static enum Type {
EMPTY, STRING, UINT, OPAQUE
}
/**
* Corresponds to 60, i.e. 60 seconds
*/
public static final long MAX_AGE_DEFAULT = 60;
/**
* Corresponds to the maximum value of the max-age option (app. 136 years)
*/
public static final long MAX_AGE_MAX = 0xFFFFFFFFL;
/**
* Corresponds to the encoded value of {@link #MAX_AGE_DEFAULT}
*/
public static final byte[] ENCODED_MAX_AGE_DEFAULT =
new BigInteger(1, Longs.toByteArray(MAX_AGE_DEFAULT)).toByteArray();
/**
* Corresponds to 5683
*/
public static final long URI_PORT_DEFAULT = 5683;
/**
* Corresponds to the encoded value of {@link #URI_PORT_DEFAULT}
*/
public static final byte[] ENCODED_URI_PORT_DEFAULT =
new BigInteger(1, Longs.toByteArray(URI_PORT_DEFAULT)).toByteArray();
private static class Characteristics {
private final Type type;
private final int minLength;
private final int maxLength;
private Characteristics(Type type, int minLength, int maxLength) {
this.type = type;
this.minLength = minLength;
this.maxLength = maxLength;
}
public Type getType() {
return type;
}
public int getMinLength() {
return minLength;
}
public int getMaxLength() {
return maxLength;
}
}
private static final HashMap<Integer, Characteristics> CHARACTERISTICS = new HashMap<>();
static {
CHARACTERISTICS.put( Option.IF_MATCH, new Characteristics( OPAQUE, 0, 8 ));
CHARACTERISTICS.put( Option.URI_HOST, new Characteristics( STRING, 1, 255 ));
CHARACTERISTICS.put( Option.ETAG, new Characteristics( OPAQUE, 1, 8 ));
CHARACTERISTICS.put( Option.IF_NONE_MATCH, new Characteristics( EMPTY, 0, 0 ));
CHARACTERISTICS.put( Option.URI_PORT, new Characteristics( UINT, 0, 2 ));
CHARACTERISTICS.put( Option.LOCATION_PATH, new Characteristics( STRING, 0, 255 ));
CHARACTERISTICS.put( Option.OBSERVE, new Characteristics( UINT, 0, 3 ));
CHARACTERISTICS.put( Option.URI_PATH, new Characteristics( STRING, 0, 255 ));
CHARACTERISTICS.put( Option.CONTENT_FORMAT, new Characteristics( UINT, 0, 2 ));
CHARACTERISTICS.put( Option.MAX_AGE, new Characteristics( UINT, 0, 4 ));
CHARACTERISTICS.put( Option.URI_QUERY, new Characteristics( STRING, 0, 255 ));
CHARACTERISTICS.put( Option.ACCEPT, new Characteristics( UINT, 0, 2 ));
CHARACTERISTICS.put( Option.LOCATION_QUERY, new Characteristics( STRING, 0, 255 ));
CHARACTERISTICS.put( Option.BLOCK_2, new Characteristics( UINT, 0, 3 ));
CHARACTERISTICS.put( Option.BLOCK_1, new Characteristics( UINT, 0, 3 ));
CHARACTERISTICS.put( Option.SIZE_2, new Characteristics( UINT, 0, 4 ));
CHARACTERISTICS.put( Option.PROXY_URI, new Characteristics( STRING, 1, 1034 ));
CHARACTERISTICS.put( Option.PROXY_SCHEME, new Characteristics( STRING, 1, 255 ));
CHARACTERISTICS.put( Option.SIZE_1, new Characteristics( UINT, 0, 4 ));
CHARACTERISTICS.put( Option.ENDPOINT_ID_1, new Characteristics( OPAQUE, 0, 8 ));
CHARACTERISTICS.put( Option.ENDPOINT_ID_2, new Characteristics( OPAQUE, 0, 8 ));
}
public static Type getType(int optionNumber) throws IllegalArgumentException {
Characteristics characteristics = CHARACTERISTICS.get(optionNumber);
if (characteristics == null) {
throw new IllegalArgumentException(String.format(UNKNOWN_OPTION, optionNumber));
} else {
return characteristics.getType();
}
}
public static int getMinLength(int optionNumber) throws IllegalArgumentException {
Characteristics characteristics = CHARACTERISTICS.get(optionNumber);
if (characteristics == null) {
throw new IllegalArgumentException(String.format(UNKNOWN_OPTION, optionNumber));
} else {
return characteristics.getMinLength();
}
}
public static int getMaxLength(int optionNumber) throws IllegalArgumentException {
Characteristics characteristics = CHARACTERISTICS.get(optionNumber);
if (characteristics == null) {
throw new IllegalArgumentException(String.format(UNKNOWN_OPTION, optionNumber));
} else {
return characteristics.getMaxLength();
}
}
public static boolean isDefaultValue(int optionNumber, byte[] value) {
if (optionNumber == Option.URI_PORT && Arrays.equals(value, ENCODED_URI_PORT_DEFAULT)) {
return true;
} else if (optionNumber == Option.MAX_AGE && Arrays.equals(value, ENCODED_MAX_AGE_DEFAULT)) {
return true;
} else if (optionNumber == Option.URI_HOST) {
String hostName = new String(value, CoapMessage.CHARSET);
if (hostName.startsWith("[") && hostName.endsWith("]")) {
hostName = hostName.substring(1, hostName.length() - 1);
}
if (InetAddresses.isInetAddress(hostName)) {
return true;
}
}
return false;
}
protected byte[] value;
protected OptionValue(int optionNumber, byte[] value, boolean allowDefault) throws IllegalArgumentException {
if (!allowDefault && OptionValue.isDefaultValue(optionNumber, value)) {
throw new IllegalArgumentException(String.format(VALUE_IS_DEFAULT_VALUE, optionNumber));
}
if (getMinLength(optionNumber) > value.length || getMaxLength(optionNumber) < value.length) {
throw new IllegalArgumentException(String.format(OUT_OF_ALLOWED_RANGE, value.length, optionNumber,
getMinLength(optionNumber), getMaxLength(optionNumber)));
}
this.value = value;
}
public byte[] getValue() {
return this.value;
}
public abstract T getDecodedValue();
@Override
public abstract int hashCode();
@Override
public abstract boolean equals(Object object);
@Override
public String toString() {
return "" + this.getDecodedValue();
}
}

View File

@ -0,0 +1,87 @@
package com.fastbee.coap.model.options;
import com.fastbee.coap.model.CoapMessage;
import lombok.extern.slf4j.Slf4j;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.Arrays;
import java.util.Locale;
@Slf4j
public class StringOptionValue extends OptionValue<String> {
public StringOptionValue(int optionNumber, byte[] value) throws IllegalArgumentException {
this(optionNumber, value, false);
}
public StringOptionValue(int optionNumber, byte[] value, boolean allowDefault) throws IllegalArgumentException {
super(optionNumber, value, allowDefault);
log.debug("String Option (#{}) created with value: '{}'.", optionNumber, this.getDecodedValue());
}
public StringOptionValue(int optionNumber, String value) throws IllegalArgumentException{
this(optionNumber, optionNumber == Option.URI_HOST ?
convertToByteArrayWithoutPercentEncoding(value.toLowerCase(Locale.ENGLISH)) :
((optionNumber == Option.URI_PATH || optionNumber == Option.URI_QUERY) ?
convertToByteArrayWithoutPercentEncoding(value) :
value.getBytes(CoapMessage.CHARSET)));
}
@Override
public String getDecodedValue() {
return new String(value, CoapMessage.CHARSET);
}
@Override
public int hashCode() {
return getDecodedValue().hashCode();
}
@Override
public boolean equals(Object object) {
if (!(object instanceof StringOptionValue))
return false;
StringOptionValue other = (StringOptionValue) object;
return Arrays.equals(this.getValue(), other.getValue());
}
public static byte[] convertToByteArrayWithoutPercentEncoding(String s) throws IllegalArgumentException{
log.debug("With percent encoding: {}", s);
ByteArrayInputStream in = new ByteArrayInputStream(s.getBytes(CoapMessage.CHARSET));
ByteArrayOutputStream out = new ByteArrayOutputStream();
int i;
do {
i = in.read();
//-1 indicates end of stream
if (i == -1) {
break;
}
//0x25 = '%'
if (i == 0x25) {
//Character.digit returns the integer value encoded as in.read(). Since we know that percent encoding
//uses bytes from 0x0 to 0xF (i.e. 0 to 15) the radix must be 16.
int d1 = Character.digit(in.read(), 16);
int d2 = Character.digit(in.read(), 16);
if (d1 == -1 || d2 == -1) {
//Unexpected end of stream (at least one byte missing after '%')
throw new IllegalArgumentException("Invalid percent encoding in: " + s);
}
//Write decoded value to output stream (e.g. sequence [0x02, 0x00] results into byte 0x20
out.write((d1 << 4) | d2);
} else {
out.write(i);
}
} while(true);
byte[] result = out.toByteArray();
log.debug("Without percent encoding: {}", new String(result, CoapMessage.CHARSET));
return result;
}
}

View File

@ -0,0 +1,51 @@
package com.fastbee.coap.model.options;
import lombok.extern.slf4j.Slf4j;
import java.math.BigInteger;
import java.util.Arrays;
@Slf4j
public class UintOptionValue extends OptionValue<Long> {
/**
* Corresponds to a value of <code>-1</code> to indicate that there is no value for that option set.
*/
public static final long UNDEFINED = -1;
public UintOptionValue(int optionNumber, byte[] value) throws IllegalArgumentException {
this(optionNumber, shortenValue(value), false);
}
public UintOptionValue(int optionNumber, byte[] value, boolean allowDefault) throws IllegalArgumentException {
super(optionNumber, shortenValue(value), allowDefault);
log.debug("Uint Option (#{}) created with value: {}", optionNumber, this.getDecodedValue());
}
@Override
public Long getDecodedValue() {
return new BigInteger(1, value).longValue();
}
@Override
public int hashCode() {
return getDecodedValue().hashCode();
}
@Override
public boolean equals(Object object) {
if (!(object instanceof UintOptionValue))
return false;
UintOptionValue other = (UintOptionValue) object;
return Arrays.equals(this.getValue(), other.getValue());
}
public static byte[] shortenValue(byte[] value) {
int index = 0;
while(index < value.length - 1 && value[index] == 0)
index++;
return Arrays.copyOfRange(value, index, value.length);
}
}

View File

@ -0,0 +1,27 @@
package com.fastbee.coap.server;
import com.fastbee.coap.codec.CoapMessageDecoder;
import com.fastbee.coap.codec.CoapMessageEncoder;
import com.fastbee.coap.handler.ReqDispatcher;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.DatagramChannel;
public class CoapServerChannelInitializer extends ChannelInitializer<DatagramChannel> {
private final ResourceRegistry resourceRegistry;
public CoapServerChannelInitializer(ResourceRegistry resourceRegistry) {
super();
this.resourceRegistry = resourceRegistry;
}
@Override
protected void initChannel(DatagramChannel datagramChannel) throws Exception {
ChannelPipeline p = datagramChannel.pipeline();
p.addLast(new CoapMessageEncoder())
.addLast(new CoapMessageDecoder())
.addLast(new ReqDispatcher(this.resourceRegistry));
}
}

View File

@ -0,0 +1,77 @@
package com.fastbee.coap.server;
import com.fastbee.coap.handler.ResourceHandler;
import com.fastbee.coap.model.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import static com.fastbee.coap.model.MessageCode.BAD_REQUEST_400;
@Slf4j
@Component
public class ResourceRegistry{
private final Map<String, ResourceHandler> handlers = new HashMap<String, ResourceHandler>();
public void register(ResourceHandler handler) {
handlers.put(handler.getPath(), handler);
}
public CoapMessage respond(CoapRequest request) {
// find the URI
String url = request.getUriPath();
log.debug("requested URL : {}", url);
if (url.length() < 1) {
// 4.00 !
return CoapResponse.createErrorResponse(request.getMessageType(), BAD_REQUEST_400,"no URL !");
}
if (".well-known/core".equalsIgnoreCase(url)) {
// discovery !
CoapResponse coapResponse = CoapResponse.createErrorResponse(MessageType.ACK, MessageCode.CONTENT_205,"");
coapResponse.setContent(getDiscovery());
coapResponse.setSender(request.getSender());
coapResponse.setMessageID(request.getMessageID());
coapResponse.setToken(request.getToken());
return coapResponse;
} else {
ResourceHandler handler = handlers.get(url);
if (handler == null) {
// 4.04 !
CoapResponse coapResponse = CoapResponse.createErrorResponse(MessageType.ACK, MessageCode.NOT_FOUND_404,"");
coapResponse.setContent("not found !".getBytes());
coapResponse.setSender(request.getSender());
return coapResponse;
} else {
return handler.handle(request);
}
}
}
private byte[] getDiscovery() {
StringBuilder b = new StringBuilder();
boolean first = true;
for (Map.Entry<String, ResourceHandler> e : handlers.entrySet()) {
// ex :</link1>;if="If1";rt="Type1 Type2";title="Link test resource",
if (first) {
first = false;
} else {
b.append(",");
}
ResourceHandler h = e.getValue();
b.append("</").append(h.getPath()).append(">");
if (h.getInterface() != null) {
b.append(";if=\"").append(h.getInterface()).append("\"");
}
if (h.getResourceType() != null) {
b.append(";rt=\"").append(h.getResourceType()).append("\"");
}
if (h.getTitle() != null) {
b.append(";title=\"").append(h.getTitle()).append("\"");
}
}
return b.toString().getBytes(StandardCharsets.UTF_8);
}
}

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.fastbee</groupId>
<artifactId>fastbee-server</artifactId>
<version>3.8.5</version>
</parent>
<artifactId>http-server</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.fastbee</groupId>
<artifactId>iot-server-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session</artifactId>
<version>1.3.5.RELEASE</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,55 @@
package com.fastbee.http.auth;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.*;
import io.netty.util.CharsetUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
@Slf4j
@Component
public class BasicAuth {
@Value("${server.http.auth.user.name)")
private String user;
@Value("${server.http.auth.user.password)")
private String password;
private static final String REALM = "FastbeeBasic";
public boolean auth(ChannelHandlerContext ctx, String authHeader) {
if (authHeader != null && authHeader.startsWith("Basic ")) {
String encodedCredentials = authHeader.substring("Basic ".length());
String credentials = new String(Base64.getDecoder().decode(encodedCredentials), StandardCharsets.UTF_8);
String[] parts = credentials.split(":", 2);
if (parts.length == 2 && validateUser(parts[0], parts[1])) {
// 用户名和密码验证通过,继续处理请求
return true;
} else {
// 发送401 Unauthorized响应
sendUnauthorizedResponse(ctx);
}
} else {
// 发送401 Unauthorized响应
sendUnauthorizedResponse(ctx);
}
return false;
}
private boolean validateUser(String username, String password) {
// 实现用户验证逻辑
return username.equals(user) && password.equals(this.password);
}
public void sendUnauthorizedResponse(ChannelHandlerContext ctx) {
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.UNAUTHORIZED,
Unpooled.copiedBuffer("Unauthorized\r\n", CharsetUtil.UTF_8));
response.headers().set(HttpHeaderNames.WWW_AUTHENTICATE, "Basic realm=\"" + REALM + "\"");
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
}

View File

@ -0,0 +1,47 @@
package com.fastbee.http.auth;
import com.fastbee.http.utils.DigestAuthUtil;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.*;
import io.netty.util.CharsetUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.security.NoSuchAlgorithmException;
import java.util.UUID;
@Slf4j
@Component
public class DigestAuth {
private static final String REALM = "FastbeeDigest";
private static final String NONCE = UUID.randomUUID().toString();
@Value("${server.http.auth.user.name)")
private String user;
@Value("${server.http.auth.user.password)")
private String password;
public boolean auth(ChannelHandlerContext ctx, FullHttpRequest request) throws NoSuchAlgorithmException {
if (new DigestAuthUtil().doAuthenticatePlainTextPassword(request,
password)) {
return true;
} else {
// 发送401 Unauthorized响应
sendUnauthorizedResponse(ctx);
return false;
}
}
public void sendUnauthorizedResponse(ChannelHandlerContext ctx) {
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.UNAUTHORIZED,
Unpooled.copiedBuffer("Unauthorized\r\n", CharsetUtil.UTF_8));
response.headers().set(HttpHeaderNames.WWW_AUTHENTICATE,
"Digest realm=\"" + REALM + "\", nonce=\"" + NONCE + "\"");
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
}

View File

@ -0,0 +1,9 @@
package com.fastbee.http.handler;
import io.netty.handler.codec.http.FullHttpRequest;
import javax.servlet.http.HttpSession;
public interface IHttpReqHandler {
public void processMsg(FullHttpRequest req, HttpSession session);
}

View File

@ -0,0 +1,9 @@
package com.fastbee.http.handler;
import io.netty.handler.codec.http.FullHttpResponse;
import java.text.ParseException;
public interface IHttpResHandler {
public void processMsg(FullHttpResponse req) throws ParseException;
}

View File

@ -0,0 +1,47 @@
package com.fastbee.http.handler.req;
import com.alibaba.fastjson2.JSON;
import com.fastbee.common.core.thingsModel.ThingsModelSimpleItem;
import com.fastbee.http.service.IHttpMqttService;
import com.fastbee.http.handler.IHttpReqHandler;
import com.fastbee.http.server.HttpListener;
import com.fastbee.iot.domain.Device;
import com.fastbee.iot.service.IDeviceService;
import io.netty.handler.codec.http.FullHttpRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpSession;
import java.nio.charset.StandardCharsets;
import java.util.List;
@Slf4j
@Component
public class EventHttpReqHandler implements InitializingBean, IHttpReqHandler {
@Autowired
private HttpListener httpListener;
@Autowired
private IHttpMqttService mqttService;
@Autowired
private IDeviceService deviceService;
@Override
public void afterPropertiesSet() throws Exception {
String uri = "/event/post";
httpListener.addRequestProcessor(uri, this);
}
@Override
public void processMsg(FullHttpRequest req, HttpSession session) {
String serialNumber = (String) session.getAttribute("SerialNumber");
Device device = deviceService.selectDeviceBySerialNumber(serialNumber);
if (device != null) {
List<ThingsModelSimpleItem> thingsModelSimpleItems = JSON.parseArray(req.content().toString(StandardCharsets.UTF_8), ThingsModelSimpleItem.class);
mqttService.publishEvent(device,thingsModelSimpleItems);
}
}
}

View File

@ -0,0 +1,55 @@
package com.fastbee.http.handler.req;
import com.alibaba.fastjson2.JSON;
import com.fastbee.common.enums.DeviceStatus;
import com.fastbee.common.utils.DateUtils;
import com.fastbee.http.service.IHttpMqttService;
import com.fastbee.http.handler.IHttpReqHandler;
import com.fastbee.http.manager.HttpSessionManager;
import com.fastbee.http.server.HttpListener;
import com.fastbee.iot.domain.Device;
import com.fastbee.iot.service.IDeviceService;
import io.netty.handler.codec.http.FullHttpRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpSession;
import java.nio.charset.StandardCharsets;
@Slf4j
@Component
public class InfoHttpReqHandler implements InitializingBean, IHttpReqHandler {
@Autowired
private HttpListener httpListener;
@Autowired
private HttpSessionManager sessionManager;
@Autowired
private IHttpMqttService mqttService;
@Autowired
private IDeviceService deviceService;
@Override
public void afterPropertiesSet() throws Exception {
String uri = "/info/post";
httpListener.addRequestProcessor(uri, this);
}
@Override
public void processMsg(FullHttpRequest req, HttpSession session) {
//设备上报基本信息后保存到会话中
Device device = JSON.parseObject(req.content().toString(StandardCharsets.UTF_8), Device.class);
session.setAttribute("SerialNumber", device.getSerialNumber());
session.setAttribute("productId", device.getProductId());
device.setActiveTime(DateUtils.getNowDate());
device.setStatus(DeviceStatus.ONLINE.getType());
sessionManager.saveSession(session.getId(), session);
//更新设备信息
deviceService.updateDevice(device);
//发布到mqtt
mqttService.publishInfo(device);
}
}

View File

@ -0,0 +1,40 @@
package com.fastbee.http.handler.req;
import com.fastbee.common.utils.DateUtils;
import com.fastbee.http.handler.IHttpReqHandler;
import com.fastbee.http.server.HttpListener;
import com.fastbee.iot.domain.Device;
import com.fastbee.iot.service.IDeviceService;
import io.netty.handler.codec.http.FullHttpRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpSession;
@Slf4j
@Component
public class KeepaliveHttpReqHandler implements InitializingBean, IHttpReqHandler {
@Autowired
private HttpListener httpListener;
@Autowired
private IDeviceService deviceService;
@Override
public void processMsg(FullHttpRequest req, HttpSession session) {
String serialNumber = (String) session.getAttribute("SerialNumber");
Device device = deviceService.selectDeviceBySerialNumber(serialNumber);
if (device != null) {
device.setActiveTime(DateUtils.getNowDate());
deviceService.updateDevice(device);
}
}
@Override
public void afterPropertiesSet() throws Exception {
String uri = "/keepalive";
httpListener.addRequestProcessor(uri, this);
}
}

View File

@ -0,0 +1,46 @@
package com.fastbee.http.handler.req;
import com.alibaba.fastjson2.JSON;
import com.fastbee.common.core.thingsModel.ThingsModelSimpleItem;
import com.fastbee.http.service.IHttpMqttService;
import com.fastbee.http.handler.IHttpReqHandler;
import com.fastbee.http.server.HttpListener;
import com.fastbee.iot.domain.Device;
import com.fastbee.iot.service.IDeviceService;
import io.netty.handler.codec.http.FullHttpRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpSession;
import java.nio.charset.StandardCharsets;
import java.util.List;
@Slf4j
@Component
public class MonitorHttpReqHandler implements InitializingBean, IHttpReqHandler {
@Autowired
private HttpListener httpListener;
@Autowired
private IHttpMqttService mqttService;
@Autowired
private IDeviceService deviceService;
@Override
public void afterPropertiesSet() throws Exception {
String uri = "/monitor/post";
httpListener.addRequestProcessor(uri, this);
}
@Override
public void processMsg(FullHttpRequest req, HttpSession session) {
String serialNumber = (String) session.getAttribute("SerialNumber");
Device device = deviceService.selectDeviceBySerialNumber(serialNumber);
if (device != null) {
List<ThingsModelSimpleItem> thingsModelSimpleItems = JSON.parseArray(req.content().toString(StandardCharsets.UTF_8), ThingsModelSimpleItem.class);
mqttService.publishMonitor(device,thingsModelSimpleItems);
}
}
}

View File

@ -0,0 +1,47 @@
package com.fastbee.http.handler.req;
import com.alibaba.fastjson2.JSON;
import com.fastbee.common.core.thingsModel.ThingsModelSimpleItem;
import com.fastbee.http.service.IHttpMqttService;
import com.fastbee.http.handler.IHttpReqHandler;
import com.fastbee.http.server.HttpListener;
import com.fastbee.iot.domain.Device;
import com.fastbee.iot.service.IDeviceService;
import io.netty.handler.codec.http.FullHttpRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpSession;
import java.nio.charset.StandardCharsets;
import java.util.List;
@Slf4j
@Component
public class PropertyHttpReqHandler implements InitializingBean, IHttpReqHandler {
@Autowired
private HttpListener httpListener;
@Autowired
private IHttpMqttService mqttService;
@Autowired
private IDeviceService deviceService;
@Override
public void afterPropertiesSet() throws Exception {
String uri = "/property/post";
httpListener.addRequestProcessor(uri, this);
}
@Override
public void processMsg(FullHttpRequest req, HttpSession session) {
String serialNumber = (String) session.getAttribute("SerialNumber");
Device device = deviceService.selectDeviceBySerialNumber(serialNumber);
if (device != null) {
List<ThingsModelSimpleItem> thingsModelSimpleItems = JSON.parseArray(req.content().toString(StandardCharsets.UTF_8), ThingsModelSimpleItem.class);
mqttService.publishProperty(device, thingsModelSimpleItems, 0);
}
}
}

View File

@ -0,0 +1,54 @@
package com.fastbee.http.manager;
import com.fastbee.common.core.redis.RedisCache;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Map;
@Slf4j
@Component
public class HttpSessionManager {
@Autowired
private RedisCache sessionStore;
private final ObjectMapper objectMapper = new ObjectMapper();
public HttpSessionManager() {
}
public String createSession() {
String sessionId = generateSessionId();
saveSession(sessionId, new NettyHttpSession(sessionId));
return sessionId;
}
public HttpSession getSession(String sessionId) {
String sessionData = sessionStore.getCacheObject("session:" + sessionId);
if (sessionData != null) {
try {
return new NettyHttpSession(sessionId, objectMapper.readValue(sessionData, Map.class));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return null;
}
public void saveSession(String sessionId, HttpSession session) {
try {
String sessionData = objectMapper.writeValueAsString(((NettyHttpSession) session).getAttributeMap());
sessionStore.setCacheObject("session:" + sessionId, sessionData);
sessionStore.expire("session:" + sessionId, 30 * 60);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private String generateSessionId() {
return Long.toHexString(Double.doubleToLongBits(Math.random()));
}
}

View File

@ -0,0 +1,117 @@
package com.fastbee.http.manager;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionContext;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
public class NettyHttpSession implements HttpSession {
private final String id;
private final Map<String, Object> attributes;
public NettyHttpSession(String id) {
this(id, new HashMap<>());
}
public NettyHttpSession(String id, Map<String, Object> attributes) {
this.id = id;
this.attributes = attributes;
}
@Override
public long getCreationTime() {
// Implement as needed
return 0;
}
@Override
public String getId() {
return id;
}
@Override
public long getLastAccessedTime() {
// Implement as needed
return 0;
}
@Override
public javax.servlet.ServletContext getServletContext() {
// Implement as needed
return null;
}
@Override
public void setMaxInactiveInterval(int interval) {
// Implement as needed
}
@Override
public int getMaxInactiveInterval() {
// Implement as needed
return 0;
}
@Override
public HttpSessionContext getSessionContext() {
return null;
}
@Override
public void invalidate() {
// Implement as needed
}
@Override
public boolean isNew() {
// Implement as needed
return false;
}
@Override
public Object getAttribute(String name) {
return attributes.get(name);
}
@Override
public Object getValue(String name) {
return attributes.get(name);
}
@Override
public Enumeration<String> getAttributeNames() {
// Implement as needed
return null;
}
@Override
public String[] getValueNames() {
// Implement as needed
return new String[0];
}
@Override
public void setAttribute(String name, Object value) {
attributes.put(name, value);
}
@Override
public void putValue(String name, Object value) {
attributes.put(name, value);
}
@Override
public void removeAttribute(String name) {
attributes.remove(name);
}
@Override
public void removeValue(String name) {
attributes.remove(name);
}
public Map<String, Object> getAttributeMap() {
return attributes;
}
}

View File

@ -0,0 +1,55 @@
package com.fastbee.http.server;
import com.fastbee.http.handler.IHttpReqHandler;
import com.fastbee.http.handler.IHttpResHandler;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpSession;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Component
public class HttpListener {
private static final Map<String, IHttpReqHandler> requestProcessorMap = new ConcurrentHashMap<>();
private static final Map<String, IHttpResHandler> responseProcessorMap = new ConcurrentHashMap<>();
public void addRequestProcessor(String method, IHttpReqHandler processor) {
requestProcessorMap.put(method, processor);
}
public void addResponseProcessor(String method, IHttpResHandler processor) {
responseProcessorMap.put(method, processor);
}
@Async("taskExecutor")
public void processRequest(FullHttpRequest req, HttpSession session) {
String uri = req.uri();
IHttpReqHandler sipRequestProcessor = requestProcessorMap.get(uri);
if (sipRequestProcessor == null) {
log.warn("不支持的uri:{}", uri);
return;
}
requestProcessorMap.get(uri).processMsg(req, session);
}
@Async("taskExecutor")
public void processResponse(FullHttpResponse response) {
HttpResponseStatus status = response.status();
// 响应成功
if ((status.code() >= HttpResponseStatus.OK.code()) && (status.code() < HttpResponseStatus.MULTIPLE_CHOICES.code())) {
log.info("response{},", response.content());
log.info("接收response响应status{}", status);
} else if ((status.code() >= HttpResponseStatus.CONTINUE.code()) && (status.code() < HttpResponseStatus.OK.code())) {
log.info("接收response响应status{}", status);
} else {
log.warn("接收到失败的response响应status{}", status);
}
}
}

View File

@ -0,0 +1,54 @@
package com.fastbee.http.server;
import com.fastbee.server.Server;
import io.netty.bootstrap.AbstractBootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioChannelOption;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.util.concurrent.DefaultThreadFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
public class HttpServer extends Server {
@Autowired
private HttpServerHandler httpServerHandler;
@Override
protected AbstractBootstrap initialize() {
bossGroup = new NioEventLoopGroup(1, new DefaultThreadFactory(config.name, Thread.MAX_PRIORITY));
workerGroup = new NioEventLoopGroup(config.workerCore, new DefaultThreadFactory(config.name, Thread.MAX_PRIORITY));
if (config.businessCore > 0) {
businessService = new ThreadPoolExecutor(config.businessCore, config.businessCore, 1L,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new DefaultThreadFactory(config.name + "-B", true, Thread.NORM_PRIORITY));
}
return new ServerBootstrap()
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(NioChannelOption.SO_REUSEADDR, true)
.option(NioChannelOption.SO_BACKLOG, 1024)
.childOption(NioChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new HttpServerCodec());
p.addLast(new HttpObjectAggregator(65536));
p.addLast(httpServerHandler);
}
});
}
}

View File

@ -0,0 +1,98 @@
package com.fastbee.http.server;
import com.fastbee.http.auth.BasicAuth;
import com.fastbee.http.auth.DigestAuth;
import com.fastbee.http.manager.NettyHttpSession;
import com.fastbee.http.manager.HttpSessionManager;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.codec.http.cookie.DefaultCookie;
import io.netty.handler.codec.http.cookie.ServerCookieEncoder;
import io.netty.handler.codec.http.cookie.ServerCookieDecoder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpSession;
import java.util.Set;
@Slf4j
@Component
public class HttpServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
@Autowired
private HttpSessionManager sessionManager;
@Autowired
private HttpListener httpListener;
@Autowired
private BasicAuth basicAuth;
@Autowired
private DigestAuth digestAuth;
@Value("${server.http.auth.type)")
private String authtype;
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception {
String sessionId = null;
HttpSession session;
String cookieHeader = req.headers().get(HttpHeaderNames.COOKIE);
// 使用ServerCookieDecoder解码Cookie字符串
Set<Cookie> cookies = ServerCookieDecoder.STRICT.decode(cookieHeader);
// 遍历Cookies
for (Cookie cookie : cookies) {
if ("JSESSIONID".equals(cookie.name())) {
sessionId = cookie.value();
}
}
// 未认证
if (sessionId == null) {
String authHeader = req.headers().get(HttpHeaderNames.AUTHORIZATION);
boolean check = false;
if (authHeader != null && authHeader.startsWith("Basic ")) {
if (basicAuth.auth(ctx, authHeader)) {
check = true;
}
} else if (authHeader != null && authHeader.startsWith("Digest ")) {
if (digestAuth.auth(ctx, req)) {
check = true;
}
}
if (check) {
sessionId = sessionManager.createSession();
session = sessionManager.getSession(sessionId);
((NettyHttpSession) session).setAttribute("user", "John Doe");
// 创建一个Cookie
Cookie sessionCookie = new DefaultCookie("JSESSIONID", sessionId);
// 设置一些属性,比如路径和最大年龄
sessionCookie.setPath("/");
sessionCookie.setMaxAge(30 * 60); // 30分钟
// 编码Cookie并添加到响应头中
FullHttpResponse res = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
res.headers().add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode(sessionCookie));
ctx.writeAndFlush(res);
} else {
if ("Basic".equals(authtype)) {
basicAuth.sendUnauthorizedResponse(ctx);
} else {
digestAuth.sendUnauthorizedResponse(ctx);
}
}
} else {
// 已经认证
session = sessionManager.getSession(sessionId);
// http 路由处理函数
httpListener.processRequest(req, session);
}
}
}

View File

@ -0,0 +1,18 @@
package com.fastbee.http.service;
import com.fastbee.common.core.thingsModel.ThingsModelSimpleItem;
import com.fastbee.iot.domain.Device;
import java.util.List;
public interface IHttpMqttService {
void publishInfo(Device device);
void publishStatus(Device device, int deviceStatus);
void publishEvent(Device device, List<ThingsModelSimpleItem> thingsList);
void publishProperty(Device device, List<ThingsModelSimpleItem> thingsList, int delay);
void publishMonitor(Device device, List<ThingsModelSimpleItem> thingsList);
}

View File

@ -0,0 +1,73 @@
package com.fastbee.http.service.impl;
import com.alibaba.fastjson2.JSON;
import com.fastbee.common.core.thingsModel.ThingsModelSimpleItem;
import com.fastbee.common.enums.TopicType;
import com.fastbee.common.utils.gateway.mq.TopicsUtils;
import com.fastbee.http.service.IHttpMqttService;
import com.fastbee.iot.domain.Device;
import com.fastbee.mqttclient.PubMqttClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.List;
@Slf4j
@Service
public class HttpMqttServiceImpl implements IHttpMqttService {
@Resource
private PubMqttClient mqttClient;
@Resource
private TopicsUtils topicsUtils;
@Override
public void publishInfo(Device device) {
device.setRssi(0);
device.setStatus(3);
device.setFirmwareVersion(BigDecimal.valueOf(1.0));
String topic = topicsUtils.buildTopic(device.getProductId(), device.getSerialNumber(), TopicType.DEV_INFO_POST);
mqttClient.publish(1, false, topic, JSON.toJSONString(device));
}
@Override
public void publishStatus(Device device, int deviceStatus) {
}
@Override
public void publishEvent(Device device, List<ThingsModelSimpleItem> thingsList) {
String topic = topicsUtils.buildTopic(device.getProductId(), device.getSerialNumber(), TopicType.DEV_EVENT_POST);
if (thingsList == null) {
mqttClient.publish(1, false, topic, "");
} else {
mqttClient.publish(1, false, topic, JSON.toJSONString(thingsList));
}
}
@Override
public void publishProperty(Device device, List<ThingsModelSimpleItem> thingsList, int delay) {
String pre = "";
if (delay > 0) {
pre = "$delayed/" + String.valueOf(delay) + "/";
}
String topic = topicsUtils.buildTopic(device.getProductId(), device.getSerialNumber(), TopicType.DEV_PROPERTY_POST);
if (thingsList == null) {
mqttClient.publish(1, false, topic, "");
} else {
mqttClient.publish(1, false, topic, JSON.toJSONString(thingsList));
}
}
@Override
public void publishMonitor(Device device, List<ThingsModelSimpleItem> thingsList) {
String topic = topicsUtils.buildTopic(device.getProductId(), device.getSerialNumber(), TopicType.DEV_PROPERTY_POST);
if (thingsList == null) {
mqttClient.publish(1, false, topic, "");
} else {
mqttClient.publish(1, false, topic, JSON.toJSONString(thingsList));
}
}
}

View File

@ -0,0 +1,197 @@
package com.fastbee.http.utils;
import gov.nist.core.InternalErrorHandler;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.http.*;
import io.netty.util.CharsetUtil;
import lombok.extern.slf4j.Slf4j;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DecimalFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Implements the HTTP digest authentication method server side functionality.
*/
@Slf4j
public class DigestAuthUtil {
private final MessageDigest messageDigest;
public static final String DEFAULT_ALGORITHM = "MD5";
public static final String DEFAULT_SCHEME = "Digest";
/** to hex converter */
private static final char[] toHex = { '0', '1', '2', '3', '4', '5', '6',
'7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
/**
* Default constructor.
* @throws NoSuchAlgorithmException
*/
public DigestAuthUtil()
throws NoSuchAlgorithmException {
messageDigest = MessageDigest.getInstance(DEFAULT_ALGORITHM);
}
public static String toHexString(byte b[]) {
int pos = 0;
char[] c = new char[b.length * 2];
for (byte value : b) {
c[pos++] = toHex[(value >> 4) & 0x0F];
c[pos++] = toHex[value & 0x0f];
}
return new String(c);
}
/**
* Generate the challenge string.
*
* @return a generated nonce.
*/
private String generateNonce() {
// Get the time of day and run MD5 over it.
Date date = new Date();
long time = date.getTime();
Random rand = new Random();
long pad = rand.nextLong();
String nonceString = (new Long(time)).toString()
+ (new Long(pad)).toString();
byte[] mdbytes = messageDigest.digest(nonceString.getBytes());
// Convert the mdbytes array into a hex string.
return toHexString(mdbytes);
}
public FullHttpResponse generateChallenge(String realm) {
try {
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.UNAUTHORIZED,
Unpooled.copiedBuffer("Unauthorized\r\n", CharsetUtil.UTF_8));
response.headers().set(HttpHeaderNames.WWW_AUTHENTICATE,
"Digest realm=\"" + realm + "\", nonce=\"" + generateNonce()
+ "\", opaque=\"\", stale=\"FALSE\", algorithm=\"" + DEFAULT_ALGORITHM + "\"");
return response;
} catch (Exception ex) {
InternalErrorHandler.handleException(ex);
}
return null;
}
/**
* Authenticate the inbound request.
*
* @param request - the request to authenticate.
* @param hashedPassword -- the MD5 hashed string of username:realm:plaintext password.
*
* @return true if authentication succeded and false otherwise.
*/
public boolean doAuthenticateHashedPassword(FullHttpRequest request, String hashedPassword) {
String authHeader = request.headers().get(HttpHeaderNames.AUTHORIZATION);
if ( authHeader == null ) return false;
Map<String, String> params = parseDigestParameters(authHeader);
String realm = params.get("realm");
String username = params.get("username");
if ( username == null || realm == null ) {
return false;
}
String nonce = params.get("nonce");
String uri = params.get("uri");
if (uri == null) {
return false;
}
String A2 = request.method() + ":" + uri;
byte[] mdbytes = messageDigest.digest(A2.getBytes());
String HA2 = toHexString(mdbytes);
String cnonce = params.get("cnonce");
String KD = hashedPassword + ":" + nonce;
if (cnonce != null) {
KD += ":" + cnonce;
}
KD += ":" + HA2;
mdbytes = messageDigest.digest(KD.getBytes());
String mdString = toHexString(mdbytes);
String response = params.get("response");
return mdString.equals(response);
}
/**
* Authenticate the inbound request given plain text password.
*
* @param request - the request to authenticate.
* @param pass -- the plain text password.
*
* @return true if authentication succeded and false otherwise.
*/
public boolean doAuthenticatePlainTextPassword(FullHttpRequest request, String pass) {
String authHeader = request.headers().get(HttpHeaderNames.AUTHORIZATION);
if ( authHeader == null ) return false;
Map<String, String> params = parseDigestParameters(authHeader);
String realm = params.get("realm");
String username = params.get("username");
String nonce = params.get("nonce");
String uri = params.get("uri");
if (uri == null) {
return false;
}
// qop 保护质量 包含auth默认的和auth-int增加了报文完整性检测两种策略
String qop = params.get("qop");
// nonce计数器是一个16进制的数值表示同一nonce下客户端发送出请求的数量
int nc = Integer.parseInt(params.get("nc"));
String ncStr = new DecimalFormat("00000000").format(nc);
String A1 = username + ":" + realm + ":" + pass;
String A2 = request.method() + ":" + uri;
byte[] mdbytes = messageDigest.digest(A1.getBytes());
String HA1 = toHexString(mdbytes);
mdbytes = messageDigest.digest(A2.getBytes());
String HA2 = toHexString(mdbytes);
String cnonce = params.get("cnonce");
String KD = HA1 + ":" + nonce;
if (qop != null && qop.equals("auth") ) {
if (nc != -1) {
KD += ":" + ncStr;
}
if (cnonce != null) {
KD += ":" + cnonce;
}
KD += ":" + qop;
}
KD += ":" + HA2;
mdbytes = messageDigest.digest(KD.getBytes());
String mdString = toHexString(mdbytes);
String response = params.get("response");
return mdString.equals(response);
}
private Map<String, String> parseDigestParameters(String authHeader) {
Map<String, String> params = new HashMap<>();
Pattern pattern = Pattern.compile("(\\w+)=(?:\"([^\"]*)\"|([^,]+))");
Matcher matcher = pattern.matcher(authHeader);
while (matcher.find()) {
String key = matcher.group(1);
String value = matcher.group(2) != null ? matcher.group(2) : matcher.group(3);
params.put(key, value);
}
return params;
}
}

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>fastbee-server</artifactId>
<groupId>com.fastbee</groupId>
<version>3.8.5</version>
</parent>
<artifactId>iot-server-core</artifactId>
<dependencies>
<dependency>
<groupId>com.fastbee</groupId>
<artifactId>base-server</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fastbee</groupId>
<artifactId>fastbee-mq</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,77 @@
package com.fastbee.server;
import com.fastbee.server.config.NettyConfig;
import io.netty.bootstrap.AbstractBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ExecutorService;
/**
* 基础服务器启动类
*
* @Author guanshubiao
* @Date 2022/9/12 20:22
*/
@Slf4j
@NoArgsConstructor
public abstract class Server {
protected EventLoopGroup bossGroup;
protected EventLoopGroup workerGroup;
protected ExecutorService businessService;
protected boolean isRunning;
public NettyConfig config;
protected Server(NettyConfig config){
this.config = config;
}
/*初始化方法*/
protected abstract AbstractBootstrap initialize();
public synchronized boolean start() {
if (isRunning) {
log.warn("=>服务:{},在端口:{},已经运行", config.name, config.port);
return isRunning;
}
AbstractBootstrap bootstrap = initialize();
ChannelFuture future = bootstrap.bind(config.port).awaitUninterruptibly();
future.channel().closeFuture().addListener(event -> {
if (isRunning) {
stop();
}
});
if (isRunning = future.isSuccess()) {
log.info("=>服务:{},在端口:{},启动成功!", config.name, config.port);
return isRunning;
}
if (future.cause() != null) {
log.error("服务启动失败", future.cause());
}
return isRunning;
}
public synchronized void stop() {
isRunning = false;
bossGroup.shutdownGracefully();
if (workerGroup != null) {
workerGroup.shutdownGracefully();
}
if (businessService != null) {
businessService.shutdown();
}
log.warn("=>服务:{},在端口:{},已经停止!", config.name, config.port);
}
}

View File

@ -0,0 +1,84 @@
package com.fastbee.server;
import com.fastbee.common.constant.FastBeeConstant;
import com.fastbee.server.config.NettyConfig;
import com.fastbee.server.handler.*;
import io.netty.bootstrap.AbstractBootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioChannelOption;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.concurrent.DefaultThreadFactory;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* TCP服务
* 粘包处理为 分隔符和固定长度
* 需要其他方式处理粘包,按照流程添加
*
* @author bill
*/
public class TCPServer extends Server {
public TCPServer(NettyConfig config) {
super(config);
}
@Override
protected AbstractBootstrap initialize() {
bossGroup = new NioEventLoopGroup(1, new DefaultThreadFactory(config.name, Thread.MAX_PRIORITY));
workerGroup = new NioEventLoopGroup(config.workerCore, new DefaultThreadFactory(config.name, Thread.MAX_PRIORITY));
if (config.businessCore > 0) {
businessService = new ThreadPoolExecutor(config.businessCore, config.businessCore, 1L,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new DefaultThreadFactory(config.name + "-B", true, Thread.NORM_PRIORITY));
}
return new ServerBootstrap()
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(NioChannelOption.SO_REUSEADDR, true)
.option(NioChannelOption.SO_BACKLOG, 1024)
.childOption(NioChannelOption.TCP_NODELAY, true)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
/*第二个处理器 session处理*/
private final TCPMessageAdapter adapter = new TCPMessageAdapter(config.sessionManager);
/*3.解码 适配解码器 解码后业务处理*/
private final MessageDecoderWrapper decoder = new MessageDecoderWrapper(config.decoder);
/*3.编码 适配编码器-编码后业务处理*/
private final MessageEncoderWrapper encoder = new MessageEncoderWrapper(config.encoder);
/*4.编解码后消息分发器 同步和异步处理*/
private final DispatcherHandler dispatcher = new DispatcherHandler(config.handlerMapping, config.handlerInterceptor, bossGroup);
@Override
protected void initChannel(NioSocketChannel channel) throws Exception {
channel.pipeline()
.addLast(new IdleStateHandler(config.readerIdleTime, config.writerIdleTime, config.allIdleTime)) //设置心跳时间
// .addLast(FastBeeConstant.SERVER.FRAMEDECODER, frameDecoder())//粘包处理器
.addLast(FastBeeConstant.SERVER.ADAPTER, adapter)//消息适配器
.addLast(FastBeeConstant.SERVER.DECODER, decoder) //报文解码器
.addLast(FastBeeConstant.SERVER.ENCODER, encoder) //报文编码器
.addLast(FastBeeConstant.SERVER.DISPATCHER, dispatcher); //消息分发
}
});
}
/**
* 添加TCP粘包处理器
*/
private ByteToMessageDecoder frameDecoder() {
if (config.lengthField == null) {
/*分隔符处理器,报文以固定包头包尾结束*/
return new DelimiterBasedFrameDecoder(config.maxFrameLength, config.delimiters);
}
/*报文长度的,以长度固定处理器和分隔符处理器 处理*/
return new LengthFieldAndDelimiterFrameDecoder(config.maxFrameLength, config.lengthField, config.delimiters);
}
}

View File

@ -0,0 +1,62 @@
package com.fastbee.server;
import com.fastbee.common.constant.FastBeeConstant;
import com.fastbee.server.config.NettyConfig;
import com.fastbee.server.handler.DispatcherHandler;
import com.fastbee.server.handler.MessageDecoderWrapper;
import com.fastbee.server.handler.MessageEncoderWrapper;
import com.fastbee.server.handler.UDPMessageAdapter;
import io.netty.bootstrap.AbstractBootstrap;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioChannelOption;
import io.netty.channel.socket.nio.NioDatagramChannel;
import io.netty.util.concurrent.DefaultThreadFactory;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* UDP服务
*
* @author gsb
* @date 2022/11/7 13:44
*/
public class UDPServer extends Server {
public UDPServer(NettyConfig config) {
super(config);
}
@Override
protected AbstractBootstrap initialize() {
bossGroup = new NioEventLoopGroup(1, new DefaultThreadFactory(config.name, Thread.MAX_PRIORITY));
if (config.businessCore > 0) {
businessService = new ThreadPoolExecutor(config.businessCore, config.businessCore, 1L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(), new DefaultThreadFactory(config.name + "-B", true, Thread.NORM_PRIORITY));
}
return new Bootstrap()
.group(bossGroup)
.channel(NioDatagramChannel.class)
.option(NioChannelOption.SO_REUSEADDR, true)
.option(NioChannelOption.SO_RCVBUF, 1024 * 1024 * 50)
.handler(new ChannelInitializer<NioDatagramChannel>() {
private final UDPMessageAdapter adapter = UDPMessageAdapter.newInstance(config.sessionManager, config.readerIdleTime, config.delimiters);
private final MessageDecoderWrapper decoder = new MessageDecoderWrapper(config.decoder);
private final MessageEncoderWrapper encoder = new MessageEncoderWrapper(config.encoder);
private final DispatcherHandler dispatcherHandler = new DispatcherHandler(config.handlerMapping, config.handlerInterceptor, businessService);
@Override
protected void initChannel(NioDatagramChannel channel) throws Exception {
channel.pipeline()
.addLast(FastBeeConstant.SERVER.ADAPTER, adapter)
.addLast(FastBeeConstant.SERVER.DECODER, decoder)
.addLast(FastBeeConstant.SERVER.ENCODER, encoder)
.addLast(FastBeeConstant.SERVER.DISPATCHER, dispatcherHandler);
}
});
}
}

View File

@ -0,0 +1,274 @@
package com.fastbee.server.config;
import com.fastbee.common.constant.FastBeeConstant;
import com.fastbee.common.enums.ServerType;
import com.fastbee.server.Server;
import com.fastbee.server.TCPServer;
import com.fastbee.server.UDPServer;
import com.fastbee.base.codec.Delimiter;
import com.fastbee.base.codec.LengthField;
import com.fastbee.base.codec.MessageDecoder;
import com.fastbee.base.codec.MessageEncoder;
import com.fastbee.base.core.HandlerInterceptor;
import com.fastbee.base.core.HandlerMapping;
import com.fastbee.base.session.SessionManager;
import io.netty.util.NettyRuntime;
import io.netty.util.internal.ObjectUtil;
/**
* 基础配置类
* @Author guanshubiao
* @Date 2022/9/12 20:22
*/
public class NettyConfig {
public final int workerCore;
/*boss线程核数*/
public final int businessCore;
/*读空闲时间*/
public final int readerIdleTime;
/*写空闲时间*/
public final int writerIdleTime;
/*读写空闲时间*/
public final int allIdleTime;
/*端口*/
public final Integer port;
/*TCP/UDP数据最大长度限定*/
public final Integer maxFrameLength;
/*基础编码*/
public final MessageDecoder decoder;
/*基础解码*/
public final MessageEncoder encoder;
public final Delimiter[] delimiters;
public final LengthField lengthField;
public final HandlerMapping handlerMapping;
public final HandlerInterceptor handlerInterceptor;
public final SessionManager sessionManager;
/*基础服务端*/
public Server server;
public String name;
/*服务名*/
public final ServerType type;
public NettyConfig(int workerGroup,
int businessGroup,
int readerIdleTime,
int writerIdleTime,
int allIdleTime,
Integer port,
Integer maxFrameLength,
LengthField lengthField,
Delimiter[] delimiters,
MessageDecoder decoder,
MessageEncoder encoder,
HandlerMapping handlerMapping,
HandlerInterceptor handlerInterceptor,
SessionManager sessionManager,
ServerType type,
String name,
Server server) {
/*校验值是否正确*/
ObjectUtil.checkNotNull(port, FastBeeConstant.SERVER.PORT);
ObjectUtil.checkPositive(port, FastBeeConstant.SERVER.PORT);
if (ServerType.UDP == type || ServerType.TCP == type){
ObjectUtil.checkNotNull(decoder, "decoder");
ObjectUtil.checkNotNull(encoder, "encoder");
ObjectUtil.checkNotNull(handlerMapping, "handlerMapping");
ObjectUtil.checkNotNull(handlerInterceptor, "handlerInterceptor");
}
if (type == ServerType.TCP){
ObjectUtil.checkNotNull(maxFrameLength, FastBeeConstant.SERVER.MAXFRAMELENGTH);
ObjectUtil.checkPositive(maxFrameLength, FastBeeConstant.SERVER.MAXFRAMELENGTH);
// ObjectUtil.checkNotNull(delimiters,FastBeeConstant.SERVER.DELIMITERS);
}
/*获取核数*/
int processors = NettyRuntime.availableProcessors();
this.workerCore = workerGroup > 0 ? workerGroup : processors + 2;
this.businessCore = businessGroup > 0 ? businessGroup : Math.max(1, processors >> 1);
this.readerIdleTime = readerIdleTime;
this.writerIdleTime = writerIdleTime;
this.allIdleTime = allIdleTime;
this.port = port;
this.maxFrameLength = maxFrameLength;
this.lengthField = lengthField;
this.delimiters = delimiters;
this.decoder = decoder;
this.encoder = encoder;
this.handlerMapping = handlerMapping;
this.handlerInterceptor = handlerInterceptor;
this.sessionManager = sessionManager != null ? sessionManager : new SessionManager();
this.type = type;
switch (type){
case TCP:
this.server = new TCPServer(this);
this.name = name != null ? name : ServerType.TCP.name();
break;
case UDP:
this.name = name != null ? name : ServerType.UDP.name();
this.server = new UDPServer(this);
break;
case MQTT:
case WEBSOCKET:
this.name = name != null ? name : ServerType.MQTT.name();
this.server = server;
this.server.config = this;
break;
case HTTP:
this.name = name != null ? name : ServerType.HTTP.name();
this.server = server;
this.server.config = this;
break;
case COAP:
this.name = name != null ? name : ServerType.COAP.name();
this.server = server;;
this.server.config = this;
break;
default:
}
}
public Server build() {
return server;
}
public static NettyConfig.Builder custom() {
return new Builder();
}
public static class Builder {
private int workerCore;
private int businessCore ;
private int readerIdleTime ;
private int writerIdleTime = 0;
private int allIdleTime = 0;
private Integer port;
private Integer maxFrameLength;
private LengthField lengthField;
private Delimiter[] delimiters;
private MessageDecoder decoder;
private MessageEncoder encoder;
private HandlerMapping handlerMapping;
private HandlerInterceptor handlerInterceptor;
private SessionManager sessionManager;
private ServerType type;
private String name;
private Server server;
public Builder() {
}
public Builder setThreadGroup(int workerCore, int businessCore) {
this.workerCore = workerCore;
this.businessCore = businessCore;
return this;
}
public Builder setIdleStateTime(int readerIdleTime, int writerIdleTime, int allIdleTime) {
this.readerIdleTime = readerIdleTime;
this.writerIdleTime = writerIdleTime;
this.allIdleTime = allIdleTime;
return this;
}
public Builder setPort(Integer port) {
this.port = port;
return this;
}
public Builder setServer(Server server){
this.server = server;
return this;
}
public Builder setMaxFrameLength(Integer maxFrameLength) {
this.maxFrameLength = maxFrameLength;
return this;
}
public Builder setLengthField(LengthField lengthField) {
this.lengthField = lengthField;
return this;
}
public Builder setDelimiters(byte[][] delimiters) {
Delimiter[] t = new Delimiter[delimiters.length];
for (int i = 0; i < delimiters.length; i++) {
t[i] = new Delimiter(delimiters[i]);
}
this.delimiters = t;
return this;
}
public Builder setDelimiters(Delimiter... delimiters) {
this.delimiters = delimiters;
return this;
}
public Builder setDecoder(MessageDecoder decoder) {
this.decoder = decoder;
return this;
}
public Builder setEncoder(MessageEncoder encoder) {
this.encoder = encoder;
return this;
}
public Builder setHandlerMapping(HandlerMapping handlerMapping) {
this.handlerMapping = handlerMapping;
return this;
}
public Builder setHandlerInterceptor(HandlerInterceptor handlerInterceptor) {
this.handlerInterceptor = handlerInterceptor;
return this;
}
public Builder setSessionManager(SessionManager sessionManager) {
this.sessionManager = sessionManager;
return this;
}
public Builder setType(ServerType type){
this.type = type;
return this;
}
public Builder setName(String name) {
this.name = name;
return this;
}
public Server build() {
return new NettyConfig(
this.workerCore,
this.businessCore,
this.readerIdleTime,
this.writerIdleTime,
this.allIdleTime,
this.port,
this.maxFrameLength,
this.lengthField,
this.delimiters,
this.decoder,
this.encoder,
this.handlerMapping,
this.handlerInterceptor,
this.sessionManager,
this.type,
this.name,
this.server
).build();
}
}
}

View File

@ -0,0 +1,140 @@
package com.fastbee.server.handler;
import com.fastbee.base.codec.Delimiter;
import com.fastbee.base.util.ByteBufUtils;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.TooLongFrameException;
import io.netty.util.internal.ObjectUtil;
import java.util.List;
import static io.netty.util.internal.ObjectUtil.checkPositive;
/**
* 分隔符报文解码器 -消息进站处理步骤1 可选
* @author bill
*/
public class DelimiterBasedFrameDecoder extends ByteToMessageDecoder {
/*分隔符 例如报文头 0xFF 报文尾 0x0D*/
private final Delimiter[] delimiters;
/*最大帧长度*/
private final int maxFrameLength;
private final boolean failFast;
/*是否丢弃超过固定长度的报文*/
private boolean discardingTooLongFrame;
/*最长帧长度*/
private int tooLongFrameLength;
/*构造分隔符解码器*/
public DelimiterBasedFrameDecoder(int maxFrameLength, Delimiter... delimiters) {
this(maxFrameLength, true, delimiters);
}
public DelimiterBasedFrameDecoder(int maxFrameLength, boolean failFast, Delimiter... delimiters) {
validateMaxFrameLength(maxFrameLength);
ObjectUtil.checkNonEmpty(delimiters, "delimiters");
this.delimiters = delimiters;
this.maxFrameLength = maxFrameLength;
this.failFast = failFast;
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
/*根据分隔符处理粘包报文*/
Object decoded = decode(ctx, in);
if (decoded != null) {
/*报文出站,流入下一个处理器*/
out.add(decoded);
}
}
protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) {
// 使用所有分隔符并选择产生最短帧的分隔符
int minFrameLength = Integer.MAX_VALUE;
Delimiter minDelim = null;
for (Delimiter delim : delimiters) {
int frameLength = ByteBufUtils.indexOf(buffer, delim.value);
if (frameLength >= 0 && frameLength < minFrameLength) {
/*最小报文长度*/
minFrameLength = frameLength;
minDelim = delim;
}
}
if (minDelim != null) {
int minDelimLength = minDelim.value.length;
ByteBuf frame = null;
if (discardingTooLongFrame) {
// 如果true将长度不符合报文丢弃
// 初始化原来的值
discardingTooLongFrame = false;
buffer.skipBytes(minFrameLength + minDelimLength);
int tooLongFrameLength = this.tooLongFrameLength;
this.tooLongFrameLength = 0;
if (!failFast) {
fail(tooLongFrameLength);
}
return null;
}
/*小于最小长度帧处理*/
if (minFrameLength > maxFrameLength) {
//放弃读取帧
buffer.skipBytes(minFrameLength + minDelimLength);
fail(minFrameLength);
return null;
}
/*是否需要跳过某字节*/
if (minDelim.strip) {
//忽略长度等于0的报文
if (minFrameLength != 0) {
frame = buffer.readRetainedSlice(minFrameLength);
}
buffer.skipBytes(minDelimLength);
} else {
if (minFrameLength != 0) {
frame = buffer.readRetainedSlice(minFrameLength + minDelimLength);
} else {
buffer.skipBytes(minDelimLength);
}
}
return frame;
} else {
if (!discardingTooLongFrame) {
if (buffer.readableBytes() > maxFrameLength) {
// Discard the content of the buffer until a delimiter is found.
tooLongFrameLength = buffer.readableBytes();
buffer.skipBytes(buffer.readableBytes());
discardingTooLongFrame = true;
if (failFast) {
fail(tooLongFrameLength);
}
}
} else {
// Still discarding the buffer since a delimiter is not found.
tooLongFrameLength += buffer.readableBytes();
buffer.skipBytes(buffer.readableBytes());
}
return null;
}
}
private void fail(long frameLength) {
if (frameLength > 0) {
throw new TooLongFrameException("frame length exceeds " + maxFrameLength + ": " + frameLength + " - discarded");
} else {
throw new TooLongFrameException("frame length exceeds " + maxFrameLength + " - discarding");
}
}
private static void validateMaxFrameLength(int maxFrameLength) {
checkPositive(maxFrameLength, "maxFrameLength");
}
}

View File

@ -0,0 +1,208 @@
package com.fastbee.server.handler;
import com.fastbee.base.core.HandlerInterceptor;
import com.fastbee.base.core.HandlerMapping;
import com.fastbee.base.core.hanler.BaseHandler;
import com.fastbee.base.session.Packet;
import com.fastbee.base.session.Session;
import com.fastbee.base.util.Stopwatch;
import com.fastbee.common.constant.FastBeeConstant;
import com.fastbee.common.core.mq.DeviceReport;
import com.fastbee.common.core.mq.DeviceReportBo;
import com.fastbee.common.core.mq.DeviceTestReportBo;
import com.fastbee.common.core.mq.message.DeviceMessage;
import com.fastbee.common.core.protocol.Message;
import com.fastbee.common.core.thingsModel.ThingsModelSimpleItem;
import com.fastbee.common.enums.ServerType;
import com.fastbee.common.enums.TopicType;
import com.fastbee.common.utils.DateUtils;
import com.fastbee.modbus.pak.ModbusEndPoint;
import com.fastbee.modbus.pak.TcpDtu;
import com.fastbee.mq.redischannel.producer.MessageProducer;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ExecutorService;
/**
* 消息分发处理
* @author bill
*/
@Slf4j
@ChannelHandler.Sharable
public class DispatcherHandler extends ChannelInboundHandlerAdapter {
private final HandlerMapping handlerMapping;
private final HandlerInterceptor interceptor;
private final ExecutorService executor;
public static boolean STOPWATCH = false;
private static Stopwatch s;
public DispatcherHandler(HandlerMapping handlerMapping, HandlerInterceptor interceptor, ExecutorService executor) {
if (STOPWATCH && s == null) {
s = new Stopwatch().start();
}
this.handlerMapping = handlerMapping;
this.interceptor = interceptor;
this.executor = executor;
}
/**
* TCP-4 消息处理
* @param ctx
* @param msg
*/
@Override
public final void channelRead(ChannelHandlerContext ctx, Object msg) {
if (STOPWATCH) {
s.increment();
}
Packet packet = (Packet) msg;
Message request = packet.message;
//判断是否注册
Session session = packet.session;
/**
* TCP的数据包四种情况
* messageId用于标记上传的包数据的标志位的值isPackage表示是否整个包上传
* 1. 整包上传: 包含:设备编号(注册包),数据包(心跳包) -->处理注册上传 和 上报数据
* 2. 单个注册包,或者心跳包上传,有标识位 -> 如0x80,表示注册包,只处理注册上传
* 3. 单个数据包上传,有标识位 ,如 0x89 -->表示xxxx 解码数据包转发到MQ
* 4. 单个数据包上传,无标识位 ,如 modbus --> 安装约定的协议处理
*/
// 4.单个数据包上传无标识位
if (null == request.getMessageId()){
this.handleReport(request);
}
//处理数据调试
//1. 整包上传: 包含:设备编号(注册包),数据包(心跳包) -->处理注册上传 和 上报数据
if (request.getIsPackage()){
//获取设备编号
List<ThingsModelSimpleItem> items = ((DeviceReport) request).getThingsModelSimpleItem();
for (ThingsModelSimpleItem item : items) {
if (null !=item.getId() && item.getId().equals("dev_id")){
request.setClientId(item.getValue());
}else if (null != item.getId() && item.getId().equals("imei")){
request.setClientId(item.getValue());
}else if (null != item.getId() && item.getId().equals("id")){
request.setClientId(item.getValue());
}else if (null != item.getId() && item.getId().equals("device")){
request.setClientId(item.getValue());
}
}
if (null != request.getClientId()){
((DeviceReport) request).setSerialNumber(request.getClientId());
//先处理设备注册
this.handleMessage(request,packet,ctx);
}
//处理消息转发
this.handleReport(request);
// 2/3 单个注册包心跳数据包上传匹配消息id处理
}else if (null != request.getMessageId() && !"0".equals(request.getMessageId())){
//处理心跳和设备注册
this.handleMessage(request,packet,ctx);
request.setClientId(request.getClientId()==null ? session.getClientId() : request.getClientId());
this.handleOtherMsg(request);
}
if (!session.isRegistered()){
//未注册进行注册
session.register(request);
}
}
private void handleMessage(Message message,Packet packet,ChannelHandlerContext ctx){
/*获取消息的处理方法 根据注解 @PakMapping 匹配方法*/
BaseHandler handler = handlerMapping.getHandler(Integer.parseInt(message.getMessageId()));
if (handler == null) {
Message response = interceptor.notSupported(message, packet.session);
if (response != null) {
ctx.writeAndFlush(packet.replace(response));
}
} else {
if (handler.async) {
executor.execute(() -> channelRead0(ctx, packet, handler));
} else {
channelRead0(ctx, packet, handler);
}
}
}
private void handleReport(Message request){
DeviceReport report = (DeviceReport)request;
DeviceReportBo reportBo = new DeviceReportBo();
reportBo.setThingsModelSimpleItem(report.getThingsModelSimpleItem());
reportBo.setPlatformDate(DateUtils.getNowDate());
reportBo.setSerialNumber(report.getClientId());
reportBo.setServerType(ServerType.TCP);
reportBo.setReplyMessage(report.getReplyMessage());
reportBo.setIsReply(report.getIsReply());
reportBo.setStatus(report.getStatus());
reportBo.setProtocolCode(FastBeeConstant.PROTOCOL.ModbusRtu);
reportBo.setSources(report.getSources());
MessageProducer.sendPublishMsg(reportBo);
return;
}
/**
* 推送注册和心跳
* @param request
*/
private void handleOtherMsg(Message request){
DeviceReport report = (DeviceReport)request;
DeviceTestReportBo reportBo = new DeviceTestReportBo();
reportBo.setSources(report.getSources());
reportBo.setProductId(report.getProductId());
reportBo.setSerialNumber(report.getClientId());
reportBo.setIsReply(false);
List<ThingsModelSimpleItem> itemList = new ArrayList<>();
ThingsModelSimpleItem item = new ThingsModelSimpleItem();
item.setTs(new Date());
item.setName("");
item.setId(report.getClientId());
item.setValue(report.getClientId());
itemList.add(item);
reportBo.setThingsModelSimpleItem(itemList);
MessageProducer.sendDeviceTestMsg(reportBo);
}
private void channelRead0(ChannelHandlerContext ctx, Packet packet, BaseHandler handler) {
Session session = packet.session;
Message request = packet.message;
Message response;
try {
//判断是否是前置拦截的消息类型,若不是则不处理
if (!interceptor.beforeHandle(request, session)) {
}
/*调用@PakMapping注解标注方法执行*/
response = handler.invoke(request, session);
//无返回值处理
if (handler.returnVoid) {
response = interceptor.successful(request, session);
} else {
//有返回值进行AOP下一一个方法
interceptor.afterHandle(request, response, session);
}
} catch (Exception e) {
log.warn(String.valueOf(request), e);
response = interceptor.exceptional(request, session, e);
}
//有返回值,则应答设备
if (response != null) {
ctx.writeAndFlush(packet.replace(response));
}
}
}

Some files were not shown because too many files have changed in this diff Show More