http 是典型的 C/S 架构,客户端向服务端发送请求(request),服务端做出应答(response)。HTTP server–简而言之就是一个支持http协议的服务,http是一个相对简单的请求—响应的协议,通常是运行在TCP连接之上, 通过客户端发送请求到服务端,获取服务端的响应。

我们先看一个简单的http  server例子

    package mainimport ("io""net/http""log")// hello world, the web serverfunc HelloServer(w http.ResponseWriter, req *http.Request) {io.WriteString(w, "hello, world!\n")}func main() {//路由注册http.HandleFunc("/hello", HelloServer)//开启一个http服务log.Fatal(http.ListenAndServe(":8086", nil))}

当服务器启动后,浏览器访问:http://127.0.0.1:8086/hello

网页上会显示:

以上就是http 服务的一个简单demo,下面我们主要从源码来分析http server

源码分析

一、  常用概念

  1.Handler接口

 type Handler interface {ServeHTTP(ResponseWriter, *Request)
}

   作用:Handler封装了处理客户端的请求逻辑及创建返回响应的逻辑,一个http server需要实现Handler接口。

2.Server结构体

//Server定义一个HTTP 服务
type Server struct {//监听的TCP地址Addr    string  // TCP address to listen on, ":http" if empty//要调用的处理函数,如果为nil,则默认用http.DefaultServeMuxHandler Handler // handler to invoke, http.DefaultServeMux if nil// TLSConfig optionally provides a TLS configuration for use// by ServeTLS and ListenAndServeTLS. Note that this value is// cloned by ServeTLS and ListenAndServeTLS, so it's not// possible to modify the configuration with methods like// tls.Config.SetSessionTicketKeys. To use// SetSessionTicketKeys, use Server.Serve with a TLS Listener// instead./*TLSConfig可以选择提供TLS  配置 用于使用ServeTLS 和 ListenAndServeTLS*/TLSConfig *tls.Config// ReadTimeout is the maximum duration for reading the entire// request, including the body.//// Because ReadTimeout does not let Handlers make per-request// decisions on each request body's acceptable deadline or// upload rate, most users will prefer to use// ReadHeaderTimeout. It is valid to use them both.//ReadTimeout读取请求的最长时间,包括正文ReadTimeout time.Duration// ReadHeaderTimeout is the amount of time allowed to read// request headers. The connection's read deadline is reset// after reading the headers and the Handler can decide what// is considered too slow for the body.//ReadHeaderTimeout是允许读取请求头的最长时间//读完请求头后会重置超时间,并且Handler可以判断太慢的bodyReadHeaderTimeout time.Duration// WriteTimeout is the maximum duration before timing out// writes of the response. It is reset whenever a new// request's header is read. Like ReadTimeout, it does not// let Handlers make decisions on a per-request basis.//WriteTimeout是写入响应之前的最大持续时间。 只要读取新请求的标头,它就会重置。 与ReadTimeout一样,它不允许处理程序根据请求做出决策。WriteTimeout time.Duration// IdleTimeout is the maximum amount of time to wait for the// next request when keep-alives are enabled. If IdleTimeout// is zero, the value of ReadTimeout is used. If both are// zero, ReadHeaderTimeout is used.//当开启keep-alives时,IdleTimeout是等待下个请求的最长时间,如果IdleTimeout是0,则使用ReadTimeout,如果ReadTimeout也是0,则使用ReadHeaderTimeoutIdleTimeout time.Duration// MaxHeaderBytes controls the maximum number of bytes the// server will read parsing the request header's keys and// values, including the request line. It does not limit the// size of the request body.// If zero, DefaultMaxHeaderBytes is used.//请求头的最大字节数,如果是0,则使用默认值DefaultMaxHeaderBytesMaxHeaderBytes int// 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// name negotiated. The Handler argument should be used to// handle HTTP requests and will initialize the Request's TLS// and RemoteAddr if not already set. The connection is// automatically closed when the function returns.// If TLSNextProto is not nil, HTTP/2 support is not enabled// automatically./*TLSNextProto可选地指定在发生NPN / ALPN协议升级时接管所提供的TLS连接的所有权的函数。 映射键是协商的协议名称。*/TLSNextProto map[string]func(*Server, *tls.Conn, Handler)// ConnState specifies an optional callback function that is// called when a client connection changes state. See the// ConnState type and associated constants for details./*ConnState指定在客户端连接更改状态时调用的可选回调函数。*/ConnState func(net.Conn, ConnState)// ErrorLog specifies an optional logger for errors accepting// connections, unexpected behavior from handlers, and// underlying FileSystem errors.// If nil, logging is done via the log package's standard logger.ErrorLog *log.LoggerdisableKeepAlives int32     // accessed atomically.原子访问inShutdown        int32     // accessed atomically (non-zero means we're in Shutdown) 原子访问,非零意味着已关闭nextProtoOnce     sync.Once // guards setupHTTP2_* initnextProtoErr      error     // result of http2.ConfigureServer if used     如果使用http2.ConfigureServer的结果mu         sync.Mutexlisteners  map[net.Listener]struct{}    //记录所有监听net.Listener信息activeConn map[*conn]struct{}   //记录所有处于active状态的连接doneChan   chan struct{}onShutdown []func()
}

  Server定义了一个HTTP服务,里面包含了监听的TCP地址Addr,处理客户端请求的Handler,以及其他相关的配置信息

