面试官:net/http库知道吗?能说说优缺点吗?

小白debug 2023-01-11 08:00

前言

哈喽,大家后,我是asong;这几天看了一下Go语言标准库net/http的源码,所以就来分享一下我的学习心得;为什么会突然想看http标准库呢?因为在面试的时候面试官问我你知道Go语言的net/http库吗?他有什么有缺点吗?因为我没有看过这部分源码,所以一首凉凉送给我;

废话不多说,接下来请跟着我的脚步我们一起探索net/http

本文代码基于:Go1.19.3

net/http库的一个小demo

服务端:

import (
 "fmt"
 "net/http"
)

func getProfile(w http.ResponseWriter, r *http.Request) {
 fmt.Fprintln(w, "asong")
}

func main() {
 http.HandleFunc("/profile", getProfile)
 err := http.ListenAndServe(":8080"nil)
 if err != nil {
  fmt.Printf("http server failed, err: %v\n", err)
  return
 }
}

本地启动一个server端监听8080端口,并且提供路由/profile获取个人信息;

客户端:

import (
 "fmt"
 "io/ioutil"
 "net/http"
)

func main() {
 resp, err := http.DefaultClient.Get("http://127.0.0.1:8080/profile")
 if err != nil {
  fmt.Printf("get failed, err:%v\n", err)
  return
 }
 defer resp.Body.Close()
 body, err := ioutil.ReadAll(resp.Body)
 if err != nil {
  fmt.Printf("read from resp.body failed, err:%v\n", err)
  return
 }
 fmt.Println(string(body))
}

通过这样一个简单的例子,我们可以知道客户端我们主要使用http.Client{},服务端我们主要使用http.ListenAndServehttp.HandleFunc,所以我们就可以从这两个包入手来分别看一看客户端和服务端的代码具体是怎么封装;

客户端实现

客户端级别最高的抽象是net/http.Client{},具体结构如下:

type Client struct {
    Transport RoundTripper
    CheckRedirect func(req *Request, via []*Request) error
    Jar CookieJar
    Timeout time.Duration
}
  • Transport:其类型是RoundTripper,RoundTrip代表一个本地事务,RoundTripper接口的实现主要有三个:Transport、http2Transport、fileTransport,其目的是支持更好的扩展性;
  • CheckRedirect:用来做重定向
  • Jar:其类型是CookieJar,用来做cookie管理,CookieJar接口的实现Jar结构体在源码包net/http/cookiejar/jar.go

客户端可以直接通过net/http.DefaultClient发起HTTP请求,也可以自己构建新的net/http.Client实现自定义的HTTP事务,多数情况下我们使用默认的客户端发出的请求就可以满足需求;

我们画一个UML图,如下所示:

image-20221204160448320

了解HTTP客户端的基本结构,我们接下来就开始分析客户端的基本实现;

构建Request

net/http包的Request结构体封装好了HTTP请求所需的必要信息:

type Request struct {
 Method string
 URL *url.URL
 Proto      string // "HTTP/1.0"
 ProtoMajor int    // 1
 ProtoMinor int    // 0
 Header Header
 Body io.ReadCloser
 GetBody func() (io.ReadCloser, error)
 ContentLength int64
 removed as necessary when sending and
 TransferEncoding []string
 Close bool
 Host string
 Form url.Values
 PostForm url.Values
 MultipartForm *multipart.Form
 Trailer Header
 RemoteAddr string
 RequestURI string
 TLS *tls.ConnectionState
 Cancel <-chan struct{}
 Response *Response
 ctx context.Context
}

其中包含了HTTP请求的方法、URL、协议版本、协议头以及请求体等字段,还包括了指向响应的引用:Response;其提供了NewRequest()、NewRequestWithContext()两个方法用来构建请求,这个方法可以校验HTTP请求的字段并根据输入的参数拼装成新的请求结构体,NewRequest()方法内部也是调用的NewRequestWithContext,区别就是是否使用context来做goroutine上下文传递;接下来我们看一下NewRequestWithContext方法的具体实现:

