Go net/http,web server
net/http 包实现 HTTP Server
Go 中,实现一个最简单的 http server 非常容易,代码如下:
package main
import (
"fmt"
"net/http"
) func IndexHandlers(w http.ResponseWriter, r *http.Request){
fmt.Fprintln(w, "hello, world")
} func main (){
http.HandleFunc("/", IndexHandlers)
err := http.ListenAndServe("127.0.0.1:8088", nil)
if err != nil {
fmt.Printf("listen error:[%v]", err.Error())
}
}
HTTP
通过上面这个简单的例子,来一点一点学习 Go 的 net/http 实现的 web 服务的原理
理解 HTTP 相关的网络应用,主要关注两个地方:客户端(client)和服务端(server)
两者的交互主要是 client 的 request 以及 server 的 response,主要就在于如何接受 client 的 request 并向 client 返回 response
接收 request 的过程中,最重要的莫过于路由(router),即实现一个 Multiplexer(多路复用的路由系统),Multiplexer 路由系统的目的就是为了找到处理器函数(handler),handler 将对 request 进行处理,同时构建 response
流程如下:
Clinet -> Requests -> [Multiplexer(router) -> handler -> Response -> Clinet
理解 Go 中的 http 服务,最重要的就是要理解 Multiplexer 和 handler,下面对 handler 的几个重要概念进行说明
- handler 函数:具有 func(w http.ResponseWriter, r *http.Requests) 签名的函数
- handler 处理器(函数):经过 HandlerFunc() 函数包装处理后的 handler 函数,就成为实现了 ServeHTTP() 接口方法的函数,调用 handler 处理器的 ServeHTTP() 方法时,即调用 handler 函数本身
- handler 对象:实现了 Handler 接口中 ServeHTTP() 方法的结构
Handler 接口
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
Go 没有继承,类多态的方式可以通过接口实现,所谓接口则是定义了声明了函数的签名,任何结构只要实现了与接口函数签名相同的方式,就等同于实现了接口,Go 的 http 服务都是基于 handler 进行处理
任何结构体,只要实现了 ServeHTTP() 方法,这个结构就可以称之为 handler 对象,ServeMux 会使用 handler 并调用其 ServeHTTP() 方法处理请求并返回响应
ServeMux 结构体
Go 中的 Multiplexer 是基于 ServeMux 结构体,DefaultServeMux是ServeMux的一个实例
Go 中既可以使用内置的 DefaultServeMux(默认的路由系统),也可以自定义
ServeMux 结构中最重要的字段是 m,这是一个 map,key是一些 url 模式,value 则是一个 muxEntry 结构体,定义存储了具体的 url 模式 和 Handler 接口
ServeMux 同时也实现了 Handler 接口,不过 ServeMux 的 ServeHTTP() 方法不是用来处理 request 和 构建 response 的,而是用来找到路由注册的 hanlder() 函数
ServeMux 的源码
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
hosts bool
} type muxEntry struct {
explicit bool
h Handler
pattern string
}
Server
除了 ServeMux 和 Handler,还有一个 Server 结构体需要了解,从 http.ListenAndServe 的源码可以看出,它创建了一个 server 对象,并调用 server 对象的 ListenAndServe() 方法
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
查看 Server 的结构体如下:
type Server struct {
Addr string
Handler Handler
ReadTimeout time.Duration
WriteTimeout time.Duration
TLSConfig *tls.Config MaxHeaderBytes int TLSNextProto map[string]func(*Server, *tls.Conn, Handler) ConnState func(net.Conn, ConnState)
ErrorLog *log.Logger
disableKeepAlives int32 nextProtoOnce sync.Once
nextProtoErr error
}
Server 结构存储了服务器处理请求常见的字段,其中 Handler 字段也保留了 Handler 接口类型,如果 Server 接口没有提供 Handler 接口类型,那么会使用内置的 DefaultServeMux(默认的路由系统),后面再做分析
创建 HTTP 服务
创建一个 http 服务,大致需要经历两个过程,首先需要注册路由,即提供 url 模式 和 handler 函数的映射,其次就是实例化一个 Server 的对象,并开启对客户端的监听
再看 http 服务中的代码:
//注册路由
http.HandleFunc("/", indexHandler) //实例化 Server 类型,并开启对客户端的监听
http.ListenAndServe("127.0.0.1:8000", nil) 或者: server := &Server{Addr: addr, Handler: handler}
server.ListenAndServe()
注册路由,开启服务监听,处理http请求的过程 源码分析
net/http 包提供的注册路由的 api 很简单,http.HandleFunc 选择了 DefaultServeMux 作为 Multiplexer
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
DefaultServeMux 是 ServeMux 结构体的实例,当然 http 包也提供了 NewServeMux() 方法创建一个 ServeMux 实例,默认则创建一个 DefaultServeMux
// NewServeMux allocates and returns a new ServeMux.
func NewServeMux() *ServeMux { return new(ServeMux) } // DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux var defaultServeMux ServeMux
DefaultServeMux 的 HandlerFunc(parrern, handler) 方法实际是定义在 ServeMux 下的
// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
mux.Handle(pattern, HandlerFunc(handler))
}
HandlerFunc() 是一个函数,同时实现了 Handler 接口的 ServeHTTP() 方法,使用 HandlerFunc() 函数包装路由定义的 IndexHandlers() 函数,其目的就是为了让这个函数也实现 ServeHTTP() 方法,从而实现 Handler 接口,即转变成一个 handler 处理器(函数)
发生函数类型转换的源码:
// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))
}
HandlerFunc 函数实现 Handler 接口的源码
type HandlerFunc func(ResponseWriter, *Request) // ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
最开始写的例子中
http.HandleFunc("/",Indexhandler)
这样 IndexHandler() 函数实现了 Handler 接口,接下来,ServeMux 的 Handle() 方法将会对 pattern 和 IndexHandler() 函数做一个 map 映射
Handler() 函数的主要目的在于把 IndexHandler() 函数 和 pattern 模式绑定到 map[string]muxEntry 这样一个 map
// Handle registers the handler for the given pattern.
// If a handler already exists for pattern, Handle panics.
func (mux *ServeMux) Handle(pattern string, handler Handler) {
mux.mu.Lock()
defer mux.mu.Unlock() if pattern == "" {
panic("http: invalid pattern " + pattern)
}
if handler == nil {
panic("http: nil handler")
}
if mux.m[pattern].explicit {
panic("http: multiple registrations for " + pattern)
} if mux.m == nil {
mux.m = make(map[string]muxEntry)
}
mux.m[pattern] = muxEntry{explicit: true, h: handler, pattern: pattern} if pattern[0] != '/' {
mux.hosts = true
} // Helpful behavior:
// If pattern is /tree/, insert an implicit permanent redirect for /tree.
// It can be overridden by an explicit registration.
n := len(pattern)
if n > 0 && pattern[n-1] == '/' && !mux.m[pattern[0:n-1]].explicit {
// If pattern contains a host name, strip it and use remaining
// path for redirect.
path := pattern
if pattern[0] != '/' {
// In pattern, at least the last character is a '/', so
// strings.Index can't be -1.
path = pattern[strings.Index(pattern, "/"):]
}
url := &url.URL{Path: path}
mux.m[pattern[0:n-1]] = muxEntry{h: RedirectHandler(url.String(), StatusMovedPermanently), pattern: pattern}
}
}
此时,pattern 和 IndexHandler() 的路由注册完成,接下来就是如何开启 Server 的监听,以接收客户端的请求
注册好路由之后,启动 web 服务还需要开启服务器监听,http 包中的 ListenAndServe() 方法中可以看到创建一个 Server 类型对象,并调用了 Server 类型对象的同名方法
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
// ListenAndServe listens on the TCP network address srv.Addr and then
// calls Serve to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
// If srv.Addr is blank, ":http" is used.
// ListenAndServe always returns a non-nil error.
func (srv *Server) ListenAndServe() error {
addr := srv.Addr
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}
Server 的 ListenAndServe() 方法中,会初始化监听地址 Addr,同时调用 Listen() 方法设置监听,最后将监听的 TCP 对象传入 Serve() 方法
// Serve accepts incoming connections on the Listener l, creating a
// new service goroutine for each. The service goroutines read requests and
// then call srv.Handler to reply to them.
//
// For HTTP/2 support, srv.TLSConfig should be initialized to the
// provided listener's TLS Config before calling Serve. If
// srv.TLSConfig is non-nil and doesn't include the string "h2" in
// Config.NextProtos, HTTP/2 support is not enabled.
//
// Serve always returns a non-nil error. After Shutdown or Close, the
// returned error is ErrServerClosed.
func (srv *Server) Serve(l net.Listener) error {
defer l.Close()
if fn := testHookServerServe; fn != nil {
fn(srv, l)
}
var tempDelay time.Duration // how long to sleep on accept failure if err := srv.setupHTTP2_Serve(); err != nil {
return err
} srv.trackListener(l, true)
defer srv.trackListener(l, false) baseCtx := context.Background() // base is always background, per Issue 16220
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
for {
rw, e := l.Accept()
if e != nil {
select {
case <-srv.getDoneChan():
return ErrServerClosed
default:
}
if ne, ok := e.(net.Error); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)
time.Sleep(tempDelay)
continue
}
return e
}
tempDelay = 0
c := srv.newConn(rw)
c.setState(c.rwc, StateNew) // before Serve can return
go c.serve(ctx)
}
}
监听开启之后,一旦有客户端请求过来,Go 就开启一个协程处理请求,主要逻辑都在 Serve() 方法中
Serve() 方法比较长,其主要职能就是,创建一个上下文对象,然后调用 Listener 的 Accept() 方法用来获取连接数据并使用 newConn() 方法创建连接对象,最后使用 goroutine 协程的方式处理连接请求,因为每一个连接都开启了一个协程,请求的上下文都不同,同时又保证了 Go 的高并发
Serve()方法的源码:
使用 defer 定义了函数退出时,连接关闭相关的处理,然后就是读取连接的网络数据,并处理读取完毕时候的状态,接下来就是调用 serverHandler{c.server}.ServeHTTP(w, w.req) 方法处理请求了,最后就是请求处理完毕的逻辑
// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
c.remoteAddr = c.rwc.RemoteAddr().String()
ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
defer func() {
if err := recover(); err != nil && err != ErrAbortHandler {
const size = 64 << 10
buf := make([]byte, size)
buf = buf[:runtime.Stack(buf, false)]
c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
}
if !c.hijacked() {
c.close()
c.setState(c.rwc, StateClosed)
}
}() if tlsConn, ok := c.rwc.(*tls.Conn); ok {
if d := c.server.ReadTimeout; d != 0 {
c.rwc.SetReadDeadline(time.Now().Add(d))
}
if d := c.server.WriteTimeout; d != 0 {
c.rwc.SetWriteDeadline(time.Now().Add(d))
}
if err := tlsConn.Handshake(); err != nil {
c.server.logf("http: TLS handshake error from %s: %v", c.rwc.RemoteAddr(), err)
return
}
c.tlsState = new(tls.ConnectionState)
*c.tlsState = tlsConn.ConnectionState()
if proto := c.tlsState.NegotiatedProtocol; validNPN(proto) {
if fn := c.server.TLSNextProto[proto]; fn != nil {
h := initNPNRequest{tlsConn, serverHandler{c.server}}
fn(c.server, tlsConn, h)
}
return
}
} // HTTP/1.x from here on. ctx, cancelCtx := context.WithCancel(ctx)
c.cancelCtx = cancelCtx
defer cancelCtx() c.r = &connReader{conn: c}
c.bufr = newBufioReader(c.r)
c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10) for {
w, err := c.readRequest(ctx)
if c.r.remain != c.server.initialReadLimitSize() {
// If we read any bytes off the wire, we're active.
c.setState(c.rwc, StateActive)
}
if err != nil {
const errorHeaders = "\r\nContent-Type: text/plain; charset=utf-8\r\nConnection: close\r\n\r\n" if err == errTooLarge {
// Their HTTP client may or may not be
// able to read this if we're
// responding to them and hanging up
// while they're still writing their
// request. Undefined behavior.
const publicErr = "431 Request Header Fields Too Large"
fmt.Fprintf(c.rwc, "HTTP/1.1 "+publicErr+errorHeaders+publicErr)
c.closeWriteAndWait()
return
}
if isCommonNetReadError(err) {
return // don't reply
} publicErr := "400 Bad Request"
if v, ok := err.(badRequestError); ok {
publicErr = publicErr + ": " + string(v)
} fmt.Fprintf(c.rwc, "HTTP/1.1 "+publicErr+errorHeaders+publicErr)
return
} // Expect 100 Continue support
req := w.req
if req.expectsContinue() {
if req.ProtoAtLeast(1, 1) && req.ContentLength != 0 {
// Wrap the Body reader with one that replies on the connection
req.Body = &expectContinueReader{readCloser: req.Body, resp: w}
}
} else if req.Header.get("Expect") != "" {
w.sendExpectationFailed()
return
} c.curReq.Store(w) if requestBodyRemains(req.Body) {
registerOnHitEOF(req.Body, w.conn.r.startBackgroundRead)
} else {
if w.conn.bufr.Buffered() > 0 {
w.conn.r.closeNotifyFromPipelinedRequest()
}
w.conn.r.startBackgroundRead()
} // HTTP cannot have multiple simultaneous active requests.[*]
// Until the server replies to this request, it can't read another,
// so we might as well run the handler in this goroutine.
// [*] Not strictly true: HTTP pipelining. We could let them all process
// in parallel even if their responses need to be serialized.
// But we're not going to implement HTTP pipelining because it
// was never deployed in the wild and the answer is HTTP/2.
serverHandler{c.server}.ServeHTTP(w, w.req)
w.cancelCtx()
if c.hijacked() {
return
}
w.finishRequest()
if !w.shouldReuseConnection() {
if w.requestBodyLimitHit || w.closedRequestBodyEarly() {
c.closeWriteAndWait()
}
return
}
c.setState(c.rwc, StateIdle)
c.curReq.Store((*response)(nil)) if !w.conn.server.doKeepAlives() {
// We're in shutdown mode. We might've replied
// to the user without "Connection: close" and
// they might think they can send another
// request, but such is life with HTTP/1.1.
return
} if d := c.server.idleTimeout(); d != 0 {
c.rwc.SetReadDeadline(time.Now().Add(d))
if _, err := c.bufr.Peek(4); err != nil {
return
}
}
c.rwc.SetReadDeadline(time.Time{})
}
}
serverHandler 是一个重要的结构体类型,它只有一个字段,即 Server 结构体类型,同时 serverHandler 实现了 Handler 接口,并在该接口方法中做了一个重要的事情,初始化 Multiplexer 路由多路复用器,如果 Server 类型没有指定 handler 类型对象(实现了 Handler 接口的类型),则使用内置的 DefaultServeMux 作为 Multiplexer,并调用初始化 Handler 类型对象的 ServeHTTP() 方法
// serverHandler delegates to either the server's Handler or
// DefaultServeMux and also handles "OPTIONS *" requests.
type serverHandler struct {
srv *Server
} func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
if req.RequestURI == "*" && req.Method == "OPTIONS" {
handler = globalOptionsHandler{}
}
handler.ServeHTTP(rw, req)
}
这里 DefaultServeMux 的 ServeHTTP() 方法其实也是定义在 ServeMux 结构体中的,相关代码如下:
ServeMux 的 ServeHTTP() 方法通过调用其 handler() 方法寻找注册到路由上的 handler() 处理函数,并调用该函数的 ServeHTTP() 方法,在上面的例子中则是 IndexHander() 函数
ServeMux 的 handler() 方法对 URL 做了简单的处理,然后调用 handler() 函数,后者会创建一个锁,同时调用 match() 方法返回一个 handler 和 pattern
在 match() 方法中,ServeMux 的 m 字段是 map[string]muxEntry,后者存储了 pattern 和 handler 处理器函数,因此通过迭代 m 寻找出注册路由的 pattern 模式与实际 url 匹配的 handler() 函数并返回
返回的结构一直传递到 ServeMux 的 ServerHTTP() 方法,接下来调用 handler() 函数的 ServeHTTP() 方法,即 IndexHandler() 函数,然后把 response 写到 http.RequestWriter 对象返回给客户端
IndexHandler() 函数运行结束,即 serverHandler{c.server}.ServeHTTP(w, w.req) 运行结束,接下来就是对请求处理完毕之后,断开连接的相关逻辑
// Find a handler on a handler map given a path string.
// Most-specific (longest) pattern wins.
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
// Check for exact match first.
v, ok := mux.m[path]
if ok {
return v.h, v.pattern
} // Check for longest valid match.
var n = 0
for k, v := range mux.m {
if !pathMatch(k, path) {
continue
}
if h == nil || len(k) > n {
n = len(k)
h = v.h
pattern = v.pattern
}
}
return
}
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) { // CONNECT requests are not canonicalized.
if r.Method == "CONNECT" {
return mux.handler(r.Host, r.URL.Path)
} // All other requests have any port stripped and path cleaned
// before passing to mux.handler.
host := stripHostPort(r.Host)
path := cleanPath(r.URL.Path)
if path != r.URL.Path {
_, pattern = mux.handler(host, path)
url := *r.URL
url.Path = path
return RedirectHandler(url.String(), StatusMovedPermanently), pattern
} return mux.handler(host, r.URL.Path)
} // handler is the main implementation of Handler.
// The path is known to be in canonical form, except for CONNECT methods.
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
mux.mu.RLock()
defer mux.mu.RUnlock() // Host-specific pattern takes precedence over generic ones
if mux.hosts {
h, pattern = mux.match(host + path)
}
if h == nil {
h, pattern = mux.match(path)
}
if h == nil {
h, pattern = NotFoundHandler(), ""
}
return
} // ServeHTTP dispatches the request to the handler whose
// pattern most closely matches the request URL.
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
if r.RequestURI == "*" {
if r.ProtoAtLeast(1, 1) {
w.Header().Set("Connection", "close")
}
w.WriteHeader(StatusBadRequest)
return
}
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}
至此,Go 中一个完整的 http 服务介绍完毕,包括 注册路由,开启监听,处理连接,路由处理函数
多数的 web 应用基于 HTTP 协议,客户端和服务端通过 request 和 response 的方式交互,一个 server 必不可少的两部分莫过于路由注册和连接处理,Go 通过一个 ServeMux 实现了 Multiplexer 路由多路复用器来管理路由,同时提供一个 Handler 接口提供 ServeHTTP() 方法实现了包装 handler() 处理器函数,handler() 函数处理 request 并构造 response
ServeMux 和 handler() 处理器函数的连接桥梁就是 Handler 接口,ServeMux 的 ServeHTTP() 方法实现了寻找注册路由的 handler() 处理器函数,并调用该函数的 ServeHTTP() 方法,ServeHTTP() 方法就是真正处理请求和构造响应的地方
参考链接:https://www.cnblogs.com/zhaof/p/8569743.html
ending ~
Go net/http,web server的更多相关文章
- Simotion应用案例,使用Simotion web server调试,使用Project Generator创建项目,Simosim模拟运行运行项目
Simotion web server simotion项目设计和调试过程中,web server功能越来越常用.例如Project generator生成的FBAxis, winder, print ...
- C#中自己动手创建一个Web Server(非Socket实现)
目录 介绍 Web Server在Web架构系统中的作用 Web Server与Web网站程序的交互 HTTPListener与Socket两种方式的差异 附带Demo源码概述 Demo效果截图 总结 ...
- Web Server PROPFIND Method internal IP Discosure
Title:Web Server PROPFIND Method internal IP Discosure --2012-11-09 09:47 Nessus扫描出来一个安全缺陷,Web Serv ...
- 有关于web server架构的一个小疑问
今天闲的时候trace route了yahoo和sina的域名,yahoo的如下: 1 1 ms 1 ms <1 ms 172.21.127.1 2 3 ms ...
- WebSphere之Web Server插件
WebSphere之Web Server插件 -----------------------------------2013/10/19 这里记录一下WebSphere与IHS的安装集成,和Web S ...
- 自己动手实现网络服务器(Web Server)——基于C#
前言 最近在学习网络原理,突然萌发出自己实现一个网络服务器的想法,并且由于第三代小白机器人的开发需要,我把之前使用python.PHP写的那部分代码都迁移到了C#(别问我为什么这么喜欢C#),之前使用 ...
- 转载的web server实例
asp.net—web server模拟网上购物 2014-05-08 我来说两句 来源:asp.net—web server模拟网上购物 收藏 我要投稿 在学vb的时候学到了a ...
- Web Server 与 App Server
Web Server 常见的Web Server有Apache Server与Nginx. Apache Http Server是Apache软件基金会下的一个项目,是一款开源的HTTP服务器软件(它 ...
- CentOS 6.9安装类型选择(Basic Server/Web Server)
Desktop :基本的桌面系统,包括常用的桌面软件,如文档查看工具. Minimal Desktop:基本的桌面系统,包含的软件更少. Minimal:基本的系统,不含有任何可选的软件包. Basi ...
随机推荐
- VsCode写Python代码!这代码简直和大神一样规范!太漂亮了!
VsCode写Python代码!这代码简直和大神一样规范!太漂亮了! 转 https://www.jianshu.com/p/636306763d89 VsCode虽然没有Pycharm的功能齐 ...
- flutter 数据存储 SP和sqlite
添加插件: shared_preferences: ^0.4.2 path_provider: ^1.2.0 sqflite: ^0.12.0 import 'dart:async'; import ...
- HBase在特征工程中的应用
前言HBase是一款分布式的NoSQL DB,可以轻松扩展存储和读写能力. 主要特性有: 按某精确的key获取对应的value(Get) 通过前缀匹配一段相邻的数据(Scan) 多版本 动态列 服务端 ...
- IFC构件位置信息—ObjectPlacement
在IFC标准中,采用相对坐标系对构件定位.如柱(IfcColumn)的定位信息(局部坐标系及参考坐标系)由ObjectPlacement描述.ObjectPlacement由两部分组成: (1)Pla ...
- 【Redis】CacheCloud介绍及快速开始
CacheCloud是做什么的 CacheCloud提供一个Redis云管理平台:实现多种类型(Redis Standalone.Redis Sentinel.Redis Cluster)自动部署.解 ...
- Laya的调试,调试面板,断点调试
参考: 性能统计面板介绍 版本2.1.1.1 调试面板 Laya有两个调试选项,编辑模式F9. 第一个调试模式,除了调试面板,还有一个查看当前舞台对象的面板.类似白鹭的Egret Inspector. ...
- Django安全配置(settings.py)详解
必须配置项 PASSWORD_HASHER 这个配置是在使用Django自带的密码加密函数的时候会使用的加密算法的列表.默认如下: PASSWORD_HASHERS = ( 'django.contr ...
- Flink 实现指定时长或消息条数的触发器
Flink 中窗口是很重要的一个功能,而窗口又经常配合触发器一起使用. Flink 自带的触发器大概有: CountTrigger: 指定条数触发 ContinuousEventTimeTrigger ...
- centos7上安装mysql8(下)
1.修改root密码 MySQL8和5的密码加密方式不同,mysql_native_password是5的加密方式.mysql已经将之前的mysql_native_password认证,修改成了cac ...
- HTML布局排版2如何设置div总是相对于页面居中
由于浏览器页面有大有小,为了适应浏览器宽度,可以让div总是对于浏览器居中,设置div左右两边的margin为auto可以实现这个效果.如图,第一个是默认div,无上下左右外边距,第二个是设置了mar ...