3.conn结构体

// A conn represents the server side of an HTTP connection.
//conn表示HTTP连接的服务器端。
type conn struct {// server is the server on which the connection arrived.// Immutable; never nil.//server是连接到达的服务器,不能改变,不能为空。server *Server// cancelCtx cancels the connection-level context.//用于取消连接的上下文cancelCtx context.CancelFunc// rwc is the underlying network connection.// This is never wrapped by other types and is the value given out// to CloseNotifier callers. It is usually of type *net.TCPConn or// *tls.Conn.//rwc是网络底层连接,不能被其他类型包装//常用类型是 *net.TCPConn     *tls.Connrwc net.Conn// remoteAddr is rwc.RemoteAddr().String(). It is not populated synchronously// inside the Listener's Accept goroutine, as some implementations block.// It is populated immediately inside the (*conn).serve goroutine.// This is the value of a Handler's (*Request).RemoteAddr.//rwc.RemoteAddr().String()   客户端连接地址/*它不会在Listener的Accept goroutine中同步填充,因为某些实现会阻塞。 它立即填充在(* conn).serve goroutine中。这是Handler's(* Request).RemoteAddr的值。*/remoteAddr string// tlsState is the TLS connection state when using TLS.// nil means not TLS.//如果使用的是TLS,则表示TLS连接状态,nil表示没有TLStlsState *tls.ConnectionState// werr is set to the first write error to rwc.// It is set via checkConnErrorWriter{w}, where bufw writes.//werr设置为rwc的第一个写入错误。 它通过checkConnErrorWriter {w}设置,其中bufw写入。werr error// r is bufr's read source. It's a wrapper around rwc that provides// io.LimitedReader-style limiting (while reading request headers)// and functionality to support CloseNotifier. See *connReader docs.//是数据源用于bufr读取数据,它包装了rwc,提供o.LimitedReader-styler *connReader// bufr reads from r.//从r中读取数据bufr *bufio.Reader// bufw writes to checkConnErrorWriter{c}, which populates werr on error.//给 checkConnErrorWriter{c} 写bufw *bufio.Writer// lastMethod is the method of the most recent request// on this connection, if any.lastMethod stringcurReq atomic.Value // 记录Request个数curState atomic.Value // 记录连接状态 ConnState// mu guards hijackedvmu sync.Mutex// hijackedv is whether this connection has been hijacked// by a Handler with the Hijacker interface.// It is guarded by mu.//判断Handler中是否实现Hijacker接口hijackedv bool
}

  conn表示HTTP连接,它将底层的连接如:*net.TCPConn和 *tls.Conn进行封装

4.response结构体

