Add HTTP/2 Example and Websocket wss:// too in the same time :)

Former-commit-id: fd4c12043d6ed739770236e014ccd2f0f4f5a84c
This commit is contained in:
Gerasimos (Makis) Maropoulos 2017-02-17 06:49:54 +02:00
parent 5055546a38
commit 48b470f5da
6 changed files with 353 additions and 54 deletions

View File

@ -11,7 +11,8 @@ Users already notified for some breaking-changes, this section will help you
to adapt the new changes to your application, it contains an overview of the new features too.
- Shutdown with `app.Shutdown(context.Context) error`, no need for any third-parties, with `EventPolicy.Interrupted` and Go's 1.8 Gracefully Shutdown feature you're ready to go!
- HTTP/2 Go 1.8 `context.Push(target string, opts *http.PushOptions) error` is supported
- HTTP/2 Go 1.8 `context.Push(target string, opts *http.PushOptions) error` is supported, example can be found [here](https://github.com/kataras/iris.v6/blob/master/adaptors/websocket/_examples/webocket_secure/main.go)
- Router (two lines to add, new features)
- Template engines (two lines to add, same features as before, except their easier configuration)
- Basic middleware, that have been written by me, are transfared to the main repository[/middleware](https://github.com/kataras/iris/tree/master/middleware) with a lot of improvements to the `recover middleware` (see the next)

View File

@ -0,0 +1,197 @@
package main
import (
"fmt" // optional
"io/ioutil" // optional
"os" // optional
"time" // optional
"gopkg.in/kataras/iris.v6"
"gopkg.in/kataras/iris.v6/adaptors/httprouter"
"gopkg.in/kataras/iris.v6/adaptors/view"
"gopkg.in/kataras/iris.v6/adaptors/websocket"
)
type clientPage struct {
Title string
Host string
}
func main() {
app := iris.New()
app.Adapt(iris.DevLogger()) // enable all (error) logs
app.Adapt(httprouter.New()) // select the httprouter as the servemux
app.Adapt(view.HTML("./templates", ".html")) // select the html engine to serve templates
ws := websocket.New(websocket.Config{
// the path which the websocket client should listen/registed to,
Endpoint: "/my_endpoint",
// the client-side javascript static file path
// which will be served by Iris.
// default is /iris-ws.js
// if you change that you have to change the bottom of templates/client.html
// script tag:
ClientSourcePath: "/iris-ws.js",
//
// Set the timeouts, 0 means no timeout
// websocket has more configuration, go to ../../config.go for more:
// WriteTimeout: 0,
// ReadTimeout: 0,
// by-default all origins are accepted, you can change this behavior by setting:
// CheckOrigin: (r *http.Request ) bool {},
//
//
// IDGenerator used to create (and later on, set)
// an ID for each incoming websocket connections (clients).
// The request is an argument which you can use to generate the ID (from headers for example).
// If empty then the ID is generated by DefaultIDGenerator: randomString(64):
// IDGenerator func(ctx *iris.Context) string {},
})
app.Adapt(ws) // adapt the websocket server, you can adapt more than one with different Endpoint
app.StaticWeb("/js", "./static/js") // static route to serve our javascript files
app.Get("/", func(ctx *iris.Context) {
// send our custom javascript source file before client really asks for that
// using the new go v1.8's HTTP/2 Push.
// Note that you have to listen using ListenTLS/ListenLETSENCRYPT in order this to work.
if err := ctx.ResponseWriter.Push("/js/chat.js", nil); err != nil {
app.Log(iris.DevMode, err.Error())
}
ctx.Render("client.html", clientPage{"Client Page", ctx.Host()})
})
var myChatRoom = "room1"
ws.OnConnection(func(c websocket.Connection) {
// Context returns the (upgraded) *iris.Context of this connection
// avoid using it, you normally don't need it,
// websocket has everything you need to authenticate the user BUT if it's necessary
// then you use it to receive user information, for example: from headers.
// ctx := c.Context()
// join to a room (optional)
c.Join(myChatRoom)
c.On("chat", func(message string) {
if message == "leave" {
c.Leave(myChatRoom)
c.To(myChatRoom).Emit("chat", "Client with ID: "+c.ID()+" left from the room and cannot send or receive message to/from this room.")
c.Emit("chat", "You have left from the room: "+myChatRoom+" you cannot send or receive any messages from others inside that room.")
return
}
// to all except this connection ->
// c.To(websocket.Broadcast).Emit("chat", "Message from: "+c.ID()+"-> "+message)
// to all connected clients: c.To(websocket.All)
// to the client itself ->
//c.Emit("chat", "Message from myself: "+message)
//send the message to the whole room,
//all connections are inside this room will receive this message
c.To(myChatRoom).Emit("chat", "From: "+c.ID()+": "+message)
})
// or create a new leave event
// c.On("leave", func() {
// c.Leave(myChatRoom)
// })
c.OnDisconnect(func() {
fmt.Printf("Connection with ID: %s has been disconnected!\n", c.ID())
})
})
listenTLS(app)
}
// a test listenTLS for our localhost
func listenTLS(app *iris.Framework) {
const (
testTLSCert = `-----BEGIN CERTIFICATE-----
MIIDBTCCAe2gAwIBAgIJAOYzROngkH6NMA0GCSqGSIb3DQEBBQUAMBkxFzAVBgNV
BAMMDmxvY2FsaG9zdDo4MDgwMB4XDTE3MDIxNzAzNDM1NFoXDTI3MDIxNTAzNDM1
NFowGTEXMBUGA1UEAwwObG9jYWxob3N0OjgwODAwggEiMA0GCSqGSIb3DQEBAQUA
A4IBDwAwggEKAoIBAQCfsiVHO14FpKsi0pvBv68oApQm2MO+dCvq87sDU4E0QJhG
KV1RCUmQVypChEqdLlUQsopcXSyKwbWoyg1/KNHYO3DHMfePb4bC1UD2HENq7Ph2
8QJTEi/CJvUB9hqke/YCoWYdjFiI3h3Hw8q5whGO5XR3R23z69vr5XxoNlcF2R+O
TdkzArd0CWTZS27vbgdnyi9v3Waydh/rl+QRtPUgEoCEqOOkMSMldXO6Z9GlUk9b
FQHwIuEnlSoVFB5ot5cqebEjJnWMLLP83KOCQekJeHZOyjeTe8W0Fy1DGu5fvFNh
xde9e/7XlFE//00vT7nBmJAUV/2CXC8U5lsjLEqdAgMBAAGjUDBOMB0GA1UdDgQW
BBQOfENuLn/t0Z4ZY1+RPWaz7RBH+TAfBgNVHSMEGDAWgBQOfENuLn/t0Z4ZY1+R
PWaz7RBH+TAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBG7AEEuIq6
rWCE5I2t4IXz0jN7MilqEhUWDbUajl1paYf6Ikx5QhMsFx21p6WEWYIYcnWAKZe2
chAgnnGojuxdx0qjiaH4N4xWGHsWhaesnIF1xJepLlX3kJZQURvRxM4wlljlQPIb
9tqzKP131K1HDqplAtp7nWQ72m3J0ZfzH0mYIUxuaS/uQIVtgKqdilwy/VE5dRZ9
QFIb4G9TnNThXMqgTLjfNr33jVbTuv6fzKHYNbCkP3L10ydEs/ddlREmtsn9nE8Q
XCTIYXzA2kr5kWk7d3LkUiSvu3g2S1Ol1YaIKaOQyRveseCGwR4xohLT+dPUW9dL
3hDVLlwE3mB3
-----END CERTIFICATE-----
`
testTLSKey = `-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAn7IlRzteBaSrItKbwb+vKAKUJtjDvnQr6vO7A1OBNECYRild
UQlJkFcqQoRKnS5VELKKXF0sisG1qMoNfyjR2DtwxzH3j2+GwtVA9hxDauz4dvEC
UxIvwib1AfYapHv2AqFmHYxYiN4dx8PKucIRjuV0d0dt8+vb6+V8aDZXBdkfjk3Z
MwK3dAlk2Utu724HZ8ovb91msnYf65fkEbT1IBKAhKjjpDEjJXVzumfRpVJPWxUB
8CLhJ5UqFRQeaLeXKnmxIyZ1jCyz/NyjgkHpCXh2Tso3k3vFtBctQxruX7xTYcXX
vXv+15RRP/9NL0+5wZiQFFf9glwvFOZbIyxKnQIDAQABAoIBAEzBx4ExW8PCni8i
o5LAm2PTuXniflMwa1uGwsCahmOjGI3AnAWzPRSPkNRf2a0q8+AOsMosTphy+umi
FFKmQBZ6m35i2earaE6FSbABbbYbKGGi/ccH2sSrDOBgdfXRTzF8eiSBrJw8hnvZ
87rNOLtCNnSOdJ7lItODfgRo+fLo4uQenJ8VONYwtwm1ejn8qLXq8O5zF66IYUD6
gAzqOiAWumgZL0tEmndeQ+noe4STpJZlOjiCsA12NiJaKDDeDIn5A/pXce+bYNfJ
k4yoroyq/JXBkhyuZDvX9vYp5AA+Q68h8/KmsKkifUgSGSHun5/80lYyT/f60TLX
PxT9GYECgYEA0s8qck7L29nBBTQ6IPF3GHGmqiRdfH+qhP/Jn4NtoW3XuVe4A15i
REq1L8WAiOUIBnBaD8HzbeioqJJYx1pu7x9h/GCNDhdBfwhTjnBe+JjfLqvJKnc0
HUT5wj4DVqattxKzUW8kTRBSWtVremzeffDo+EL6dnR7Bc02Ibs4WpUCgYEAwe34
Uqhie+/EFr4HjYRUNZSNgYNAJkKHVxk4qGzG5VhvjPafnHUbo+Kk/0QW7eIB+kvR
FDO8oKh9wTBrWZEcLJP4jDIKh4y8hZTo9B8EjxFONXVxZlOSYuGjheL8AiLzE7L9
C1spaKMM/MyxAXDRHpG/NeEgXM7Kn6kUGwJdNekCgYAshLNiEGHcu8+XWcAs1NFh
yB56L9PORuerzpi1pvuv65JzAaNKktQNt/krbXoHbtaTBYb/bOYLf+aeMsmsz9w9
g1MeCQXAxAiA2zFKE1D7Ds2S/ZQt8559z+MusgnicrCcyMY1nFL+M0QxCoD4CaWy
0v1f8EUUXuTcBMo5tV/hQQKBgDoBBW8jsiFDu7DZscSgOde00QZVzZAkAfsJLisi
LfNXGjZdZawUUuoX1iYLpZgNK25D0wtp1hdvjf2Ej/dAMd8bexHjvcaBT7ncqjiq
NmDcWjofIIXspTIyLwjStXGmJnJT7N/CqoYDjtTmHGND7Shpi3mAFn/r0isjFUJm
2J5RAoGALuGXxzmSRWmkIp11F/Qr3PBFWBWkrRWaH2TRLMhrU/wO8kCsSyo4PmAZ
ltOfD7InpDiCu43hcDPQ/29FUbDnmAhvMnmIQuHXGgPF/LhqEhbKPA/o/eZdQVCK
QG+tmveBBIYMed5YbWstZu/95lIHF+u8Hl+Z6xgveozfE5yqiUA=
-----END RSA PRIVATE KEY-----
`
)
// create the key and cert files on the fly, and delete them when this test finished
certFile, ferr := ioutil.TempFile("", "cert")
if ferr != nil {
panic(ferr)
}
keyFile, ferr := ioutil.TempFile("", "key")
if ferr != nil {
panic(ferr)
}
certFile.WriteString(testTLSCert)
keyFile.WriteString(testTLSKey)
// add an event when control+C pressed, to remove the temp cert and key files.
app.Adapt(iris.EventPolicy{
Interrupted: func(*iris.Framework) {
certFile.Close()
time.Sleep(50 * time.Millisecond)
os.Remove(certFile.Name())
keyFile.Close()
time.Sleep(50 * time.Millisecond)
os.Remove(keyFile.Name())
},
})
// https://localhost
app.ListenTLS("localhost:443", certFile.Name(), keyFile.Name())
}

View File

@ -0,0 +1,38 @@
var messageTxt;
var messages;
$(function () {
messageTxt = $("#messageTxt");
messages = $("#messages");
/* secure wss because we ListenTLS */
w = new Ws("wss://" + HOST + "/my_endpoint");
w.OnConnect(function () {
console.log("Websocket connection established");
});
w.OnDisconnect(function () {
appendMessage($("<div><center><h3>Disconnected</h3></center></div>"));
});
w.On("chat", function (message) {
appendMessage($("<div>" + message + "</div>"));
});
$("#sendBtn").click(function () {
w.Emit("chat", messageTxt.val().toString());
messageTxt.val("");
});
})
function appendMessage(messageDiv) {
var theDiv = messages[0];
var doScroll = theDiv.scrollTop == theDiv.scrollHeight - theDiv.clientHeight;
messageDiv.appendTo(messages);
if (doScroll) {
theDiv.scrollTop = theDiv.scrollHeight - theDiv.clientHeight;
}
}

View File

@ -0,0 +1,24 @@
<html>
<head>
<title>{{ .Title}}</title>
</head>
<body>
<div id="messages"
style="border-width: 1px; border-style: solid; height: 400px; width: 375px;">
</div>
<input type="text" id="messageTxt" />
<button type="button" id="sendBtn">Send</button>
<script type="text/javascript">
var HOST = {{.Host}}
</script>
<script src="/js/vendor/jquery-2.2.3.min.js"></script>
<!-- This is auto-serving by the Iris, you don't need to have this file in your disk-->
<script src="/iris-ws.js"></script>
<!-- -->
<script src="/js/chat.js"></script>
</body>
</html>

123
iris.go
View File

@ -100,8 +100,6 @@ type Framework struct {
// These are setted by user's call to .Adapt
policies Policies
ln net.Listener // setted on Listten/Serve funcions, available after 'Boot'
// TLSNextProto optionally specifies a function to take over
// ownership of the provided TLS connection when an NPN/ALPN
// protocol upgrade has occurred. The map key is the protocol
@ -210,19 +208,15 @@ func New(setters ...OptionSetter) *Framework {
s.Adapt(EventPolicy{Boot: func(s *Framework) {
// set the host and scheme
if s.Config.VHost == "" { // if not setted by Listen functions
if s.ln != nil { // but user called .Serve
// then take the listener's addr
s.Config.VHost = s.ln.Addr().String()
} else {
// if no .Serve or .Listen called, then the user should set the VHost manually,
// however set it to a default value here for any case
s.Config.VHost = DefaultServerAddr
}
s.Config.VHost = DefaultServerAddr
}
// if user didn't specified a scheme then get it from the VHost, which is already setted at before statements
// if user didn't specified a scheme then get it from the VHost,
// which is already setted at before statements
if s.Config.VScheme == "" {
// if :443 or :https then returns https:// otherwise http://
s.Config.VScheme = ParseScheme(s.Config.VHost)
}
}})
{
@ -436,28 +430,21 @@ func (s *Framework) Boot() (firstTime bool) {
return
}
// Serve serves incoming connections from the given listener.
//
// Serve blocks until the given listener returns permanent error.
func (s *Framework) Serve(ln net.Listener) error {
if s.ln != nil {
return errors.New("server is already started and listening")
}
s.ln = ln
func (s *Framework) setupServe() (srv *http.Server, deferFn func()) {
s.closedManually = false
s.Boot()
// post any panics to the user defined logger.
defer func() {
deferFn = func() {
// post any panics to the user defined logger.
if rerr := recover(); rerr != nil {
if err, ok := rerr.(error); ok {
s.handlePanic(err)
}
}
}()
}
srv := &http.Server{
srv = &http.Server{
ReadTimeout: s.Config.ReadTimeout,
WriteTimeout: s.Config.WriteTimeout,
MaxHeaderBytes: s.Config.MaxHeaderBytes,
@ -467,21 +454,38 @@ func (s *Framework) Serve(ln net.Listener) error {
ErrorLog: s.policies.LoggerPolicy.ToLogger(log.LstdFlags),
Handler: s.Router,
}
// Set the grace shutdown, it's just a func no need to make things complicated
// all are managed by net/http now.
s.Shutdown = func(ctx context.Context) error {
// order matters, look s.handlePanic
s.closedManually = true
err := srv.Shutdown(ctx)
s.ln = nil
return err
}
return
}
// Serve serves incoming connections from the given listener.
//
// Serve blocks until the given listener returns permanent error.
func (s *Framework) Serve(ln net.Listener) error {
if ln == nil {
return errors.New("nil net.Listener on Serve")
}
// if user called .Serve and doesn't uses any nginx-like balancers.
if s.Config.VHost == "" {
s.Config.VHost = ParseHost(ln.Addr().String())
} // Scheme will be checked from Boot state.
srv, fn := s.setupServe()
defer fn()
// print the banner and wait for system channel interrupt
go s.postServe()
// finally return the error or block here, remember,
// until go1.8 these are our best options.
return srv.Serve(s.ln)
return srv.Serve(ln)
}
func (s *Framework) postServe() {
@ -507,14 +511,15 @@ func (s *Framework) postServe() {
// If you need to manually monitor any error please use `.Serve` instead.
func (s *Framework) Listen(addr string) {
addr = ParseHost(addr)
if s.Config.VHost == "" {
s.Config.VHost = addr
// this will be set as the front-end listening addr
}
// only here, other Listen functions should throw an error if port is missing.
// User should know how to fix them on ListenUNIX/ListenTLS/ListenLETSENCRYPT/Serve,
// they are used by more 'advanced' devs, mostly.
// if .Listen called normally and VHost is not setted,
// so it's Host is the Real listening addr and user-given
if s.Config.VHost == "" {
s.Config.VHost = addr // as it is
// this will be set as the front-end listening addr
} // VScheme will be checked on Boot.
// this check, only here, other Listen functions should throw an error if port is missing.
if portIdx := strings.IndexByte(addr, ':'); portIdx < 0 {
// missing port part, add it
addr = addr + ":80"
@ -538,16 +543,27 @@ func (s *Framework) Listen(addr string) {
// If you need to manually monitor any error please use `.Serve` instead.
func (s *Framework) ListenTLS(addr string, certFile, keyFile string) {
addr = ParseHost(addr)
if s.Config.VHost == "" {
s.Config.VHost = addr
// this will be set as the front-end listening addr
{
// set it before Boot, be-careful VHost and VScheme are used by nginx users too
// we don't want to alt them.
if s.Config.VHost == "" {
s.Config.VHost = addr
// this will be set as the front-end listening addr
}
if s.Config.VScheme == "" {
s.Config.VScheme = SchemeHTTPS
}
}
ln, err := TLS(addr, certFile, keyFile)
if err != nil {
s.handlePanic(err)
}
s.Must(s.Serve(ln))
srv, fn := s.setupServe()
// We are doing the same parts as .Serve does but instead we run srv.ListenAndServeTLS
// because of un-exported net/http.server.go:setupHTTP2_ListenAndServeTLS function which
// broke our previous flow but no problem :)
defer fn()
// print the banner and wait for system channel interrupt
go s.postServe()
s.Must(srv.ListenAndServeTLS(certFile, keyFile))
}
// ListenLETSENCRYPT starts a server listening at the specific nat address
@ -557,16 +573,25 @@ func (s *Framework) ListenTLS(addr string, certFile, keyFile string) {
// if you skip the second parameter then the cache file is "./letsencrypt.cache"
// if you want to disable cache then simple pass as second argument an empty empty string ""
//
// example: https://github.com/iris-contrib/examples/blob/master/letsencrypt/main.go
// Note: HTTP/2 Push is not working with LETSENCRYPT, you have to use ListenTLS to enable HTTP/2
// Because net/http's author didn't exported the functions to tell the server that is using HTTP/2...
//
// supports localhost domains for testing,
// NOTE: if you are ready for production then use `$app.Serve(iris.LETSENCRYPTPROD("mydomain.com"))` instead
// example: https://github.com/iris-contrib/examples/blob/master/letsencrypt/main.go
func (s *Framework) ListenLETSENCRYPT(addr string, cacheFileOptional ...string) {
addr = ParseHost(addr)
if s.Config.VHost == "" {
s.Config.VHost = addr
// this will be set as the front-end listening addr
{
// set it before Boot, be-careful VHost and VScheme are used by nginx users too
// we don't want to alt them.
if s.Config.VHost == "" {
s.Config.VHost = addr
// this will be set as the front-end listening addr
}
if s.Config.VScheme == "" {
s.Config.VScheme = SchemeHTTPS
}
}
ln, err := LETSENCRYPT(addr, cacheFileOptional...)
if err != nil {
s.handlePanic(err)

View File

@ -607,9 +607,9 @@ func (router *Router) StaticHandler(reqPath string, systemPath string, showList
// second parameter: the system directory
// third OPTIONAL parameter: the exception routes
// (= give priority to these routes instead of the static handler)
// for more options look iris.StaticHandler.
// for more options look router.StaticHandler.
//
// iris.StaticWeb("/static", "./static")
// router.StaticWeb("/static", "./static")
//
// As a special case, the returned file server redirects any request
// ending in "/index.html" to the same path, without the final
@ -618,8 +618,22 @@ func (router *Router) StaticHandler(reqPath string, systemPath string, showList
// StaticWeb calls the StaticHandler(reqPath, systemPath, listingDirectories: false, gzip: false ).
func (router *Router) StaticWeb(reqPath string, systemPath string, exceptRoutes ...RouteInfo) RouteInfo {
h := router.StaticHandler(reqPath, systemPath, false, false, exceptRoutes...)
routePath := validateWildcard(reqPath, "file")
return router.registerResourceRoute(routePath, h)
paramName := "file"
routePath := validateWildcard(reqPath, paramName)
handler := func(ctx *Context) {
h(ctx)
if fname := ctx.Param(paramName); fname != "" {
cType := fs.TypeByExtension(fname)
if cType != contentBinary && !strings.Contains(cType, "charset") {
cType += "; charset=" + ctx.framework.Config.Charset
}
ctx.SetContentType(cType)
}
}
return router.registerResourceRoute(routePath, handler)
}
// Layout oerrides the parent template layout with a more specific layout for this Party