《鬼灭之刃》——无限列车篇 & 游郭篇

​ 看完,脑海中只剩下大哥。 ​ ——近乎完美的人。 ​ 强大,自信,善良,勤奋,阳光,谦逊,细心……你可以在大哥身上找到几乎所有、能想到的、为人所能拥有的优秀品质。没有比他更优秀的人了,堪称完美。 ​ 像太阳,永远在发光、发热,让人想靠近,给予人温暖。——是我期望中的形象。 ​ 整篇故事,几乎没有什么真正令人厌恶的人。像大反派,如上弦三,上弦六,似乎都是不错的人。上弦三我不太了解,但见弹幕都称其三哥,想必是不错的。上弦六的兄妹二人,杀人确实是杀得不少,但兄妹的境遇也确实是糟糕,只能说有因有果吧。——为人时未被温柔以待,作鬼时开始大杀特杀。很难对其杀人行为产生反感。 ​ 鬼头头叫啥来着?哦,无惨。无惨出来晒太阳。 ​ 无惨没什么闪光点,也并不了解其背景故事,也没见弹幕怎么去夸赞他,说的最多的不过是 “晒太阳”。想必无惨应该算个大恶人吧。 ​ ​ 与大哥相比,音柱就显得稍有逊色了。我一直没理解为什么叫音柱,为什么不叫华丽柱?但是“华丽”这个形象,也不够深入人心。帅是真的帅,但毕竟都是动漫人物,大家基本都很帅,华丽柱不那么能拉开差距了。 ​ 音柱自己也说了,他比不上炼狱(这才记起大哥的名字)。 ​ 就故事来讲,主角肯定是要成长的。如果音柱比炼狱还强,那杀上弦鬼的事哪儿有主角的份?在炼狱那儿奋发图强,在音柱这儿着手杀上弦鬼,这才符合主角的成长历程。所以,音柱实力不如炼狱,是必然的,是符合剧情走向的。 ​ 善逸也很帅!特别是那个声音,睡着时候说话的语气,清晰的思路,主导战场……真是帅极了主角的风采都被抢了不少。这样看来,也就猪猪没有大显身手了,希望在之后的剧情里面,能够好好表现一番。猪猪给人的印象就是“猪突猛进”,其实也不错 ​ 最后是铁头娃主角,不多评价。同样是非常完美的人,只是相比起来,我更欣赏如太阳一般的大哥。 ​ 大哥,一路走好。

七月 11, 2022  |  28 字  |  总阅读

【Go】UDP 报文转发

