Add 'context.OnConnectionClose(callbackFn) bool' and 'context.OnClose(callbackFn)' and give a use case example. More on this path later on, stay tuned.

Former-commit-id: dc6580f072d076b8cb204a681e45905210981153
This commit is contained in:
Gerasimos (Makis) Maropoulos 2018-08-02 17:46:35 +03:00
parent d98da25ffb
commit 5d9ded37c4
9 changed files with 243 additions and 60 deletions

View File

@ -6,6 +6,57 @@ This folder provides easy to understand code snippets on how to get started with
It doesn't always contain the "best ways" but it does cover each important feature that will make you so excited to GO with iris!
## Running the examples
1. Install the Go Programming Language, version 1.9+ from [here](https://golang.org/dl).
2. Install Iris: `go get -u github.com/kataras/iris`
3. Install any external packages that required by the examples
<details>
<summary>External packages</summary>
```bash
cd _examples && go get ./...
# or
go get github.com/iris-contrib/middleware/...
go get github.com/betacraft/yaag/irisyaag
go get github.com/markbates/goth/...
go get github.com/getsentry/raven-go/...
go get github.com/casbin/casbin
go get github.com/markbates/goth/...
go get github.com/aws/aws-sdk-go/...
go get github.com/getsentry/raven-go/...
go get github.com/casbin/casbin
go get github.com/aws/aws-sdk-go/...
go get github.com/prometheus/client_golang/...
go get github.com/didip/tollbooth
go get github.com/valyala/quicktemplate
go get github.com/shiyanhui/hero
go get github.com/go-xorm/xorm
go get github.com/nfnt/resize
go get github.com/prometheus/client_golang/...
go get github.com/didip/tollbooth
go get github.com/valyala/quicktemplate
go get github.com/shiyanhui/hero
go get github.com/go-xorm/xorm
go get github.com/nfnt/resize
go get github.com/dgrijalva/jwt-go
go get github.com/newrelic/go-agent
go get github.com/valyala/tcplisten
go get github.com/kataras/bindata/cmd/bindata
```
</details>
And execute
```sh
$ cd $GOPATH/src/github.com/kataras/iris/_examples/overview
$ go run main.go
```
> Test the examples by opening a terminal window and execute: `GOCACHE=off && cd _examples && go test -v ./...`
### Overview
- [Hello world!](hello-world/main.go)

View File

@ -54,7 +54,7 @@ func (r resource) loadFromBase(dir string) string {
result := string(b)
if runtime.GOOS != "windows" {
result = strings.Replace(result, "\n", "\r\n", -1)
// result = strings.Replace(result, "\n", "\r\n", -1)
}
return result
}

View File

@ -1,8 +1,8 @@
// Code generated by bindata. DO NOT EDIT.
// sources:
// assets\css\bootstrap.min.css
// assets\favicon.ico
// assets\js\jquery-2.1.1.js
// assets/css/bootstrap.min.css
// assets/favicon.ico
// assets/js/jquery-2.1.1.js
package main
@ -55,7 +55,7 @@ func (fi gzipBindataFileInfo) Sys() interface{} {
return nil
}
var _gzipBindataAssetscssbootstrapmincss = []byte(
var _gzipBindataAssetsCssBootstrapmincss = []byte(
"\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xec\xbd\xef\x8f\xe3\x38\x92\x20\xfa\xb9\x1a\xe8\xff\x41\x5b\x8d\x41\x57" +
"\x4d\x59\x2e\xf9\x77\xda\x89\xce\xb7\xfb\xe6\x0e\xb7\x03\xdc\xec\x97\x9b\x0f\x07\xf4\xf4\x7b\xa0\x25\xda\xd6\x94" +
"\x2c\x69\x24\x39\x2b\xb3\xfb\xf6\xfe\xf6\x83\xf8\x4b\x41\x32\x48\xd1\x4e\x77\xcf\xdc\x62\xb7\xee\x7a\x9c\x62\x44" +
@ -806,14 +806,14 @@ var _gzipBindataAssetscssbootstrapmincss = []byte(
"\x6f\x60\x83\x31\xfd\x46\x3e\xae\xb6\x0e\x28\xfe\xfd\xe4\x72\x0f\x83\x02\x88\x8b\x01\x82\xf5\x3c\x3e\x46\xfe\x4f" +
"\x00\x00\x00\xff\xff\x47\x37\xb0\x07\xc9\x28\x02\x00")
func gzipBindataAssetscssbootstrapmincss() (*gzipAsset, error) {
bytes := _gzipBindataAssetscssbootstrapmincss
func gzipBindataAssetsCssBootstrapmincss() (*gzipAsset, error) {
bytes := _gzipBindataAssetsCssBootstrapmincss
info := gzipBindataFileInfo{
name: "assets/css/bootstrap.min.css",
size: 141513,
md5checksum: "",
mode: os.FileMode(438),
modTime: time.Unix(1520997241, 0),
mode: os.FileMode(511),
modTime: time.Unix(1521004692, 0),
}
a := &gzipAsset{bytes: bytes, info: info}
@ -821,7 +821,7 @@ func gzipBindataAssetscssbootstrapmincss() (*gzipAsset, error) {
return a, nil
}
var _gzipBindataAssetsfaviconico = []byte(
var _gzipBindataAssetsFaviconico = []byte(
"\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xec\x5a\x0b\x70\x55\xc7\x79\xde\x7b\xce\x45\x12\xb2\x90\xc4\xc3\xe6\x61" +
"\x3b\x90\xf8\x31\xc4\x19\x6c\x32\xe3\xc4\x34\xe3\xc6\x34\x6d\xed\xd4\x69\x62\x32\x49\x9a\xa6\x75\xea\x36\x33\xee" +
"\xd8\x9e\xb4\x75\xdc\x7a\xa6\xc5\x31\x02\xa6\xd3\x84\x47\x28\x6f\x3b\xc6\x3c\xcc\xeb\x9e\xbd\x08\x21\x04\x92\x28" +
@ -967,14 +967,14 @@ var _gzipBindataAssetsfaviconico = []byte(
"\x0c\x24\x28\xcd\x41\x65\xd4\x2a\xc1\x2e\x17\x0e\x2a\x5b\xac\xb2\xc7\x2a\x4d\xab\x4c\xa1\xdf\xff\x0f\x00\x00\xff" +
"\xff\xc6\xb9\x24\x2f\xee\x3a\x00\x00")
func gzipBindataAssetsfaviconico() (*gzipAsset, error) {
bytes := _gzipBindataAssetsfaviconico
func gzipBindataAssetsFaviconico() (*gzipAsset, error) {
bytes := _gzipBindataAssetsFaviconico
info := gzipBindataFileInfo{
name: "assets/favicon.ico",
size: 15086,
md5checksum: "",
mode: os.FileMode(438),
modTime: time.Unix(1520997241, 0),
mode: os.FileMode(511),
modTime: time.Unix(1521004692, 0),
}
a := &gzipAsset{bytes: bytes, info: info}
@ -982,7 +982,7 @@ func gzipBindataAssetsfaviconico() (*gzipAsset, error) {
return a, nil
}
var _gzipBindataAssetsjsjquery211js = []byte(
var _gzipBindataAssetsJsJquery211js = []byte(
"\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcc\xfd\x7b\x77\xdb\x46\x96\x28\x8a\xff\x2d\xaf\xe5\xef\x50\xa2\x73\x64" +
"\xd0\xe2\x43\xb2\x93\x74\x42\x99\xd6\xcf\xb1\x9d\x1e\x9d\x5f\xec\xb8\x23\x67\x32\xf7\x4a\xca\xa4\x48\x14\xc5\xb2" +
"\x41\x80\x41\x81\x92\xd8\xa1\xfa\xb3\xdf\xb5\x1f\xf5\x02\x40\xd9\xee\x4e\xcf\x39\x3d\x6b\x62\x11\x28\xd4\x63\xd7" +
@ -3634,14 +3634,14 @@ var _gzipBindataAssetsjsjquery211js = []byte(
"\xfa\x6a\x18\xef\xb5\xaa\xc6\xb4\x04\x30\x01\x3d\xd6\x92\x74\x16\xc4\xcc\x86\x81\x87\x9d\x94\xce\x89\xbb\xe5\xdc" +
"\x66\xfb\xff\x03\x00\x00\xff\xff\x1d\x30\x27\xdd\x1d\xea\x03\x00")
func gzipBindataAssetsjsjquery211js() (*gzipAsset, error) {
bytes := _gzipBindataAssetsjsjquery211js
func gzipBindataAssetsJsJquery211js() (*gzipAsset, error) {
bytes := _gzipBindataAssetsJsJquery211js
info := gzipBindataFileInfo{
name: "assets/js/jquery-2.1.1.js",
size: 256541,
md5checksum: "",
mode: os.FileMode(438),
modTime: time.Unix(1520997241, 0),
mode: os.FileMode(511),
modTime: time.Unix(1521004692, 0),
}
a := &gzipAsset{bytes: bytes, info: info}
@ -3706,9 +3706,9 @@ func GzipAssetNames() []string {
// _gzipbindata is a table, holding each asset generator, mapped to its name.
//
var _gzipbindata = map[string]func() (*gzipAsset, error){
"assets/css/bootstrap.min.css": gzipBindataAssetscssbootstrapmincss,
"assets/favicon.ico": gzipBindataAssetsfaviconico,
"assets/js/jquery-2.1.1.js": gzipBindataAssetsjsjquery211js,
"assets/css/bootstrap.min.css": gzipBindataAssetsCssBootstrapmincss,
"assets/favicon.ico": gzipBindataAssetsFaviconico,
"assets/js/jquery-2.1.1.js": gzipBindataAssetsJsJquery211js,
}
@ -3764,11 +3764,11 @@ type gzipBintree struct {
var _gzipbintree = &gzipBintree{Func: nil, Children: map[string]*gzipBintree{
"assets": {Func: nil, Children: map[string]*gzipBintree{
"css": {Func: nil, Children: map[string]*gzipBintree{
"bootstrap.min.css": {Func: gzipBindataAssetscssbootstrapmincss, Children: map[string]*gzipBintree{}},
"bootstrap.min.css": {Func: gzipBindataAssetsCssBootstrapmincss, Children: map[string]*gzipBintree{}},
}},
"favicon.ico": {Func: gzipBindataAssetsfaviconico, Children: map[string]*gzipBintree{}},
"favicon.ico": {Func: gzipBindataAssetsFaviconico, Children: map[string]*gzipBintree{}},
"js": {Func: nil, Children: map[string]*gzipBintree{
"jquery-2.1.1.js": {Func: gzipBindataAssetsjsjquery211js, Children: map[string]*gzipBintree{}},
"jquery-2.1.1.js": {Func: gzipBindataAssetsJsJquery211js, Children: map[string]*gzipBintree{}},
}},
}},
}}

View File

@ -55,7 +55,7 @@ func (r resource) loadFromBase(dir string) string {
result := string(b)
if runtime.GOOS != "windows" {
result = strings.Replace(result, "\n", "\r\n", -1)
// result = strings.Replace(result, "\n", "\r\n", -1)
}
return result
}
@ -98,8 +98,11 @@ func TestEmbeddingGzipFilesIntoApp(t *testing.T) {
}
buf := new(bytes.Buffer)
reader.WriteTo(buf)
if rawContents != buf.String() {
t.Fatalf("[%d] of '%s': expected body:\n%s but got:\n%s", i, url, rawContents, buf.String())
if expected, got := rawContents, buf.String(); expected != got {
// t.Fatalf("[%d] of '%s': expected body:\n%s but got:\n%s", i, url, expected, got)
// let's reduce the output here...
// they are big files, no need to check for length here.
t.Fatalf("[%d] %s, expected body to look like: '%s...%s' but got '%s...%s'", i, url, expected[:40], expected[len(rawContents)-40:], got[:40], got[len(got)-40:])
}
}()
}

View File

@ -47,7 +47,7 @@ func (r resource) loadFromBase(dir string) string {
}
result := string(b)
if runtime.GOOS != "windows" {
result = strings.Replace(result, "\n", "\r\n", -1)
// result = strings.Replace(result, "\n", "\r\n", -1)
}
return result
}

View File

@ -6,11 +6,9 @@ package main
import (
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/kataras/golog"
"github.com/kataras/iris"
// Note:
// For some reason the latest vscode-go language extension does not provide enough intelligence (parameters documentation and go to definition features)
@ -80,10 +78,10 @@ func (b *Broker) listen() {
func (b *Broker) ServeHTTP(ctx context.Context) {
// Make sure that the writer supports flushing.
//
flusher, ok := ctx.ResponseWriter().(http.Flusher)
flusher, ok := ctx.ResponseWriter().Flusher()
if !ok {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.StatusCode(iris.StatusHTTPVersionNotSupported)
ctx.WriteString("Streaming unsupported!")
return
}
@ -102,19 +100,27 @@ func (b *Broker) ServeHTTP(ctx context.Context) {
// Signal the broker that we have a new connection.
b.newClients <- messageChan
// Remove this client from the map of connected clients
// when this handler exits.
defer func() {
b.closingClients <- messageChan
}()
// Listen to connection close or when the entire request handler chain exits and un-register messageChan.
// using the `ctx.ResponseWriter().CloseNotifier()` and `defer` for this single handler of the route:
/*
notifier, ok := ctx.ResponseWriter().CloseNotifier()
if ok {
go func() {
<-notifier.CloseNotify()
b.closingClients <- messageChan
}()
}
// Listen to connection close and un-register messageChan.
notify := ctx.ResponseWriter().(http.CloseNotifier).CloseNotify()
go func() {
<-notify
defer func() {
b.closingClients <- messageChan
}()
*/
// or by using the `ctx.OnClose`, which will take care all of the above for you:
ctx.OnClose(func() {
// Remove this client from the map of connected clients
// when this handler exits.
b.closingClients <- messageChan
}()
})
// block waiting for messages broadcast on this connection's messageChan.
for {
@ -164,5 +170,5 @@ func main() {
// TIP: If you make use of it inside a web frontend application
// then checkout the "optional.sse.js.html" to use the javascript's API for SSE,
// it will also remove the browser's "loading" indicator while receiving those event messages.
app.Run(iris.Addr(":8080"))
app.Run(iris.Addr(":8080"), iris.WithoutServerError(iris.ErrServerClosed))
}

View File

@ -1,4 +1,5 @@
// +build go1.11beta2
package main
import (

View File

@ -340,6 +340,32 @@ type Context interface {
// IsStopped checks and returns true if the current position of the Context is 255,
// means that the StopExecution() was called.
IsStopped() bool
// OnConnectionClose registers the "cb" function which will fire (on its own goroutine, no need to be registered goroutine by the end-dev)
// when the underlying connection has gone away.
//
// This mechanism can be used to cancel long operations on the server
// if the client has disconnected before the response is ready.
//
// It depends on the `http#CloseNotify`.
// CloseNotify may wait to notify until Request.Body has been
// fully read.
//
// After the main Handler has returned, there is no guarantee
// that the channel receives a value.
//
// Finally, it reports whether the protocol supports pipelines (HTTP/1.1 with pipelines disabled is not supported).
// The "cb" will not fire for sure if the output value is false.
//
// Note that you can register only one callback for the entire request handler chain/per route.
//
// Look the `ResponseWriter#CloseNotifier` for more.
OnConnectionClose(fnGoroutine func()) bool
// OnClose registers the callback function "cb" to the underline connection closing event using the `Context#OnConnectionClose`
// and also in the end of the request handler using the `ResponseWriter#SetBeforeFlush`.
// Note that you can register only one callback for the entire request handler chain/per route.
//
// Look the `Context#OnConnectionClose` and `ResponseWriter#SetBeforeFlush` for more.
OnClose(cb func())
// +------------------------------------------------------------+
// | Current "user/request" storage |
@ -1355,6 +1381,76 @@ func (ctx *context) IsStopped() bool {
return ctx.currentHandlerIndex == stopExecutionIndex
}
// OnConnectionClose registers the "cb" function which will fire (on its own goroutine, no need to be registered goroutine by the end-dev)
// when the underlying connection has gone away.
//
// This mechanism can be used to cancel long operations on the server
// if the client has disconnected before the response is ready.
//
// It depends on the `http#CloseNotify`.
// CloseNotify may wait to notify until Request.Body has been
// fully read.
//
// After the main Handler has returned, there is no guarantee
// that the channel receives a value.
//
// Finally, it reports whether the protocol supports pipelines (HTTP/1.1 with pipelines disabled is not supported).
// The "cb" will not fire for sure if the output value is false.
//
// Note that you can register only one callback for the entire request handler chain/per route.
//
// Look the `ResponseWriter#CloseNotifier` for more.
func (ctx *context) OnConnectionClose(cb func()) bool {
// Note that `ctx.ResponseWriter().CloseNotify()` can already do the same
// but it returns a channel which will never fire if it the protocol version is not compatible,
// here we don't want to allocate an empty channel, just skip it.
notifier, ok := ctx.writer.CloseNotifier()
if !ok {
return false
}
notify := notifier.CloseNotify()
go func() {
<-notify
if cb != nil {
cb()
}
}()
return true
}
// OnClose registers the callback function "cb" to the underline connection closing event using the `Context#OnConnectionClose`
// and also in the end of the request handler using the `ResponseWriter#SetBeforeFlush`.
// Note that you can register only one callback for the entire request handler chain/per route.
//
// Look the `Context#OnConnectionClose` and `ResponseWriter#SetBeforeFlush` for more.
func (ctx *context) OnClose(cb func()) {
if cb == nil {
return
}
// Register the on underline connection close handler first.
ctx.OnConnectionClose(cb)
// Author's notes:
// This is fired on `ctx.ResponseWriter().FlushResponse()` which is fired by the framework automatically, internally, on the end of request handler(s),
// it is not fired on the underline streaming function of the writer: `ctx.ResponseWriter().Flush()` (which can be fired more than one if streaming is supported by the client).
// The `FlushResponse` is called only once, so add the "cb" here, no need to add done request handlers each time `OnClose` is called by the end-dev.
//
// Don't allow more than one because we don't allow that on `OnConnectionClose` too:
// old := ctx.writer.GetBeforeFlush()
// if old != nil {
// ctx.writer.SetBeforeFlush(func() {
// old()
// cb()
// })
// return
// }
ctx.writer.SetBeforeFlush(cb)
}
// +------------------------------------------------------------+
// | Current "user/request" storage |
// | and share information between the handlers - Values(). |

View File

@ -83,6 +83,21 @@ type ResponseWriter interface {
// WiteTo writes a response writer (temp: status code, headers and body) to another response writer
WriteTo(ResponseWriter)
// Flusher indicates if `Flush` is supported by the client.
//
// The default HTTP/1.x and HTTP/2 ResponseWriter implementations
// support Flusher, but ResponseWriter wrappers may not. Handlers
// should always test for this ability at runtime.
//
// Note that even for ResponseWriters that support Flush,
// if the client is connected through an HTTP proxy,
// the buffered data may not reach the client until the response
// completes.
Flusher() (http.Flusher, bool)
// CloseNotifier indicates if the protocol supports the underline connection closure notification.
CloseNotifier() (http.CloseNotifier, bool)
}
// +------------------------------------------------------------+
@ -296,21 +311,25 @@ func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return nil, nil, errors.New("hijack is not supported by this ResponseWriter")
}
// Flusher indicates if `Flush` is supported by the client.
//
// The default HTTP/1.x and HTTP/2 ResponseWriter implementations
// support Flusher, but ResponseWriter wrappers may not. Handlers
// should always test for this ability at runtime.
//
// Note that even for ResponseWriters that support Flush,
// if the client is connected through an HTTP proxy,
// the buffered data may not reach the client until the response
// completes.
func (w *responseWriter) Flusher() (http.Flusher, bool) {
flusher, canFlush := w.ResponseWriter.(http.Flusher)
return flusher, canFlush
}
// Flush sends any buffered data to the client.
func (w *responseWriter) Flush() {
// The Flusher interface is implemented by ResponseWriters that allow
// an HTTP handler to flush buffered data to the client.
//
// The default HTTP/1.x and HTTP/2 ResponseWriter implementations
// support Flusher, but ResponseWriter wrappers may not. Handlers
// should always test for this ability at runtime.
//
// Note that even for ResponseWriters that support Flush,
// if the client is connected through an HTTP proxy,
// the buffered data may not reach the client until the response
// completes.
if fl, isFlusher := w.ResponseWriter.(http.Flusher); isFlusher {
fl.Flush()
if flusher, ok := w.Flusher(); ok {
flusher.Flush()
}
}
@ -349,6 +368,12 @@ func (w *responseWriter) Push(target string, opts *http.PushOptions) error {
return ErrPushNotSupported
}
// CloseNotifier indicates if the protocol supports the underline connection closure notification.
func (w *responseWriter) CloseNotifier() (http.CloseNotifier, bool) {
notifier, supportsCloseNotify := w.ResponseWriter.(http.CloseNotifier)
return notifier, supportsCloseNotify
}
// CloseNotify returns a channel that receives at most a
// single value (true) when the client connection has gone
// away.
@ -368,9 +393,10 @@ func (w *responseWriter) Push(target string, opts *http.PushOptions) error {
// is a problem, use HTTP/2 or only use CloseNotify on methods
// such as POST.
func (w *responseWriter) CloseNotify() <-chan bool {
if notifier, supportsCloseNotify := w.ResponseWriter.(http.CloseNotifier); supportsCloseNotify {
if notifier, ok := w.CloseNotifier(); ok {
return notifier.CloseNotify()
}
ch := make(chan bool, 1)
return ch
}