GoLang实战——微服务网关

1. 网关

1.1. 网关应该具备的基本功能

  • 支持多种协议代理:tcp/http/websocket/grpc
  • 支持多种负载均衡策略:轮询/权重轮询/hash一致性
  • 支持下游服务发现:主动探测/自动服务发现
  • 支持横向扩容:加机器就能解决高并发

1.2. 借助网关处理使得服务高可用、高并发

  • 限流:请求QPS限制
  • 熔断:错误率达阈值则服务熔断
  • 降级:确保核心业务可用
  • 权限认证:请求拦截

2. 网络基础

2.1 OSI七层网络协议

在这里插入图片描述

2.2 经典协议与数据包

2.2.1 TCP的数据包

在这里插入图片描述

2.2.1 TCP的装包过程

在这里插入图片描述

2.3 三次握手与四次挥手

  • 三次握手的最主要目的是保证连接是双工的,可靠更多是通过重传机制来保证的
  • 因为连接是全双工的,双方必须都收到对方的FIN包及确认才可关闭

2.3.1 三次握手

在这里插入图片描述

2.3.2 四次握手

在这里插入图片描述
问题1:为什么TIME-WAIT需要等待2MSL

  • 保证TCP协议的全双工连接能够可靠关闭

  • 保证这次连接的重复数据段从网络中消失

  • MSL:Maximum Segment Lifetime,30秒~60秒

  • 因为如果被关闭方(服务器),未接受到客户端发出了ACK。则会重新发出FIN包,如果客户端关闭过快的话,则服务端会一直处于LAST-ACK而无法关闭。客户端等待2MSL的时间足够服务器重传FIN包,之后客户端接收到FIN包就知道之前的包已经丢失,重新传输一个ACK包即可。

问题2:为什么会出现大量的CLOSE-WAIT

  • 首先close_wait一般出现在被动关闭放
  • 并发请求太多导致
    • 在高并发场景下,服务器的CPU、内存和I/O资源可能变得紧张,导致某些需要处理连接关闭的线程或进程被阻塞或延迟执行。这可能延缓了对CLOSE-WAIT状态连接的处理,使得这些连接积累得更多。
  • 被动关闭方未及时释放端口资源导致

被动关闭方未释放端口资源导致

package main

import (
	"fmt"
	"net"
)

func main() {
	//1、监听端口
	listener, err := net.Listen("tcp", "0.0.0.0:9090")
	if err != nil {
		fmt.Printf("listen fail, err: %v\n", err)
		return
	}
	//2.建立套接字连接
	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Printf("accept fail, err: %v\n", err)
			continue
		}
		//3. 创建处理协程
		go func(conn net.Conn) {
			//defer conn.Close() //思考题:这里不填写会有啥问题?
			for {
				var buf [128]byte
				n, err := conn.Read(buf[:])
				if err != nil {
					fmt.Printf("read from connect failed, err: %v\n", err)
					break
				}
				str := string(buf[:n])
				fmt.Printf("receive from client, data: %v\n", str)
			}
		}(conn)
	}
}


2.3.3 实战抓包

在这里插入图片描述

2.4 TCP流量、拥塞控制

2.4.1 为什么需要流量控制

  • 由于通讯双发,网速和处理速度不同。通讯任意一方发送过快都会导致对方消息处理不过来,所以就需要把数据放到缓冲区中
  • 如果缓冲区满了,发送方还在疯狂发送,那接收方只能把数据包丢弃。因此我们需要控制发送速率。
  • 缓冲区的剩余大小称之为接收窗口,用变量win表示,如果win=0,则发送方停止发送
    在这里插入图片描述
    此时会有疑问了?那发送方怎么知道下一次发送是什么时候

不用担心,发送方会有一个探测包,专门用来获取接收方空余的win的数量

2.4.2 为什么需要拥塞控制

  • 流量控制与拥塞控制是两个概念
    • 拥塞控制是调解网络的负载
    • 流量控制是调解发送方和接收方
  • 接收方网络资源繁忙,因未及时相应ACK导致发送方重传大量数据,这样将会导致网络更加拥堵
  • 拥塞控制是动态调整win大小,而不是依赖缓冲区大小确定窗口大小

2.4.3 慢启动、拥塞避免、快恢复

在这里插入图片描述

2.5 为啥会出现粘包、拆包,如何处理?

