【Go】使用 logrus 日志库

相比起 zap 日志库,logrus 性能会低一些,但在很多场景,并不需要太高性能,而 logrus 库的自定义能力较为强大,可以任意定义日志格式,较为方便。 package main import ( "context" "fmt" "github.com/sirupsen/logrus" "io" "os" "path/filepath" "runtime" "strings" ) const logPath = "./log.txt" var log = New() func New() *Logger { return &Logger{logger: logrus.New()} } type Logger struct { ctx context.Context logger *logrus.Logger } func (l *Logger) SetFormatter(formatter logrus.Formatter) { l.logger.SetFormatter(formatter) } func (l *Logger) SetOutput(output io.Writer) { l.logger.SetOutput(output) } func (l *Logger) SetLevel(level logrus.Level) { l.logger.SetLevel(level) } func (l *Logger) WithContext(ctx context.Context) *Logger { return &Logger{ctx: ctx, logger: l.logger} } func (l *Logger) Debug(format string, args ...any) { l.log(logrus.DebugLevel, format, args...) } func (l *Logger) Info(format string, args ...any) { l.log(logrus.InfoLevel, format, args...) } func (l *Logger) Warn(format string, args ...any) { l.log(logrus.WarnLevel, format, args...) } func (l *Logger) Error(format string, args ...any) { l.log(logrus.ErrorLevel, format, args...) } func (l *Logger) Fatal(format string, args ...any) { l.log(logrus.FatalLevel, format, args...) } func (l *Logger) log(level logrus.Level, format string, args ...any) { format = fmt.Sprintf("[%s] %s", GetCallerShort(3), format) l.logger.WithContext(l.ctx).Logf(level, format, args...) } type Formatter struct{} func (f *Formatter) Format(entry *logrus.Entry) ([]byte, error) { pid := os.Getpid() time := entry.Time.Format("2006-01-02T15:04:05.000000") level := strings.ToUpper(entry.Level.String()) traceId := GetTraceIdFromCtx(entry.Context) content := fmt.Sprintf("%s %s %d %s %s\n", time, level, pid, traceId, entry.Message) return []byte(content), nil } // GetCallerShort 获取调用点文件名 + 行号 func GetCallerShort(skip int) string { _, file, line, ok := runtime.Caller(skip + 1) if !ok { return "" } _, file = filepath.Split(file) return fmt.Sprintf("%s:%d", file, line) } // CtxSetTraceId 在 ctx 中设置 trace id func CtxSetTraceId(ctx context.Context) context.Context { return context.WithValue(ctx, "trace_id", "id1") } // GetTraceIdFromCtx 打印日志时,从 ctx 中获取 trace id 打印 func GetTraceIdFromCtx(ctx context.Context) string { if ctx == nil { return "-" } val := ctx.Value("trace_id") if traceId, ok := val.(string); ok { return fmt.Sprintf("trace-%s", traceId) } else { return "-" } } func init() { f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o640) if err != nil { panic(err) } log.SetFormatter(&Formatter{}) log.SetOutput(io.MultiWriter(os.Stdout, f)) log.SetLevel(logrus.DebugLevel) } func main() { ctx := context.Background() ctx = CtxSetTraceId(ctx) log.WithContext(ctx).Info("main failed: %s", "detail") }

三月 13, 2024  |  326 字  |  总阅读

ARP 协议