func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) {
    // 默认使用GET方法
 if method == "" {
  method = "GET"
 }
    // 校验方法是否有效,也就是是否是GET、POST、PUT等
 if !validMethod(method) {
  return nil, fmt.Errorf("net/http: invalid method %q", method)
 }
    // ctx必须要传递,NewRequest方法调用时会传递context.Background()
 if ctx == nil {
  return nil, errors.New("net/http: nil Context")
 }
    // 解析URL,解析Scheme、Host、Path等信息
 u, err := urlpkg.Parse(url)
 if err != nil {
  return nil, err
 }
    // body在下面会根据其类型包装成io.ReadCloser类型
 rc, ok := body.(io.ReadCloser)
 if !ok && body != nil {
  rc = io.NopCloser(body)
 }
 // The host's colon:port should be normalized. See Issue 14836.
 u.Host = removeEmptyPort(u.Host)
 req := &Request{
  ctx:        ctx,
  Method:     method,
  URL:        u,
  Proto:      "HTTP/1.1",
  ProtoMajor: 1,
  ProtoMinor: 1,
  Header:     make(Header),
  Body:       rc,
  Host:       u.Host,
 }
 if body != nil {
  switch v := body.(type) {
  case *bytes.Buffer:
   req.ContentLength = int64(v.Len())
   buf := v.Bytes()
   req.GetBody = func() (io.ReadCloser, error) {
    r := bytes.NewReader(buf)
    return io.NopCloser(r), nil
   }
  case *bytes.Reader:
   req.ContentLength = int64(v.Len())
   snapshot := *v
   req.GetBody = func() (io.ReadCloser, error) {
    r := snapshot
    return io.NopCloser(&r), nil
   }
  case *strings.Reader:
   req.ContentLength = int64(v.Len())
   snapshot := *v
   req.GetBody = func() (io.ReadCloser, error) {
    r := snapshot
    return io.NopCloser(&r), nil
   }
  default:
  if req.GetBody != nil && req.ContentLength == 0 {
   req.Body = NoBody
   req.GetBody = func() (io.ReadCloser, error) { return NoBody, nil }
  }
 }

 return req, nil
}

整个构建请求过程中只有处理body的时候稍有一些复杂,我们需要根据body的类型使用不同的方法将其包装成io.ReadCloser类型;

启动事务

构建HTTP请求后,接下来我们需要开启HTTP事务进行请求并且等待远程响应,我们以net/http.Client.Do()方法为例子,我们看一下它的调用链路:

  • net/http.Client.Do()
  • net/http.Client.do()
  • net/http.Client.send()
  • net/http.Send()
  • net/http.Transport.RoundTrip()

RoundTrip()是RoundTripper类型中的一个的方法,net/http.Transport是其中的一个实现,在net/http/transport.go文件中我们可以找到这个方法:

// roundTrip implements a RoundTripper over HTTP.
func (t *Transport) roundTrip(req *Request) (*Response, error) {
 t.nextProtoOnce.Do(t.onceSetNextProtoDefaults)
 ctx := req.Context()
 trace := httptrace.ContextClientTrace(ctx)
    // 省略前置检查部分
    .....
 for {
        // 用来检测ctx退出信号
  select {
  case <-ctx.Done():
   req.closeBody()
   return nil, ctx.Err()
  default:
  }

  // 获取连接,这块就是我们要看的重点,Go语言通过连接池对资源进行了复用;
  pconn, err := t.getConn(treq, cm)
  if err != nil {
   t.setReqCanceler(cancelKey, nil)
   req.closeBody()
   return nil, err
  }

  var resp *Response
  if pconn.alt != nil {
   // HTTP/2 path.
   t.setReqCanceler(cancelKey, nil// not cancelable with CancelRequest
   resp, err = pconn.alt.RoundTrip(req)
  } else {
            // 开始处理响应
   resp, err = pconn.roundTrip(treq)
  }
  if err == nil {
   resp.Request = origReq
   return resp, nil
  }

  // Rewind the body if we're able to.
  req, err = rewindBody(req)
  if err != nil {
   return nil, err
  }
 }
}

代码一大堆,我们只要重点看两部分即可:

  • net/http.Transport.getConn()获取连接
  • net/http.persistConn.roundTrip()处理写入HTTP请求并在select中等待响应的返回;

获取连接

func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (pc *persistConn, err error) {
 req := treq.Request
 trace := treq.trace
 ctx := req.Context()
 if trace != nil && trace.GetConn != nil {
  trace.GetConn(cm.addr())
 }

 w := &wantConn{
  cm:         cm,
  key:        cm.key(),
  ctx:        ctx,
  ready:      make(chan struct{}, 1),
  beforeDial: testHookPrePendingDial,
  afterDial:  testHookPostPendingDial,
 }
 defer func() {
  if err != nil {
   w.cancel(t, err)
  }
 }()

 // 在队列中有闲置连接,直接返回
 if delivered := t.queueForIdleConn(w); delivered {
  pc := w.pc
  return pc, nil
 }

 cancelc := make(chan error, 1)
 t.setReqCanceler(treq.cancelKey, func(err error) { cancelc <- err })

 // 放到队列中等待建立新的连接
 t.queueForDial(w)

 // 阻塞等待连接
 select {
 case <-w.ready:
  return w.pc, w.err
 case <-req.Cancel:
  return nil, errRequestCanceledConn
 case <-req.Context().Done():
  return nil, req.Context().Err()
 case err := <-cancelc:
  if err == errRequestCanceled {
   err = errRequestCanceledConn
  }
  return nil, err
 }
}

因为连接的建议会消耗比较多的时间,带来较大的开下,所以Go语言使用了连接池对资源进行分配和复用,先调用net/http.Transport.queueForIdleConn()获取等待闲置的连接,如果没有获取到在调用net/http.Transport.queueForDial在队列中等待建立新的连接,通过select监听连接是否建立完毕,超时未获取到连接会上剖错误,我们继续在queueForDial追踪TCP连接的建立:

func (t *Transport) queueForDial(w *wantConn) {
 w.beforeDial()
 if t.MaxConnsPerHost <= 0 {
  go t.dialConnFor(w)
  return
 }

 t.connsPerHostMu.Lock()
 defer t.connsPerHostMu.Unlock()

 if n := t.connsPerHost[w.key]; n < t.MaxConnsPerHost {
  if t.connsPerHost == nil {
   t.connsPerHost = make(map[connectMethodKey]int)
  }
  t.connsPerHost[w.key] = n + 1
  go t.dialConnFor(w)
  return
 }
    ....
}

我们会启动一个goroutine做tcp的建连,最终调用dialConn方法,在这个方法内做持久化连接,调用net库的dial方法进行TCP连接:

func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (pconn *persistConn, err error) {
 pconn = &persistConn{
  t:             t,
  cacheKey:      cm.key(),
  reqch:         make(chan requestAndChan, 1),
  writech:       make(chan writeRequest, 1),
  closech:       make(chan struct{}),
  writeErrCh:    make(chan error, 1),
  writeLoopDone: make(chan struct{}),
 }

 conn, err := t.dial(ctx, "tcp", cm.addr())
 if err != nil {
  return nil, err
 }
 pconn.conn = conn

 pconn.br = bufio.NewReaderSize(pconn, t.readBufferSize())
 pconn.bw = bufio.NewWriterSize(persistConnWriter{pconn}, t.writeBufferSize())

 go pconn.readLoop()
 go pconn.writeLoop()
 return pconn, nil
}

在连接建立后,代码中我们我们还看到分别启动了两个goroutine,readLoop用于从tcp连接中读取数据,writeLoop用于从tcp连接中写入数据;

我们看一下writeLoop方法:

func (pc *persistConn) writeLoop() {
 defer close(pc.writeLoopDone)
 for {
  select {
  case wr := <-pc.writech:
   startBytesWritten := pc.nwrite
   err := wr.req.Request.write(pc.bw, pc.isProxy, wr.req.extra, pc.waitForContinue(wr.continueCh))
   if bre, ok := err.(requestBodyReadError); ok {
    err = bre.error
    wr.req.setError(err)
   }
   if err == nil {
    err = pc.bw.Flush()
   }
   if err != nil {
    if pc.nwrite == startBytesWritten {
     err = nothingWrittenError{err}
    }
   }
   pc.writeErrCh <- err // to the body reader, which might recycle us
   wr.ch <- err         // to the roundTrip function
   if err != nil {
    pc.close(err)
    return
   }
  case <-pc.closech:
   return
  }
 }
}

监听writech通道,所以的数据发送都是在这个循环中写入的;

net/http.Transport{}中提供了连接池配置参数,开发者可以自行定义:

type Transport struct {
 ......
    MaxIdleConns int
    MaxIdleConnsPerHost int
    MaxConnsPerHost int
    IdleConnTimeout time.Duration
    ......
}

处理HTTP请求

net/http.persistConn.roundTrip()会处理HTTP请求,我们看其具体实现:

func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) {
    ......
 startBytesWritten := pc.nwrite
 writeErrCh := make(chan error, 1)
    //writeLoop循环写入
 pc.writech <- writeRequest{req, writeErrCh, continueCh}
    
    // 等待响应数据
    resc := make(chan responseAndError)
    // readLoop循环等待响应
 pc.reqch <- requestAndChan{
  req:        req.Request,
  cancelKey:  req.cancelKey,
  ch:         resc,
  addedGzip:  requestedGzip,
  continueCh: continueCh,
  callerGone: gone,
 }
    
 for {
  select {
  case err := <-writeErrCh:
  case <-pcClosed:
   pcClosed = nil
   if canceled || pc.t.replaceReqCanceler(req.cancelKey, nil) {
    if debugRoundTrip {
     req.logf("closech recv: %T %#v", pc.closed, pc.closed)
    }
    return nil, pc.mapRoundTripError(req, startBytesWritten, pc.closed)
   }
  case <-respHeaderTimer:
   if debugRoundTrip {
    req.logf("timeout waiting for response headers.")
   }
   pc.close(errTimeout)
   return nil, errTimeout
  case re := <-resc:
   if (re.res == nil) == (re.err == nil) {
    panic(fmt.Sprintf("internal error: exactly one of res or err should be set; nil=%v", re.res == nil))
   }
   if debugRoundTrip {
    req.logf("resc recv: %p, %T/%#v", re.res, re.err, re.err)
   }
   if re.err != nil {
    return nil, pc.mapRoundTripError(req, startBytesWritten, re.err)
   }
   return re.res, nil
  case <-cancelChan:
   canceled = pc.t.cancelRequest(req.cancelKey, errRequestCanceled)
   cancelChan = nil
  case <-ctxDoneChan:
   canceled = pc.t.cancelRequest(req.cancelKey, req.Context().Err())
   cancelChan = nil
   ctxDoneChan = nil
  }
 }
}

我们重点关注这两个通道:

  • pc.writech :其类型是chan writeRequest ,writeLoop协程会循环写入数据,net/http.Request.write会根据net/http.Request结构中的字段按照HTTP协议组成TCP数据段,TCP协议栈会负责将HTTP请求中的内容发送到目标服务器上;
  • pc.reqch:其类型是chan requestAndChan,readLoop协程会循环读取响应数据并且调用net/http.ReadResponse进行协议解析,其中包含状态码、协议版本、请求头等内容;

小结

我们简单总结一下net/http库中HTTP客户端的实现:

  • net/http.Client是级别最高的抽象,其中transport用于开启HTTP事务,jar用于处理cookie;
  • net/http.Transport中主要逻辑两部分:
    • 从连接池中获取持久化连接
    • 使用持久化连接处理HTTP请求

net/http库中默认有一个DefaultClient可以直接使用,DefaultClient有对应DefaultTransport,可以满足我们大多数场景,如果需要使用自己管理HTTP客户端的头域、重定向等策略,那么可以自定义Client,如果需要管理代理、TLS配置、连接池、压缩等设置,可以自定义Transport;

因为HTTP协议的版本是不断变化的,所以为了可扩展性,transport是一个接口类型,具体的是实现是Transporthttp2TransportfileTransport,这样实现扩展性变高,值得我们学习;

HTTP在建立连接时会耗费大量的资源,需要开辟一个goroutine去创建TCP连接,连接建立后会在创建两个goroutine用于HTTP请求的写入和响应的解析,然后使用channel进行通信,所以要合理利用连接池,避免大量的TCP连接的建立可以优化性能;

服务端

我们可以用net/http库快速搭建HTTP服务,HTTP服务端主要包含两部分:

  • 注册处理器:net/http.HandleFunc函数用于注册处理器
  • 监听端口:net/http.ListenAndServe用于处理请求

注册处理器

直接调用net/http.HandleFunc可以注册路由和处理函数:

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
 DefaultServeMux.HandleFunc(pattern, handler)
}