在这里插入图片描述

  • 应用程序写入的数据大于套接字缓冲区大小,这将会发生拆包
  • 应用程序写入的数据小于套接字缓冲区大小,网卡将应用程序多次写入的数据一次性发送到网络上,这将会发送粘包。
  • 进行MSS(TCP的最大报文长度)大小的TCP分段,当TCP报文长度-TCP头部长度>MSS的时候将发生拆包
  • 接收方法不及时读取套接字内缓冲区数据,这将发生粘包

2.5.1 如何获取完整应用数据报文

  • 使用带消息头的协议,头部写入包长度,然后再读取包内容
  • 设置定长消息,每次读取定长内容,长度不够时空位补固定字符
  • 设置消息便捷,服务端从网络流中按消息边界分离出消息内容,一般使用‘\n’
  • 更复杂的协议,例如json。protobuf

2.5.2 代码示意

在这里插入图片描述
解包

const Msg_Header = "12345678"

func Encode(bytesBuffer io.Writer, content string) error {
	//msg_header+content_len+content
	//8+4+content_len
	// 写入header
	if err := binary.Write(bytesBuffer, binary.BigEndian, []byte(Msg_Header)); err != nil {
		return err
	}
	// 写入len
	clen := int32(len([]byte(content)))
	if err := binary.Write(bytesBuffer, binary.BigEndian, clen); err != nil {
		return err
	}
	// 写入content
	if err := binary.Write(bytesBuffer, binary.BigEndian, []byte(content)); err != nil {
		return err
	}
	return nil
}

func Decode(bytesBuffer io.Reader) (bodyBuf []byte, err error) {
	MagicBuf := make([]byte, len(Msg_Header))
	if _, err = io.ReadFull(bytesBuffer, MagicBuf); err != nil {
		return nil, err
	}
	if string(MagicBuf) != Msg_Header {
		return nil, errors.New("msg_header error")
	}

	lengthBuf := make([]byte, 4)
	if _, err = io.ReadFull(bytesBuffer, lengthBuf); err != nil {
		return nil, err
	}

	// 通过大端字节序进行解码
	length := binary.BigEndian.Uint32(lengthBuf)
	bodyBuf = make([]byte, length)
	if _, err = io.ReadFull(bytesBuffer, bodyBuf); err != nil {
		return nil, err
	}
	return bodyBuf, err
}

客户端

func main() {
	conn, err := net.Dial("tcp", "localhost:9090")
	defer conn.Close()
	if err != nil {
		fmt.Printf("connect failed, err : %v\n", err.Error())
		return
	}
	unpack.Encode(conn, "hello world 0!!!")
}

服务端

func main() {
	//simple tcp server
	//1.监听端口
	listener, err := net.Listen("tcp", "0.0.0.0:9090")
	if err != nil {
		fmt.Printf("listen fail, err: %v\n", err)
		return
	}

	//2.接收请求
	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Printf("accept fail, err: %v\n", err)
			continue
		}

		//3.创建协程
		go process(conn)
	}
}

func process(conn net.Conn) {
	defer conn.Close()
	for {
		bt, err := unpack.Decode(conn)
		if err != nil {
			fmt.Printf("read from connect failed, err: %v\n", err)
			break
		}
		str := string(bt)
		fmt.Printf("receive from client, data: %v\n", str)
	}
}

2.6 基于golang实现TCP、UDP、Http服务器与客户端

2.6.1 TCP服务器与客户端

在这里插入图片描述

2.6.2.1 tcp_client
package client

import (
	"bufio"
	"fmt"
	"net"
	"os"
	"strings"
)

func main()  {
	doSend()
	fmt.Print("doSend over")
	doSend()
	fmt.Print("doSend over")
	//select {}
}


func doSend() {
	//1、连接服务器
	conn, err := net.Dial("tcp", "localhost:9090")
	defer conn.Close()	//思考题:这里不填写会有啥问题?
	if err != nil {
		fmt.Printf("connect failed, err : %v\n", err.Error())
		return
	}
	//2、读取命令行输入
	inputReader := bufio.NewReader(os.Stdin)
	for {
		// 3、一直读取直到读到\n
		input, err := inputReader.ReadString('\n')
		if err != nil {
			fmt.Printf("read from console failed, err: %v\n", err)
			break
		}
		// 4、读取Q时停止
		trimmedInput := strings.TrimSpace(input)
		if trimmedInput == "Q" {
			break
		}
		// 5、回复服务器信息
		_, err = conn.Write([]byte(trimmedInput))
		if err != nil {
			fmt.Printf("write failed , err : %v\n", err)
			break
		}
	}
}

