The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .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


--------------------------------------------------------------------------------