mirror of
https://github.com/xtaci/smux.git
synced 2026-10-19 16:16:13 +00:00
712 lines
18 KiB
Go
712 lines
18 KiB
Go
// MIT License
|
|
//
|
|
// Copyright (c) 2016-2017 xtaci
|
|
//
|
|
// 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.
|
|
|
|
package smux
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"runtime"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
defaultAcceptBacklog = 1024
|
|
minShaperNotifySize = 16
|
|
maxShaperSize = 1024
|
|
openCloseTimeout = 30 * time.Second // Timeout for opening/closing streams
|
|
)
|
|
|
|
// resultChanPool reduces allocation of result channels
|
|
var resultChanPool = sync.Pool{
|
|
New: func() any {
|
|
return make(chan writeResult, 1)
|
|
},
|
|
}
|
|
|
|
// CLASSID represents the class of a frame
|
|
type CLASSID int
|
|
|
|
const (
|
|
CLSCTRL CLASSID = iota // prioritized control signal
|
|
CLSDATA
|
|
)
|
|
|
|
// timeoutError representing timeouts for operations such as accept, read and write
|
|
//
|
|
// To better cooperate with the standard library, timeoutError should implement the standard library's `net.Error`.
|
|
//
|
|
// For example, using smux to implement net.Listener and work with http.Server, the keep-alive connection (*smux.Stream) will be unexpectedly closed.
|
|
// For more details, see https://github.com/xtaci/smux/pull/99.
|
|
type timeoutError struct{}
|
|
|
|
func (timeoutError) Error() string { return "timeout" }
|
|
func (timeoutError) Temporary() bool { return true }
|
|
func (timeoutError) Timeout() bool { return true }
|
|
|
|
var (
|
|
ErrInvalidProtocol = errors.New("invalid protocol")
|
|
ErrConsumed = errors.New("peer consumed more than sent")
|
|
ErrGoAway = errors.New("stream id overflows, should start a new connection")
|
|
ErrTimeout net.Error = &timeoutError{}
|
|
ErrWouldBlock = errors.New("operation would block on IO")
|
|
)
|
|
|
|
// writeRequest represents a request to write a frame
|
|
type writeRequest struct {
|
|
class CLASSID
|
|
frame Frame
|
|
seq uint32
|
|
result chan writeResult
|
|
}
|
|
|
|
// writeResult represents the result of a write request
|
|
type writeResult struct {
|
|
n int
|
|
err error
|
|
}
|
|
|
|
// Session defines a multiplexed connection for streams
|
|
type Session struct {
|
|
conn io.ReadWriteCloser
|
|
|
|
config *Config
|
|
goAway int32 // flag id exhausted
|
|
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
|
|
dieOnce sync.Once
|
|
closed int32 // atomic flag for fast IsClosed check
|
|
|
|
// socket error handling
|
|
socketReadError atomic.Value
|
|
socketWriteError atomic.Value
|
|
chSocketReadError chan struct{}
|
|
chSocketWriteError chan struct{}
|
|
socketReadErrorOnce sync.Once
|
|
socketWriteErrorOnce sync.Once
|
|
|
|
// smux protocol errors
|
|
protoError atomic.Value
|
|
chProtoError chan struct{}
|
|
protoErrorOnce sync.Once
|
|
|
|
chAccepts chan *stream
|
|
|
|
sessionIsActive int32 // flag session is active
|
|
acceptDeadline atomic.Value // deadline for Accept()
|
|
|
|
requestID uint32 // Monotonic increasing write request ID
|
|
shaper chan writeRequest // a shaper for writing
|
|
sq *shaperQueue
|
|
chShaperPending chan struct{}
|
|
chShaperConsumed chan struct{}
|
|
}
|
|
|
|
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.shaper = make(chan writeRequest, maxShaperSize)
|
|
s.chSocketReadError = make(chan struct{})
|
|
s.chSocketWriteError = make(chan struct{})
|
|
s.chProtoError = make(chan struct{})
|
|
s.chShaperPending = make(chan struct{}, 1)
|
|
s.chShaperConsumed = make(chan struct{}, 1)
|
|
s.sq = NewShaperQueue()
|
|
|
|
if client {
|
|
s.nextStreamID = 1
|
|
} else {
|
|
s.nextStreamID = 0
|
|
}
|
|
|
|
go s.shaperLoop()
|
|
go s.recvLoop()
|
|
go s.sendLoop()
|
|
if !config.KeepAliveDisabled {
|
|
go s.keepalive()
|
|
}
|
|
return s
|
|
}
|
|
|
|
// OpenStream is used to create a new stream
|
|
func (s *Session) OpenStream() (*Stream, error) {
|
|
if s.IsClosed() {
|
|
return nil, io.ErrClosedPipe
|
|
}
|
|
|
|
// generate stream id
|
|
s.nextStreamIDLock.Lock()
|
|
if s.goAway > 0 {
|
|
s.nextStreamIDLock.Unlock()
|
|
return nil, ErrGoAway
|
|
}
|
|
|
|
// check for stream id overflow
|
|
if s.nextStreamID+2 < s.nextStreamID {
|
|
s.goAway = 1
|
|
s.nextStreamIDLock.Unlock()
|
|
return nil, ErrGoAway
|
|
}
|
|
|
|
// allocate next stream id
|
|
s.nextStreamID += 2
|
|
sid := s.nextStreamID
|
|
s.nextStreamIDLock.Unlock()
|
|
|
|
stream := newStream(sid, s.config.MaxFrameSize, s)
|
|
|
|
if _, err := s.writeControlFrame(newFrame(byte(s.config.Version), cmdSYN, sid)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
s.streamLock.Lock()
|
|
defer s.streamLock.Unlock()
|
|
select {
|
|
case <-s.chSocketReadError:
|
|
return nil, s.socketReadError.Load().(error)
|
|
case <-s.chSocketWriteError:
|
|
return nil, s.socketWriteError.Load().(error)
|
|
case <-s.die:
|
|
return nil, io.ErrClosedPipe
|
|
default:
|
|
s.streams[sid] = stream
|
|
wrapper := &Stream{stream: stream}
|
|
// NOTE(x): disabled finalizer for issue #997
|
|
/*
|
|
runtime.SetFinalizer(wrapper, func(s *Stream) {
|
|
s.Close()
|
|
})
|
|
*/
|
|
return wrapper, nil
|
|
}
|
|
}
|
|
|
|
// Open returns a generic ReadWriteCloser
|
|
func (s *Session) Open() (io.ReadWriteCloser, error) {
|
|
return s.OpenStream()
|
|
}
|
|
|
|
// 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.acceptDeadline.Load().(time.Time); ok && !d.IsZero() {
|
|
timer := time.NewTimer(time.Until(d))
|
|
defer timer.Stop()
|
|
deadline = timer.C
|
|
}
|
|
|
|
select {
|
|
case stream := <-s.chAccepts:
|
|
wrapper := &Stream{stream: stream}
|
|
runtime.SetFinalizer(wrapper, func(s *Stream) {
|
|
s.Close()
|
|
})
|
|
return wrapper, nil
|
|
case <-deadline:
|
|
return nil, ErrTimeout
|
|
case <-s.chSocketReadError:
|
|
return nil, s.socketReadError.Load().(error)
|
|
case <-s.chProtoError:
|
|
return nil, s.protoError.Load().(error)
|
|
case <-s.die:
|
|
return nil, io.ErrClosedPipe
|
|
}
|
|
}
|
|
|
|
// Accept Returns a generic ReadWriteCloser instead of smux.Stream
|
|
func (s *Session) Accept() (io.ReadWriteCloser, error) {
|
|
return s.AcceptStream()
|
|
}
|
|
|
|
// Close is used to close the session and all streams.
|
|
func (s *Session) Close() error {
|
|
var once bool
|
|
s.dieOnce.Do(func() {
|
|
atomic.StoreInt32(&s.closed, 1)
|
|
close(s.die)
|
|
once = true
|
|
})
|
|
|
|
if !once {
|
|
return io.ErrClosedPipe
|
|
}
|
|
|
|
s.streamLock.Lock()
|
|
for k := range s.streams {
|
|
s.streams[k].sessionClose()
|
|
}
|
|
s.streamLock.Unlock()
|
|
return s.conn.Close()
|
|
}
|
|
|
|
// CloseChan can be used by someone who wants to be notified immediately when this
|
|
// session is closed
|
|
func (s *Session) CloseChan() <-chan struct{} {
|
|
return s.die
|
|
}
|
|
|
|
// notifyBucket notifies recvLoop that bucket is available
|
|
func (s *Session) notifyBucket() {
|
|
select {
|
|
case s.bucketNotify <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (s *Session) notifyReadError(err error) {
|
|
s.socketReadErrorOnce.Do(func() {
|
|
s.socketReadError.Store(err)
|
|
close(s.chSocketReadError)
|
|
})
|
|
}
|
|
|
|
func (s *Session) notifyWriteError(err error) {
|
|
s.socketWriteErrorOnce.Do(func() {
|
|
s.socketWriteError.Store(err)
|
|
close(s.chSocketWriteError)
|
|
})
|
|
}
|
|
|
|
func (s *Session) notifyProtoError(err error) {
|
|
s.protoErrorOnce.Do(func() {
|
|
s.protoError.Store(err)
|
|
close(s.chProtoError)
|
|
})
|
|
}
|
|
|
|
// IsClosed does a safe check to see if we have shutdown
|
|
func (s *Session) IsClosed() bool {
|
|
return atomic.LoadInt32(&s.closed) != 0
|
|
}
|
|
|
|
// 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.acceptDeadline.Store(t)
|
|
return nil
|
|
}
|
|
|
|
// LocalAddr satisfies net.Conn interface
|
|
func (s *Session) LocalAddr() net.Addr {
|
|
if ts, ok := s.conn.(interface {
|
|
LocalAddr() net.Addr
|
|
}); ok {
|
|
return ts.LocalAddr()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RemoteAddr satisfies net.Conn interface
|
|
func (s *Session) RemoteAddr() net.Addr {
|
|
if ts, ok := s.conn.(interface {
|
|
RemoteAddr() net.Addr
|
|
}); ok {
|
|
return ts.RemoteAddr()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// notify the session that a stream has closed
|
|
func (s *Session) streamClosed(sid uint32) {
|
|
s.streamLock.Lock()
|
|
defer s.streamLock.Unlock()
|
|
|
|
stream, ok := s.streams[sid]
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
if n := stream.recycleTokens(); n > 0 {
|
|
// return remaining tokens to the bucket
|
|
if atomic.AddInt32(&s.bucket, int32(n)) > 0 {
|
|
s.notifyBucket()
|
|
}
|
|
}
|
|
delete(s.streams, sid)
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
}
|
|
|
|
// recvLoop keeps on reading from underlying connection if tokens are available
|
|
func (s *Session) recvLoop() {
|
|
var hdr rawHeader
|
|
var updHdr updHeader
|
|
|
|
for {
|
|
// Wait until we have tokens or session is closed.
|
|
for atomic.LoadInt32(&s.bucket) <= 0 && !s.IsClosed() {
|
|
select {
|
|
case <-s.bucketNotify:
|
|
case <-s.die:
|
|
// If it returns here, Accept() and OpenStream() are unblocked with io.ErrClosedPipe,
|
|
// causing recvLoop to exit gracefully. If recvLoop is blocked in io.ReadFull, however,
|
|
// it will be unblocked by a socket read error instead.
|
|
return
|
|
}
|
|
}
|
|
|
|
// As long as we have tokens, try to read frames.
|
|
// read header first
|
|
_, err := io.ReadFull(s.conn, hdr[:])
|
|
if err != nil {
|
|
s.notifyReadError(err)
|
|
return
|
|
}
|
|
|
|
// Mark the session as active
|
|
atomic.StoreInt32(&s.sessionIsActive, 1)
|
|
|
|
// validate protocol version
|
|
if hdr.Version() != byte(s.config.Version) {
|
|
s.notifyProtoError(ErrInvalidProtocol)
|
|
return
|
|
}
|
|
|
|
// handle different command types
|
|
sid := hdr.StreamID()
|
|
switch hdr.Cmd() {
|
|
case cmdNOP:
|
|
if hdr.Length() != 0 {
|
|
s.notifyProtoError(ErrInvalidProtocol)
|
|
return
|
|
}
|
|
case cmdSYN: // stream opening
|
|
if hdr.Length() != 0 {
|
|
s.notifyProtoError(ErrInvalidProtocol)
|
|
return
|
|
}
|
|
var accepted *stream
|
|
s.streamLock.Lock()
|
|
if _, ok := s.streams[sid]; !ok {
|
|
stream := newStream(sid, s.config.MaxFrameSize, s)
|
|
s.streams[sid] = stream
|
|
accepted = stream
|
|
}
|
|
s.streamLock.Unlock()
|
|
|
|
if accepted != nil {
|
|
select {
|
|
case s.chAccepts <- accepted:
|
|
case <-s.die:
|
|
}
|
|
}
|
|
|
|
case cmdFIN: // stream closing
|
|
if hdr.Length() != 0 {
|
|
s.notifyProtoError(ErrInvalidProtocol)
|
|
return
|
|
}
|
|
s.streamLock.Lock()
|
|
st := s.streams[sid]
|
|
s.streamLock.Unlock()
|
|
if st != nil {
|
|
st.fin() // fin unblocks the readers and writers
|
|
}
|
|
|
|
case cmdPSH: // data frame
|
|
if hdr.Length() == 0 {
|
|
continue
|
|
}
|
|
|
|
// read payload from the underlying connection
|
|
pNewbuf := defaultAllocator.Get(int(hdr.Length()))
|
|
written, err := io.ReadFull(s.conn, *pNewbuf)
|
|
if err != nil {
|
|
s.notifyReadError(err)
|
|
|
|
// recycle the buffer immediately.
|
|
defaultAllocator.Put(pNewbuf)
|
|
return
|
|
}
|
|
|
|
// push data to the corresponding stream
|
|
s.streamLock.Lock()
|
|
if stream, ok := s.streams[sid]; ok {
|
|
stream.pushBytes(pNewbuf)
|
|
// deduct tokens from the bucket
|
|
atomic.AddInt32(&s.bucket, -int32(written))
|
|
stream.wakeupReader()
|
|
} else {
|
|
// data directed to a missing/closed stream, recycle the buffer immediately.
|
|
defaultAllocator.Put(pNewbuf)
|
|
}
|
|
s.streamLock.Unlock()
|
|
|
|
case cmdUPD: // a window update signal (v2 only)
|
|
if s.config.Version != 2 {
|
|
s.notifyProtoError(ErrInvalidProtocol)
|
|
return
|
|
}
|
|
if hdr.Length() != szCmdUPD {
|
|
s.notifyProtoError(ErrInvalidProtocol)
|
|
return
|
|
}
|
|
|
|
_, err := io.ReadFull(s.conn, updHdr[:])
|
|
if err != nil {
|
|
s.notifyReadError(err)
|
|
return
|
|
}
|
|
|
|
// update the window size for the corresponding stream
|
|
s.streamLock.Lock()
|
|
st := s.streams[sid]
|
|
s.streamLock.Unlock()
|
|
if st != nil {
|
|
st.update(updHdr.Consumed(), updHdr.Window())
|
|
}
|
|
|
|
default:
|
|
s.notifyProtoError(ErrInvalidProtocol)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// keepalive sends NOP frames periodically to keep the connection alive
|
|
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.writeFrameInternal(newFrame(byte(s.config.Version), cmdNOP, 0), tickerPing.C, CLSCTRL)
|
|
s.notifyBucket() // force a wakeup signal to the recvLoop
|
|
case <-tickerTimeout.C:
|
|
if !atomic.CompareAndSwapInt32(&s.sessionIsActive, 1, 0) {
|
|
// recvLoop may block while bucket is 0, in this case,
|
|
// session should not be closed.
|
|
if atomic.LoadInt32(&s.bucket) > 0 {
|
|
s.Close()
|
|
return
|
|
}
|
|
}
|
|
case <-s.die:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// shaperLoop implements a priority queue and bandwidth shaping for write requests.
|
|
// Eg: Control messages are prioritized over data messages, and shaper tries
|
|
// its best to keep fair bandwidth among streams.
|
|
func (s *Session) shaperLoop() {
|
|
chShaper := s.shaper
|
|
|
|
for {
|
|
select {
|
|
case <-s.die:
|
|
return
|
|
case r := <-chShaper:
|
|
s.sq.Push(r)
|
|
// batch drain: collect more requests if available
|
|
for len(chShaper) > 0 && s.sq.Len() < maxShaperSize {
|
|
select {
|
|
case r := <-chShaper:
|
|
s.sq.Push(r)
|
|
default:
|
|
}
|
|
}
|
|
// notify sendLoop there are pending requests
|
|
s.notifyShaperPending()
|
|
|
|
if s.sq.Len() >= maxShaperSize {
|
|
// stop accepting new requests temporarily if shaper queue is full
|
|
chShaper = nil
|
|
}
|
|
case <-s.chShaperConsumed:
|
|
// re-enable shaper channel
|
|
chShaper = s.shaper
|
|
}
|
|
}
|
|
}
|
|
|
|
// notifyShaperPending notifies sendLoop that there are pending requests
|
|
func (s *Session) notifyShaperPending() {
|
|
select {
|
|
case s.chShaperPending <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
// notifyShaperConsumed notifies when shaper queue is being consumed
|
|
func (s *Session) notifyShaperConsumed() {
|
|
select {
|
|
case s.chShaperConsumed <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
// sendLoop sends frames over the underlying connection
|
|
func (s *Session) sendLoop() {
|
|
var buf []byte
|
|
var n int
|
|
var err error
|
|
var vec [][]byte // vector for writeBuffers
|
|
|
|
bw, ok := s.conn.(interface {
|
|
WriteBuffers(v [][]byte) (n int, err error)
|
|
})
|
|
|
|
if ok {
|
|
buf = make([]byte, headerSize)
|
|
vec = make([][]byte, 2)
|
|
} else {
|
|
buf = make([]byte, (1<<16)+headerSize)
|
|
}
|
|
|
|
EVENT_LOOP:
|
|
for {
|
|
select {
|
|
case <-s.die:
|
|
return
|
|
case <-s.chShaperPending:
|
|
for {
|
|
request, ok := s.sq.Pop()
|
|
if !ok {
|
|
// notify shaperLoop to accept new requests
|
|
s.notifyShaperConsumed()
|
|
goto EVENT_LOOP
|
|
}
|
|
|
|
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)
|
|
|
|
// support for scatter-gather I/O
|
|
if len(vec) > 0 {
|
|
vec[0] = buf[:headerSize]
|
|
vec[1] = request.frame.data
|
|
n, err = bw.WriteBuffers(vec)
|
|
} else {
|
|
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
|
|
|
|
// store conn error
|
|
if err != nil {
|
|
s.notifyWriteError(err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// writeControlFrame writes the control frame to the underlying connection
|
|
// and returns the number of bytes written if successful
|
|
func (s *Session) writeControlFrame(f Frame) (n int, err error) {
|
|
timer := time.NewTimer(openCloseTimeout)
|
|
defer timer.Stop()
|
|
|
|
return s.writeFrameInternal(f, timer.C, CLSCTRL)
|
|
}
|
|
|
|
// internal writeFrame version to support deadline used in keepalive
|
|
func (s *Session) writeFrameInternal(f Frame, deadline <-chan time.Time, class CLASSID) (int, error) {
|
|
// get result channel from pool
|
|
resultCh := resultChanPool.Get().(chan writeResult)
|
|
|
|
req := writeRequest{
|
|
class: class,
|
|
frame: f,
|
|
seq: atomic.AddUint32(&s.requestID, 1),
|
|
result: resultCh,
|
|
}
|
|
select {
|
|
case s.shaper <- req:
|
|
case <-s.die:
|
|
resultChanPool.Put(resultCh)
|
|
return 0, io.ErrClosedPipe
|
|
case <-s.chSocketWriteError:
|
|
resultChanPool.Put(resultCh)
|
|
return 0, s.socketWriteError.Load().(error)
|
|
case <-deadline:
|
|
resultChanPool.Put(resultCh)
|
|
return 0, ErrTimeout
|
|
}
|
|
|
|
select {
|
|
case result := <-resultCh:
|
|
resultChanPool.Put(resultCh)
|
|
return result.n, result.err
|
|
case <-s.die:
|
|
// Cannot recycle channel here - sendLoop may still write to it
|
|
return 0, io.ErrClosedPipe
|
|
case <-s.chSocketWriteError:
|
|
// Cannot recycle channel here - sendLoop may still write to it
|
|
return 0, s.socketWriteError.Load().(error)
|
|
case <-deadline:
|
|
// Cannot recycle channel here - sendLoop may still write to it
|
|
return 0, ErrTimeout
|
|
}
|
|
}
|