我们可以看到处理函数是一个统一的格式:

handler func(ResponseWriter, *Request)

默认调用HTTP服务起的DefaultServeMux处理请求,DefaultServeMux本质是ServeMux

type ServeMux struct {
 mu    sync.RWMutex  // 读写锁,保证并发安全,注册处理器时会加写锁做保护
 m     map[string]muxEntry // 路由规则,一个string对应一个mux实体,这里的string就是注册的路由表达式
 es    []muxEntry // slice of entries sorted from longest to shortest.
 hosts bool       // whether any patterns contain hostnames
}
  • mu:需要加读写锁保证并发安全,注册处理器时会加写锁保证写map的数据正确性,这个map就是pattern和handler;
  • m:存储路由规则,key就是pattern,value是muEntry实体,muEntry实体中包含:pattern和handler
  • es:存储的也是muxEntry实体,因为我们使用map存储路由和handler的对应关系,所以只能索引静态路由,并不支持[path_param],所以这块的作用是当在map中没有找到匹配的路由时,会遍历这个切片进行前缀匹配,这个切片按照路由长度进行排序;
  • hosts:这个也是用来应对特殊case,如果我们注册的路由没有以/开始,那么就认为我们注册的路由包含host,所以路由匹配时需要加上host;

我看看一下路由注册函数:

