add client ledis lib and ledis-cli

This commit is contained in:
siddontang 2014-06-21 22:12:37 +08:00
parent 9e09e607c8
commit 55bce5f553
6 changed files with 771 additions and 0 deletions

View File

@ -0,0 +1,81 @@
package main
import (
"bufio"
"flag"
"fmt"
"github.com/siddontang/ledisdb/client/go/ledis"
"os"
"strings"
)
var ip = flag.String("h", "127.0.0.1", "ledisdb server ip (default 127.0.0.1)")
var port = flag.Int("p", 6380, "ledisdb server port (default 6380)")
var socket = flag.String("s", "", "ledisdb server socket, overwrite ip and port")
func main() {
flag.Parse()
cfg := new(ledis.Config)
if len(*socket) > 0 {
cfg.Addr = *socket
} else {
cfg.Addr = fmt.Sprintf("%s:%d", *ip, *port)
}
cfg.MaxIdleConns = 1
c := ledis.NewClient(cfg)
reader := bufio.NewReader(os.Stdin)
for {
fmt.Printf("ledis %s > ", cfg.Addr)
cmd, _ := reader.ReadString('\n')
cmds := strings.Fields(cmd)
if len(cmds) == 0 {
continue
} else {
args := make([]interface{}, len(cmds[1:]))
for i := range args {
args[i] = cmds[1+i]
}
r, err := c.Do(cmds[0], args...)
if err != nil {
fmt.Printf("%s", err.Error())
} else {
printReply(r)
}
fmt.Printf("\n")
}
}
}
func printReply(reply interface{}) {
switch reply := reply.(type) {
case int64:
fmt.Printf("(integer) %d", reply)
case string:
fmt.Printf("%q", reply)
case []byte:
fmt.Printf("%q", reply)
case nil:
fmt.Printf("(empty list or set)")
case ledis.Error:
fmt.Printf("%s", string(reply))
case []interface{}:
for i, v := range reply {
fmt.Printf("%d) ", i)
if v == nil {
fmt.Printf("(nil)")
} else {
fmt.Printf("%q", v)
}
}
default:
fmt.Printf("invalid ledis reply")
}
}

96
client/go/ledis/client.go Normal file
View File

@ -0,0 +1,96 @@
package ledis
import (
"container/list"
"strings"
"sync"
"time"
)
const (
pingPeriod time.Duration = 3 * time.Second
)
type Config struct {
Addr string
MaxIdleConns int
}
type Client struct {
sync.Mutex
cfg *Config
proto string
conns *list.List
}
func NewClient(cfg *Config) *Client {
c := new(Client)
c.cfg = cfg
if strings.Contains(cfg.Addr, "/") {
c.proto = "unix"
} else {
c.proto = "tcp"
}
c.conns = list.New()
return c
}
func (c *Client) Do(cmd string, args ...interface{}) (interface{}, error) {
co := c.get()
r, err := co.Do(cmd, args...)
c.put(co)
return r, err
}
func (c *Client) Close() {
c.Lock()
defer c.Unlock()
for c.conns.Len() > 0 {
e := c.conns.Front()
co := e.Value.(*Conn)
c.conns.Remove(e)
co.finalize()
}
}
func (c *Client) Get() *Conn {
return c.get()
}
func (c *Client) get() *Conn {
c.Lock()
if c.conns.Len() == 0 {
c.Unlock()
return c.newConn()
} else {
e := c.conns.Front()
co := e.Value.(*Conn)
c.conns.Remove(e)
c.Unlock()
return co
}
}
func (c *Client) put(conn *Conn) {
c.Lock()
if c.conns.Len() >= c.cfg.MaxIdleConns {
c.Unlock()
conn.finalize()
} else {
conn.lastActive = time.Now()
c.conns.PushFront(conn)
c.Unlock()
}
}

309
client/go/ledis/conn.go Normal file
View File

