You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

606 lines
14 KiB
Go

2 months ago
package main
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net"
"os"
"os/signal"
"path/filepath"
"sort"
"strings"
"sync"
"syscall"
"time"
)
// ==================== 配置 ====================
type Config struct {
Server ServerConfig `json:"server"`
Remote RemoteConfig `json:"remote"`
Timing TimingConfig `json:"timing"`
Log LogConfig `json:"log"`
Reverse bool `json:"reverse"` // true=正(翻转发送逻辑), false=反(默认)
}
type ServerConfig struct {
Port int `json:"port"` // 本机 TCP 监听端口
}
type RemoteConfig struct {
Host string `json:"host"` // 远端 TCP 服务地址
Port int `json:"port"` // 远端 TCP 服务端口
DialTimeout int `json:"dial_timeout_seconds"` // 连接超时
}
type TimingConfig struct {
StartupDelaySeconds int `json:"startup_delay_seconds"` // 启动后延迟发送
ResendDelaySeconds int `json:"resend_delay_seconds"` // 收到后重发延迟
}
type LogConfig struct {
Dir string `json:"dir"` // 日志目录
Level string `json:"level"` // debug/info/warn/error
KeepDays int `json:"keep_days"` // 日志保留天数
}
func defaultConfig() Config {
return Config{
Server: ServerConfig{Port: 9600},
Remote: RemoteConfig{
Host: "127.0.0.1",
Port: 9000,
DialTimeout: 5,
},
Timing: TimingConfig{
StartupDelaySeconds: 180,
ResendDelaySeconds: 180,
},
Log: LogConfig{
Dir: "logs",
Level: "info",
KeepDays: 60,
},
Reverse: false,
}
}
func loadConfig(path string) (*Config, error) {
cfg := defaultConfig()
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
log.Printf("[WARN] 配置文件 %s 不存在,使用默认配置", path)
return &cfg, nil
}
return nil, fmt.Errorf("读取配置文件失败: %w", err)
}
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("解析配置文件失败: %w", err)
}
if cfg.Server.Port <= 0 || cfg.Server.Port > 65535 {
return nil, fmt.Errorf("无效的服务端口: %d", cfg.Server.Port)
}
if cfg.Remote.Port <= 0 || cfg.Remote.Port > 65535 {
return nil, fmt.Errorf("无效的远端端口: %d", cfg.Remote.Port)
}
if cfg.Log.KeepDays <= 0 {
cfg.Log.KeepDays = 60
}
if cfg.Log.Dir == "" {
cfg.Log.Dir = "logs"
}
return &cfg, nil
}
// ==================== 按天滚动日志 ====================
type DailyLogger struct {
dir string
level string
keepDays int
logger *log.Logger
mu sync.Mutex
date string // 当前日志日期
file *os.File
}
func NewDailyLogger(dir, level string, keepDays int) *DailyLogger {
os.MkdirAll(dir, 0755)
dl := &DailyLogger{
dir: dir,
level: level,
keepDays: keepDays,
}
dl.rotate()
return dl
}
func (dl *DailyLogger) rotate() {
today := time.Now().Format("2006-01-02")
if dl.date == today && dl.logger != nil {
return
}
// 关闭旧文件
if dl.file != nil {
dl.file.Close()
}
dl.date = today
filename := filepath.Join(dl.dir, fmt.Sprintf("tcp_service_%s.log", today))
f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Printf("[ERROR] 无法创建日志文件 %s: %v", filename, err)
dl.logger = log.New(os.Stdout, "", log.LstdFlags|log.Lmicroseconds)
dl.file = nil
return
}
dl.file = f
multiWriter := io.MultiWriter(os.Stdout, f)
dl.logger = log.New(multiWriter, "", log.LstdFlags|log.Lmicroseconds)
// 清理过期日志
go dl.cleanOldLogs()
}
func (dl *DailyLogger) cleanOldLogs() {
entries, err := os.ReadDir(dl.dir)
if err != nil {
return
}
cutoff := time.Now().AddDate(0, 0, -dl.keepDays)
var expired []string
for _, e := range entries {
if e.IsDir() || !strings.HasPrefix(e.Name(), "tcp_service_") {
continue
}
// 从文件名提取日期: tcp_service_2006-01-02.log
name := e.Name()
dateStr := strings.TrimPrefix(name, "tcp_service_")
dateStr = strings.TrimSuffix(dateStr, ".log")
t, err := time.Parse("2006-01-02", dateStr)
if err != nil {
continue
}
if t.Before(cutoff) {
expired = append(expired, filepath.Join(dl.dir, name))
}
}
sort.Strings(expired)
for _, p := range expired {
os.Remove(p)
dl.Logf("[INFO] 清理过期日志: %s", filepath.Base(p))
}
}
// Logf 写日志(自动检查日期滚动)
func (dl *DailyLogger) Logf(format string, args ...interface{}) {
dl.mu.Lock()
defer dl.mu.Unlock()
today := time.Now().Format("2006-01-02")
if dl.date != today {
dl.rotate()
}
msg := fmt.Sprintf(format, args...)
dl.logger.Println(msg)
}
// ==================== 远端 TCP 客户端 ====================
type TCPClient struct {
host string
port int
timeout time.Duration
log *DailyLogger
mu sync.Mutex
conn net.Conn
writer *bufio.Writer
ctx context.Context
cancel context.CancelFunc
}
func NewTCPClient(host string, port int, dialTimeoutSec int, log *DailyLogger) *TCPClient {
ctx, cancel := context.WithCancel(context.Background())
return &TCPClient{
host: host,
port: port,
timeout: time.Duration(dialTimeoutSec) * time.Second,
log: log,
ctx: ctx,
cancel: cancel,
}
}
func (c *TCPClient) Connect() error {
c.mu.Lock()
defer c.mu.Unlock()
if c.conn != nil {
c.conn.Close()
}
addr := fmt.Sprintf("%s:%d", c.host, c.port)
c.log.Logf("[INFO] 正在连接远端 %s ...", addr)
d := net.Dialer{Timeout: c.timeout}
conn, err := d.DialContext(c.ctx, "tcp", addr)
if err != nil {
return fmt.Errorf("连接远端 %s 失败: %w", addr, err)
}
conn.SetDeadline(time.Time{})
c.conn = conn
c.writer = bufio.NewWriter(conn)
c.log.Logf("[INFO] 已连接远端 %s", addr)
go c.receiveLoop()
return nil
}
func (c *TCPClient) Send(msg string) error {
// 最多重试 2 次(首次 + 1次重连重试)
for attempt := 0; attempt < 2; attempt++ {
// 确保连接存在
c.mu.Lock()
needConnect := c.conn == nil
c.mu.Unlock()
if needConnect {
if err := c.Connect(); err != nil {
return err
}
}
c.mu.Lock()
if c.conn == nil {
c.mu.Unlock()
continue
}
c.log.Logf("[INFO] >>> 发送远端: %s", msg)
_, err := c.writer.WriteString(msg)
if err != nil {
c.log.Logf("[ERROR] 发送失败: %v, 重试中...", err)
c.closeLocked()
c.mu.Unlock()
continue
}
if err := c.writer.Flush(); err != nil {
c.log.Logf("[ERROR] Flush失败: %v, 重试中...", err)
c.closeLocked()
c.mu.Unlock()
continue
}
c.mu.Unlock()
return nil
}
return fmt.Errorf("发送失败: 重试次数已用完")
}
func (c *TCPClient) receiveLoop() {
c.mu.Lock()
conn := c.conn
c.mu.Unlock()
if conn == nil {
return
}
reader := bufio.NewReader(conn)
for {
select {
case <-c.ctx.Done():
return
default:
}
line, err := reader.ReadString('\n')
if err != nil {
if err == io.EOF {
c.log.Logf("[WARN] 远端关闭了连接")
} else {
c.log.Logf("[WARN] 远端接收错误: %v", err)
}
// 只关闭自己的连接,避免误关已被替换的新连接
c.mu.Lock()
if c.conn == conn {
c.closeLocked()
} else {
conn.Close()
}
c.mu.Unlock()
return
}
c.log.Logf("[INFO] <<< 远端返回: %s", strings.TrimRight(line, "\r\n"))
}
}
func (c *TCPClient) closeLocked() {
if c.conn != nil {
c.conn.Close()
c.conn = nil
c.writer = nil
}
}
func (c *TCPClient) Close() {
c.cancel()
c.mu.Lock()
defer c.mu.Unlock()
c.closeLocked()
}
// ==================== TCP 服务端 ====================
type TCPServer struct {
port int
client *TCPClient
cfg *TimingConfig
log *DailyLogger
reverse bool
ctx context.Context
cancel context.CancelFunc
autoOffTimer *time.Timer // 自动关灯定时器
timerMu sync.Mutex
}
func NewTCPServer(port int, client *TCPClient, cfg *TimingConfig, reverse bool, log *DailyLogger) *TCPServer {
ctx, cancel := context.WithCancel(context.Background())
return &TCPServer{
port: port,
client: client,
cfg: cfg,
log: log,
reverse: reverse,
ctx: ctx,
cancel: cancel,
}
}
// sendMsg 发送消息(根据 reverse 参数翻转)
func (s *TCPServer) sendMsg(msg string) error {
if s.reverse {
msg = flipMsg(msg)
}
return s.client.Send(msg)
}
// flipMsg 翻转消息内容 <1,0,post> ↔ <1,1,post>
func flipMsg(msg string) string {
switch msg {
case "<1,0,post>":
return "<1,1,post>"
case "<1,1,post>":
return "<1,0,post>"
default:
return msg
}
}
// flipMsgCond 根据条件翻转(用于 main 等非 TCPServer 上下文)
func flipMsgCond(msg string, reverse bool) string {
if reverse {
return flipMsg(msg)
}
return msg
}
func (s *TCPServer) Start() error {
addr := fmt.Sprintf(":%d", s.port)
listener, err := net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf("TCP 监听 %s 失败: %w", addr, err)
}
s.log.Logf("[INFO] TCP 服务监听 %s", addr)
go func() {
<-s.ctx.Done()
listener.Close()
}()
for {
conn, err := listener.Accept()
if err != nil {
select {
case <-s.ctx.Done():
return nil
default:
s.log.Logf("[ERROR] Accept 失败: %v", err)
continue
}
}
go s.handleConn(conn)
}
}
func (s *TCPServer) handleConn(conn net.Conn) {
defer conn.Close()
remoteAddr := conn.RemoteAddr().String()
s.log.Logf("[INFO] 新连接: %s", remoteAddr)
reader := bufio.NewReader(conn)
for {
select {
case <-s.ctx.Done():
return
default:
}
line, err := reader.ReadString('>')
if err != nil {
if err != io.EOF {
s.log.Logf("[WARN] 读取 %s 错误: %v", remoteAddr, err)
}
s.log.Logf("[INFO] 连接断开: %s", remoteAddr)
return
}
cmd := strings.TrimSpace(line)
switch cmd {
case "<1,0,post>":
s.log.Logf("[INFO] <<< 收到命令 [%s]: %s 关灯命令", remoteAddr, cmd)
go s.handleCmd1_0()
case "<1,1,post>":
s.log.Logf("[INFO] <<< 收到命令 [%s]: %s 开灯命令", remoteAddr, cmd)
go s.handleCmd1_1()
default:
s.log.Logf("[WARN] 未知命令: %s", cmd)
continue
}
}
}
// cancelAutoOffTimer 取消自动关灯定时器(如果有的话)
func (s *TCPServer) cancelAutoOffTimer() {
s.timerMu.Lock()
defer s.timerMu.Unlock()
if s.autoOffTimer != nil {
s.autoOffTimer.Stop()
s.autoOffTimer = nil
s.log.Logf("[INFO] 已取消自动关灯定时器")
}
}
// resetAutoOffTimer 重置自动关灯定时器(先取消旧的,再创建新的)
func (s *TCPServer) resetAutoOffTimer() {
s.timerMu.Lock()
if s.autoOffTimer != nil {
s.autoOffTimer.Stop()
}
delay := time.Duration(s.cfg.ResendDelaySeconds) * time.Second
msg := s.effectiveMsg("<1,1,post>")
s.log.Logf("[INFO] 重置自动关灯定时器: %v 后发送 %s", delay, msg)
s.autoOffTimer = time.AfterFunc(delay, func() {
s.timerMu.Lock()
s.autoOffTimer = nil
s.timerMu.Unlock()
s.log.Logf("[INFO] 自动关灯触发: 发送 %s", msg)
if err := s.sendMsg("<1,1,post>"); err != nil {
s.log.Logf("[ERROR] 自动关灯发送失败: %v", err)
}
})
s.timerMu.Unlock()
}
// handleCmd1_0: 收到 <1,0,post>(关灯命令)→ 远端发送 <1,1,post>,并取消自动关灯定时器
func (s *TCPServer) handleCmd1_0() {
s.cancelAutoOffTimer() // 关灯了就不用再自动关了
s.log.Logf("[INFO] 处理 <1,0,post>: 向远端发送 %s", s.effectiveMsg("<1,1,post>"))
if err := s.sendMsg("<1,1,post>"); err != nil {
s.log.Logf("[ERROR] 发送失败: %v", err)
}
}
func (s *TCPServer) effectiveMsg(msg string) string {
if s.reverse {
return flipMsg(msg)
}
return msg
}
// handleCmd1_1: 收到 <1,1,post>(开灯命令)→ 远端发送 <1,0,post>,重置自动关灯定时器
func (s *TCPServer) handleCmd1_1() {
s.log.Logf("[INFO] 处理 <1,1,post>: 向远端发送 %s", s.effectiveMsg("<1,0,post>"))
if err := s.sendMsg("<1,0,post>"); err != nil {
s.log.Logf("[ERROR] 发送失败: %v", err)
}
// 重置自动关灯定时器(如果之前有旧的会被取消)
s.resetAutoOffTimer()
}
// ==================== 主函数 ====================
func main() {
configPath := "config.json"
if len(os.Args) > 1 {
configPath = os.Args[1]
}
cfg, err := loadConfig(configPath)
if err != nil {
log.Fatalf("[FATAL] %v", err)
}
// 初始化按天滚动日志
dailyLog := NewDailyLogger(cfg.Log.Dir, cfg.Log.Level, cfg.Log.KeepDays)
dailyLog.Logf("[INFO] ======== TCP 转发服务启动 ========")
dailyLog.Logf("[INFO] 配置: 监听端口=%d, 远端=%s:%d, 启动延迟=%ds, 重发延迟=%ds, 正反=%v, 日志保留=%d天",
cfg.Server.Port,
cfg.Remote.Host, cfg.Remote.Port,
cfg.Timing.StartupDelaySeconds, cfg.Timing.ResendDelaySeconds,
cfg.Reverse,
cfg.Log.KeepDays)
// 创建远端 TCP 客户端
remoteClient := NewTCPClient(cfg.Remote.Host, cfg.Remote.Port, cfg.Remote.DialTimeout, dailyLog)
defer remoteClient.Close()
// 启动时连接远端
if err := remoteClient.Connect(); err != nil {
dailyLog.Logf("[WARN] 启动时连接远端失败: %v (将在收到命令时自动重连)", err)
}
// 启动延迟 → 自动发送 <1,1,post>(正模式时翻转)
startupDelay := time.Duration(cfg.Timing.StartupDelaySeconds) * time.Second
startupMsg := flipMsgCond("<1,1,post>", cfg.Reverse)
dailyLog.Logf("[INFO] 将在 %v 后自动发送 %s", startupDelay, startupMsg)
time.AfterFunc(startupDelay, func() {
dailyLog.Logf("[INFO] 启动定时触发: 发送 %s", startupMsg)
if err := remoteClient.Send(startupMsg); err != nil {
dailyLog.Logf("[ERROR] 启动定时发送失败: %v", err)
}
})
// 启动 TCP 服务端
server := NewTCPServer(cfg.Server.Port, remoteClient, &cfg.Timing, cfg.Reverse, dailyLog)
// 优雅关闭
go func() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
dailyLog.Logf("[INFO] 收到退出信号,正在关闭...")
shutdownMsg := flipMsgCond("<1,1,post>", cfg.Reverse)
dailyLog.Logf("[INFO] 退出前发送 %s", shutdownMsg)
if err := remoteClient.Send(shutdownMsg); err != nil {
dailyLog.Logf("[ERROR] 退出发送失败: %v", err)
}
server.cancel()
remoteClient.Close()
os.Exit(0)
}()
if err := server.Start(); err != nil {
dailyLog.Logf("[FATAL] 服务启动失败: %v", err)
os.Exit(1)
}
}