func (mux *ServeMux) Handle(pattern string, handler Handler) {
    // 加锁,保证并发安全
 mux.mu.Lock()
 defer mux.mu.Unlock()

 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 {
  mux.m = make(map[string]muxEntry)
 }
 e := muxEntry{h: handler, pattern: pattern}
    // map存储路由和处理函数的映射
 mux.m[pattern] = e
    // 如果路由最后加了`/`放入到切片后在路由匹配时做前缀匹配
 if pattern[len(pattern)-1] == '/' {
  mux.es = appendSorted(mux.es, e)
 }
 // 如果路由第一位不是/,则认为注册的路由加上了host,所以在路由匹配时使用host+path进行匹配;
 if pattern[0] != '/' {
  mux.hosts = true
 }
}

监听端口

net/http库提供了ListenAndServe()用来监听TCP连接并处理请求:

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

在这里初始化Server结构,然后调用ListenAndServe

func (srv *Server) ListenAndServe() error {
 if srv.shuttingDown() {
  return ErrServerClosed
 }
 addr := srv.Addr
 if addr == "" {
  addr = ":http"
 }
    // 调用net进行tcp连接
 ln, err := net.Listen("tcp", addr)
 if err != nil {
  return err
 }
 return srv.Serve(ln)
}

我们调用net网络库进行tcp连接,这里包含了创建socket、bind绑定socket与地址,listen端口的操作,最后调用Serve方法循环等待客户端的请求:

