<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>铃仙的个人博客</title><description>Blog</description><link>https://fuwari.vercel.app/</link><language>zh_CN</language><item><title>视频转图片序列</title><link>https://fuwari.vercel.app/posts/%E8%A7%86%E9%A2%91%E8%BD%AC%E5%9B%BE%E7%89%87%E5%BA%8F%E5%88%97/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E8%A7%86%E9%A2%91%E8%BD%AC%E5%9B%BE%E7%89%87%E5%BA%8F%E5%88%97/</guid><description>这篇文章介绍了如何使用FFmpeg将视频拆分成图片序列，并提取音频，适合在网页上播放</description><pubDate>Mon, 18 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;ul&gt;
&lt;li&gt;常常在轻量级场景下使用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;流程:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;AI生成视频
      ↓
导出MP4
      ↓
ffmpeg拆帧
      ↓
438张WebP
      ↓
JSON描述播放规则
      ↓
网页播放
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;FFmpeg 拆帧&lt;/h2&gt;
&lt;p&gt;假设有一个视频：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;video.mp4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;拆成 PNG&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;ffmpeg -i video.mp4 frames/frame_%04d.png
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;frames/
├── frame_0001.png
├── frame_0002.png
├── frame_0003.png
...
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;拆成 WebP（更适合网页）&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;ffmpeg -i video.mp4 frames/frame_%04d.webp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;frames/
├── frame_0001.webp
├── frame_0002.webp
├── frame_0003.webp
...
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;指定帧率&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;例如原视频 60 FPS，但只想导出 30 FPS：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ffmpeg -i video.mp4 -vf fps=30 frames/frame_%04d.webp
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;从第 0 帧开始命名&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;默认是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;frame_0001.webp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你的播放器要求：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;frame_0000.webp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ffmpeg -i video.mp4 -start_number 0 frames/frame_%04d.webp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;得到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;frame_0000.webp
frame_0001.webp
frame_0002.webp
...
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;同时提取音频&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;提取音频：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ffmpeg -i video.mp4 -vn -c:a copy audio.m4a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-vn&lt;/code&gt;：不要视频&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-c:a copy&lt;/code&gt;：直接复制音频流，不重新编码&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;示例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p frames