2.6.2.1 tcp_server
package main

import (
	"fmt"
	"net"
)

func main() {
	//1、监听端口
	listener, err := net.Listen("tcp", "0.0.0.0:9090")
	if err != nil {
		fmt.Printf("listen fail, err: %v\n", err)
		return
	}

	//2.建立套接字连接
	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Printf("accept fail, err: %v\n", err)
			continue
		}

		//3. 创建处理协程
		go process(conn)
	}
}

func process(conn net.Conn) {
	defer conn.Close()	//思考题:这里不填写会有啥问题?
	for {
		var buf [128]byte
		n, err := conn.Read(buf[:])

		if err != nil {
			fmt.Printf("read from connect failed, err: %v\n", err)
			break
		}
		str := string(buf[:n])
		fmt.Printf("receive from client, data: %v\n", str)
	}
}

2.6.2 UDP服务器与客户端

在这里插入图片描述

2.6.2.1 udp_client
func main() {
	//step 1 连接服务器
	conn, err := net.DialUDP("udp", nil, &net.UDPAddr{
		IP:   net.IPv4(127, 0, 0, 1),
		Port: 9090,
	})

	if err != nil {
		fmt.Printf("connect failed, err: %v\n", err)
		return
	}

	for i := 0; i < 100; i++ {
		//step 2 发送数据
		_, err = conn.Write([]byte("hello server!"))
		if err != nil {
			fmt.Printf("send data failed, err : %v\n", err)
			return
		}

		//step 3 接收数据
		result := make([]byte, 1024)
		n, remoteAddr, err := conn.ReadFromUDP(result)
		if err != nil {
			fmt.Printf("receive data failed, err: %v\n", err)
			return
		}
		fmt.Printf("receive from addr: %v  data: %v\n", remoteAddr, string(result[:n]))
	}
}
2.6.2.2 udp_server
func main() {
	//step 1 监听服务器
	listen, err := net.ListenUDP("udp", &net.UDPAddr{
		IP:   net.IPv4(0, 0, 0, 0),
		Port: 9090,
	})
	if err != nil {
		fmt.Printf("listen failed, err:%v\n", err)
		return
	}

	//step 2 循环读取消息内容
	for {
		var data [1024]byte
		n, addr, err := listen.ReadFromUDP(data[:])
		if err != nil {
			fmt.Printf("read failed from addr: %v, err: %v\n", addr, err)
			break
		}

		go func() {
			//todo sth
			//step 3 回复数据
			fmt.Printf("addr: %v data: %v  count: %v\n", addr, string(data[:n]), n)
			_, err = listen.WriteToUDP([]byte("received success!"), addr)
			if err != nil {
				fmt.Printf("write failed, err: %v\n", err)
			}
		}()
	}
}

2.6.3 Http服务器与客户端

在这里插入图片描述

2.6.3.1 http_server
package main

import (
	"log"
	"net/http"
	"time"
)

var (
	Addr = ":1210"
)

func main() {
	// 创建路由器
	mux := http.NewServeMux()
	// 设置路由规则
	mux.HandleFunc("/bye", sayBye)
	// 创建服务器
	server := &http.Server{
		Addr:         Addr,
		WriteTimeout: time.Second * 3,
		Handler:      mux,
	}
	// 监听端口并提供服务
	log.Println("Starting httpserver at "+Addr)
	log.Fatal(server.ListenAndServe())
}

func sayBye(w http.ResponseWriter, r *http.Request) {
	time.Sleep(1 * time.Second)
	w.Write([]byte("bye bye ,this is httpServer"))
}
2.6.3.2 http_client
package main

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

func main() {
	// 创建连接池
	transport := &http.Transport{
		DialContext: (&net.Dialer{
			Timeout:   30 * time.Second, //连接超时
			KeepAlive: 30 * time.Second, //探活时间
		}).DialContext,
		MaxIdleConns:          100,              //最大空闲连接
		IdleConnTimeout:       90 * time.Second, //空闲超时时间
		TLSHandshakeTimeout:   10 * time.Second, //tls握手超时时间
		ExpectContinueTimeout: 1 * time.Second,  //100-continue状态码超时时间
	}
	// 创建客户端
	client := &http.Client{
		Timeout:   time.Second * 30, //请求超时时间
		Transport: transport,
	}
	// 请求数据
	resp, err := client.Get("http://127.0.0.1:1210/bye")
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	// 读取内容
	bds, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(bds))
}

2.7 linux常用和网关的命令

