在运行服务器实例前释放端口的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
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)
}
}
}
并得到了如下输出:

在您的代码中,当端口被占用时,您执行了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)
}
}
这个实现做了以下改进:
- 将端口释放逻辑封装到单独的函数中
- 添加了
waitForPort函数来等待端口完全释放 - 在释放端口后重新尝试创建监听器
- 确保端口可用后再启动HTTP服务器
注意:使用fuser -k需要相应的系统权限,在某些环境下可能需要sudo权限。