func (srv *Server) Serve(l net.Listener) error {

 origListener := l
 l = &onceCloseListener{Listener: l}
 defer l.Close()

 if err := srv.setupHTTP2_Serve(); err != nil {
  return err
 }

 if !srv.trackListener(&l, true) {
  return ErrServerClosed
 }
 defer srv.trackListener(&l, false)

 baseCtx := context.Background()
 if srv.BaseContext != nil {
  baseCtx = srv.BaseContext(origListener)
  if baseCtx == nil {
   panic("BaseContext returned a nil context")
  }
 }

 var tempDelay time.Duration // how long to sleep on accept failure

 ctx := context.WithValue(baseCtx, ServerContextKey, srv)
 for {
        // 接收客户端请求
  rw, err := l.Accept()
  if err != nil {
   select {
   case <-srv.getDoneChan():
    return ErrServerClosed
   default:
   }
            // 网络错误进行延时等待
   if ne, ok := err.(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", err, tempDelay)
    time.Sleep(tempDelay)
    continue
   }
   return err
  }
  connCtx := ctx
  if cc := srv.ConnContext; cc != nil {
   connCtx = cc(connCtx, rw)
   if connCtx == nil {
    panic("ConnContext returned nil")
   }
  }
  tempDelay = 0
        // 创建一个新的连接
  c := srv.newConn(rw)
  c.setState(c.rwc, StateNew, runHooks) // before Serve can return
        // 读取起一个goroutine处理客户端请求
  go c.serve(connCtx)
 }
}

