First commit

This commit is contained in:
Tevin Zhang 2016-06-01 12:07:09 +08:00
parent 69c453cad7
commit 0c7007486a
5 changed files with 275 additions and 0 deletions

41
README.md Normal file
View File

@ -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)

10
err.go Normal file
View File

@ -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 }

164
shaker.go Normal file
View File

@ -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())
}

25
shaker_test.go Normal file
View File

@ -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)
}
}

35
socket.go Normal file
View File

@ -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)
}