【背景】 如 Wireguard 等工作在三层的 VPN,不会主动转发 UDP 广播报文,但能转发指定 IP 的 UDP 报文。 现有一程序,仅在 PC 主网卡(192.168.1.106)上发送从 14001 端口到 14001 端口的 UDP 广播报文。这种报文一是不会使用虚拟网卡(10.0.0.3)发送,二是就算使用虚拟网卡发送,该种虚拟网卡(Wireguard)也无法发送 UDP 广播报文。 想将报文捕获,使用虚拟网卡(10.0.0.3)指定目的 IP (10.0.0.2)进行转发。 【语言】Go 开发 主要用到 Google 的三个库: github.com/google/gopacket github.com/google/gopacket/layers github.com/google/gopacket/pcap 进行捕获和解析报文。其中,捕获报文在 Windows 上使用 ncap(Wireshark 底层也使用这个),在 Linux 上使用 libpcap(tcpdump 底层使用这个)。 代码不多,如下: package main import ( "context" "errors" "flag" "fmt" "github.com/google/gopacket" "github.com/google/gopacket/layers" "github.com/google/gopacket/pcap" "log" "net" "os" "os/signal" "strings" ) var packetChan chan *Packet type UDP struct { SrcIP net.IP DstIP net.IP SrcPort int DstPort int Content []byte } func (u *UDP) Send() error { laddr := &net.UDPAddr{IP: u.SrcIP, Port: u.SrcPort} raddr := &net.UDPAddr{IP: u.DstIP, Port: u.DstPort} if conn, err := net.DialUDP("udp", laddr, raddr); err != nil { return err } else { defer conn.Close() if _, err := conn.Write(u.Content); err != nil { return err } else { return nil } } } type Packet struct { Ethernet layers.Ethernet IP4 layers.IPv4 TCP layers.TCP UDP layers.UDP Payload gopacket.Payload Decoded []gopacket.LayerType } // NewPacket 解析报文 func NewPacket(packetData []byte) (*Packet, error) { var p Packet parser := gopacket.NewDecodingLayerParser(layers.LayerTypeEthernet, &p.Ethernet, &p.IP4, &p.TCP, &p.UDP, &p.Payload) if err := parser.DecodeLayers(packetData, &p.Decoded); err != nil { return nil, err } else { return &p, nil } } // String 显示报文信息 // 用于打印。 func (p *Packet) String() string { var info []string for _, layerType := range p.Decoded { switch layerType { case layers.LayerTypeEthernet: info = append(info, p.Ethernet.SrcMAC.String()+" > "+p.Ethernet.DstMAC.String(), "Ethernet Type: "+p.Ethernet.EthernetType.String(), ) case layers.LayerTypeIPv4: info = append(info, p.IP4.SrcIP.String()+" > "+p.IP4.DstIP.String(), "Protocol: "+p.IP4.Protocol.String(), ) case layers.LayerTypeTCP: info = append(info, p.TCP.SrcPort.String()+" > "+p.TCP.DstPort.String(), ) case layers.LayerTypeUDP: info = append(info, p.UDP.SrcPort.String()+" > "+p.UDP.DstPort.String(), ) case gopacket.LayerTypePayload: info = append(info, "Content: "+string(p.Payload.LayerContents()), ) } } return strings.Join(info, " | ") } // findDevByIp 通过设备 IP 查找设备 // 在 Linux 中,设备名很容易获取,如 “eth0”; 但在 Windows 中则较难, 因而有此函数。 func findDevByIp(ip net.IP) (*pcap.Interface, error) { devices, err := pcap.FindAllDevs() if err != nil { return nil, err } for _, device := range devices { for _, address := range device.Addresses { if address.IP.Equal(ip) { return &device, nil } } } return nil, errors.New("find device failed by ip") } // capture 捕获报文,解析并放入 chan func capture(dev *pcap.Interface, filter string) { if h, err := pcap.OpenLive(dev.Name, 4096, true, -1); err != nil { log.Panicln(err) } else if err := h.SetBPFFilter(filter); err != nil { log.Panicln(err) } else { defer h.Close() for { if packetData, _, err := h.ReadPacketData(); err != nil { log.Panicln(err) } else if p, err := NewPacket(packetData); err != nil { log.Panicln(err) } else { fmt.Println(p.String()) packetChan <- p } } } } // redirect 转发报文 // 指定源 IP 和目的 IP,端口不变。 func redirect(ctx context.Context, srcIP, dstIP net.IP) { for { select { case <-ctx.Done(): return case p := <-packetChan: u := UDP{ SrcIP: srcIP, DstIP: dstIP, SrcPort: int(p.UDP.SrcPort), DstPort: int(p.UDP.DstPort), Content: p.Payload.LayerContents(), } if err := u.Send(); err != nil { log.Printf("send p failed: %+v", u) } } } } func Run(ctx context.Context, devIp, filter, srcIp, dstIp string) { dev, err := findDevByIp(net.ParseIP(devIp)) if err != nil { log.Panicf("find device by ip failed: ip=%s, err=%s", devIp, err) } packetChan = make(chan *Packet, 64) go capture(dev, filter) redirect(ctx, net.ParseIP(srcIp), net.ParseIP(dstIp)) } func main() { var ( devIP = flag.String("d", "192.168.1.106", "Device IP.") filter = flag.String("f", "port 14001", "BPF filter expression.") srcIP = flag.String("src-ip", "10.0.1.3", "Redirect src IP.") dstIP = flag.String("dst-ip", "10.0.1.2", "Redirect dst IP.") ) flag.Parse() ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) defer stop() Run(ctx, *devIP, *filter, *srcIP, *dstIP) } 编译后使用命令如下: ...