# 查看相关端口的连接状态
netstat -Aaln | grep 9090

3. http服务器源码解读

在这里插入图片描述

函数是一等公民
当称函数为“一等公民”(First-Class Citizen或First-Class Function)时,这意味着函数被当作与其他数据类型同等重要的基本单位来对待,享有与其他数据类型相同的操作权限。具体来说,一个语言如果支持以下几点,就可以认为其将函数视为一等公民:

  • 可存储在变量中:函数可以被赋值给一个变量,就像整数、字符串等其他数据类型一样。
  • 可作为参数传递:函数可以作为另一个函数的参数传递进去,使得程序能够接受函数作为输入,这是高阶函数的基础。
  • 可作为返回值:一个函数执行完毕后,可以返回另一个函数作为结果。
  • 可存于数据结构中:函数可以被放入数组、列表、字典等数据结构中,与其他数据混合存储和操作。
  • 支持匿名函数:语言支持无需名称的函数定义(如lambda表达式),这些函数可以现场创建并立即使用。

这些特性使得函数式编程风格成为可能,允许开发者以更加灵活和强大的方式组织和复用代码。例如,在实现策略模式时,可以轻松地交换不同的函数策略,因为它们都是作为同等地位的对象来处理的。总的来说,函数作为一等公民极大地丰富了编程的表达力,使得代码更加简洁、动态,并促进了诸如函数式编程这样的编程范式的应用。

func main() {
	// 创建路由器
	mux := http.NewServeMux()
	// 设置路由规则
	mux.HandleFunc("/bye", sayBye)
	// 创建服务器
	server := &http.Server{
		Addr:         Addr,
		WriteTimeout: time.Second * 3,
		Handler:      mux,
	}
	// 监听端口并提供服务
	log.Println("Starting httpserver at "+Addr)
	log.Fatal(server.ListenAndServe())
}

func sayBye(w http.ResponseWriter, r *http.Request) {
	time.Sleep(1 * time.Second)
	w.Write([]byte("bye bye ,this is httpServer"))
}

3.1 注册路由

3.1.1 相关的主体

// 路由器的主体
type ServeMux struct {
	// 读写锁
	mu    sync.RWMutex
	// 存路径模式和对应的回调的实体
	m     map[string]muxEntry
	// 存前缀匹配回调的实体
	es    []muxEntry // slice of entries sorted from longest to shortest.
	hosts bool       // whether any patterns contain hostnames
}

// 回调的实体
type muxEntry struct {
	// 回调函数
	h       Handler
	// 模式
	pattern string
}

// 我们设置的回调函数都需要实现这个接口
type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

通过函数类型来实现这个

// 使用了适配器的模式,自己需要你的函数有和我一样的参数即可,名字可以任意取
type HandlerFunc func(http.ResponseWriter, *http.Request)

func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	f(w, r)
}

func HelloHandler(res http.ResponseWriter, req *http.Request) {
	res.Write([]byte("Hello world"))
}

3.1.2 如何注册路由

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}
	mux.m[pattern] = e
	// 该模式是否是前缀匹配
	if pattern[len(pattern)-1] == '/' {
		mux.es = appendSorted(mux.es, e)
	}

	if pattern[0] != '/' {
		mux.hosts = true
	}
}

3.2 开启服务

server.ListenAndServe()

// 创建服务器
server := &http.Server{
	Addr:         Addr,
	WriteTimeout: time.Second * 3,
	// 需要把路由放入其中
	Handler:      mux,
}
// 开启端口并且开始服务
func (srv *Server) ListenAndServe() error {
	// 避免在服务器已经关闭或正在关闭的过程中再次尝试启动监听。
	if srv.shuttingDown() {
		return ErrServerClosed
	}
	addr := srv.Addr
	if addr == "" {
		addr = ":http"
	}
	// 创建一个TCP监听器
	ln, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}
	// 启动服务器并开始在创建的监听器(ln)上接受和处理连接。这个方法会阻塞直到服务器关闭(例如通过调用Server.Close())或者监听出现错误。
	return srv.Serve(ln)
}
func (srv *Server) Serve(l net.Listener) error {
	// 检查是否有测试钩子testHookServerServe,如果有,则调用它,这通常是用于测试目的的。
	if fn := testHookServerServe; fn != nil {
		fn(srv, l) // call hook with unwrapped listener
	}
	// 包装监听器: 为了确保监听器只关闭一次,代码创建了一个onceCloseListener包装器,
	// 它在关闭时会防止重复关闭底层监听器。原始监听器l被替换为这个包装器,并在方法最后确保关闭它。
	origListener := l
	l = &onceCloseListener{Listener: l}
	defer l.Close()
	// 根据监听器返回的连接类型及配置,决定是否启用HTTP/2支持
	if err := srv.setupHTTP2_Serve(); err != nil {
		return err
	}
	// 使用srv.trackListener来管理监听器的状态,确保在服务器关闭时能正确处理监听器。
	if !srv.trackListener(&l, true) {
		return ErrServerClosed
	}
	defer srv.trackListener(&l, false)
	// 如果服务器配置了BaseContext回调函数,会使用它来获取基础上下文。此上下文会作为后续请求处理的基础,并且必须非nil。
	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 {
			if srv.shuttingDown() {
				return ErrServerClosed
			}
			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
		// 如果服务器配置了ConnContext回调函数,会使用它来基于基础上下文创建特定于连接的上下文。
		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来通过调用c.serve(connCtx)来处理这个连接上的请求。
		go c.serve(connCtx)
	}
}