从上述代码我们可以到每个HTTP请求服务端都会单独创建一个goroutine来处理请求,我们一下处理过程:

// 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())
 var inFlightResponse *response
 defer func() {
        // 添加recover函数防止panic引发主程序挂掉;
  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)
  }
 }()


 // 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 {
        // 读取请求,从连接中获取HTTP请求并构建一个实现了`net/http.Conn.ResponseWriter`接口的变量`net/http.response`
  w, err := c.readRequest(ctx)
  if c.r.remain != c.server.initialReadLimitSize() {
   c.setState(c.rwc, StateActive, runHooks)
  }
  if err != nil {
  }
  // 处理请求
  serverHandler{c.server}.ServeHTTP(w, w.req)
 }
}

我们继续跟踪ServeHTTP方法,ServeMux是一个HTTP请求的多路复用器,在这里可以根据请求的URL匹配合适的处理器,我们看代码:

func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
 if r.RequestURI == "*" {
  if r.ProtoAtLeast(11) {
   w.Header().Set("Connection""close")
  }
  w.WriteHeader(StatusBadRequest)
  return
 }
    // 进行路由匹配,获取注册的处理函数
 h, _ := mux.Handler(r)
    // 这块就是执行我们注册的handler,也就是例子中的getProfile()
 h.ServeHTTP(w, r)
}

mux.Handler()中我们就看到了路由匹配的代码:

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
}
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
 // 先从map中查找
 v, ok := mux.m[path]
 if ok {
        // 找打了返回注册的函数
  return v.h, v.pattern
 }

 // 从切片中进行前缀匹配
 for _, e := range mux.es {
  if strings.HasPrefix(path, e.pattern) {
   return e.h, e.pattern
  }
 }
 return nil""
}

小结

服务端的代码看主逻辑主要是看两部分,一个是注册处理器,标准库使用map进行存储,本质是一个静态索引,同时维护了一个切片,用来做前缀匹配,只要以/结尾的,都会在切片中存储;服务端监听端口本质也是使用net网络库进行TCP连接,然后监听对应的TCP连接,每一个HTTP请求都会开一个goroutine去处理请求,所以如果有海量请求,会在一瞬间创建大量的goroutine,这个可能是一个性能瓶颈点,所以小伙伴要注意下这块的性能问题;

总结

net/HTTP的总体代码行数是比较多的,我们只需要看主要逻辑是怎么实现的就可以了,别人问你原理能打出来个所以然就行,不必要扣细节,当出现问题或者想具体了解某部分协议的时候在细看源码对应部分即可;

像我们过了一遍源码后我们就知道当前net/http的一些优缺点,比如优点是HTTP客户端使用了连接池,避免频繁建立带来的大开销,缺点是HTTP服务端的路由只是一个静态索引匹配,对于动态路由匹配支持的不好,并且每一个请求都会创建一个gouroutine进行处理,海量请求到来时需要考虑这块的性能瓶颈;

net/http标准库可以让我们很快的就实现一个HTTP服务器,并且也有很多我们值得借鉴学习的地方,所以源码的学习还是很必要的;

好啦,今天的文章就到这里了,我是asong,我们下期见~


