mirror of https://github.com/fatedier/frp
parent
b600a07ec0
commit
93d86ca635
@ -0,0 +1,24 @@
|
|||||||
|
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Folders
|
||||||
|
_obj
|
||||||
|
_test
|
||||||
|
|
||||||
|
# Architecture specific extensions/prefixes
|
||||||
|
*.[568vq]
|
||||||
|
[568vq].out
|
||||||
|
|
||||||
|
*.cgo1.go
|
||||||
|
*.cgo2.c
|
||||||
|
_cgo_defun.c
|
||||||
|
_cgo_gotypes.go
|
||||||
|
_cgo_export.*
|
||||||
|
|
||||||
|
_testmain.go
|
||||||
|
|
||||||
|
*.exe
|
||||||
|
*.test
|
||||||
|
*.prof
|
@ -0,0 +1,12 @@
|
|||||||
|
language: go
|
||||||
|
go_import_path: github.com/pkg/errors
|
||||||
|
go:
|
||||||
|
- 1.4.x
|
||||||
|
- 1.5.x
|
||||||
|
- 1.6.x
|
||||||
|
- 1.7.x
|
||||||
|
- 1.8.x
|
||||||
|
- tip
|
||||||
|
|
||||||
|
script:
|
||||||
|
- go test -v ./...
|
@ -0,0 +1,23 @@
|
|||||||
|
Copyright (c) 2015, Dave Cheney <dave@cheney.net>
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
@ -0,0 +1,52 @@
|
|||||||
|
# errors [![Travis-CI](https://travis-ci.org/pkg/errors.svg)](https://travis-ci.org/pkg/errors) [![AppVeyor](https://ci.appveyor.com/api/projects/status/b98mptawhudj53ep/branch/master?svg=true)](https://ci.appveyor.com/project/davecheney/errors/branch/master) [![GoDoc](https://godoc.org/github.com/pkg/errors?status.svg)](http://godoc.org/github.com/pkg/errors) [![Report card](https://goreportcard.com/badge/github.com/pkg/errors)](https://goreportcard.com/report/github.com/pkg/errors)
|
||||||
|
|
||||||
|
Package errors provides simple error handling primitives.
|
||||||
|
|
||||||
|
`go get github.com/pkg/errors`
|
||||||
|
|
||||||
|
The traditional error handling idiom in Go is roughly akin to
|
||||||
|
```go
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
which applied recursively up the call stack results in error reports without context or debugging information. The errors package allows programmers to add context to the failure path in their code in a way that does not destroy the original value of the error.
|
||||||
|
|
||||||
|
## Adding context to an error
|
||||||
|
|
||||||
|
The errors.Wrap function returns a new error that adds context to the original error. For example
|
||||||
|
```go
|
||||||
|
_, err := ioutil.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "read failed")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
## Retrieving the cause of an error
|
||||||
|
|
||||||
|
Using `errors.Wrap` constructs a stack of errors, adding context to the preceding error. Depending on the nature of the error it may be necessary to reverse the operation of errors.Wrap to retrieve the original error for inspection. Any error value which implements this interface can be inspected by `errors.Cause`.
|
||||||
|
```go
|
||||||
|
type causer interface {
|
||||||
|
Cause() error
|
||||||
|
}
|
||||||
|
```
|
||||||
|
`errors.Cause` will recursively retrieve the topmost error which does not implement `causer`, which is assumed to be the original cause. For example:
|
||||||
|
```go
|
||||||
|
switch err := errors.Cause(err).(type) {
|
||||||
|
case *MyError:
|
||||||
|
// handle specifically
|
||||||
|
default:
|
||||||
|
// unknown error
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
[Read the package documentation for more information](https://godoc.org/github.com/pkg/errors).
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
We welcome pull requests, bug fixes and issue reports. With that said, the bar for adding new symbols to this package is intentionally set high.
|
||||||
|
|
||||||
|
Before proposing a change, please discuss your change by raising an issue.
|
||||||
|
|
||||||
|
## Licence
|
||||||
|
|
||||||
|
BSD-2-Clause
|
@ -0,0 +1,32 @@
|
|||||||
|
version: build-{build}.{branch}
|
||||||
|
|
||||||
|
clone_folder: C:\gopath\src\github.com\pkg\errors
|
||||||
|
shallow_clone: true # for startup speed
|
||||||
|
|
||||||
|
environment:
|
||||||
|
GOPATH: C:\gopath
|
||||||
|
|
||||||
|
platform:
|
||||||
|
- x64
|
||||||
|
|
||||||
|
# http://www.appveyor.com/docs/installed-software
|
||||||
|
install:
|
||||||
|
# some helpful output for debugging builds
|
||||||
|
- go version
|
||||||
|
- go env
|
||||||
|
# pre-installed MinGW at C:\MinGW is 32bit only
|
||||||
|
# but MSYS2 at C:\msys64 has mingw64
|
||||||
|
- set PATH=C:\msys64\mingw64\bin;%PATH%
|
||||||
|
- gcc --version
|
||||||
|
- g++ --version
|
||||||
|
|
||||||
|
build_script:
|
||||||
|
- go install -v ./...
|
||||||
|
|
||||||
|
test_script:
|
||||||
|
- set PATH=C:\gopath\bin;%PATH%
|
||||||
|
- go test -v ./...
|
||||||
|
|
||||||
|
#artifacts:
|
||||||
|
# - path: '%GOPATH%\bin\*.exe'
|
||||||
|
deploy: off
|
@ -0,0 +1,269 @@
|
|||||||
|
// Package errors provides simple error handling primitives.
|
||||||
|
//
|
||||||
|
// The traditional error handling idiom in Go is roughly akin to
|
||||||
|
//
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// which applied recursively up the call stack results in error reports
|
||||||
|
// without context or debugging information. The errors package allows
|
||||||
|
// programmers to add context to the failure path in their code in a way
|
||||||
|
// that does not destroy the original value of the error.
|
||||||
|
//
|
||||||
|
// Adding context to an error
|
||||||
|
//
|
||||||
|
// The errors.Wrap function returns a new error that adds context to the
|
||||||
|
// original error by recording a stack trace at the point Wrap is called,
|
||||||
|
// and the supplied message. For example
|
||||||
|
//
|
||||||
|
// _, err := ioutil.ReadAll(r)
|
||||||
|
// if err != nil {
|
||||||
|
// return errors.Wrap(err, "read failed")
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// If additional control is required the errors.WithStack and errors.WithMessage
|
||||||
|
// functions destructure errors.Wrap into its component operations of annotating
|
||||||
|
// an error with a stack trace and an a message, respectively.
|
||||||
|
//
|
||||||
|
// Retrieving the cause of an error
|
||||||
|
//
|
||||||
|
// Using errors.Wrap constructs a stack of errors, adding context to the
|
||||||
|
// preceding error. Depending on the nature of the error it may be necessary
|
||||||
|
// to reverse the operation of errors.Wrap to retrieve the original error
|
||||||
|
// for inspection. Any error value which implements this interface
|
||||||
|
//
|
||||||
|
// type causer interface {
|
||||||
|
// Cause() error
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// can be inspected by errors.Cause. errors.Cause will recursively retrieve
|
||||||
|
// the topmost error which does not implement causer, which is assumed to be
|
||||||
|
// the original cause. For example:
|
||||||
|
//
|
||||||
|
// switch err := errors.Cause(err).(type) {
|
||||||
|
// case *MyError:
|
||||||
|
// // handle specifically
|
||||||
|
// default:
|
||||||
|
// // unknown error
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// causer interface is not exported by this package, but is considered a part
|
||||||
|
// of stable public API.
|
||||||
|
//
|
||||||
|
// Formatted printing of errors
|
||||||
|
//
|
||||||
|
// All error values returned from this package implement fmt.Formatter and can
|
||||||
|
// be formatted by the fmt package. The following verbs are supported
|
||||||
|
//
|
||||||
|
// %s print the error. If the error has a Cause it will be
|
||||||
|
// printed recursively
|
||||||
|
// %v see %s
|
||||||
|
// %+v extended format. Each Frame of the error's StackTrace will
|
||||||
|
// be printed in detail.
|
||||||
|
//
|
||||||
|
// Retrieving the stack trace of an error or wrapper
|
||||||
|
//
|
||||||
|
// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are
|
||||||
|
// invoked. This information can be retrieved with the following interface.
|
||||||
|
//
|
||||||
|
// type stackTracer interface {
|
||||||
|
// StackTrace() errors.StackTrace
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Where errors.StackTrace is defined as
|
||||||
|
//
|
||||||
|
// type StackTrace []Frame
|
||||||
|
//
|
||||||
|
// The Frame type represents a call site in the stack trace. Frame supports
|
||||||
|
// the fmt.Formatter interface that can be used for printing information about
|
||||||
|
// the stack trace of this error. For example:
|
||||||
|
//
|
||||||
|
// if err, ok := err.(stackTracer); ok {
|
||||||
|
// for _, f := range err.StackTrace() {
|
||||||
|
// fmt.Printf("%+s:%d", f)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// stackTracer interface is not exported by this package, but is considered a part
|
||||||
|
// of stable public API.
|
||||||
|
//
|
||||||
|
// See the documentation for Frame.Format for more details.
|
||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// New returns an error with the supplied message.
|
||||||
|
// New also records the stack trace at the point it was called.
|
||||||
|
func New(message string) error {
|
||||||
|
return &fundamental{
|
||||||
|
msg: message,
|
||||||
|
stack: callers(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errorf formats according to a format specifier and returns the string
|
||||||
|
// as a value that satisfies error.
|
||||||
|
// Errorf also records the stack trace at the point it was called.
|
||||||
|
func Errorf(format string, args ...interface{}) error {
|
||||||
|
return &fundamental{
|
||||||
|
msg: fmt.Sprintf(format, args...),
|
||||||
|
stack: callers(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fundamental is an error that has a message and a stack, but no caller.
|
||||||
|
type fundamental struct {
|
||||||
|
msg string
|
||||||
|
*stack
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fundamental) Error() string { return f.msg }
|
||||||
|
|
||||||
|
func (f *fundamental) Format(s fmt.State, verb rune) {
|
||||||
|
switch verb {
|
||||||
|
case 'v':
|
||||||
|
if s.Flag('+') {
|
||||||
|
io.WriteString(s, f.msg)
|
||||||
|
f.stack.Format(s, verb)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
case 's':
|
||||||
|
io.WriteString(s, f.msg)
|
||||||
|
case 'q':
|
||||||
|
fmt.Fprintf(s, "%q", f.msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithStack annotates err with a stack trace at the point WithStack was called.
|
||||||
|
// If err is nil, WithStack returns nil.
|
||||||
|
func WithStack(err error) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &withStack{
|
||||||
|
err,
|
||||||
|
callers(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type withStack struct {
|
||||||
|
error
|
||||||
|
*stack
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *withStack) Cause() error { return w.error }
|
||||||
|
|
||||||
|
func (w *withStack) Format(s fmt.State, verb rune) {
|
||||||
|
switch verb {
|
||||||
|
case 'v':
|
||||||
|
if s.Flag('+') {
|
||||||
|
fmt.Fprintf(s, "%+v", w.Cause())
|
||||||
|
w.stack.Format(s, verb)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
case 's':
|
||||||
|
io.WriteString(s, w.Error())
|
||||||
|
case 'q':
|
||||||
|
fmt.Fprintf(s, "%q", w.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap returns an error annotating err with a stack trace
|
||||||
|
// at the point Wrap is called, and the supplied message.
|
||||||
|
// If err is nil, Wrap returns nil.
|
||||||
|
func Wrap(err error, message string) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err = &withMessage{
|
||||||
|
cause: err,
|
||||||
|
msg: message,
|
||||||
|
}
|
||||||
|
return &withStack{
|
||||||
|
err,
|
||||||
|
callers(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapf returns an error annotating err with a stack trace
|
||||||
|
// at the point Wrapf is call, and the format specifier.
|
||||||
|
// If err is nil, Wrapf returns nil.
|
||||||
|
func Wrapf(err error, format string, args ...interface{}) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err = &withMessage{
|
||||||
|
cause: err,
|
||||||
|
msg: fmt.Sprintf(format, args...),
|
||||||
|
}
|
||||||
|
return &withStack{
|
||||||
|
err,
|
||||||
|
callers(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMessage annotates err with a new message.
|
||||||
|
// If err is nil, WithMessage returns nil.
|
||||||
|
func WithMessage(err error, message string) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &withMessage{
|
||||||
|
cause: err,
|
||||||
|
msg: message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type withMessage struct {
|
||||||
|
cause error
|
||||||
|
msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() }
|
||||||
|
func (w *withMessage) Cause() error { return w.cause }
|
||||||
|
|
||||||
|
func (w *withMessage) Format(s fmt.State, verb rune) {
|
||||||
|
switch verb {
|
||||||
|
case 'v':
|
||||||
|
if s.Flag('+') {
|
||||||
|
fmt.Fprintf(s, "%+v\n", w.Cause())
|
||||||
|
io.WriteString(s, w.msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
case 's', 'q':
|
||||||
|
io.WriteString(s, w.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cause returns the underlying cause of the error, if possible.
|
||||||
|
// An error value has a cause if it implements the following
|
||||||
|
// interface:
|
||||||
|
//
|
||||||
|
// type causer interface {
|
||||||
|
// Cause() error
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// If the error does not implement Cause, the original error will
|
||||||
|
// be returned. If the error is nil, nil will be returned without further
|
||||||
|
// investigation.
|
||||||
|
func Cause(err error) error {
|
||||||
|
type causer interface {
|
||||||
|
Cause() error
|
||||||
|
}
|
||||||
|
|
||||||
|
for err != nil {
|
||||||
|
cause, ok := err.(causer)
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
err = cause.Cause()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
@ -0,0 +1,186 @@
|
|||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"path"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Frame represents a program counter inside a stack frame.
|
||||||
|
type Frame uintptr
|
||||||
|
|
||||||
|
// pc returns the program counter for this frame;
|
||||||
|
// multiple frames may have the same PC value.
|
||||||
|
func (f Frame) pc() uintptr { return uintptr(f) - 1 }
|
||||||
|
|
||||||
|
// file returns the full path to the file that contains the
|
||||||
|
// function for this Frame's pc.
|
||||||
|
func (f Frame) file() string {
|
||||||
|
fn := runtime.FuncForPC(f.pc())
|
||||||
|
if fn == nil {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
file, _ := fn.FileLine(f.pc())
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
// line returns the line number of source code of the
|
||||||
|
// function for this Frame's pc.
|
||||||
|
func (f Frame) line() int {
|
||||||
|
fn := runtime.FuncForPC(f.pc())
|
||||||
|
if fn == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
_, line := fn.FileLine(f.pc())
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format formats the frame according to the fmt.Formatter interface.
|
||||||
|
//
|
||||||
|
// %s source file
|
||||||
|
// %d source line
|
||||||
|
// %n function name
|
||||||
|
// %v equivalent to %s:%d
|
||||||
|
//
|
||||||
|
// Format accepts flags that alter the printing of some verbs, as follows:
|
||||||
|
//
|
||||||
|
// %+s path of source file relative to the compile time GOPATH
|
||||||
|
// %+v equivalent to %+s:%d
|
||||||
|
func (f Frame) Format(s fmt.State, verb rune) {
|
||||||
|
switch verb {
|
||||||
|
case 's':
|
||||||
|
switch {
|
||||||
|
case s.Flag('+'):
|
||||||
|
pc := f.pc()
|
||||||
|
fn := runtime.FuncForPC(pc)
|
||||||
|
if fn == nil {
|
||||||
|
io.WriteString(s, "unknown")
|
||||||
|
} else {
|
||||||
|
file, _ := fn.FileLine(pc)
|
||||||
|
fmt.Fprintf(s, "%s\n\t%s", fn.Name(), file)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
io.WriteString(s, path.Base(f.file()))
|
||||||
|
}
|
||||||
|
case 'd':
|
||||||
|
fmt.Fprintf(s, "%d", f.line())
|
||||||
|
case 'n':
|
||||||
|
name := runtime.FuncForPC(f.pc()).Name()
|
||||||
|
io.WriteString(s, funcname(name))
|
||||||
|
case 'v':
|
||||||
|
f.Format(s, 's')
|
||||||
|
io.WriteString(s, ":")
|
||||||
|
f.Format(s, 'd')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StackTrace is stack of Frames from innermost (newest) to outermost (oldest).
|
||||||
|
type StackTrace []Frame
|
||||||
|
|
||||||
|
// Format formats the stack of Frames according to the fmt.Formatter interface.
|
||||||
|
//
|
||||||
|
// %s lists source files for each Frame in the stack
|
||||||
|
// %v lists the source file and line number for each Frame in the stack
|
||||||
|
//
|
||||||
|
// Format accepts flags that alter the printing of some verbs, as follows:
|
||||||
|
//
|
||||||
|
// %+v Prints filename, function, and line number for each Frame in the stack.
|
||||||
|
func (st StackTrace) Format(s fmt.State, verb rune) {
|
||||||
|
switch verb {
|
||||||
|
case 'v':
|
||||||
|
switch {
|
||||||
|
case s.Flag('+'):
|
||||||
|
for _, f := range st {
|
||||||
|
fmt.Fprintf(s, "\n%+v", f)
|
||||||
|
}
|
||||||
|
case s.Flag('#'):
|
||||||
|
fmt.Fprintf(s, "%#v", []Frame(st))
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(s, "%v", []Frame(st))
|
||||||
|
}
|
||||||
|
case 's':
|
||||||
|
fmt.Fprintf(s, "%s", []Frame(st))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stack represents a stack of program counters.
|
||||||
|
type stack []uintptr
|
||||||
|
|
||||||
|
func (s *stack) Format(st fmt.State, verb rune) {
|
||||||
|
switch verb {
|
||||||
|
case 'v':
|
||||||
|
switch {
|
||||||
|
case st.Flag('+'):
|
||||||
|
for _, pc := range *s {
|
||||||
|
f := Frame(pc)
|
||||||
|
fmt.Fprintf(st, "\n%+v", f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stack) StackTrace() StackTrace {
|
||||||
|
f := make([]Frame, len(*s))
|
||||||
|
for i := 0; i < len(f); i++ {
|
||||||
|
f[i] = Frame((*s)[i])
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func callers() *stack {
|
||||||
|
const depth = 32
|
||||||
|
var pcs [depth]uintptr
|
||||||
|
n := runtime.Callers(3, pcs[:])
|
||||||
|
var st stack = pcs[0:n]
|
||||||
|
return &st
|
||||||
|
}
|
||||||
|
|
||||||
|
// funcname removes the path prefix component of a function's name reported by func.Name().
|
||||||
|
func funcname(name string) string {
|
||||||
|
i := strings.LastIndex(name, "/")
|
||||||
|
name = name[i+1:]
|
||||||
|
i = strings.Index(name, ".")
|
||||||
|
return name[i+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimGOPATH(name, file string) string {
|
||||||
|
// Here we want to get the source file path relative to the compile time
|
||||||
|
// GOPATH. As of Go 1.6.x there is no direct way to know the compiled
|
||||||
|
// GOPATH at runtime, but we can infer the number of path segments in the
|
||||||
|
// GOPATH. We note that fn.Name() returns the function name qualified by
|
||||||
|
// the import path, which does not include the GOPATH. Thus we can trim
|
||||||
|
// segments from the beginning of the file path until the number of path
|
||||||
|
// separators remaining is one more than the number of path separators in
|
||||||
|
// the function name. For example, given:
|
||||||
|
//
|
||||||
|
// GOPATH /home/user
|
||||||
|
// file /home/user/src/pkg/sub/file.go
|
||||||
|
// fn.Name() pkg/sub.Type.Method
|
||||||
|
//
|
||||||
|
// We want to produce:
|
||||||
|
//
|
||||||
|
// pkg/sub/file.go
|
||||||
|
//
|
||||||
|
// From this we can easily see that fn.Name() has one less path separator
|
||||||
|
// than our desired output. We count separators from the end of the file
|
||||||
|
// path until it finds two more than in the function name and then move
|
||||||
|
// one character forward to preserve the initial path segment without a
|
||||||
|
// leading separator.
|
||||||
|
const sep = "/"
|
||||||
|
goal := strings.Count(name, sep) + 2
|
||||||
|
i := len(file)
|
||||||
|
for n := 0; n < goal; n++ {
|
||||||
|
i = strings.LastIndex(file[:i], sep)
|
||||||
|
if i == -1 {
|
||||||
|
// not enough separators found, set i so that the slice expression
|
||||||
|
// below leaves file unmodified
|
||||||
|
i = -len(sep)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// get back to 0 or trim the leading separator
|
||||||
|
file = file[i+len(sep):]
|
||||||
|
return file
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Folders
|
||||||
|
_obj
|
||||||
|
_test
|
||||||
|
|
||||||
|
# Architecture specific extensions/prefixes
|
||||||
|
*.[568vq]
|
||||||
|
[568vq].out
|
||||||
|
|
||||||
|
*.cgo1.go
|
||||||
|
*.cgo2.c
|
||||||
|
_cgo_defun.c
|
||||||
|
_cgo_gotypes.go
|
||||||
|
_cgo_export.*
|
||||||
|
|
||||||
|
_testmain.go
|
||||||
|
|
||||||
|
*.exe
|
||||||
|
*.test
|
||||||
|
*.prof
|
@ -0,0 +1,15 @@
|
|||||||
|
language: go
|
||||||
|
go:
|
||||||
|
- tip
|
||||||
|
|
||||||
|
before_install:
|
||||||
|
- go get -t -v ./...
|
||||||
|
|
||||||
|
install:
|
||||||
|
- go get github.com/xtaci/smux
|
||||||
|
|
||||||
|
script:
|
||||||
|
- go test -coverprofile=coverage.txt -covermode=atomic -bench .
|
||||||
|
|
||||||
|
after_success:
|
||||||
|
- bash <(curl -s https://codecov.io/bash)
|
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2016-2017 Daniel Fu
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
@ -0,0 +1,99 @@
|
|||||||
|
<img src="smux.png" alt="smux" height="35px" />
|
||||||
|
|
||||||
|
[![GoDoc][1]][2] [![MIT licensed][3]][4] [![Build Status][5]][6] [![Go Report Card][7]][8] [![Coverage Statusd][9]][10]
|
||||||
|
|
||||||
|
<img src="mux.jpg" alt="smux" height="120px" />
|
||||||
|
|
||||||
|
[1]: https://godoc.org/github.com/xtaci/smux?status.svg
|
||||||
|
[2]: https://godoc.org/github.com/xtaci/smux
|
||||||
|
[3]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||||
|
[4]: LICENSE
|
||||||
|
[5]: https://travis-ci.org/xtaci/smux.svg?branch=master
|
||||||
|
[6]: https://travis-ci.org/xtaci/smux
|
||||||
|
[7]: https://goreportcard.com/badge/github.com/xtaci/smux
|
||||||
|
[8]: https://goreportcard.com/report/github.com/xtaci/smux
|
||||||
|
[9]: https://codecov.io/gh/xtaci/smux/branch/master/graph/badge.svg
|
||||||
|
[10]: https://codecov.io/gh/xtaci/smux
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
Smux ( **S**imple **MU**ltiple**X**ing) is a multiplexing library for Golang. It relies on an underlying connection to provide reliability and ordering, such as TCP or [KCP](https://github.com/xtaci/kcp-go), and provides stream-oriented multiplexing. The original intention of this library is to power the connection management for [kcp-go](https://github.com/xtaci/kcp-go).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
1. Tiny, less than 600 LOC.
|
||||||
|
2. ***Token bucket*** controlled receiving, which provides smoother bandwidth graph(see picture below).
|
||||||
|
3. Session-wide receive buffer, shared among streams, tightly controlled overall memory usage.
|
||||||
|
4. Minimized header(8Bytes), maximized payload.
|
||||||
|
5. Well-tested on millions of devices in [kcptun](https://github.com/xtaci/kcptun).
|
||||||
|
|
||||||
|
![smooth bandwidth curve](curve.jpg)
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
For complete documentation, see the associated [Godoc](https://godoc.org/github.com/xtaci/smux).
|
||||||
|
|
||||||
|
## Specification
|
||||||
|
|
||||||
|
```
|
||||||
|
VERSION(1B) | CMD(1B) | LENGTH(2B) | STREAMID(4B) | DATA(LENGTH)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
The API of smux are mostly taken from [yamux](https://github.com/hashicorp/yamux)
|
||||||
|
|
||||||
|
```go
|
||||||
|
|
||||||
|
func client() {
|
||||||
|
// Get a TCP connection
|
||||||
|
conn, err := net.Dial(...)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup client side of smux
|
||||||
|
session, err := smux.Client(conn, nil)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open a new stream
|
||||||
|
stream, err := session.OpenStream()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stream implements io.ReadWriteCloser
|
||||||
|
stream.Write([]byte("ping"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func server() {
|
||||||
|
// Accept a TCP connection
|
||||||
|
conn, err := listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup server side of smux
|
||||||
|
session, err := smux.Server(conn, nil)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accept a stream
|
||||||
|
stream, err := session.AcceptStream()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for a message
|
||||||
|
buf := make([]byte, 4)
|
||||||
|
stream.Read(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Stable
|
After Width: | Height: | Size: 104 KiB |
@ -0,0 +1,60 @@
|
|||||||
|
package smux
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
version = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
const ( // cmds
|
||||||
|
cmdSYN byte = iota // stream open
|
||||||
|
cmdFIN // stream close, a.k.a EOF mark
|
||||||
|
cmdPSH // data push
|
||||||
|
cmdNOP // no operation
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
sizeOfVer = 1
|
||||||
|
sizeOfCmd = 1
|
||||||
|
sizeOfLength = 2
|
||||||
|
sizeOfSid = 4
|
||||||
|
headerSize = sizeOfVer + sizeOfCmd + sizeOfSid + sizeOfLength
|
||||||
|
)
|
||||||
|
|
||||||
|
// Frame defines a packet from or to be multiplexed into a single connection
|
||||||
|
type Frame struct {
|
||||||
|
ver byte
|
||||||
|
cmd byte
|
||||||
|
sid uint32
|
||||||
|
data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFrame(cmd byte, sid uint32) Frame {
|
||||||
|
return Frame{ver: version, cmd: cmd, sid: sid}
|
||||||
|
}
|
||||||
|
|
||||||
|
type rawHeader []byte
|
||||||
|
|
||||||
|
func (h rawHeader) Version() byte {
|
||||||
|
return h[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h rawHeader) Cmd() byte {
|
||||||
|
return h[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h rawHeader) Length() uint16 {
|
||||||
|
return binary.LittleEndian.Uint16(h[2:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h rawHeader) StreamID() uint32 {
|
||||||
|
return binary.LittleEndian.Uint32(h[4:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h rawHeader) String() string {
|
||||||
|
return fmt.Sprintf("Version:%d Cmd:%d StreamID:%d Length:%d",
|
||||||
|
h.Version(), h.Cmd(), h.StreamID(), h.Length())
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
package smux
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config is used to tune the Smux session
|
||||||
|
type Config struct {
|
||||||
|
// KeepAliveInterval is how often to send a NOP command to the remote
|
||||||
|
KeepAliveInterval time.Duration
|
||||||
|
|
||||||
|
// KeepAliveTimeout is how long the session
|
||||||
|
// will be closed if no data has arrived
|
||||||
|
KeepAliveTimeout time.Duration
|
||||||
|
|
||||||
|
// MaxFrameSize is used to control the maximum
|
||||||
|
// frame size to sent to the remote
|
||||||
|
MaxFrameSize int
|
||||||
|
|
||||||
|
// MaxReceiveBuffer is used to control the maximum
|
||||||
|
// number of data in the buffer pool
|
||||||
|
MaxReceiveBuffer int
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultConfig is used to return a default configuration
|
||||||
|
func DefaultConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
KeepAliveInterval: 10 * time.Second,
|
||||||
|
KeepAliveTimeout: 30 * time.Second,
|
||||||
|
MaxFrameSize: 4096,
|
||||||
|
MaxReceiveBuffer: 4194304,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyConfig is used to verify the sanity of configuration
|
||||||
|
func VerifyConfig(config *Config) error {
|
||||||
|
if config.KeepAliveInterval == 0 {
|
||||||
|
return errors.New("keep-alive interval must be positive")
|
||||||
|
}
|
||||||
|
if config.KeepAliveTimeout < config.KeepAliveInterval {
|
||||||
|
return fmt.Errorf("keep-alive timeout must be larger than keep-alive interval")
|
||||||
|
}
|
||||||
|
if config.MaxFrameSize <= 0 {
|
||||||
|
return errors.New("max frame size must be positive")
|
||||||
|
}
|
||||||
|
if config.MaxFrameSize > 65535 {
|
||||||
|
return errors.New("max frame size must not be larger than 65535")
|
||||||
|
}
|
||||||
|
if config.MaxReceiveBuffer <= 0 {
|
||||||
|
return errors.New("max receive buffer must be positive")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server is used to initialize a new server-side connection.
|
||||||
|
func Server(conn io.ReadWriteCloser, config *Config) (*Session, error) {
|
||||||
|
if config == nil {
|
||||||
|
config = DefaultConfig()
|
||||||
|
}
|
||||||
|
if err := VerifyConfig(config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return newSession(config, conn, false), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client is used to initialize a new client-side connection.
|
||||||
|
func Client(conn io.ReadWriteCloser, config *Config) (*Session, error) {
|
||||||
|
if config == nil {
|
||||||
|
config = DefaultConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := VerifyConfig(config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return newSession(config, conn, true), nil
|
||||||
|
}
|
After Width: | Height: | Size: 6.1 KiB |
@ -0,0 +1,353 @@
|
|||||||
|
package smux
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultAcceptBacklog = 1024
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
errBrokenPipe = "broken pipe"
|
||||||
|
errInvalidProtocol = "invalid protocol version"
|
||||||
|
errGoAway = "stream id overflows, should start a new connection"
|
||||||
|
)
|
||||||
|
|
||||||
|
type writeRequest struct {
|
||||||
|
frame Frame
|
||||||
|
result chan writeResult
|
||||||
|
}
|
||||||
|
|
||||||
|
type writeResult struct {
|
||||||
|
n int
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session defines a multiplexed connection for streams
|
||||||
|
type Session struct {
|
||||||
|
conn io.ReadWriteCloser
|
||||||
|
|
||||||
|
config *Config
|
||||||
|
nextStreamID uint32 // next stream identifier
|
||||||
|
nextStreamIDLock sync.Mutex
|
||||||
|
|
||||||
|
bucket int32 // token bucket
|
||||||
|
bucketNotify chan struct{} // used for waiting for tokens
|
||||||
|
|
||||||
|
streams map[uint32]*Stream // all streams in this session
|
||||||
|
streamLock sync.Mutex // locks streams
|
||||||
|
|
||||||
|
die chan struct{} // flag session has died
|
||||||
|
dieLock sync.Mutex
|
||||||
|
chAccepts chan *Stream
|
||||||
|
|
||||||
|
dataReady int32 // flag data has arrived
|
||||||
|
|
||||||
|
goAway int32 // flag id exhausted
|
||||||
|
|
||||||
|
deadline atomic.Value
|
||||||
|
|
||||||
|
writes chan writeRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSession(config *Config, conn io.ReadWriteCloser, client bool) *Session {
|
||||||
|
s := new(Session)
|
||||||
|
s.die = make(chan struct{})
|
||||||
|
s.conn = conn
|
||||||
|
s.config = config
|
||||||
|
s.streams = make(map[uint32]*Stream)
|
||||||
|
s.chAccepts = make(chan *Stream, defaultAcceptBacklog)
|
||||||
|
s.bucket = int32(config.MaxReceiveBuffer)
|
||||||
|
s.bucketNotify = make(chan struct{}, 1)
|
||||||
|
s.writes = make(chan writeRequest)
|
||||||
|
|
||||||
|
if client {
|
||||||
|
s.nextStreamID = 1
|
||||||
|
} else {
|
||||||
|
s.nextStreamID = 0
|
||||||
|
}
|
||||||
|
go s.recvLoop()
|
||||||
|
go s.sendLoop()
|
||||||
|
go s.keepalive()
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenStream is used to create a new stream
|
||||||
|
func (s *Session) OpenStream() (*Stream, error) {
|
||||||
|
if s.IsClosed() {
|
||||||
|
return nil, errors.New(errBrokenPipe)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate stream id
|
||||||
|
s.nextStreamIDLock.Lock()
|
||||||
|
if s.goAway > 0 {
|
||||||
|
s.nextStreamIDLock.Unlock()
|
||||||
|
return nil, errors.New(errGoAway)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.nextStreamID += 2
|
||||||
|
sid := s.nextStreamID
|
||||||
|
if sid == sid%2 { // stream-id overflows
|
||||||
|
s.goAway = 1
|
||||||
|
s.nextStreamIDLock.Unlock()
|
||||||
|
return nil, errors.New(errGoAway)
|
||||||
|
}
|
||||||
|
s.nextStreamIDLock.Unlock()
|
||||||
|
|
||||||
|
stream := newStream(sid, s.config.MaxFrameSize, s)
|
||||||
|
|
||||||
|
if _, err := s.writeFrame(newFrame(cmdSYN, sid)); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "writeFrame")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.streamLock.Lock()
|
||||||
|
s.streams[sid] = stream
|
||||||
|
s.streamLock.Unlock()
|
||||||
|
return stream, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AcceptStream is used to block until the next available stream
|
||||||
|
// is ready to be accepted.
|
||||||
|
func (s *Session) AcceptStream() (*Stream, error) {
|
||||||
|
var deadline <-chan time.Time
|
||||||
|
if d, ok := s.deadline.Load().(time.Time); ok && !d.IsZero() {
|
||||||
|
timer := time.NewTimer(d.Sub(time.Now()))
|
||||||
|
defer timer.Stop()
|
||||||
|
deadline = timer.C
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case stream := <-s.chAccepts:
|
||||||
|
return stream, nil
|
||||||
|
case <-deadline:
|
||||||
|
return nil, errTimeout
|
||||||
|
case <-s.die:
|
||||||
|
return nil, errors.New(errBrokenPipe)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close is used to close the session and all streams.
|
||||||
|
func (s *Session) Close() (err error) {
|
||||||
|
s.dieLock.Lock()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-s.die:
|
||||||
|
s.dieLock.Unlock()
|
||||||
|
return errors.New(errBrokenPipe)
|
||||||
|
default:
|
||||||
|
close(s.die)
|
||||||
|
s.dieLock.Unlock()
|
||||||
|
s.streamLock.Lock()
|
||||||
|
for k := range s.streams {
|
||||||
|
s.streams[k].sessionClose()
|
||||||
|
}
|
||||||
|
s.streamLock.Unlock()
|
||||||
|
s.notifyBucket()
|
||||||
|
return s.conn.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// notifyBucket notifies recvLoop that bucket is available
|
||||||
|
func (s *Session) notifyBucket() {
|
||||||
|
select {
|
||||||
|
case s.bucketNotify <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsClosed does a safe check to see if we have shutdown
|
||||||
|
func (s *Session) IsClosed() bool {
|
||||||
|
select {
|
||||||
|
case <-s.die:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NumStreams returns the number of currently open streams
|
||||||
|
func (s *Session) NumStreams() int {
|
||||||
|
if s.IsClosed() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
s.streamLock.Lock()
|
||||||
|
defer s.streamLock.Unlock()
|
||||||
|
return len(s.streams)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDeadline sets a deadline used by Accept* calls.
|
||||||
|
// A zero time value disables the deadline.
|
||||||
|
func (s *Session) SetDeadline(t time.Time) error {
|
||||||
|
s.deadline.Store(t)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// notify the session that a stream has closed
|
||||||
|
func (s *Session) streamClosed(sid uint32) {
|
||||||
|
s.streamLock.Lock()
|
||||||
|
if n := s.streams[sid].recycleTokens(); n > 0 { // return remaining tokens to the bucket
|
||||||
|
if atomic.AddInt32(&s.bucket, int32(n)) > 0 {
|
||||||
|
s.notifyBucket()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete(s.streams, sid)
|
||||||
|
s.streamLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// returnTokens is called by stream to return token after read
|
||||||
|
func (s *Session) returnTokens(n int) {
|
||||||
|
if atomic.AddInt32(&s.bucket, int32(n)) > 0 {
|
||||||
|
s.notifyBucket()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// session read a frame from underlying connection
|
||||||
|
// it's data is pointed to the input buffer
|
||||||
|
func (s *Session) readFrame(buffer []byte) (f Frame, err error) {
|
||||||
|
if _, err := io.ReadFull(s.conn, buffer[:headerSize]); err != nil {
|
||||||
|
return f, errors.Wrap(err, "readFrame")
|
||||||
|
}
|
||||||
|
|
||||||
|
dec := rawHeader(buffer)
|
||||||
|
if dec.Version() != version {
|
||||||
|
return f, errors.New(errInvalidProtocol)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.ver = dec.Version()
|
||||||
|
f.cmd = dec.Cmd()
|
||||||
|
f.sid = dec.StreamID()
|
||||||
|
if length := dec.Length(); length > 0 {
|
||||||
|
if _, err := io.ReadFull(s.conn, buffer[headerSize:headerSize+length]); err != nil {
|
||||||
|
return f, errors.Wrap(err, "readFrame")
|
||||||
|
}
|
||||||
|
f.data = buffer[headerSize : headerSize+length]
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// recvLoop keeps on reading from underlying connection if tokens are available
|
||||||
|
func (s *Session) recvLoop() {
|
||||||
|
buffer := make([]byte, (1<<16)+headerSize)
|
||||||
|
for {
|
||||||
|
for atomic.LoadInt32(&s.bucket) <= 0 && !s.IsClosed() {
|
||||||
|
<-s.bucketNotify
|
||||||
|
}
|
||||||
|
|
||||||
|
if f, err := s.readFrame(buffer); err == nil {
|
||||||
|
atomic.StoreInt32(&s.dataReady, 1)
|
||||||
|
|
||||||
|
switch f.cmd {
|
||||||
|
case cmdNOP:
|
||||||
|
case cmdSYN:
|
||||||
|
s.streamLock.Lock()
|
||||||
|
if _, ok := s.streams[f.sid]; !ok {
|
||||||
|
stream := newStream(f.sid, s.config.MaxFrameSize, s)
|
||||||
|
s.streams[f.sid] = stream
|
||||||
|
select {
|
||||||
|
case s.chAccepts <- stream:
|
||||||
|
case <-s.die:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.streamLock.Unlock()
|
||||||
|
case cmdFIN:
|
||||||
|
s.streamLock.Lock()
|
||||||
|
if stream, ok := s.streams[f.sid]; ok {
|
||||||
|
stream.markRST()
|
||||||
|
stream.notifyReadEvent()
|
||||||
|
}
|
||||||
|
s.streamLock.Unlock()
|
||||||
|
case cmdPSH:
|
||||||
|
s.streamLock.Lock()
|
||||||
|
if stream, ok := s.streams[f.sid]; ok {
|
||||||
|
atomic.AddInt32(&s.bucket, -int32(len(f.data)))
|
||||||
|
stream.pushBytes(f.data)
|
||||||
|
stream.notifyReadEvent()
|
||||||
|
}
|
||||||
|
s.streamLock.Unlock()
|
||||||
|
default:
|
||||||
|
s.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) keepalive() {
|
||||||
|
tickerPing := time.NewTicker(s.config.KeepAliveInterval)
|
||||||
|
tickerTimeout := time.NewTicker(s.config.KeepAliveTimeout)
|
||||||
|
defer tickerPing.Stop()
|
||||||
|
defer tickerTimeout.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-tickerPing.C:
|
||||||
|
s.writeFrame(newFrame(cmdNOP, 0))
|
||||||
|
s.notifyBucket() // force a signal to the recvLoop
|
||||||
|
case <-tickerTimeout.C:
|
||||||
|
if !atomic.CompareAndSwapInt32(&s.dataReady, 1, 0) {
|
||||||
|
s.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-s.die:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) sendLoop() {
|
||||||
|
buf := make([]byte, (1<<16)+headerSize)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-s.die:
|
||||||
|
return
|
||||||
|
case request, ok := <-s.writes:
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
buf[0] = request.frame.ver
|
||||||
|
buf[1] = request.frame.cmd
|
||||||
|
binary.LittleEndian.PutUint16(buf[2:], uint16(len(request.frame.data)))
|
||||||
|
binary.LittleEndian.PutUint32(buf[4:], request.frame.sid)
|
||||||
|
copy(buf[headerSize:], request.frame.data)
|
||||||
|
n, err := s.conn.Write(buf[:headerSize+len(request.frame.data)])
|
||||||
|
|
||||||
|
n -= headerSize
|
||||||
|
if n < 0 {
|
||||||
|
n = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
result := writeResult{
|
||||||
|
n: n,
|
||||||
|
err: err,
|
||||||
|
}
|
||||||
|
|
||||||
|
request.result <- result
|
||||||
|
close(request.result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeFrame writes the frame to the underlying connection
|
||||||
|
// and returns the number of bytes written if successful
|
||||||
|
func (s *Session) writeFrame(f Frame) (n int, err error) {
|
||||||
|
req := writeRequest{
|
||||||
|
frame: f,
|
||||||
|
result: make(chan writeResult, 1),
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-s.die:
|
||||||
|
return 0, errors.New(errBrokenPipe)
|
||||||
|
case s.writes <- req:
|
||||||
|
}
|
||||||
|
|
||||||
|
result := <-req.result
|
||||||
|
return result.n, result.err
|
||||||
|
}
|
After Width: | Height: | Size: 9.7 KiB |
@ -0,0 +1,261 @@
|
|||||||
|
package smux
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Stream implements net.Conn
|
||||||
|
type Stream struct {
|
||||||
|
id uint32
|
||||||
|
rstflag int32
|
||||||
|
sess *Session
|
||||||
|
buffer bytes.Buffer
|
||||||
|
bufferLock sync.Mutex
|
||||||
|
frameSize int
|
||||||
|
chReadEvent chan struct{} // notify a read event
|
||||||
|
die chan struct{} // flag the stream has closed
|
||||||
|
dieLock sync.Mutex
|
||||||
|
readDeadline atomic.Value
|
||||||
|
writeDeadline atomic.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// newStream initiates a Stream struct
|
||||||
|
func newStream(id uint32, frameSize int, sess *Session) *Stream {
|
||||||
|
s := new(Stream)
|
||||||
|
s.id = id
|
||||||
|
s.chReadEvent = make(chan struct{}, 1)
|
||||||
|
s.frameSize = frameSize
|
||||||
|
s.sess = sess
|
||||||
|
s.die = make(chan struct{})
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID returns the unique stream ID.
|
||||||
|
func (s *Stream) ID() uint32 {
|
||||||
|
return s.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read implements net.Conn
|
||||||
|
func (s *Stream) Read(b []byte) (n int, err error) {
|
||||||
|
var deadline <-chan time.Time
|
||||||
|
if d, ok := s.readDeadline.Load().(time.Time); ok && !d.IsZero() {
|
||||||
|
timer := time.NewTimer(d.Sub(time.Now()))
|
||||||
|
defer timer.Stop()
|
||||||
|
deadline = timer.C
|
||||||
|
}
|
||||||
|
|
||||||
|
READ:
|
||||||
|
select {
|
||||||
|
case <-s.die:
|
||||||
|
return 0, errors.New(errBrokenPipe)
|
||||||
|
case <-deadline:
|
||||||
|
return n, errTimeout
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
s.bufferLock.Lock()
|
||||||
|
n, err = s.buffer.Read(b)
|
||||||
|
s.bufferLock.Unlock()
|
||||||
|
|
||||||
|
if n > 0 {
|
||||||
|
s.sess.returnTokens(n)
|
||||||
|
return n, nil
|
||||||
|
} else if atomic.LoadInt32(&s.rstflag) == 1 {
|
||||||
|
_ = s.Close()
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-s.chReadEvent:
|
||||||
|
goto READ
|
||||||
|
case <-deadline:
|
||||||
|
return n, errTimeout
|
||||||
|
case <-s.die:
|
||||||
|
return 0, errors.New(errBrokenPipe)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write implements net.Conn
|
||||||
|
func (s *Stream) Write(b []byte) (n int, err error) {
|
||||||
|
var deadline <-chan time.Time
|
||||||
|
if d, ok := s.writeDeadline.Load().(time.Time); ok && !d.IsZero() {
|
||||||
|
timer := time.NewTimer(d.Sub(time.Now()))
|
||||||
|
defer timer.Stop()
|
||||||
|
deadline = timer.C
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-s.die:
|
||||||
|
return 0, errors.New(errBrokenPipe)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
frames := s.split(b, cmdPSH, s.id)
|
||||||
|
sent := 0
|
||||||
|
for k := range frames {
|
||||||
|
req := writeRequest{
|
||||||
|
frame: frames[k],
|
||||||
|
result: make(chan writeResult, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case s.sess.writes <- req:
|
||||||
|
case <-s.die:
|
||||||
|
return sent, errors.New(errBrokenPipe)
|
||||||
|
case <-deadline:
|
||||||
|
return sent, errTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case result := <-req.result:
|
||||||
|
sent += result.n
|
||||||
|
if result.err != nil {
|
||||||
|
return sent, result.err
|
||||||
|
}
|
||||||
|
case <-s.die:
|
||||||
|
return sent, errors.New(errBrokenPipe)
|
||||||
|
case <-deadline:
|
||||||
|
return sent, errTimeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sent, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements net.Conn
|
||||||
|
func (s *Stream) Close() error {
|
||||||
|
s.dieLock.Lock()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-s.die:
|
||||||
|
s.dieLock.Unlock()
|
||||||
|
return errors.New(errBrokenPipe)
|
||||||
|
default:
|
||||||
|
close(s.die)
|
||||||
|
s.dieLock.Unlock()
|
||||||
|
s.sess.streamClosed(s.id)
|
||||||
|
_, err := s.sess.writeFrame(newFrame(cmdFIN, s.id))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetReadDeadline sets the read deadline as defined by
|
||||||
|
// net.Conn.SetReadDeadline.
|
||||||
|
// A zero time value disables the deadline.
|
||||||
|
func (s *Stream) SetReadDeadline(t time.Time) error {
|
||||||
|
s.readDeadline.Store(t)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetWriteDeadline sets the write deadline as defined by
|
||||||
|
// net.Conn.SetWriteDeadline.
|
||||||
|
// A zero time value disables the deadline.
|
||||||
|
func (s *Stream) SetWriteDeadline(t time.Time) error {
|
||||||
|
s.writeDeadline.Store(t)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDeadline sets both read and write deadlines as defined by
|
||||||
|
// net.Conn.SetDeadline.
|
||||||
|
// A zero time value disables the deadlines.
|
||||||
|
func (s *Stream) SetDeadline(t time.Time) error {
|
||||||
|
if err := s.SetReadDeadline(t); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.SetWriteDeadline(t); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// session closes the stream
|
||||||
|
func (s *Stream) sessionClose() {
|
||||||
|
s.dieLock.Lock()
|
||||||
|
defer s.dieLock.Unlock()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-s.die:
|
||||||
|
default:
|
||||||
|
close(s.die)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalAddr satisfies net.Conn interface
|
||||||
|
func (s *Stream) LocalAddr() net.Addr {
|
||||||
|
if ts, ok := s.sess.conn.(interface {
|
||||||
|
LocalAddr() net.Addr
|
||||||
|
}); ok {
|
||||||
|
return ts.LocalAddr()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoteAddr satisfies net.Conn interface
|
||||||
|
func (s *Stream) RemoteAddr() net.Addr {
|
||||||
|
if ts, ok := s.sess.conn.(interface {
|
||||||
|
RemoteAddr() net.Addr
|
||||||
|
}); ok {
|
||||||
|
return ts.RemoteAddr()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pushBytes a slice into buffer
|
||||||
|
func (s *Stream) pushBytes(p []byte) {
|
||||||
|
s.bufferLock.Lock()
|
||||||
|
s.buffer.Write(p)
|
||||||
|
s.bufferLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// recycleTokens transform remaining bytes to tokens(will truncate buffer)
|
||||||
|
func (s *Stream) recycleTokens() (n int) {
|
||||||
|
s.bufferLock.Lock()
|
||||||
|
n = s.buffer.Len()
|
||||||
|
s.buffer.Reset()
|
||||||
|
s.bufferLock.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// split large byte buffer into smaller frames, reference only
|
||||||
|
func (s *Stream) split(bts []byte, cmd byte, sid uint32) []Frame {
|
||||||
|
frames := make([]Frame, 0, len(bts)/s.frameSize+1)
|
||||||
|
for len(bts) > s.frameSize {
|
||||||
|
frame := newFrame(cmd, sid)
|
||||||
|
frame.data = bts[:s.frameSize]
|
||||||
|
bts = bts[s.frameSize:]
|
||||||
|
frames = append(frames, frame)
|
||||||
|
}
|
||||||
|
if len(bts) > 0 {
|
||||||
|
frame := newFrame(cmd, sid)
|
||||||
|
frame.data = bts
|
||||||
|
frames = append(frames, frame)
|
||||||
|
}
|
||||||
|
return frames
|
||||||
|
}
|
||||||
|
|
||||||
|
// notify read event
|
||||||
|
func (s *Stream) notifyReadEvent() {
|
||||||
|
select {
|
||||||
|
case s.chReadEvent <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mark this stream has been reset
|
||||||
|
func (s *Stream) markRST() {
|
||||||
|
atomic.StoreInt32(&s.rstflag, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var errTimeout error = &timeoutError{}
|
||||||
|
|
||||||
|
type timeoutError struct{}
|
||||||
|
|
||||||
|
func (e *timeoutError) Error() string { return "i/o timeout" }
|
||||||
|
func (e *timeoutError) Timeout() bool { return true }
|
||||||
|
func (e *timeoutError) Temporary() bool { return true }
|
Loading…
Reference in new issue