在运行服务器实例前释放端口的Golang实现

在运行服务器实例前释放端口的Golang实现 我想在基于linux的系统上运行一个go服务器,在某些情况下发现同一端口被另一个应用程序占用,因此我希望终止该端口上正在运行的进程,然后启动我的服务器。为此,我编写了以下代码:

func main() {

	host := "127.0.0.1"
	port := "8070"
	server := http.Server{
		Addr: "127.0.0.1:8070",
	}

	http.Handle("/www/", http.StripPrefix("/www/", http.FileServer(http.Dir("./www"))))

	ln, err := net.Listen("tcp", ":"+port)

	if err != nil {
		fmt.Fprintf(os.Stderr, "Can't listen on port %q: %s \n", port, err)
        // kill the running process at this port
		_, err := exec.Command("fuser", "-k", "8070/tcp").Output()

		if err != nil {
			fmt.Printf("Failed to kill process at Port %q\n", port)
		} else {
			fmt.Printf("TCP Port %q is available\n", port)
			server.ListenAndServe()
		}

	} else {
		ln.Close()
		server.ListenAndServe()
	}
}

我能够收到响应TCP Port 8070 is available,这意味着有另一个正在运行的进程并且它已被终止,但我的应用程序直接关闭了,而没有在已经释放的同一端口上运行我的服务器!


更多关于在运行服务器实例前释放端口的Golang实现的实战教程也可以访问 https://www.itying.com/category-94-b0.html

7 回复

检查返回的错误。它存在是有原因的。

更多关于在运行服务器实例前释放端口的Golang实现的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


ln 仍然是 nil。请尝试再次监听。

嗯,不太明白你的意思,你能提供一些示例代码吗?谢谢。

hyousef:

在某些情况下,我发现同一个端口被另一个应用程序占用了。

我很好奇。既然有 65,535 个其他端口可供选择,为什么还会是同一个端口呢?

以下代码(未经充分测试)的目的是在调用实际的监听之前,获取套接字文件描述符并为其设置选项。

package main

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"syscall"

	"golang.org/x/sys/unix"
)

func main() {
	runServer()
}
func runServer() {

	host := "127.0.0.1"
	port := "8070"

	ctx := context.Background()

	server := http.Server{
		Addr: host + ":" + port,
	}
	defer func() {
		_ = server.Shutdown(ctx)
	}()

	lc := net.ListenConfig{
		Control: setReusePort,
	}
	ln, err := lc.Listen(ctx, "tcp4", host+":"+port)
	if err != nil {
		return
	}
	defer ln.Close()

	if err := server.Serve(ln); err != nil {
		return
	}
}

func setReusePort(network, address string, c syscall.RawConn) error {
	c.Control(rawConnSetReusePort)
	return nil
}

func rawConnSetReusePort(fd uintptr) {
	syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, unix.SO_REUSEPORT, 1)
}

另外请注意 server.Shutdown()ln.Close() 有点过度了。

我能够通过 panic ... recover 解决这个问题,如下所示:

package main

import (
	"fmt"
	"net"
	"net/http"
	"onsen/resources"
	"onsen/routes"
	"os"
	"os/exec"
)

func main() {
	http.Handle("/webUI/", http.StripPrefix("/webUI/", http.FileServer(http.FS(resources.WebUI))))
	http.Handle("/www/", http.StripPrefix("/www/", http.FileServer(http.Dir("./www"))))

	for key, value := range routes.Urls() {
		http.HandleFunc(key, value)
	}
	runServer()
}
func runServer() {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("Recovered in f", r)
			runServer()
		}
	}()

	host := "127.0.0.1"
	port := "8070"

	server := http.Server{
		Addr: fmt.Sprintf("%v:%v", host, port),
	}

	ln, err := net.Listen("tcp", ":"+port)

	if err != nil {
		fmt.Fprintf(os.Stderr, "Can't listen on port %q: %s \n", port, err)
		// kill the running process at this port
		_, err := exec.Command("fuser", "-k", "8070/tcp").Output()

		if err != nil {
			panic(fmt.Sprintf("Failed to kill process at Port %q\n", port))
		} else {
			fmt.Printf("TCP Port %q is available\n", port)
			fmt.Println("server started...")
			if err := server.Serve(ln); err != nil {
				panic(err)
			}
		}
	} else {
		fmt.Println("server started...")
		if err := server.Serve(ln); err != nil {
			panic(err)
		}
	}
}

并得到了如下输出:

image

在您的代码中,当端口被占用时,您执行了fuser -k命令来终止占用端口的进程,但之后没有重新尝试监听端口。server.ListenAndServe()只在fuser命令执行成功后调用,但此时端口可能尚未完全释放,或者需要重新创建监听器。以下是修正后的实现:

package main

import (
	"fmt"
	"net"
	"net/http"
	"os"
	"os/exec"
	"time"
)

func killProcessOnPort(port string) error {
	cmd := exec.Command("fuser", "-k", port+"/tcp")
	return cmd.Run()
}

func waitForPort(port string, timeout time.Duration) bool {
	deadline := time.Now().Add(timeout)
	for time.Now().Before(deadline) {
		conn, err := net.Dial("tcp", ":"+port)
		if err != nil {
			return true
		}
		conn.Close()
		time.Sleep(100 * time.Millisecond)
	}
	return false
}

func main() {
	port := "8070"
	
	// 先尝试正常监听
	ln, err := net.Listen("tcp", ":"+port)
	if err != nil {
		fmt.Printf("端口 %s 被占用,尝试释放...\n", port)
		
		// 终止占用端口的进程
		if err := killProcessOnPort(port); err != nil {
			fmt.Printf("释放端口失败: %v\n", err)
			os.Exit(1)
		}
		
		// 等待端口释放
		if !waitForPort(port, 2*time.Second) {
			fmt.Printf("端口 %s 释放超时\n", port)
			os.Exit(1)
		}
		
		// 重新尝试监听
		ln, err = net.Listen("tcp", ":"+port)
		if err != nil {
			fmt.Printf("端口 %s 仍然不可用: %v\n", port, err)
			os.Exit(1)
		}
		ln.Close()
	} else {
		ln.Close()
	}

	// 配置并启动服务器
	server := http.Server{
		Addr: ":" + port,
	}
	
	http.Handle("/www/", 
		http.StripPrefix("/www/", 
			http.FileServer(http.Dir("./www"))))
	
	fmt.Printf("服务器启动在端口 %s\n", port)
	if err := server.ListenAndServe(); err != nil {
		fmt.Printf("服务器启动失败: %v\n", err)
	}
}

这个实现做了以下改进:

  1. 将端口释放逻辑封装到单独的函数中
  2. 添加了waitForPort函数来等待端口完全释放
  3. 在释放端口后重新尝试创建监听器
  4. 确保端口可用后再启动HTTP服务器

注意:使用fuser -k需要相应的系统权限,在某些环境下可能需要sudo权限。

回到顶部