ffmpeg -i video.mp4 \
-vf fps=30 \
-start_number 0 \
-qscale:v 80 \
frames/frame_%04d.webp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再提取音频：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ffmpeg -i video.mp4 -vn -c:a copy audio.m4a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;资源包结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;naiwa/
├── config.json
├── audio.m4a
└── frames/
    ├── frame_0000.webp
    ├── frame_0001.webp
    ├── ...
    └── frame_0437.webp
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>搭建博客后端过程中的笔记</title><link>https://fuwari.vercel.app/posts/%E6%90%AD%E5%BB%BA%E5%8D%9A%E5%AE%A2%E5%90%8E%E7%AB%AF%E7%AC%94%E8%AE%B0/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E6%90%AD%E5%BB%BA%E5%8D%9A%E5%AE%A2%E5%90%8E%E7%AB%AF%E7%AC%94%E8%AE%B0/</guid><description>这篇文章记录了我在搭建博客后端过程中遇到的一些问题和解决方案，涵盖了数据库配置、路由设计、JWT认证、图片管理等方面的内容</description><pubDate>Fri, 18 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;博客&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;数据库:主从配置，读写分离
搜索：es&lt;/p&gt;
&lt;p&gt;`git commit feat-[content]&lt;/p&gt;
&lt;h2&gt;配置初始化&lt;/h2&gt;
&lt;p&gt;使用yaml进行配置保存&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;system:  
  ip:  
  port: 8080  
  env: dev # 一般用于gin的日志输出  
log:  
  app: blogx_server  
  dir: logs  

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;日志初始化&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;为什么用日志库而不是标准日志&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;功能更强大&lt;/li&gt;
&lt;li&gt;显示更清晰&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;日志格式&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;logs/Date/App&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;连接数据库&lt;/h2&gt;
&lt;p&gt;==读写分离==
插件:&lt;a href=&quot;https://github.com/go-gorm/dbresolver&quot;&gt;https://github.com/go-gorm/dbresolver&lt;/a&gt;
能自动识别读写操作,进行操作分发&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;settings.yaml&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;db:  
  user: root  
  password: root  
  host: 127.0.0.1  
  port: 5432  
  database: gvb_db  
  debug: false  
  source: postgres  
# 数据库配置一样，模拟多个数据库读写分离  
db1:  
  user: root  
  password: root  
  host: 127.0.0.1  
  port: 5432  
  database: gvb_db  
  debug: false  
  source: postgres
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;⚠️如果连接不上远程数据库&lt;/strong&gt;
查看🛜网络的代理设置，看是否设置了sock代理，可能连接被代理拦截了，增加白名单即可&lt;/p&gt;
&lt;h2&gt;路由初始化&lt;/h2&gt;
&lt;p&gt;将api从路由里剥离出来，避免耦合&lt;/p&gt;
&lt;h2&gt;根据ip获取地理位置&lt;/h2&gt;
&lt;p&gt;经常用于社交平台
通过ip地址去定位
在网上冲浪都是走公网ip
哪些是内网？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;192.168.0.0
172.16-32
10.。。
127.0.0.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ip2region: &quot;github.com/lionsoul2014/ip2region/binding/golang/xdb&quot;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;离线数据库查询，效率高，精度低
或利用现有网站去发请求&lt;/li&gt;
&lt;li&gt;精准度高，效率低&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;初始化ip2region&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func InitIPDB() {  
    var dbPath = &quot;init/ip2region.xdb&quot;  
    _searcher, err := xdb.NewWithFileOnly(dbPath)  
    if err != nil {  
       logrus.Fatalf(&quot;ip地址数据库加载失败: %s\n&quot;, err)  
       return  
    }  
    //不关闭因为后面还需要用  
    //defer searcher.Close()  
    searcher = _searcher  
}  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过区间判断是否是内网&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func HasLocalIPAddr(ip string) bool {  
    return HasLocalIP(net.ParseIP(ip))  
}  
  
// HasLocalIP 通过ip判断内网  
func HasLocalIP(ip net.IP) bool {  
    if ip.IsLoopback() {  
       return true  
    }  
    ip4 := ip.To4()  
    if ip4 == nil {  
       return false  
    }  
    return ip4[0] == 10 ||  
       (ip4[0] == 172 &amp;amp;&amp;amp; ip4[1] &amp;gt;= 16 &amp;amp;&amp;amp; ip[4] &amp;lt;= 31) ||  
       (ip4[0] == 192 &amp;amp;&amp;amp; ip4[1] == 168) ||  
       (ip4[0] == 169 &amp;amp;&amp;amp; ip4[1] == 254)  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果不是内网，再进行查表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var searcher *xdb.Searcher  
const LOCFOMMAT = 5  
func GetIPLoc(ip string) (location string) {  
    //利用区间先快速判断是否是内网  
    if ipUtils.HasLocalIPAddr(ip) {  
       return &quot;内网&quot;  
    }  
    region, err := searcher.SearchByStr(ip)  
    if err != nil {  
       logrus.Warnf(&quot;错误的ip地址:[%s]&quot;, ip)  
       return &quot;异常地址&quot;  
    }  
    //处理addrList  
    _addrList := strings.Split(region, &quot;|&quot;)  
    if len(_addrList) != LOCFOMMAT {  
       //出现概率目前极低  
       logrus.Warnf(&quot;异常的ip地址:[%s]&quot;, ip)  
       return &quot;未知地址&quot;  
    }  
 //...
 //处理格式
 //...
    return region  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;表结构搭建&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;优先核心表&lt;/li&gt;
&lt;li&gt;表的设计尽量保证后期表结构不变化&lt;/li&gt;
&lt;li&gt;要考虑&lt;strong&gt;冗余字段&lt;/strong&gt;：==字段可以多但是不能少==&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;日志系统&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;登录日志&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;操作日志&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;获取请求体-&amp;gt;可以放在请求中间件里&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;静态路由&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;r.static(),路径映射URL&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;JWT&lt;/h2&gt;
&lt;p&gt;jwt库
&quot;github.com/dgrijalva/jwt-go&quot;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;定义 Claims 结构&lt;/strong&gt;
自定义 Claims，包含用户的基本信息（如 UserID、Username、Role），并组合 jwt.StandardClaims 形成 MyClaims。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;type Claims struct {  
    UserID   uint   `json:&quot;userID&quot;`  
    Username string `json:&quot;username&quot;`  
    Role     uint8  `json:&quot;role&quot;`  
}  
  
type MyClaims struct {  
    Claims  
    jwt.StandardClaims  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;生成token&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// GetToken 转换 token
func GetToken(claims Claims) (string, error) {  
    cla := MyClaims{  
       Claims: claims,  
       StandardClaims: jwt.StandardClaims{  
          ExpiresAt: time.Now().Add(time.Duration(global.Config.Jwt.Expire) * time.Hour).Unix(), // 过期时间  
          Issuer:    global.Config.Jwt.Issuer,                                                   // 签发人  
       },  
    }  
    //设置签名算法  
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, cla)  
    return token.SignedString([]byte(global.Config.Jwt.Secret)) // 进行签名生成对应的token  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;解析 Token&lt;/strong&gt;
通过 ParseToken(tokenString) 解析 JWT：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;校验签名是否合法&lt;/li&gt;
&lt;li&gt;判断是否过期、是否非法或无效&lt;/li&gt;
&lt;li&gt;成功后返回自定义的 MyClaims&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;func ParseToken(tokenString string) (*MyClaims, error) {  
    if tokenString == &quot;&quot; {  
       //如果未登录，直接返回  
       return nil, errors.New(&quot;请登录&quot;)  
    }  
    token, err := jwt.ParseWithClaims(tokenString, &amp;amp;MyClaims{}, func(token *jwt.Token) (interface{}, error) {  
       return []byte(global.Config.Jwt.Secret), nil  
    })  
    if err != nil {  
       //如果出错,判断出错类型  
       if strings.Contains(err.Error(), &quot;token is expired&quot;) {  
          return nil, errors.New(&quot;token过期&quot;)  
       }  
       if strings.Contains(err.Error(), &quot;signature is invalid&quot;) {  
          return nil, errors.New(&quot;token无效&quot;)  
       }  
       if strings.Contains(err.Error(), &quot;token contains an invalid&quot;) {  
          return nil, errors.New(&quot;token非法&quot;)  
       }  
       return nil, err  
    }  
    //断言确定token有效  
    if claims, ok := token.Claims.(*MyClaims); ok &amp;amp;&amp;amp; token.Valid {  
       return claims, nil  
    }  
    return nil, errors.New(&quot;invalid token&quot;)  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;拓展：双token&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单token 过期时间长，安全程度低&lt;/li&gt;
&lt;li&gt;双token包括access_token用来鉴权，refresh_token用来获取新access_token，时间可以设置为较长&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;jwt缺点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;不能主动失效
Redis添加黑名单&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;图片管理&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;图片上传的文件名重复问题&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;直接存hash&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;七牛云&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;TODO&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;用户管理&lt;/h2&gt;
&lt;h3&gt;图片验证码库:&quot;github.com/mojocn/base64Captcha&quot;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;存储器设置&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// 根据自己需求更改验证码存储上限和过期时间  
var result = base64Captcha.NewMemoryStore(10240, 3*time.Minute)  
//或者使用默认配置 10240,10min
var result = base64Captcha.DefaultMemStore
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;生成器配置&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// digitConfig 生成图形化数字验证码配置  
func digitConfig() *base64Captcha.DriverDigit {  
    digitType := &amp;amp;base64Captcha.DriverDigit{  
       Height:   50,  
       Width:    100,  
       Length:   5,  
       MaxSkew:  0.45,  
       DotCount: 80,  
    }  
    return digitType  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;生成&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// CreateCode  
// @Result id 验证码id  
// @Result bse64s 图片base64编码  
// @Result err 错误  
func CreateCode() (string, string, string, error) {  
    var driver base64Captcha.Driver  
    //纯数字验证码  
    driver = digitConfig()  
    if driver == nil {  
       logrus.Errorf(&quot;图形化数字验证码配置失败&quot;)  
    }  
    // 创建验证码并传入创建的类型的配置，以及存储的对象  
    c := base64Captcha.NewCaptcha(driver, result)  
    id, b64s, answer, err := c.Generate()  
    return id, b64s, answer, err  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;发邮件&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import (
    &quot;log&quot;
    &quot;net/smtp&quot;

    &quot;github.com/jordan-wright/email&quot;
)

func main() {
    e := email.NewEmail()
    //设置发送方的邮箱
    e.From = &quot;dj &amp;lt;XXX@qq.com&amp;gt;&quot;
    // 设置接收方的邮箱
    e.To = []string{&quot;XXX@qq.com&quot;}
    //设置抄送如果抄送多人逗号隔开
    e.Cc = []string{&quot;XXX@qq.com&quot;,XXX@qq.com}
    //设置秘密抄送
    e.Bcc = []string{&quot;XXX@qq.com&quot;}
    //设置主题
    e.Subject = &quot;这是主题&quot;
    //设置文件发送的内容
    e.Text = []byte(&quot;www.topgoer.com是个不错的go语言中文文档&quot;)
    //设置服务器相关的配置
    err := e.Send(&quot;smtp.qq.com:25&quot;, smtp.PlainAuth(&quot;&quot;, &quot;你的邮箱账号&quot;, &quot;这块是你的授权码&quot;, &quot;smtp.qq.com&quot;))
    if err != nil {
        log.Fatal(err)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;setting.yaml配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;email:  
  domain: &quot;&quot;  # 邮件服务器 
  port: 0   # 465 开ssl 587 没有开ssl
  sendEmail: &quot;&quot;   
  authCode: &quot;&quot;  
  sendNickname: &quot;&quot;  
  ssl: false  
  tls: false
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;用户密码一定不能明文存储在数据库里&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;QQ登录&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;TODO&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;随便做个页面：有qq登录就行&lt;/li&gt;
&lt;li&gt;审核&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;用户名+密码登录&lt;/h2&gt;
&lt;h2&gt;命令行创建用户&lt;/h2&gt;
&lt;p&gt;包:&quot;golang.org/x/crypto/ssh/terminal&quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;实现了密码的加密显示(&quot; * &quot;)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;terminal.ReadPassword(int(os.Stdin.Fd()))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;邮箱绑定&lt;/h2&gt;
&lt;h2&gt;&lt;a&gt;PostgreSQL主从配置&lt;/a&gt;&lt;/h2&gt;
&lt;h2&gt;es配置&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;🔍 1.&lt;/strong&gt;  &lt;strong&gt;强大的全文搜索能力&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;支持模糊查询、分词、匹配度排序。&lt;/li&gt;
&lt;li&gt;能处理拼写错误、同义词等复杂搜索需求。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;⚡ 2.&lt;/strong&gt;  &lt;strong&gt;高性能&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;数据检索速度快，尤其适合处理大规模数据。&lt;/li&gt;
&lt;li&gt;查询和写入都具备良好的延迟控制&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;docker-compose&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;  es:
    image: &quot;elasticsearch:7.12.0&quot;
    restart: always
    privileged: true
    environment:
      discovery.type: single-node
      ES_JAVA_OPTS: &quot;-Xms512m -Xmx512m&quot;
    volumes:
      - ./es/data:/usr/share/elasticsearch/data
    networks:
      blogx_network:
        ipv4_address: 10.2.0.5
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;修改data目录的权限&lt;a href=&quot;https://www.cnblogs.com/zydev/p/16039565.html#%E4%BF%AE%E6%94%B9data%E7%9B%AE%E5%BD%95%E7%9A%84%E6%9D%83%E9%99%90&quot;&gt;#&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;chmod -R 0777 ./es/data&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;a&gt;es v9笔记&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Mysql同步数据到es的方式&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;同步双写&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;异步双写&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据抽取&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据订阅&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;canal( 选用)&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;PG同步es&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;strong&gt;TODO&lt;/strong&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;防Xss注入&lt;/h2&gt;
&lt;h2&gt;go Markdown 解析&lt;/h2&gt;
&lt;p&gt;md-&amp;gt;html-&amp;gt;text
rune?
&lt;strong&gt;&lt;code&gt;rune&lt;/code&gt;&lt;/strong&gt; 是一个内置的数据类型，用于表示 &lt;strong&gt;Unicode 字符&lt;/strong&gt;（Unicode code point）。它的本质是 &lt;code&gt;int32&lt;/code&gt; 的别名（占 4 个字节），用来处理 UTF-8 编码的字符&lt;/p&gt;
&lt;h2&gt;文章引入缓存&lt;/h2&gt;
&lt;p&gt;避免频繁访问数据库
引入缓存：浏览量，点赞数等&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如:点赞的时候，在缓存里面记录一个key,value，key 就是文章id，value 就是点赞数&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;查询文章的时候，从缓存里面查点赞数，响应的时候，实际点赞数=数据库中点赞数＋缓存中的点赞
数
在每天的0点进行数据同步&lt;/p&gt;
&lt;h2&gt;文章删除&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;管理员可以删任意的文章，如果删的是用户的，应该给用户发一个系统消息&lt;/li&gt;
&lt;li&gt;用户只能删自己发布的文章&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;删文章如果是物理删除，就需要删除对应的关晚记录
文章点赞，文章收藏，文章置顶，文章评论，文章浏览&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;定时任务&lt;/h2&gt;
&lt;p&gt;库:Cron
用于在非高峰期同步缓存数据到数据库&lt;/p&gt;
&lt;h2&gt;全局通知&lt;/h2&gt;
&lt;p&gt;方案:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;管理员创建一条全局通知就给所有用户发一条系统消息(只适用于人少的情况)&lt;/li&gt;
&lt;li&gt;用户在系统消息界面主动查询&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;WebSocket&lt;/h2&gt;
&lt;p&gt;websocket是&lt;a href=&quot;https://so.csdn.net/so/search?q=socket&amp;amp;spm=1001.2101.3001.7020&quot;&gt;socket&lt;/a&gt;连接和http协议的结合体，可以实现网页和服务端的长连接&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;不要在中间件加任何认证,认证通过查请求头去认证&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;go get github.com/gorilla/websocket
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://www.fengfengzhidao.com/article/htkS14sBEG4v2tWkYG4A#websocket&quot;&gt;https://www.fengfengzhidao.com/article/htkS14sBEG4v2tWkYG4A#websocket&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;建立连接的时机？
维护用户在线表&lt;/p&gt;
&lt;p&gt;如果目的方在线，就建立ws连接
否则直接入库&lt;/p&gt;
&lt;h2&gt;文章查询&lt;/h2&gt;
&lt;p&gt;es,降级(db)&lt;/p&gt;
&lt;p&gt;分数-&amp;gt;相关度&lt;/p&gt;
&lt;h2&gt;获取服务器资源占用量&lt;/h2&gt;
&lt;p&gt;包：&lt;code&gt;github.com/shirou/gopsutil&lt;/code&gt;&lt;/p&gt;
</content:encoded></item><item><title>莉莉丝go面试题</title><link>https://fuwari.vercel.app/posts/%E8%8E%89%E8%8E%89%E4%B8%9D-go/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E8%8E%89%E8%8E%89%E4%B8%9D-go/</guid><description>莉莉丝的某次Go语言相关的面试题</description><pubDate>Mon, 30 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;gc的是什么&lt;/h1&gt;
&lt;p&gt;go的gc利用多线程，使用三色标记法同步检测进行内存垃圾回收
主要回收&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;堆上分配的对象&lt;/li&gt;
&lt;li&gt;不再有引用的对象&lt;/li&gt;
&lt;li&gt;临时变量&lt;/li&gt;
&lt;li&gt;闭包捕获的变量&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;堆栈存的是什么&lt;/h1&gt;
&lt;h2&gt;堆&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;动态分配的变量&lt;/li&gt;
&lt;li&gt;闭包中捕获的变量&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;栈&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;函数参数&lt;/li&gt;
&lt;li&gt;临时变量&lt;/li&gt;
&lt;li&gt;函数返回地址&lt;/li&gt;
&lt;li&gt;函数调用帧&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;go编译器通过逃逸分析决定变量的存放位置&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;为什么用redis不用本地缓存&lt;/h1&gt;
&lt;p&gt;因为本地缓存无法共享，无法拓展，无法持久化
而redis高性能，可共享，可拓展，可过期，支持多种持久化方式&lt;/p&gt;
&lt;p&gt;本地缓存性能更高，但是分布式系统对一致性和可拓展性的要求优先级更高&lt;/p&gt;
&lt;h1&gt;redis持久化机制&lt;/h1&gt;
&lt;h2&gt;RDB （快照）&lt;/h2&gt;
&lt;p&gt;每隔一段时间就对数据生成一个快照，以二进制形式存储在磁盘的dump.rdb文件
优点:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;恢复时性能高
缺点:&lt;/li&gt;
&lt;li&gt;数据量大时可能造成卡顿&lt;/li&gt;
&lt;li&gt;两次快照间隔中可能丢失数据&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;AOF （日志追加）&lt;/h2&gt;
&lt;p&gt;AOF记录每一个写命令并追加到文件末尾，恢复时进行操作重放
优点:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;安全性高&lt;/li&gt;
&lt;li&gt;可读性强
缺点&lt;/li&gt;
&lt;li&gt;持久化文件比rdb大（引出 AOF重写）&lt;/li&gt;
&lt;li&gt;重放性能没有rdb高&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;混合持久化&lt;/h2&gt;
&lt;p&gt;结合RDB和AOF，AOF重写时，前半部分使用rdb，后续使用AOF追加&lt;/p&gt;
&lt;h1&gt;数据一致性怎么保证&lt;/h1&gt;
&lt;p&gt;应用场景? 单机数据库，分布式系统,缓存数据库双写&lt;/p&gt;
&lt;h2&gt;事务的ACID&lt;/h2&gt;
&lt;p&gt;redo log -&amp;gt; 一致性
undo log -&amp;gt; 回滚&lt;/p&gt;
&lt;h2&gt;raft算法&lt;/h2&gt;
&lt;p&gt;leader选举,日志复制，安全性&lt;/p&gt;
&lt;h2&gt;redis，mysql双写&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;延迟双删&lt;/li&gt;
&lt;li&gt;先删除数据库，再删除缓存&lt;/li&gt;
&lt;li&gt;binlog监听（canal）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;消息队列实现异步一致性&lt;/p&gt;
&lt;h1&gt;登录实现&lt;/h1&gt;
&lt;h2&gt;session&lt;/h2&gt;
&lt;p&gt;登录，服务器存session，后续浏览器自动携带cookie，服务器收到后去查sessionID&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务器需要存session拓展性差&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;token(jwt)&lt;/h2&gt;
&lt;p&gt;登录，服务器颁发token，后续浏览器自动携带token，服务器中间件解析token&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无法主动过期&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;安全&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;https / 防止 Xss&lt;/li&gt;
&lt;li&gt;OAuth2.0 / OpenID Connect：用于第三方登录（如微信、Google 登录）。&lt;/li&gt;
&lt;li&gt;单点登录（SSO）：多个系统共享登录状态，常用 CAS 或 SAML 协议。&lt;/li&gt;
&lt;li&gt;多因素认证（MFA）：密码 + 短信/验证码/生物识别，提升安全性。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>小林计算机网络笔记</title><link>https://fuwari.vercel.app/posts/%E5%B0%8F%E6%9E%97%E8%AE%A1%E7%BD%91/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E5%B0%8F%E6%9E%97%E8%AE%A1%E7%BD%91/</guid><description>这篇文章记录了我在学习计算机网络时的一些笔记</description><pubDate>Tue, 17 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://www.xiaolincoding.com/network&quot;&gt;链接&lt;/a&gt;
幂等性: 重复请求得到的结果是一样的&lt;/p&gt;
&lt;h1&gt;HTTP&lt;/h1&gt;
&lt;h2&gt;缓存技术&lt;/h2&gt;
&lt;p&gt;HTTP 缓存有两种实现方式，分别是&lt;strong&gt;强制缓存和协商缓存&lt;/strong&gt;。
![[截屏2025-06-17 00.02.02.png|535x449]]
&lt;strong&gt;强制缓存&lt;/strong&gt;
浏览器判断缓存没有过期就直接使用浏览器的本地缓存，主动权在浏览器
&lt;strong&gt;协商缓存&lt;/strong&gt;
与服务端协商后,通过协商结果判断是否使用缓存&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;时间&lt;/li&gt;
&lt;li&gt;ETag&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;注意，协商缓存这两个字段都需要配合强制缓存中 Cache-Control 字段来使用，只有在未能命中强制缓存的时候，才能发起带有协商缓存字段的请求。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;怎么优化HTTP/1.1&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;避免发送HTTP请求
&lt;ul&gt;
&lt;li&gt;缓存&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;减少HTTP请求次数
&lt;ul&gt;
&lt;li&gt;减少重定向次数&lt;/li&gt;
&lt;li&gt;合并请求
&lt;ul&gt;
&lt;li&gt;比如网站小图片利用&lt;code&gt;CSS Image Sprites&lt;/code&gt;技术合并&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;延迟发送请求&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;减小响应数据大小
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;无损压缩 &lt;code&gt;content-encoding&lt;/code&gt;(gzip,brotli)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;有损压缩 &lt;code&gt;Accept: audio/*; q=0.2, audio/basic&lt;/code&gt; q：质量因子&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;HTTP 2.0&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;头部压缩&lt;/li&gt;
&lt;li&gt;二进制帧&lt;/li&gt;
&lt;li&gt;并发传输&lt;/li&gt;
&lt;li&gt;服务器主动推送
&lt;strong&gt;缺点&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;队头堵塞&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;HTTP 3.0&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;美中不足的HTTP2.0&lt;/strong&gt;
队头堵塞造成了很大的性能瓶颈，但它其实是tcp协议本身的缺陷,所以3.0改用udp协议
![[截屏2025-07-18 15.39.39.png|426x396]]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;由于tcp是字节流协议，当有包丢失时，后续的包因为序号对不上，只能卡在缓存里，无法接受，在接收方看来就是堵塞&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;TLS握手延迟&lt;/strong&gt;
每次发起http请求，都要进行tcp三次握手和tls四次握手，而且由于tcp本身的“拥塞控制”特点引起的慢启动特性，产生了延迟
&lt;strong&gt;QUIC协议&lt;/strong&gt;
HTTP3.0通过QUIC协议实现
还有一个特点是:==QUIC实现在用户层面，相比TCP在内核层面，更加容易更新迭代==&lt;/p&gt;
&lt;h1&gt;HTTPS&lt;/h1&gt;
&lt;p&gt;S指&quot;TLS/SSL&quot;加密&lt;/p&gt;
&lt;h2&gt;为什么要引入HTTPS&lt;/h2&gt;
&lt;p&gt;因为http的明文传输导致了极大的安全隐患
为了解决这个问题，首先想到的是对称加密，但是对称加密无法保证密钥本身传输的安全性
于是引入==摘要算法和数字签名==
&lt;strong&gt;摘要算法和数字签名&lt;/strong&gt;
摘要算法就是对传输内容取Hash
数字签名就是用发送者的私钥对内容进行加密，接收方用发送方公开的密钥进行解密
但是这样虽然表面上传输安全，但是如果有怀有恶意的人把自己的公钥发给接收方，再用自己的私钥对伪装的内容加密，这样接收方也无法判断内容的真实性，这主要的问题就是:==如何证明发送方的身份==,于是引入权威机构来为发送方证明，这就是==数字证书==
&lt;strong&gt;数字证书&lt;/strong&gt;
发送方将自己的公钥给权威机构(CA)，CA用自己的私钥对发送方公钥等内容(域名,有效期等)进行签名，加密的结果就是==数字证书==,这样接收方收到内容后可以拿证书用CA的公钥解密证书的签名，如果发现解密结果的hash和计算的的证书的hash一致，就告知接收方发送方的身份合法，由此保障传输安全&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;操作系统（如 Windows、macOS）、浏览器（如 Chrome、Firefox）或 HTTP 客户端（如 cURL）内置了全球受信任的 &lt;strong&gt;根 CA 证书列表&lt;/strong&gt;（包含公钥）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;HTTPS如何建立连接的&lt;/h2&gt;
&lt;p&gt;多了一层TLS/SSL层
TLS四次握手，利用&lt;strong&gt;RSA算法&lt;/strong&gt;或者&lt;strong&gt;ECDHE算法&lt;/strong&gt;交换密钥，之后就是用这个密钥进行加密的HTTP通信&lt;/p&gt;
&lt;p&gt;TLS四次握手:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;客户端发起加密通信请求，并发送一个随机数，同时发送自己使用的TLS协议版本&lt;/li&gt;
&lt;li&gt;确认TLS版本，回发一个服务端生成的随机数,确认算法等密码套件&lt;/li&gt;
&lt;li&gt;客户端先通过CA公钥确认数字证书的合法性，如果合法就用解密的服务器公钥加密报文，并发送一个服务器公钥加密的随机数；今后使用公钥加密的通知；以及密钥交换结束的通知&lt;/li&gt;
&lt;li&gt;服务端收到第三个随机数，通过协商的算法进行加密，计算出本次通信的会话密钥，然后发送今后使用公钥加密的通知；以及密钥交换结束的通知&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;HTTPS如何保障内容的完整性&lt;/h2&gt;
&lt;p&gt;TLS记录协议把内容分成多个数据端，分别进行压缩，同时附上一个MAC值(哈希值)用来保证数据的真实性，再一起通过对称密码进行加密，并添加报头&lt;/p&gt;
&lt;h2&gt;HTTPS是绝对安全的吗&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;中间人攻击&lt;/strong&gt;
例如：伪装基站截获信息转发给中间服务器，建立TLS连接后再和真正服务端建立连接，这样就能偷看数据了&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;但前提是客户端选择接受中间服务器的数字证书,这个证书往往能被识别出是不正确的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;现代浏览器首选**ECDHE + RSA
&lt;code&gt;TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;密钥交换：ECDHE（客户端与服务器协商出会话密钥）&lt;/li&gt;
&lt;li&gt;签名验证：RSA（用于验证服务器证书或 ECDHE 参数）&lt;/li&gt;
&lt;li&gt;安全性： 支持前向保密&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;HTTPS性能优化&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;损耗在哪?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一个环节， TLS 协议握手过程；&lt;/li&gt;
&lt;li&gt;第二个环节，握手后的对称加密报文传输。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;如何优化&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;硬件，软件&lt;/li&gt;
&lt;li&gt;协议优化
&lt;ul&gt;
&lt;li&gt;密钥交换过程选择ECDHE而不是RSA算法(ECDHE第三次抢跑，RSA 密钥交换算法的 TLS 握手过程慢且长)&lt;/li&gt;
&lt;li&gt;TLS升级:RTT合并，密码套件升级(防==降级攻击==)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;证书优化
&lt;ul&gt;
&lt;li&gt;周期请求并缓存&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;会话复用
&lt;ul&gt;
&lt;li&gt;SessionID&lt;/li&gt;
&lt;li&gt;Session Ticket&lt;/li&gt;
&lt;li&gt;==重放攻击== -&amp;gt;&lt;em&gt;对会话密钥设定一个合理的过期时间&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;RPC&lt;/h1&gt;
&lt;p&gt;本质上不算协议，而是一种调用方式
 RPC 有很多种实现方式，&lt;strong&gt;不一定非得基于 TCP 协议&lt;/strong&gt;
早期C/S架构使用RPC而B/S架构使用HTTP，现在区分不明显，RPC常用于公司内部多个微服务直接的通信&lt;/p&gt;
&lt;h1&gt;WebSocket&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;服务器推送方案&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTTP不断轮询&lt;/li&gt;
&lt;li&gt;长轮询
这两种只适用于简单场景，当可能会有大量消息主动推送时，就要考虑websocket了&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;TCP&lt;/h1&gt;
&lt;h2&gt;基本认识&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;如何确定TCP连接&lt;/strong&gt;
TCP四元组可以唯一确定一个连接&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;源地址&lt;/li&gt;
&lt;li&gt;源端口&lt;/li&gt;
&lt;li&gt;目的地址&lt;/li&gt;
&lt;li&gt;目的端口
&lt;strong&gt;TCP连接上限&lt;/strong&gt;
取决于客户端的ip数和端口数
但服务端的最大并发连接数肯定达不到理想值
因为:1.系统限制 2.内存限制&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;TCP和UDP的区别&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;连接&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;TCP面向连接&lt;/li&gt;
&lt;li&gt;UDP无连接&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;服务对象&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;TCP一对一&lt;/li&gt;
&lt;li&gt;UDP一对一或一对多，多对多&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;可靠性&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;TCP可靠&lt;/li&gt;
&lt;li&gt;UDP不可靠&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;分片&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;TCP在传输层分片(如大于MSS)&lt;/li&gt;
&lt;li&gt;UDP在IP层分片(如大于MTU)&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;首部开销&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;TCP最小20字节&lt;/li&gt;
&lt;li&gt;UDP 8字节&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;拥塞控制，流量控制&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;TCP有拥塞控制&lt;/li&gt;
&lt;li&gt;UDP没有&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;传输方式&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;TCP基于字节流&lt;/li&gt;
&lt;li&gt;UDP基于数据报&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;TCP连接建立&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;- 客户端 TCP 连接 TIME_WAIT 状态过多，会导致端口资源耗尽而无法建立新的连接吗？&lt;/em&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;会的，如果客户端每次都用新端口，老端口都处于TIME_WAIT状态，那可会导致端口耗尽，无法发起新连接&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;三次握手&lt;/strong&gt;
==第三次握手是
可以携带数据的，前两次握手是不可以携带数据的==&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果已经建立了连接，但是客户端突然出现故障了怎么办？&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;TCP的保活机制&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;也可实现心跳机制，设置定时器，规定时间内未发起请求，定时器时间一到就释放
&lt;strong&gt;如果已经建立了连接，但是服务端的进程崩溃会发生什么？&lt;/strong&gt;
会交给内核释放资源，但同时也能完成和客户端的四次挥手&lt;/p&gt;
&lt;h2&gt;TCP 特性&lt;/h2&gt;
&lt;h4&gt;重传机制&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;超时重传&lt;/strong&gt;
==时间驱动重传==
超时时间是动态变化的
&lt;strong&gt;快速重传&lt;/strong&gt;
==数据驱动重传
![[截屏2025-07-21 10.35.09.png|484x370]]==
问题在于:重传是重传一个还是重传所有？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SACK:TCP 头部附加上已经传输的数据&lt;/li&gt;
&lt;li&gt;D-SACK:告诉「发送方」有哪些数据被重复接收了&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;流量控制&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;滑动窗口&lt;/strong&gt; swnd rwnd
滑动窗口并非一成不变的，发送窗口和接收窗口存放的字节都是放在操作系统的缓冲区内的，会被操作系统调整&lt;/p&gt;
&lt;p&gt;![[22.jpg.webp]]&lt;/p&gt;
&lt;p&gt;那如果系统缓存和窗口大小同时减小呢？
&lt;em&gt;可能会导致丢包&lt;/em&gt;
为了防止这种情况发生，&lt;strong&gt;TCP 规定是不允许同时减少缓存又收缩窗口的&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果窗口调整ACK丢失呢?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;为了解决这个问题TCP为每个连接都设置了==持续定时器==,只要有一方收到了对方的零窗口通知，就启动定时器，如果定时器超时，就会发送&lt;strong&gt;窗口探测 ( Window probe ) 报文&lt;/strong&gt;,请求对方当前的窗口大小,如果连续3次都是0，可能就会发送&lt;code&gt;RST&lt;/code&gt;关闭连接&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;糊涂窗口综合症&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;简单来说，就是窗口越缩越小，导致每次传输成本过大
![[26.png.webp]]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;解决方法:
接收方：不发送小窗口通知
发送方:   避免发小数据（Nagle算法）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if 有数据要发送 {
  if 可用窗口大小 &amp;gt;= MSS and 可发送的数据 &amp;gt;= MSS {
    立刻发送MSS大小的数据 
  } else {
    if 有未确认的数据 {
      将数据放入缓存等待接收ACK 
  } else {
    立刻发送数据 
  } 
 }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;拥塞控制&lt;/h4&gt;
&lt;p&gt;为什么要有拥塞控制？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;因为流量控制只是对发送方接收方来说的，没有考虑网络状况&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;cwnd:拥塞窗口(swnd=min(cwnd,rwnd))
拥塞就缩小，反正扩大&lt;/p&gt;
&lt;p&gt;怎么知道当前网络是否出现了拥塞呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;看有无超时重传现象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;主要是四个算法:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;拥塞避免&lt;/li&gt;
&lt;li&gt;慢启动&lt;/li&gt;
&lt;li&gt;拥塞发生&lt;/li&gt;
&lt;li&gt;快速恢复&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;慢启动&lt;/strong&gt;：当发送方每收到一个 ACK，拥塞窗口 cwnd 的大小就会加 1&lt;/p&gt;
&lt;p&gt;慢启动的上限叫做慢启动门限ssthresh&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;cwnd&amp;lt;ssthresh:慢启动算法&lt;/li&gt;
&lt;li&gt;cwnd&amp;gt;ssthresh:拥塞避免算法
![[截屏2025-07-21 17.24.56.png]]&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;TCP 半连接队列和全连接队列&lt;/h2&gt;
&lt;p&gt;![[3.jpg.webp|506x506]]&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ss&lt;/code&gt;命令：查看TCP全连接队列的使用情况&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1 -l 显示正在监听（listening）的socket
2 -n 不解析服务名称
3 -t 只显示tcpsocket
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;netstat -s | grep overflowed&lt;/code&gt; 可以查看全连接队列溢出情况
&lt;em&gt;如果持续不断地有连接因为 TCP 全连接队列溢出被丢弃，就应该调大 backlog 以及 somaxconn 参数。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;防范SYN攻击:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;增大全连接队列&lt;/li&gt;
&lt;li&gt;开启 tcp_syncookies&lt;/li&gt;
&lt;li&gt;减少SYN/ACK重传次数&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;如何优化TCP&lt;/h2&gt;
&lt;h4&gt;三次握手的性能提升&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;客户端优化&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;降低超时重传次数上限
&lt;strong&gt;服务端优化&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;增大半连接队列容量&lt;/li&gt;
&lt;li&gt;开启syncookie&lt;/li&gt;
&lt;li&gt;开启TCP_FastOpen功能(绕过三次握手)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;四次挥手的性能提升&lt;/h4&gt;
&lt;p&gt;即使 close() 被调用，&lt;strong&gt;内核还是会接收 ACK、发送 ACK&lt;/strong&gt; ——只是不能再用这个 socket 做读写了&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;主动方&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;降低FIN_WAIT1状态下的重传数&lt;/li&gt;
&lt;li&gt;复用处于TIME_WAIT状态下的连接&lt;/li&gt;
&lt;li&gt;增大TIME_WAIT上限个数
&lt;strong&gt;被动方&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;降低FIN_WAIT状态下的重传数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;存在TIME_WAIT状态的原因&lt;/strong&gt;(默认60s)&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;为什么关闭同个端口的TCP马上重连会显示端口占用的原因&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;为了防止历史连接中的数据影响下一次同个四元组的TCP连接&lt;/li&gt;
&lt;li&gt;保证被动关闭的一方能够正常关闭&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;TCP传输数据的性能提升&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;扩大窗口大小&lt;/li&gt;
&lt;li&gt;扩大发送方/接收方缓冲区&lt;/li&gt;
&lt;li&gt;调整内存范围&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;为什么每次TCP建立连接时，序列号都不一样呢&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;为了避免该四元组受到上一次连接的数据干扰&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;同样设计目的的还有TIME_WAIT状态&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PAW机制&lt;/strong&gt;：防止TCP包中的序列号发生绕回
开启tcp_timestamp的情况下，连接双方维护一个时间戳，每收到一个包就对比看时间戳是否是递增的，不是就丢弃
&lt;strong&gt;什么时候SYN报文会被丢弃&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果同时开启recycle和timestamp，就会开启叫做&lt;code&gt;per-host&lt;/code&gt;的PAW机制，这种机制不对四元组作检查，而是只对ip做检查&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;所以如果客户端通过NAT网关，A，B客户端显示为同一ip，而有一方的时间戳比较小，就会导致其包被认为超时而丢弃&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;有一方的半连接队列或全连接队列满了&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;服务端收到SYN请求后，内核把该连接储存到半连接队列(SYN队列)中，当完成三次握手后，再把连接从半连接队列拿出放到全连接队列中(Accept队列),等待进程取出&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;已建立的TCP，收到SYN会发生什么&lt;/h2&gt;
&lt;p&gt;服务端收到客户端重连发送的SYN(乱序)，会返回正常顺序的ACK，客户端发现序列号不是自己想要的，就会发送RST终止连接&lt;/p&gt;
&lt;p&gt;如何优雅关闭一个TCP连接？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;KILL（❌）&lt;/li&gt;
&lt;li&gt;killcx 通过伪造四元组骗取正确序列号，再伪造正确序列号终止连接(主动)&lt;/li&gt;
&lt;li&gt;tcpkill 监听，伪造正确序列号(被动)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;四次挥手中收到乱序的FIN包怎么处理&lt;/h2&gt;
&lt;p&gt;会加入乱序队列，等到收到正常的数据包再检查乱序队列看是否有可用数据包，有的话才进入TIME_WAIT状态&lt;/p&gt;
</content:encoded></item><item><title>Golang.net/http包学习记录</title><link>https://fuwari.vercel.app/posts/httpnet%E5%8C%85%E7%AC%94%E8%AE%B0/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/httpnet%E5%8C%85%E7%AC%94%E8%AE%B0/</guid><description>这篇文章记录了我在学习Golang的net/http包时的一些笔记和示例代码，涵盖了HTTP请求、响应、重定向、Cookie、代理等方面的内容</description><pubDate>Mon, 07 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;请求该网站，可以将请求的内容以json的形式返回
&lt;a href=&quot;http://httpbin.org/&quot;&gt;http://httpbin.org/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如何打印请求内容？&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 打印请求
dump, _ := httputil.DumpRequest(req, true)
fmt.Println(string(dump))
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Example&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;流程：发送请求——&amp;gt;接收返回内容&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;func get() {
    r, err := http.Get(&quot;http://httpbin.org/get&quot;)
    if err != nil {
       panic(err)
    }
    //记得关闭
    defer r.Body.Close()
    content, err := io.ReadAll(r.Body)
    if err != nil {
       panic(err)
    }
    //回应内容
    fmt.Println(string(content))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;net/http默认只封装了get和post方法，put和delete需要自己根据实现&lt;/p&gt;
&lt;p&gt;实现方法:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;NewRequest&lt;/li&gt;
&lt;li&gt;设置Content-type&lt;/li&gt;
&lt;li&gt;选择Client执行Do方法发送请求&lt;/li&gt;
&lt;li&gt;接收返回内容&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;// put sends an HTTP PUT request to http://httpbin.org/put and prints the response.
// The function demonstrates how to make a PUT request with custom headers.
// It sets the Content-Type header but sends no body in this example.
func put() {
 // Create a new PUT request with nil body
 request, err := http.NewRequest(http.MethodPut, &quot;http://httpbin.org/put&quot;, nil)
 if err != nil {
  panic(err) // Handle error if request creation fails
 }

 // Set the Content-Type header (though we&apos;re not sending any content in this example)
 request.Header.Set(&quot;Content-Type&quot;, &quot;application/x-www-form-urlencoded&quot;)

 // Send the request using the default HTTP client
 r, err := http.DefaultClient.Do(request)
 if err != nil {
  panic(err) // Handle error if request fails
 }
 defer r.Body.Close() // Ensure the response body is closed when we&apos;re done

 // Read the entire response body
 content, err := io.ReadAll(r.Body)
 if err != nil {
  panic(err) // Handle error if reading response fails
 }

 // Print the response content as string
 fmt.Println(string(content))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Request&lt;/h2&gt;
&lt;h3&gt;1. 带查询参数请求&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;func requestByParams() {
    req, err := http.NewRequest(&quot;GET&quot;, &quot;http://httpbin.org/get&quot;, nil)
    if err != nil {
       panic(err)
    }
    //设置url参数
    params := make(url.Values)
    params.Set(&quot;param1&quot;, &quot;value1&quot;)
    params.Set(&quot;param2&quot;, &quot;value2&quot;)
    params.Add(&quot;param1&quot;, &quot;value3&quot;)
    fmt.Println(req.Method, params)
    //编码成param1=value1&amp;amp;param1=value3格式
    fmt.Println(params.Encode())
    req.URL.RawQuery = params.Encode()
    r, err := http.DefaultClient.Do(req)
    if err != nil {
       panic(err)
    }
    printBody(r)
}
//请求内容 ： param1=value1&amp;amp;param1=value3&amp;amp;param2=value2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 定制请求头&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;func requestByHeaders() {
    req, err := http.NewRequest(&quot;GET&quot;, &quot;http://httpbin.org/get&quot;, nil)
    if err != nil {
       panic(err)
    }
    //可以通过修改User-Agent简单绕过反爬
    req.Header.Set(&quot;User-Agent&quot;, &quot;Safari&quot;)
    req.Header.Add(&quot;header1&quot;, &quot;value1&quot;)
    req.Header.Add(&quot;header2&quot;, &quot;value2&quot;)
    r, err := http.DefaultClient.Do(req)
    if err != nil {
       panic(err)
    }
    printBody(r)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Response&lt;/h2&gt;
&lt;h3&gt;状态&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;func getStat(r *http.Response) {
    fmt.Println(r.StatusCode) //状态码  200
    fmt.Println(r.Status)     //状态信息  ok 200
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;头&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;func header(r *http.Response) {
    //Header是一个map，通过get的方式比通过at的方式好处:大小写忽略
    fmt.Println(r.Header.Get(&quot;content-type&quot;))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;编码&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;需要解码的主要场景&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;响应内容使用非UTF-8编码时
&lt;ul&gt;
&lt;li&gt;网页常用编码如 GBK、ISO-8859-1(西欧)、EUC-JP(日文)等&lt;/li&gt;
&lt;li&gt;若不正确解码，中文字符等会显示为乱码&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Content-Type头部指定了字符集
&lt;ul&gt;
&lt;li&gt;如 &lt;code&gt;Content-Type: text/html; charset=gbk&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func encoding(r *http.Response) {
    //可以通过网页的头部猜测网页的编码信息
    //引入包:golang.org/x/net/html
    bufReader := bufio.NewReader(r.Body)
    bytes, _ := bufReader.Peek(1024) //预读,不会移动reader的读取位置
    //提供content-type的原因：Content-Type 提供的上下文（如语言区域）能显著提高准确性，检测优先级高
    res, _, _ := charset.DetermineEncoding(bytes, r.Header.Get(&quot;content-type&quot;))
    fmt.Println(res)
    //不解码结果
    //rawBody, _ := io.ReadAll(bufReader)
    //fmt.Println(string(rawBody))

    // 当编码不是UTF-8时需要解码转换
    //引入包:golang.org/x/text/transform，解码
    bodyReader := transform.NewReader(bufReader, res.NewDecoder())
    content, _ := io.ReadAll(bodyReader)
    fmt.Println(string(content))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Post&lt;/h2&gt;
&lt;h3&gt;提交表单&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;func postForm() {
    //http.Post()
    data := make(url.Values)
    data.Add(&quot;name&quot;, &quot;John Doe&quot;)
    data.Add(&quot;email&quot;, &quot;johndoe@gmail.com&quot;)

    u := &quot;http://httpbin.org/post&quot;
    //相当于http.Post(u,content-type,data)里让content-type=&quot;application/x-www-form-urlencoded&quot;的封装
    r, _ := http.PostForm(u, data)
    defer r.Body.Close()
    content, _ := io.ReadAll(r.Body)

    fmt.Println(string(content))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;提交JSON格式内容&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;func postJson() {
    //http.Post()
    sourceData := struct {
       Name  string `json:&quot;name&quot;`
       Email string `json:&quot;email&quot;`
    }{&quot;John Doe&quot;, &quot;johndoe@gmail.com&quot;}
    data, _ := json.Marshal(sourceData)
    u := &quot;http://httpbin.org/post&quot;
    r, _ := http.Post(u, &quot;application/json&quot;, bytes.NewReader(data))
    defer r.Body.Close()
    content, _ := io.ReadAll(r.Body)

    fmt.Println(string(content))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;提交文件&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;mark style=&quot;background: #FFF3A3A6;&quot;&amp;gt;muiltipart&amp;lt;/mark&amp;gt;实现拼接请求&lt;/li&gt;
&lt;li&gt;multipart会随机创建一个boundary&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;func postFile() {
    //数据缓冲区
    body := &amp;amp;bytes.Buffer{}
    //创建拼接类
    writer := multipart.NewWriter(body)
    //增加字段
    _ = writer.WriteField(&quot;name&quot;, &quot;John Doe&quot;)
    _ = writer.WriteField(&quot;email&quot;, &quot;johndoe@gmail.com&quot;)
    //上传文件 表单名 + 文件名
    uploadFile_1, _ := writer.CreateFormFile(&quot;uploadfile_1&quot;, &quot;test.jpg&quot;)
    file, _ := os.Open(&quot;./request.go&quot;)
    defer file.Close()
    _, err := io.Copy(uploadFile_1, file)
    if err != nil {
       return
    }
    //关闭拼接
    _ = writer.Close()
    fmt.Println(writer.FormDataContentType())
    fmt.Println(body)
    r, _ := http.Post(&quot;http://httpbin.org/post&quot;, writer.FormDataContentType(), body)
    defer r.Body.Close()
    content, _ := io.ReadAll(r.Body)
    fmt.Println(string(content))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Download&lt;/h2&gt;
&lt;p&gt;实现带进度条的下载&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func downloadWithProcess(url, filename string) {
    r, err := http.Get(url)
    if err != nil {
       fmt.Println(err)
       return
    }
    defer func(Body io.ReadCloser) {
       err := Body.Close()
       if err != nil {
          fmt.Println(err)
       }
    }(r.Body)
    out, err := os.Create(filename)
    if err != nil {
       fmt.Println(err)
       return
    }
    defer func(out *os.File) {
       err := out.Close()
       if err != nil {
          fmt.Println(err)
       }
    }(out)
    newReader := &amp;amp;Reader{
       Reader: r.Body,
       Total:  r.ContentLength,
    }
    _, err = io.Copy(out, newReader)
    if err != nil {
       fmt.Println(err)
    }
    fmt.Println(&quot;\nDone&quot;)

}

// Reader 重写Reader实现进度条功能
// 本质是io.copy内部有循环调用Read，而我们重写了Read，才能实现进度条刷新
type Reader struct {
    io.Reader
    Total   int64
    Current int64
}

func (r *Reader) Read(p []byte) (n int, err error) {
    n, err = r.Reader.Read(p)
    r.Current += int64(n)
    // \r实现每次打印都回到行首
    fmt.Printf(&quot;\rDownloading: %.2f of %%100&quot;, float64(r.Current*10000/r.Total)/100)
    return
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Redirect&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;DefaultClient是包内提供的一个已经设置了一些默认属性和Client变量
可以如下根据需求自定义client&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;//默认的检查重定向实现
func defaultCheckRedirect(req *Request, via []*Request) error {
  if len(via) &amp;gt;= 10 {
   return errors.New(&quot;stopped after 10 redirects&quot;)
  }
  return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;设置重定向上限&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;func redirectLimits() {
    client := &amp;amp;http.Client{
       CheckRedirect: func(r *http.Request, via []*http.Request) error {
          if len(via) &amp;gt;= 10 {
             //如果重定向次数过多
             return errors.New(&quot;stopped after 10 redirects&quot;)
          }
          return nil
       },
    }
    //重定向11次，报错
    resp, err := client.Get(&quot;https://httpbin.org/absolute-redirect/11&quot;)
    if err != nil {
       fmt.Println(err)
       return
    }
    defer resp.Body.Close()
    content, _ := io.ReadAll(resp.Body)
    fmt.Println(string(content))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;禁止重定向&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;client := &amp;amp;http.Client{
    CheckRedirect: func(r *http.Request, via []*http.Request) error {
        return http.ErrUseLastResponse
    },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Cookie&lt;/h2&gt;
&lt;p&gt;![cookie示意图](截屏2025-04-19 16.28.32.png)&lt;/p&gt;
&lt;p&gt;cookie 的分类有两种，一种是会话期 cookie，一种是持久性 cookie&lt;/p&gt;
&lt;h3&gt;手动附加Cookie&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 不用jar手动附加cookie
func redirWithCookieManual() {
    client := &amp;amp;http.Client{
       CheckRedirect: func(r *http.Request, via []*http.Request) error {
          //禁止重定向
          return http.ErrUseLastResponse
       },
    }
    firstReq, _ := http.NewRequest(http.MethodGet, &quot;https://httpbin.org/cookies/set?freeform=sada&amp;amp;name=tie&quot;, nil)
    firstRes, _ := client.Do(firstReq)
    defer firstRes.Body.Close()
    //获得服务端返回的cookies
    //手动重定向地址
    secondReq, _ := http.NewRequest(http.MethodGet, &quot;https://httpbin.org/cookies&quot;, nil)
    for _, cookie := range firstRes.Cookies() {
       //将cookie附加到重定向的请求中
       secondReq.AddCookie(cookie)
    }
    secondRes, _ := client.Do(secondReq)
    defer secondRes.Body.Close()
    content, _ := io.ReadAll(secondRes.Body)
    fmt.Println(string(content))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用Jar自动附加Cookie&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 使用Jar附加cookie
func jarCookie() {
    jar, _ := cookiejar.New(nil)
    client := &amp;amp;http.Client{
       Jar: jar,
    }
    r, _ := client.Get(&quot;https://httpbin.org/cookies/set?freeform=sada&amp;amp;name=tie&quot;)
    defer r.Body.Close()
    _, _ = io.Copy(os.Stdout, r.Body)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;标准库只提供了会话期cookie&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;Proxy&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;proxyUrl, _ := url.Parse(&quot;http://127.0.0.1:7897&quot;)  //代理启动的端口
t := http.Transport{
    Proxy: http.ProxyURL(proxyUrl),
}
//代理一般分两种，http代理和shadowsocks的代理,socks5
client := &amp;amp;http.Client{Transport: &amp;amp;t}
r, _ := client.Get(&quot;https://google.com&quot;)
defer r.Body.Close()
_, _ = io.Copy(os.Stdout, r.Body)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>JWT快速入门</title><link>https://fuwari.vercel.app/posts/jwt/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/jwt/</guid><description>这篇文章介绍了JWT（Json Web Token）的基本概念、结构以及使用方法，帮助读者快速了解和使用JWT进行身份验证和信息传递</description><pubDate>Sat, 01 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;什么是jwt&lt;/h3&gt;
&lt;p&gt;Json Web Token，通过数字签名的方式，以json对象为载体，在不同的服务终端之间安全的传输信息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VySWQiOjEyMywidXNlcm5hbWUiOiJhbGljZSJ9.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它由三部分组成：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Header（头部）&lt;/strong&gt;：说明用的加密算法，例如 HS256&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Payload（载荷）&lt;/strong&gt;：存放用户信息，比如 userId, role, exp&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Signature（签名）&lt;/strong&gt;：用密钥签名，确保数据没被篡改
格式就是：
Header.Payload.Signature&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;使用&lt;/h3&gt;
&lt;p&gt;假设你登录成功后，服务器生成一个 JWT：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Header:
{
  &quot;alg&quot;: &quot;HS256&quot;,
  &quot;typ&quot;: &quot;JWT&quot;
}

Payload:
{
  &quot;userId&quot;: 123,
  &quot;username&quot;: &quot;alice&quot;,
  &quot;exp&quot;: 1713207687
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;服务器用密钥生成签名，然后把整个 JWT 返回给前端，前端以后每次请求都带着它：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Authorization: Bearer &amp;lt;你的JWT&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;服务器验证这个 Token，就知道你是谁，而且&lt;strong&gt;不需要查数据库&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;验证签名的过程是：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;用 &lt;strong&gt;同样的密钥&lt;/strong&gt; 和算法（比如 HS256）&lt;/li&gt;
&lt;li&gt;对 Header.Payload 重新生成一遍签名&lt;/li&gt;
&lt;li&gt;看是否和原来的 &lt;code&gt;&amp;lt;Signature&amp;gt;&lt;/code&gt; 一样
如果一样，说明 token 没被篡改
如果不一样，说明 token 被伪造或篡改了&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;注意&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;JWT 存在前端&lt;/strong&gt;（通常放在 localStorage 或 cookie 中）&lt;/li&gt;
&lt;li&gt;不能存敏感信息（JWT 是明文可解码的）&lt;/li&gt;
&lt;li&gt;超时后需重新登录或刷新 token（配合 refresh token）&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item></channel></rss>