package com.feeyo.util.jedis; import com.feeyo.util.jedis.exception.*; import java.io.IOException; import java.net.InetSocketAddress; import java.net.Socket; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicLong; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * BIO 实现,独立通道用于心跳及信息检测服务 * * @author zhuam * */ public class JedisConnection { private static Logger LOGGER = LoggerFactory.getLogger( JedisConnection.class ); // redis command header public static final byte DOLLAR_BYTE = '$'; public static final byte ASTERISK_BYTE = '*'; public static final byte PLUS_BYTE = '+'; public static final byte MINUS_BYTE = '-'; public static final byte COLON_BYTE = ':'; // socket default info public static final String DEFAULT_HOST = "localhost"; public static final int DEFAULT_PORT = 6379; public static final int DEFAULT_TIMEOUT = 2000; public static final int DEFAULT_DATABASE = 0; public static final String CHARSET = "UTF-8"; private static final byte[][] EMPTY_ARGS = new byte[0][]; private String host = DEFAULT_HOST; private int port = DEFAULT_PORT; private int connectionTimeout = DEFAULT_TIMEOUT; private int soTimeout = 0; private boolean broken = false; private Socket socket; private RedisOutputStream outputStream; private RedisInputStream inputStream; protected Pool<JedisConnection> dataSource = null; private static AtomicLong createCnt = new AtomicLong(0); private static AtomicLong closeCnt = new AtomicLong(0); public JedisConnection(final String host, final int port) { this.host = host; this.port = port; // createCnt.incrementAndGet(); } public JedisConnection(final String host, final int port, int connectionTimeout, int soTimeout ) { this.host = host; this.port = port; this.connectionTimeout = connectionTimeout; this.soTimeout = soTimeout; // createCnt.incrementAndGet(); // int s = (int) (createCnt.get() - closeCnt.get()); if ( s > 50 ) { LOGGER.info("jedis connection size={}", s); } } void connect() { if (!isConnected()) { try { socket = new Socket(); socket.setReuseAddress(true); socket.setKeepAlive(true); socket.setTcpNoDelay(true); socket.setSoLinger(true, 0); socket.connect(new InetSocketAddress(host, port), connectionTimeout); if ( soTimeout > 0) socket.setSoTimeout(soTimeout); outputStream = new RedisOutputStream(socket.getOutputStream()); inputStream = new RedisInputStream(socket.getInputStream()); } catch (IOException ex) { broken = true; throw new JedisConnectionException("Failed connecting to host " + host + ":" + port, ex); } } } void disconnect() { if (isConnected()) { try { outputStream.flush(); socket.close(); } catch (IOException ex) { broken = true; throw new JedisConnectionException(ex); } finally { if (socket != null) { try { socket.close(); } catch (IOException e) { // ignored } } } } // try { // if ( outputStream != null) // outputStream.flush(); // // if ( socket != null ) { // socket.close(); // socket = null; // } // // } catch (IOException ex) { // broken = true; // throw new JedisConnectionException(ex); // } finally { // // if ( inputStream != null ) // try { // inputStream.close(); // inputStream = null; // } catch (IOException e1) { // // ignored // } // // if ( outputStream != null) // try { // outputStream.close(); // outputStream = null; // } catch (IOException e1) { // // ignore // } // // if (socket != null) { // try { // socket.close(); // socket = null; // } catch (IOException e) { // // ignored // } // } // } } public void close() { closeCnt.incrementAndGet(); if (dataSource != null) { if (isBroken()) { this.dataSource.returnBrokenResource(this); } else { this.dataSource.returnResource(this); } } else { disconnect(); } } public void setDataSource(Pool<JedisConnection> jedisPool) { this.dataSource = jedisPool; } public boolean isConnected() { return socket != null && socket.isBound() && !socket.isClosed() && socket.isConnected() && !socket.isInputShutdown() && !socket.isOutputShutdown(); } public boolean isBroken() { return broken; } // write get reply //----------------------------------------------------------------- protected void flush() { try { outputStream.flush(); } catch (IOException ex) { broken = true; throw new JedisConnectionException(ex); } } public String getStatusCodeReply() { flush(); final byte[] resp = (byte[]) readProtocolWithCheckingBroken(); if (null == resp) { return null; } else { return SafeEncoder.encode(resp); } } public String getBulkReply() { final byte[] result = getBinaryBulkReply(); if (null != result) { return SafeEncoder.encode(result); } else { return null; } } public byte[] getBinaryBulkReply() { flush(); return (byte[]) readProtocolWithCheckingBroken(); } public Long getIntegerReply() { flush(); return (Long) readProtocolWithCheckingBroken(); } public List<String> getMultiBulkReply() { List<byte[]> data = getBinaryMultiBulkReply(); if (null == data) { return Collections.emptyList(); } List<byte[]> l = (List<byte[]>) data; final ArrayList<String> result = new ArrayList<String>(l.size()); for (final byte[] barray : l) { if (barray == null) { result.add(null); } else { result.add(SafeEncoder.encode(barray)); } } return result; } @SuppressWarnings("unchecked") public List<byte[]> getBinaryMultiBulkReply() { flush(); return (List<byte[]>) readProtocolWithCheckingBroken(); } @SuppressWarnings("unchecked") public List<Object> getRawObjectMultiBulkReply() { return (List<Object>) readProtocolWithCheckingBroken(); } public List<Object> getObjectMultiBulkReply() { flush(); return getRawObjectMultiBulkReply(); } @SuppressWarnings("unchecked") public List<Long> getIntegerMultiBulkReply() { flush(); return (List<Long>) readProtocolWithCheckingBroken(); } public List<Object> getMany(final int count) { flush(); final List<Object> responses = new ArrayList<Object>(count); for (int i = 0; i < count; i++) { try { responses.add(readProtocolWithCheckingBroken()); } catch (JedisDataException e) { responses.add(e); } } return responses; } public Object getOne() { flush(); return readProtocolWithCheckingBroken(); } protected Object readProtocolWithCheckingBroken() { try { return read(inputStream); } catch (JedisConnectionException exc) { broken = true; throw exc; } } // write command but not flush // --------------------------------------------------------------------------- public void sendCommand(final RedisCommand cmd, final String... args) { final byte[][] bargs = new byte[args.length][]; for (int i = 0; i < args.length; i++) { bargs[i] = SafeEncoder.encode(args[i]); } sendCommand(cmd, bargs); } public void sendCommand(final RedisCommand cmd) { sendCommand(cmd, EMPTY_ARGS); } public void sendCommand(final byte[]... args) { try { connect(); sendCommand(outputStream, args); } catch (JedisConnectionException ex) { /* * When client send request which formed by invalid protocol, Redis * send back error message before close connection. We try to read * it to provide reason of failure. */ try { String errorMessage = readErrorLineIfPossible(inputStream); if (errorMessage != null && errorMessage.length() > 0) { ex = new JedisConnectionException(errorMessage, ex.getCause()); } } catch (Exception e) { /* * Catch any IOException or JedisConnectionException occurred * from InputStream#read and just ignore. This approach is safe * because reading error message is optional and connection will * eventually be closed. */ } // Any other exceptions related to connection? broken = true; throw ex; } } public void sendCommand(final RedisCommand cmd, final byte[]... args) { try { connect(); sendCommand(outputStream, cmd.getRaw(), args); } catch (JedisConnectionException ex) { /* * When client send request which formed by invalid protocol, Redis * send back error message before close connection. We try to read * it to provide reason of failure. */ try { String errorMessage = readErrorLineIfPossible(inputStream); if (errorMessage != null && errorMessage.length() > 0) { ex = new JedisConnectionException(errorMessage, ex.getCause()); } } catch (Exception e) { /* * Catch any IOException or JedisConnectionException occurred * from InputStream#read and just ignore. This approach is safe * because reading error message is optional and connection will * eventually be closed. */ } // Any other exceptions related to connection? broken = true; throw ex; } } private void sendCommand(final RedisOutputStream os, final byte[] command, final byte[]... args) { try { os.write(ASTERISK_BYTE); os.writeIntCrLf(args.length + 1); os.write(DOLLAR_BYTE); os.writeIntCrLf(command.length); os.write(command); os.writeCrLf(); for (final byte[] arg : args) { os.write(DOLLAR_BYTE); os.writeIntCrLf(arg.length); os.write(arg); os.writeCrLf(); } } catch (IOException e) { throw new JedisConnectionException(e); } } private void sendCommand(final RedisOutputStream os, final byte[]... args) { try { os.write(ASTERISK_BYTE); os.writeIntCrLf(args.length); for (final byte[] arg : args) { os.write(DOLLAR_BYTE); os.writeIntCrLf(arg.length); os.write(arg); os.writeCrLf(); } } catch (IOException e) { throw new JedisConnectionException(e); } } // read reply and parse // ------------------------------------------------------------------- public static Object read(final RedisInputStream is) { return process(is); } public static String readErrorLineIfPossible(RedisInputStream is) { final byte b = is.readByte(); // if buffer contains other type of response, just ignore. if (b != MINUS_BYTE) { return null; } return is.readLine(); } private static Object process(final RedisInputStream is) { final byte b = is.readByte(); if (b == PLUS_BYTE) { return processStatusCodeReply(is); } else if (b == DOLLAR_BYTE) { return processBulkReply(is); } else if (b == ASTERISK_BYTE) { return processMultiBulkReply(is); } else if (b == COLON_BYTE) { return processInteger(is); } else if (b == MINUS_BYTE) { processError(is); return null; } else { throw new JedisConnectionException("Unknown reply: " + (char) b); } } private static byte[] getBinaryReply(final RedisInputStream is) { try { return is.readAll(); } catch (Exception e) { throw new JedisConnectionException(e); } } public byte[] getBinaryReply() { flush(); return getBinaryReply(inputStream); } private static byte[] processStatusCodeReply(final RedisInputStream is) { return is.readLineBytes(); } private static byte[] processBulkReply(final RedisInputStream is) { final int len = is.readIntCrLf(); if (len == -1) { return null; } final byte[] read = new byte[len]; int offset = 0; while (offset < len) { final int size = is.read(read, offset, (len - offset)); if (size == -1) throw new JedisConnectionException("It seems like server has closed the connection."); offset += size; } // read 2 more bytes for the command delimiter is.readByte(); is.readByte(); return read; } private static Long processInteger(final RedisInputStream is) { return is.readLongCrLf(); } private static List<Object> processMultiBulkReply(final RedisInputStream is) { final int num = is.readIntCrLf(); if (num == -1) { return null; } final List<Object> ret = new ArrayList<Object>(num); for (int i = 0; i < num; i++) { try { ret.add(process(is)); } catch (JedisDataException e) { ret.add(e); } } return ret; } // process info // ------------------------------------------------------------------------------- private static final String ASK_RESPONSE = "ASK"; private static final String MOVED_RESPONSE = "MOVED"; private static final String CLUSTERDOWN_RESPONSE = "CLUSTERDOWN"; private static final String BUSY_RESPONSE = "BUSY"; private static final String NOSCRIPT_RESPONSE = "NOSCRIPT"; private static void processError(final RedisInputStream is) { String message = is.readLine(); // TODO: I'm not sure if this is the best way to do this. // Maybe Read only first 5 bytes instead? if (message.startsWith(MOVED_RESPONSE)) { String[] movedInfo = parseTargetHostAndSlot(message); throw new JedisMovedDataException(message, new HostAndPort(movedInfo[1], Integer.valueOf(movedInfo[2])), Integer.valueOf(movedInfo[0])); } else if (message.startsWith(ASK_RESPONSE)) { String[] askInfo = parseTargetHostAndSlot(message); throw new JedisAskDataException(message, new HostAndPort(askInfo[1], Integer.valueOf(askInfo[2])), Integer.valueOf(askInfo[0])); } else if (message.startsWith(CLUSTERDOWN_RESPONSE)) { throw new JedisClusterException(message); } else if (message.startsWith(BUSY_RESPONSE)) { throw new JedisBusyException(message); } else if (message.startsWith(NOSCRIPT_RESPONSE)) { throw new JedisNoScriptException(message); } throw new JedisDataException(message); } public String getHost() { return host; } public int getPort() { return port; } public String ping() { sendCommand(RedisCommand.PING); return getStatusCodeReply(); } private static String[] parseTargetHostAndSlot(String clusterRedirectResponse) { String[] response = new String[3]; String[] messageInfo = clusterRedirectResponse.split(" "); String[] targetHostAndPort = HostAndPort.extractParts(messageInfo[2]); response[0] = messageInfo[1]; response[1] = targetHostAndPort[0]; response[2] = targetHostAndPort[1]; return response; } }