评论
  •  万万没想到!科幻电影中的人形机器人,正在一步步走进我们人类的日常生活中来了。1月17日,乐聚将第100台全尺寸人形机器人交付北汽越野车,再次吹响了人形机器人疯狂进厂打工的号角。无独有尔,银河通用机器人作为一家成立不到两年时间的创业公司,在短短一年多时间内推出革命性的第一代产品Galbot G1,这是一款轮式、双臂、身体可折叠的人形机器人,得到了美团战投、经纬创投、IDG资本等众多投资方的认可。作为一家成立仅仅只有两年多时间的企业,智元机器人也把机器人从梦想带进了现实。2024年8月1
    刘旷 2025-01-21 11:15 616浏览
  • 临近春节,各方社交及应酬也变得多起来了,甚至一月份就排满了各式约见。有的是关系好的专业朋友的周末“恳谈会”,基本是关于2025年经济预判的话题,以及如何稳定工作等话题;但更多的预约是来自几个客户老板及副总裁们的见面,他们为今年的经济预判与企业发展焦虑而来。在聊天过程中,我发现今年的聊天有个很有意思的“点”,挺多人尤其关心我到底是怎么成长成现在的多领域风格的,还能掌握一些经济趋势的分析能力,到底学过哪些专业、在企业管过哪些具体事情?单单就这个一个月内,我就重复了数次“为什么”,再辅以我上次写的:《
    牛言喵语 2025-01-22 17:10 150浏览
  • 嘿,咱来聊聊RISC-V MCU技术哈。 这RISC-V MCU技术呢,简单来说就是基于一个叫RISC-V的指令集架构做出的微控制器技术。RISC-V这个啊,2010年的时候,是加州大学伯克利分校的研究团队弄出来的,目的就是想搞个新的、开放的指令集架构,能跟上现代计算的需要。到了2015年,专门成立了个RISC-V基金会,让这个架构更标准,也更好地推广开了。这几年啊,这个RISC-V的生态系统发展得可快了,好多公司和机构都加入了RISC-V International,还推出了不少RISC-V
    丙丁先生 2025-01-21 12:10 418浏览
  • 现在为止,我们已经完成了Purple Pi OH主板的串口调试和部分配件的连接,接下来,让我们趁热打铁,完成剩余配件的连接!注:配件连接前请断开主板所有供电,避免敏感电路损坏!1.1 耳机接口主板有一路OTMP 标准四节耳机座J6,具备进行音频输出及录音功能,接入耳机后声音将优先从耳机输出,如下图所示:1.21.2 相机接口MIPI CSI 接口如上图所示,支持OV5648 和OV8858 摄像头模组。接入摄像头模组后,使用系统相机软件打开相机拍照和录像,如下图所示:1.3 以太网接口主板有一路
    Industio_触觉智能 2025-01-20 11:04 186浏览
  • 高速先生成员--黄刚这不马上就要过年了嘛,高速先生就不打算给大家上难度了,整一篇简单但很实用的文章给大伙瞧瞧好了。相信这个标题一出来,尤其对于PCB设计工程师来说,心就立马凉了半截。他们辛辛苦苦进行PCB的过孔设计,高速先生居然说设计多大的过孔他们不关心!另外估计这时候就跳出很多“挑刺”的粉丝了哈,因为翻看很多以往的文章,高速先生都表达了过孔孔径对高速性能的影响是很大的哦!咋滴,今天居然说孔径不关心了?别,别急哈,听高速先生在这篇文章中娓娓道来。首先还是要对各位设计工程师的设计表示肯定,毕竟像我
    一博科技 2025-01-21 16:17 143浏览
  •     IPC-2581是基于ODB++标准、结合PCB行业特点而指定的PCB加工文件规范。    IPC-2581旨在替代CAM350格式,成为PCB加工行业的新的工业规范。    有一些免费软件,可以查看(不可修改)IPC-2581数据文件。这些软件典型用途是工艺校核。    1. Vu2581        出品:Downstream     
    电子知识打边炉 2025-01-22 11:12 117浏览
  • 故障现象 一辆2007款日产天籁车,搭载VQ23发动机(气缸编号如图1所示,点火顺序为1-2-3-4-5-6),累计行驶里程约为21万km。车主反映,该车起步加速时偶尔抖动,且行驶中加速无力。 图1 VQ23发动机的气缸编号 故障诊断接车后试车,发动机怠速运转平稳,但只要换挡起步,稍微踩下一点加速踏板,就能感觉到车身明显抖动。用故障检测仪检测,发动机控制模块(ECM)无故障代码存储,且无失火数据流。用虹科Pico汽车示波器测量气缸1点火信号(COP点火信号)和曲轴位置传感器信
    虹科Pico汽车示波器 2025-01-23 10:46 58浏览
  • 本文介绍瑞芯微开发板/主板Android配置APK默认开启性能模式方法,开启性能模式后,APK的CPU使用优先级会有所提高。触觉智能RK3562开发板演示,搭载4核A53处理器,主频高达2.0GHz;内置独立1Tops算力NPU,可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示与控制等行业。源码修改修改源码根目录下文件device/rockchip/rk3562/package_performance.xml并添加以下内容,注意"+"号为添加内容,"com.tencent.mm"为AP
    Industio_触觉智能 2025-01-17 14:09 189浏览
  • Ubuntu20.04默认情况下为root账号自动登录,本文介绍如何取消root账号自动登录,改为通过输入账号密码登录,使用触觉智能EVB3568鸿蒙开发板演示,搭载瑞芯微RK3568,四核A55处理器,主频2.0Ghz,1T算力NPU;支持OpenHarmony5.0及Linux、Android等操作系统,接口丰富,开发评估快人一步!添加新账号1、使用adduser命令来添加新用户,用户名以industio为例,系统会提示设置密码以及其他信息,您可以根据需要填写或跳过,命令如下:root@id
    Industio_触觉智能 2025-01-17 14:14 140浏览
  • 2024年是很平淡的一年,能保住饭碗就是万幸了,公司业绩不好,跳槽又不敢跳,还有一个原因就是老板对我们这些员工还是很好的,碍于人情也不能在公司困难时去雪上加霜。在工作其间遇到的大问题没有,小问题还是有不少,这里就举一两个来说一下。第一个就是,先看下下面的这个封装,你能猜出它的引脚间距是多少吗?这种排线座比较常规的是0.6mm间距(即排线是0.3mm间距)的,而这个规格也是我们用得最多的,所以我们按惯性思维来看的话,就会认为这个座子就是0.6mm间距的,这样往往就不会去细看规格书了,所以这次的运气
    wuliangu 2025-01-21 00:15 294浏览
  • 随着消费者对汽车驾乘体验的要求不断攀升,汽车照明系统作为确保道路安全、提升驾驶体验以及实现车辆与环境交互的重要组成,日益受到业界的高度重视。近日,2024 DVN(上海)国际汽车照明研讨会圆满落幕。作为照明与传感创新的全球领导者,艾迈斯欧司朗受邀参与主题演讲,并现场展示了其多项前沿技术。本届研讨会汇聚来自全球各地400余名汽车、照明、光源及Tier 2供应商的专业人士及专家共聚一堂。在研讨会第一环节中,艾迈斯欧司朗系统解决方案工程副总裁 Joachim Reill以深厚的专业素养,主持该环节多位
    艾迈斯欧司朗 2025-01-16 20:51 282浏览
  •  光伏及击穿,都可视之为 复合的逆过程,但是,复合、光伏与击穿,不单是进程的方向相反,偏置状态也不一样,复合的工况,是正偏,光伏是零偏,击穿与漂移则是反偏,光伏的能源是外来的,而击穿消耗的是结区自身和电源的能量,漂移的载流子是 客席载流子,须借外延层才能引入,客席载流子 不受反偏PN结的空乏区阻碍,能漂不能漂,只取决于反偏PN结是否处于外延层的「射程」范围,而穿通的成因,则是因耗尽层的过度扩张,致使跟 端子、外延层或其他空乏区 碰触,当耗尽层融通,耐压 (反向阻断能力) 即告彻底丧失,
    MrCU204 2025-01-17 11:30 209浏览
  • 日前,商务部等部门办公厅印发《手机、平板、智能手表(手环)购新补贴实施方案》明确,个人消费者购买手机、平板、智能手表(手环)3类数码产品(单件销售价格不超过6000元),可享受购新补贴。每人每类可补贴1件,每件补贴比例为减去生产、流通环节及移动运营商所有优惠后最终销售价格的15%,每件最高不超过500元。目前,京东已经做好了承接手机、平板等数码产品国补优惠的落地准备工作,未来随着各省市关于手机、平板等品类的国补开启,京东将第一时间率先上线,满足消费者的换新升级需求。为保障国补的真实有效发放,基于
    华尔街科技眼 2025-01-17 10:44 233浏览
  • 数字隔离芯片是一种实现电气隔离功能的集成电路,在工业自动化、汽车电子、光伏储能与电力通信等领域的电气系统中发挥着至关重要的作用。其不仅可令高、低压系统之间相互独立,提高低压系统的抗干扰能力,同时还可确保高、低压系统之间的安全交互,使系统稳定工作,并避免操作者遭受来自高压系统的电击伤害。典型数字隔离芯片的简化原理图值得一提的是,数字隔离芯片历经多年发展,其应用范围已十分广泛,凡涉及到在高、低压系统之间进行信号传输的场景中基本都需要应用到此种芯片。那么,电气工程师在进行电路设计时到底该如何评估选择一
    华普微HOPERF 2025-01-20 16:50 113浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