@ -0,0 +1,309 @@
package ledis
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"net"
"strconv"
"time"
)
// Error represents an error returned in a command reply.
type Error string
func (err Error) Error() string { return string(err) }
type Conn struct {
client *Client
c net.Conn
br *bufio.Reader
bw *bufio.Writer
lastActive time.Time
// Scratch space for formatting argument length.
// '*' or '$', length, "\r\n"
lenScratch [32]byte
// Scratch space for formatting integers and floats.
numScratch [40]byte
}
func (c *Conn) Close() {
c.client.put(c)
}
func (c *Conn) Do(cmd string, args ...interface{}) (interface{}, error) {
if err := c.connect(); err != nil {
return nil, err
}
if err := c.writeCommand(cmd, args); err != nil {
c.finalize()
return nil, err
}
if err := c.bw.Flush(); err != nil {
c.finalize()
return nil, err
}
if reply, err := c.readReply(); err != nil {
c.finalize()
return nil, err
} else {
if e, ok := reply.(Error); ok {
return reply, e
} else {
return reply, nil
}
}
}
func (c *Conn) finalize() {
if c.c != nil {
c.c.Close()
c.c = nil
}
}
func (c *Conn) connect() error {
if c.c != nil {
return nil
}
var err error
c.c, err = net.Dial(c.client.proto, c.client.cfg.Addr)
if err != nil {
return err
}
if c.br != nil {
c.br.Reset(c.c)
} else {
c.br = bufio.NewReader(c.c)
}
if c.bw != nil {
c.bw.Reset(c.c)
} else {
c.bw = bufio.NewWriter(c.c)
}
return nil
}
func (c *Conn) writeLen(prefix byte, n int) error {
c.lenScratch[len(c.lenScratch)-1] = '\n'
c.lenScratch[len(c.lenScratch)-2] = '\r'
i := len(c.lenScratch) - 3
for {
c.lenScratch[i] = byte('0' + n%10)
i -= 1
n = n / 10
if n == 0 {
break
}
}
c.lenScratch[i] = prefix
_, err := c.bw.Write(c.lenScratch[i:])
return err
}
func (c *Conn) writeString(s string) error {
c.writeLen('$', len(s))
c.bw.WriteString(s)
_, err := c.bw.WriteString("\r\n")
return err
}
func (c *Conn) writeBytes(p []byte) error {
c.writeLen('$', len(p))
c.bw.Write(p)
_, err := c.bw.WriteString("\r\n")
return err
}
func (c *Conn) writeInt64(n int64) error {
return c.writeBytes(strconv.AppendInt(c.numScratch[:0], n, 10))
}
func (c *Conn) writeFloat64(n float64) error {
return c.writeBytes(strconv.AppendFloat(c.numScratch[:0], n, 'g', -1, 64))
}
func (c *Conn) writeCommand(cmd string, args []interface{}) (err error) {
c.writeLen('*', 1+len(args))
err = c.writeString(cmd)
for _, arg := range args {
if err != nil {
break
}
switch arg := arg.(type) {
case string:
err = c.writeString(arg)
case []byte:
err = c.writeBytes(arg)
case int:
err = c.writeInt64(int64(arg))
case int64:
err = c.writeInt64(arg)
case float64:
err = c.writeFloat64(arg)
case bool:
if arg {
err = c.writeString("1")
} else {
err = c.writeString("0")
}
case nil:
err = c.writeString("")
default:
var buf bytes.Buffer
fmt.Fprint(&buf, arg)
err = c.writeBytes(buf.Bytes())
}
}
return err
}
func (c *Conn) readLine() ([]byte, error) {
p, err := c.br.ReadSlice('\n')
if err == bufio.ErrBufferFull {
return nil, errors.New("ledis: long response line")
}
if err != nil {
return nil, err
}
i := len(p) - 2
if i < 0 || p[i] != '\r' {
return nil, errors.New("ledis: bad response line terminator")
}
return p[:i], nil
}
// parseLen parses bulk string and array lengths.
func parseLen(p []byte) (int, error) {
if len(p) == 0 {
return -1, errors.New("ledis: malformed length")
}
if p[0] == '-' && len(p) == 2 && p[1] == '1' {
// handle $-1 and $-1 null replies.
return -1, nil
}
var n int
for _, b := range p {
n *= 10
if b < '0' || b > '9' {
return -1, errors.New("ledis: illegal bytes in length")
}
n += int(b - '0')
}
return n, nil
}
// parseInt parses an integer reply.
func parseInt(p []byte) (interface{}, error) {
if len(p) == 0 {
return 0, errors.New("ledis: malformed integer")
}
var negate bool
if p[0] == '-' {
negate = true
p = p[1:]
if len(p) == 0 {
return 0, errors.New("ledis: malformed integer")
}
}
var n int64
for _, b := range p {
n *= 10
if b < '0' || b > '9' {
return 0, errors.New("ledis: illegal bytes in length")
}
n += int64(b - '0')
}
if negate {
n = -n
}
return n, nil
}
var (
okReply interface{} = "OK"
pongReply interface{} = "PONG"
)
func (c *Conn) readReply() (interface{}, error) {
line, err := c.readLine()
if err != nil {
return nil, err
}
if len(line) == 0 {
return nil, errors.New("ledis: short response line")
}
switch line[0] {
case '+':
switch {
case len(line) == 3 && line[1] == 'O' && line[2] == 'K':
// Avoid allocation for frequent "+OK" response.
return okReply, nil
case len(line) == 5 && line[1] == 'P' && line[2] == 'O' && line[3] == 'N' && line[4] == 'G':
// Avoid allocation in PING command benchmarks :)
return pongReply, nil
default:
return string(line[1:]), nil
}
case '-':
return Error(string(line[1:])), nil
case ':':
return parseInt(line[1:])
case '$':
n, err := parseLen(line[1:])
if n < 0 || err != nil {
return nil, err
}
p := make([]byte, n)
_, err = io.ReadFull(c.br, p)
if err != nil {
return nil, err
}
if line, err := c.readLine(); err != nil {
return nil, err
} else if len(line) != 0 {
return nil, errors.New("ledis: bad bulk string format")
}
return p, nil
case '*':
n, err := parseLen(line[1:])
if n < 0 || err != nil {
return nil, err
}
r := make([]interface{}, n)
for i := range r {
r[i], err = c.readReply()
if err != nil {
return nil, err
}
}
return r, nil
}
return nil, errors.New("ledis: unexpected response line")
}
func (c *Client) newConn() *Conn {
co := new(Conn)
co.client = c
return co
}

