好的,我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 getsockname 函数,它用于获取本地(本端)套接字的地址信息,即套接字绑定的本地 IP 地址和端口号。


1. 函数介绍 链接到标题

getsockname 是一个 Linux 系统调用,其功能是查询一个套接字的本地协议地址(local protocol address)。这个地址包含了套接字绑定的本地 IP 地址本地端口号

你可以把它想象成查看自己电话的号码

  • 你有一个电话(套接字)。
  • 你想知道这个电话的号码是多少(本地地址),以便告诉别人如何联系你。
  • getsockname 就是查看这个号码的功能。

这对于以下场景非常有用:

  1. 客户端: 客户端在调用 connect 之前通常不显式调用 bind。操作系统会自动为其分配一个临时的本地端口。客户端可以使用 getsockname 来发现操作系统为它分配了哪个本地端口。
  2. 服务器: 服务器显式调用 bind 来指定监听的端口。getsockname 可以用来确认绑定的地址是否是预期的,尤其是在绑定到 INADDR_ANY (0.0.0.0) 时,可以查看具体绑定到了哪个接口。
  3. 调试: 在网络编程调试中,getsockname 是一个非常有用的工具,可以检查套接字的实际状态。

2. 函数原型 链接到标题

#include <sys/socket.h> // 必需

int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

3. 功能 链接到标题

  • 获取本地地址: 检索与套接字 sockfd 关联的本地协议地址
  • 填充结构体: 将获取到的地址信息填充到调用者提供的 struct sockaddr 结构体指针 addr 所指向的内存中。
  • 返回地址大小: 通过 addrlen 参数返回实际存储在 addr 中的地址结构的大小。

4. 参数 链接到标题

  • int sockfd: 这是一个有效的套接字文件描述符。
  • struct sockaddr *addr: 这是一个指向套接字地址结构的指针,用于接收本地地址信息。
    • 调用者需要提供一个足够大的缓冲区(例如 struct sockaddr_instruct sockaddr_in6)并将其地址传递给 addr
  • socklen_t *addrlen: 这是一个指向 socklen_t 类型变量的指针。
    • 输入: 在调用 getsockname 时,这个变量必须被初始化为 addr 指向的缓冲区的大小(以字节为单位)。例如,如果 addr 指向 struct sockaddr_in,则 *addrlen 应初始化为 sizeof(struct sockaddr_in)
    • 输出: getsockname 返回时,这个变量会被更新为实际存储在 addr 中的地址结构的大小。这对于处理不同大小的地址结构(如 IPv4 和 IPv6)很有用。

5. 返回值 链接到标题

  • 成功时: 返回 0。同时,addr 指向的结构体被成功填充,*addrlen 被更新为实际地址大小。
  • 失败时: 返回 -1,并设置全局变量 errno 来指示具体的错误原因(例如 EBADF sockfd 无效,EINVAL addrlen 指针无效,ENOTSOCK sockfd 不是一个套接字等)。

6. 相似函数,或关联函数 链接到标题

  • getpeername: 用于获取对端(peer)套接字的地址信息。对于已连接的套接字,它返回连接到的远程主机的 IP 地址和端口号。
  • bind: 用于将套接字与一个本地地址进行绑定。getsockname 可以用来验证 bind 的结果。
  • connect: 客户端使用此函数连接到服务器。连接成功后,getsockname 可以显示客户端被分配的本地地址。
  • socket: 创建套接字。

7. 示例代码 链接到标题

示例 1:客户端使用 getsockname 获取本地地址 链接到标题

这个例子演示了 TCP 客户端在连接到服务器后,如何使用 getsockname 来发现操作系统为其自动分配的本地 IP 地址和端口号。

// getsockname_client.c
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h> // inet_ntoa, ntohs
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define SERVER_PORT 8090
#define SERVER_IP "127.0.0.1"

