Go语言限制SFTP并发会话:实现单活跃SSH会话控制
问题解决方案
核心问题是当前代码每次调用Download都会新建SSH/SFTP会话,并发场景下会同时存在多个会话导致设备重启。以下是两种可行的解决思路:
方案一:互斥锁控制单会话并发
通过给SftpClient添加互斥锁,保证同一时间只有一个请求能创建并使用SSH会话,从根本上避免多会话并发。
步骤1:修改SftpClient结构体
添加互斥锁字段:
import "sync" type SftpClient struct { // 保留原有字段(serverAddr、retryTimeout、sshConfig等) mu sync.Mutex }
步骤2:修改tryDownload方法
在创建SSH连接前加锁,执行完成后自动释放:
func (c *SftpClient) tryDownload() ([]byte, bool) { c.mu.Lock() defer c.mu.Unlock() // 确保锁一定会释放,即使发生panic defer func() { if err := recover(); err != nil { log.Errorf("panic occurred: %s", err) } }() // 原有tryDownload的代码逻辑保持不变 client, err := sshDial(c.serverAddr, c.retryTimeout, c.sshConfig) data := []byte{} downloaded := false if err != nil { log.Errorf("Failed to SSH dial to %s: %s", c.serverAddr, err) return data, downloaded } defer client.Close() // 后续SFTP操作代码... }
该方案实现简单,改动量小,适合快速解决问题,但每次请求都需要重新建立SSH连接,存在一定的连接开销。
方案二:复用SSH连接(更高效)
通过维护一个持久的SSH/SFTP连接,所有请求复用该连接,彻底避免重复创建会话的问题,同时需要处理连接失效后的自动重连。
步骤1:修改SftpClient结构体
添加连接实例和互斥锁:
import "sync" import "golang.org/x/crypto/ssh" import "github.com/pkg/sftp" type SftpClient struct { // 保留原有字段 mu sync.Mutex sshClient *ssh.Client sftpClient *sftp.Client }
步骤2:添加获取有效SFTP连接的方法
func (c *SftpClient) getValidSftpClient() (*sftp.Client, error) { c.mu.Lock() defer c.mu.Unlock() // 检查现有连接是否有效 if c.sshClient != nil && c.sftpClient != nil { // 执行简单操作检测连接存活状态 if _, err := c.sftpClient.ReadDir("."); err == nil { return c.sftpClient, nil } // 连接失效,关闭旧连接 c.sftpClient.Close() c.sshClient.Close() } // 创建新连接 sshClient, err := sshDial(c.serverAddr, c.retryTimeout, c.sshConfig) if err != nil { return nil, err } sftpClient, err := sftp.NewClient(sshClient) if err != nil { sshClient.Close() return nil, err } c.sshClient = sshClient c.sftpClient = sftpClient return sftpClient, nil }
步骤3:修改tryDownload方法
复用现有连接,不再每次创建新连接:
func (c *SftpClient) tryDownload() ([]byte, bool) { defer func() { if err := recover(); err != nil { log.Errorf("panic occurred: %s", err) } }() data := []byte{} downloaded := false // 获取有效SFTP连接 sftpClient, err := c.getValidSftpClient() if err != nil { log.Errorf("Failed to get valid SFTP client: %s", err) return data, downloaded } // 原有SFTP操作逻辑(无需再手动关闭连接) fList, err := sftpClient.ReadDir(c.directory) if err != nil { log.Errorf("Failed to read directory %s on %s: %s", c.directory, c.serverAddr, err) return data, downloaded } // 后续文件操作代码... }
步骤4:添加关闭连接的方法
在客户端不再使用时,主动释放连接:
func (c *SftpClient) Close() error { c.mu.Lock() defer c.mu.Unlock() var err error if c.sftpClient != nil { if e := c.sftpClient.Close(); e != nil { err = e } } if c.sshClient != nil { if e := c.sshClient.Close(); e != nil && err == nil { err = e } } return err }
该方案减少了重复建立SSH连接的开销,适合频繁调用的场景,但需要处理连接失效的重连逻辑,代码复杂度稍高。
额外优化:修复sshDial的goroutine泄漏问题
原sshDial函数使用无缓冲channel,超时后goroutine会因发送阻塞导致泄漏,修改为带缓冲的channel:
func sshDial(addr string, retryTimeout time.Duration, sshConfig *ssh.ClientConfig) (*ssh.Client, error) { ch := make(chan *ssh.Client, 1) // 带缓冲的channel,避免goroutine阻塞 go func() { client, err := ssh.Dial("tcp", addr, sshConfig) if err != nil { ch <- nil } else { ch <- client } }() select { case <-time.After(retryTimeout): return nil, fmt.Errorf("SSH dialing timed-out") case client := <-ch: if client != nil { return client, nil } return nil, fmt.Errorf("SSH dial failed") } }
内容的提问来源于stack exchange,提问作者Silvester
相关产品推荐
相关产品推荐

