package org.handlersocket; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; /** * MySQL pluginの一つであるHandlerSocket(http://github.com/ahiguti/HandlerSocket-Plugin-for-MySQL/)のJavaクライアント実装です。 * * @author moaikids * */ public class HandlerSocket { private static final byte TOKEN_SEPARATOR = 0x09; private static final byte COMMAND_TERMINATE = 0x0a; private static final int SOCKET_TIMEOUT = 30 * 1000; private static final int SOCKET_BUFFER_SIZE = 8 * 1024; private static final int EXECUTE_BUFFER_SIZE = 8 * 1024; private int timeout = SOCKET_TIMEOUT; private int sendBufferSize = SOCKET_BUFFER_SIZE; private int receiveBufferSize = SOCKET_BUFFER_SIZE; private int executeBufferSize = EXECUTE_BUFFER_SIZE; private boolean isBlocking = false;//Blockingモードで動作するかどうか。trueはBlocking/falseはNon-Blocking private boolean tcpNoDelay = true; SocketChannel socket; Selector selector; BlockingQueue<byte[]> commandBuffer; Command command; int currentResultSize = 0;//直前に実行されたコマンドのレスポンスデータサイズ public HandlerSocket(){ super(); commandBuffer = new LinkedBlockingQueue<byte[]>(); command = new Command(); } public void clear(){ this.commandBuffer.clear(); this.currentResultSize = 0; } public Command command(){ return command; } /** * 現在未発行のコマンドの総バイトサイズを返却します。 * @return */ public int getCommandSize(){ int total = 0; for(byte[] b : commandBuffer){ total += b.length; } return total; } /** * 直前に実行されたコマンドのレスポンスデータサイズを返却します。 * @return */ public int getCurrentResponseSize(){ return currentResultSize; } /** * HandlerSocketと接続します。 * @param host * @param port * @throws IOException */ public void open(String host, int port) throws IOException{ open(InetAddress.getByName(host), port); } /** * HandlerSocketと接続します。 * @param address * @param port * @throws IOException */ public void open(InetAddress address, int port) throws IOException{ if(socket != null && socket.isConnected()){ close(); } selector = Selector.open(); socket = SocketChannel.open(); socket.configureBlocking(isBlocking); socket.socket().setReceiveBufferSize(receiveBufferSize); socket.socket().setSendBufferSize(sendBufferSize); socket.socket().setSoTimeout(timeout); socket.socket().setTcpNoDelay(tcpNoDelay); socket.connect(new InetSocketAddress(address, port)); while(!socket.finishConnect()){} } public synchronized List<HandlerSocketResult> execute() throws IOException{ //TODO コマンドが一つもない場合の処理はどうするか?今回は何もしないでnullを返す。 if(commandBuffer.size() == 0) return null; currentResultSize = 0; socket.register(selector, socket.validOps()); List<HandlerSocketResult> results = new ArrayList<HandlerSocketResult>(); //TODO HandlerSocketとの送受信をすべてインラインで記述するか?ひとまずインラインで。 //TODO OutputStream数珠つなぎの影響で無駄なbufferコピーが発生してないか。調べて最適な形に。 //TODO 送受信途中でエラーが発生した場合どうすれば良いか。フェールセーフな方式の検討。 //TODO 一度に実行するコマンドの上限を設けるか?今は無制限。 try{ boolean processComplete = false; while(!processComplete && selector.select() > 0){ Iterator iterator = selector.selectedKeys().iterator(); while(iterator.hasNext()){ SelectionKey key = (SelectionKey)iterator.next(); iterator.remove(); if(key.isWritable()){ SocketChannel channel = (SocketChannel)key.channel(); final ByteArrayOutputStream buf = new ByteArrayOutputStream(); for(byte[] command ; (command = commandBuffer.poll()) != null ; ){ buf.write(command); } channel.register(selector, SelectionKey.OP_READ); ByteBuffer wb = ByteBuffer.wrap(buf.toByteArray()); while(wb.remaining()>0){ channel.write(wb); } }else if(key.isReadable()){ ByteArrayOutputStream buffer = new ByteArrayOutputStream(); int readSize = 0; ByteBuffer rb = ByteBuffer.allocate(executeBufferSize); rb.clear(); for(int size = 0 ; (size = socket.read(rb)) > 0; ){ currentResultSize += size; readSize += size; rb.flip(); buffer.write(rb.array(), 0, size); rb.position(0); rb.clear(); if(size < executeBufferSize) break; } ResponseParser parser = new ResponseParser(); results = parser.parse(buffer.toByteArray()); processComplete = true; break; } } } }finally{ } return results; } /** * HandlerSocketとの接続を切断します。 * @throws IOException */ public void close() throws IOException{ socket.close(); } public int getTimeout() { return timeout; } public void setTimeout(int timeout) { this.timeout = timeout; } public int getSendBufferSize() { return sendBufferSize; } public void setSendBufferSize(int sendBufferSize) { this.sendBufferSize = sendBufferSize; } public int getReceiveBufferSize() { return receiveBufferSize; } public void setReceiveBufferSize(int receiveBufferSize) { this.receiveBufferSize = receiveBufferSize; } public int getExecuteBufferSize() { return executeBufferSize; } public void setExecuteBufferSize(int executeBufferSize) { this.executeBufferSize = executeBufferSize; } public boolean isTcpNoDelay() { return tcpNoDelay; } public void setTcpNoDelay(boolean tcpNoDelay) { this.tcpNoDelay = tcpNoDelay; } /** * HanlerSocketのコマンドを実行します。実行したコマンドはqueueに格納され、HanlerSocket#execute時にまとめて実行されます。 * @author moaikids * */ public class Command{ private static final String OPERATOR_OPEN_INDEX = "P"; private static final String OPERATOR_INSERT = "+"; private static final String OPERATOR_UPDATE = "U"; private static final String OPERATOR_DELETE = "D"; private static final String DEFAULT_ENCODING = "UTF-8"; private String encoding = DEFAULT_ENCODING; public Command(){ super(); } public Command(String encoding){ this(); this.encoding = encoding; } /** * open_indexコマンドを実行します。 * @param id * @param db * @param table * @param index * @param fieldList * @throws IOException */ public void openIndex(String id, String db, String table, String index, String fieldList) throws IOException{ ByteArrayOutputStream buffer = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(buffer); //header writeToken(dos, OPERATOR_OPEN_INDEX); writeTokenSeparator(dos); //id writeToken(dos, id); writeTokenSeparator(dos); //db writeToken(dos, db); writeTokenSeparator(dos); //table writeToken(dos, table); writeTokenSeparator(dos); //index writeToken(dos, index); writeTokenSeparator(dos); //field list writeToken(dos, fieldList); writeCommandTerminate(dos); dos.flush(); commandBuffer.add(buffer.toByteArray()); } /** * findコマンドを実行します。 * @param id * @param keys * @throws IOException */ public void find(String id, String key) throws IOException{ find(id, key , "=" , "1", "0"); } /** * findコマンドを実行します。 * @param id * @param keys * @param operator * @param limit * @param offset * @throws IOException */ public void find(String id, String key, String operator , String limit, String offset) throws IOException{ ByteArrayOutputStream buffer = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(buffer); //id writeToken(dos, id); writeTokenSeparator(dos); //operator writeToken(dos, operator); writeTokenSeparator(dos); //key nums writeToken(dos, "1"); writeTokenSeparator(dos); //key writeToken(dos, key); writeTokenSeparator(dos); //limit writeToken(dos, limit); writeTokenSeparator(dos); //offset writeToken(dos, offset); writeCommandTerminate(dos); dos.flush(); commandBuffer.add(buffer.toByteArray()); } public void insert(String id, String key, byte[] val) throws IOException{ ByteArrayOutputStream buffer = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(buffer); //id writeToken(dos, id); writeTokenSeparator(dos); //operator writeToken(dos, OPERATOR_INSERT); writeTokenSeparator(dos); //key nums writeToken(dos, "2"); writeTokenSeparator(dos); writeToken(dos, key == null ? null : key.getBytes(encoding)); writeTokenSeparator(dos); // value writeToken(dos, val == null ? null : val.toString().getBytes(encoding)); writeCommandTerminate(dos); dos.flush(); commandBuffer.add(buffer.toByteArray()); } /** * insertコマンドを実行します。 * @param id * @param keys * @throws IOException */ public void insert(String id, String[] keys) throws IOException{ ByteArrayOutputStream buffer = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(buffer); //id writeToken(dos, id); writeTokenSeparator(dos); //operator writeToken(dos, OPERATOR_INSERT); writeTokenSeparator(dos); //key nums writeToken(dos, String.valueOf(keys.length)); writeTokenSeparator(dos); for(int i = 0 ; i < keys.length ; i++){ writeToken(dos, keys[i] == null ? null : keys[i].getBytes(encoding)); if(i == keys.length - 1){ writeCommandTerminate(dos); }else{ writeTokenSeparator(dos); } } dos.flush(); commandBuffer.add(buffer.toByteArray()); } /** * find_modify(delete)を実行します。 * @param id * @param keys * @param operator * @param limit * @param offset * @throws IOException */ public void findModifyDelete(String id, String key, String operator , String limit, String offset) throws IOException{ findModify(id, key, operator, limit, offset, OPERATOR_DELETE, new byte[1]); } /** * find_modify(update)を実行します。 * @param id * @param keys * @param operator * @param limit * @param offset * @param values * @throws IOException */ public void findModifyUpdate(String id, String key, String operator , String limit, String offset, byte[] value) throws IOException{ findModify(id, key, operator, limit, offset, OPERATOR_UPDATE, value); } /** * * @param id * @param keys * @param operator * @param limit * @param offset * @param modOperation * @param values * @throws IOException */ private void findModify( String id, String key, String operator , String limit, String offset, String modOperation, byte[] value) throws IOException{ ByteArrayOutputStream buffer = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(buffer); //id writeToken(dos, id); writeTokenSeparator(dos); //operator writeToken(dos, operator); writeTokenSeparator(dos); //key nums writeToken(dos, "1"); writeTokenSeparator(dos); //key writeToken(dos, key == null ? null : key.getBytes(encoding)); writeTokenSeparator(dos); //limit writeToken(dos, limit); writeTokenSeparator(dos); //offset writeToken(dos, offset); writeTokenSeparator(dos); //modify operator writeToken(dos, modOperation); writeTokenSeparator(dos); //modify value writeToken(dos, value.length == 0 ? null : value); writeCommandTerminate(dos); dos.flush(); commandBuffer.add(buffer.toByteArray()); } private void writeToken(DataOutputStream dos, String token) throws IOException{ if(token == null){ dos.writeByte(0x00); }else{ for(char c : token.toCharArray()){ if(c > 255){ dos.writeChar(c); }else{ dos.writeByte((byte)c); } } } } private void writeToken(DataOutputStream dos, byte[] token) throws IOException{ if(token == null){ dos.writeByte(0x00); }else{ for(byte b : token){ dos.writeByte(b); } } } private void writeTokenSeparator(DataOutputStream dos) throws IOException{ dos.writeByte(TOKEN_SEPARATOR); } private void writeCommandTerminate(DataOutputStream dos) throws IOException{ dos.writeByte(COMMAND_TERMINATE); } } class ResponseParser{ private static final String DEFAULT_ENCODING = "UTF-8"; private String encoding = DEFAULT_ENCODING; public ResponseParser(){ super(); } public ResponseParser(String encoding){ this(); this.encoding = encoding; } public List<HandlerSocketResult> parse(byte[] data) throws UnsupportedEncodingException{ List<HandlerSocketResult> results = new ArrayList<HandlerSocketResult>(); //TODO 中途半端で終わったレスポンス内容は破棄しています。その実装で良いか確認。 for(int i = 0 ; i < data.length ; ){ List<byte[]> messages = new ArrayList<byte[]>(); ByteArrayOutputStream buf = new ByteArrayOutputStream(); HandlerSocketResult result = new HandlerSocketResult(); int status = data[i] - 0x30 ; i++; if(i >= data.length) break; if(data[i] != 0x09) throw new RuntimeException(); //TOOD i++; if(i >= data.length) break;//0x09 int fieldNum = data[i] - 0x30 ; i++; if(i >= data.length) break; if(data[i] == 0x0a){ result.setStatus(status); result.setFieldNum(fieldNum); result.setMessages(messages); results.add(result); i++;//0x09 or 0x0a continue; }else{ i++;//0x09 or 0x0a } while(true){ if(data.length <= i) break; byte b = data[i]; i++; if(b == COMMAND_TERMINATE){ messages.add(buf.toByteArray()); result.setStatus(status); result.setFieldNum(fieldNum); result.setMessages(messages); results.add(result); break; } if(b == TOKEN_SEPARATOR){ messages.add(buf.toByteArray()); buf = new ByteArrayOutputStream(); continue; } buf.write(b); } } return results; } public String getEncoding() { return encoding; } public void setEncoding(String encoding) { this.encoding = encoding; } } }