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连接就会被认为已经断开,发起方会关闭这个连接,以释放资源并避免资源的无效占用。
- 最大空闲连接
- 连接池中最大的空闲连接总数
- 空闲超时时间
- 空闲连接超过这个时间就会失效
这是一个整体的超时架构。