七月 10, 2022  |  649 字  |  总阅读

【Go】利用 signal + context 实现优雅退出

package main import ( "context" "fmt" "os" "os/signal" "sync" "time" ) var wg sync.WaitGroup func worker(name string, ctx context.Context, t time.Duration) { fmt.Println(name, ": enter worker") select { case <-ctx.Done(): fmt.Println(name, ": worker context cancel, exit") case <-time.After(t): fmt.Println(name, ": worker process complete, exit") } wg.Done() } func main() { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) defer stop() wg.Add(2) go worker("worker1", ctx, time.Second*2) go worker("worker2", ctx, time.Second*4) wg.Wait() fmt.Println("exit main") }

七月 10, 2022  |  70 字  |  总阅读

【React】使用 Nginx 部署 React + Router 项目

【React】使用 Nginx 部署 React + Router 项目 项目编译 & 打包 npm run build 编译好的静态文件会在 build 目录下。 React 项目 Nginx 配置 server { server_name web.vksir.zone; location / { root /var/www/ns-web; index index.html; } } React + Router 项目 Nginx 配置 如果使用了 react-router,则 Nginx 配置会多出一项: server { server_name web.vksir.zone; location / { root /var/www/ns-web; index index.html; try_files $uri /index.html; } } 配置代理 如果开发时使用了 http-proxy-middleware,打包成静态文件后,这个包将会失去作用,需要使用 Nginx 代替其提供代理功能。 如果 src/setupProxy.js 文件内容如下: const { createProxyMiddleware } = require('http-proxy-middleware'); module.exports = function(app) { app.use('/ns_server', createProxyMiddleware({ target: 'http://server.vksir.zone', changeOrigin: true, pathRewrite: { '^/ns_server': '' } })); }; 那么 Nginx 配置应当如下: ...

六月 5, 2022  |  300 字  |  总阅读

【Go】利用 reflect 实现结构体设置默认值

【Go】利用 reflect 实现结构体设置默认值 写 API 时经常会需要结构体中某个参数拥有默认值。但如 Gin 只有 ShouldBindQuery 这种 form 类型支持设置默认值,常用的 ShouldBindJSON 这种 json 类型却不支持,很奇怪。 Gin 中 bind 结构体设置默认值 package main import ( "fmt" "github.com/gin-gonic/gin" ) type FormData struct { Name string `form:"name,default=vksir"` Age int `form:"age,default=18"` } type JsonData struct { Name string `json:"name,default=vksir"` Age int `json:"age,default=18"` } func main() { e := gin.Default() e.GET("/", func(c *gin.Context) { var fd FormData c.ShouldBindQuery(&fd) var jd JsonData c.ShouldBindJSON(&jd) fmt.Printf("FormData: %+v\n", fd) fmt.Printf("JsonData:%+v\n", jd) }) e.Run() } 请求会打印如下结果: FormData: {Name:vksir Age:18} JsonData:{Name: Age:0} 也就是说 JsonData 的默认值没有生效,如果看源码也可以发现 ShouldBindJSON 是没有设置默认值的动作的。 简单实现结构体设置默认值 编辑 structutil/struct.go: package structutil import ( "reflect" "strconv" ) func SetDefault(v any) error { typeOf := reflect.TypeOf(v).Elem() valueOf := reflect.ValueOf(v).Elem() return subSetDefault(typeOf, valueOf) } func subSetDefault(typeOf reflect.Type, valueOf reflect.Value) error { for i := 0; i < typeOf.NumField(); i++ { tField := typeOf.Field(i) vField := valueOf.Field(i) switch tField.Type.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: defaultVal, ok := tField.Tag.Lookup("default") if !ok { continue } defaultValInt, err := strconv.ParseInt(defaultVal, 10, 64) if err != nil { return err } vField.SetInt(defaultValInt) case reflect.String: defaultVal, ok := tField.Tag.Lookup("default") if !ok { continue } vField.SetString(defaultVal) case reflect.Struct: err := subSetDefault(tField.Type, vField) if err != nil { return err } } } return nil } 简单使用: ...