View File

@ -0,0 +1,13 @@
// Copyright 2012 Gary Burd
//
// 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.

View File

@ -0,0 +1,15 @@
package ledis
import (
"testing"
)
func TestClient(t *testing.T) {
cfg := new(Config)
cfg.Addr = "127.0.0.1:6380"
cfg.MaxIdleConns = 4
c := NewClient(cfg)
c.Close()
}

257
client/go/ledis/reply.go Normal file
View File

@ -0,0 +1,257 @@
package ledis
import (
"errors"
"fmt"
"strconv"
)
// ErrNil indicates that a reply value is nil.
var ErrNil = errors.New("ledis: nil returned")
// Int is a helper that converts a command reply to an integer. If err is not
// equal to nil, then Int returns 0, err. Otherwise, Int converts the
// reply to an int as follows:
//
// Reply type Result
// integer int(reply), nil
// bulk string parsed reply, nil
// nil 0, ErrNil
// other 0, error
func Int(reply interface{}, err error) (int, error) {
if err != nil {
return 0, err
}
switch reply := reply.(type) {
case int64:
x := int(reply)
if int64(x) != reply {
return 0, strconv.ErrRange
}
return x, nil
case []byte:
n, err := strconv.ParseInt(string(reply), 10, 0)
return int(n), err
case nil:
return 0, ErrNil
case Error:
return 0, reply
}
return 0, fmt.Errorf("ledis: unexpected type for Int, got type %T", reply)
}
// Int64 is a helper that converts a command reply to 64 bit integer. If err is
// not equal to nil, then Int returns 0, err. Otherwise, Int64 converts the
// reply to an int64 as follows:
//
// Reply type Result
// integer reply, nil
// bulk string parsed reply, nil
// nil 0, ErrNil
// other 0, error
func Int64(reply interface{}, err error) (int64, error) {
if err != nil {
return 0, err
}
switch reply := reply.(type) {
case int64:
return reply, nil
case []byte:
n, err := strconv.ParseInt(string(reply), 10, 64)
return n, err
case nil:
return 0, ErrNil
case Error:
return 0, reply
}
return 0, fmt.Errorf("ledis: unexpected type for Int64, got type %T", reply)
}
var errNegativeInt = errors.New("ledis: unexpected value for Uint64")
// Uint64 is a helper that converts a command reply to 64 bit integer. If err is
// not equal to nil, then Int returns 0, err. Otherwise, Int64 converts the
// reply to an int64 as follows:
//
// Reply type Result
// integer reply, nil
// bulk string parsed reply, nil
// nil 0, ErrNil
// other 0, error
func Uint64(reply interface{}, err error) (uint64, error) {
if err != nil {
return 0, err
}
switch reply := reply.(type) {
case int64:
if reply < 0 {
return 0, errNegativeInt
}
return uint64(reply), nil
case []byte:
n, err := strconv.ParseUint(string(reply), 10, 64)
return n, err
case nil:
return 0, ErrNil
case Error:
return 0, reply
}
return 0, fmt.Errorf("ledis: unexpected type for Uint64, got type %T", reply)
}
// Float64 is a helper that converts a command reply to 64 bit float. If err is
// not equal to nil, then Float64 returns 0, err. Otherwise, Float64 converts
// the reply to an int as follows:
//
// Reply type Result
// bulk string parsed reply, nil
// nil 0, ErrNil
// other 0, error
func Float64(reply interface{}, err error) (float64, error) {
if err != nil {
return 0, err
}
switch reply := reply.(type) {
case []byte:
n, err := strconv.ParseFloat(string(reply), 64)
return n, err
case nil:
return 0, ErrNil
case Error:
return 0, reply
}
return 0, fmt.Errorf("ledis: unexpected type for Float64, got type %T", reply)
}
// String is a helper that converts a command reply to a string. If err is not
// equal to nil, then String returns "", err. Otherwise String converts the
// reply to a string as follows:
//
// Reply type Result
// bulk string string(reply), nil
// simple string reply, nil
// nil "", ErrNil
// other "", error
func String(reply interface{}, err error) (string, error) {
if err != nil {
return "", err
}
switch reply := reply.(type) {
case []byte:
return string(reply), nil
case string:
return reply, nil
case nil:
return "", ErrNil
case Error:
return "", reply
}
return "", fmt.Errorf("ledis: unexpected type for String, got type %T", reply)
}
// Bytes is a helper that converts a command reply to a slice of bytes. If err
// is not equal to nil, then Bytes returns nil, err. Otherwise Bytes converts
// the reply to a slice of bytes as follows:
//
// Reply type Result
// bulk string reply, nil
// simple string []byte(reply), nil
// nil nil, ErrNil
// other nil, error
func Bytes(reply interface{}, err error) ([]byte, error) {
if err != nil {
return nil, err
}
switch reply := reply.(type) {
case []byte:
return reply, nil
case string:
return []byte(reply), nil
case nil:
return nil, ErrNil
case Error:
return nil, reply
}
return nil, fmt.Errorf("ledis: unexpected type for Bytes, got type %T", reply)
}
// Bool is a helper that converts a command reply to a boolean. If err is not
// equal to nil, then Bool returns false, err. Otherwise Bool converts the
// reply to boolean as follows:
//
// Reply type Result
// integer value != 0, nil
// bulk string strconv.ParseBool(reply)
// nil false, ErrNil
// other false, error
func Bool(reply interface{}, err error) (bool, error) {
if err != nil {
return false, err
}
switch reply := reply.(type) {
case int64:
return reply != 0, nil
case []byte:
return strconv.ParseBool(string(reply))
case nil:
return false, ErrNil
case Error:
return false, reply
}
return false, fmt.Errorf("ledis: unexpected type for Bool, got type %T", reply)
}
// MultiBulk is deprecated. Use Values.
func MultiBulk(reply interface{}, err error) ([]interface{}, error) { return Values(reply, err) }
// Values is a helper that converts an array command reply to a []interface{}.
// If err is not equal to nil, then Values returns nil, err. Otherwise, Values
// converts the reply as follows:
//
// Reply type Result
// array reply, nil
// nil nil, ErrNil
// other nil, error
func Values(reply interface{}, err error) ([]interface{}, error) {
if err != nil {
return nil, err
}
switch reply := reply.(type) {
case []interface{}:
return reply, nil
case nil:
return nil, ErrNil
case Error:
return nil, reply
}
return nil, fmt.Errorf("ledis: unexpected type for Values, got type %T", reply)
}
// Strings is a helper that converts an array command reply to a []string. If
// err is not equal to nil, then Strings returns nil, err. If one of the array
// items is not a bulk string or nil, then Strings returns an error.
func Strings(reply interface{}, err error) ([]string, error) {
if err != nil {
return nil, err
}
switch reply := reply.(type) {
case []interface{}:
result := make([]string, len(reply))
for i := range reply {
if reply[i] == nil {
continue
}
p, ok := reply[i].([]byte)
if !ok {
return nil, fmt.Errorf("ledis: unexpected element type for Strings, got type %T", reply[i])
}
result[i] = string(p)
}
return result, nil
case nil:
return nil, ErrNil
case Error:
return nil, reply
}
return nil, fmt.Errorf("ledis: unexpected type for Strings, got type %T", reply)
}