ARP 格式 struct arphdr { unsigned short int ar_hrd; /* Format of hardware address. */ unsigned short int ar_pro; /* Format of protocol address. */ unsigned char ar_hln; /* Length of hardware address. */ unsigned char ar_pln; /* Length of protocol address. */ unsigned short int ar_op; /* ARP opcode (command). */ }; struct ether_arp { struct arphdr ea_hdr; /* fixed-size header */ uint8_t arp_sha[ETH_ALEN]; /* sender hardware address */ uint8_t arp_spa[4]; /* sender protocol address */ uint8_t arp_tha[ETH_ALEN]; /* target hardware address */ uint8_t arp_tpa[4]; /* target protocol address */ }; Ethernet II 格式 typedef unsigned short __u16; typedef __u16 __bitwise __be16; struct ethhdr { unsigned char h_dest[ETH_ALEN]; /* destination eth addr */ unsigned char h_source[ETH_ALEN]; /* source ether addr */ __be16 h_proto; /* packet type ID field */ } __attribute__((packed)); C 收发 ARP 报文 发 #include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <netinet/in.h> #include <linux/if_ether.h> #include <netinet/if_ether.h> #include <linux/if_packet.h> #include <net/if.h> #include <string.h> #include <net/if_arp.h> #include <arpa/inet.h> #include <unistd.h> int main() { // Create raw socket int socket_fd = socket(AF_PACKET, SOCK_RAW, 0); if (socket_fd < 0) { perror("create socket failed"); exit(1); } // Bind interface and get local mac struct sockaddr_ll local; local.sll_family = AF_PACKET; local.sll_ifindex = (int) if_nametoindex("eth0"); socklen_t local_len = sizeof local; if (bind(socket_fd, (struct sockaddr *) &local, local_len) < 0) { perror("bind dev failed"); exit(1); } getsockname(socket_fd, (struct sockaddr *) &local, &local_len); // Create packet unsigned char buf[256]; struct ethhdr *eh = (struct ethhdr *) buf; struct ether_arp *arp = (struct ether_arp *) (eh + 1); size_t size = (unsigned char *) (struct ether_arp *) (arp + 1) - buf; // Ethernet II header memset(eh->h_dest, 255, ETH_ALEN); memcpy(eh->h_source, local.sll_addr, ETH_ALEN); eh->h_proto = htons(ETH_P_ARP); // Arp header arp->ea_hdr.ar_hrd = htons(ARPHRD_ETHER); arp->ea_hdr.ar_pro = htons(ETH_P_IP); arp->ea_hdr.ar_hln = ETH_ALEN; arp->ea_hdr.ar_pln = 4; arp->ea_hdr.ar_op = htons(ARPOP_REQUEST); // Sender addr memcpy(arp->arp_sha, local.sll_addr, ETH_ALEN); struct in_addr src_ip; inet_aton("172.22.211.129", &src_ip); memcpy(arp->arp_spa, &src_ip.s_addr, 4); // Target addr memset(arp->arp_tha, 255, ETH_ALEN); struct in_addr dst_ip; inet_aton("172.22.208.1", &dst_ip); memcpy(arp->arp_tpa, &dst_ip.s_addr, 4); while (1) { if (send(socket_fd, buf, size, 0) < 0) { perror("send arp failed"); exit(1); } printf("send arp success\n"); sleep(1); } } 存在 char * 和 struct 的相互转化,因结构体个变量地址连续且排列顺序固定,所以可以这么做。——如此编程精巧但易出错,只能说不愧是 C! ...

三月 4, 2023  |  662 字  |  总阅读

【Go】Gin 自定义参数校验和错误信息