六月 4, 2022  |  263 字  |  总阅读

【Go】实现分级日志

话不多说直接上代码: package log import ( "errors" "fmt" "log" "os" ) const ( LevelDebug = (1 + iota) * 10 LevelInfo LevelWaring LevelError LevelCritical ) var debugLogger *log.Logger var debugFileLogger *log.Logger var infoLogger *log.Logger var infoFileLogger *log.Logger var warningLogger *log.Logger var warningFileLogger *log.Logger var errorLogger *log.Logger var errorFileLogger *log.Logger var criticalLogger *log.Logger var criticalFileLogger *log.Logger var flag = log.Ldate | log.Ltime | log.Lshortfile | log.Lmsgprefix var logLevel = LevelInfo func SetLevel(level int) error { if level != LevelDebug && level != LevelInfo && level != LevelWaring && level != LevelError && level != LevelCritical { return errors.New(fmt.Sprintf("invalid level: %d", level)) } else { logLevel = level return nil } } func AddFileOutput(filePath string) error { logWriter, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0655) if err != nil { return err } else { debugFileLogger = log.New(logWriter, "[DEBUG] ", flag) infoFileLogger = log.New(logWriter, "[INFO] ", flag) warningFileLogger = log.New(logWriter, "[WARNING] ", flag) errorFileLogger = log.New(logWriter, "[ERROR] ", flag) criticalFileLogger = log.New(logWriter, "[CRITICAL] ", flag) return nil } } func Debug(format string, v ...any) { logWithLevel(debugLogger, LevelDebug, format, v) logWithLevel(debugFileLogger, LevelDebug, format, v) } func Info(format string, v ...any) { logWithLevel(infoLogger, LevelDebug, format, v) logWithLevel(infoFileLogger, LevelDebug, format, v) } func Waring(format string, v ...any) { logWithLevel(warningLogger, LevelDebug, format, v) logWithLevel(warningFileLogger, LevelDebug, format, v) } func Error(format string, v ...any) { logWithLevel(errorLogger, LevelDebug, format, v) logWithLevel(errorFileLogger, LevelDebug, format, v) } func Critical(format string, v ...any) { logWithLevel(criticalLogger, LevelDebug, format, v) logWithLevel(criticalFileLogger, LevelDebug, format, v) } func init() { debugLogger = log.New(os.Stderr, "[DEBUG] ", flag) infoLogger = log.New(os.Stderr, "[INFO] ", flag) warningLogger = log.New(os.Stderr, "[WARNING] ", flag) errorLogger = log.New(os.Stderr, "[ERROR] ", flag) criticalLogger = log.New(os.Stderr, "[CRITICAL] ", flag) } func logWithLevel(logger *log.Logger, level int, format string, v []any) { if logger != nil && logLevel >= level { logger.Printf(format, v...) } } 简单使用如下: ...

五月 30, 2022  |  317 字  |  总阅读

【React】React + Router + MUI 工程搭建

初始化 create-react-app npx create-react-app my-app 安装 MUI 在工程路径下执行: npm install @mui/material @emotion/react @emotion/styled npm install @mui/material @mui/styled-engine-sc styled-components npm install @mui/icons-material 在 public/index.html 中引入: <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" /> <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" /> 引入一个按钮。修改 src/App.js 如下: import './App.css'; import {Button} from "@mui/material"; function App() { return ( <> <Button variant="contained">你好,世界</Button> </> ); } export default App; 修改 src/App.css 如下: body { min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; } 启动 npm start 如此打开 http://localhost:3000 可看到简单页面: 跨域请求 配置跨域 npm install http-proxy-middleware 创建文件 src/setupProxy.js 如下: ...

