/* Copyright [2011] [University of Rostock] * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *****************************************************************************/ package org.ws4d.coap.connection; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.DatagramChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.util.HashMap; import java.util.PriorityQueue; import java.util.concurrent.ConcurrentLinkedQueue; import org.apache.log4j.ConsoleAppender; import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.apache.log4j.SimpleLayout; import org.ws4d.coap.interfaces.CoapChannel; import org.ws4d.coap.interfaces.CoapChannelManager; import org.ws4d.coap.interfaces.CoapClient; import org.ws4d.coap.interfaces.CoapClientChannel; import org.ws4d.coap.interfaces.CoapMessage; import org.ws4d.coap.interfaces.CoapServerChannel; import org.ws4d.coap.interfaces.CoapSocketHandler; import org.ws4d.coap.messages.AbstractCoapMessage; import org.ws4d.coap.messages.CoapEmptyMessage; import org.ws4d.coap.messages.CoapPacketType; import org.ws4d.coap.tools.TimeoutHashMap; /** * @author Christian Lerche <christian.lerche@uni-rostock.de> * @author Nico Laum <nico.laum@uni-rostock.de> */ public class BasicCoapSocketHandler implements CoapSocketHandler { /* the socket handler has its own logger * TODO: implement different socket handler for client and server channels */ private final static Logger logger = Logger.getLogger(BasicCoapSocketHandler.class); protected WorkerThread workerThread = null; protected HashMap<ChannelKey, CoapClientChannel> clientChannels = new HashMap<ChannelKey, CoapClientChannel>(); protected HashMap<ChannelKey, CoapServerChannel> serverChannels = new HashMap<ChannelKey, CoapServerChannel>(); private CoapChannelManager channelManager = null; private DatagramChannel dgramChannel = null; public static final int UDP_BUFFER_SIZE = 66000; // max UDP size = 65535 byte[] sendBuffer = new byte[UDP_BUFFER_SIZE]; private int localPort; public BasicCoapSocketHandler(CoapChannelManager channelManager, int port) throws IOException { logger.addAppender(new ConsoleAppender(new SimpleLayout())); // ALL | DEBUG | INFO | WARN | ERROR | FATAL | OFF: logger.setLevel(Level.WARN); this.channelManager = channelManager; dgramChannel = DatagramChannel.open(); dgramChannel.socket().bind(new InetSocketAddress(port)); //port can be 0, then a free port is chosen this.localPort = dgramChannel.socket().getLocalPort(); dgramChannel.configureBlocking(false); workerThread = new WorkerThread(); workerThread.start(); } public BasicCoapSocketHandler(CoapChannelManager channelManager) throws IOException { this(channelManager, 0); } protected class WorkerThread extends Thread { Selector selector = null; /* contains all received message keys of a remote (message id generated by the remote) to detect duplications */ TimeoutHashMap<MessageKey, Boolean> duplicateRemoteMap = new TimeoutHashMap<MessageKey, Boolean>(CoapMessage.ACK_RST_RETRANS_TIMEOUT_MS); /* contains all received message keys of the host (message id generated by the host) to detect duplications */ TimeoutHashMap<Integer, Boolean> duplicateHostMap = new TimeoutHashMap<Integer, Boolean>(CoapMessage.ACK_RST_RETRANS_TIMEOUT_MS); /* contains all messages that (possibly) needs to be retransmitted (ACK, RST)*/ TimeoutHashMap<MessageKey, CoapMessage> retransMsgMap = new TimeoutHashMap<MessageKey, CoapMessage>(CoapMessage.ACK_RST_RETRANS_TIMEOUT_MS); /* contains all messages that are not confirmed yet (CON), * MessageID is always generated by Host and therefore unique */ TimeoutHashMap<Integer, CoapMessage> timeoutConMsgMap = new TimeoutHashMap<Integer, CoapMessage>(CoapMessage.ACK_RST_RETRANS_TIMEOUT_MS); /* this queue handles the timeout objects in the right order*/ private PriorityQueue<TimeoutObject<Integer>> timeoutQueue = new PriorityQueue<TimeoutObject<Integer>>(); public ConcurrentLinkedQueue<CoapMessage> sendBuffer = new ConcurrentLinkedQueue<CoapMessage>(); /* Contains all sent messages sorted by message ID */ long startTime; static final int POLLING_INTERVALL = 10000; ByteBuffer dgramBuffer; public WorkerThread() { dgramBuffer = ByteBuffer.allocate(UDP_BUFFER_SIZE); startTime = System.currentTimeMillis(); try { selector = Selector.open(); dgramChannel.register(selector, SelectionKey.OP_READ); } catch (IOException e1) { e1.printStackTrace(); } } public void close() { if (clientChannels != null) clientChannels.clear(); if (serverChannels != null) serverChannels.clear(); try { dgramChannel.close(); } catch (IOException e) { e.printStackTrace(); } /* TODO: wake up thread and kill it*/ } @Override public void run() { logger.log(Level.INFO, "Receive Thread started."); long waitFor = POLLING_INTERVALL; InetSocketAddress addr = null; while (dgramChannel != null) { /* send all messages in the send buffer */ sendBufferedMessages(); /* handle incoming packets */ dgramBuffer.clear(); try { addr = (InetSocketAddress) dgramChannel.receive(dgramBuffer); } catch (IOException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } if (addr != null){ logger.log(Level.INFO, "handle incomming msg"); handleIncommingMessage(dgramBuffer, addr); } /* handle timeouts */ waitFor = handleTimeouts(); /* TODO: find a good strategy when to update the timeout maps */ duplicateRemoteMap.update(); duplicateHostMap.update(); retransMsgMap.update(); timeoutConMsgMap.update(); /* wait until * 1. selector.wakeup() is called by sendMessage() * 2. incomming packet * 3. timeout */ try { /*FIXME: don't make a select, when something is in the sendQueue, otherwise the packet will be sent after some delay * move this check and the select to a critical section */ selector.select(waitFor); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } protected synchronized void addMessageToSendBuffer(CoapMessage msg){ sendBuffer.add(msg); /* send immediately */ selector.wakeup(); } private void sendBufferedMessages(){ CoapMessage msg = sendBuffer.poll(); while(msg != null){ sendUdpMsg(msg); msg = sendBuffer.poll(); } } private void handleIncommingMessage(ByteBuffer buffer, InetSocketAddress addr) { CoapMessage msg; try { msg = AbstractCoapMessage.parseMessage(buffer.array(), buffer.position()); } catch (Exception e) { logger.warn("Received invalid message: message dropped!"); e.printStackTrace(); return; } CoapPacketType packetType = msg.getPacketType(); int msgId = msg.getMessageID(); MessageKey msgKey = new MessageKey(msgId, addr.getAddress(), addr.getPort()); if (msg.isRequest()){ /* --- INCOMING REQUEST: This is an incoming client request with a message key generated by the remote client*/ if (packetType == CoapPacketType.ACK || packetType == CoapPacketType.RST){ logger.warn("Invalid Packet Type: Request can not be in a ACK or a RST packet"); return; } /* check for duplicates and retransmit the response if a duplication is detected */ if (isRemoteDuplicate(msgKey)){ retransmitRemoteDuplicate(msgKey); return; } /* find or create server channel and handle incoming message */ CoapServerChannel channel = serverChannels.get(new ChannelKey(addr.getAddress(), addr.getPort())); if (channel == null){ /*no server channel found -> create*/ channel = channelManager.createServerChannel(BasicCoapSocketHandler.this, msg, addr.getAddress(), addr.getPort()); if (channel != null){ /* add the new channel to the channel map */ addServerChannel(channel); logger.info("Created new server channel."); } else { /* create failed -> server doesn't accept the connection --> send RST*/ CoapChannel fakeChannel = new BasicCoapServerChannel(BasicCoapSocketHandler.this, null, addr.getAddress(), addr.getPort()); CoapEmptyMessage rstMsg = new CoapEmptyMessage(CoapPacketType.RST, msgId); rstMsg.setChannel(fakeChannel); sendMessage(rstMsg); return; } } msg.setChannel(channel); channel.handleMessage(msg); return; } else if (msg.isResponse()){ /* --- INCOMING RESPONSE: This is an incoming server response (message ID generated by host) * or a separate server response (message ID generated by remote)*/ if (packetType == CoapPacketType.RST){ logger.warn("Invalid Packet Type: RST packet must be empty"); return; } /* check for separate response */ if (packetType == CoapPacketType.CON){ /* This is a separate response, the message ID is generated by the remote */ if (isRemoteDuplicate(msgKey)){ retransmitRemoteDuplicate(msgKey); return; } /* This is a separate Response */ CoapClientChannel channel = clientChannels.get(new ChannelKey(addr.getAddress(), addr.getPort())); if (channel == null){ logger.warn("Could not find channel of incomming separat response: message dropped"); return; } msg.setChannel(channel); channel.handleMessage(msg); return; } /* normal response (ACK or NON), message id was generated by host */ if (isHostDuplicate(msgId)){ /* drop duplicate responses */ return; } /* confirm the request*/ /* confirm message by removing it from the non confirmedMsgMap*/ /* Corresponding to the spec the server should be aware of a NON as answer to a CON*/ timeoutConMsgMap.remove(msgId); CoapClientChannel channel = clientChannels.get(new ChannelKey(addr.getAddress(), addr.getPort())); if (channel == null){ logger.warn("Could not find channel of incomming response: message dropped"); return; } msg.setChannel(channel); channel.handleMessage(msg); return; } else if (msg.isEmpty()){ if (packetType == CoapPacketType.CON || packetType == CoapPacketType.NON){ /* TODO: is this always true? */ logger.warn("Invalid Packet Type: CON or NON packets cannot be empty"); return; } /* ACK or RST, Message Id was generated by the host*/ if (isHostDuplicate(msgId)){ /* drop duplicate responses */ return; } /* confirm */ timeoutConMsgMap.remove(msgId); /* get channel */ /* This can be an ACK/RST for a client or a server channel */ CoapChannel channel = clientChannels.get(new ChannelKey(addr.getAddress(), addr.getPort())); if (channel == null){ channel = serverChannels.get(new ChannelKey(addr.getAddress(), addr.getPort())); } if (channel == null){ logger.warn("Could not find channel of incomming response: message dropped"); return; } msg.setChannel(channel); if (packetType == CoapPacketType.ACK ){ /* separate response ACK */ channel.handleMessage(msg); return; } if (packetType == CoapPacketType.RST ){ /* connection closed by remote */ channel.handleMessage(msg); return; } } else { logger.error("Invalid Message Type: not a request, not a response, not empty"); } } private long handleTimeouts(){ long nextTimeout = POLLING_INTERVALL; while (true){ TimeoutObject<Integer> tObj = timeoutQueue.peek(); if (tObj == null){ /* timeout queue is empty */ nextTimeout = POLLING_INTERVALL; break; } nextTimeout = tObj.expires - System.currentTimeMillis(); if (nextTimeout > 0){ /* timeout not expired */ break; } /* timeout expired, sendMessage will send the message and create a new timeout * if the message was already confirmed, nonConfirmedMsgMap.get() will return null */ timeoutQueue.poll(); Integer msgId = tObj.object; /* retransmit message after expired timeout*/ sendUdpMsg((CoapMessage) timeoutConMsgMap.get(msgId)); } return nextTimeout; } private boolean isRemoteDuplicate(MessageKey msgKey){ if (duplicateRemoteMap.get(msgKey) != null){ logger.info("Detected duplicate message"); return true; } return false; } private void retransmitRemoteDuplicate(MessageKey msgKey){ CoapMessage retransMsg = (CoapMessage) retransMsgMap.get(msgKey); if (retransMsg == null){ logger.warn("Detected duplicate message but no response could be found"); } else { sendUdpMsg(retransMsg); } } private boolean isHostDuplicate(int msgId){ if (duplicateHostMap.get(msgId) != null){ logger.info("Detected duplicate message"); return true; } return false; } private void sendUdpMsg(CoapMessage msg) { if (msg == null){ return; } CoapPacketType packetType = msg.getPacketType(); InetAddress inetAddr = msg.getChannel().getRemoteAddress(); int port = msg.getChannel().getRemotePort(); int msgId = msg.getMessageID(); if (packetType == CoapPacketType.CON){ /* in case of a CON this is a Request * requests must be added to the timeout queue * except this was the last retransmission */ if(msg.maxRetransReached()){ /* the connection is broken */ timeoutConMsgMap.remove(msgId); msg.getChannel().lostConnection(true, false); return; } msg.incRetransCounterAndTimeout(); timeoutConMsgMap.put(msgId, msg); TimeoutObject<Integer> tObj = new TimeoutObject<Integer>(msgId, msg.getTimeout() + System.currentTimeMillis()); timeoutQueue.add(tObj); } if (packetType == CoapPacketType.ACK || packetType == CoapPacketType.RST){ /* save this type of messages for a possible retransmission */ retransMsgMap.put(new MessageKey(msgId, inetAddr, port), msg); } /* Nothing to do for NON*/ /* send message*/ ByteBuffer buf = ByteBuffer.wrap(msg.serialize()); /*TODO: check if serialization could fail... then do not put it to any Map!*/ try { dgramChannel.send(buf, new InetSocketAddress(inetAddr, port)); logger.log(Level.INFO, "Send Msg with ID: " + msg.getMessageID()); } catch (IOException e) { e.printStackTrace(); logger.error("Send UDP message failed"); } } } private class MessageKey{ public int msgID; public InetAddress inetAddr; public int port; public MessageKey(int msgID, InetAddress inetAddr, int port) { super(); this.msgID = msgID; this.inetAddr = inetAddr; this.port = port; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + getOuterType().hashCode(); result = prime * result + ((inetAddr == null) ? 0 : inetAddr.hashCode()); result = prime * result + msgID; result = prime * result + port; return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; MessageKey other = (MessageKey) obj; if (!getOuterType().equals(other.getOuterType())) return false; if (inetAddr == null) { if (other.inetAddr != null) return false; } else if (!inetAddr.equals(other.inetAddr)) return false; if (msgID != other.msgID) return false; if (port != other.port) return false; return true; } private BasicCoapSocketHandler getOuterType() { return BasicCoapSocketHandler.this; } } private class TimeoutObject<T> implements Comparable<TimeoutObject>{ private long expires; private T object; public TimeoutObject(T object, long expires) { this.expires = expires; this.object = object; } public T getObject() { return object; } public int compareTo(TimeoutObject o){ return (int) (this.expires - o.expires); } } private void addClientChannel(CoapClientChannel channel) { clientChannels.put(new ChannelKey(channel.getRemoteAddress(), channel.getRemotePort()), channel); } private void addServerChannel(CoapServerChannel channel) { serverChannels.put(new ChannelKey(channel.getRemoteAddress(), channel.getRemotePort()), channel); } public int getLocalPort() { return localPort; } public void removeClientChannel(CoapClientChannel channel) { clientChannels.remove(new ChannelKey(channel.getRemoteAddress(), channel.getRemotePort())); } public void removeServerChannel(CoapServerChannel channel) { serverChannels.remove(new ChannelKey(channel.getRemoteAddress(), channel.getRemotePort())); } public void close() { workerThread.close(); } public void sendMessage(CoapMessage message) { if (workerThread != null) { workerThread.addMessageToSendBuffer(message); } } public CoapClientChannel connect(CoapClient client, InetAddress remoteAddress, int remotePort) { if (client == null){ return null; } if (clientChannels.containsKey(new ChannelKey(remoteAddress, remotePort))){ /* channel already exists */ logger.warn("Cannot connect: Client channel already exists"); return null; } CoapClientChannel channel = new BasicCoapClientChannel(this, client, remoteAddress, remotePort); addClientChannel(channel); return channel; } public CoapChannelManager getChannelManager() { return this.channelManager; } }