First commit
This commit is contained in:
parent
69c453cad7
commit
0c7007486a
|
@ -0,0 +1,41 @@
|
|||
# TCP Shaker :heartbeat:
|
||||
[![GoDoc](https://godoc.org/github.com/tevino/tcp-shaker?status.svg)](https://godoc.org/github.com/tevino/tcp-shaker)
|
||||
|
||||
Performing TCP handshake without ACK, useful for health checking.
|
||||
|
||||
HAProxy do this exactly the same, which is:
|
||||
|
||||
- SYN
|
||||
- SYN-ACK
|
||||
- RST
|
||||
|
||||
## Why do I have to do this?
|
||||
Usually when you establish a TCP connection(e.g. net.Dial), these are the first three packets (TCP three-way handshake):
|
||||
|
||||
- Client -> Server: SYN
|
||||
- Server -> Client: SYN-ACK
|
||||
- Client -> Server: ACK
|
||||
|
||||
**This package tries to avoid the last ACK when doing handshakes.**
|
||||
|
||||
By sending the last ACK, the connection is considered established.
|
||||
|
||||
However as for TCP health checking the last ACK may not necessary.
|
||||
|
||||
The Server could be considered alive after it sends back SYN-ACK.
|
||||
|
||||
### Benefits of avoiding the last ACK:
|
||||
1. Less packets better efficiency
|
||||
2. The health checking is less obvious
|
||||
|
||||
The second one is essential, because it bothers server less.
|
||||
|
||||
Usually this means the server will not notice the health checking traffic at all, **thus the act of health chekcing will not be
|
||||
considered as some misbehaviour of client.**
|
||||
|
||||
## Requirements:
|
||||
- Linux 2.4 or newer
|
||||
|
||||
## TODO:
|
||||
|
||||
- [ ] IPv6 support (Test environment needed, PRs are welcomed)
|
|
@ -0,0 +1,10 @@
|
|||
package tcp
|
||||
|
||||
// ErrTimeout indicates I/O timeout
|
||||
var ErrTimeout = &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 }
|
|
@ -0,0 +1,164 @@
|
|||
// Package tcp is used to perform TCP handshake without ACK.
|
||||
// Useful for health checking, HAProxy do this exactly the same.
|
||||
// Which is SYN, SYN-ACK, RST.
|
||||
//
|
||||
// Why do I have to do this?
|
||||
// Usually when you establish a TCP connection(e.g. net.Dial), these
|
||||
// are the first three packets (TCP three-way handshake):
|
||||
//
|
||||
// SYN: Client -> Server
|
||||
// SYN-ACK: Server -> Client
|
||||
// ACK: Client -> Server
|
||||
//
|
||||
// This package tries to avoid the last ACK when doing handshakes.
|
||||
//
|
||||
// By sending the last ACK, the connection is considered established.
|
||||
// However as for TCP health checking the last ACK may not necessary.
|
||||
// The Server could be considered alive after it sends back SYN-ACK.
|
||||
//
|
||||
// Benefits of avoiding the last ACK:
|
||||
//
|
||||
// 1. Less packets better efficiency
|
||||
//
|
||||
// 2. The health checking is less obvious
|
||||
//
|
||||
// The second one is essential, because it bothers server less.
|
||||
// Usually this means the server will not notice the health checking
|
||||
// traffic at all, thus the act of health chekcing will not be
|
||||
// considered as some misbehaviour of client.
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
const maxEpollEvents = 32
|
||||
|
||||
// Shaker contains an epoll instance for TCP handshake checking
|
||||
type Shaker struct {
|
||||
epollFd int
|
||||
}
|
||||
|
||||
// Init creates inner epoll instance, call this before anything else
|
||||
func (s *Shaker) Init() error {
|
||||
var err error
|
||||
s.epollFd, err = syscall.EpollCreate1(0)
|
||||
if err != nil {
|
||||
return os.NewSyscallError("epoll_create1", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Test performs a TCP check with given TCP address and timeout
|
||||
// A successful check will result in nil error
|
||||
// ErrTimeout is returned if timeout
|
||||
// Note: timeout includes domain resolving
|
||||
func (s *Shaker) Test(addr string, timeout time.Duration) error {
|
||||
deadline := time.Now().Add(timeout)
|
||||
|
||||
rAddr, err := parseSockAddr(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fd, err := createSocket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer syscall.Close(fd)
|
||||
|
||||
if err = setSockopts(fd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = s.connect(fd, rAddr, deadline); err != nil {
|
||||
return err
|
||||
}
|
||||
if reached(deadline) {
|
||||
return ErrTimeout
|
||||
}
|
||||
|
||||
s.addEpoll(fd)
|
||||
timeoutMS := int(timeout.Nanoseconds() / 1000000)
|
||||
// check for connect error
|
||||
for {
|
||||
succeed, err := s.wait(fd, timeoutMS)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect error: %s", err)
|
||||
}
|
||||
if reached(deadline) {
|
||||
return ErrTimeout
|
||||
}
|
||||
if succeed {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the inner epoll fd
|
||||
func (s *Shaker) Close() error {
|
||||
return syscall.Close(s.epollFd)
|
||||
}
|
||||
|
||||
func (s *Shaker) addEpoll(fd int) error {
|
||||
var event syscall.EpollEvent
|
||||
event.Events = syscall.EPOLLOUT
|
||||
event.Fd = int32(fd)
|
||||
if err := syscall.EpollCtl(s.epollFd, syscall.EPOLL_CTL_ADD, fd, &event); err != nil {
|
||||
return os.NewSyscallError("epoll_ctl", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// wait waits for epoll event of given fd
|
||||
// The boolean returned indicates whether the connect is successful
|
||||
func (s *Shaker) wait(fd int, timeoutMS int) (bool, error) {
|
||||
var events [maxEpollEvents]syscall.EpollEvent
|
||||
nevents, err := syscall.EpollWait(s.epollFd, events[:], timeoutMS)
|
||||
if err != nil {
|
||||
return false, os.NewSyscallError("epoll_wait", err)
|
||||
}
|
||||
|
||||
for ev := 0; ev < nevents; ev++ {
|
||||
if int(events[ev].Fd) == fd {
|
||||
errCode, err := syscall.GetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_ERROR)
|
||||
if err != nil {
|
||||
return false, os.NewSyscallError("getsockopt", err)
|
||||
}
|
||||
if errCode != 0 {
|
||||
return false, fmt.Errorf("getsockopt[%d]", errCode)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (s *Shaker) connect(fd int, addr syscall.Sockaddr, deadline time.Time) error {
|
||||
switch err := syscall.Connect(fd, addr); err {
|
||||
case syscall.EINPROGRESS, syscall.EALREADY, syscall.EINTR:
|
||||
case nil, syscall.EISCONN:
|
||||
// already connected
|
||||
case syscall.EINVAL:
|
||||
// On Solaris we can see EINVAL if the socket has
|
||||
// already been accepted and closed by the server.
|
||||
// Treat this as a successful connection--writes to
|
||||
// the socket will see EOF. For details and a test
|
||||
// case in C see https://golang.org/issue/6828.
|
||||
if runtime.GOOS == "solaris" {
|
||||
return nil
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
return os.NewSyscallError("connect", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func reached(deadline time.Time) bool {
|
||||
return !deadline.IsZero() && deadline.Before(time.Now())
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package tcp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ExampleShaker() {
|
||||
s := Shaker{}
|
||||
if err := s.Init(); err != nil {
|
||||
log.Fatal("Shaker init failed:", err)
|
||||
}
|
||||
|
||||
timeout := time.Second * 1
|
||||
err := s.Test("google.com:80", timeout)
|
||||
switch err {
|
||||
case ErrTimeout:
|
||||
fmt.Println("Connect to Google timeout")
|
||||
case nil:
|
||||
fmt.Println("Connect to Google succeded")
|
||||
default:
|
||||
fmt.Println("Connect to Google failed:", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package tcp
|
||||
|
||||
import (
|
||||
"net"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func parseSockAddr(addr string) (syscall.Sockaddr, error) {
|
||||
tAddr, err := net.ResolveTCPAddr("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var addr4 [4]byte
|
||||
if tAddr.IP != nil {
|
||||
copy(addr4[:], tAddr.IP.To4()) // copy last 4 bytes of slice to array
|
||||
}
|
||||
return &syscall.SockaddrInet4{Port: tAddr.Port, Addr: addr4}, nil
|
||||
}
|
||||
|
||||
func createSocket() (int, error) {
|
||||
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, 0)
|
||||
syscall.CloseOnExec(fd)
|
||||
return fd, err
|
||||
}
|
||||
|
||||
func setSockopts(fd int) error {
|
||||
err := syscall.SetNonblock(fd, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
linger := syscall.Linger{Onoff: 1, Linger: 0}
|
||||
syscall.SetsockoptLinger(fd, syscall.SOL_SOCKET, syscall.SO_LINGER, &linger)
|
||||
return syscall.SetsockoptInt(fd, syscall.SOL_TCP, syscall.TCP_QUICKACK, 0)
|
||||
}
|
Loading…
Reference in New Issue