You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

端口解绑方法及代码修改:实现进程接收信号后自动释放端口

端口占用问题的解决方案

问题描述

我启动了一个绑定端口的TCP服务器进程,代码如下:

#include <sys/socket.h>
#include <sys/un.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#define BUF_SIZE 500
#define LISTEN_BACKLOG 50
#define handle_error(msg) \
    do { perror(msg); exit(EXIT_FAILURE); } while (0)
int main(int argc, char *argv[]) {
    struct addrinfo hints;
    struct addrinfo *res, *rp;
    struct sockaddr_storage peer_addr;
    socklen_t peer_addr_len;
    int sfd, cfd;
    if(argc != 3) {
        fprintf(stderr, "Usage: %s address port\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = AI_PASSIVE;
    hints.ai_protocol = 0;
    hints.ai_addr = NULL;
    hints.ai_next = NULL;
    if(getaddrinfo(argv[1], argv[2], &hints, &res) != 0)
        handle_error("getaddrinfo");
    // Try each socket until we bind
    for(rp = res; rp != NULL; rp = rp->ai_next){
        sfd = socket(rp->ai_family, rp->ai_socktype, 0);
        if(sfd == -1) continue;
        if(bind(sfd, rp->ai_addr, rp->ai_addrlen) == 0) break;
        else close(sfd);
    }
    freeaddrinfo(res);
    if (rp == NULL){
        fprintf(stderr, "Could not bind to any socket\n");
        exit(EXIT_FAILURE);
    }
    // Set the TCP socket to listen state
    if (listen(sfd, LISTEN_BACKLOG) == -1)
        handle_error("listen");
    printf("Server started");
    for(;;){
        // Accept
        peer_addr_len = sizeof(struct sockaddr_storage);
        cfd = accept(sfd, (struct sockaddr *) &peer_addr, &peer_addr_len);
        if (cfd == -1) handle_error("accept");
        // Fork
        pid_t pid = fork();
        if (pid == 0){
            //Child code
            pid_t my_pid = getpid();
            char buf[BUF_SIZE];
            while(1){
                ssize_t nread = recv(cfd, buf, BUF_SIZE, 0);
                if (nread == 0) {
                    close(cfd);
                    exit(EXIT_SUCCESS);
                } else {
                    buf[nread] = 0;
                    fprintf(stdout, "Worker %i has received a message: %s", my_pid, buf);
                    ssize_t nsent = send(cfd, buf, nread, 0);
                    if (nsent != nread)
                        fprintf(stderr, "Error sending response");
                }
            }
        }
        close(sfd);
    }

但杀死进程后,无法再次使用同一端口启动程序,用sudo lsof -i -P -n | grep LISTEN检查发现端口仍处于LISTEN状态,进程还在占用。想知道:

  1. 如何临时解除这些端口的绑定?
  2. 如何修改代码,让进程收到信号时自动解绑端口?

解答

1. 临时释放端口的方法

你可以用以下两种方式快速解决当前的端口占用问题:

  • 通过进程ID杀死占用进程
    lsof的输出中找到对应端口的进程ID(PID),然后执行:
    sudo kill -9 <目标PID>
    
    如果一次没效果,可以多执行几次,确保进程完全终止。
  • 直接通过端口杀死进程
    使用fuser命令直接定位并杀死占用指定端口的所有进程(以端口8080为例):
    sudo fuser -k 8080/tcp
    

这些都是临时应急方案,想要彻底避免这个问题,得从代码层面优化。

2. 修改代码实现优雅退出与端口释放

你的代码存在几个导致端口无法正常释放的问题:

  • 父进程在fork后错误地关闭了监听套接字sfd,导致后续无法继续接受新连接,还可能引发资源释放异常;
  • 没有处理终止信号,进程被强制杀死时,监听套接字可能没被正确关闭,导致端口长时间处于占用状态。

下面是优化后的代码,添加了信号处理和套接字选项配置:

#include <sys/socket.h>
#include <sys/un.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <signal.h>
#include <errno.h>

#define BUF_SIZE 500
#define LISTEN_BACKLOG 50
#define handle_error(msg) \
    do { perror(msg); exit(EXIT_FAILURE); } while (0)

// 全局变量保存监听套接字,方便信号处理函数访问
static int sfd_global = -1;

// 信号处理函数,用于优雅退出
void handle_signal(int sig) {
    printf("Received signal %d, shutting down...\n", sig);
    if (sfd_global != -1) {
        close(sfd_global); // 关闭监听套接字,释放端口
        sfd_global = -1;
    }
    exit(EXIT_SUCCESS);
}

int main(int argc, char *argv[]) {
    struct addrinfo hints;
    struct addrinfo *res, *rp;
    struct sockaddr_storage peer_addr;
    socklen_t peer_addr_len;
    int cfd;

    // 注册信号处理函数,处理SIGINT(Ctrl+C)和SIGTERM
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = handle_signal;
    if (sigaction(SIGINT, &sa, NULL) == -1)
        handle_error("sigaction SIGINT");
    if (sigaction(SIGTERM, &sa, NULL) == -1)
        handle_error("sigaction SIGTERM");

    if(argc != 3) {
        fprintf(stderr, "Usage: %s address port\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = AI_PASSIVE;
    hints.ai_protocol = 0;
    hints.ai_addr = NULL;
    hints.ai_next = NULL;

    if(getaddrinfo(argv[1], argv[2], &hints, &res) != 0)
        handle_error("getaddrinfo");

    // Try each socket until we bind
    for(rp = res; rp != NULL; rp = rp->ai_next){
        sfd_global = socket(rp->ai_family, rp->ai_socktype, 0);
        if(sfd_global == -1) continue;
        // 添加SO_REUSEADDR选项,允许端口快速重用(即使处于TIME_WAIT状态)
        int optval = 1;
        if (setsockopt(sfd_global, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) == -1)
            handle_error("setsockopt SO_REUSEADDR");
        if(bind(sfd_global, rp->ai_addr, rp->ai_addrlen) == 0) break;
        else close(sfd_global);
    }
    freeaddrinfo(res);

    if (rp == NULL){
        fprintf(stderr, "Could not bind to any socket\n");
        exit(EXIT_FAILURE);
    }

    // Set the TCP socket to listen state
    if (listen(sfd_global, LISTEN_BACKLOG) == -1)
        handle_error("listen");

    printf("Server started, listening on %s:%s\n", argv[1], argv[2]);

    for(;;){
        // Accept
        peer_addr_len = sizeof(struct sockaddr_storage);
        cfd = accept(sfd_global, (struct sockaddr *) &peer_addr, &peer_addr_len);
        if (cfd == -1) {
            // 如果是被信号中断,继续循环,否则报错
            if (errno == EINTR) continue;
            handle_error("accept");
        }
        // Fork
        pid_t pid = fork();
        if (pid == 0){
            //Child code
            // 子进程不需要监听套接字,关闭它
            close(sfd_global);
            pid_t my_pid = getpid();
            char buf[BUF_SIZE];
            while(1){
                ssize_t nread = recv(cfd, buf, BUF_SIZE, 0);
                if (nread == 0) {
                    close(cfd);
                    exit(EXIT_SUCCESS);
                } else if (nread == -1) {
                    if (errno == EINTR) continue;
                    perror("recv");
                    close(cfd);
                    exit(EXIT_FAILURE);
                } else {
                    buf[nread] = 0;
                    fprintf(stdout, "Worker %i has received a message: %s", my_pid, buf);
                    ssize_t nsent = send(cfd, buf, nread, 0);
                    if (nsent != nread)
                        fprintf(stderr, "Error sending response");
                }
            }
        }
        // 父进程关闭连接套接字
        close(cfd);
    }
}

关键修改点说明:

  • 全局监听套接字:定义全局变量sfd_global保存监听套接字的文件描述符,方便信号处理函数访问;
  • 信号处理函数:注册SIGINT(Ctrl+C触发)和SIGTERMkill命令触发)的处理函数,收到信号时优雅关闭监听套接字并退出;
  • SO_REUSEADDR选项:添加这个套接字选项,允许端口快速重用,即使端口处于TCP的TIME_WAIT状态也能直接绑定;
  • 修复父进程逻辑:父进程在fork后关闭的是连接套接字cfd,而非监听套接字sfd,这样父进程可以持续接受新连接;
  • 子进程资源清理:子进程启动后立即关闭监听套接字,减少不必要的文件描述符占用;
  • 中断错误处理:处理acceptrecv被信号中断的EINTR错误,避免进程意外退出。

修改后,当你用Ctrl+C或者kill <PID>终止进程时,程序会自动关闭监听套接字,释放端口,下次启动就能直接绑定同一端口了。


内容的提问来源于stack exchange,提问作者Jsevillamol

火山引擎 最新活动