3.3 处理连接

c.serve(connCtx)
这一段的源代码挺长的,但有一句十分重要
serverHandler{c.server}.ServeHTTP(w, w.req)
利用server创建了一个serverHandler

type serverHandler struct {
	srv *Server
}

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
	// 读取的是我们创建Server时候放入的Handler
	handler := sh.srv.Handler
	if handler == nil {
		handler = DefaultServeMux
	}
	if !sh.srv.DisableGeneralOptionsHandler && req.RequestURI == "*" && req.Method == "OPTIONS" {
		handler = globalOptionsHandler{}
	}
	// 这里实际上调用的是ServeMux的ServeHTTP
	handler.ServeHTTP(rw, req)
}
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
	}
	// 这一句其实调用的是mux.handler
	h, _ := mux.Handler(r)
	h.ServeHTTP(w, r)
}

输入的是主机+访问路径。返回的是对应的Handler和匹配的模式。之后就可以根据对应的handler来处理连接请求了
在这里插入图片描述

4. http客户端源码解读

4.1 主要结构体

  • type Client struct{}
    • Transport RoundTripper(连接池)
    • Timeout time.Duration(超时时间)
  • type RoundTripper interface {}
    • RoundTrip(*Request)(*Response,error)请求下游接口

4.2 请求流程

  • func(c *Client)Get(url string)
    • c.Do(req)
    • c.do(req)
    • c.send(req,deadline)
    • send(req,c.transport(),deadline)
    • resp,err = rt.RoundTrip(req)

4.3 Transport(连接池)

4.3.1 结构体

// Transport 结构体代表了执行HTTP请求的传输层。它负责处理连接复用、超时以及网络通信的其他方面。

type Transport struct {
    // idleMu 用于保护 idleConn 和 closeIdle 字段,防止并发访问。
    idleMu sync.Mutex
    // closeIdle 表示用户已请求关闭所有空闲连接。
    closeIdle bool
    // idleConn 是一个映射,按连接方法键(connectMethodKey)存储最近空闲的持久连接列表。
    idleConn map[connectMethodKey][]*persistConn 
    // idleConnWait 是一个映射,关联了当前空闲但可能需要的连接的等待队列和对应的连接方法键。
    idleConnWait map[connectMethodKey]wantConnQueue
    // idleLRU 负责管理空闲连接的最近最少使用(LRU)淘汰,以维持连接池的大小限制。
    idleLRU connLRU
	......
}

// 往往代表的是一个网站的连接地址
type connectMethodKey struct {
	// 代理、协议、地址
	proxy, scheme, addr string
	onlyH1              bool
}

4.3.2 roundTrip的流程

Transport实现了roundTrip这个接口
并在这个接口中调用了获取连接的方法
在这里插入图片描述