Gin 使用 validator 库做数据校验,如下可以实现自定义校验、及自定义校验错误信息。 其中,自定义错误信息无法用于多层嵌套结构体,可能可以通过反射做到,但感觉在性能上很捉急。 代码 package main import ( "errors" "fmt" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "github.com/go-playground/validator/v10" "net/http" "reflect" "strings" ) type Body struct { Name string `json:"name" binding:"oneof=vk vksir" err:"one of vk,vksir"` Age int `json:"age" binding:"BodyAgeValidate" err:"only 18"` } func BodyAgeValidate(f validator.FieldLevel) bool { value := f.Field().Int() if value != 18 { return false } return true } func GetValidateErr(obj any, rawErr error) error { validationErrs, ok := rawErr.(validator.ValidationErrors) if !ok { return rawErr } var errString []string for _, validationErr := range validationErrs { field, ok := reflect.TypeOf(obj).FieldByName(validationErr.Field()) if ok { if e := field.Tag.Get("err"); e != "" { errString = append(errString, fmt.Sprintf("%s: %s", validationErr.Namespace(), e)) continue } } errString = append(errString, validationErr.Error()) } return errors.New(strings.Join(errString, "\n")) } func ping(c *gin.Context) { b := Body{} if err := c.ShouldBindJSON(&b); err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": GetValidateErr(b, err).Error()}) return } c.JSON(http.StatusOK, gin.H{}) } func main() { e := gin.Default() if v, ok := binding.Validator.Engine().(*validator.Validate); ok { v.RegisterValidation("BodyAgeValidate", BodyAgeValidate) } e.GET("/ping", ping) e.Run("127.0.0.1:8080") } 运行 curl --location --request GET 'http://127.0.0.1:8080/ping' \ --header 'Content-Type: application/json' \ --data-raw '{ "name": "v", "age": 19 }' { "detail": "Body.Name: one of vk,vksir\nBody.Age: only 18" } 参考文档: ...

二月 8, 2023  |  203 字  |  总阅读

【Go】使用 zap 日志库

代码 loging\logging.go package logging import ( "go.uber.org/zap" "go.uber.org/zap/zapcore" "os" ) const logPath = "./log.txt" func SugaredLogger() *zap.SugaredLogger { return zap.S() } func init() { encoder := getEncoder() writeSyncer := getWriteSyncer() core := zapcore.NewCore(encoder, writeSyncer, zap.DebugLevel) logger := zap.New(core, zap.AddCaller(), zap.AddStacktrace(zap.ErrorLevel)) zap.ReplaceGlobals(logger) } func getEncoder() zapcore.Encoder { encoderConfig := zap.NewProductionEncoderConfig() encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder return zapcore.NewConsoleEncoder(encoderConfig) } func getWriteSyncer() zapcore.WriteSyncer { f, err := os.OpenFile(logPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { panic(err) } return zapcore.NewMultiWriteSyncer(zapcore.AddSync(f), zapcore.AddSync(os.Stdout)) } 使用 main.go package main import ( "GoCode/logging" ) var log = logging.SugaredLogger() func main() { log.Info("Hello zap") } 2023-02-06T01:26:29.514+0800 INFO GoCode/main.go:11 Hello zap

二月 8, 2023  |  102 字  |  总阅读

【Postgresql】Postgresql 常用命令

创建用户和数据库 # 进入命令行 su -u postgre psql # 创建用户 create user vksir with password 'passwd'; # 创建数据库并赋予用户权限 create database db owner vksir; grant all privileges on datebase db to vksir; # 退出 \q # 使用新用户登录数据库 psql -U vksir -d db 命令行命令 \h:查看SQL命令的解释,比如\h select。 \?:查看psql命令列表。 \l:列出所有数据库。 \c [database_name]:连接其他数据库。 \d:列出当前数据库的所有表格。 \d [table_name]:列出某一张表格的结构。 \du:列出所有用户。 SQL 语句 # 创建新表 create table t_person ( id serial primary key, name text not null, age int, alive bool ); # 插入数据 insert into t_person(name) values ('vksir'); # 选择记录 select * from t_person; # 更新数据 update t_person set age = '18' where name = 'vksir'; # 删除记录 delete from t_person where name = 'vksir'; # 删除表格 drop table t_person; 参考文档: ...

二月 8, 2023  |  119 字  |  总阅读

【C++】Windows 下使用 Cmake 编译附带第三方库的 C++ 程序

C++ 环境搭建 下载 Cygwin 并安装,选择 Category-Devel,勾选如下 Package: 开发简单 C++ 项目 打开 Cygwin64 Terminal。 mkdir -p /home/dev/cpp-code && cd /home/dev/cpp-code 编辑文件 /home/dev/cpp-code/main.cpp 如下: #include <fmt/core.h> int main() { fmt::print("Hello World!\n"); } 编辑文件 /home/dev/cpp-code/CMakeLists.txt 如下: cmake_minimum_required(VERSION 3.23) project(cpp_code) set(CMAKE_CXX_STANDARD 14) include(FetchContent) FetchContent_Declare( fmt GIT_REPOSITORY https://github.com/fmtlib/fmt.git GIT_TAG 9.1.0 ) FetchContent_MakeAvailable(fmt) add_executable(hello_world main.cpp) target_link_libraries(hello_world PRIVATE fmt::fmt) 这里使用了一个第三方库 fmt。 Cmake 编译 执行以下命令编译: cd /home/dev/cpp-code mkdir build && cd build cmake .. make 日志打印如下: $ cmake .. -- The C compiler identification is GNU 11.3.0 -- The CXX compiler identification is GNU 11.3.0 -- Detecting C compiler ABI info -- Detecting C compiler ABI info - done -- Check for working C compiler: /usr/bin/cc - skipped -- Detecting C compile features -- Detecting C compile features - done -- Detecting CXX compiler ABI info -- Detecting CXX compiler ABI info - done -- Check for working CXX compiler: /usr/bin/c++.exe - skipped -- Detecting CXX compile features -- Detecting CXX compile features - done -- Module support is disabled. -- Version: 9.1.0 -- Build type: -- CXX_STANDARD: 14 -- Performing Test has_std_14_flag -- Performing Test has_std_14_flag - Success -- Performing Test has_std_1y_flag -- Performing Test has_std_1y_flag - Success -- Required features: cxx_variadic_templates -- Configuring done -- Generating done -- Build files have been written to: /home/dev/cpp-code/build $ make [ 20%] Building CXX object _deps/fmt-build/CMakeFiles/fmt.dir/src/format.cc.o [ 40%] Building CXX object _deps/fmt-build/CMakeFiles/fmt.dir/src/os.cc.o [ 60%] Linking CXX static library libfmt.a [ 60%] Built target fmt [ 80%] Building CXX object CMakeFiles/hello_world.dir/main.cpp.o [100%] Linking CXX executable hello_world.exe [100%] Built target hello_world 查看编译后的程序,并运行: ...

十二月 17, 2022  |  263 字  |  总阅读

【Shell】标准的 Shell 脚本

记录一下 shell 脚本模板。 #!/usr/bin/env bash args_num=$# action="${1}" ALLOWED_ACTION_ARGS=("set" "unset" "test") function print_ok() { local msg="${1}" echo "${msg}" } function print_err() { local msg="${1}" echo "${msg}" > /dev/stderr } function contain() { local list="${1}" local ele="${2}" for i in ${list[*]}; do if [ "${ele}" == "${i}" ]; then return 0 fi done return 1 } function check_args_num() { if [ ${args_num} != 1 ]; then print_err "Wrong args num" return 1 fi } function check_action_arg() { if ! contain "${ALLOWED_ACTION_ARGS[*]}" "${action}"; then print_err "Action arg must be [ set || unset || test ]" return 1 fi } function set_proxy () { export ALL_PROXY=http://www.vksir.zone print_ok "Set proxy success" } function unset_proxy() { unset ALL_PROXY print_ok "Unset proxy success" } function test_proxy() { if curl -k https://www.google.com --connect-timeout 3 >/dev/null 2>&1; then print_ok "Proxy is available" else print_err "Proxy is not available" fi } if ! check_args_num; then return 1 fi if ! check_action_arg; then return 1 fi if [ "${action}" == "set" ]; then set_proxy elif [ "${action}" == "unset" ]; then unset_proxy elif [ "${action}" == "test" ]; then test_proxy fi return $? source proxy set source proxy unset source proxy test 注意,这里如果想写 source 脚本,那就不能使用 exit,否则会使 ssh 会话退出。 ...

九月 1, 2022  |  198 字  |  总阅读

【Electron】使用 Electron + Vue + Antd + Python 构建桌面程序

写在前面 也算是个记录吧,以免以后想写桌面程序又要走一遍弯路。这玩意不难,只是也有些坑,会耗些时间。 前端真没什么好说的,搞框架、搞设计的人很强,但调包这件事,人人都会干。 搭建前端 创建 vue 项目 yarn global add @vue/cli vue create electron-vue 参考:https://cli.vuejs.org/zh/guide/installation.html。 引入 electron 有现成的不用是…… cd electron-vue vue add electron-builder 参考:https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/#installation。 没事儿不要自己瞎折腾,有时候遇上一些奇怪的报错,作为后端开发党,很难解决。 试一试 yarn electron:serve 引入 Antd Vue yarn add ant-design-vue 参考:https://www.antdv.com/docs/vue/getting-started-cn。 这里如果使用 npm 进行安装 npm i --save ant-design-vue,就会报错。——咱也不懂,咱也懒得看。 修改 src/main.js 如下: import { createApp } from 'vue' import App from './App.vue' import Antd from 'ant-design-vue' import 'ant-design-vue/dist/antd.css' createApp(App).use(Antd).mount('#app') 修改 src/components/HelloWorld.vue 如下: <script setup> import {ref} from "vue"; const content = ref('hello electron') function click() { content.value = 'hello vue' } </script> <template> <a-button type="primary" @click="click">click me</a-button> <a-typography-paragraph>{{ content }}</a-typography-paragraph> </template> 搭建后端 后端用什么都行,go、python 都可以。 ...

八月 30, 2022  |  349 字  |  总阅读

【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 字  |  总阅读