Hello World 服务器 server.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package mainimport ( "net/http" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) func main () { e := echo.New() e.Use(middleware.Logger()) e.Use(middleware.Recover()) e.GET("/" , func (c echo.Context) error { return c.String(http.StatusOK, "Hello, World!\n" ) }) e.Logger.Fatal(e.Start(":1323" )) }
客户端
Auto TLS 这个例子演示如何自动从 Let’s Encrypt 获得 TLS 证书。 Echo#StartAutoTLS
接受一个接听 443 端口的网络地址。类似 <DOMAIN>:443
这样。
如果没有错误,访问 https://<DOMAIN>
,可以看到一个 TLS 加密的欢迎界面。
服务器 server.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 package mainimport ( "crypto/tls" "golang.org/x/crypto/acme" "net/http" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "golang.org/x/crypto/acme/autocert" ) func main () { e := echo.New() e.AutoTLSManager.Cache = autocert.DirCache("/var/www/.cache" ) e.Use(middleware.Recover()) e.Use(middleware.Logger()) e.GET("/" , func (c echo.Context) error { return c.HTML(http.StatusOK, ` <h1>Welcome to Echo!</h1> <h3>TLS certificates automatically installed from Let's Encrypt :)</h3> ` ) }) e.Logger.Fatal(e.StartAutoTLS(":443" )) } func customHTTPServer () { e := echo.New() e.Use(middleware.Recover()) e.Use(middleware.Logger()) e.GET("/" , func (c echo.Context) error { return c.HTML(http.StatusOK, ` <h1>Welcome to Echo!</h1> <h3>TLS certificates automatically installed from Let's Encrypt :)</h3> ` ) }) autoTLSManager := autocert.Manager{ Prompt: autocert.AcceptTOS, Cache: autocert.DirCache("/var/www/.cache" ), } s := http.Server{ Addr: ":443" , Handler: e, TLSConfig: &tls.Config{ GetCertificate: autoTLSManager.GetCertificate, NextProtos: []string {acme.ALPNProto}, }, } if err := s.ListenAndServeTLS("" , "" ); err != http.ErrServerClosed { e.Logger.Fatal(err) } }
CRUD 服务端 server.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 package mainimport ( "net/http" "strconv" "sync" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) type ( user struct { ID int `json:"id"` Name string `json:"name"` } ) var ( users = map [int ]*user{} seq = 1 lock = sync.Mutex{} ) func createUser (c echo.Context) error { lock.Lock() defer lock.Unlock() u := &user{ ID: seq, } if err := c.Bind(u); err != nil { return err } users[u.ID] = u seq++ return c.JSON(http.StatusCreated, u) } func getUser (c echo.Context) error { lock.Lock() defer lock.Unlock() id, _ := strconv.Atoi(c.Param("id" )) return c.JSON(http.StatusOK, users[id]) } func updateUser (c echo.Context) error { lock.Lock() defer lock.Unlock() u := new (user) if err := c.Bind(u); err != nil { return err } id, _ := strconv.Atoi(c.Param("id" )) users[id].Name = u.Name return c.JSON(http.StatusOK, users[id]) } func deleteUser (c echo.Context) error { lock.Lock() defer lock.Unlock() id, _ := strconv.Atoi(c.Param("id" )) delete (users, id) return c.NoContent(http.StatusNoContent) } func getAllUsers (c echo.Context) error { lock.Lock() defer lock.Unlock() return c.JSON(http.StatusOK, users) } func main () { e := echo.New() e.Use(middleware.Logger()) e.Use(middleware.Recover()) e.GET("/users" , getAllUsers) e.POST("/users" , createUser) e.GET("/users/:id" , getUser) e.PUT("/users/:id" , updateUser) e.DELETE("/users/:id" , deleteUser) e.Logger.Fatal(e.Start(":1323" )) }
客户端 curl
注:curl 是一个命令行工具,用来请求 Web 服务器。它的名字就是客户端(client)的 URL 工具的意思。
使用教程请到官网进行查看:curl
创建 User 1 2 3 4 curl -X POST \ -H 'Content-Type: application/json' \ -d '{"name":"Joe Smith"}' \ localhost:1323/users
Response
1 2 3 4 { "id": 1, "name": "Joe Smith" }
获取 User 1 curl localhost:1323/users/1
Response
1 2 3 4 { "id": 1, "name": "Joe Smith" }
更新 User 1 2 3 4 curl -X PUT \ -H 'Content-Type: application/json' \ -d '{"name":"Joe"}' \ localhost:1323/users/1
Response
1 2 3 4 { "id": 1, "name": "Joe" }
删除 User 1 curl -X DELETE localhost:1323/users/1
Response
NoContent - 204
接口测试 根据路由定义发送 POST 请求的 JSON 信息,创建 user
可以得到响应结果
用 PUT 请求对数据进行更新,返回更新结果
调用 DELETE 请求删除数据,可以看到返回空内容
再尝试访问 ID 为 1 的页面,可以看到返回为 null ,说明 users[1] 不存在(被删除或者未创建,在此处是被删除),可以通过 PUT 请求更新数据,使 users[1] 重新有值
访问 127.0.0.1:1323/users
响应返回 users 内的所有数据
注:每次通过 GET 请求创建 user 时,user 总数 seq 都会自动加 1 ,在通过 DELETE 请求删除某个已创建的 user 时,seq 并不会减少(也就是 user 的 ID 值不会改变),例如当前 users 最大的 ID 为 5 ,如果对 127.0.0.1:1323/users/5
进行 DELETE 请求,并再次用 GET 请求新创建一个 user ,那么这个新创建的 user 的 ID 是 6 ,不会因为经过一次 DELETE 就向前延递,seq 的值也为 6。
CORS 跨源资源共享
(CORS )(或通俗地译为跨域资源共享)是一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其它 origin (域,协议和端口),使得浏览器允许这些 origin 访问加载自己的资源。跨源资源共享还通过一种机制来检查服务器是否会允许要发送的真实请求,该机制通过浏览器发起一个到服务器托管的跨源资源的”预检”请求。在预检中,浏览器发送的头中标示有 HTTP 方法和真实请求中会用到的头。
使用被允许的 Origins server.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package mainimport ( "net/http" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) var ( users = []string {"Joe" , "Veer" , "Zion" } ) func getUsers (c echo.Context) error { return c.JSON(http.StatusOK, users) } func main () { e := echo.New() e.Use(middleware.Logger()) e.Use(middleware.Recover()) e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ AllowOrigins: []string {"https://labstack.com" , "https://labstack.net" }, AllowMethods: []string {http.MethodGet, http.MethodPut, http.MethodPost, http.MethodDelete}, })) e.GET("/api/users" , getUsers) e.Logger.Fatal(e.Start(":1323" )) }
自定义函数来允许 Origins server.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package mainimport ( "net/http" "regexp" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) var ( users = []string {"Joe" , "Veer" , "Zion" } ) func getUsers (c echo.Context) error { return c.JSON(http.StatusOK, users) } func allowOrigin (origin string ) (bool , error ) { return regexp.MatchString(`^https:\/\/labstack\.(net|com)$` , origin) } func main () { e := echo.New() e.Use(middleware.Logger()) e.Use(middleware.Recover()) e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ AllowOriginFunc: allowOrigin, AllowMethods: []string {http.MethodGet, http.MethodPut, http.MethodPost, http.MethodDelete}, })) e.GET("/api/users" , getUsers) e.Logger.Fatal(e.Start(":1323" )) }
HTTP2 HTTP/2 (原本的名字是 HTTP/2.0) 是万维网使用的 HTTP 网络协议的第二个主要版本。HTTP/2 提供了更快的速度和更好的用户体验。
特性
使用二进制格式传输数据,而不是文本。使得在解析和优化扩展上更为方便。
多路复用,所有的请求都是通过一个 TCP 连接并发完成。
对消息头采用 HPACK 进行压缩传输,能够节省消息头占用的网络的流量。
Server Push:服务端能够更快的把资源推送给客户端。
怎样运行 HTTP2 和 HTTPS 服务? Step 1 生成一个自签名的 X.509 TLS 证书(HTTP/2 需要 TLS 才能运行)
1 go run $GOROOT/src/crypto/tls/generate_cert.go --host localhost
上面的命令会生一个cert.pem
和key.pem
文件。
这里只是展示使用,所以我们用了自签名的证书,正式环境建议去 CA 申请证书。
Step 2 创建一个仅将请求信息输出到客户机的控制器
1 2 3 4 5 6 7 8 9 10 11 12 13 e.GET("/request" , func (c echo.Context) error { req := c.Request() format := ` <code> Protocol: %s<br> Host: %s<br> Remote Address: %s<br> Method: %s<br> Path: %s<br> </code> ` return c.HTML(http.StatusOK, fmt.Sprintf(format, req.Proto, req.Host, req.RemoteAddr, req.Method, req.URL.Path)) })
Step 3 使用 cert.pem
和 key.pem
启动TLS服务器
1 2 3 if err := e.StartTLS(":1323" , "cert.pem" , "key.pem" ); err != http.ErrServerClosed { log.Fatal(err) }
或者使用自定义的HTTP服务器和您自己的TLSConfig
1 2 3 4 5 6 7 8 9 10 11 s := http.Server{ Addr: ":8443" , Handler: e, TLSConfig: &tls.Config{ }, } if err := s.ListenAndServeTLS("cert.pem" , "key.pem" ); err != http.ErrServerClosed { log.Fatal(err) }
Step 4 启动服务器并到 https://localhost:1323/request 下查看以下输出
1 2 3 4 5 Protocol: HTTP/2.0 Host: localhost:1323 Remote Address: [::1]:60288 Method: GET Path: /
server.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package mainimport ( "fmt" "net/http" "github.com/labstack/echo/v4" ) func main () { e := echo.New() e.GET("/request" , func (c echo.Context) error { req := c.Request() format := ` <code> Protocol: %s<br> Host: %s<br> Remote Address: %s<br> Method: %s<br> Path: %s<br> </code> ` return c.HTML(http.StatusOK, fmt.Sprintf(format, req.Proto, req.Host, req.RemoteAddr, req.Method, req.URL.Path)) }) e.Logger.Fatal(e.StartTLS(":1323" , "cert.pem" , "key.pem" )) }
最后
客户端
最初使用 postman 尝试访问得到错误响应(即无法验证第一个证书)
再次尝试使用浏览器访问,成功得到返回的响应结果,因为证书无法验证,所以浏览器提示不安全。
在控制台看到几次请求(包括未成功和成功的 Logger 信息)
中间件 怎样自己写一个中间件? 比如有以下需求
通过该中间件去统计请求数目、状态和时间。
中间件向响应写入自定义 Server
标头。
之前在学习 Echo 入门的时候提到过一个统计总请求数的程序,现在让我们对其进行优化和功能的增加。
服务端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 package mainimport ( "fmt" "net/http" "strconv" "sync" "time" "github.com/labstack/echo/v4" ) type ( Stats struct { Uptime time.Time `json:"uptime"` RequestCount uint64 `json:"requestCount"` Statuses map [string ]int `json:"statuses"` mutex sync.RWMutex } ) func NewStats () *Stats { return &Stats{ Uptime: time.Now(), Statuses: map [string ]int {}, } } func (s *Stats) Process(next echo.HandlerFunc) echo.HandlerFunc { return func (c echo.Context) error { if err := next(c); err != nil { c.Error(err) } s.mutex.Lock() defer s.mutex.Unlock() s.RequestCount++ status := strconv.Itoa(c.Response().Status) s.Statuses[status]++ return nil } } func (s *Stats) Handle(c echo.Context) error { s.mutex.RLock() defer s.mutex.RUnlock() return c.JSON(http.StatusOK, s) } func ServerHeader (next echo.HandlerFunc) echo.HandlerFunc { return func (c echo.Context) error { c.Response().Header().Set(echo.HeaderServer, "Echo/3.0" ) return next(c) } } func main () { e := echo.New() e.Debug = true s := NewStats() e.Use(s.Process) e.GET("/stats" , s.Handle) e.Use(ServerHeader) e.GET("/" , func (c echo.Context) error { return c.String(http.StatusOK, "Hello, World!" ) }) e.Logger.Fatal(e.Start(":1323" )) }
客户端 响应头 1 2 3 4 Content-Length:122 Content-Type:application/json; charset=utf-8 Date:Thu, 14 Apr 2016 20:31:46 GMT Server:Echo/3.0
响应体 1 2 3 4 5 6 7 8 { "uptime": "2016-04-14T13:28:48.486548936-07:00", "requestCount": 5, "statuses": { "200": 4, "404": 1 } }
让我们在本地进行一次尝试
第一次访问 localhost:1323/stats
,看到响应结果中的 requestCount 的值为 0,且 statuses 体为空,这与我们所预想的有所不同,在外面看来,此次访问已经成功,requestCount 应该为 1,且 statuses 应该为 “statuses”: { “200”: 1 } 。
让我们再次进行访问,可以看到此次访问后 requestCount 加了 1 并且 statuses 也确实为 “statuses”: { “200”: 1 } 了,这说明访问计数是生效的,那我们不仅要问一下,为什么第一次访问不为 requestCount 不为 1 且 statuses 为空。
让我们修改下服务器代码,将 Process 中间件改为如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func (s *Stats) Process(next echo.HandlerFunc) echo.HandlerFunc { return func (c echo.Context) error { if err := next(c); err != nil { c.Error(err) } s.mutex.Lock() defer s.mutex.Unlock() fmt.Println(s.RequestCount) s.RequestCount++ fmt.Println(s.RequestCount) status := strconv.Itoa(c.Response().Status) s.Statuses[status]++ return nil } }
当我们运行程序,并且向服务器进行一次请求后
可以看到程序打印出 0 1 ,这与我们所想的是一样的,即每经过一次访问,中间件 Process 就将 s.RequestCount 的值加一,但是为什么服务器响应的值却是 0 呢。
我们再将 Handle 控制器修改为以下
1 2 3 4 5 6 7 func (s *Stats) Handle(c echo.Context) error { s.mutex.RLock() defer s.mutex.RUnlock() fmt.Println(s.RequestCount) return c.JSON(http.StatusOK, s) }
当我们再次运行程序,并且向服务器进行一次请求后
可以看到程序依次打印出 0 0 1 ,这说明了什么?这说明了对 Handle 控制器的调用时是在 Process 中间件以前的,所以它返回的响应结果才为 1 和 空。那么为什么会这样呢?让我们回到中间件的定义:
在echo框架中中间件 (Middleware)指的是可以拦截http请求-响应 生命周期的特殊函数,在请求-响应生命周期中可以注册多个中间件,每个中间件执行不同的功能,一个中间执行完再轮到下一个中间件执行。
由于中间件的作用是拦截 http 请求,而上面的案例又明确地告诉我们中间件的执行是在一次请求之后的,即不是在路由的控制器被执行之前先执行的,那么我们不妨大胆的猜测,中间件的执行是在一次请求之后的,即在控制器执行之后的。那么一切都说得通了😸
让我们将上述程序修改到最初的模样,也就是无需调用 fmt 包的模样
让我们先对 localhost:1323/ 进行一次访问,看到响应结果 Hello, World!
再尝试访问一个 localhost:1323/ 下的不存在的路由 localhost:1323/s ,看到响应结果为 404 Not Found
最后尝试访问 localhost:1323/stats ,响应返回至今的请求数,以及成功响应的次数,根据上例我们可以得知,此次响应的结果不包含在内,所以再此次响应之前就已经有了两次请求记录,并且一次为响应状态为 200,一次响应状态为 404,这恰好是我们前两次访问的结果。
流式响应 概念 响应式流(Reactive Streams)是以带非阻塞背压方式处理异步数据流的标准,提供一组最小化的接口,方法和协议来描述必要的操作和实体。
要解决的问题:
系统之间高并发的大量数据流交互通常采用异步的发布-订阅模式。数据由发布者推送给订阅者的过程中,容易产生的一个问题是,当发布者即生产者产生的数据速度远远大于订阅者即消费者的消费速度时,消费者会承受巨大的资源压力(pressure)而有可能崩溃。
解决原理:
为了解决以上问题,数据流的速度需要被控制,即流量控制 (flow control),以防止快速的数据流不会压垮目标。因此需要反压即背压 (back pressure),生产者和消费者之间需要通过实现一种背压机制来互操作。实现这种背压机制要求是异步非阻塞 的,如果是同步阻塞的,消费者在处理数据时生产者必须等待,会产生性能问题。
解决方法:
响应式流(Reactive Streams)通过定义一组实体,接口和互操作方法,给出了实现非阻塞背压的标准。第三方遵循这个标准来实现具体的解决方案,常见的有Reactor,RxJava,Akka Streams,Ratpack等。
原理
当数据产生的时候发送数据
使用分块传输编码(Chunked transfer encoding)的流式 JOSN 响应。
服务端 server.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package mainimport ( "encoding/json" "net/http" "time" "github.com/labstack/echo/v4" ) type ( Geolocation struct { Altitude float64 Latitude float64 Longitude float64 } ) var ( locations = []Geolocation{ {-97 , 37.819929 , -122.478255 }, {1899 , 39.096849 , -120.032351 }, {2619 , 37.865101 , -119.538329 }, {42 , 33.812092 , -117.918974 }, {15 , 37.77493 , -122.419416 }, } ) func main () { e := echo.New() e.GET("/" , func (c echo.Context) error { c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSON) c.Response().WriteHeader(http.StatusOK) enc := json.NewEncoder(c.Response()) for _, l := range locations { if err := enc.Encode(l); err != nil { return err } c.Response().Flush() time.Sleep(1 * time.Second) } return nil }) e.Logger.Fatal(e.Start(":1323" )) }
客户端
输出 1 2 3 4 5 {"Altitude":-97,"Latitude":37.819929,"Longitude":-122.478255} {"Altitude":1899,"Latitude":39.096849,"Longitude":-120.032351} {"Altitude":2619,"Latitude":37.865101,"Longitude":-119.538329} {"Altitude":42,"Latitude":33.812092,"Longitude":-117.918974} {"Altitude":15,"Latitude":37.77493,"Longitude":-122.419416}
一次请求的响应
可以看到由于 time.Sleep(1 * time.Second)
共执行了五次的作用整个响应的时间为 5.06s 。
注:上述程序中的生产者 locations 在程序开始时就已经发布完了所有资源(数据),而 time.Sleep(1 * time.Second)
仅仅是针对消费者(即客户端)的,它不会影响生产者的再生产,而是对消费者的接收做了限制,即每秒只接受一条记录,这种背压机制是异步非阻塞的。
WebSocket 概念 3.1 WebSocket 诞生背景
早期,很多网站为了实现推送技术,所用的技术都是轮询(也叫短轮询)。轮询是指由浏览器每隔一段时间向服务器发出 HTTP 请求,然后服务器返回最新的数据给客户端。
常见的轮询方式分为轮询与长轮询,它们的区别如下图所示:
为了更加直观感受轮询与长轮询之间的区别,我们来看一下具体的代码:
这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而 HTTP 请求与响应可能会包含较长的头部,其中真正有效的数据可能只是很小的一部分,所以这样会消耗很多带宽资源。
PS: 关于短轮询、长轮询技术的前世今身,可以详细读这两篇:《新手入门贴:史上最全Web端即时通讯技术原理详解 》、《Web端即时通讯技术盘点:短轮询、Comet、Websocket、SSE 》。
比较新的轮询技术是 Comet 。这种技术虽然可以实现双向通信,但仍然需要反复发出请求。而且在 Comet 中普遍采用的 HTTP 长连接也会消耗服务器资源。
在这种情况下,HTML5 定义了 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
Websocket 使用 ws 或 wss 的统一资源标志符(URI),其中 wss 表示使用了 TLS 的 Websocket。
如:
ws://echo.websocket.org
wss://echo.websocket.org
3.2 WebSocket 简介
WebSocket 是一种网络传输协议,可在单个 TCP 连接上进行全双工通信,位于 OSI 模型的应用层。WebSocket 协议在 2011 年由 IETF 标准化为 RFC 6455 ,后由 RFC 7936 补充规范。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。
介绍完轮询和 WebSocket 的相关内容之后,接下来用一张图看一下 XHR Polling(短轮询) 与 WebSocket 之间的区别。
XHR Polling与 WebSocket 之间的区别如下图所示:
3.3 WebSocket 优点
普遍认为,WebSocket的优点有如下几点:
1) 较少的控制开销:在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小;
2) 更强的实时性:由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于 HTTP 请求需要等待客户端发起请求服务端才能响应,延迟明显更少;
3) 保持连接状态:与 HTTP 不同的是,WebSocket 需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息;
4) 更好的二进制支持:WebSocket 定义了二进制帧,相对 HTTP,可以更轻松地处理二进制内容;
5) 可以支持扩展:WebSocket 定义了扩展,用户可以扩展协议、实现部分自定义的子协议。
下面介绍 WebSocket 在 Echo 中的实现
使用 net
库的 WebSocket 服务端 server.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package mainimport ( "fmt" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "golang.org/x/net/websocket" ) func hello (c echo.Context) error { websocket.Handler(func (ws *websocket.Conn) { defer ws.Close() for { err := websocket.Message.Send(ws, "Hello, Client!" ) if err != nil { c.Logger().Error(err) } msg := "" err = websocket.Message.Receive(ws, &msg) if err != nil { c.Logger().Error(err) } fmt.Printf("%s\n" , msg) } }).ServeHTTP(c.Response(), c.Request()) return nil } func main () { e := echo.New() e.Use(middleware.Logger()) e.Use(middleware.Recover()) e.Static("/" , "../public" ) e.GET("/ws" , hello) e.Logger.Fatal(e.Start(":1323" )) }
使用 gorilla
的 WebSocket 服务端 server.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package mainimport ( "fmt" "github.com/gorilla/websocket" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) var ( upgrader = websocket.Upgrader{} ) func hello (c echo.Context) error { ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil ) if err != nil { return err } defer ws.Close() for { err := ws.WriteMessage(websocket.TextMessage, []byte ("Hello, Client!" )) if err != nil { c.Logger().Error(err) } _, msg, err := ws.ReadMessage() if err != nil { c.Logger().Error(err) } fmt.Printf("%s\n" , msg) } } func main () { e := echo.New() e.Use(middleware.Logger()) e.Use(middleware.Recover()) e.Static("/" , "../public" ) e.GET("/ws" , hello) e.Logger.Fatal(e.Start(":1323" )) }
客户端 index.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 <!doctype html > <html lang ="en" > <head > <meta charset ="utf-8" > <title > WebSocket</title > </head > <body > <p id ="output" > </p > <script > var loc = window .location ; var uri = 'ws:' ; if (loc.protocol === 'https:' ) { uri = 'wss:' ; } uri += '//' + loc.host ; uri += loc.pathname + 'ws' ; ws = new WebSocket (uri) ws.onopen = function ( ) { console .log ('Connected' ) } ws.onmessage = function (evt ) { var out = document .getElementById ('output' ); out.innerHTML += evt.data + '<br>' ; } setInterval (function ( ) { ws.send ('Hello, Server!' ); }, 1000 ); </script > </body > </html >
输出示例 Client
1 2 3 4 5 Hello, Client! Hello, Client! Hello, Client! Hello, Client! Hello, Client!
Server
1 2 3 4 5 Hello, Server! Hello, Server! Hello, Server! Hello, Server! Hello, Server!
一个 demo 我们在项目的父目录下创建一个同级目录 pubilc 再创建客户端 index.html
此时大致的目录结构如下(有删减)
1 2 3 4 5 6 7 8 ├───public │ └───index.html └───learnecho ├───.hintrc ├───go.mod ├───go.sum ├───learnecho.exe └───server.go
启动服务器,并在浏览器访问 http://localhost:1323/
可以看到客户端和服务器之间已经建立连接了,并且可以互相主动推送数据。
若我们想修改静态资源 url 的前缀,则要修改服务器和客户端两部分的代码。
server.go
修改静态资源地址
1 2 3 e.Static("/" , "../public" ) -> e.Static("/haha" , "../public" )
index.html
修改 uri
1 2 3 uri += loc.pathname + 'ws' ; -> uri += '/ws' ;
若只想更改客户端的地址,则只需要修改服务器代码,例如我们想要将客户端放在当前项目的 public 目录下
server.go
1 2 3 e.Static("/" , "../public" ) -> e.Static("/" , "./public" )
无需再修改客户端代码
注:在本例下进行请求的时候,请求地址的末必须和静态地址前缀严格一致,例如:定义 e.Static(“/“, “./public”),则请求地址一定为 http://localhost:1323/ ,不可为 http://localhost:1323/index.html 。因为本例中客户端是根据静态资源地址的前缀来关联服务端的。当然,你想将 uri += loc.pathname + 'ws';
修改为 uri += '/ws';
也是可以的。
放一个可以在本地进行 websocket 测试的 html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="utf-8" /> <meta http-equiv ="X-UA-Compatible" content ="IE=edge" > <meta name ="viewport" content ="width=device-width, initial-scale=1" > <title > 本地websocket测试</title > <meta name ="robots" content ="all" /> <meta name ="keywords" content ="本地,websocket,测试工具" /> <meta name ="description" content ="本地,websocket,测试工具" /> <style > .btn-group { display : inline-block; } </style > </head > <body > <input type ='text' value ='ws://localhost:3000/api/ws' class ="form-control" style ='width:390px;display:inline' id ='wsaddr' /> <div class ="btn-group" > <button type ="button" class ="btn btn-default" onclick ='addsocket();' > 连接</button > <button type ="button" class ="btn btn-default" onclick ='closesocket();' > 断开</button > <button type ="button" class ="btn btn-default" onclick ='$("#wsaddr").val("")' > 清空</button > </div > <div class ="row" > <div id ="output" style ="border:1px solid #ccc;height:365px;overflow: auto;margin: 20px 0;" > </div > <input type ="text" id ='message' class ="form-control" style ='width:810px' placeholder ="待发信息" onkeydown ="en(event);" > <span class ="input-group-btn" > <button class ="btn btn-default" type ="button" onclick ="doSend();" > 发送</button > </span > </div > </div > </body > <script crossorigin ="anonymous" integrity ="sha384-LVoNJ6yst/aLxKvxwp6s2GAabqPczfWh6xzm38S/YtjUyZ+3aTKOnD/OJVGYLZDl" src ="https://lib.baomitu.com/jquery/3.5.0/jquery.min.js" > </script > <script language ="javascript" type ="text/javascript" > function formatDate (now ) { var year = now.getFullYear (); var month = now.getMonth () + 1 ; var date = now.getDate (); var hour = now.getHours (); var minute = now.getMinutes (); var second = now.getSeconds (); return year + "-" + (month = month < 10 ? ("0" + month) : month) + "-" + (date = date < 10 ? ("0" + date) : date) + " " + (hour = hour < 10 ? ("0" + hour) : hour) + ":" + (minute = minute < 10 ? ("0" + minute) : minute) + ":" + ( second = second < 10 ? ("0" + second) : second); } var output; var websocket; function init ( ) { output = document .getElementById ("output" ); testWebSocket (); } function addsocket ( ) { var wsaddr = $("#wsaddr" ).val (); if (wsaddr == '' ) { alert ("请填写websocket的地址" ); return false ; } StartWebSocket (wsaddr); } function closesocket ( ) { websocket.close (); } function StartWebSocket (wsUri ) { websocket = new WebSocket (wsUri); websocket.onopen = function (evt ) { onOpen (evt) }; websocket.onclose = function (evt ) { onClose (evt) }; websocket.onmessage = function (evt ) { onMessage (evt) }; websocket.onerror = function (evt ) { onError (evt) }; } function onOpen (evt ) { writeToScreen ("<span style='color:red'>连接成功,现在你可以发送信息啦!!!</span>" ); } function onClose (evt ) { writeToScreen ("<span style='color:red'>websocket连接已断开!!!</span>" ); websocket.close (); } function onMessage (evt ) { writeToScreen ('<span style="color:blue">服务端回应 ' + formatDate (new Date ()) + '</span><br/><span class="bubble">' + evt.data + '</span>' ); } function onError (evt ) { writeToScreen ('<span style="color: red;">发生错误:</span> ' + evt.data ); } function doSend ( ) { var message = $("#message" ).val (); if (message == '' ) { alert ("请先填写发送信息" ); $("#message" ).focus (); return false ; } if (typeof websocket === "undefined" ) { alert ("websocket还没有连接,或者连接失败,请检测" ); return false ; } if (websocket.readyState == 3 ) { alert ("websocket已经关闭,请重新连接" ); return false ; } console .log (websocket); $("#message" ).val ('' ); writeToScreen ('<span style="color:green">你发送的信息 ' + formatDate (new Date ()) + '</span><br/>' + message); websocket.send (message); } function writeToScreen (message ) { var div = "<div class='newmessage'>" + message + "</div>" ; var d = $("#output" ); var d = d[0 ]; var doScroll = d.scrollTop == d.scrollHeight - d.clientHeight ; $("#output" ).append (div); if (doScroll) { d.scrollTop = d.scrollHeight - d.clientHeight ; } } function en (event ) { var evt = evt ? evt : (window .event ? window .event : null ); if (evt.keyCode == 13 ) { doSend () } } </script > </html >
JSONP JSONP 是一个能够被跨域访问资源的方法。
服务端 server.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package mainimport ( "math/rand" "net/http" "time" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) func main () { e := echo.New() e.Use(middleware.Logger()) e.Use(middleware.Recover()) e.Static("/" , "JSONP示例" ) e.GET("/jsonp" , func (c echo.Context) error { callback := c.QueryParam("callback" ) var content struct { Response string `json:"response"` Timestamp time.Time `json:"timestamp"` Random int `json:"random"` } content.Response = "Sent via JSONP" content.Timestamp = time.Now().UTC() content.Random = rand.Intn(1000 ) return c.JSONP(http.StatusOK, callback, &content) }) e.Logger.Fatal(e.Start(":1323" )) }
客户端 index.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <!DOCTYPE html > <html > <head > <meta http-equiv ="Content-Type" content ="text/html; charset=UTF-8" /> <link rel ="stylesheet" href ="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" > <title > JSONP</title > <script type ="text/javascript" src ="//ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js" > </script > <script type ="text/javascript" > var host_prefix = 'http://localhost:1323' ; $(document ).ready (function ( ) { $("#jsonp-button" ).click (function (e ) { e.preventDefault (); var url = host_prefix + '/jsonp?callback=?' ; $.getJSON (url, function (jsonp ) { console .log (jsonp); $("#jsonp-response" ).html (JSON .stringify (jsonp, null , 2 )); }); }); }); </script > </head > <body > <div class ="container" style ="margin-top: 50px;" > <input type ="button" class ="btn btn-primary btn-lg" id ="jsonp-button" value ="Get JSONP response" > <p > <pre id ="jsonp-response" > </pre > </p > </div > </body > </html >
启动服务器,在浏览器进行一次请求,Get JSONP response 后获得跨域资源
文件上传 上传单个文件 服务端 server.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 package mainimport ( "fmt" "io" "net/http" "os" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) func upload (c echo.Context) error { name := c.FormValue("name" ) email := c.FormValue("email" ) file, err := c.FormFile("file" ) if err != nil { return err } src, err := file.Open() if err != nil { return err } defer src.Close() dst, err := os.Create(file.Filename) if err != nil { return err } defer dst.Close() if _, err = io.Copy(dst, src); err != nil { return err } return c.HTML(http.StatusOK, fmt.Sprintf("<p>File %s uploaded successfully with fields name=%s and email=%s.</p>" , file.Filename, name, email)) } func main () { e := echo.New() e.Use(middleware.Logger()) e.Use(middleware.Recover()) e.Static("/" , "文件上传示例" ) e.POST("/upload" , upload) e.Logger.Fatal(e.Start(":1323" )) }
客户端 index.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <!doctype html > <html lang ="en" > <head > <meta charset ="utf-8" > <title > Single file upload</title > </head > <body > <h1 > Upload single file with fields</h1 > <form action ="/upload" method ="post" enctype ="multipart/form-data" > Name: <input type ="text" name ="name" > <br > Email: <input type ="email" name ="email" > <br > Files: <input type ="file" name ="file" > <br > <br > <input type ="submit" value ="Submit" > </form > </body > </html >
上传多个文件 服务端 server.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 package mainimport ( "fmt" "io" "net/http" "os" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) func upload (c echo.Context) error { name := c.FormValue("name" ) email := c.FormValue("email" ) form, err := c.MultipartForm() if err != nil { return err } files := form.File["files" ] for _, file := range files { src, err := file.Open() if err != nil { return err } defer src.Close() dst, err := os.Create(file.Filename) if err != nil { return err } defer dst.Close() if _, err = io.Copy(dst, src); err != nil { return err } } return c.HTML(http.StatusOK, fmt.Sprintf("<p>Uploaded successfully %d files with fields name=%s and email=%s.</p>" , len (files), name, email)) } func main () { e := echo.New() e.Use(middleware.Logger()) e.Use(middleware.Recover()) e.Static("/" , "文件上传示例" ) e.POST("/upload" , upload) e.Logger.Fatal(e.Start(":1323" )) }
客户端 index.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <!doctype html > <html lang ="en" > <head > <meta charset ="utf-8" > <title > Multiple file upload</title > </head > <body > <h1 > Upload multiple files with fields</h1 > <form action ="/upload" method ="post" enctype ="multipart/form-data" > Name: <input type ="text" name ="name" > <br > Email: <input type ="email" name ="email" > <br > Files: <input type ="file" name ="files" multiple > <br > <br > <input type ="submit" value ="Submit" > </form > </body > </html >
示例(单个文件) 请求 http://localhost:1323/
填好表单后选择要上传的文件,再 Submit
可以看到文件被成功上传,并且表单中的 name 和 email 值也被成功获取。
在服务端所在的目录下,可以看到文件被成功创建并复制。
若先在服务端所在目录下创建一个空的文件 login.png
再尝试上传一个用内容的同名文件 login.png
我们再次查看原来的空文件
发现已经有了和上传的文件一样的内容
子域名 客户端
server.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 package mainimport ( "net/http" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) type ( Host struct { Echo *echo.Echo } ) func main () { hosts := map [string ]*Host{} api := echo.New() api.Use(middleware.Logger()) api.Use(middleware.Recover()) hosts["api.localhost:1323" ] = &Host{api} api.GET("/" , func (c echo.Context) error { return c.String(http.StatusOK, "API" ) }) blog := echo.New() blog.Use(middleware.Logger()) blog.Use(middleware.Recover()) hosts["blog.localhost:1323" ] = &Host{blog} blog.GET("/" , func (c echo.Context) error { return c.String(http.StatusOK, "Blog" ) }) site := echo.New() site.Use(middleware.Logger()) site.Use(middleware.Recover()) hosts["localhost:1323" ] = &Host{site} site.GET("/" , func (c echo.Context) error { return c.String(http.StatusOK, "Website" ) }) e := echo.New() e.Any("/*" , func (c echo.Context) (err error ) { req := c.Request() res := c.Response() host := hosts[req.Host] if host == nil { err = echo.ErrNotFound } else { host.Echo.ServeHTTP(res, req) } return }) e.Logger.Fatal(e.Start(":1323" )) }
服务端
通过对原域名和子域名的请求,可以返回不同的响应,也可以在服务端实现对不同子域名提供不同的服务。
在终端我们可以看到请求和响应信息的输出,这说明每次请求都以 e.Any("/*", func(c echo.Context) (err error){...}
作为入口,再匹配各自路由中的控制器函数,来对客户端做出响应。
JWT 概念 什么是JWT?
JWT (JSON Web Token) 是目前最流行的跨域认证解决方案,是一种基于 Token 的认证授权机制。 从 JWT 的全称可以看出,JWT 本身也是 Token,一种规范化之后的 JSON 结构的 Token。
JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 Session 信息。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。
JWT由哪些部分组成?
JWT 本质上就是一组字串,通过(.
)切分成三个为 Base64 编码的部分:
Header : 描述 JWT 的元数据,定义了生成签名的算法以及 Token
的类型。
Payload : 用来存放实际需要传递的数据
Signature(签名) :服务器通过 Payload、Header 和一个密钥(Secret)使用 Header 里面指定的签名算法(默认是 HMAC SHA256)生成。
JWT 通常是这样的:xxxxx.yyyyy.zzzzz
。
示例:
1 2 3 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ. SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
你可以在 jwt.ioopen in new window 这个网站上对其 JWT 进行解码,解码之后得到的就是 Header、Payload、Signature 这三部分。
Header 和 Payload 都是 JSON 格式的数据,Signature 由 Payload、Header 和 Secret(密钥)通过特定的计算公式和加密算法得到。
Header
Header 通常由两部分组成:
typ
(Type):令牌类型,也就是 JWT。
alg
(Algorithm) :签名算法,比如 HS256。
示例:
1 2 3 4 { "alg" : "HS256" , "typ" : "JWT" }
JSON 形式的 Header 被转换成 Base64 编码,成为 JWT 的第一部分。
Payload
Payload 也是 JSON 格式数据,其中包含了 Claims(声明,包含 JWT 的相关信息)。
Claims 分为三种类型:
下面是一些常见的注册声明:
iss
(issuer):JWT 签发方。
iat
(issued at time):JWT 签发时间。
sub
(subject):JWT 主题。
aud
(audience):JWT 接收方。
exp
(expiration time):JWT 的过期时间。
nbf
(not before time):JWT 生效时间,早于该定义的时间的 JWT 不能被接受处理。
jti
(JWT ID):JWT 唯一标识。
示例:
1 2 3 4 5 6 7 8 { "uid" : "ff1212f5-d8d1-4496-bf41-d2dda73de19a" , "sub" : "1234567890" , "name" : "John Doe" , "exp" : 15323232 , "iat" : 1516239022 , "scope" : [ "admin" , "user" ] }
Payload 部分默认是不加密的,一定不要将隐私信息存放在 Payload 当中!!!
JSON 形式的 Payload 被转换成 Base64 编码,成为 JWT 的第二部分。
Signature
Signature 部分是对前两部分的签名,作用是防止 JWT(主要是 payload) 被篡改。
这个签名的生成需要用到:
Header + Payload。
存放在服务端的密钥(一定不要泄露出去)。
签名算法。
签名的计算公式如下:
1 2 3 4 HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用”点”(.
)分隔,这个字符串就是 JWT 。
可以在这里 找到 JWT 中间件配置。
JWT 使用 HS256 算法认证。
JWT 从 Authorization
请求头取出数据。
服务端 使用自定义claims的服务端 server.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 package mainimport ( "net/http" "time" "github.com/golang-jwt/jwt" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) type jwtCustomClaims struct { Name string `json:"name"` Admin bool `json:"admin"` jwt.StandardClaims } func login (c echo.Context) error { username := c.FormValue("username" ) password := c.FormValue("password" ) if username != "jon" || password != "shhh!" { return echo.ErrUnauthorized } claims := &jwtCustomClaims{ "Jon Snow" , true , jwt.StandardClaims{ ExpiresAt: time.Now().Add(time.Hour * 72 ).Unix(), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) t, err := token.SignedString([]byte ("secret" )) if err != nil { return err } return c.JSON(http.StatusOK, echo.Map{ "token" : t, }) } func accessible (c echo.Context) error { return c.String(http.StatusOK, "Accessible" ) } func restricted (c echo.Context) error { user := c.Get("user" ).(*jwt.Token) claims := user.Claims.(*jwtCustomClaims) name := claims.Name return c.String(http.StatusOK, "Welcome " +name+"!" ) } func main () { e := echo.New() e.Use(middleware.Logger()) e.Use(middleware.Recover()) e.POST("/login" , login) e.GET("/" , accessible) r := e.Group("/restricted" ) config := middleware.JWTConfig{ Claims: &jwtCustomClaims{}, SigningKey: []byte ("secret" ), } r.Use(middleware.JWTWithConfig(config)) r.GET("" , restricted) e.Logger.Fatal(e.Start(":1323" )) }
服务端使用用户定义的KeyFunc server.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 package mainimport ( "context" "errors" "fmt" "net/http" jwt "github.com/golang-jwt/jwt" echo "github.com/labstack/echo/v4" middleware "github.com/labstack/echo/v4/middleware" jwk "github.com/lestrrat-go/jwx/jwk" ) func getKey (token *jwt.Token) (interface {}, error ) { keySet, err := jwk.Fetch(context.Background(), "https://www.googleapis.com/oauth2/v3/certs" ) if err != nil { return nil , err } keyID, ok := token.Header["kid" ].(string ) if !ok { return nil , errors.New("expecting JWT header to have a key ID in the kid field" ) } key, found := keySet.LookupKeyID(keyID) if !found { return nil , fmt.Errorf("unable to find key %q" , keyID) } var pubkey interface {} if err := key.Raw(&pubkey); err != nil { return nil , fmt.Errorf("Unable to get the public key. Error: %s" , err.Error()) } return pubkey, nil } func accessible (c echo.Context) error { return c.String(http.StatusOK, "Accessible" ) } func restricted (c echo.Context) error { user := c.Get("user" ).(*jwt.Token) claims := user.Claims.(jwt.MapClaims) name := claims["name" ].(string ) return c.String(http.StatusOK, "Welcome " +name+"!" ) } func main () { e := echo.New() e.Use(middleware.Logger()) e.Use(middleware.Recover()) e.GET("/" , accessible) r := e.Group("/restricted" ) { config := middleware.JWTConfig{ KeyFunc: getKey, } r.Use(middleware.JWTWithConfig(config)) r.GET("" , restricted) } e.Logger.Fatal(e.Start(":1323" )) }
客户端 curl
登录 使用账号和密码登录获取 token。
1 curl -X POST -d 'username=jon' -d 'password=shhh!' localhost:1323/login
响应
1 2 3 { "token" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NjE5NTcxMzZ9.RB3arc4-OyzASAaUhC2W3ReWaXAt_z2Fd3BN4aWTgEY" }
请求 在 Authorization
请求头设置 token,发送请求获取资源。
1 curl localhost:1323/restricted -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NjE5NTcxMzZ9.RB3arc4-OyzASAaUhC2W3ReWaXAt_z2Fd3BN4aWTgEY"
响应
示例(服务端定义clams)
先通过 POST 传表单数据用于创建 token
注:“”
不是 token 的内容,“”
内的值才是
访问受限组的 localhost:1323/restricted (有认证)并在请求头中添加 Authorization 其值为 Bearer Token,得到成功访问的响应
尝试访问无认证路由 localhost:1323/ ,得到 Accessible 的正常访问响应
平滑关闭 服务端 server.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package mainimport ( "context" "net/http" "os" "os/signal" "time" "github.com/labstack/echo/v4" "github.com/labstack/gommon/log" ) func main () { e := echo.New() e.Logger.SetLevel(log.INFO) e.GET("/" , func (c echo.Context) error { time.Sleep(5 * time.Second) return c.JSON(http.StatusOK, "OK" ) }) go func () { if err := e.Start(":1323" ); err != nil && err != http.ErrServerClosed { e.Logger.Fatal("shutting down the server" ) } }() quit := make (chan os.Signal, 1 ) signal.Notify(quit, os.Interrupt) <-quit ctx, cancel := context.WithTimeout(context.Background(), 10 *time.Second) defer cancel() if err := e.Shutdown(ctx); err != nil { e.Logger.Fatal(err) } }
Requires go1.8+
客户端 启动服务器并发送请求
服务器正常启动,在 5 秒后返回响应 “OK”
资源嵌入 服务端 使用 go 1.16嵌入功能 server.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package mainimport ( "embed" "io/fs" "log" "net/http" "os" "github.com/labstack/echo/v4" ) var embededFiles embed.FSfunc getFileSystem (useOS bool ) http.FileSystem { if useOS { log.Print("using live mode" ) return http.FS(os.DirFS("app" )) } log.Print("using embed mode" ) fsys, err := fs.Sub(embededFiles, "app" ) if err != nil { panic (err) } return http.FS(fsys) } func main () { e := echo.New() useOS := len (os.Args) > 1 && os.Args[1 ] == "live" assetHandler := http.FileServer(getFileSystem(useOS)) e.GET("/" , echo.WrapHandler(assetHandler)) e.GET("/static/*" , echo.WrapHandler(http.StripPrefix("/static/" , assetHandler))) e.Logger.Fatal(e.Start(":1323" )) }
Source Code
在创建 app 文件夹后//go:embed app
依旧报错,则重启一下 vs
不懂 //go:embed
参考:读取文件 | 梧席的小站 (wuster.store)
使用 go.rice server.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport ( "net/http" "github.com/GeertJohan/go.rice" "github.com/labstack/echo/v4" ) func main () { e := echo.New() assetHandler := http.FileServer(rice.MustFindBox("app" ).HTTPBox()) e.GET("/" , echo.WrapHandler(assetHandler)) e.GET("/static/*" , echo.WrapHandler(http.StripPrefix("/static/" , assetHandler))) e.Logger.Fatal(e.Start(":1323" )) }
Source Code
示例 使用 embed 模式
使用 live 模式
我们尝试对 http://localhost:1323/ 进行请求
可以得到响应返回的 app 文件夹中的目录
我们尝试对 http://localhost:1323/static/wood.jpg 进行请求
发现 /static/ 已经绑定了 app 文件夹,并实现了资源嵌入
参考文献:
Streaming Response Recipe | Echo - High performance, minimalist Go web framework (labstack.com)
响应式流(Reactive,Streams)_浮生梦浮生的博客-CSDN博客_响应式流
万字长文,一篇吃透WebSocket:概念、原理、易错常识、动手实践 - 腾讯云开发者社区-腾讯云 (tencent.com)
JWT 基础概念详解 | JavaGuide
facebookarchive/grace: Graceful restart & zero downtime deploy for Go servers. (github.com)
Embed Resources Recipe | Echo - High performance, minimalist Go web framework (labstack.com)
用Postman调试跨域问题 - 上课爱睡觉 - 博客园 (cnblogs.com)
JSON Web Tokens - jwt.io
本地测试websocket连接通信案例_冯小东的博客-CSDN博客_本地websocket测试
curl 的用法指南 - 阮一峰的网络日志 (ruanyifeng.com)
facebookarchive/grace: Graceful restart & zero downtime deploy for Go servers. (github.com)