├── .github └── workflows │ └── go.yml ├── LICENSE ├── README.md ├── benchmarks ├── .gitignore ├── README.md ├── analyze.go ├── bench-echo.sh ├── bench-http.sh ├── bench-redis.sh ├── bench.sh ├── fasthttp-server │ └── main.go ├── net-echo-server │ └── main.go ├── net-http-server │ └── main.go └── out │ ├── echo.png │ ├── echo.txt │ ├── http.png │ ├── http.txt │ ├── redis1.txt │ ├── redis16.txt │ ├── redis8.txt │ ├── redis_pipeline_1.png │ ├── redis_pipeline_16.png │ └── redis_pipeline_8.png ├── evio.go ├── evio_other.go ├── evio_std.go ├── evio_test.go ├── evio_unix.go ├── examples ├── echo-server │ └── main.go ├── http-server │ └── main.go └── redis-server │ └── main.go ├── go.mod ├── go.sum ├── internal ├── internal_bsd.go ├── internal_darwin.go ├── internal_linux.go ├── internal_openbsd.go ├── internal_unix.go ├── notequeue.go └── socktoaddr.go └── logo.png /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ^1.15 20 | 21 | - name: Check out code into the Go module directory 22 | uses: actions/checkout@v2 23 | 24 | - name: Get dependencies 25 | run: | 26 | go get -v -t -d ./... 27 | if [ -f Gopkg.toml ]; then 28 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 29 | dep ensure 30 | fi 31 | 32 | - name: Build 33 | run: go build -v . 34 | 35 | - name: Test 36 | run: go test -v . 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Joshua J Baker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | <p align="center"> 2 | <img 3 | src="logo.png" 4 | width="213" height="75" border="0" alt="evio"> 5 | <br> 6 | <a href="https://godoc.org/github.com/tidwall/evio"><img src="https://img.shields.io/badge/api-reference-blue.svg?style=flat-square" alt="GoDoc"></a> 7 | </p> 8 | 9 | `evio` is an event loop networking framework that is fast and small. It makes direct [epoll](https://en.wikipedia.org/wiki/Epoll) and [kqueue](https://en.wikipedia.org/wiki/Kqueue) syscalls rather than using the standard Go [net](https://golang.org/pkg/net/) package, and works in a similar manner as [libuv](https://github.com/libuv/libuv) and [libevent](https://github.com/libevent/libevent). 10 | 11 | The goal of this project is to create a server framework for Go that performs on par with [Redis](http://redis.io) and [Haproxy](http://www.haproxy.org) for packet handling. It was built to be the foundation for [Tile38](https://github.com/tidwall/tile38) and a future L7 proxy for Go. 12 | 13 | *Please note: Evio should not be considered as a drop-in replacement for the standard Go net or net/http packages.* 14 | 15 | ## Features 16 | 17 | - [Fast](#performance) single-threaded or [multithreaded](#multithreaded) event loop 18 | - Built-in [load balancing](#load-balancing) options 19 | - Simple API 20 | - Low memory usage 21 | - Supports tcp, [udp](#udp), and unix sockets 22 | - Allows [multiple network binding](#multiple-addresses) on the same event loop 23 | - Flexible [ticker](#ticker) event 24 | - Fallback for non-epoll/kqueue operating systems by simulating events with the [net](https://golang.org/pkg/net/) package 25 | - [SO_REUSEPORT](#so_reuseport) socket option 26 | 27 | ## Getting Started 28 | 29 | ### Installing 30 | 31 | To start using evio, install Go and run `go get`: 32 | 33 | ```sh 34 | $ go get -u github.com/tidwall/evio 35 | ``` 36 | 37 | This will retrieve the library. 38 | 39 | ### Usage 40 | 41 | Starting a server is easy with `evio`. Just set up your events and pass them to the `Serve` function along with the binding address(es). Each connections is represented as an `evio.Conn` object that is passed to various events to differentiate the clients. At any point you can close a client or shutdown the server by return a `Close` or `Shutdown` action from an event. 42 | 43 | Example echo server that binds to port 5000: 44 | 45 | ```go 46 | package main 47 | 48 | import "github.com/tidwall/evio" 49 | 50 | func main() { 51 | var events evio.Events 52 | events.Data = func(c evio.Conn, in []byte) (out []byte, action evio.Action) { 53 | out = in 54 | return 55 | } 56 | if err := evio.Serve(events, "tcp://localhost:5000"); err != nil { 57 | panic(err.Error()) 58 | } 59 | } 60 | ``` 61 | 62 | Here the only event being used is `Data`, which fires when the server receives input data from a client. 63 | The exact same input data is then passed through the output return value, which is then sent back to the client. 64 | 65 | Connect to the echo server: 66 | 67 | ```sh 68 | $ telnet localhost 5000 69 | ``` 70 | 71 | ### Events 72 | 73 | The event type has a bunch of handy events: 74 | 75 | - `Serving` fires when the server is ready to accept new connections. 76 | - `Opened` fires when a connection has opened. 77 | - `Closed` fires when a connection has closed. 78 | - `Detach` fires when a connection has been detached using the `Detach` return action. 79 | - `Data` fires when the server receives new data from a connection. 80 | - `Tick` fires immediately after the server starts and will fire again after a specified interval. 81 | 82 | ### Multiple addresses 83 | 84 | A server can bind to multiple addresses and share the same event loop. 85 | 86 | ```go 87 | evio.Serve(events, "tcp://192.168.0.10:5000", "unix://socket") 88 | ``` 89 | 90 | ### Ticker 91 | 92 | The `Tick` event fires ticks at a specified interval. 93 | The first tick fires immediately after the `Serving` events. 94 | 95 | ```go 96 | events.Tick = func() (delay time.Duration, action Action){ 97 | log.Printf("tick") 98 | delay = time.Second 99 | return 100 | } 101 | ``` 102 | 103 | ## UDP 104 | 105 | The `Serve` function can bind to UDP addresses. 106 | 107 | - All incoming and outgoing packets are not buffered and sent individually. 108 | - The `Opened` and `Closed` events are not availble for UDP sockets, only the `Data` event. 109 | 110 | ## Multithreaded 111 | 112 | The `events.NumLoops` options sets the number of loops to use for the server. 113 | A value greater than 1 will effectively make the server multithreaded for multi-core machines. 114 | Which means you must take care when synchonizing memory between event callbacks. 115 | Setting to 0 or 1 will run the server as single-threaded. 116 | Setting to -1 will automatically assign this value equal to `runtime.NumProcs()`. 117 | 118 | ## Load balancing 119 | 120 | The `events.LoadBalance` options sets the load balancing method. 121 | Load balancing is always a best effort to attempt to distribute the incoming connections between multiple loops. 122 | This option is only available when `events.NumLoops` is set. 123 | 124 | - `Random` requests that connections are randomly distributed. 125 | - `RoundRobin` requests that connections are distributed to a loop in a round-robin fashion. 126 | - `LeastConnections` assigns the next accepted connection to the loop with the least number of active connections. 127 | 128 | ## SO_REUSEPORT 129 | 130 | Servers can utilize the [SO_REUSEPORT](https://lwn.net/Articles/542629/) option which allows multiple sockets on the same host to bind to the same port. 131 | 132 | Just provide `reuseport=true` to an address: 133 | 134 | ```go 135 | evio.Serve(events, "tcp://0.0.0.0:1234?reuseport=true")) 136 | ``` 137 | 138 | ## More examples 139 | 140 | Please check out the [examples](examples) subdirectory for a simplified [redis](examples/redis-server/main.go) clone, an [echo](examples/echo-server/main.go) server, and a very basic [http](examples/http-server/main.go) server. 141 | 142 | To run an example: 143 | 144 | ```sh 145 | $ go run examples/http-server/main.go 146 | $ go run examples/redis-server/main.go 147 | $ go run examples/echo-server/main.go 148 | ``` 149 | 150 | ## Performance 151 | 152 | ### Benchmarks 153 | 154 | These benchmarks were run on an ec2 c4.xlarge instance in single-threaded mode (GOMAXPROC=1) over Ipv4 localhost. 155 | Check out [benchmarks](benchmarks) for more info. 156 | 157 | <img src="benchmarks/out/echo.png" width="336" height="144" border="0" alt="echo benchmark"><img src="benchmarks/out/http.png" width="336" height="144" border="0" alt="http benchmark"><img src="benchmarks/out/redis_pipeline_1.png" width="336" height="144" border="0" alt="redis 1 benchmark"><img src="benchmarks/out/redis_pipeline_8.png" width="336" height="144" border="0" alt="redis 8 benchmark"> 158 | 159 | 160 | ## Contact 161 | 162 | Josh Baker [@tidwall](http://twitter.com/tidwall) 163 | 164 | ## License 165 | 166 | `evio` source code is available under the MIT [License](/LICENSE). 167 | 168 | -------------------------------------------------------------------------------- /benchmarks/.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | socket -------------------------------------------------------------------------------- /benchmarks/README.md: -------------------------------------------------------------------------------- 1 | ## evio benchmark tools 2 | 3 | Required tools: 4 | 5 | - [bombardier](https://github.com/codesenberg/bombardier) for HTTP 6 | - [tcpkali](https://github.com/machinezone/tcpkali) for Echo 7 | - [Redis](http://redis.io) for Redis 8 | 9 | Required Go packages: 10 | 11 | ``` 12 | go get gonum.org/v1/plot/... 13 | go get -u github.com/valyala/fasthttp 14 | go get -u github.com/tidwall/redcon 15 | ``` 16 | 17 | And of course [Go](https://golang.org) is required. 18 | 19 | Run `bench.sh` for all benchmarks. 20 | 21 | ## Notes 22 | 23 | - The current results were run on an Ec2 c4.xlarge instance. 24 | - The servers started in single-threaded mode (GOMAXPROC=1). 25 | - Network clients connected over Ipv4 localhost. 26 | 27 | Like all benchmarks ever made in the history of whatever, YMMV. Please tweak and run in your environment and let me know if you see any glaring issues. 28 | -------------------------------------------------------------------------------- /benchmarks/analyze.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "math" 7 | "strconv" 8 | "strings" 9 | 10 | "gonum.org/v1/plot" 11 | "gonum.org/v1/plot/plotter" 12 | "gonum.org/v1/plot/plotutil" 13 | "gonum.org/v1/plot/vg" 14 | ) 15 | 16 | var category string 17 | var kind string 18 | var connections, commands, pipeline, seconds int 19 | var rate float64 20 | var values []float64 21 | var names []string 22 | 23 | func main() { 24 | analyze() 25 | autoplot() 26 | } 27 | 28 | func autoplot() { 29 | if category == "" { 30 | return 31 | } 32 | var title = category 33 | path := strings.Replace("out/"+category+".png", " ", "_", -1) 34 | 35 | plotit( 36 | path, 37 | title, 38 | values, 39 | names, 40 | ) 41 | 42 | } 43 | 44 | func analyze() { 45 | lines := readlines("out/http.txt", "out/echo.txt", "out/redis1.txt", "out/redis8.txt", "out/redis16.txt") 46 | var err error 47 | for _, line := range lines { 48 | rlines := strings.Split(line, "\r") 49 | line = strings.TrimSpace(rlines[len(rlines)-1]) 50 | if strings.HasPrefix(line, "--- ") { 51 | if strings.HasSuffix(line, " START ---") { 52 | autoplot() 53 | category = strings.ToLower(strings.Replace(strings.Replace(line, "--- ", "", -1), " START ---", "", -1)) 54 | category = strings.Replace(category, "bench ", "", -1) 55 | values = nil 56 | names = nil 57 | } else { 58 | kind = strings.ToLower(strings.Replace(strings.Replace(line, "--- ", "", -1), " ---", "", -1)) 59 | } 60 | connections, commands, pipeline, seconds = 0, 0, 0, 0 61 | } else if strings.HasPrefix(line, "*** ") { 62 | details := strings.Split(strings.ToLower(strings.Replace(line, "*** ", "", -1)), ", ") 63 | for _, item := range details { 64 | if strings.HasSuffix(item, " connections") { 65 | connections, err = strconv.Atoi(strings.Split(item, " ")[0]) 66 | must(err) 67 | } else if strings.HasSuffix(item, " commands") { 68 | commands, err = strconv.Atoi(strings.Split(item, " ")[0]) 69 | must(err) 70 | } else if strings.HasSuffix(item, " commands pipeline") { 71 | pipeline, err = strconv.Atoi(strings.Split(item, " ")[0]) 72 | must(err) 73 | 74 | } else if strings.HasSuffix(item, " seconds") { 75 | seconds, err = strconv.Atoi(strings.Split(item, " ")[0]) 76 | must(err) 77 | } 78 | } 79 | } else { 80 | switch { 81 | case category == "echo": 82 | if strings.HasPrefix(line, "Packet rate estimate: ") { 83 | rate, err = strconv.ParseFloat(strings.Split(strings.Split(line, ": ")[1], "↓,")[0], 64) 84 | must(err) 85 | output() 86 | } 87 | case category == "http": 88 | if strings.HasPrefix(line, "Reqs/sec ") { 89 | rate, err = strconv.ParseFloat( 90 | strings.Split(strings.TrimSpace(strings.Split(line, "Reqs/sec ")[1]), " ")[0], 64) 91 | must(err) 92 | output() 93 | } 94 | case strings.HasPrefix(category, "redis"): 95 | if strings.HasPrefix(line, "PING_INLINE: ") { 96 | rate, err = strconv.ParseFloat(strings.Split(strings.Split(line, ": ")[1], " ")[0], 64) 97 | must(err) 98 | output() 99 | } 100 | } 101 | } 102 | } 103 | } 104 | 105 | func output() { 106 | name := kind 107 | names = append(names, name) 108 | values = append(values, rate) 109 | //csv += fmt.Sprintf("%s,%s,%d,%d,%d,%d,%f\n", category, kind, connections, commands, pipeline, seconds, rate) 110 | } 111 | 112 | func readlines(paths ...string) (lines []string) { 113 | for _, path := range paths { 114 | data, err := ioutil.ReadFile(path) 115 | must(err) 116 | lines = append(lines, strings.Split(string(data), "\n")...) 117 | } 118 | return 119 | } 120 | 121 | func must(err error) { 122 | if err != nil { 123 | panic(err) 124 | } 125 | } 126 | 127 | func plotit(path, title string, values []float64, names []string) { 128 | plot.DefaultFont = "Helvetica" 129 | var groups []plotter.Values 130 | for _, value := range values { 131 | groups = append(groups, plotter.Values{value}) 132 | } 133 | p, err := plot.New() 134 | if err != nil { 135 | panic(err) 136 | } 137 | p.Title.Text = title 138 | p.Y.Tick.Marker = commaTicks{} 139 | p.Y.Label.Text = "Req/s" 140 | bw := 25.0 141 | w := vg.Points(bw) 142 | var bars []plot.Plotter 143 | var barsp []*plotter.BarChart 144 | for i := 0; i < len(values); i++ { 145 | bar, err := plotter.NewBarChart(groups[i], w) 146 | if err != nil { 147 | panic(err) 148 | } 149 | bar.LineStyle.Width = vg.Length(0) 150 | bar.Color = plotutil.Color(i) 151 | bar.Offset = vg.Length( 152 | (float64(w) * float64(i)) - 153 | (float64(w)*float64(len(values)))/2) 154 | bars = append(bars, bar) 155 | barsp = append(barsp, bar) 156 | } 157 | p.Add(bars...) 158 | for i, name := range names { 159 | p.Legend.Add(fmt.Sprintf("%s (%.0f req/s)", name, values[i]), barsp[i]) 160 | } 161 | 162 | p.Legend.Top = true 163 | p.NominalX("") 164 | 165 | if err := p.Save(7*vg.Inch, 3*vg.Inch, path); err != nil { 166 | panic(err) 167 | } 168 | } 169 | 170 | // PreciseTicks is suitable for the Tick.Marker field of an Axis, it returns a 171 | // set of tick marks with labels that have been rounded less agressively than 172 | // what DefaultTicks provides. 173 | type PreciseTicks struct{} 174 | 175 | // Ticks returns Ticks in a specified range 176 | func (PreciseTicks) Ticks(min, max float64) []plot.Tick { 177 | const suggestedTicks = 3 178 | 179 | if max <= min { 180 | panic("illegal range") 181 | } 182 | 183 | tens := math.Pow10(int(math.Floor(math.Log10(max - min)))) 184 | n := (max - min) / tens 185 | for n < suggestedTicks-1 { 186 | tens /= 10 187 | n = (max - min) / tens 188 | } 189 | 190 | majorMult := int(n / (suggestedTicks - 1)) 191 | switch majorMult { 192 | case 7: 193 | majorMult = 6 194 | case 9: 195 | majorMult = 8 196 | } 197 | majorDelta := float64(majorMult) * tens 198 | val := math.Floor(min/majorDelta) * majorDelta 199 | // Makes a list of non-truncated y-values. 200 | var labels []float64 201 | for val <= max { 202 | if val >= min { 203 | labels = append(labels, val) 204 | } 205 | val += majorDelta 206 | } 207 | prec := int(math.Ceil(math.Log10(val)) - math.Floor(math.Log10(majorDelta))) 208 | // Makes a list of big ticks. 209 | var ticks []plot.Tick 210 | for _, v := range labels { 211 | vRounded := round(v, prec) 212 | ticks = append(ticks, plot.Tick{Value: vRounded, Label: strconv.FormatFloat(vRounded, 'f', -1, 64)}) 213 | } 214 | minorDelta := majorDelta / 2 215 | switch majorMult { 216 | case 3, 6: 217 | minorDelta = majorDelta / 3 218 | case 5: 219 | minorDelta = majorDelta / 5 220 | } 221 | 222 | val = math.Floor(min/minorDelta) * minorDelta 223 | for val <= max { 224 | found := false 225 | for _, t := range ticks { 226 | if t.Value == val { 227 | found = true 228 | } 229 | } 230 | if val >= min && val <= max && !found { 231 | ticks = append(ticks, plot.Tick{Value: val}) 232 | } 233 | val += minorDelta 234 | } 235 | return ticks 236 | } 237 | 238 | type commaTicks struct{} 239 | 240 | // Ticks computes the default tick marks, but inserts commas 241 | // into the labels for the major tick marks. 242 | func (commaTicks) Ticks(min, max float64) []plot.Tick { 243 | tks := PreciseTicks{}.Ticks(min, max) 244 | for i, t := range tks { 245 | if t.Label == "" { // Skip minor ticks, they are fine. 246 | continue 247 | } 248 | tks[i].Label = addCommas(t.Label) 249 | } 250 | return tks 251 | } 252 | 253 | // AddCommas adds commas after every 3 characters from right to left. 254 | // NOTE: This function is a quick hack, it doesn't work with decimal 255 | // points, and may have a bunch of other problems. 256 | func addCommas(s string) string { 257 | rev := "" 258 | n := 0 259 | for i := len(s) - 1; i >= 0; i-- { 260 | rev += string(s[i]) 261 | n++ 262 | if n%3 == 0 { 263 | rev += "," 264 | } 265 | } 266 | s = "" 267 | for i := len(rev) - 1; i >= 0; i-- { 268 | s += string(rev[i]) 269 | } 270 | if strings.HasPrefix(s, ",") { 271 | s = s[1:] 272 | } 273 | return s 274 | } 275 | 276 | // round returns the half away from zero rounded value of x with a prec precision. 277 | // 278 | // Special cases are: 279 | // round(±0) = +0 280 | // round(±Inf) = ±Inf 281 | // round(NaN) = NaN 282 | func round(x float64, prec int) float64 { 283 | if x == 0 { 284 | // Make sure zero is returned 285 | // without the negative bit set. 286 | return 0 287 | } 288 | // Fast path for positive precision on integers. 289 | if prec >= 0 && x == math.Trunc(x) { 290 | return x 291 | } 292 | pow := math.Pow10(prec) 293 | intermed := x * pow 294 | if math.IsInf(intermed, 0) { 295 | return x 296 | } 297 | if x < 0 { 298 | x = math.Ceil(intermed - 0.5) 299 | } else { 300 | x = math.Floor(intermed + 0.5) 301 | } 302 | 303 | if x == 0 { 304 | return 0 305 | } 306 | 307 | return x / pow 308 | } 309 | -------------------------------------------------------------------------------- /benchmarks/bench-echo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | echo "" 6 | echo "--- BENCH ECHO START ---" 7 | echo "" 8 | 9 | cd $(dirname "${BASH_SOURCE[0]}") 10 | function cleanup { 11 | echo "--- BENCH ECHO DONE ---" 12 | kill -9 $(jobs -rp) 13 | wait $(jobs -rp) 2>/dev/null 14 | } 15 | trap cleanup EXIT 16 | 17 | mkdir -p bin 18 | $(pkill -9 net-echo-server || printf "") 19 | $(pkill -9 evio-echo-server || printf "") 20 | 21 | function gobench { 22 | echo "--- $1 ---" 23 | if [ "$3" != "" ]; then 24 | go build -o $2 $3 25 | fi 26 | GOMAXPROCS=1 $2 --port $4 & 27 | sleep 1 28 | echo "*** 50 connections, 10 seconds, 6 byte packets" 29 | nl=#39;\r\n' 30 | tcpkali --workers 1 -c 50 -T 10s -m "PING{$nl}" 127.0.0.1:$4 31 | echo "--- DONE ---" 32 | echo "" 33 | } 34 | 35 | gobench "GO STDLIB" bin/net-echo-server net-echo-server/main.go 5001 36 | gobench "EVIO" bin/evio-echo-server ../examples/echo-server/main.go 5002 37 | -------------------------------------------------------------------------------- /benchmarks/bench-http.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | echo "" 6 | echo "--- BENCH HTTP START ---" 7 | echo "" 8 | 9 | cd $(dirname "${BASH_SOURCE[0]}") 10 | function cleanup { 11 | echo "--- BENCH HTTP DONE ---" 12 | kill -9 $(jobs -rp) 13 | wait $(jobs -rp) 2>/dev/null 14 | } 15 | trap cleanup EXIT 16 | 17 | mkdir -p bin 18 | $(pkill -9 net-http-server || printf "") 19 | $(pkill -9 fasthttp-server || printf "") 20 | $(pkill -9 evio-http-server || printf "") 21 | 22 | function gobench { 23 | echo "--- $1 ---" 24 | if [ "$3" != "" ]; then 25 | go build -o $2 $3 26 | fi 27 | GOMAXPROCS=1 $2 --port $4 & 28 | sleep 1 29 | echo "*** 50 connections, 10 seconds" 30 | bombardier -c 50 http://127.0.0.1:$4 31 | echo "--- DONE ---" 32 | echo "" 33 | } 34 | 35 | gobench "GO STDLIB" bin/net-http-server net-http-server/main.go 8081 36 | gobench "FASTHTTP" bin/fasthttp-server fasthttp-server/main.go 8083 37 | gobench "EVIO" bin/evio-http-server ../examples/http-server/main.go 8084 38 | -------------------------------------------------------------------------------- /benchmarks/bench-redis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | pl=$1 6 | if [ "$pl" == "" ]; then 7 | pl="1" 8 | fi 9 | 10 | echo "" 11 | echo "--- BENCH REDIS PIPELINE $pl START ---" 12 | echo "" 13 | 14 | cd $(dirname "${BASH_SOURCE[0]}") 15 | function cleanup { 16 | echo "--- BENCH REDIS PIPELINE $pl DONE ---" 17 | kill -9 $(jobs -rp) 18 | wait $(jobs -rp) 2>/dev/null 19 | } 20 | trap cleanup EXIT 21 | 22 | mkdir -p bin 23 | $(pkill -9 redis-server || printf "") 24 | $(pkill -9 evio-redis-server || printf "") 25 | 26 | function gobench { 27 | echo "--- $1 ---" 28 | if [ "$3" != "" ]; then 29 | go build -o $2 $3 30 | fi 31 | GOMAXPROCS=1 $2 --port $4 & 32 | sleep 1 33 | echo "*** 50 connections, 1000000 commands, $pl commands pipeline" 34 | redis-benchmark -p $4 -t ping_inline -q -c 50 -P $pl -n 1000000 35 | # echo "*** 50 connections, 1000000 commands, 10 commands pipeline" 36 | # redis-benchmark -p $4 -t ping_inline -q -c 50 -P 10 -n 1000000 37 | # echo "*** 50 connections, 1000000 commands, 20 commands pipeline" 38 | # redis-benchmark -p $4 -t ping_inline -q -c 50 -P 20 -n 1000000 39 | echo "--- DONE ---" 40 | echo "" 41 | } 42 | gobench "REAL REDIS" redis-server "" 6392 43 | gobench "EVIO REDIS CLONE" bin/evio-redis-server ../examples/redis-server/main.go 6393 44 | -------------------------------------------------------------------------------- /benchmarks/bench.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | cd $(dirname "${BASH_SOURCE[0]}") 6 | 7 | mkdir -p out/ 8 | 9 | ./bench-http.sh 2>&1 | tee out/http.txt 10 | ./bench-echo.sh 2>&1 | tee out/echo.txt 11 | ./bench-redis.sh 1 2>&1 | tee out/redis1.txt 12 | ./bench-redis.sh 8 2>&1 | tee out/redis8.txt 13 | ./bench-redis.sh 16 2>&1 | tee out/redis16.txt 14 | 15 | go run analyze.go -------------------------------------------------------------------------------- /benchmarks/fasthttp-server/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Joshua J Baker. All rights reserved. 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "flag" 9 | "fmt" 10 | "log" 11 | 12 | "github.com/valyala/fasthttp" 13 | ) 14 | 15 | var res string 16 | 17 | func main() { 18 | var port int 19 | flag.IntVar(&port, "port", 8080, "server port") 20 | flag.Parse() 21 | go log.Printf("http server started on port %d", port) 22 | err := fasthttp.ListenAndServe(fmt.Sprintf(":%d", port), 23 | func(c *fasthttp.RequestCtx) { 24 | _, werr := c.WriteString("Hello World!\r\n") 25 | if werr != nil { 26 | log.Fatal(werr) 27 | } 28 | }) 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /benchmarks/net-echo-server/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Joshua J Baker. All rights reserved. 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "flag" 9 | "fmt" 10 | "log" 11 | "net" 12 | ) 13 | 14 | func main() { 15 | var port int 16 | flag.IntVar(&port, "port", 5000, "server port") 17 | flag.Parse() 18 | ln, err := net.Listen("tcp4", fmt.Sprintf(":%d", port)) 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | defer ln.Close() 23 | log.Printf("echo server started on port %d", port) 24 | var id int 25 | for { 26 | conn, err := ln.Accept() 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | id++ 31 | go func(id int, conn net.Conn) { 32 | defer func() { 33 | //log.Printf("closed: %d", id) 34 | conn.Close() 35 | }() 36 | //log.Printf("opened: %d: %s", id, conn.RemoteAddr().String()) 37 | var packet [0xFFF]byte 38 | for { 39 | n, err := conn.Read(packet[:]) 40 | if err != nil { 41 | return 42 | } 43 | conn.Write(packet[:n]) 44 | } 45 | }(id, conn) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /benchmarks/net-http-server/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Joshua J Baker. All rights reserved. 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "flag" 9 | "fmt" 10 | "log" 11 | "net/http" 12 | "strings" 13 | ) 14 | 15 | var res string 16 | 17 | func main() { 18 | var port int 19 | var aaaa bool 20 | flag.IntVar(&port, "port", 8080, "server port") 21 | flag.BoolVar(&aaaa, "aaaa", false, "aaaaa....") 22 | flag.Parse() 23 | if aaaa { 24 | res = strings.Repeat("a", 1024) 25 | } else { 26 | res = "Hello World!\r\n" 27 | } 28 | log.Printf("http server started on port %d", port) 29 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 30 | w.Write([]byte(res)) 31 | }) 32 | err := http.ListenAndServe(fmt.Sprintf(":%d", port), nil) 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /benchmarks/out/echo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidwall/evio/fe6081760191618105c0671e6aa05f06e7c6e5a9/benchmarks/out/echo.png -------------------------------------------------------------------------------- /benchmarks/out/echo.txt: -------------------------------------------------------------------------------- 1 | 2 | --- BENCH ECHO START --- 3 | 4 | --- GO STDLIB --- 5 | 2017/11/04 13:13:38 echo server started on port 5001 6 | *** 50 connections, 10 seconds, 6 byte packets 7 | Destination: [127.0.0.1]:5001 8 | Interface lo address [127.0.0.1]:0 9 | Using interface lo to connect to [127.0.0.1]:5001 10 | Ramped up to 50 connections. 11 | Total data sent: 9165.8 MiB (9610985472 bytes) 12 | Total data received: 8951.1 MiB (9385891515 bytes) 13 | Bandwidth per channel: 303.867⇅ Mbps (37983.4 kBps) 14 | Aggregate bandwidth: 7506.663↓, 7686.689↑ Mbps 15 | Packet rate estimate: 732150.4↓, 659753.8↑ (6↓, 45↑ TCP MSS/op) 16 | Test duration: 10.0027 s. 17 | --- DONE --- 18 | 19 | --- EVIO --- 20 | 2017/11/04 13:13:50 echo server started on port 5002 21 | *** 50 connections, 10 seconds, 6 byte packets 22 | Destination: [127.0.0.1]:5002 23 | Interface lo address [127.0.0.1]:0 24 | Using interface lo to connect to [127.0.0.1]:5002 25 | Ramped up to 50 connections. 26 | Total data sent: 15441.1 MiB (16191127552 bytes) 27 | Total data received: 15430.5 MiB (16180050837 bytes) 28 | Bandwidth per channel: 517.825⇅ Mbps (64728.2 kBps) 29 | Aggregate bandwidth: 12941.205↓, 12950.064↑ Mbps 30 | Packet rate estimate: 1184847.1↓, 1111512.9↑ (12↓, 45↑ TCP MSS/op) 31 | Test duration: 10.0022 s. 32 | --- DONE --- 33 | 34 | --- BENCH ECHO DONE --- 35 | -------------------------------------------------------------------------------- /benchmarks/out/http.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidwall/evio/fe6081760191618105c0671e6aa05f06e7c6e5a9/benchmarks/out/http.png -------------------------------------------------------------------------------- /benchmarks/out/http.txt: -------------------------------------------------------------------------------- 1 | 2 | --- BENCH HTTP START --- 3 | 4 | --- GO STDLIB --- 5 | 2017/11/06 11:43:15 http server started on port 8081 6 | *** 50 connections, 10 seconds 7 | Bombarding http://127.0.0.1:8081 for 10s using 50 connections 8 | [------------------------------------------------------------------------------] [=======>-------------------------------------------------------------------] 9s [==============>------------------------------------------------------------] 8s [======================>----------------------------------------------------] 7s [=============================>---------------------------------------------] 6s [=====================================>-------------------------------------] 5s [============================================>------------------------------] 4s [====================================================>----------------------] 3s [===========================================================>---------------] 2s [===================================================================>-------] 1s [===========================================================================] 0s [==========================================================================] 10s 9 | Done! 10 | Statistics Avg Stdev Max 11 | Reqs/sec 42487.26 9452.41 53042 12 | Latency 1.17ms 742.47us 12.53ms 13 | HTTP codes: 14 | 1xx - 0, 2xx - 424966, 3xx - 0, 4xx - 0, 5xx - 0 15 | others - 0 16 | Throughput: 7.82MB/s 17 | --- DONE --- 18 | 19 | --- FASTHTTP --- 20 | 2017/11/06 11:43:27 http server started on port 8083 21 | *** 50 connections, 10 seconds 22 | Bombarding http://127.0.0.1:8083 for 10s using 50 connections 23 | [------------------------------------------------------------------------------] [=======>-------------------------------------------------------------------] 9s [==============>------------------------------------------------------------] 8s [======================>----------------------------------------------------] 7s [=============================>---------------------------------------------] 6s [=====================================>-------------------------------------] 5s [============================================>------------------------------] 4s [====================================================>----------------------] 3s [===========================================================>---------------] 2s [===================================================================>-------] 1s [===========================================================================] 0s [==========================================================================] 10s 24 | Done! 25 | Statistics Avg Stdev Max 26 | Reqs/sec 104926.32 2744.15 117354 27 | Latency 474.64us 255.41us 11.06ms 28 | HTTP codes: 29 | 1xx - 0, 2xx - 1049311, 3xx - 0, 4xx - 0, 5xx - 0 30 | others - 0 31 | Throughput: 21.11MB/s 32 | --- DONE --- 33 | 34 | --- EVIO --- 35 | 2017/11/06 11:43:38 http server started on port 8084 36 | *** 50 connections, 10 seconds 37 | Bombarding http://127.0.0.1:8084 for 10s using 50 connections 38 | [------------------------------------------------------------------------------] [=======>-------------------------------------------------------------------] 9s [==============>------------------------------------------------------------] 8s [======================>----------------------------------------------------] 7s [=============================>---------------------------------------------] 6s [=====================================>-------------------------------------] 5s [============================================>------------------------------] 4s [====================================================>----------------------] 3s [===========================================================>---------------] 2s [===================================================================>-------] 1s [===========================================================================] 0s [==========================================================================] 10s 39 | Done! 40 | Statistics Avg Stdev Max 41 | Reqs/sec 123821.87 2821.88 130897 42 | Latency 401.99us 121.11us 12.88ms 43 | HTTP codes: 44 | 1xx - 0, 2xx - 1238166, 3xx - 0, 4xx - 0, 5xx - 0 45 | others - 0 46 | Throughput: 19.60MB/s 47 | --- DONE --- 48 | 49 | --- BENCH HTTP DONE --- 50 | -------------------------------------------------------------------------------- /benchmarks/out/redis1.txt: -------------------------------------------------------------------------------- 1 | 2 | --- BENCH REDIS PIPELINE 1 START --- 3 | 4 | --- REAL REDIS --- 5 | 31889:C 04 Nov 13:14:02.373 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo 6 | 31889:C 04 Nov 13:14:02.373 # Redis version=4.0.2, bits=64, commit=00000000, modified=0, pid=31889, just started 7 | 31889:C 04 Nov 13:14:02.373 # Configuration loaded 8 | 31889:M 04 Nov 13:14:02.374 * Increased maximum number of open files to 10032 (it was originally set to 1024). 9 | 31889:M 04 Nov 13:14:02.374 * Running mode=standalone, port=6392. 10 | 31889:M 04 Nov 13:14:02.374 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128. 11 | 31889:M 04 Nov 13:14:02.374 # Server initialized 12 | 31889:M 04 Nov 13:14:02.374 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect. 13 | 31889:M 04 Nov 13:14:02.374 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled. 14 | 31889:M 04 Nov 13:14:02.374 * Ready to accept connections 15 | *** 50 connections, 1000000 commands, 1 commands pipeline 16 | PING_INLINE: -nan PING_INLINE: 171620.00 PING_INLINE: 175064.00 PING_INLINE: 175986.67 PING_INLINE: 176586.00 PING_INLINE: 176886.41 PING_INLINE: 177081.33 PING_INLINE: 177301.14 PING_INLINE: 177444.50 PING_INLINE: 177331.11 PING_INLINE: 177247.20 PING_INLINE: 177178.91 PING_INLINE: 177169.00 PING_INLINE: 177084.31 PING_INLINE: 177083.72 PING_INLINE: 177058.67 PING_INLINE: 177036.50 PING_INLINE: 177041.17 PING_INLINE: 177017.11 PING_INLINE: 177013.69 PING_INLINE: 177009.80 PING_INLINE: 177003.81 PING_INLINE: 177004.36 PING_INLINE: 176991.14 requests per second 17 | 18 | --- DONE --- 19 | 20 | --- EVIO REDIS CLONE --- 21 | 2017/11/04 13:14:09 redis server started on port 6393 22 | 2017/11/04 13:14:09 redis server started at socket 23 | *** 50 connections, 1000000 commands, 1 commands pipeline 24 | PING_INLINE: -nan PING_INLINE: 167180.00 PING_INLINE: 173258.00 PING_INLINE: 175005.33 PING_INLINE: 176102.00 PING_INLINE: 176358.41 PING_INLINE: 176593.33 PING_INLINE: 176877.72 PING_INLINE: 177103.00 PING_INLINE: 177186.67 PING_INLINE: 177269.59 PING_INLINE: 177322.19 PING_INLINE: 177363.67 PING_INLINE: 177420.00 PING_INLINE: 177448.86 PING_INLINE: 177411.73 PING_INLINE: 177371.75 PING_INLINE: 177334.59 PING_INLINE: 177310.44 PING_INLINE: 177264.62 PING_INLINE: 177205.00 PING_INLINE: 177171.62 PING_INLINE: 177173.64 PING_INLINE: 177147.92 requests per second 25 | 26 | --- DONE --- 27 | 28 | --- BENCH REDIS PIPELINE 1 DONE --- 29 | -------------------------------------------------------------------------------- /benchmarks/out/redis16.txt: -------------------------------------------------------------------------------- 1 | 2 | --- BENCH REDIS PIPELINE 16 START --- 3 | 4 | --- REAL REDIS --- 5 | 32002:C 04 Nov 13:14:20.410 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo 6 | 32002:C 04 Nov 13:14:20.410 # Redis version=4.0.2, bits=64, commit=00000000, modified=0, pid=32002, just started 7 | 32002:C 04 Nov 13:14:20.410 # Configuration loaded 8 | 32002:M 04 Nov 13:14:20.411 * Increased maximum number of open files to 10032 (it was originally set to 1024). 9 | 32002:M 04 Nov 13:14:20.411 * Running mode=standalone, port=6392. 10 | 32002:M 04 Nov 13:14:20.412 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128. 11 | 32002:M 04 Nov 13:14:20.412 # Server initialized 12 | 32002:M 04 Nov 13:14:20.412 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect. 13 | 32002:M 04 Nov 13:14:20.412 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled. 14 | 32002:M 04 Nov 13:14:20.412 * Ready to accept connections 15 | *** 50 connections, 1000000 commands, 16 commands pipeline 16 | PING_INLINE: 0.00 PING_INLINE: 874135.50 PING_INLINE: 877221.56 PING_INLINE: 877315.62 PING_INLINE: 877810.12 PING_INLINE: 879507.50 requests per second 17 | 18 | --- DONE --- 19 | 20 | --- EVIO REDIS CLONE --- 21 | 2017/11/04 13:14:22 redis server started on port 6393 22 | 2017/11/04 13:14:22 redis server started at socket 23 | *** 50 connections, 1000000 commands, 16 commands pipeline 24 | PING_INLINE: -nan PING_INLINE: 2127552.00 PING_INLINE: 2123142.25 requests per second 25 | 26 | --- DONE --- 27 | 28 | --- BENCH REDIS PIPELINE 16 DONE --- 29 | -------------------------------------------------------------------------------- /benchmarks/out/redis8.txt: -------------------------------------------------------------------------------- 1 | 2 | --- BENCH REDIS PIPELINE 8 START --- 3 | 4 | --- REAL REDIS --- 5 | 31946:C 04 Nov 13:14:16.084 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo 6 | 31946:C 04 Nov 13:14:16.084 # Redis version=4.0.2, bits=64, commit=00000000, modified=0, pid=31946, just started 7 | 31946:C 04 Nov 13:14:16.084 # Configuration loaded 8 | 31946:M 04 Nov 13:14:16.084 * Increased maximum number of open files to 10032 (it was originally set to 1024). 9 | 31946:M 04 Nov 13:14:16.085 * Running mode=standalone, port=6392. 10 | 31946:M 04 Nov 13:14:16.085 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128. 11 | 31946:M 04 Nov 13:14:16.085 # Server initialized 12 | 31946:M 04 Nov 13:14:16.085 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect. 13 | 31946:M 04 Nov 13:14:16.085 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled. 14 | 31946:M 04 Nov 13:14:16.085 * Ready to accept connections 15 | *** 50 connections, 1000000 commands, 8 commands pipeline 16 | PING_INLINE: -nan PING_INLINE: 823072.00 PING_INLINE: 859168.00 PING_INLINE: 870933.31 PING_INLINE: 876680.00 PING_INLINE: 878734.62 requests per second 17 | 18 | --- DONE --- 19 | 20 | --- EVIO REDIS CLONE --- 21 | 2017/11/04 13:14:18 redis server started on port 6393 22 | 2017/11/04 13:14:18 redis server started at socket 23 | *** 50 connections, 1000000 commands, 8 commands pipeline 24 | PING_INLINE: -nan PING_INLINE: 1284896.00 PING_INLINE: 1284144.00 PING_INLINE: 1285141.38 PING_INLINE: 1285347.00 requests per second 25 | 26 | --- DONE --- 27 | 28 | --- BENCH REDIS PIPELINE 8 DONE --- 29 | -------------------------------------------------------------------------------- /benchmarks/out/redis_pipeline_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidwall/evio/fe6081760191618105c0671e6aa05f06e7c6e5a9/benchmarks/out/redis_pipeline_1.png -------------------------------------------------------------------------------- /benchmarks/out/redis_pipeline_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidwall/evio/fe6081760191618105c0671e6aa05f06e7c6e5a9/benchmarks/out/redis_pipeline_16.png -------------------------------------------------------------------------------- /benchmarks/out/redis_pipeline_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidwall/evio/fe6081760191618105c0671e6aa05f06e7c6e5a9/benchmarks/out/redis_pipeline_8.png -------------------------------------------------------------------------------- /evio.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Joshua J Baker. All rights reserved. 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package evio 6 | 7 | import ( 8 | "io" 9 | "net" 10 | "os" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | // Action is an action that occurs after the completion of an event. 16 | type Action int 17 | 18 | const ( 19 | // None indicates that no action should occur following an event. 20 | None Action = iota 21 | // Detach detaches a connection. Not available for UDP connections. 22 | Detach 23 | // Close closes the connection. 24 | Close 25 | // Shutdown shutdowns the server. 26 | Shutdown 27 | ) 28 | 29 | // Options are set when the client opens. 30 | type Options struct { 31 | // TCPKeepAlive (SO_KEEPALIVE) socket option. 32 | TCPKeepAlive time.Duration 33 | // ReuseInputBuffer will forces the connection to share and reuse the 34 | // same input packet buffer with all other connections that also use 35 | // this option. 36 | // Default value is false, which means that all input data which is 37 | // passed to the Data event will be a uniquely copied []byte slice. 38 | ReuseInputBuffer bool 39 | } 40 | 41 | // Server represents a server context which provides information about the 42 | // running server and has control functions for managing state. 43 | type Server struct { 44 | // The addrs parameter is an array of listening addresses that align 45 | // with the addr strings passed to the Serve function. 46 | Addrs []net.Addr 47 | // NumLoops is the number of loops that the server is using. 48 | NumLoops int 49 | } 50 | 51 | // Conn is an evio connection. 52 | type Conn interface { 53 | // Context returns a user-defined context. 54 | Context() interface{} 55 | // SetContext sets a user-defined context. 56 | SetContext(interface{}) 57 | // AddrIndex is the index of server address that was passed to the Serve call. 58 | AddrIndex() int 59 | // LocalAddr is the connection's local socket address. 60 | LocalAddr() net.Addr 61 | // RemoteAddr is the connection's remote peer address. 62 | RemoteAddr() net.Addr 63 | // Wake triggers a Data event for this connection. 64 | Wake() 65 | } 66 | 67 | // LoadBalance sets the load balancing method. 68 | type LoadBalance int 69 | 70 | const ( 71 | // Random requests that connections are randomly distributed. 72 | Random LoadBalance = iota 73 | // RoundRobin requests that connections are distributed to a loop in a 74 | // round-robin fashion. 75 | RoundRobin 76 | // LeastConnections assigns the next accepted connection to the loop with 77 | // the least number of active connections. 78 | LeastConnections 79 | ) 80 | 81 | // Events represents the server events for the Serve call. 82 | // Each event has an Action return value that is used manage the state 83 | // of the connection and server. 84 | type Events struct { 85 | // NumLoops sets the number of loops to use for the server. Setting this 86 | // to a value greater than 1 will effectively make the server 87 | // multithreaded for multi-core machines. Which means you must take care 88 | // with synchonizing memory between all event callbacks. Setting to 0 or 1 89 | // will run the server single-threaded. Setting to -1 will automatically 90 | // assign this value equal to runtime.NumProcs(). 91 | NumLoops int 92 | // LoadBalance sets the load balancing method. Load balancing is always a 93 | // best effort to attempt to distribute the incoming connections between 94 | // multiple loops. This option is only works when NumLoops is set. 95 | LoadBalance LoadBalance 96 | // Serving fires when the server can accept connections. The server 97 | // parameter has information and various utilities. 98 | Serving func(server Server) (action Action) 99 | // Opened fires when a new connection has opened. 100 | // The info parameter has information about the connection such as 101 | // it's local and remote address. 102 | // Use the out return value to write data to the connection. 103 | // The opts return value is used to set connection options. 104 | Opened func(c Conn) (out []byte, opts Options, action Action) 105 | // Closed fires when a connection has closed. 106 | // The err parameter is the last known connection error. 107 | Closed func(c Conn, err error) (action Action) 108 | // Detached fires when a connection has been previously detached. 109 | // Once detached it's up to the receiver of this event to manage the 110 | // state of the connection. The Closed event will not be called for 111 | // this connection. 112 | // The conn parameter is a ReadWriteCloser that represents the 113 | // underlying socket connection. It can be freely used in goroutines 114 | // and should be closed when it's no longer needed. 115 | Detached func(c Conn, rwc io.ReadWriteCloser) (action Action) 116 | // PreWrite fires just before any data is written to any client socket. 117 | PreWrite func() 118 | // Data fires when a connection sends the server data. 119 | // The in parameter is the incoming data. 120 | // Use the out return value to write data to the connection. 121 | Data func(c Conn, in []byte) (out []byte, action Action) 122 | // Tick fires immediately after the server starts and will fire again 123 | // following the duration specified by the delay return value. 124 | Tick func() (delay time.Duration, action Action) 125 | } 126 | 127 | // Serve starts handling events for the specified addresses. 128 | // 129 | // Addresses should use a scheme prefix and be formatted 130 | // like `tcp://192.168.0.10:9851` or `unix://socket`. 131 | // Valid network schemes: 132 | // tcp - bind to both IPv4 and IPv6 133 | // tcp4 - IPv4 134 | // tcp6 - IPv6 135 | // udp - bind to both IPv4 and IPv6 136 | // udp4 - IPv4 137 | // udp6 - IPv6 138 | // unix - Unix Domain Socket 139 | // 140 | // The "tcp" network scheme is assumed when one is not specified. 141 | func Serve(events Events, addr ...string) error { 142 | var lns []*listener 143 | defer func() { 144 | for _, ln := range lns { 145 | ln.close() 146 | } 147 | }() 148 | var stdlib bool 149 | for _, addr := range addr { 150 | var ln listener 151 | var stdlibt bool 152 | ln.network, ln.addr, ln.opts, stdlibt = parseAddr(addr) 153 | if stdlibt { 154 | stdlib = true 155 | } 156 | if ln.network == "unix" { 157 | os.RemoveAll(ln.addr) 158 | } 159 | var err error 160 | if ln.network == "udp" { 161 | if ln.opts.reusePort { 162 | ln.pconn, err = reuseportListenPacket(ln.network, ln.addr) 163 | } else { 164 | ln.pconn, err = net.ListenPacket(ln.network, ln.addr) 165 | } 166 | } else { 167 | if ln.opts.reusePort { 168 | ln.ln, err = reuseportListen(ln.network, ln.addr) 169 | } else { 170 | ln.ln, err = net.Listen(ln.network, ln.addr) 171 | } 172 | } 173 | if err != nil { 174 | return err 175 | } 176 | if ln.pconn != nil { 177 | ln.lnaddr = ln.pconn.LocalAddr() 178 | } else { 179 | ln.lnaddr = ln.ln.Addr() 180 | } 181 | if !stdlib { 182 | if err := ln.system(); err != nil { 183 | return err 184 | } 185 | } 186 | lns = append(lns, &ln) 187 | } 188 | if stdlib { 189 | return stdserve(events, lns) 190 | } 191 | return serve(events, lns) 192 | } 193 | 194 | // InputStream is a helper type for managing input streams from inside 195 | // the Data event. 196 | type InputStream struct{ b []byte } 197 | 198 | // Begin accepts a new packet and returns a working sequence of 199 | // unprocessed bytes. 200 | func (is *InputStream) Begin(packet []byte) (data []byte) { 201 | data = packet 202 | if len(is.b) > 0 { 203 | is.b = append(is.b, data...) 204 | data = is.b 205 | } 206 | return data 207 | } 208 | 209 | // End shifts the stream to match the unprocessed data. 210 | func (is *InputStream) End(data []byte) { 211 | if len(data) > 0 { 212 | if len(data) != len(is.b) { 213 | is.b = append(is.b[:0], data...) 214 | } 215 | } else if len(is.b) > 0 { 216 | is.b = is.b[:0] 217 | } 218 | } 219 | 220 | type listener struct { 221 | ln net.Listener 222 | lnaddr net.Addr 223 | pconn net.PacketConn 224 | opts addrOpts 225 | f *os.File 226 | fd int 227 | network string 228 | addr string 229 | } 230 | 231 | type addrOpts struct { 232 | reusePort bool 233 | } 234 | 235 | func parseAddr(addr string) (network, address string, opts addrOpts, stdlib bool) { 236 | network = "tcp" 237 | address = addr 238 | opts.reusePort = false 239 | if strings.Contains(address, "://") { 240 | network = strings.Split(address, "://")[0] 241 | address = strings.Split(address, "://")[1] 242 | } 243 | if strings.HasSuffix(network, "-net") { 244 | stdlib = true 245 | network = network[:len(network)-4] 246 | } 247 | q := strings.Index(address, "?") 248 | if q != -1 { 249 | for _, part := range strings.Split(address[q+1:], "&") { 250 | kv := strings.Split(part, "=") 251 | if len(kv) == 2 { 252 | switch kv[0] { 253 | case "reuseport": 254 | if len(kv[1]) != 0 { 255 | switch kv[1][0] { 256 | default: 257 | opts.reusePort = kv[1][0] >= '1' && kv[1][0] <= '9' 258 | case 'T', 't', 'Y', 'y': 259 | opts.reusePort = true 260 | } 261 | } 262 | } 263 | } 264 | } 265 | address = address[:q] 266 | } 267 | return 268 | } 269 | -------------------------------------------------------------------------------- /evio_other.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Joshua J Baker. All rights reserved. 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build !darwin,!netbsd,!freebsd,!openbsd,!dragonfly,!linux 6 | 7 | package evio 8 | 9 | import ( 10 | "errors" 11 | "net" 12 | "os" 13 | ) 14 | 15 | func (ln *listener) close() { 16 | if ln.ln != nil { 17 | ln.ln.Close() 18 | } 19 | if ln.pconn != nil { 20 | ln.pconn.Close() 21 | } 22 | if ln.network == "unix" { 23 | os.RemoveAll(ln.addr) 24 | } 25 | } 26 | 27 | func (ln *listener) system() error { 28 | return nil 29 | } 30 | 31 | func serve(events Events, listeners []*listener) error { 32 | return stdserve(events, listeners) 33 | } 34 | 35 | func reuseportListenPacket(proto, addr string) (l net.PacketConn, err error) { 36 | return nil, errors.New("reuseport is not available") 37 | } 38 | 39 | func reuseportListen(proto, addr string) (l net.Listener, err error) { 40 | return nil, errors.New("reuseport is not available") 41 | } 42 | -------------------------------------------------------------------------------- /evio_std.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Joshua J Baker. All rights reserved. 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package evio 6 | 7 | import ( 8 | "errors" 9 | "io" 10 | "net" 11 | "runtime" 12 | "sync" 13 | "sync/atomic" 14 | "time" 15 | ) 16 | 17 | var errClosing = errors.New("closing") 18 | var errCloseConns = errors.New("close conns") 19 | 20 | type stdserver struct { 21 | events Events // user events 22 | loops []*stdloop // all the loops 23 | lns []*listener // all the listeners 24 | loopwg sync.WaitGroup // loop close waitgroup 25 | lnwg sync.WaitGroup // listener close waitgroup 26 | cond *sync.Cond // shutdown signaler 27 | serr error // signal error 28 | accepted uintptr // accept counter 29 | } 30 | 31 | type stdudpconn struct { 32 | addrIndex int 33 | localAddr net.Addr 34 | remoteAddr net.Addr 35 | in []byte 36 | } 37 | 38 | func (c *stdudpconn) Context() interface{} { return nil } 39 | func (c *stdudpconn) SetContext(ctx interface{}) {} 40 | func (c *stdudpconn) AddrIndex() int { return c.addrIndex } 41 | func (c *stdudpconn) LocalAddr() net.Addr { return c.localAddr } 42 | func (c *stdudpconn) RemoteAddr() net.Addr { return c.remoteAddr } 43 | func (c *stdudpconn) Wake() {} 44 | 45 | type stdloop struct { 46 | idx int // loop index 47 | ch chan interface{} // command channel 48 | conns map[*stdconn]bool // track all the conns bound to this loop 49 | } 50 | 51 | type stdconn struct { 52 | addrIndex int 53 | localAddr net.Addr 54 | remoteAddr net.Addr 55 | conn net.Conn // original connection 56 | ctx interface{} // user-defined context 57 | loop *stdloop // owner loop 58 | lnidx int // index of listener 59 | donein []byte // extra data for done connection 60 | done int32 // 0: attached, 1: closed, 2: detached 61 | } 62 | 63 | type wakeReq struct { 64 | c *stdconn 65 | } 66 | 67 | func (c *stdconn) Context() interface{} { return c.ctx } 68 | func (c *stdconn) SetContext(ctx interface{}) { c.ctx = ctx } 69 | func (c *stdconn) AddrIndex() int { return c.addrIndex } 70 | func (c *stdconn) LocalAddr() net.Addr { return c.localAddr } 71 | func (c *stdconn) RemoteAddr() net.Addr { return c.remoteAddr } 72 | func (c *stdconn) Wake() { c.loop.ch <- wakeReq{c} } 73 | 74 | type stdin struct { 75 | c *stdconn 76 | in []byte 77 | } 78 | 79 | type stderr struct { 80 | c *stdconn 81 | err error 82 | } 83 | 84 | // waitForShutdown waits for a signal to shutdown 85 | func (s *stdserver) waitForShutdown() error { 86 | s.cond.L.Lock() 87 | s.cond.Wait() 88 | err := s.serr 89 | s.cond.L.Unlock() 90 | return err 91 | } 92 | 93 | // signalShutdown signals a shutdown an begins server closing 94 | func (s *stdserver) signalShutdown(err error) { 95 | s.cond.L.Lock() 96 | s.serr = err 97 | s.cond.Signal() 98 | s.cond.L.Unlock() 99 | } 100 | 101 | func stdserve(events Events, listeners []*listener) error { 102 | numLoops := events.NumLoops 103 | if numLoops <= 0 { 104 | if numLoops == 0 { 105 | numLoops = 1 106 | } else { 107 | numLoops = runtime.NumCPU() 108 | } 109 | } 110 | 111 | s := &stdserver{} 112 | s.events = events 113 | s.lns = listeners 114 | s.cond = sync.NewCond(&sync.Mutex{}) 115 | 116 | //println("-- server starting") 117 | if events.Serving != nil { 118 | var svr Server 119 | svr.NumLoops = numLoops 120 | svr.Addrs = make([]net.Addr, len(listeners)) 121 | for i, ln := range listeners { 122 | svr.Addrs[i] = ln.lnaddr 123 | } 124 | action := events.Serving(svr) 125 | switch action { 126 | case Shutdown: 127 | return nil 128 | } 129 | } 130 | for i := 0; i < numLoops; i++ { 131 | s.loops = append(s.loops, &stdloop{ 132 | idx: i, 133 | ch: make(chan interface{}), 134 | conns: make(map[*stdconn]bool), 135 | }) 136 | } 137 | var ferr error 138 | defer func() { 139 | // wait on a signal for shutdown 140 | ferr = s.waitForShutdown() 141 | 142 | // notify all loops to close by closing all listeners 143 | for _, l := range s.loops { 144 | l.ch <- errClosing 145 | } 146 | 147 | // wait on all loops to main loop channel events 148 | s.loopwg.Wait() 149 | 150 | // shutdown all listeners 151 | for i := 0; i < len(s.lns); i++ { 152 | s.lns[i].close() 153 | } 154 | 155 | // wait on all listeners to complete 156 | s.lnwg.Wait() 157 | 158 | // close all connections 159 | s.loopwg.Add(len(s.loops)) 160 | for _, l := range s.loops { 161 | l.ch <- errCloseConns 162 | } 163 | s.loopwg.Wait() 164 | 165 | }() 166 | s.loopwg.Add(numLoops) 167 | for i := 0; i < numLoops; i++ { 168 | go stdloopRun(s, s.loops[i]) 169 | } 170 | s.lnwg.Add(len(listeners)) 171 | for i := 0; i < len(listeners); i++ { 172 | go stdlistenerRun(s, listeners[i], i) 173 | } 174 | return ferr 175 | } 176 | 177 | func stdlistenerRun(s *stdserver, ln *listener, lnidx int) { 178 | var ferr error 179 | defer func() { 180 | s.signalShutdown(ferr) 181 | s.lnwg.Done() 182 | }() 183 | var packet [0xFFFF]byte 184 | for { 185 | if ln.pconn != nil { 186 | // udp 187 | n, addr, err := ln.pconn.ReadFrom(packet[:]) 188 | if err != nil { 189 | ferr = err 190 | return 191 | } 192 | l := s.loops[int(atomic.AddUintptr(&s.accepted, 1))%len(s.loops)] 193 | l.ch <- &stdudpconn{ 194 | addrIndex: lnidx, 195 | localAddr: ln.lnaddr, 196 | remoteAddr: addr, 197 | in: append([]byte{}, packet[:n]...), 198 | } 199 | } else { 200 | // tcp 201 | conn, err := ln.ln.Accept() 202 | if err != nil { 203 | ferr = err 204 | return 205 | } 206 | l := s.loops[int(atomic.AddUintptr(&s.accepted, 1))%len(s.loops)] 207 | c := &stdconn{conn: conn, loop: l, lnidx: lnidx} 208 | l.ch <- c 209 | go func(c *stdconn) { 210 | var packet [0xFFFF]byte 211 | for { 212 | n, err := c.conn.Read(packet[:]) 213 | if err != nil { 214 | c.conn.SetReadDeadline(time.Time{}) 215 | l.ch <- &stderr{c, err} 216 | return 217 | } 218 | l.ch <- &stdin{c, append([]byte{}, packet[:n]...)} 219 | } 220 | }(c) 221 | } 222 | } 223 | } 224 | 225 | func stdloopRun(s *stdserver, l *stdloop) { 226 | var err error 227 | tick := make(chan bool) 228 | tock := make(chan time.Duration) 229 | defer func() { 230 | //fmt.Println("-- loop stopped --", l.idx) 231 | if l.idx == 0 && s.events.Tick != nil { 232 | close(tock) 233 | go func() { 234 | for range tick { 235 | } 236 | }() 237 | } 238 | s.signalShutdown(err) 239 | s.loopwg.Done() 240 | stdloopEgress(s, l) 241 | s.loopwg.Done() 242 | }() 243 | if l.idx == 0 && s.events.Tick != nil { 244 | go func() { 245 | for { 246 | tick <- true 247 | delay, ok := <-tock 248 | if !ok { 249 | break 250 | } 251 | time.Sleep(delay) 252 | } 253 | }() 254 | } 255 | //fmt.Println("-- loop started --", l.idx) 256 | for { 257 | select { 258 | case <-tick: 259 | delay, action := s.events.Tick() 260 | switch action { 261 | case Shutdown: 262 | err = errClosing 263 | } 264 | tock <- delay 265 | case v := <-l.ch: 266 | switch v := v.(type) { 267 | case error: 268 | err = v 269 | case *stdconn: 270 | err = stdloopAccept(s, l, v) 271 | case *stdin: 272 | err = stdloopRead(s, l, v.c, v.in) 273 | case *stdudpconn: 274 | err = stdloopReadUDP(s, l, v) 275 | case *stderr: 276 | err = stdloopError(s, l, v.c, v.err) 277 | case wakeReq: 278 | err = stdloopRead(s, l, v.c, nil) 279 | } 280 | } 281 | if err != nil { 282 | return 283 | } 284 | } 285 | } 286 | 287 | func stdloopEgress(s *stdserver, l *stdloop) { 288 | var closed bool 289 | loop: 290 | for v := range l.ch { 291 | switch v := v.(type) { 292 | case error: 293 | if v == errCloseConns { 294 | closed = true 295 | for c := range l.conns { 296 | stdloopClose(s, l, c) 297 | } 298 | } 299 | case *stderr: 300 | stdloopError(s, l, v.c, v.err) 301 | } 302 | if len(l.conns) == 0 && closed { 303 | break loop 304 | } 305 | } 306 | } 307 | 308 | func stdloopError(s *stdserver, l *stdloop, c *stdconn, err error) error { 309 | delete(l.conns, c) 310 | closeEvent := true 311 | switch atomic.LoadInt32(&c.done) { 312 | case 0: // read error 313 | c.conn.Close() 314 | if err == io.EOF { 315 | err = nil 316 | } 317 | case 1: // closed 318 | c.conn.Close() 319 | err = nil 320 | case 2: // detached 321 | err = nil 322 | if s.events.Detached == nil { 323 | c.conn.Close() 324 | } else { 325 | closeEvent = false 326 | switch s.events.Detached(c, &stddetachedConn{c.conn, c.donein}) { 327 | case Shutdown: 328 | return errClosing 329 | } 330 | } 331 | } 332 | if closeEvent { 333 | if s.events.Closed != nil { 334 | switch s.events.Closed(c, err) { 335 | case Shutdown: 336 | return errClosing 337 | } 338 | } 339 | } 340 | return nil 341 | } 342 | 343 | type stddetachedConn struct { 344 | conn net.Conn // original conn 345 | in []byte // extra input data 346 | } 347 | 348 | func (c *stddetachedConn) Read(p []byte) (n int, err error) { 349 | if len(c.in) > 0 { 350 | if len(c.in) <= len(p) { 351 | copy(p, c.in) 352 | n = len(c.in) 353 | c.in = nil 354 | return 355 | } 356 | copy(p, c.in[:len(p)]) 357 | n = len(p) 358 | c.in = c.in[n:] 359 | return 360 | } 361 | return c.conn.Read(p) 362 | } 363 | 364 | func (c *stddetachedConn) Write(p []byte) (n int, err error) { 365 | return c.conn.Write(p) 366 | } 367 | 368 | func (c *stddetachedConn) Close() error { 369 | return c.conn.Close() 370 | } 371 | 372 | func (c *stddetachedConn) Wake() {} 373 | 374 | func stdloopRead(s *stdserver, l *stdloop, c *stdconn, in []byte) error { 375 | if atomic.LoadInt32(&c.done) == 2 { 376 | // should not ignore reads for detached connections 377 | c.donein = append(c.donein, in...) 378 | return nil 379 | } 380 | if s.events.Data != nil { 381 | out, action := s.events.Data(c, in) 382 | if len(out) > 0 { 383 | if s.events.PreWrite != nil { 384 | s.events.PreWrite() 385 | } 386 | c.conn.Write(out) 387 | } 388 | switch action { 389 | case Shutdown: 390 | return errClosing 391 | case Detach: 392 | return stdloopDetach(s, l, c) 393 | case Close: 394 | return stdloopClose(s, l, c) 395 | } 396 | } 397 | return nil 398 | } 399 | 400 | func stdloopReadUDP(s *stdserver, l *stdloop, c *stdudpconn) error { 401 | if s.events.Data != nil { 402 | out, action := s.events.Data(c, c.in) 403 | if len(out) > 0 { 404 | if s.events.PreWrite != nil { 405 | s.events.PreWrite() 406 | } 407 | s.lns[c.addrIndex].pconn.WriteTo(out, c.remoteAddr) 408 | } 409 | switch action { 410 | case Shutdown: 411 | return errClosing 412 | } 413 | } 414 | return nil 415 | } 416 | 417 | func stdloopDetach(s *stdserver, l *stdloop, c *stdconn) error { 418 | atomic.StoreInt32(&c.done, 2) 419 | c.conn.SetReadDeadline(time.Now()) 420 | return nil 421 | } 422 | 423 | func stdloopClose(s *stdserver, l *stdloop, c *stdconn) error { 424 | atomic.StoreInt32(&c.done, 1) 425 | c.conn.SetReadDeadline(time.Now()) 426 | return nil 427 | } 428 | 429 | func stdloopAccept(s *stdserver, l *stdloop, c *stdconn) error { 430 | l.conns[c] = true 431 | c.addrIndex = c.lnidx 432 | c.localAddr = s.lns[c.lnidx].lnaddr 433 | c.remoteAddr = c.conn.RemoteAddr() 434 | 435 | if s.events.Opened != nil { 436 | out, opts, action := s.events.Opened(c) 437 | if len(out) > 0 { 438 | if s.events.PreWrite != nil { 439 | s.events.PreWrite() 440 | } 441 | c.conn.Write(out) 442 | } 443 | if opts.TCPKeepAlive > 0 { 444 | if c, ok := c.conn.(*net.TCPConn); ok { 445 | c.SetKeepAlive(true) 446 | c.SetKeepAlivePeriod(opts.TCPKeepAlive) 447 | } 448 | } 449 | switch action { 450 | case Shutdown: 451 | return errClosing 452 | case Detach: 453 | return stdloopDetach(s, l, c) 454 | case Close: 455 | return stdloopClose(s, l, c) 456 | } 457 | } 458 | return nil 459 | } 460 | -------------------------------------------------------------------------------- /evio_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Joshua J Baker. All rights reserved. 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package evio 6 | 7 | import ( 8 | "bufio" 9 | "fmt" 10 | "io" 11 | "math/rand" 12 | "net" 13 | "os" 14 | "strings" 15 | "sync" 16 | "sync/atomic" 17 | "testing" 18 | "time" 19 | ) 20 | 21 | func TestServe(t *testing.T) { 22 | // start a server 23 | // connect 10 clients 24 | // each client will pipe random data for 1-3 seconds. 25 | // the writes to the server will be random sizes. 0KB - 1MB. 26 | // the server will echo back the data. 27 | // waits for graceful connection closing. 28 | t.Run("stdlib", func(t *testing.T) { 29 | t.Run("tcp", func(t *testing.T) { 30 | t.Run("1-loop", func(t *testing.T) { 31 | testServe("tcp-net", ":9997", false, 10, 1, Random) 32 | }) 33 | t.Run("5-loop", func(t *testing.T) { 34 | testServe("tcp-net", ":9998", false, 10, 5, LeastConnections) 35 | }) 36 | t.Run("N-loop", func(t *testing.T) { 37 | testServe("tcp-net", ":9999", false, 10, -1, RoundRobin) 38 | }) 39 | }) 40 | t.Run("unix", func(t *testing.T) { 41 | t.Run("1-loop", func(t *testing.T) { 42 | testServe("tcp-net", ":9989", true, 10, 1, Random) 43 | }) 44 | t.Run("5-loop", func(t *testing.T) { 45 | testServe("tcp-net", ":9988", true, 10, 5, LeastConnections) 46 | }) 47 | t.Run("N-loop", func(t *testing.T) { 48 | testServe("tcp-net", ":9987", true, 10, -1, RoundRobin) 49 | }) 50 | }) 51 | }) 52 | t.Run("poll", func(t *testing.T) { 53 | t.Run("tcp", func(t *testing.T) { 54 | t.Run("1-loop", func(t *testing.T) { 55 | testServe("tcp", ":9991", false, 10, 1, Random) 56 | }) 57 | t.Run("5-loop", func(t *testing.T) { 58 | testServe("tcp", ":9992", false, 10, 5, LeastConnections) 59 | }) 60 | t.Run("N-loop", func(t *testing.T) { 61 | testServe("tcp", ":9993", false, 10, -1, RoundRobin) 62 | }) 63 | }) 64 | t.Run("unix", func(t *testing.T) { 65 | t.Run("1-loop", func(t *testing.T) { 66 | testServe("tcp", ":9994", true, 10, 1, Random) 67 | }) 68 | t.Run("5-loop", func(t *testing.T) { 69 | testServe("tcp", ":9995", true, 10, 5, LeastConnections) 70 | }) 71 | t.Run("N-loop", func(t *testing.T) { 72 | testServe("tcp", ":9996", true, 10, -1, RoundRobin) 73 | }) 74 | }) 75 | }) 76 | 77 | } 78 | 79 | func testServe(network, addr string, unix bool, nclients, nloops int, balance LoadBalance) { 80 | var started int32 81 | var connected int32 82 | var disconnected int32 83 | 84 | var events Events 85 | events.LoadBalance = balance 86 | events.NumLoops = nloops 87 | events.Serving = func(srv Server) (action Action) { 88 | return 89 | } 90 | events.Opened = func(c Conn) (out []byte, opts Options, action Action) { 91 | c.SetContext(c) 92 | atomic.AddInt32(&connected, 1) 93 | out = []byte("sweetness\r\n") 94 | opts.TCPKeepAlive = time.Minute * 5 95 | if c.LocalAddr() == nil { 96 | panic("nil local addr") 97 | } 98 | if c.RemoteAddr() == nil { 99 | panic("nil local addr") 100 | } 101 | return 102 | } 103 | events.Closed = func(c Conn, err error) (action Action) { 104 | if c.Context() != c { 105 | panic("invalid context") 106 | } 107 | atomic.AddInt32(&disconnected, 1) 108 | if atomic.LoadInt32(&connected) == atomic.LoadInt32(&disconnected) && 109 | atomic.LoadInt32(&disconnected) == int32(nclients) { 110 | action = Shutdown 111 | } 112 | return 113 | } 114 | events.Data = func(c Conn, in []byte) (out []byte, action Action) { 115 | out = in 116 | return 117 | } 118 | events.Tick = func() (delay time.Duration, action Action) { 119 | if atomic.LoadInt32(&started) == 0 { 120 | for i := 0; i < nclients; i++ { 121 | go startClient(network, addr, nloops) 122 | } 123 | atomic.StoreInt32(&started, 1) 124 | } 125 | delay = time.Second / 5 126 | return 127 | } 128 | var err error 129 | if unix { 130 | socket := strings.Replace(addr, ":", "socket", 1) 131 | os.RemoveAll(socket) 132 | defer os.RemoveAll(socket) 133 | err = Serve(events, network+"://"+addr, "unix://"+socket) 134 | } else { 135 | err = Serve(events, network+"://"+addr) 136 | } 137 | if err != nil { 138 | panic(err) 139 | } 140 | } 141 | 142 | func startClient(network, addr string, nloops int) { 143 | onetwork := network 144 | network = strings.Replace(network, "-net", "", -1) 145 | rand.Seed(time.Now().UnixNano()) 146 | c, err := net.Dial(network, addr) 147 | if err != nil { 148 | panic(err) 149 | } 150 | defer c.Close() 151 | rd := bufio.NewReader(c) 152 | msg, err := rd.ReadBytes('\n') 153 | if err != nil { 154 | panic(err) 155 | } 156 | if string(msg) != "sweetness\r\n" { 157 | panic("bad header") 158 | } 159 | duration := time.Duration((rand.Float64()*2+1)*float64(time.Second)) / 8 160 | start := time.Now() 161 | for time.Since(start) < duration { 162 | sz := rand.Int() % (1024 * 1024) 163 | data := make([]byte, sz) 164 | if _, err := rand.Read(data); err != nil { 165 | panic(err) 166 | } 167 | if _, err := c.Write(data); err != nil { 168 | panic(err) 169 | } 170 | data2 := make([]byte, len(data)) 171 | if _, err := io.ReadFull(rd, data2); err != nil { 172 | panic(err) 173 | } 174 | if string(data) != string(data2) { 175 | fmt.Printf("mismatch %s/%d: %d vs %d bytes\n", onetwork, nloops, len(data), len(data2)) 176 | //panic("mismatch") 177 | } 178 | } 179 | } 180 | 181 | func must(err error) { 182 | if err != nil { 183 | panic(err) 184 | } 185 | } 186 | func TestTick(t *testing.T) { 187 | var wg sync.WaitGroup 188 | wg.Add(1) 189 | go func() { 190 | defer wg.Done() 191 | testTick("tcp", ":9991", false) 192 | }() 193 | wg.Add(1) 194 | go func() { 195 | defer wg.Done() 196 | testTick("tcp", ":9992", true) 197 | }() 198 | wg.Add(1) 199 | go func() { 200 | defer wg.Done() 201 | testTick("unix", "socket1", false) 202 | }() 203 | wg.Add(1) 204 | go func() { 205 | defer wg.Done() 206 | testTick("unix", "socket2", true) 207 | }() 208 | wg.Wait() 209 | } 210 | func testTick(network, addr string, stdlib bool) { 211 | var events Events 212 | var count int 213 | start := time.Now() 214 | events.Tick = func() (delay time.Duration, action Action) { 215 | if count == 25 { 216 | action = Shutdown 217 | return 218 | } 219 | count++ 220 | delay = time.Millisecond * 10 221 | return 222 | } 223 | if stdlib { 224 | must(Serve(events, network+"-net://"+addr)) 225 | } else { 226 | must(Serve(events, network+"://"+addr)) 227 | } 228 | dur := time.Since(start) 229 | if dur < 250&time.Millisecond || dur > time.Second { 230 | panic("bad ticker timing") 231 | } 232 | } 233 | 234 | func TestShutdown(t *testing.T) { 235 | var wg sync.WaitGroup 236 | wg.Add(1) 237 | go func() { 238 | defer wg.Done() 239 | testShutdown("tcp", ":9991", false) 240 | }() 241 | wg.Add(1) 242 | go func() { 243 | defer wg.Done() 244 | testShutdown("tcp", ":9992", true) 245 | }() 246 | wg.Add(1) 247 | go func() { 248 | defer wg.Done() 249 | testShutdown("unix", "socket1", false) 250 | }() 251 | wg.Add(1) 252 | go func() { 253 | defer wg.Done() 254 | testShutdown("unix", "socket2", true) 255 | }() 256 | wg.Wait() 257 | } 258 | func testShutdown(network, addr string, stdlib bool) { 259 | var events Events 260 | var count int 261 | var clients int64 262 | var N = 10 263 | events.Opened = func(c Conn) (out []byte, opts Options, action Action) { 264 | atomic.AddInt64(&clients, 1) 265 | return 266 | } 267 | events.Closed = func(c Conn, err error) (action Action) { 268 | atomic.AddInt64(&clients, -1) 269 | return 270 | } 271 | events.Tick = func() (delay time.Duration, action Action) { 272 | if count == 0 { 273 | // start clients 274 | for i := 0; i < N; i++ { 275 | go func() { 276 | conn, err := net.Dial(network, addr) 277 | must(err) 278 | defer conn.Close() 279 | _, err = conn.Read([]byte{0}) 280 | if err == nil { 281 | panic("expected error") 282 | } 283 | }() 284 | } 285 | } else { 286 | if int(atomic.LoadInt64(&clients)) == N { 287 | action = Shutdown 288 | } 289 | } 290 | count++ 291 | delay = time.Second / 20 292 | return 293 | } 294 | if stdlib { 295 | must(Serve(events, network+"-net://"+addr)) 296 | } else { 297 | must(Serve(events, network+"://"+addr)) 298 | } 299 | if clients != 0 { 300 | panic("did not call close on all clients") 301 | } 302 | } 303 | 304 | func TestDetach(t *testing.T) { 305 | t.Run("poll", func(t *testing.T) { 306 | t.Run("tcp", func(t *testing.T) { 307 | testDetach("tcp", ":9991", false) 308 | }) 309 | t.Run("unix", func(t *testing.T) { 310 | testDetach("unix", "socket1", false) 311 | }) 312 | }) 313 | t.Run("stdlib", func(t *testing.T) { 314 | t.Run("tcp", func(t *testing.T) { 315 | testDetach("tcp", ":9992", true) 316 | }) 317 | t.Run("unix", func(t *testing.T) { 318 | testDetach("unix", "socket2", true) 319 | }) 320 | }) 321 | } 322 | 323 | func testDetach(network, addr string, stdlib bool) { 324 | // we will write a bunch of data with the text "--detached--" in the 325 | // middle followed by a bunch of data. 326 | rand.Seed(time.Now().UnixNano()) 327 | rdat := make([]byte, 10*1024) 328 | if _, err := rand.Read(rdat); err != nil { 329 | panic("random error: " + err.Error()) 330 | } 331 | expected := []byte(string(rdat) + "--detached--" + string(rdat)) 332 | var cin []byte 333 | var events Events 334 | events.Data = func(c Conn, in []byte) (out []byte, action Action) { 335 | cin = append(cin, in...) 336 | if len(cin) >= len(expected) { 337 | if string(cin) != string(expected) { 338 | panic("mismatch client -> server") 339 | } 340 | return cin, Detach 341 | } 342 | return 343 | } 344 | 345 | var done int64 346 | events.Detached = func(c Conn, conn io.ReadWriteCloser) (action Action) { 347 | go func() { 348 | p := make([]byte, len(expected)) 349 | defer conn.Close() 350 | _, err := io.ReadFull(conn, p) 351 | must(err) 352 | conn.Write(expected) 353 | }() 354 | return 355 | } 356 | 357 | events.Serving = func(srv Server) (action Action) { 358 | go func() { 359 | p := make([]byte, len(expected)) 360 | _ = expected 361 | conn, err := net.Dial(network, addr) 362 | must(err) 363 | defer conn.Close() 364 | conn.Write(expected) 365 | _, err = io.ReadFull(conn, p) 366 | must(err) 367 | conn.Write(expected) 368 | _, err = io.ReadFull(conn, p) 369 | must(err) 370 | atomic.StoreInt64(&done, 1) 371 | }() 372 | return 373 | } 374 | events.Tick = func() (delay time.Duration, action Action) { 375 | delay = time.Second / 5 376 | if atomic.LoadInt64(&done) == 1 { 377 | action = Shutdown 378 | } 379 | return 380 | } 381 | if stdlib { 382 | must(Serve(events, network+"-net://"+addr)) 383 | } else { 384 | must(Serve(events, network+"://"+addr)) 385 | } 386 | } 387 | 388 | func TestBadAddresses(t *testing.T) { 389 | var events Events 390 | events.Serving = func(srv Server) (action Action) { 391 | return Shutdown 392 | } 393 | if err := Serve(events, "tulip://howdy"); err == nil { 394 | t.Fatalf("expected error") 395 | } 396 | if err := Serve(events, "howdy"); err == nil { 397 | t.Fatalf("expected error") 398 | } 399 | if err := Serve(events, "tcp://"); err != nil { 400 | t.Fatalf("expected nil, got '%v'", err) 401 | } 402 | } 403 | 404 | func TestInputStream(t *testing.T) { 405 | var s InputStream 406 | in := []byte("HELLO") 407 | data := s.Begin(in) 408 | if string(data) != string(in) { 409 | t.Fatalf("expected '%v', got '%v'", in, data) 410 | } 411 | s.End(in[3:]) 412 | data = s.Begin([]byte("WLY")) 413 | if string(data) != "LOWLY" { 414 | t.Fatalf("expected '%v', got '%v'", "LOWLY", data) 415 | } 416 | s.End(nil) 417 | data = s.Begin([]byte("PLAYER")) 418 | if string(data) != "PLAYER" { 419 | t.Fatalf("expected '%v', got '%v'", "PLAYER", data) 420 | } 421 | } 422 | 423 | func TestReuseInputBuffer(t *testing.T) { 424 | reuses := []bool{true, false} 425 | for _, reuse := range reuses { 426 | var events Events 427 | events.Opened = func(c Conn) (out []byte, opts Options, action Action) { 428 | opts.ReuseInputBuffer = reuse 429 | return 430 | } 431 | var prev []byte 432 | events.Data = func(c Conn, in []byte) (out []byte, action Action) { 433 | if prev == nil { 434 | prev = in 435 | } else { 436 | reused := string(in) == string(prev) 437 | if reused != reuse { 438 | t.Fatalf("expected %v, got %v", reuse, reused) 439 | } 440 | action = Shutdown 441 | } 442 | return 443 | } 444 | events.Serving = func(_ Server) (action Action) { 445 | go func() { 446 | c, err := net.Dial("tcp", ":9991") 447 | must(err) 448 | defer c.Close() 449 | c.Write([]byte("packet1")) 450 | time.Sleep(time.Second / 5) 451 | c.Write([]byte("packet2")) 452 | }() 453 | return 454 | } 455 | must(Serve(events, "tcp://:9991")) 456 | } 457 | 458 | } 459 | 460 | func TestReuseport(t *testing.T) { 461 | var events Events 462 | events.Serving = func(s Server) (action Action) { 463 | return Shutdown 464 | } 465 | var wg sync.WaitGroup 466 | wg.Add(5) 467 | for i := 0; i < 5; i++ { 468 | var t = "1" 469 | if i%2 == 0 { 470 | t = "true" 471 | } 472 | go func(t string) { 473 | defer wg.Done() 474 | must(Serve(events, "tcp://:9991?reuseport="+t)) 475 | }(t) 476 | } 477 | wg.Wait() 478 | } 479 | -------------------------------------------------------------------------------- /evio_unix.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Joshua J Baker. All rights reserved. 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build darwin netbsd freebsd openbsd dragonfly linux 6 | 7 | package evio 8 | 9 | import ( 10 | "io" 11 | "net" 12 | "os" 13 | "runtime" 14 | "sync" 15 | "sync/atomic" 16 | "syscall" 17 | "time" 18 | 19 | reuseport "github.com/kavu/go_reuseport" 20 | "github.com/tidwall/evio/internal" 21 | ) 22 | 23 | type conn struct { 24 | fd int // file descriptor 25 | lnidx int // listener index in the server lns list 26 | out []byte // write buffer 27 | sa syscall.Sockaddr // remote socket address 28 | reuse bool // should reuse input buffer 29 | opened bool // connection opened event fired 30 | action Action // next user action 31 | ctx interface{} // user-defined context 32 | addrIndex int // index of listening address 33 | localAddr net.Addr // local addre 34 | remoteAddr net.Addr // remote addr 35 | loop *loop // connected loop 36 | } 37 | 38 | func (c *conn) Context() interface{} { return c.ctx } 39 | func (c *conn) SetContext(ctx interface{}) { c.ctx = ctx } 40 | func (c *conn) AddrIndex() int { return c.addrIndex } 41 | func (c *conn) LocalAddr() net.Addr { return c.localAddr } 42 | func (c *conn) RemoteAddr() net.Addr { return c.remoteAddr } 43 | func (c *conn) Wake() { 44 | if c.loop != nil { 45 | c.loop.poll.Trigger(c) 46 | } 47 | } 48 | 49 | type server struct { 50 | events Events // user events 51 | loops []*loop // all the loops 52 | lns []*listener // all the listeners 53 | wg sync.WaitGroup // loop close waitgroup 54 | cond *sync.Cond // shutdown signaler 55 | balance LoadBalance // load balancing method 56 | accepted uintptr // accept counter 57 | tch chan time.Duration // ticker channel 58 | 59 | //ticktm time.Time // next tick time 60 | } 61 | 62 | type loop struct { 63 | idx int // loop index in the server loops list 64 | poll *internal.Poll // epoll or kqueue 65 | packet []byte // read packet buffer 66 | fdconns map[int]*conn // loop connections fd -> conn 67 | count int32 // connection count 68 | } 69 | 70 | // waitForShutdown waits for a signal to shutdown 71 | func (s *server) waitForShutdown() { 72 | s.cond.L.Lock() 73 | s.cond.Wait() 74 | s.cond.L.Unlock() 75 | } 76 | 77 | // signalShutdown signals a shutdown an begins server closing 78 | func (s *server) signalShutdown() { 79 | s.cond.L.Lock() 80 | s.cond.Signal() 81 | s.cond.L.Unlock() 82 | } 83 | 84 | func serve(events Events, listeners []*listener) error { 85 | // figure out the correct number of loops/goroutines to use. 86 | numLoops := events.NumLoops 87 | if numLoops <= 0 { 88 | if numLoops == 0 { 89 | numLoops = 1 90 | } else { 91 | numLoops = runtime.NumCPU() 92 | } 93 | } 94 | 95 | s := &server{} 96 | s.events = events 97 | s.lns = listeners 98 | s.cond = sync.NewCond(&sync.Mutex{}) 99 | s.balance = events.LoadBalance 100 | s.tch = make(chan time.Duration) 101 | 102 | //println("-- server starting") 103 | if s.events.Serving != nil { 104 | var svr Server 105 | svr.NumLoops = numLoops 106 | svr.Addrs = make([]net.Addr, len(listeners)) 107 | for i, ln := range listeners { 108 | svr.Addrs[i] = ln.lnaddr 109 | } 110 | action := s.events.Serving(svr) 111 | switch action { 112 | case None: 113 | case Shutdown: 114 | return nil 115 | } 116 | } 117 | 118 | defer func() { 119 | // wait on a signal for shutdown 120 | s.waitForShutdown() 121 | 122 | // notify all loops to close by closing all listeners 123 | for _, l := range s.loops { 124 | l.poll.Trigger(errClosing) 125 | } 126 | 127 | // wait on all loops to complete reading events 128 | s.wg.Wait() 129 | 130 | // close loops and all outstanding connections 131 | for _, l := range s.loops { 132 | for _, c := range l.fdconns { 133 | loopCloseConn(s, l, c, nil) 134 | } 135 | l.poll.Close() 136 | } 137 | //println("-- server stopped") 138 | }() 139 | 140 | // create loops locally and bind the listeners. 141 | for i := 0; i < numLoops; i++ { 142 | l := &loop{ 143 | idx: i, 144 | poll: internal.OpenPoll(), 145 | packet: make([]byte, 0xFFFF), 146 | fdconns: make(map[int]*conn), 147 | } 148 | for _, ln := range listeners { 149 | l.poll.AddRead(ln.fd) 150 | } 151 | s.loops = append(s.loops, l) 152 | } 153 | // start loops in background 154 | s.wg.Add(len(s.loops)) 155 | for _, l := range s.loops { 156 | go loopRun(s, l) 157 | } 158 | return nil 159 | } 160 | 161 | func loopCloseConn(s *server, l *loop, c *conn, err error) error { 162 | atomic.AddInt32(&l.count, -1) 163 | delete(l.fdconns, c.fd) 164 | syscall.Close(c.fd) 165 | if s.events.Closed != nil { 166 | switch s.events.Closed(c, err) { 167 | case None: 168 | case Shutdown: 169 | return errClosing 170 | } 171 | } 172 | return nil 173 | } 174 | 175 | func loopDetachConn(s *server, l *loop, c *conn, err error) error { 176 | if s.events.Detached == nil { 177 | return loopCloseConn(s, l, c, err) 178 | } 179 | l.poll.ModDetach(c.fd) 180 | 181 | atomic.AddInt32(&l.count, -1) 182 | delete(l.fdconns, c.fd) 183 | if err := syscall.SetNonblock(c.fd, false); err != nil { 184 | return err 185 | } 186 | switch s.events.Detached(c, &detachedConn{fd: c.fd}) { 187 | case None: 188 | case Shutdown: 189 | return errClosing 190 | } 191 | return nil 192 | } 193 | 194 | func loopNote(s *server, l *loop, note interface{}) error { 195 | var err error 196 | switch v := note.(type) { 197 | case time.Duration: 198 | delay, action := s.events.Tick() 199 | switch action { 200 | case None: 201 | case Shutdown: 202 | err = errClosing 203 | } 204 | s.tch <- delay 205 | case error: // shutdown 206 | err = v 207 | case *conn: 208 | // Wake called for connection 209 | if l.fdconns[v.fd] != v { 210 | return nil // ignore stale wakes 211 | } 212 | return loopWake(s, l, v) 213 | } 214 | return err 215 | } 216 | 217 | func loopRun(s *server, l *loop) { 218 | defer func() { 219 | //fmt.Println("-- loop stopped --", l.idx) 220 | s.signalShutdown() 221 | s.wg.Done() 222 | }() 223 | 224 | if l.idx == 0 && s.events.Tick != nil { 225 | go loopTicker(s, l) 226 | } 227 | 228 | //fmt.Println("-- loop started --", l.idx) 229 | l.poll.Wait(func(fd int, note interface{}) error { 230 | if fd == 0 { 231 | return loopNote(s, l, note) 232 | } 233 | c := l.fdconns[fd] 234 | switch { 235 | case c == nil: 236 | return loopAccept(s, l, fd) 237 | case !c.opened: 238 | return loopOpened(s, l, c) 239 | case len(c.out) > 0: 240 | return loopWrite(s, l, c) 241 | case c.action != None: 242 | return loopAction(s, l, c) 243 | default: 244 | return loopRead(s, l, c) 245 | } 246 | }) 247 | } 248 | 249 | func loopTicker(s *server, l *loop) { 250 | for { 251 | if err := l.poll.Trigger(time.Duration(0)); err != nil { 252 | break 253 | } 254 | time.Sleep(<-s.tch) 255 | } 256 | } 257 | 258 | func loopAccept(s *server, l *loop, fd int) error { 259 | for i, ln := range s.lns { 260 | if ln.fd == fd { 261 | if len(s.loops) > 1 { 262 | switch s.balance { 263 | case LeastConnections: 264 | n := atomic.LoadInt32(&l.count) 265 | for _, lp := range s.loops { 266 | if lp.idx != l.idx { 267 | if atomic.LoadInt32(&lp.count) < n { 268 | return nil // do not accept 269 | } 270 | } 271 | } 272 | case RoundRobin: 273 | idx := int(atomic.LoadUintptr(&s.accepted)) % len(s.loops) 274 | if idx != l.idx { 275 | return nil // do not accept 276 | } 277 | atomic.AddUintptr(&s.accepted, 1) 278 | } 279 | } 280 | if ln.pconn != nil { 281 | return loopUDPRead(s, l, i, fd) 282 | } 283 | nfd, sa, err := syscall.Accept(fd) 284 | if err != nil { 285 | if err == syscall.EAGAIN { 286 | return nil 287 | } 288 | return err 289 | } 290 | if err := syscall.SetNonblock(nfd, true); err != nil { 291 | return err 292 | } 293 | c := &conn{fd: nfd, sa: sa, lnidx: i, loop: l} 294 | c.out = nil 295 | l.fdconns[c.fd] = c 296 | l.poll.AddReadWrite(c.fd) 297 | atomic.AddInt32(&l.count, 1) 298 | break 299 | } 300 | } 301 | return nil 302 | } 303 | 304 | func loopUDPRead(s *server, l *loop, lnidx, fd int) error { 305 | n, sa, err := syscall.Recvfrom(fd, l.packet, 0) 306 | if err != nil || n == 0 { 307 | return nil 308 | } 309 | if s.events.Data != nil { 310 | var sa6 syscall.SockaddrInet6 311 | switch sa := sa.(type) { 312 | case *syscall.SockaddrInet4: 313 | sa6.ZoneId = 0 314 | sa6.Port = sa.Port 315 | for i := 0; i < 12; i++ { 316 | sa6.Addr[i] = 0 317 | } 318 | sa6.Addr[12] = sa.Addr[0] 319 | sa6.Addr[13] = sa.Addr[1] 320 | sa6.Addr[14] = sa.Addr[2] 321 | sa6.Addr[15] = sa.Addr[3] 322 | case *syscall.SockaddrInet6: 323 | sa6 = *sa 324 | } 325 | c := &conn{} 326 | c.addrIndex = lnidx 327 | c.localAddr = s.lns[lnidx].lnaddr 328 | c.remoteAddr = internal.SockaddrToAddr(&sa6) 329 | in := append([]byte{}, l.packet[:n]...) 330 | out, action := s.events.Data(c, in) 331 | if len(out) > 0 { 332 | if s.events.PreWrite != nil { 333 | s.events.PreWrite() 334 | } 335 | syscall.Sendto(fd, out, 0, sa) 336 | } 337 | switch action { 338 | case Shutdown: 339 | return errClosing 340 | } 341 | } 342 | return nil 343 | } 344 | 345 | func loopOpened(s *server, l *loop, c *conn) error { 346 | c.opened = true 347 | c.addrIndex = c.lnidx 348 | c.localAddr = s.lns[c.lnidx].lnaddr 349 | c.remoteAddr = internal.SockaddrToAddr(c.sa) 350 | if s.events.Opened != nil { 351 | out, opts, action := s.events.Opened(c) 352 | if len(out) > 0 { 353 | c.out = append([]byte{}, out...) 354 | } 355 | c.action = action 356 | c.reuse = opts.ReuseInputBuffer 357 | if opts.TCPKeepAlive > 0 { 358 | if _, ok := s.lns[c.lnidx].ln.(*net.TCPListener); ok { 359 | internal.SetKeepAlive(c.fd, int(opts.TCPKeepAlive/time.Second)) 360 | } 361 | } 362 | } 363 | if len(c.out) == 0 && c.action == None { 364 | l.poll.ModRead(c.fd) 365 | } 366 | return nil 367 | } 368 | 369 | func loopWrite(s *server, l *loop, c *conn) error { 370 | if s.events.PreWrite != nil { 371 | s.events.PreWrite() 372 | } 373 | n, err := syscall.Write(c.fd, c.out) 374 | if err != nil { 375 | if err == syscall.EAGAIN { 376 | return nil 377 | } 378 | return loopCloseConn(s, l, c, err) 379 | } 380 | if n == len(c.out) { 381 | // release the connection output page if it goes over page size, 382 | // otherwise keep reusing existing page. 383 | if cap(c.out) > 4096 { 384 | c.out = nil 385 | } else { 386 | c.out = c.out[:0] 387 | } 388 | } else { 389 | c.out = c.out[n:] 390 | } 391 | if len(c.out) == 0 && c.action == None { 392 | l.poll.ModRead(c.fd) 393 | } 394 | return nil 395 | } 396 | 397 | func loopAction(s *server, l *loop, c *conn) error { 398 | switch c.action { 399 | default: 400 | c.action = None 401 | case Close: 402 | return loopCloseConn(s, l, c, nil) 403 | case Shutdown: 404 | return errClosing 405 | case Detach: 406 | return loopDetachConn(s, l, c, nil) 407 | } 408 | if len(c.out) == 0 && c.action == None { 409 | l.poll.ModRead(c.fd) 410 | } 411 | return nil 412 | } 413 | 414 | func loopWake(s *server, l *loop, c *conn) error { 415 | if s.events.Data == nil { 416 | return nil 417 | } 418 | out, action := s.events.Data(c, nil) 419 | c.action = action 420 | if len(out) > 0 { 421 | c.out = append([]byte{}, out...) 422 | } 423 | if len(c.out) != 0 || c.action != None { 424 | l.poll.ModReadWrite(c.fd) 425 | } 426 | return nil 427 | } 428 | 429 | func loopRead(s *server, l *loop, c *conn) error { 430 | var in []byte 431 | n, err := syscall.Read(c.fd, l.packet) 432 | if n == 0 || err != nil { 433 | if err == syscall.EAGAIN { 434 | return nil 435 | } 436 | return loopCloseConn(s, l, c, err) 437 | } 438 | in = l.packet[:n] 439 | if !c.reuse { 440 | in = append([]byte{}, in...) 441 | } 442 | if s.events.Data != nil { 443 | out, action := s.events.Data(c, in) 444 | c.action = action 445 | if len(out) > 0 { 446 | c.out = append(c.out[:0], out...) 447 | } 448 | } 449 | if len(c.out) != 0 || c.action != None { 450 | l.poll.ModReadWrite(c.fd) 451 | } 452 | return nil 453 | } 454 | 455 | type detachedConn struct { 456 | fd int 457 | } 458 | 459 | func (c *detachedConn) Close() error { 460 | err := syscall.Close(c.fd) 461 | if err != nil { 462 | return err 463 | } 464 | c.fd = -1 465 | return nil 466 | } 467 | 468 | func (c *detachedConn) Read(p []byte) (n int, err error) { 469 | n, err = syscall.Read(c.fd, p) 470 | if err != nil { 471 | return n, err 472 | } 473 | if n == 0 { 474 | if len(p) == 0 { 475 | return 0, nil 476 | } 477 | return 0, io.EOF 478 | } 479 | return n, nil 480 | } 481 | 482 | func (c *detachedConn) Write(p []byte) (n int, err error) { 483 | n = len(p) 484 | for len(p) > 0 { 485 | nn, err := syscall.Write(c.fd, p) 486 | if err != nil { 487 | return n, err 488 | } 489 | p = p[nn:] 490 | } 491 | return n, nil 492 | } 493 | 494 | func (ln *listener) close() { 495 | if ln.fd != 0 { 496 | syscall.Close(ln.fd) 497 | } 498 | if ln.f != nil { 499 | ln.f.Close() 500 | } 501 | if ln.ln != nil { 502 | ln.ln.Close() 503 | } 504 | if ln.pconn != nil { 505 | ln.pconn.Close() 506 | } 507 | if ln.network == "unix" { 508 | os.RemoveAll(ln.addr) 509 | } 510 | } 511 | 512 | // system takes the net listener and detaches it from it's parent 513 | // event loop, grabs the file descriptor, and makes it non-blocking. 514 | func (ln *listener) system() error { 515 | var err error 516 | switch netln := ln.ln.(type) { 517 | case nil: 518 | switch pconn := ln.pconn.(type) { 519 | case *net.UDPConn: 520 | ln.f, err = pconn.File() 521 | } 522 | case *net.TCPListener: 523 | ln.f, err = netln.File() 524 | case *net.UnixListener: 525 | ln.f, err = netln.File() 526 | } 527 | if err != nil { 528 | ln.close() 529 | return err 530 | } 531 | ln.fd = int(ln.f.Fd()) 532 | return syscall.SetNonblock(ln.fd, true) 533 | } 534 | 535 | func reuseportListenPacket(proto, addr string) (l net.PacketConn, err error) { 536 | return reuseport.ListenPacket(proto, addr) 537 | } 538 | 539 | func reuseportListen(proto, addr string) (l net.Listener, err error) { 540 | return reuseport.Listen(proto, addr) 541 | } 542 | -------------------------------------------------------------------------------- /examples/echo-server/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Joshua J Baker. All rights reserved. 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "flag" 9 | "fmt" 10 | "log" 11 | "strings" 12 | 13 | "github.com/tidwall/evio" 14 | ) 15 | 16 | func main() { 17 | var port int 18 | var loops int 19 | var udp bool 20 | var trace bool 21 | var reuseport bool 22 | var stdlib bool 23 | 24 | flag.IntVar(&port, "port", 5000, "server port") 25 | flag.BoolVar(&udp, "udp", false, "listen on udp") 26 | flag.BoolVar(&reuseport, "reuseport", false, "reuseport (SO_REUSEPORT)") 27 | flag.BoolVar(&trace, "trace", false, "print packets to console") 28 | flag.IntVar(&loops, "loops", 0, "num loops") 29 | flag.BoolVar(&stdlib, "stdlib", false, "use stdlib") 30 | flag.Parse() 31 | 32 | var events evio.Events 33 | events.NumLoops = loops 34 | events.Serving = func(srv evio.Server) (action evio.Action) { 35 | log.Printf("echo server started on port %d (loops: %d)", port, srv.NumLoops) 36 | if reuseport { 37 | log.Printf("reuseport") 38 | } 39 | if stdlib { 40 | log.Printf("stdlib") 41 | } 42 | return 43 | } 44 | events.Data = func(c evio.Conn, in []byte) (out []byte, action evio.Action) { 45 | if trace { 46 | log.Printf("%s", strings.TrimSpace(string(in))) 47 | } 48 | out = in 49 | return 50 | } 51 | scheme := "tcp" 52 | if udp { 53 | scheme = "udp" 54 | } 55 | if stdlib { 56 | scheme += "-net" 57 | } 58 | log.Fatal(evio.Serve(events, fmt.Sprintf("%s://:%d?reuseport=%t", scheme, port, reuseport))) 59 | } 60 | -------------------------------------------------------------------------------- /examples/http-server/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Joshua J Baker. All rights reserved. 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "bytes" 9 | "flag" 10 | "fmt" 11 | "log" 12 | "os" 13 | "strconv" 14 | "strings" 15 | "time" 16 | 17 | "github.com/tidwall/evio" 18 | ) 19 | 20 | var res string 21 | 22 | type request struct { 23 | proto, method string 24 | path, query string 25 | head, body string 26 | remoteAddr string 27 | } 28 | 29 | func main() { 30 | var port int 31 | var loops int 32 | var aaaa bool 33 | var noparse bool 34 | var unixsocket string 35 | var stdlib bool 36 | 37 | flag.StringVar(&unixsocket, "unixsocket", "", "unix socket") 38 | flag.IntVar(&port, "port", 8080, "server port") 39 | flag.BoolVar(&aaaa, "aaaa", false, "aaaaa....") 40 | flag.BoolVar(&noparse, "noparse", true, "do not parse requests") 41 | flag.BoolVar(&stdlib, "stdlib", false, "use stdlib") 42 | flag.IntVar(&loops, "loops", 0, "num loops") 43 | flag.Parse() 44 | 45 | if os.Getenv("NOPARSE") == "1" { 46 | noparse = true 47 | } 48 | 49 | if aaaa { 50 | res = strings.Repeat("a", 1024) 51 | } else { 52 | res = "Hello World!\r\n" 53 | } 54 | 55 | var events evio.Events 56 | events.NumLoops = loops 57 | events.Serving = func(srv evio.Server) (action evio.Action) { 58 | log.Printf("http server started on port %d (loops: %d)", port, srv.NumLoops) 59 | if unixsocket != "" { 60 | log.Printf("http server started at %s", unixsocket) 61 | } 62 | if stdlib { 63 | log.Printf("stdlib") 64 | } 65 | return 66 | } 67 | 68 | events.Opened = func(c evio.Conn) (out []byte, opts evio.Options, action evio.Action) { 69 | c.SetContext(&evio.InputStream{}) 70 | //log.Printf("opened: laddr: %v: raddr: %v", c.LocalAddr(), c.RemoteAddr()) 71 | return 72 | } 73 | 74 | events.Closed = func(c evio.Conn, err error) (action evio.Action) { 75 | //log.Printf("closed: %s: %s", c.LocalAddr().String(), c.RemoteAddr().String()) 76 | return 77 | } 78 | 79 | events.Data = func(c evio.Conn, in []byte) (out []byte, action evio.Action) { 80 | if in == nil { 81 | return 82 | } 83 | is := c.Context().(*evio.InputStream) 84 | data := is.Begin(in) 85 | if noparse && bytes.Contains(data, []byte("\r\n\r\n")) { 86 | // for testing minimal single packet request -> response. 87 | out = appendresp(nil, "200 OK", "", res) 88 | return 89 | } 90 | // process the pipeline 91 | var req request 92 | for { 93 | leftover, err := parsereq(data, &req) 94 | if err != nil { 95 | // bad thing happened 96 | out = appendresp(out, "500 Error", "", err.Error()+"\n") 97 | action = evio.Close 98 | break 99 | } else if len(leftover) == len(data) { 100 | // request not ready, yet 101 | break 102 | } 103 | // handle the request 104 | req.remoteAddr = c.RemoteAddr().String() 105 | out = appendhandle(out, &req) 106 | data = leftover 107 | } 108 | is.End(data) 109 | return 110 | } 111 | var ssuf string 112 | if stdlib { 113 | ssuf = "-net" 114 | } 115 | // We at least want the single http address. 116 | addrs := []string{fmt.Sprintf("tcp"+ssuf+"://:%d", port)} 117 | if unixsocket != "" { 118 | addrs = append(addrs, fmt.Sprintf("unix"+ssuf+"://%s", unixsocket)) 119 | } 120 | // Start serving! 121 | log.Fatal(evio.Serve(events, addrs...)) 122 | } 123 | 124 | // appendhandle handles the incoming request and appends the response to 125 | // the provided bytes, which is then returned to the caller. 126 | func appendhandle(b []byte, req *request) []byte { 127 | return appendresp(b, "200 OK", "", res) 128 | } 129 | 130 | // appendresp will append a valid http response to the provide bytes. 131 | // The status param should be the code plus text such as "200 OK". 132 | // The head parameter should be a series of lines ending with "\r\n" or empty. 133 | func appendresp(b []byte, status, head, body string) []byte { 134 | b = append(b, "HTTP/1.1"...) 135 | b = append(b, ' ') 136 | b = append(b, status...) 137 | b = append(b, '\r', '\n') 138 | b = append(b, "Server: evio\r\n"...) 139 | b = append(b, "Date: "...) 140 | b = time.Now().AppendFormat(b, "Mon, 02 Jan 2006 15:04:05 GMT") 141 | b = append(b, '\r', '\n') 142 | if len(body) > 0 { 143 | b = append(b, "Content-Length: "...) 144 | b = strconv.AppendInt(b, int64(len(body)), 10) 145 | b = append(b, '\r', '\n') 146 | } 147 | b = append(b, head...) 148 | b = append(b, '\r', '\n') 149 | if len(body) > 0 { 150 | b = append(b, body...) 151 | } 152 | return b 153 | } 154 | 155 | // parsereq is a very simple http request parser. This operation 156 | // waits for the entire payload to be buffered before returning a 157 | // valid request. 158 | func parsereq(data []byte, req *request) (leftover []byte, err error) { 159 | sdata := string(data) 160 | var i, s int 161 | var top string 162 | var clen int 163 | var q = -1 164 | // method, path, proto line 165 | for ; i < len(sdata); i++ { 166 | if sdata[i] == ' ' { 167 | req.method = sdata[s:i] 168 | for i, s = i+1, i+1; i < len(sdata); i++ { 169 | if sdata[i] == '?' && q == -1 { 170 | q = i - s 171 | } else if sdata[i] == ' ' { 172 | if q != -1 { 173 | req.path = sdata[s:q] 174 | req.query = req.path[q+1 : i] 175 | } else { 176 | req.path = sdata[s:i] 177 | } 178 | for i, s = i+1, i+1; i < len(sdata); i++ { 179 | if sdata[i] == '\n' && sdata[i-1] == '\r' { 180 | req.proto = sdata[s:i] 181 | i, s = i+1, i+1 182 | break 183 | } 184 | } 185 | break 186 | } 187 | } 188 | break 189 | } 190 | } 191 | if req.proto == "" { 192 | return data, fmt.Errorf("malformed request") 193 | } 194 | top = sdata[:s] 195 | for ; i < len(sdata); i++ { 196 | if i > 1 && sdata[i] == '\n' && sdata[i-1] == '\r' { 197 | line := sdata[s : i-1] 198 | s = i + 1 199 | if line == "" { 200 | req.head = sdata[len(top)+2 : i+1] 201 | i++ 202 | if clen > 0 { 203 | if len(sdata[i:]) < clen { 204 | break 205 | } 206 | req.body = sdata[i : i+clen] 207 | i += clen 208 | } 209 | return data[i:], nil 210 | } 211 | if strings.HasPrefix(line, "Content-Length:") { 212 | n, err := strconv.ParseInt(strings.TrimSpace(line[len("Content-Length:"):]), 10, 64) 213 | if err == nil { 214 | clen = int(n) 215 | } 216 | } 217 | } 218 | } 219 | // not enough data 220 | return data, nil 221 | } 222 | -------------------------------------------------------------------------------- /examples/redis-server/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Joshua J Baker. All rights reserved. 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "flag" 9 | "fmt" 10 | "log" 11 | "strings" 12 | "sync" 13 | 14 | "github.com/tidwall/evio" 15 | "github.com/tidwall/redcon" 16 | ) 17 | 18 | type conn struct { 19 | is evio.InputStream 20 | addr string 21 | } 22 | 23 | func main() { 24 | var port int 25 | var unixsocket string 26 | var stdlib bool 27 | var loops int 28 | var balance string 29 | flag.IntVar(&port, "port", 6380, "server port") 30 | flag.IntVar(&loops, "loops", 0, "num loops") 31 | flag.StringVar(&unixsocket, "unixsocket", "socket", "unix socket") 32 | flag.StringVar(&balance, "balance", "random", "random, round-robin, least-connections") 33 | flag.BoolVar(&stdlib, "stdlib", false, "use stdlib") 34 | flag.Parse() 35 | 36 | var mu sync.RWMutex 37 | var keys = make(map[string]string) 38 | var events evio.Events 39 | switch balance { 40 | default: 41 | log.Fatalf("invalid -balance flag: '%v'", balance) 42 | case "random": 43 | events.LoadBalance = evio.Random 44 | case "round-robin": 45 | events.LoadBalance = evio.RoundRobin 46 | case "least-connections": 47 | events.LoadBalance = evio.LeastConnections 48 | } 49 | events.NumLoops = loops 50 | events.Serving = func(srv evio.Server) (action evio.Action) { 51 | log.Printf("redis server started on port %d (loops: %d)", port, srv.NumLoops) 52 | if unixsocket != "" { 53 | log.Printf("redis server started at %s (loops: %d)", unixsocket, srv.NumLoops) 54 | } 55 | if stdlib { 56 | log.Printf("stdlib") 57 | } 58 | return 59 | } 60 | events.Opened = func(ec evio.Conn) (out []byte, opts evio.Options, action evio.Action) { 61 | //fmt.Printf("opened: %v\n", ec.RemoteAddr()) 62 | ec.SetContext(&conn{}) 63 | return 64 | } 65 | events.Closed = func(ec evio.Conn, err error) (action evio.Action) { 66 | // fmt.Printf("closed: %v\n", ec.RemoteAddr()) 67 | return 68 | } 69 | 70 | events.Data = func(ec evio.Conn, in []byte) (out []byte, action evio.Action) { 71 | if in == nil { 72 | log.Printf("wake from %s\n", ec.RemoteAddr()) 73 | return nil, evio.Close 74 | } 75 | c := ec.Context().(*conn) 76 | data := c.is.Begin(in) 77 | var n int 78 | var complete bool 79 | var err error 80 | var args [][]byte 81 | for action == evio.None { 82 | complete, args, _, data, err = redcon.ReadNextCommand(data, args[:0]) 83 | if err != nil { 84 | action = evio.Close 85 | out = redcon.AppendError(out, err.Error()) 86 | break 87 | } 88 | if !complete { 89 | break 90 | } 91 | if len(args) > 0 { 92 | n++ 93 | switch strings.ToUpper(string(args[0])) { 94 | default: 95 | out = redcon.AppendError(out, "ERR unknown command '"+string(args[0])+"'") 96 | case "PING": 97 | if len(args) > 2 { 98 | out = redcon.AppendError(out, "ERR wrong number of arguments for '"+string(args[0])+"' command") 99 | } else if len(args) == 2 { 100 | out = redcon.AppendBulk(out, args[1]) 101 | } else { 102 | out = redcon.AppendString(out, "PONG") 103 | } 104 | case "WAKE": 105 | go ec.Wake() 106 | out = redcon.AppendString(out, "OK") 107 | case "ECHO": 108 | if len(args) != 2 { 109 | out = redcon.AppendError(out, "ERR wrong number of arguments for '"+string(args[0])+"' command") 110 | } else { 111 | out = redcon.AppendBulk(out, args[1]) 112 | } 113 | case "SHUTDOWN": 114 | out = redcon.AppendString(out, "OK") 115 | action = evio.Shutdown 116 | case "QUIT": 117 | out = redcon.AppendString(out, "OK") 118 | action = evio.Close 119 | case "GET": 120 | if len(args) != 2 { 121 | out = redcon.AppendError(out, "ERR wrong number of arguments for '"+string(args[0])+"' command") 122 | } else { 123 | key := string(args[1]) 124 | mu.Lock() 125 | val, ok := keys[key] 126 | mu.Unlock() 127 | if !ok { 128 | out = redcon.AppendNull(out) 129 | } else { 130 | out = redcon.AppendBulkString(out, val) 131 | } 132 | } 133 | case "SET": 134 | if len(args) != 3 { 135 | out = redcon.AppendError(out, "ERR wrong number of arguments for '"+string(args[0])+"' command") 136 | } else { 137 | key, val := string(args[1]), string(args[2]) 138 | mu.Lock() 139 | keys[key] = val 140 | mu.Unlock() 141 | out = redcon.AppendString(out, "OK") 142 | } 143 | case "DEL": 144 | if len(args) < 2 { 145 | out = redcon.AppendError(out, "ERR wrong number of arguments for '"+string(args[0])+"' command") 146 | } else { 147 | var n int 148 | mu.Lock() 149 | for i := 1; i < len(args); i++ { 150 | if _, ok := keys[string(args[i])]; ok { 151 | n++ 152 | delete(keys, string(args[i])) 153 | } 154 | } 155 | mu.Unlock() 156 | out = redcon.AppendInt(out, int64(n)) 157 | } 158 | case "FLUSHDB": 159 | mu.Lock() 160 | keys = make(map[string]string) 161 | mu.Unlock() 162 | out = redcon.AppendString(out, "OK") 163 | } 164 | } 165 | } 166 | c.is.End(data) 167 | return 168 | } 169 | var ssuf string 170 | if stdlib { 171 | ssuf = "-net" 172 | } 173 | addrs := []string{fmt.Sprintf("tcp"+ssuf+"://:%d", port)} 174 | if unixsocket != "" { 175 | addrs = append(addrs, fmt.Sprintf("unix"+ssuf+"://%s", unixsocket)) 176 | } 177 | err := evio.Serve(events, addrs...) 178 | if err != nil { 179 | log.Fatal(err) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tidwall/evio 2 | 3 | go 1.15 4 | 5 | require github.com/kavu/go_reuseport v1.5.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/kavu/go_reuseport v1.5.0 h1:UNuiY2OblcqAtVDE8Gsg1kZz8zbBWg907sP1ceBV+bk= 2 | github.com/kavu/go_reuseport v1.5.0/go.mod h1:CG8Ee7ceMFSMnx/xr25Vm0qXaj2Z4i5PWoUx+JZ5/CU= 3 | -------------------------------------------------------------------------------- /internal/internal_bsd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Joshua J Baker. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build darwin netbsd freebsd openbsd dragonfly 6 | 7 | package internal 8 | 9 | import ( 10 | "syscall" 11 | ) 12 | 13 | // Poll ... 14 | type Poll struct { 15 | fd int 16 | changes []syscall.Kevent_t 17 | notes noteQueue 18 | } 19 | 20 | // OpenPoll ... 21 | func OpenPoll() *Poll { 22 | l := new(Poll) 23 | p, err := syscall.Kqueue() 24 | if err != nil { 25 | panic(err) 26 | } 27 | l.fd = p 28 | _, err = syscall.Kevent(l.fd, []syscall.Kevent_t{{ 29 | Ident: 0, 30 | Filter: syscall.EVFILT_USER, 31 | Flags: syscall.EV_ADD | syscall.EV_CLEAR, 32 | }}, nil, nil) 33 | if err != nil { 34 | panic(err) 35 | } 36 | 37 | return l 38 | } 39 | 40 | // Close ... 41 | func (p *Poll) Close() error { 42 | return syscall.Close(p.fd) 43 | } 44 | 45 | // Trigger ... 46 | func (p *Poll) Trigger(note interface{}) error { 47 | p.notes.Add(note) 48 | _, err := syscall.Kevent(p.fd, []syscall.Kevent_t{{ 49 | Ident: 0, 50 | Filter: syscall.EVFILT_USER, 51 | Fflags: syscall.NOTE_TRIGGER, 52 | }}, nil, nil) 53 | return err 54 | } 55 | 56 | // Wait ... 57 | func (p *Poll) Wait(iter func(fd int, note interface{}) error) error { 58 | events := make([]syscall.Kevent_t, 128) 59 | for { 60 | n, err := syscall.Kevent(p.fd, p.changes, events, nil) 61 | if err != nil && err != syscall.EINTR { 62 | return err 63 | } 64 | p.changes = p.changes[:0] 65 | if err := p.notes.ForEach(func(note interface{}) error { 66 | return iter(0, note) 67 | }); err != nil { 68 | return err 69 | } 70 | for i := 0; i < n; i++ { 71 | if fd := int(events[i].Ident); fd != 0 { 72 | if err := iter(fd, nil); err != nil { 73 | return err 74 | } 75 | } 76 | } 77 | } 78 | } 79 | 80 | // AddRead ... 81 | func (p *Poll) AddRead(fd int) { 82 | p.changes = append(p.changes, 83 | syscall.Kevent_t{ 84 | Ident: uint64(fd), Flags: syscall.EV_ADD, Filter: syscall.EVFILT_READ, 85 | }, 86 | ) 87 | } 88 | 89 | // AddReadWrite ... 90 | func (p *Poll) AddReadWrite(fd int) { 91 | p.changes = append(p.changes, 92 | syscall.Kevent_t{ 93 | Ident: uint64(fd), Flags: syscall.EV_ADD, Filter: syscall.EVFILT_READ, 94 | }, 95 | syscall.Kevent_t{ 96 | Ident: uint64(fd), Flags: syscall.EV_ADD, Filter: syscall.EVFILT_WRITE, 97 | }, 98 | ) 99 | } 100 | 101 | // ModRead ... 102 | func (p *Poll) ModRead(fd int) { 103 | p.changes = append(p.changes, syscall.Kevent_t{ 104 | Ident: uint64(fd), Flags: syscall.EV_DELETE, Filter: syscall.EVFILT_WRITE, 105 | }) 106 | } 107 | 108 | // ModReadWrite ... 109 | func (p *Poll) ModReadWrite(fd int) { 110 | p.changes = append(p.changes, syscall.Kevent_t{ 111 | Ident: uint64(fd), Flags: syscall.EV_ADD, Filter: syscall.EVFILT_WRITE, 112 | }) 113 | } 114 | 115 | // ModDetach ... 116 | func (p *Poll) ModDetach(fd int) { 117 | p.changes = append(p.changes, 118 | syscall.Kevent_t{ 119 | Ident: uint64(fd), Flags: syscall.EV_DELETE, Filter: syscall.EVFILT_READ, 120 | }, 121 | syscall.Kevent_t{ 122 | Ident: uint64(fd), Flags: syscall.EV_DELETE, Filter: syscall.EVFILT_WRITE, 123 | }, 124 | ) 125 | } 126 | -------------------------------------------------------------------------------- /internal/internal_darwin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Joshua J Baker. All rights reserved. 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package internal 6 | 7 | import "syscall" 8 | 9 | // SetKeepAlive sets the keepalive for the connection 10 | func SetKeepAlive(fd, secs int) error { 11 | if err := syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, 0x8, 1); err != nil { 12 | return err 13 | } 14 | switch err := syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, 0x101, secs); err { 15 | case nil, syscall.ENOPROTOOPT: // OS X 10.7 and earlier don't support this option 16 | default: 17 | return err 18 | } 19 | return syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, syscall.TCP_KEEPALIVE, secs) 20 | } 21 | -------------------------------------------------------------------------------- /internal/internal_linux.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Joshua J Baker. All rights reserved. 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package internal 6 | 7 | import ( 8 | "syscall" 9 | "unsafe" 10 | ) 11 | 12 | // Poll ... 13 | type Poll struct { 14 | fd int // epoll fd 15 | wfd int // wake fd 16 | notes noteQueue 17 | } 18 | 19 | // OpenPoll ... 20 | func OpenPoll() *Poll { 21 | l := new(Poll) 22 | p, err := syscall.EpollCreate1(0) 23 | if err != nil { 24 | panic(err) 25 | } 26 | l.fd = p 27 | r0, _, e0 := syscall.Syscall(syscall.SYS_EVENTFD2, 0, 0, 0) 28 | if e0 != 0 { 29 | syscall.Close(p) 30 | panic(err) 31 | } 32 | l.wfd = int(r0) 33 | l.AddRead(l.wfd) 34 | return l 35 | } 36 | 37 | // Close ... 38 | func (p *Poll) Close() error { 39 | if err := syscall.Close(p.wfd); err != nil { 40 | return err 41 | } 42 | return syscall.Close(p.fd) 43 | } 44 | 45 | // Trigger ... 46 | func (p *Poll) Trigger(note interface{}) error { 47 | p.notes.Add(note) 48 | var x uint64 = 1 49 | _, err := syscall.Write(p.wfd, (*(*[8]byte)(unsafe.Pointer(&x)))[:]) 50 | return err 51 | } 52 | 53 | // Wait ... 54 | func (p *Poll) Wait(iter func(fd int, note interface{}) error) error { 55 | events := make([]syscall.EpollEvent, 64) 56 | for { 57 | n, err := syscall.EpollWait(p.fd, events, 100) 58 | if err != nil && err != syscall.EINTR { 59 | return err 60 | } 61 | if err := p.notes.ForEach(func(note interface{}) error { 62 | return iter(0, note) 63 | }); err != nil { 64 | return err 65 | } 66 | for i := 0; i < n; i++ { 67 | if fd := int(events[i].Fd); fd != p.wfd { 68 | if err := iter(fd, nil); err != nil { 69 | return err 70 | } 71 | } else if fd == p.wfd { 72 | var data [8]byte 73 | syscall.Read(p.wfd, data[:]) 74 | } 75 | } 76 | } 77 | } 78 | 79 | // AddReadWrite ... 80 | func (p *Poll) AddReadWrite(fd int) { 81 | if err := syscall.EpollCtl(p.fd, syscall.EPOLL_CTL_ADD, fd, 82 | &syscall.EpollEvent{Fd: int32(fd), 83 | Events: syscall.EPOLLIN | syscall.EPOLLOUT, 84 | }, 85 | ); err != nil { 86 | panic(err) 87 | } 88 | } 89 | 90 | // AddRead ... 91 | func (p *Poll) AddRead(fd int) { 92 | if err := syscall.EpollCtl(p.fd, syscall.EPOLL_CTL_ADD, fd, 93 | &syscall.EpollEvent{Fd: int32(fd), 94 | Events: syscall.EPOLLIN, 95 | }, 96 | ); err != nil { 97 | panic(err) 98 | } 99 | } 100 | 101 | // ModRead ... 102 | func (p *Poll) ModRead(fd int) { 103 | if err := syscall.EpollCtl(p.fd, syscall.EPOLL_CTL_MOD, fd, 104 | &syscall.EpollEvent{Fd: int32(fd), 105 | Events: syscall.EPOLLIN, 106 | }, 107 | ); err != nil { 108 | panic(err) 109 | } 110 | } 111 | 112 | // ModReadWrite ... 113 | func (p *Poll) ModReadWrite(fd int) { 114 | if err := syscall.EpollCtl(p.fd, syscall.EPOLL_CTL_MOD, fd, 115 | &syscall.EpollEvent{Fd: int32(fd), 116 | Events: syscall.EPOLLIN | syscall.EPOLLOUT, 117 | }, 118 | ); err != nil { 119 | panic(err) 120 | } 121 | } 122 | 123 | // ModDetach ... 124 | func (p *Poll) ModDetach(fd int) { 125 | if err := syscall.EpollCtl(p.fd, syscall.EPOLL_CTL_DEL, fd, 126 | &syscall.EpollEvent{Fd: int32(fd), 127 | Events: syscall.EPOLLIN | syscall.EPOLLOUT, 128 | }, 129 | ); err != nil { 130 | panic(err) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /internal/internal_openbsd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Joshua J Baker. All rights reserved. 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package internal 6 | 7 | // SetKeepAlive sets the keepalive for the connection 8 | func SetKeepAlive(fd, secs int) error { 9 | // OpenBSD has no user-settable per-socket TCP keepalive options. 10 | return nil 11 | } 12 | -------------------------------------------------------------------------------- /internal/internal_unix.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Joshua J Baker. All rights reserved. 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build netbsd freebsd dragonfly linux 6 | 7 | package internal 8 | 9 | import "syscall" 10 | 11 | func SetKeepAlive(fd, secs int) error { 12 | if err := syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_KEEPALIVE, 1); err != nil { 13 | return err 14 | } 15 | if err := syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, syscall.TCP_KEEPINTVL, secs); err != nil { 16 | return err 17 | } 18 | return syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, syscall.TCP_KEEPIDLE, secs) 19 | } 20 | -------------------------------------------------------------------------------- /internal/notequeue.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Joshua J Baker. All rights reserved. 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package internal 6 | 7 | import "sync" 8 | 9 | // this is a good candiate for a lock-free structure. 10 | 11 | type noteQueue struct { 12 | mu sync.Mutex 13 | notes []interface{} 14 | } 15 | 16 | func (q *noteQueue) Add(note interface{}) (one bool) { 17 | q.mu.Lock() 18 | q.notes = append(q.notes, note) 19 | n := len(q.notes) 20 | q.mu.Unlock() 21 | return n == 1 22 | } 23 | 24 | func (q *noteQueue) ForEach(iter func(note interface{}) error) error { 25 | q.mu.Lock() 26 | if len(q.notes) == 0 { 27 | q.mu.Unlock() 28 | return nil 29 | } 30 | notes := q.notes 31 | q.notes = nil 32 | q.mu.Unlock() 33 | for _, note := range notes { 34 | if err := iter(note); err != nil { 35 | return err 36 | } 37 | } 38 | q.mu.Lock() 39 | if q.notes == nil { 40 | for i := range notes { 41 | notes[i] = nil 42 | } 43 | q.notes = notes[:0] 44 | } 45 | q.mu.Unlock() 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /internal/socktoaddr.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Joshua J Baker. All rights reserved. 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package internal 6 | 7 | import ( 8 | "net" 9 | "syscall" 10 | ) 11 | 12 | // SockaddrToAddr returns a go/net friendly address 13 | func SockaddrToAddr(sa syscall.Sockaddr) net.Addr { 14 | var a net.Addr 15 | switch sa := sa.(type) { 16 | case *syscall.SockaddrInet4: 17 | a = &net.TCPAddr{ 18 | IP: append([]byte{}, sa.Addr[:]...), 19 | Port: sa.Port, 20 | } 21 | case *syscall.SockaddrInet6: 22 | var zone string 23 | if sa.ZoneId != 0 { 24 | if ifi, err := net.InterfaceByIndex(int(sa.ZoneId)); err == nil { 25 | zone = ifi.Name 26 | } 27 | } 28 | if zone == "" && sa.ZoneId != 0 { 29 | } 30 | a = &net.TCPAddr{ 31 | IP: append([]byte{}, sa.Addr[:]...), 32 | Port: sa.Port, 33 | Zone: zone, 34 | } 35 | case *syscall.SockaddrUnix: 36 | a = &net.UnixAddr{Net: "unix", Name: sa.Name} 37 | } 38 | return a 39 | } 40 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidwall/evio/fe6081760191618105c0671e6aa05f06e7c6e5a9/logo.png --------------------------------------------------------------------------------