int main() {
    int sock;
    struct sockaddr_in server_addr, local_addr;
    socklen_t local_addr_len = sizeof(local_addr);

    // 1. 创建套接字
    sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }
    printf("Client socket created (fd: %d)\n", sock);

    // 2. (可选) 在 connect 之前调用 getsockname
    // 此时套接字未连接,未显式绑定,getsockname 的行为是未指定的
    // 通常返回 0.0.0.0:0 或类似的未指定地址
    printf("--- Before connect ---\n");
    if (getsockname(sock, (struct sockaddr *)&local_addr, &local_addr_len) == 0) {
        printf("Local address before connect: %s:%d\n",
               inet_ntoa(local_addr.sin_addr), ntohs(local_addr.sin_port));
    } else {
        perror("getsockname before connect failed");
    }

    // 3. 配置服务器地址并连接
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
        fprintf(stderr, "Invalid server address\n");
        close(sock);
        exit(EXIT_FAILURE);
    }

    printf("Connecting to server %s:%d...\n", SERVER_IP, SERVER_PORT);
    if (connect(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("connect failed");
        close(sock);
        exit(EXIT_FAILURE);
    }
    printf("Connected to server successfully.\n");

    // 4. (关键) 连接后调用 getsockname 获取本地地址
    printf("\n--- After connect ---\n");
    // 重新初始化长度,以防被上次调用修改
    local_addr_len = sizeof(local_addr);
    if (getsockname(sock, (struct sockaddr *)&local_addr, &local_addr_len) == 0) {
        printf("Local address (assigned by OS): %s:%d\n",
               inet_ntoa(local_addr.sin_addr), ntohs(local_addr.sin_port));
    } else {
        perror("getsockname after connect failed");
    }

    // 5. (对比) 使用 getpeername 获取对端地址
    struct sockaddr_in peer_addr;
    socklen_t peer_addr_len = sizeof(peer_addr);
    if (getpeername(sock, (struct sockaddr *)&peer_addr, &peer_addr_len) == 0) {
        printf("Peer address (server): %s:%d\n",
               inet_ntoa(peer_addr.sin_addr), ntohs(peer_addr.sin_port));
    } else {
        perror("getpeername failed");
    }

    printf("\nClient is now connected and knows its local and peer addresses.\n");

    // ... 进行通信 ...

    close(sock);
    printf("Client socket closed.\n");
    return 0;
}

代码解释:

  1. 创建一个 TCP 套接字。
  2. connect 之前调用 getsockname。此时行为未定义,通常返回未指定的地址(如 0.0.0.0:0)。
  3. 配置服务器地址并调用 connect 连接到服务器。
  4. 关键步骤: connect 成功返回后,立即调用 getsockname(sock, (struct sockaddr *)&local_addr, &local_addr_len)
    • sock: 要查询的套接字。
    • &local_addr: 指向用于存储本地地址的 sockaddr_in 结构的指针。
    • &local_addr_len: 指向 socklen_t 变量的指针,该变量在调用前被初始化为 sizeof(local_addr)
  5. getsockname 成功后,打印出本地地址(IP 和端口)。这个端口是操作系统自动分配的临时端口。
  6. 对比: 调用 getpeername 获取对端(服务器)的地址。
  7. 最后关闭套接字。

示例 2:服务器使用 getsockname 验证绑定地址 链接到标题

这个例子演示了 TCP 服务器如何在绑定后使用 getsockname 来确认其本地地址。

// getsockname_server.c
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define PORT 8090
#define BACKLOG 5