type response struct {conn             *conn  //网络连接req              *Request // 客户端的请求信息reqBody          io.ReadClosercancelCtx        context.CancelFunc // when ServeHTTP exits 当ServeHTTP退出时调用wroteHeader      bool               // reply header has been (logically) written   header是否已经写入wroteContinue    bool               // 100 Continue response was written       100 Continue 响应已写入wants10KeepAlive bool               // HTTP/1.0 w/ Connection "keep-alive"  是否保持长连接wantsClose       bool               // HTTP request has Connection "close"   客户端想要断开连接w  *bufio.Writer // buffers output in chunks to chunkWriter  将缓冲区块输出到chunkWritercw chunkWriter// handlerHeader is the Header that Handlers get access to,// which may be retained and mutated even after WriteHeader.// handlerHeader is copied into cw.header at WriteHeader// time, and privately mutated thereafter.handlerHeader HeadercalledHeader  bool // handler accessed handlerHeader via Headerwritten       int64 // number of bytes written in body 写入到body中的字节数contentLength int64 // explicitly-declared Content-Length; or -1  声明的Content-Lengthstatus        int   // status code passed to WriteHeader    传递给WriteHeader的状态码// close connection after this reply.  set on request and// updated after response from handler if there's a// "Connection: keep-alive" response header and a// Content-Length.closeAfterReply bool  //判断在响应后是否关闭连接// requestBodyLimitHit is set by requestTooLarge when// maxBytesReader hits its max size. It is checked in// WriteHeader, to make sure we don't consume the// remaining request body to try to advance to the next HTTP// request. Instead, when this is set, we stop reading// subsequent requests on this connection and stop reading// input from it.requestBodyLimitHit bool// trailers are the headers to be sent after the handler// finishes writing the body. This field is initialized from// the Trailer response header when the response header is// written.trailers []stringhandlerDone atomicBool // set true when the handler exits// Buffers for Date, Content-Length, and status codedateBuf   [len(TimeFormat)]byteclenBuf   [10]bytestatusBuf [3]byte// closeNotifyCh is the channel returned by CloseNotify.// TODO(bradfitz): this is currently (for Go 1.8) always// non-nil. Make this lazily-created again as it used to be?closeNotifyCh  chan booldidCloseNotify int32 // atomic (only 0->1 winner should send)
}

二、http服务创建过程

我们根据上面提到过的demo来分析http服务创建的过程

// hello world, the web serverfunc HelloServer(w http.ResponseWriter, req *http.Request) {io.WriteString(w, "hello, world!\n")}func main() {//路由注册http.HandleFunc("/hello", HelloServer)//开启一个http服务log.Fatal(http.ListenAndServe(":8086", nil))}

大致流程如下:

    第一步:路由注册

// HandleFunc registers the handler function for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {DefaultServeMux.HandleFunc(pattern, handler)
}

         这里使用http.HandleFunc是使用http包自带的DefaultServeMux来进行服务的路由注册与管理。DefaultServeMux是全局变量,DefaultServeMux的定义如下:

// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMuxvar defaultServeMux ServeMux/*
HandleFunc根据给定的pattern注册handler函数
路由注册
*/
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {mux.Handle(pattern, HandlerFunc(handler))
}
/*
Handle 根据给定的pattern注册 handler,如果pattern已经注册过,则panic
*/
func (mux *ServeMux) Handle(pattern string, handler Handler) {mux.mu.Lock()defer mux.mu.Unlock()/*如果pattern为"",或者handler为nil,或者该pattern已经注册,则panic*/if pattern == "" {panic("http: invalid pattern")}if handler == nil {panic("http: nil handler")}if _, exist := mux.m[pattern]; exist {panic("http: multiple registrations for " + pattern)}if mux.m == nil { //如果m为nil,则初始化mux.m = make(map[string]muxEntry)}//将pattern和handler注册到m中mux.m[pattern] = muxEntry{h: handler, pattern: pattern}if pattern[0] != '/' {  //pattern第一个字符为'/',则hosts为true。说明该pattern包含主机名mux.hosts = true}
}

从以上代码可以看到路由的注册最后注册到了ServeMux结构体的m字段中,m是map格式储存了pattern和Handler的对应关系。ServeMux结构体如下:

  type ServeMux struct {mu    sync.RWMutexm     map[string]muxEntry   //path和muxEntry的对应关系hosts bool // pattern是否包含主机名
}type muxEntry struct {h       Handlerpattern string
}

ServeMux是HTTP请求的多路复用路由器,负责接收http handler的注册和路由解析。将所有的路径(pattern)与对应的处理函数映射存入到map表中。它将每个传入请求的URL与已注册模式列表进行匹配,并获取与URL最匹配的handler。

二、开启服务,监听客户端连接

func ListenAndServe(addr string, handler Handler) error {server := &Server{Addr: addr, Handler: handler}return server.ListenAndServe()
}

ListenAndServe 监听TCP的网络地址然后调用Serve的handler来处理连接上的请求,接收到的连接默认是启用TCP的keep-alives,也就是保持长连接,如果Handler为nil,则默认使用DefaultServeMux。

根据addr是监听的tcp地址和Handler创建服务server,然后调用ListenAndServe方法,ListenAndServe方法如下:

func (srv *Server) ListenAndServe() error {addr := srv.Addrif addr == "" {addr = ":http"}ln, err := net.Listen("tcp", addr)if err != nil {return err}return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}

监听TCP网络地址srv.Addr,然后调用Serve来处理传入连接上的请求。 接受的连接配置为启用TCP长连接。 如果srv.Addr为空,则使用":http"。

func (srv *Server) Serve(l net.Listener) error {defer l.Close()//如果testHookServerServe不为nil,则调用if fn := testHookServerServe; fn != nil {fn(srv, l)}var tempDelay time.Duration // how long to sleep on accept failure  当接收失败休眠多久//设置HTTP2的相关信息if err := srv.setupHTTP2_Serve(); err != nil {return err}//添加到srv的listeners中,listeners记录所有的监听器srv.trackListener(l, true)defer srv.trackListener(l, false)//初始化上下文,其父级是emptyCtx,用于保存key为ServerContextKey,value是对应的Server服务baseCtx := context.Background() // base is always background, per Issue 16220ctx := context.WithValue(baseCtx, ServerContextKey, srv)//监听新的tcp连接for {rw, e := l.Accept()  //rw监听到连接的tcp,rw是net.Connif e != nil {select {case <-srv.getDoneChan():  //Server已关闭return ErrServerCloseddefault:}if ne, ok := e.(net.Error); ok && ne.Temporary() {  //如果是接收失败的错误if tempDelay == 0 {  //接收等待时间为0,则给于默认值tempDelay = 5 * time.Millisecond} else {  //接收等待时间扩大两倍tempDelay *= 2}if max := 1 * time.Second; tempDelay > max {  //最大等待时间为1s,如果大于1s,则赋值为1stempDelay = max}srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)time.Sleep(tempDelay)continue}return e}tempDelay = 0  //接收失败等待时间置为0c := srv.newConn(rw)   //将net.Conn封装成http.connc.setState(c.rwc, StateNew) // 设置连接状态go c.serve(ctx)  //每一个新的连接,开启一个协程执行serve函数}
}

Serve方法 接收Listener l 上传入的连接,为每个连接开启一个服的goroutine,新开启的goroutine执行serve方法。也就是读取客户端的请求数据,然后根据URL获取对应的Handler,Handler就是处理请求并响应客户端的请求。

func (c *conn) serve(ctx context.Context) {c.remoteAddr = c.rwc.RemoteAddr().String()  //获取远程地址ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())   //创建一个子Context,父Context为ctxdefer func() {if err := recover(); err != nil && err != ErrAbortHandler {  //处理panicconst size = 64 << 10buf := 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() {  //没有hijacked挂住,则关闭连接,改变ConnStatec.close()c.setState(c.rwc, StateClosed)}}()//处理tls的连接if tlsConn, ok := c.rwc.(*tls.Conn); ok {   //如果该连接是tls.Conn  ,常见连接是net.TcpConn 和 tls.Connif 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}}  //初始化NPN请求fn(c.server, tlsConn, h)}return}}// HTTP/1.x from here on.//创建上下文ctx, cancelCtx := context.WithCancel(ctx)c.cancelCtx = cancelCtxdefer cancelCtx()//初始化connReaderc.r = &connReader{conn: c}//初始化bufioReader用于读数据c.bufr = newBufioReader(c.r)//初始化bufw用于写数据c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)for {//读取Request,并封装成Responsew, 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 supportreq := w.reqif req.expectsContinue() {  //如果用户的请求期望 100-continueif req.ProtoAtLeast(1, 1) && req.ContentLength != 0 {// Wrap the Body reader with one that replies on the connection//将请求的body进行封装req.Body = &expectContinueReader{readCloser: req.Body, resp: w}}} else if req.Header.get("Expect") != "" { //如果Header的Expect的值不是100-continue则返回错误码w.sendExpectationFailed()return}c.curReq.Store(w)  //储存response,并且response中有requestif 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.//调用自己实现的Handler,用于处理Request,并且返回ResponseserverHandler{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() {  //过了keep alive时间// 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{})  //重置ReadDeadline}
}

serve用于循环读取用户的请求数据Request,并封装成Response,然后调用调用自己传入的Handler,如果未指定Handler则使用默认的DefaultServeMux。

serverHandler{c.server}.ServeHTTP(w, w.req)func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {handler := sh.srv.Handlerif handler == nil {  //如果没有实现handler则用DefaultServeMuxhandler = DefaultServeMux}if req.RequestURI == "*" && req.Method == "OPTIONS" {handler = globalOptionsHandler{}}handler.ServeHTTP(rw, req)
}

调用handler的ServeHTTP来处理客户端的请求,并响应。

我们看一下默认的Handler DefaultServeMux的ServeHTTP

func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {/*处理RequestURI为*的情况*/if r.RequestURI == "*" {if r.ProtoAtLeast(1, 1) {w.Header().Set("Connection", "close")}w.WriteHeader(StatusBadRequest)return}//根据请求找到匹配的handlerh, _ := mux.Handler(r)h.ServeHTTP(w, r)
}

看到Handler函数根据用户请求去匹配对应的Handler,如以上demo,根据pattern     /hello,可以找到注册到路由的handler即HelloServer,由HelloServer处理请求,并写入response。

Golang http之server源码详解相关推荐

  1. Golang http之transport源码详解

    使用golang net/http库发送http请求,最后都是调用 transport的 RoundTrip方法中. type RoundTripper interface {RoundTrip(*R ...

  2. udhcp源码详解(五) 之DHCP包--options字段

    中间有很长一段时间没有更新udhcp源码详解的博客,主要是源码里的函数太多,不知道要不要一个一个讲下去,要知道讲DHCP的实现理论的话一篇博文也就可以大致的讲完,但实现的源码却要关心很多的问题,比如说 ...

  3. Redis从精通到入门——数据类型Zset实现源码详解

    Redis数据类型之Zset详解 Zset简介 Zset常用操作 应用场景 Zset实现 源码阅读 Zset-ziplist实现 图解Zset-ziplist Zset-字典(dict) + 跳表(z ...

  4. OpenstackSDK 源码详解

    OpenstackSDK 源码详解 openstacksdk是基于当前最新版openstacksdk-0.17.2版本,可从 GitHub:OpenstackSDK 获取到最新的源码.openstac ...

  5. webbench1.5源码详解

    webbench1.5源码详解 前言        Webbench是Linux下的一个网站压力测试工具,它是由Lionbridge公司(http://www.lionbridge.com)开发.   ...

  6. 源码详解Android 9.0(P) 系统启动流程之SystemServer

    源码详解Android 9.0(P) 系统启动流程目录: 源码详解Android 9.0(P)系统启动流程之init进程(第一阶段) 源码详解Android 9.0(P)系统启动流程之init进程(第 ...

  7. OkHttp3源码详解

    前言:为什么有些人宁愿吃生活的苦也不愿吃学习的苦,大概是因为懒惰吧,学习的苦是需要自己主动去吃的,而生活的苦,你躺着不动它就会来找你了. 一.概述 OKHttp是一个非常优秀的网络请求框架,已经被谷歌 ...

  8. 【Live555】live555源码详解(九):ServerMediaSession、ServerMediaSubsession、live555MediaServer

    [Live555]live555源码详解系列笔记 继承协作关系图 下面红色表示本博客将要介绍的三个类所在的位置: ServerMediaSession.ServerMediaSubsession.Dy ...

  9. 【Live555】live555源码详解系列笔记

    [Live555]liveMedia下载.配置.编译.安装.基本概念 [Live555]live555源码详解(一):BasicUsageEnvironment.UsageEnvironment [L ...

最新文章

  1. L1-016 查验身份证(2016年天梯赛模拟赛第8题)
  2. 利用circpedia 数据库探究circRNA的可变剪切
  3. Indicator Weather 13.06 发布 增加 Kelvin 支持
  4. XSLT 与 Java集成常见技术关键点
  5. Thymeleaf语法规则
  6. c语言怎么往栈中输入元素,C语言栈操作
  7. 概率论-3.3 多维随机变量函数的分布
  8. 查看python版本和安装路径
  9. pandas 判断是否等于nan_Python之pandas笔记
  10. apachemod_wsgidjango部署多个项目
  11. android 默认焦点设置_Android界面设计基础:控件焦点4个步骤
  12. android手机慢,Android手机运行慢?!教你一秒“提速”50%
  13. S3C2440时钟电源管理
  14. 操作系统调度算法理解
  15. 【python初级】os.listdir返回目录中包含的文件以及文件夹的列表
  16. win7系统调整屏幕刷新率方法
  17. 基于服务器部署的OCR在线识别应用
  18. 《论韩愈 》——陈寅恪
  19. DMA RDMA 技术详解
  20. 鹏城实验室麒麟V10飞腾2000+体验

热门文章

  1. php和python对比-PHP与Python对比 如何选择?
  2. 为什么女生读博(或直博)的比例越来越高?
  3. Linux内核实时监控键盘输入
  4. 从项目交接看项目文档管理
  5. 盲图像超分辨率重建 ( CVPR,2022) (Pytorch)(附代码)
  6. 机械臂-运动轨迹(简单整理)
  7. lisp+等高线点线矛盾检查_1∶2000数字线划图质量检查方法研究
  8. Java+Kafka消息队列
  9. Use Case框图
  10. golang中的图像image处理之马赛克效果