/* * Licensed to the Apache Software Foundation (ASF) under one or more contributor license * agreements. See the NOTICE file distributed with this work for additional information regarding * copyright ownership. The ASF licenses this file to You 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.apache.geode.memcached; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.UnknownHostException; import java.nio.channels.ClosedByInterruptException; import java.nio.channels.ServerSocketChannel; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; import org.apache.geode.LogWriter; import org.apache.geode.cache.Cache; import org.apache.geode.cache.CacheFactory; import org.apache.geode.cache.Region; import org.apache.geode.cache.RegionShortcut; import org.apache.geode.distributed.internal.InternalDistributedSystem; import org.apache.geode.internal.net.SocketCreator; import org.apache.geode.internal.cache.GemFireCacheImpl; import org.apache.geode.internal.memcached.ConnectionHandler; /** * This is the Server that listens for incoming memcached client connections. This server * understands the memcached ASCII protocol documented in * <a href="https://github.com/memcached/memcached/blob/master/doc/protocol.txt">memcached source * control</a> It then translates these commands to the corresponding GemFire commands, and stores * the data in GemFire in a {@link Region} named "gemcached". * <p> * "gemcached" region is {@link RegionShortcut#PARTITION} by default, though a cache.xml can be * provided to override region attributes. * * This class has a Main method that can be used to start the server. * * */ public class GemFireMemcachedServer { /** * The protocol used by GemFireMemcachedServer */ public enum Protocol { ASCII, BINARY } private static LogWriter logger; /** * Name of the GemFire region in which data is stored, value id "gemcached" */ public static final String REGION_NAME = "gemcached"; /** * version of gemcached server */ public static final String version = "0.2"; /** * the configured address to listen for client connections */ private final String bindAddress; /** * the port to listen for client connections */ private final int serverPort; private final int DEFAULT_PORT = 11212; /** * the thread executor pool to handle requests from clients. We create one thread for each client. */ private ExecutorService executor = Executors.newCachedThreadPool(new ThreadFactory() { private final AtomicInteger counter = new AtomicInteger(); @Override public Thread newThread(Runnable r) { Thread t = new Thread(r); t.setName("Gemcached-" + counter.incrementAndGet()); t.setDaemon(true); return t; } }); /** * GemFire cache where data will be stored */ private Cache cache; /** * thread that listens for client connections */ private Thread acceptor; /** * The protocol that this server understands, ASCII by default */ private final Protocol protocol; /** * Create an instance of the server. to start the server {@link #start()} must be called. * * @param port the port on which the server listens for new memcached client connections. */ public GemFireMemcachedServer(int port) { this.bindAddress = ""; if (port <= 0) { this.serverPort = DEFAULT_PORT; } else { this.serverPort = port; } this.protocol = Protocol.ASCII; } /** * Create an instance of the server. to start the server {@link #start()} must be called. * * @param bindAddress the address on which the server listens for new memcached client * connections. * @param port the port on which the server listens for new memcached client connections. * @param protocol the protocol that this server should understand * @see Protocol */ public GemFireMemcachedServer(String bindAddress, int port, Protocol protocol) { this.bindAddress = bindAddress; if (port <= 0) { this.serverPort = DEFAULT_PORT; } else { this.serverPort = port; } this.protocol = protocol; } /** * Starts an embedded GemFire caching node, and then listens for new memcached client connections. */ public void start() { startGemFire(); try { startMemcachedServer(); } catch (IOException e) { throw new RuntimeException("Could not start Server", e); } catch (InterruptedException e) { throw new RuntimeException("Could not start Server", e); } } private void startGemFire() { this.cache = GemFireCacheImpl.getInstance(); if (this.cache == null) { CacheFactory cacheFactory = new CacheFactory(); this.cache = cacheFactory.create(); } logger = this.cache.getLogger(); } private void startMemcachedServer() throws IOException, InterruptedException { ServerSocketChannel channel = ServerSocketChannel.open(); final ServerSocket serverSocket = channel.socket(); serverSocket.setReceiveBufferSize(getSocketBufferSize()); serverSocket.setReuseAddress(true); serverSocket.bind(new InetSocketAddress(getBindAddress(), serverPort)); if (logger.fineEnabled()) { logger.fine("GemFireMemcachedServer configured socket buffer size:" + getSocketBufferSize()); } final CountDownLatch latch = new CountDownLatch(1); acceptor = new Thread(new Runnable() { public void run() { for (;;) { Socket s = null; try { latch.countDown(); s = serverSocket.accept(); s.setKeepAlive(SocketCreator.ENABLE_TCP_KEEP_ALIVE); handleNewClient(s); } catch (ClosedByInterruptException e) { try { serverSocket.close(); } catch (IOException e1) { e1.printStackTrace(); } break; } catch (IOException e) { e.printStackTrace(); break; } } } }, "AcceptorThread"); acceptor.setDaemon(true); acceptor.start(); latch.await(); logger.config("GemFireMemcachedServer server started on host:" + SocketCreator.getLocalHost() + " port: " + this.serverPort); } private InetAddress getBindAddress() throws UnknownHostException { return this.bindAddress == null || this.bindAddress.isEmpty() ? SocketCreator.getLocalHost() : InetAddress.getByName(this.bindAddress); } private int getSocketBufferSize() { InternalDistributedSystem system = (InternalDistributedSystem) cache.getDistributedSystem(); return system.getConfig().getSocketBufferSize(); } private void handleNewClient(Socket s) { ConnectionHandler connHandler = new ConnectionHandler(s, cache, protocol); executor.execute(connHandler); } /** * shuts down this server and closes the embedded GemFire caching node */ public void shutdown() { if (acceptor != null) { this.acceptor.interrupt(); } this.executor.shutdownNow(); this.cache.close(); } /** * * @param args */ public static void main(String[] args) { int port = getPort(args); GemFireMemcachedServer server = new GemFireMemcachedServer(port); server.start(); while (true) { try { System.in.read(); } catch (IOException e) { e.printStackTrace(); } } } private static int getPort(String[] args) { int port = 0; if (args != null && args.length > 0) { for (int i = 0; i < args.length; i++) { if (args[i].startsWith("-port")) { String p = args[i].substring(args[i].indexOf('=')); p = p.trim(); port = Integer.parseInt(p); } } } return port; } }