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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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)
}
}