add a http client
This commit is contained in:
commit
0f3fd4b1f9
43
README.md
Normal file
43
README.md
Normal file
@ -0,0 +1,43 @@
|
||||
# HTTP Client
|
||||
|
||||
This plugin is a http client for go-micro.
|
||||
|
||||
The http client wraps net/http to provide a robust go-micro client with service discovery, load balancing and streaming.
|
||||
|
||||
## Usage
|
||||
|
||||
### Use directly
|
||||
|
||||
```go
|
||||
import "github.com/micro/go-plugins/client/http"
|
||||
|
||||
service := micro.NewService(
|
||||
micro.Name("my.service"),
|
||||
micro.Client(http.NewClient()),
|
||||
)
|
||||
```
|
||||
|
||||
### Use with flags
|
||||
|
||||
```go
|
||||
import _ "github.com/micro/go-plugins/client/http"
|
||||
```
|
||||
|
||||
```shell
|
||||
go run main.go --client=http
|
||||
```
|
||||
|
||||
### Call Service
|
||||
|
||||
Assuming you have a http service "my.service" with path "/foo/bar"
|
||||
```go
|
||||
// new client
|
||||
client := http.NewClient()
|
||||
|
||||
// create request/response
|
||||
request := client.NewRequest("my.service", "/foo/bar", &proto.Request{})
|
||||
response := new(proto.Response)
|
||||
|
||||
// call service
|
||||
err := client.Call(context.TODO(), request, response)
|
||||
```
|
14
buffer.go
Normal file
14
buffer.go
Normal file
@ -0,0 +1,14 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
)
|
||||
|
||||
type buffer struct {
|
||||
*bytes.Buffer
|
||||
}
|
||||
|
||||
func (b *buffer) Close() error {
|
||||
b.Buffer.Reset()
|
||||
return nil
|
||||
}
|
61
codec.go
Normal file
61
codec.go
Normal file
@ -0,0 +1,61 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/micro/go-micro/codec"
|
||||
"github.com/micro/go-micro/codec/jsonrpc"
|
||||
"github.com/micro/go-micro/codec/protorpc"
|
||||
)
|
||||
|
||||
type jsonCodec struct{}
|
||||
|
||||
type protoCodec struct{}
|
||||
|
||||
type Codec interface {
|
||||
Marshal(v interface{}) ([]byte, error)
|
||||
Unmarshal(b []byte, v interface{}) error
|
||||
String() string
|
||||
}
|
||||
|
||||
var (
|
||||
defaultHTTPCodecs = map[string]Codec{
|
||||
"application/json": jsonCodec{},
|
||||
"application/proto": protoCodec{},
|
||||
"application/protobuf": protoCodec{},
|
||||
"application/octet-stream": protoCodec{},
|
||||
}
|
||||
|
||||
defaultRPCCodecs = map[string]codec.NewCodec{
|
||||
"application/json": jsonrpc.NewCodec,
|
||||
"application/json-rpc": jsonrpc.NewCodec,
|
||||
"application/protobuf": protorpc.NewCodec,
|
||||
"application/proto-rpc": protorpc.NewCodec,
|
||||
"application/octet-stream": protorpc.NewCodec,
|
||||
}
|
||||
)
|
||||
|
||||
func (protoCodec) Marshal(v interface{}) ([]byte, error) {
|
||||
return proto.Marshal(v.(proto.Message))
|
||||
}
|
||||
|
||||
func (protoCodec) Unmarshal(data []byte, v interface{}) error {
|
||||
return proto.Unmarshal(data, v.(proto.Message))
|
||||
}
|
||||
|
||||
func (protoCodec) String() string {
|
||||
return "proto"
|
||||
}
|
||||
|
||||
func (jsonCodec) Marshal(v interface{}) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
func (jsonCodec) Unmarshal(data []byte, v interface{}) error {
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
func (jsonCodec) String() string {
|
||||
return "json"
|
||||
}
|
485
http.go
Normal file
485
http.go
Normal file
@ -0,0 +1,485 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/micro/go-micro/broker"
|
||||
"github.com/micro/go-micro/client"
|
||||
"github.com/micro/go-micro/cmd"
|
||||
"github.com/micro/go-micro/codec"
|
||||
errors "github.com/micro/go-micro/errors"
|
||||
"github.com/micro/go-micro/metadata"
|
||||
"github.com/micro/go-micro/registry"
|
||||
"github.com/micro/go-micro/selector"
|
||||
"github.com/micro/go-micro/transport"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type httpClient struct {
|
||||
once sync.Once
|
||||
opts client.Options
|
||||
}
|
||||
|
||||
func init() {
|
||||
cmd.DefaultClients["http"] = NewClient
|
||||
}
|
||||
|
||||
func (h *httpClient) call(ctx context.Context, address string, req client.Request, rsp interface{}, opts client.CallOptions) error {
|
||||
header := make(http.Header)
|
||||
if md, ok := metadata.FromContext(ctx); ok {
|
||||
for k, v := range md {
|
||||
header.Set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// set timeout in nanoseconds
|
||||
header.Set("Timeout", fmt.Sprintf("%d", opts.RequestTimeout))
|
||||
// set the content type for the request
|
||||
header.Set("Content-Type", req.ContentType())
|
||||
|
||||
// get codec
|
||||
cf, err := h.newHTTPCodec(req.ContentType())
|
||||
if err != nil {
|
||||
return errors.InternalServerError("go.micro.client", err.Error())
|
||||
}
|
||||
|
||||
// marshal request
|
||||
b, err := cf.Marshal(req.Request())
|
||||
if err != nil {
|
||||
return errors.InternalServerError("go.micro.client", err.Error())
|
||||
}
|
||||
|
||||
buf := &buffer{bytes.NewBuffer(b)}
|
||||
defer buf.Close()
|
||||
|
||||
hreq := &http.Request{
|
||||
Method: "POST",
|
||||
URL: &url.URL{
|
||||
Scheme: "http",
|
||||
Host: address,
|
||||
Path: req.Method(),
|
||||
},
|
||||
Header: header,
|
||||
Body: buf,
|
||||
ContentLength: int64(len(b)),
|
||||
Host: address,
|
||||
}
|
||||
|
||||
// make the request
|
||||
hrsp, err := http.DefaultClient.Do(hreq.WithContext(ctx))
|
||||
if err != nil {
|
||||
return errors.InternalServerError("go.micro.client", err.Error())
|
||||
}
|
||||
defer hrsp.Body.Close()
|
||||
|
||||
// parse response
|
||||
b, err = ioutil.ReadAll(hrsp.Body)
|
||||
if err != nil {
|
||||
return errors.InternalServerError("go.micro.client", err.Error())
|
||||
}
|
||||
|
||||
// unmarshal
|
||||
if err := cf.Unmarshal(b, rsp); err != nil {
|
||||
return errors.InternalServerError("go.micro.client", err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *httpClient) stream(ctx context.Context, address string, req client.Request, opts client.CallOptions) (client.Streamer, error) {
|
||||
header := make(http.Header)
|
||||
if md, ok := metadata.FromContext(ctx); ok {
|
||||
for k, v := range md {
|
||||
header.Set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// set timeout in nanoseconds
|
||||
header.Set("Timeout", fmt.Sprintf("%d", opts.RequestTimeout))
|
||||
// set the content type for the request
|
||||
header.Set("Content-Type", req.ContentType())
|
||||
|
||||
// get codec
|
||||
cf, err := h.newHTTPCodec(req.ContentType())
|
||||
if err != nil {
|
||||
return nil, errors.InternalServerError("go.micro.client", err.Error())
|
||||
}
|
||||
|
||||
cc, err := net.Dial("tcp", address)
|
||||
if err != nil {
|
||||
return nil, errors.InternalServerError("go.micro.client", fmt.Sprintf("Error dialing: %v", err))
|
||||
}
|
||||
|
||||
return &httpStream{
|
||||
address: address,
|
||||
context: ctx,
|
||||
closed: make(chan bool),
|
||||
conn: cc,
|
||||
codec: cf,
|
||||
header: header,
|
||||
reader: bufio.NewReader(cc),
|
||||
request: req,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *httpClient) newHTTPCodec(contentType string) (Codec, error) {
|
||||
if c, ok := defaultHTTPCodecs[contentType]; ok {
|
||||
return c, nil
|
||||
}
|
||||
return nil, fmt.Errorf("Unsupported Content-Type: %s", contentType)
|
||||
}
|
||||
|
||||
func (h *httpClient) newCodec(contentType string) (codec.NewCodec, error) {
|
||||
if c, ok := h.opts.Codecs[contentType]; ok {
|
||||
return c, nil
|
||||
}
|
||||
if cf, ok := defaultRPCCodecs[contentType]; ok {
|
||||
return cf, nil
|
||||
}
|
||||
return nil, fmt.Errorf("Unsupported Content-Type: %s", contentType)
|
||||
}
|
||||
|
||||
func (h *httpClient) Init(opts ...client.Option) error {
|
||||
for _, o := range opts {
|
||||
o(&h.opts)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *httpClient) Options() client.Options {
|
||||
return h.opts
|
||||
}
|
||||
|
||||
func (h *httpClient) NewPublication(topic string, msg interface{}) client.Publication {
|
||||
return newHTTPPublication(topic, msg, "application/proto")
|
||||
}
|
||||
|
||||
func (h *httpClient) NewRequest(service, method string, req interface{}, reqOpts ...client.RequestOption) client.Request {
|
||||
return newHTTPRequest(service, method, req, h.opts.ContentType, reqOpts...)
|
||||
}
|
||||
|
||||
func (h *httpClient) NewProtoRequest(service, method string, req interface{}, reqOpts ...client.RequestOption) client.Request {
|
||||
return newHTTPRequest(service, method, req, "application/proto", reqOpts...)
|
||||
}
|
||||
|
||||
func (h *httpClient) NewJsonRequest(service, method string, req interface{}, reqOpts ...client.RequestOption) client.Request {
|
||||
return newHTTPRequest(service, method, req, "application/json", reqOpts...)
|
||||
}
|
||||
|
||||
func (h *httpClient) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error {
|
||||
// make a copy of call opts
|
||||
callOpts := h.opts.CallOptions
|
||||
for _, opt := range opts {
|
||||
opt(&callOpts)
|
||||
}
|
||||
|
||||
// get next nodes from the selector
|
||||
next, err := h.opts.Selector.Select(req.Service(), callOpts.SelectOptions...)
|
||||
if err != nil && err == selector.ErrNotFound {
|
||||
return errors.NotFound("go.micro.client", err.Error())
|
||||
} else if err != nil {
|
||||
return errors.InternalServerError("go.micro.client", err.Error())
|
||||
}
|
||||
|
||||
// check if we already have a deadline
|
||||
d, ok := ctx.Deadline()
|
||||
if !ok {
|
||||
// no deadline so we create a new one
|
||||
ctx, _ = context.WithTimeout(ctx, callOpts.RequestTimeout)
|
||||
} else {
|
||||
// got a deadline so no need to setup context
|
||||
// but we need to set the timeout we pass along
|
||||
opt := client.WithRequestTimeout(d.Sub(time.Now()))
|
||||
opt(&callOpts)
|
||||
}
|
||||
|
||||
// should we noop right here?
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
|
||||
default:
|
||||
}
|
||||
|
||||
// make copy of call method
|
||||
hcall := h.call
|
||||
|
||||
// wrap the call in reverse
|
||||
for i := len(callOpts.CallWrappers); i > 0; i-- {
|
||||
hcall = callOpts.CallWrappers[i-1](hcall)
|
||||
}
|
||||
|
||||
// return errors.New("go.micro.client", "request timeout", 408)
|
||||
call := func(i int) error {
|
||||
// call backoff first. Someone may want an initial start delay
|
||||
t, err := callOpts.Backoff(ctx, req, i)
|
||||
if err != nil {
|
||||
return errors.InternalServerError("go.micro.client", err.Error())
|
||||
}
|
||||
|
||||
// only sleep if greater than 0
|
||||
if t.Seconds() > 0 {
|
||||
time.Sleep(t)
|
||||
}
|
||||
|
||||
// select next node
|
||||
node, err := next()
|
||||
if err != nil && err == selector.ErrNotFound {
|
||||
return errors.NotFound("go.micro.client", err.Error())
|
||||
} else if err != nil {
|
||||
return errors.InternalServerError("go.micro.client", err.Error())
|
||||
}
|
||||
|
||||
// set the address
|
||||
addr := node.Address
|
||||
if node.Port > 0 {
|
||||
addr = fmt.Sprintf("%s:%d", addr, node.Port)
|
||||
}
|
||||
|
||||
// make the call
|
||||
err = hcall(ctx, addr, req, rsp, callOpts)
|
||||
h.opts.Selector.Mark(req.Service(), node, err)
|
||||
return err
|
||||
}
|
||||
|
||||
ch := make(chan error, callOpts.Retries)
|
||||
var gerr error
|
||||
|
||||
for i := 0; i < callOpts.Retries; i++ {
|
||||
go func() {
|
||||
ch <- call(i)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
|
||||
case err := <-ch:
|
||||
// if the call succeeded lets bail early
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
retry, rerr := callOpts.Retry(ctx, req, i, err)
|
||||
if rerr != nil {
|
||||
return rerr
|
||||
}
|
||||
|
||||
if !retry {
|
||||
return err
|
||||
}
|
||||
|
||||
gerr = err
|
||||
}
|
||||
}
|
||||
|
||||
return gerr
|
||||
}
|
||||
|
||||
func (h *httpClient) CallRemote(ctx context.Context, addr string, req client.Request, rsp interface{}, opts ...client.CallOption) error {
|
||||
callOpts := h.opts.CallOptions
|
||||
for _, opt := range opts {
|
||||
opt(&callOpts)
|
||||
}
|
||||
return h.call(ctx, addr, req, rsp, callOpts)
|
||||
}
|
||||
|
||||
func (h *httpClient) Stream(ctx context.Context, req client.Request, opts ...client.CallOption) (client.Streamer, error) {
|
||||
// make a copy of call opts
|
||||
callOpts := h.opts.CallOptions
|
||||
for _, opt := range opts {
|
||||
opt(&callOpts)
|
||||
}
|
||||
|
||||
// get next nodes from the selector
|
||||
next, err := h.opts.Selector.Select(req.Service(), callOpts.SelectOptions...)
|
||||
if err != nil && err == selector.ErrNotFound {
|
||||
return nil, errors.NotFound("go.micro.client", err.Error())
|
||||
} else if err != nil {
|
||||
return nil, errors.InternalServerError("go.micro.client", err.Error())
|
||||
}
|
||||
|
||||
// check if we already have a deadline
|
||||
d, ok := ctx.Deadline()
|
||||
if !ok {
|
||||
// no deadline so we create a new one
|
||||
ctx, _ = context.WithTimeout(ctx, callOpts.RequestTimeout)
|
||||
} else {
|
||||
// got a deadline so no need to setup context
|
||||
// but we need to set the timeout we pass along
|
||||
opt := client.WithRequestTimeout(d.Sub(time.Now()))
|
||||
opt(&callOpts)
|
||||
}
|
||||
|
||||
// should we noop right here?
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
|
||||
default:
|
||||
}
|
||||
|
||||
call := func(i int) (client.Streamer, error) {
|
||||
// call backoff first. Someone may want an initial start delay
|
||||
t, err := callOpts.Backoff(ctx, req, i)
|
||||
if err != nil {
|
||||
return nil, errors.InternalServerError("go.micro.client", err.Error())
|
||||
}
|
||||
|
||||
// only sleep if greater than 0
|
||||
if t.Seconds() > 0 {
|
||||
time.Sleep(t)
|
||||
}
|
||||
|
||||
node, err := next()
|
||||
if err != nil && err == selector.ErrNotFound {
|
||||
return nil, errors.NotFound("go.micro.client", err.Error())
|
||||
} else if err != nil {
|
||||
return nil, errors.InternalServerError("go.micro.client", err.Error())
|
||||
}
|
||||
|
||||
addr := node.Address
|
||||
if node.Port > 0 {
|
||||
addr = fmt.Sprintf("%s:%d", addr, node.Port)
|
||||
}
|
||||
|
||||
stream, err := h.stream(ctx, addr, req, callOpts)
|
||||
h.opts.Selector.Mark(req.Service(), node, err)
|
||||
return stream, err
|
||||
}
|
||||
|
||||
type response struct {
|
||||
stream client.Streamer
|
||||
err error
|
||||
}
|
||||
|
||||
ch := make(chan response, callOpts.Retries)
|
||||
var grr error
|
||||
|
||||
for i := 0; i < callOpts.Retries; i++ {
|
||||
go func() {
|
||||
s, err := call(i)
|
||||
ch <- response{s, err}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
|
||||
case rsp := <-ch:
|
||||
// if the call succeeded lets bail early
|
||||
if rsp.err == nil {
|
||||
return rsp.stream, nil
|
||||
}
|
||||
|
||||
retry, rerr := callOpts.Retry(ctx, req, i, err)
|
||||
if rerr != nil {
|
||||
return nil, rerr
|
||||
}
|
||||
|
||||
if !retry {
|
||||
return nil, rsp.err
|
||||
}
|
||||
|
||||
grr = rsp.err
|
||||
}
|
||||
}
|
||||
|
||||
return nil, grr
|
||||
}
|
||||
|
||||
func (h *httpClient) StreamRemote(ctx context.Context, addr string, req client.Request, opts ...client.CallOption) (client.Streamer, error) {
|
||||
callOpts := h.opts.CallOptions
|
||||
for _, opt := range opts {
|
||||
opt(&callOpts)
|
||||
}
|
||||
return h.stream(ctx, addr, req, callOpts)
|
||||
}
|
||||
|
||||
func (h *httpClient) Publish(ctx context.Context, p client.Publication, opts ...client.PublishOption) error {
|
||||
md, ok := metadata.FromContext(ctx)
|
||||
if !ok {
|
||||
md = make(map[string]string)
|
||||
}
|
||||
md["Content-Type"] = p.ContentType()
|
||||
|
||||
cf, err := h.newCodec(p.ContentType())
|
||||
if err != nil {
|
||||
return errors.InternalServerError("go.micro.client", err.Error())
|
||||
}
|
||||
|
||||
b := &buffer{bytes.NewBuffer(nil)}
|
||||
if err := cf(b).Write(&codec.Message{Type: codec.Publication}, p.Message()); err != nil {
|
||||
return errors.InternalServerError("go.micro.client", err.Error())
|
||||
}
|
||||
|
||||
h.once.Do(func() {
|
||||
h.opts.Broker.Connect()
|
||||
})
|
||||
|
||||
return h.opts.Broker.Publish(p.Topic(), &broker.Message{
|
||||
Header: md,
|
||||
Body: b.Bytes(),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *httpClient) String() string {
|
||||
return "http"
|
||||
}
|
||||
|
||||
func newClient(opts ...client.Option) client.Client {
|
||||
options := client.Options{
|
||||
CallOptions: client.CallOptions{
|
||||
Backoff: client.DefaultBackoff,
|
||||
Retry: client.DefaultRetry,
|
||||
Retries: client.DefaultRetries,
|
||||
RequestTimeout: client.DefaultRequestTimeout,
|
||||
DialTimeout: transport.DefaultDialTimeout,
|
||||
},
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
if len(options.ContentType) == 0 {
|
||||
options.ContentType = "application/proto"
|
||||
}
|
||||
|
||||
if options.Broker == nil {
|
||||
options.Broker = broker.DefaultBroker
|
||||
}
|
||||
|
||||
if options.Registry == nil {
|
||||
options.Registry = registry.DefaultRegistry
|
||||
}
|
||||
|
||||
if options.Selector == nil {
|
||||
options.Selector = selector.NewSelector(
|
||||
selector.Registry(options.Registry),
|
||||
)
|
||||
}
|
||||
|
||||
rc := &httpClient{
|
||||
once: sync.Once{},
|
||||
opts: options,
|
||||
}
|
||||
|
||||
c := client.Client(rc)
|
||||
|
||||
// wrap in reverse
|
||||
for i := len(options.Wrappers); i > 0; i-- {
|
||||
c = options.Wrappers[i-1](c)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func NewClient(opts ...client.Option) client.Client {
|
||||
return newClient(opts...)
|
||||
}
|
280
http_test.go
Normal file
280
http_test.go
Normal file
@ -0,0 +1,280 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/micro/go-micro/client"
|
||||
"github.com/micro/go-micro/registry"
|
||||
"github.com/micro/go-micro/selector"
|
||||
"github.com/micro/go-plugins/client/http/proto"
|
||||
"github.com/micro/go-plugins/registry/memory"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestHTTPClient(t *testing.T) {
|
||||
r := memory.NewRegistry()
|
||||
s := selector.NewSelector(selector.Registry(r))
|
||||
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/foo/bar", func(w http.ResponseWriter, r *http.Request) {
|
||||
// only accept post
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "expect post method", 500)
|
||||
return
|
||||
}
|
||||
|
||||
// get codec
|
||||
ct := r.Header.Get("Content-Type")
|
||||
codec, ok := defaultHTTPCodecs[ct]
|
||||
if !ok {
|
||||
http.Error(w, "codec not found", 500)
|
||||
return
|
||||
}
|
||||
b, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
// extract message
|
||||
msg := new(test.Message)
|
||||
if err := codec.Unmarshal(b, msg); err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
// marshal response
|
||||
b, err = codec.Marshal(msg)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
// write response
|
||||
w.Write(b)
|
||||
})
|
||||
go http.Serve(l, mux)
|
||||
|
||||
host, sport, err := net.SplitHostPort(l.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
port, _ := strconv.Atoi(sport)
|
||||
|
||||
if err := r.Register(®istry.Service{
|
||||
Name: "test.service",
|
||||
Nodes: []*registry.Node{
|
||||
{
|
||||
Id: "test.service.1",
|
||||
Address: host,
|
||||
Port: port,
|
||||
},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
c := NewClient(client.Selector(s))
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
msg := &test.Message{
|
||||
Seq: int64(i),
|
||||
Data: fmt.Sprintf("message %d", i),
|
||||
}
|
||||
req := c.NewRequest("test.service", "/foo/bar", msg)
|
||||
rsp := new(test.Message)
|
||||
err := c.Call(context.TODO(), req, rsp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rsp.Seq != msg.Seq {
|
||||
t.Fatalf("invalid seq %d for %d", rsp.Seq, msg.Seq)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPClientStream(t *testing.T) {
|
||||
r := memory.NewRegistry()
|
||||
s := selector.NewSelector(selector.Registry(r))
|
||||
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/foo/bar", func(w http.ResponseWriter, r *http.Request) {
|
||||
// only accept post
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "expect post method", 500)
|
||||
return
|
||||
}
|
||||
|
||||
// hijack the connection
|
||||
hj, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
http.Error(w, "could not hijack conn", 500)
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
// hijacked
|
||||
conn, bufrw, err := hj.Hijack()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// read off the first request
|
||||
// get codec
|
||||
ct := r.Header.Get("Content-Type")
|
||||
codec, ok := defaultHTTPCodecs[ct]
|
||||
if !ok {
|
||||
http.Error(w, "codec not found", 500)
|
||||
return
|
||||
}
|
||||
b, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
// extract message
|
||||
msg := new(test.Message)
|
||||
if err := codec.Unmarshal(b, msg); err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
// marshal response
|
||||
b, err = codec.Marshal(msg)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
// write response
|
||||
rsp := &http.Response{
|
||||
Header: r.Header,
|
||||
Body: &buffer{bytes.NewBuffer(b)},
|
||||
Status: "200 OK",
|
||||
StatusCode: 200,
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
ContentLength: int64(len(b)),
|
||||
}
|
||||
|
||||
// write response
|
||||
rsp.Write(bufrw)
|
||||
bufrw.Flush()
|
||||
|
||||
reader := bufio.NewReader(conn)
|
||||
|
||||
for {
|
||||
r, err := http.ReadRequest(reader)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
b, err = ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
// extract message
|
||||
msg := new(test.Message)
|
||||
if err := codec.Unmarshal(b, msg); err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
// marshal response
|
||||
b, err = codec.Marshal(msg)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
rsp := &http.Response{
|
||||
Header: r.Header,
|
||||
Body: &buffer{bytes.NewBuffer(b)},
|
||||
Status: "200 OK",
|
||||
StatusCode: 200,
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
ContentLength: int64(len(b)),
|
||||
}
|
||||
|
||||
// write response
|
||||
rsp.Write(bufrw)
|
||||
bufrw.Flush()
|
||||
}
|
||||
})
|
||||
go http.Serve(l, mux)
|
||||
|
||||
host, sport, err := net.SplitHostPort(l.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
port, _ := strconv.Atoi(sport)
|
||||
|
||||
if err := r.Register(®istry.Service{
|
||||
Name: "test.service",
|
||||
Nodes: []*registry.Node{
|
||||
{
|
||||
Id: "test.service.1",
|
||||
Address: host,
|
||||
Port: port,
|
||||
},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
c := NewClient(client.Selector(s))
|
||||
req := c.NewRequest("test.service", "/foo/bar", new(test.Message))
|
||||
stream, err := c.Stream(context.TODO(), req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stream.Close()
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
msg := &test.Message{
|
||||
Seq: int64(i),
|
||||
Data: fmt.Sprintf("message %d", i),
|
||||
}
|
||||
err := stream.Send(msg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rsp := new(test.Message)
|
||||
err = stream.Recv(rsp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rsp.Seq != msg.Seq {
|
||||
t.Fatalf("invalid seq %d for %d", rsp.Seq, msg.Seq)
|
||||
}
|
||||
}
|
||||
}
|
60
proto/test.pb.go
Normal file
60
proto/test.pb.go
Normal file
@ -0,0 +1,60 @@
|
||||
// Code generated by protoc-gen-go.
|
||||
// source: github.com/micro/go-plugins/client/http/proto/test.proto
|
||||
// DO NOT EDIT!
|
||||
|
||||
/*
|
||||
Package test is a generated protocol buffer package.
|
||||
|
||||
It is generated from these files:
|
||||
github.com/micro/go-plugins/client/http/proto/test.proto
|
||||
|
||||
It has these top-level messages:
|
||||
Message
|
||||
*/
|
||||
package test
|
||||
|
||||
import proto "github.com/golang/protobuf/proto"
|
||||
import fmt "fmt"
|
||||
import math "math"
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
var _ = proto.Marshal
|
||||
var _ = fmt.Errorf
|
||||
var _ = math.Inf
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the proto package it is being compiled against.
|
||||
// A compilation error at this line likely means your copy of the
|
||||
// proto package needs to be updated.
|
||||
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
|
||||
|
||||
type Message struct {
|
||||
Seq int64 `protobuf:"varint,1,opt,name=seq" json:"seq,omitempty"`
|
||||
Data string `protobuf:"bytes,2,opt,name=data" json:"data,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Message) Reset() { *m = Message{} }
|
||||
func (m *Message) String() string { return proto.CompactTextString(m) }
|
||||
func (*Message) ProtoMessage() {}
|
||||
func (*Message) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
|
||||
|
||||
func init() {
|
||||
proto.RegisterType((*Message)(nil), "test.Message")
|
||||
}
|
||||
|
||||
func init() {
|
||||
proto.RegisterFile("github.com/micro/go-plugins/client/http/proto/test.proto", fileDescriptor0)
|
||||
}
|
||||
|
||||
var fileDescriptor0 = []byte{
|
||||
// 131 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x1c, 0xcb, 0xb1, 0x0e, 0x82, 0x30,
|
||||
0x10, 0x06, 0xe0, 0x54, 0x88, 0xc6, 0x4e, 0xa6, 0x13, 0x23, 0x71, 0x62, 0x91, 0x1b, 0x5c, 0x7c,
|
||||
0x09, 0x17, 0xde, 0xa0, 0xd4, 0x4b, 0x69, 0x02, 0x5c, 0xe5, 0x7e, 0xde, 0x9f, 0xd0, 0xed, 0x5b,
|
||||
0x3e, 0xfb, 0x89, 0x09, 0xd3, 0x3e, 0xf6, 0x41, 0x16, 0x5a, 0x52, 0xd8, 0x84, 0xa2, 0xbc, 0xf2,
|
||||
0xbc, 0xc7, 0xb4, 0x2a, 0x85, 0x39, 0xf1, 0x0a, 0x9a, 0x80, 0x4c, 0x79, 0x13, 0x08, 0x81, 0x15,
|
||||
0x7d, 0xa1, 0xab, 0x4f, 0x3f, 0xc9, 0xde, 0xbe, 0xac, 0xea, 0x23, 0xbb, 0x87, 0xad, 0x94, 0xff,
|
||||
0x8d, 0x69, 0x4d, 0x57, 0x0d, 0x27, 0x9d, 0xb3, 0xf5, 0xcf, 0xc3, 0x37, 0x97, 0xd6, 0x74, 0xf7,
|
||||
0xa1, 0x78, 0xbc, 0x96, 0xfd, 0x3e, 0x02, 0x00, 0x00, 0xff, 0xff, 0xff, 0x16, 0x8d, 0x95, 0x79,
|
||||
0x00, 0x00, 0x00,
|
||||
}
|
8
proto/test.proto
Normal file
8
proto/test.proto
Normal file
@ -0,0 +1,8 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package test;
|
||||
|
||||
message Message {
|
||||
int64 seq = 1;
|
||||
string data = 2;
|
||||
}
|
31
publication.go
Normal file
31
publication.go
Normal file
@ -0,0 +1,31 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"github.com/micro/go-micro/client"
|
||||
)
|
||||
|
||||
type httpPublication struct {
|
||||
topic string
|
||||
contentType string
|
||||
message interface{}
|
||||
}
|
||||
|
||||
func newHTTPPublication(topic string, message interface{}, contentType string) client.Publication {
|
||||
return &httpPublication{
|
||||
message: message,
|
||||
topic: topic,
|
||||
contentType: contentType,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *httpPublication) ContentType() string {
|
||||
return h.contentType
|
||||
}
|
||||
|
||||
func (h *httpPublication) Topic() string {
|
||||
return h.topic
|
||||
}
|
||||
|
||||
func (h *httpPublication) Message() interface{} {
|
||||
return h.message
|
||||
}
|
48
request.go
Normal file
48
request.go
Normal file
@ -0,0 +1,48 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"github.com/micro/go-micro/client"
|
||||
)
|
||||
|
||||
type httpRequest struct {
|
||||
service string
|
||||
method string
|
||||
contentType string
|
||||
request interface{}
|
||||
opts client.RequestOptions
|
||||
}
|
||||
|
||||
func newHTTPRequest(service, method string, request interface{}, contentType string, reqOpts ...client.RequestOption) client.Request {
|
||||
var opts client.RequestOptions
|
||||
for _, o := range reqOpts {
|
||||
o(&opts)
|
||||
}
|
||||
|
||||
return &httpRequest{
|
||||
service: service,
|
||||
method: method,
|
||||
request: request,
|
||||
contentType: contentType,
|
||||
opts: opts,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *httpRequest) ContentType() string {
|
||||
return h.contentType
|
||||
}
|
||||
|
||||
func (h *httpRequest) Service() string {
|
||||
return h.service
|
||||
}
|
||||
|
||||
func (h *httpRequest) Method() string {
|
||||
return h.method
|
||||
}
|
||||
|
||||
func (h *httpRequest) Request() interface{} {
|
||||
return h.request
|
||||
}
|
||||
|
||||
func (h *httpRequest) Stream() bool {
|
||||
return h.opts.Stream
|
||||
}
|
128
stream.go
Normal file
128
stream.go
Normal file
@ -0,0 +1,128 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
|
||||
"github.com/micro/go-micro/client"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// Implements the streamer interface
|
||||
type httpStream struct {
|
||||
sync.RWMutex
|
||||
address string
|
||||
codec Codec
|
||||
context context.Context
|
||||
header http.Header
|
||||
seq uint64
|
||||
closed chan bool
|
||||
err error
|
||||
conn net.Conn
|
||||
reader *bufio.Reader
|
||||
request client.Request
|
||||
}
|
||||
|
||||
var (
|
||||
errShutdown = errors.New("connection is shut down")
|
||||
)
|
||||
|
||||
func (h *httpStream) isClosed() bool {
|
||||
select {
|
||||
case <-h.closed:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (h *httpStream) Context() context.Context {
|
||||
return h.context
|
||||
}
|
||||
|
||||
func (h *httpStream) Request() client.Request {
|
||||
return h.request
|
||||
}
|
||||
|
||||
func (h *httpStream) Send(msg interface{}) error {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
if h.isClosed() {
|
||||
h.err = errShutdown
|
||||
return errShutdown
|
||||
}
|
||||
|
||||
b, err := h.codec.Marshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf := &buffer{bytes.NewBuffer(b)}
|
||||
defer buf.Close()
|
||||
|
||||
req := &http.Request{
|
||||
Method: "POST",
|
||||
URL: &url.URL{
|
||||
Scheme: "http",
|
||||
Host: h.address,
|
||||
Path: h.request.Method(),
|
||||
},
|
||||
Header: h.header,
|
||||
Body: buf,
|
||||
ContentLength: int64(len(b)),
|
||||
Host: h.address,
|
||||
}
|
||||
|
||||
return req.Write(h.conn)
|
||||
}
|
||||
|
||||
func (h *httpStream) Recv(msg interface{}) error {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
if h.isClosed() {
|
||||
h.err = errShutdown
|
||||
return errShutdown
|
||||
}
|
||||
|
||||
rsp, err := http.ReadResponse(h.reader, new(http.Request))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rsp.Body.Close()
|
||||
|
||||
b, err := ioutil.ReadAll(rsp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rsp.StatusCode != 200 {
|
||||
return errors.New(rsp.Status + ": " + string(b))
|
||||
}
|
||||
|
||||
return h.codec.Unmarshal(b, msg)
|
||||
}
|
||||
|
||||
func (h *httpStream) Error() error {
|
||||
h.RLock()
|
||||
defer h.RUnlock()
|
||||
return h.err
|
||||
}
|
||||
|
||||
func (h *httpStream) Close() error {
|
||||
select {
|
||||
case <-h.closed:
|
||||
return nil
|
||||
default:
|
||||
close(h.closed)
|
||||
return h.conn.Close()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user