五月 29, 2022  |  440 字  |  总阅读

【Python】日志 logging

记录 logging 简单用法。 #!/usr/bin/env python # coding=utf-8 import time import logging LOG_PATH = f'./{time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime())}.log' LOG = logging.getLogger(__name__) formatter = logging.Formatter('[%(asctime)s][%(levelname)s][%(filename)s:%(lineno)d][%(threadName)s][%(funcName)s] %(message)s') LOG.setLevel(logging.DEBUG) def init_log(): sh = logging.StreamHandler() sh.setFormatter(formatter) sh.setLevel(logging.DEBUG) fh = logging.FileHandler(LOG_PATH, 'w', encoding='utf-8') fh.setFormatter(formatter) fh.setLevel(logging.DEBUG) LOG.addHandler(sh) LOG.addHandler(fh) init_log() 更多 Formatter 格式配置见 [logging — Python 的日志记录工具 — Python 3.11.1 文档](https://docs.python.org/zh-cn/3/library/logging.html?highlight=logging formatter#logging.LogRecord)。

五月 8, 2022  |  52 字  |  总阅读

【Python】命令行解析器 argparse

记录 argparse 简单用法。 #!/usr/bin/env python # coding=utf-8 import argparse parser = argparse.ArgumentParser() parser.add_argument('-n', '--name', metavar='<NAME>', required=True) parser.add_argument('-a', '--age', metavar='<AGE>', required=True, type=int) args = parser.parse_args() if __name__ == '__main__': print(args) $ ./script.py -n vksir -a 18 Namespace(age=18, name='vksir')

五月 8, 2022  |  37 字  |  总阅读

【Hexo】CI/CD——持续集成&持续部署

这里不讲 CI/CD 的概念,我们想要做到的,就是自动化编译、部署博客,把注意力都集中在写作上。 之前已经讲过 《【Hexo】Git 一键部署》,但那仅仅是一键部署。在部署之前,还需要在本地安装 Nodejs 环境,安装依赖包,然后编译,最后才能开始部署。当然,Nodejs 环境和依赖一台机器上安装一次就够了,以后只需编译就行,但如果需要在多台机器上写作,那么每台机器都要装环境、装依赖…… 也还行,还比较便利,但还不够。 我们可以借助 CI/CD 工具,完成环境搭建、编译、部署等一系列工作,真正做到一键上传博客。 说到这里,我想到了 WordPress。 WordPress 很容易就可以做到一键上传博客,完全不用搞什么 CI/CD。 免费的 CI/CD 工具不少,这里我选择了 Gihub Action。博客文章都是存储在 Github 库中,用 Gihub Action 也比较方便。 如下是工程文件, https://github.com/vksir/blog-posts/blob/master/.github/workflows/main.yml 关于博客系统 WordPress 确实是好,但不太适合我: 文章都存储的 MySQL 数据库中。 数据库很可靠,但是我的服务器并不可靠。服务器是租的,上面的数据我并不是很放心。比起个人服务器,我更愿意把数据存储在 Github 这种地方。 性能消耗大。 WordPress 需要 MySQL、PHP 支撑,光跑这俩个组件就要占用大量内存,1H2G 的服务器有点难吃下。 Hexo 也挺好的,但也有问题: 很简单,也很复杂。 简单指的是很容易就能生成博客页面,复杂是指,如果想要做到 WordPress 那样便利,其实很麻烦,要装一堆插件,配置一堆东西,完了还没有 WordPress 好用。 二次开发也不容易。 博客主题经常就会有变更需求,只能硬写 Html、CSS、JS,着实是有点难为我这个后端开发者。如果能用用 React 之类的框架,那还简单点。 最好就是自己动手写博客,想怎么样就怎么样。框架好说,数据也好说,难的是主题。从零开始写 CSS,简直魔鬼……

五月 8, 2022  |  60 字  |  总阅读