此方法展示了 Transport 如何智能地管理连接,

  • 优先复用空闲连接queueForIdleConn
  • 没有的话调用queueForDial尝试创建,若已经达到上线则等待之前的完成放回连接池
  • 并妥善处理请求取消和上下文超时,确保资源的有效利用和请求的正确处理。
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())
	}
	// 初始化一个 wantConn 结构体 w,用于记录请求的上下文、连接方法、就绪通道等信息,并设置了测试钩子函数。
	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
		// 直接返回空闲连接 pc,并根据追踪需求记录连接复用信息。
		if pc.alt == nil && trace != nil && trace.GotConn != nil {
			trace.GotConn(pc.gotIdleConnTrace(pc.idleAt))
		}
		// 设置请求取消器以便跟踪请求取消状态。
		t.setReqCanceler(treq.cancelKey, func(error) {})
		return pc, nil
	}
	// 若未获取到空闲连接,则准备新建连接:
	// 创建一个用于取消请求的通道 cancelc。
	cancelc := make(chan error, 1)
	//设置请求取消器,当请求被取消时通过 cancelc 发送错误信息。
	t.setReqCanceler(treq.cancelKey, func(err error) { cancelc <- err })

	//将 wantConn 加入到等待新建连接的队列中(queueForDial(w)),此时连接可能尚未建立,需等待。
	t.queueForDial(w)

	// 等待连接完成或请求取消:
	select {
	case <-w.ready:
		// w.ready 通道有消息,表示连接准备完成:
		// 检查是否成功获取连接 w.pc,并记录连接获取成功信息(仅HTTP/1)。
		// 处理连接建立过程中的错误 w.err,优先处理请求取消的情况。
		if w.pc != nil && w.pc.alt == nil && trace != nil && trace.GotConn != nil {
			trace.GotConn(httptrace.GotConnInfo{Conn: w.pc.conn, Reused: w.pc.isReused()})
		}
		if w.err != nil {
			select {
			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
			default:
				// return below
			}
		}
		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
	}
}
func (t *Transport) queueForDial(w *wantConn) {
	w.beforeDial()
	// 小于0,代表没有上限,立刻创建新链接
	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
	}
	// 否则加入等待队列
	if t.connsPerHostWait == nil {
		t.connsPerHostWait = make(map[connectMethodKey]wantConnQueue)
	}
	q := t.connsPerHostWait[w.key]
	q.cleanFront()
	q.pushBack(w)
	t.connsPerHostWait[w.key] = q
}
func (t *Transport) tryPutIdleConn(pconn *persistConn) error {
	...
	// 获取持久链接的key
	key := pconn.cacheKey
	// 查看是否有和该链接相同的key在等待
	if q, ok := t.idleConnWait[key]; ok {
		done := false
		if pconn.alt == nil {
			for q.len() > 0 {
				// 从等待队列中取出一个请求wantRequest
				w := q.popFront()
				// 通过调用tryDeliver并close(w.ready)向case传递信号,表明已经接受到了复用通道
				if w.tryDeliver(pconn, nil) {
					done = true
					break
				}
			}
		} else {
			for q.len() > 0 {
				w := q.popFront()
				w.tryDeliver(pconn, nil)
			}
		}
		if q.len() == 0 {
			delete(t.idleConnWait, key)
		} else {
			t.idleConnWait[key] = q
		}
		if done {
			return nil
		}
	}

	......
}

4.4 http超时控制

在这里插入图片描述

	// 创建连接池
	transport := &http.Transport{
		DialContext: (&net.Dialer{
			Timeout:   30 * time.Second, //连接超时
			KeepAlive: 30 * time.Second, //探活时间
		}).DialContext,
		MaxIdleConns:          100,              //最大空闲连接
		IdleConnTimeout:       90 * time.Second, //空闲超时时间
		TLSHandshakeTimeout:   10 * time.Second, //tls握手超时时间
		ExpectContinueTimeout: 1 * time.Second,  //100-continue状态码超时时间
	}
	// 创建客户端
	client := &http.Client{
		Timeout:   time.Second * 30, //请求超时时间
		Transport: transport,
	}
  • TCP连接超时:是指在尝试建立TCP连接过程中,如果在预定时间内没有完成连接的握手过程,就会发生连接超时。
  • TCP探活时间:是指在TCP连接空闲一段时间后,为了检测连接是否仍然有效,发起方(通常是客户端或服务器端)会向对方发送一个特殊的数据包,称为TCP Keepalive探测报文。这个时间间隔就是TCP探活时间,它是TCP协议栈中可配置的参数之一。
    • 在没有数据传输的情况下,如果超过这个设定的时间没有通信,发起方就会自动发送一个探活包。如果对方是活跃的,它会响应一个ACK报文,这样就确认了连接仍然是有效的。如果多次(通常也是可配置的)发送探活包都没有得到响应,TCP连接就会被认为已经断开,发起方会关闭这个连接,以释放资源并避免资源的无效占用。
  • 最大空闲连接
    • 连接池中最大的空闲连接总数
  • 空闲超时时间
    • 空闲连接超过这个时间就会失效

这是一个整体的超时架构。
在这里插入图片描述

  • 38
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值