// Copyright 2016 Twitter. All rights reserved. // // 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 com.twitter.heron.common.network; import java.io.IOException; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.nio.channels.SelectableChannel; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.time.Duration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import com.google.protobuf.Message; import com.twitter.heron.common.basics.ISelectHandler; import com.twitter.heron.common.basics.NIOLooper; /** * Any Heron Server should implement this abstract class. * In particular it should implement * a) onConnect method called when a new client connects to us * b) onClose method called when a existing client closes * c) onRequest method called when we have a new request * d) onMessage method called when we have a new message */ public abstract class HeronServer implements ISelectHandler { private static final Logger LOG = Logger.getLogger(HeronServer.class.getName()); // The socket that we will use for accepting connections private ServerSocketChannel acceptChannel; // Define the address where we need to listen on private InetSocketAddress endpoint; private HeronSocketOptions socketOptions; // Our own looper private NIOLooper nioLooper; // All the clients that we have connected private Map<SocketChannel, SocketChannelHelper> activeConnections; // Map from protobuf message's name to protobuf message's builder private Map<String, Message.Builder> requestMap; private Map<String, Message.Builder> messageMap; /** * Constructor * * @param s the NIOLooper bind with this socket server * @param host the host of remote endpoint to communicate with * @param port the port of remote endpoint to communicate with */ public HeronServer(NIOLooper s, String host, int port, HeronSocketOptions options) { nioLooper = s; endpoint = new InetSocketAddress(host, port); socketOptions = options; requestMap = new HashMap<String, Message.Builder>(); messageMap = new HashMap<String, Message.Builder>(); activeConnections = new HashMap<SocketChannel, SocketChannelHelper>(); } InetSocketAddress getEndpoint() { return endpoint; } // Register the protobuf Message's name with protobuf Message public void registerOnMessage(Message.Builder builder) { messageMap.put(builder.getDescriptorForType().getFullName(), builder); } // Register the protobuf Message's name with protobuf Message public void registerOnRequest(Message.Builder builder) { requestMap.put(builder.getDescriptorForType().getFullName(), builder); } public boolean start() { try { acceptChannel = ServerSocketChannel.open(); acceptChannel.configureBlocking(false); acceptChannel.socket().bind(endpoint); nioLooper.registerAccept(acceptChannel, this); return true; } catch (IOException e) { LOG.log(Level.SEVERE, "Failed to start server", e); return false; } } // Stop the HeronServer and clean relative staff public void stop() { if (acceptChannel == null || !acceptChannel.isOpen()) { LOG.info("Fail to stop server; not yet open."); return; } // Clear all connected socket and related stuff for (Map.Entry<SocketChannel, SocketChannelHelper> connections : activeConnections.entrySet()) { SocketChannel channel = connections.getKey(); SocketAddress channelAddress = channel.socket().getRemoteSocketAddress(); LOG.info("Closing connected channel from client: " + channelAddress); LOG.info("Removing all interest on channel: " + channelAddress); nioLooper.removeAllInterest(channel); // Dispatch the child instance onClose(channel); // Clear the SocketChannelHelper connections.getValue().clear(); } // Clear state inside the HeronServer activeConnections.clear(); requestMap.clear(); messageMap.clear(); try { acceptChannel.close(); } catch (IOException e) { LOG.log(Level.SEVERE, "Failed to close server", e); } } @Override public void handleAccept(SelectableChannel channel) { try { SocketChannel socketChannel = acceptChannel.accept(); if (socketChannel != null) { socketChannel.configureBlocking(false); // Set the maximum possible send and receive buffers socketChannel.socket().setSendBufferSize( (int) socketOptions.getSocketSendBufferSize().asBytes()); socketChannel.socket().setReceiveBufferSize( (int) socketOptions.getSocketReceivedBufferSize().asBytes()); socketChannel.socket().setTcpNoDelay(true); SocketChannelHelper helper = new SocketChannelHelper(nioLooper, this, socketChannel, socketOptions); activeConnections.put(socketChannel, helper); onConnect(socketChannel); } } catch (IOException e) { LOG.log(Level.SEVERE, "Error while accepting a new connection ", e); // Note:- we are not calling onError } } @Override public void handleRead(SelectableChannel channel) { SocketChannelHelper helper = activeConnections.get(channel); if (helper == null) { LOG.severe("Unknown connection is ready for read"); return; } List<IncomingPacket> packets = helper.read(); for (IncomingPacket ipt : packets) { handlePacket(channel, ipt); } } @Override public void handleWrite(SelectableChannel channel) { SocketChannelHelper helper = activeConnections.get(channel); if (helper == null) { LOG.severe("Unknown connection is ready for read"); return; } helper.write(); } @Override public void handleConnect(SelectableChannel channel) { throw new RuntimeException("Server cannot have handleConnect"); } /** * Handle an incomingPacket and invoke either onRequest or * onMessage() to handle it */ private void handlePacket(SelectableChannel channel, IncomingPacket incomingPacket) { String typeName = incomingPacket.unpackString(); REQID rid = incomingPacket.unpackREQID(); Message.Builder bldr = requestMap.get(typeName); boolean isRequest = false; if (bldr != null) { // This is a request isRequest = true; } else { bldr = messageMap.get(typeName); } if (bldr != null) { // Clear the earlier state of Message.Builder // Otherwise it would merge new Message with old state bldr.clear(); incomingPacket.unpackMessage(bldr); if (bldr.isInitialized()) { Message msg = bldr.build(); if (isRequest) { onRequest(rid, (SocketChannel) channel, msg); } else { onMessage((SocketChannel) channel, msg); } } else { // Message failed to be deser LOG.severe("Could not deserialize protobuf of type " + typeName); handleError(channel); } return; } else { LOG.severe("Unexpected protobuf type received " + typeName); handleError(channel); } } // Clean the stuff when meeting some errors public void handleError(SelectableChannel channel) { SocketAddress channelAddress = ((SocketChannel) channel).socket().getRemoteSocketAddress(); LOG.info("Handling error from channel: " + channelAddress); SocketChannelHelper helper = activeConnections.get(channel); if (helper == null) { LOG.severe("Inactive channel had error?"); return; } helper.clear(); LOG.info("Removing all interest on channel: " + channelAddress); nioLooper.removeAllInterest(channel); try { channel.close(); } catch (IOException e) { LOG.severe("Error closing connection in handleError"); } activeConnections.remove(channel); onClose((SocketChannel) channel); } // Send back the response to the client. // A false return value means that the response could not be sent. // Upon returning true, it does not mean that the response was actually // sent out, merely that the response was queueud to be sent out. // Actual send occurs when the socket becomes writable and all prev // responses/messages are sent. public boolean sendResponse(REQID rid, SocketChannel channel, Message response) { SocketChannelHelper helper = activeConnections.get(channel); if (helper == null) { LOG.severe("Trying to send a response on an unknown connection"); return false; } OutgoingPacket opk = new OutgoingPacket(rid, response); helper.sendPacket(opk); return true; } // This method is used if you want to communicate with the other end // on a non-request-response based communication. public boolean sendMessage(SocketChannel channel, Message message) { return sendResponse(REQID.zeroREQID, channel, message); } public NIOLooper getNIOLooper() { return nioLooper; } // Add a timer to be invoked after timer duration. public void registerTimerEvent(Duration timer, Runnable task) { nioLooper.registerTimerEvent(timer, task); } ///////////////////////////////////////////////////////// // This is the interface that needs to be implemented by // all Heron Servers. ///////////////////////////////////////////////////////// // What action do you want to take when a new client // connects to you. public abstract void onConnect(SocketChannel channel); // What action do you want to take when you get a new // request from a particular client public abstract void onRequest(REQID rid, SocketChannel channel, Message request); // What action do you want to take when you get a new // message from a particular client public abstract void onMessage(SocketChannel channel, Message message); // What action do you want to take when a client // closes its connection to you. public abstract void onClose(SocketChannel channel); ///////////////////////////////////////////////////////// // Following protected methods are just used for testing ///////////////////////////////////////////////////////// protected Map<String, Message.Builder> getMessageMap() { return messageMap; } protected Map<String, Message.Builder> getRequestMap() { return requestMap; } protected ServerSocketChannel getAcceptChannel() { return acceptChannel; } protected Map<SocketChannel, SocketChannelHelper> getActiveConnections() { return activeConnections; } }