int main() {
    int server_fd;
    struct sockaddr_in address, local_addr;
    socklen_t local_addr_len = sizeof(local_addr);
    int opt = 1;

    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 配置服务器地址结构
    memset(&address, 0, sizeof(address));
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY; // 绑定到所有接口
    address.sin_port = htons(PORT);

    printf("Binding server socket to port %d (INADDR_ANY)...\n", PORT);
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // --- 关键: 使用 getsockname 验证绑定的地址 ---
    printf("\n--- Verifying bound address with getsockname ---\n");
    if (getsockname(server_fd, (struct sockaddr *)&local_addr, &local_addr_len) == 0) {
        printf("Server socket bound to:\n");
        printf("  Address Family: AF_INET (%d)\n", local_addr.sin_family);
        printf("  Local IP Address: %s\n", inet_ntoa(local_addr.sin_addr));
        printf("  Local Port: %d\n", ntohs(local_addr.sin_port));
        // 当绑定到 INADDR_ANY 时,getsockname 通常仍显示 0.0.0.0
        // 要知道实际监听的接口,需要更复杂的网络检查
    } else {
        perror("getsockname failed");
    }

    if (listen(server_fd, BACKLOG) < 0) {
        perror("listen failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }
    printf("\nServer is listening.\n");

    printf("Server setup complete. Run client to connect.\n");
    pause(); // 等待信号退出

    close(server_fd);
    return 0;
}

代码解释:

  1. 创建、设置选项、配置地址结构(绑定到 INADDR_ANYPORT)。
  2. 调用 bind 将套接字绑定到指定地址。
  3. 关键步骤: 调用 getsockname(server_fd, ...) 来获取绑定后的本地地址。
  4. 打印出通过 getsockname 获取的地址信息。
    • 注意:当绑定到 INADDR_ANY 时,getsockname 返回的 IP 地址通常仍然是 0.0.0.0。这表示它监听所有接口。要获取实际的、具体的本地 IP 地址,需要更高级的网络接口查询(如 getifaddrs)。
  5. 调用 listen 开始监听。
  6. 程序挂起等待连接。

示例 3:绑定到特定 IP 后使用 getsockname 链接到标题

这个例子演示了服务器绑定到特定 IP 地址后,getsockname 如何返回该特定地址。

// getsockname_specific_ip.c
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define SPECIFIC_IP "127.0.0.1" // 绑定到本地回环
#define PORT 8091
#define BACKLOG 5

int main() {
    int server_fd;
    struct sockaddr_in address, local_addr;
    socklen_t local_addr_len = sizeof(local_addr);
    int opt = 1;

    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 配置服务器地址结构 (绑定到特定 IP)
    memset(&address, 0, sizeof(address));
    address.sin_family = AF_INET;
    if (inet_pton(AF_INET, SPECIFIC_IP, &address.sin_addr) <= 0) {
        fprintf(stderr, "Invalid specific IP address: %s\n", SPECIFIC_IP);
        close(server_fd);
        exit(EXIT_FAILURE);
    }
    address.sin_port = htons(PORT);

    printf("Binding server socket to %s:%d...\n", SPECIFIC_IP, PORT);
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // --- 使用 getsockname 验证绑定的特定地址 ---
    printf("\n--- Verifying bound address with getsockname ---\n");
    if (getsockname(server_fd, (struct sockaddr *)&local_addr, &local_addr_len) == 0) {
        printf("Server socket confirmed bound to:\n");
        printf("  Local IP Address: %s\n", inet_ntoa(local_addr.sin_addr));
        printf("  Local Port: %d\n", ntohs(local_addr.sin_port));
        // 这次应该显示 127.0.0.1
    } else {
        perror("getsockname failed");
    }

    if (listen(server_fd, BACKLOG) < 0) {
        perror("listen failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }
    printf("\nServer is listening on %s:%d.\n", SPECIFIC_IP, PORT);

    pause(); // 等待信号退出

    close(server_fd);
    return 0;
}

代码解释:

  1. 与上一个服务器示例类似,但这次将 address.sin_addr 设置为一个特定的 IP 地址 (127.0.0.1)。
  2. 调用 bind 绑定到该特定地址。
  3. 调用 getsockname
  4. 打印结果。这次 getsockname 返回的 IP 地址应该就是绑定的那个特定地址 (127.0.0.1)。

示例 4:UDP 套接字使用 getsockname 链接到标题

这个例子演示了 getsockname 同样适用于 UDP 套接字。

// getsockname_udp.c
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define UDP_PORT 8092

int main() {
    int udp_sock;
    struct sockaddr_in addr, local_addr;
    socklen_t local_addr_len = sizeof(local_addr);

    // 1. 创建 UDP 套接字
    udp_sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (udp_sock < 0) {
        perror("UDP socket creation failed");
        exit(EXIT_FAILURE);
    }
    printf("UDP socket created (fd: %d)\n", udp_sock);

    // 2. 配置并绑定地址
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port = htons(UDP_PORT);

    printf("Binding UDP socket to port %d...\n", UDP_PORT);
    if (bind(udp_sock, (const struct sockaddr *)&addr, sizeof(addr)) < 0) {
        perror("UDP bind failed");
        close(udp_sock);
        exit(EXIT_FAILURE);
    }

    // 3. 使用 getsockname 获取绑定的地址
    printf("\n--- Getting UDP socket's local name ---\n");
    if (getsockname(udp_sock, (struct sockaddr *)&local_addr, &local_addr_len) == 0) {
        printf("UDP socket bound to:\n");
        printf("  IP Address: %s\n", inet_ntoa(local_addr.sin_addr));
        printf("  Port: %d\n", ntohs(local_addr.sin_port));
    } else {
        perror("getsockname on UDP socket failed");
    }

    printf("\nUDP socket is ready. You can send datagrams to it.\n");
    pause(); // 等待信号退出

    close(udp_sock);
    printf("UDP socket closed.\n");
    return 0;
}

代码解释:

  1. 创建一个 UDP (SOCK_DGRAM) 套接字。
  2. 将其绑定到 INADDR_ANYUDP_PORT
  3. 调用 getsockname 获取并打印绑定的本地地址信息。

重要提示与注意事项: 链接到标题

  1. 适用于所有类型套接字: getsockname 可以用于 SOCK_STREAM (TCP)、SOCK_DGRAM (UDP) 以及其他类型的套接字。
  2. 区分 getsocknamegetpeername:
    • getsockname: 获取本地地址(我的地址)。
    • getpeername: 获取对端地址(对方的地址),仅对已连接的套接字有效。
  3. addrlen 的初始化: 在调用前,必须*addrlen 初始化为目标缓冲区的大小。调用后,它会被更新为实际的地址结构大小。
  4. 绑定到 INADDR_ANY: 当套接字绑定到 INADDR_ANY (0.0.0.0) 时,getsockname 通常也返回 0.0.0.0,而不是具体的本地 IP 地址。
  5. 客户端自动分配: 对于客户端,即使没有显式调用 bind,在 connect 成功后,getsockname 也能返回操作系统自动分配的本地地址。
  6. 错误处理: 始终检查返回值。在套接字无效或未正确创建时调用会失败。

总结:

getsockname 是一个简单但非常实用的函数,用于查询套接字的本地地址信息。无论是在客户端发现自动分配的端口,还是在服务器端验证绑定的地址,它都是一个重要的调试和信息获取工具。理解其参数(特别是 addrlen 的输入输出特性)对于正确使用它至关重要。