好的,我们继续按照您列表的顺序,从最后一个已介绍的函数 (listen) 往前,来介绍下一个重要的系统调用。在您的列表中,listen 之前的一个函数是 bind,但我们已经介绍过了。

让我们回顾一下列表的结尾部分:

sendto, recvfrom, sendmsg, recvmsg, shutdown, bind, listen, getsockname, getpeername, …

我们已经介绍了 getsockname,所以接下来应该介绍 getpeername


1. 函数介绍 見出しへのリンク

getpeername 是一个 Linux 系统调用,用于获取已连接套接字的对方(peer)的协议地址。这个地址包含了与本端套接字建立连接的那个远程主机的 IP 地址端口号

你可以把它想象成查看已接通电话的对方号码

  • 你和朋友正在通话(套接字已连接)。
  • 你想知道现在和你通话的人的电话号码是多少(对方地址)。
  • getpeername 就是查看这个对方号码的功能。

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

  1. 服务器: 服务器 accept 一个连接后,得到一个新的已连接套接字。服务器可以使用 getpeername 来获取是哪个客户端(IP 和端口)连接了进来。
  2. 客户端: 客户端在 connect 成功后,可以使用 getpeername 来确认它连接到了哪个服务器地址(IP 和端口),尤其是在连接时使用了域名,想确认解析后的具体 IP。
  3. 调试和日志: 在网络程序中记录连接信息时,getpeername 是获取对端地址的标准方法。

2. 函数原型 見出しへのリンク

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

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

3. 功能 見出しへのリンク

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

4. 参数 見出しへのリンク

  • int sockfd: 这是一个有效的、已连接的套接字文件描述符。
    • 对于 TCP,这意味着已经成功调用了 connect(客户端)或 accept(服务器返回的新套接字)。
    • 对于 UDP,如果调用了 connect,也可以使用 getpeername
    • 如果套接字未连接,调用会失败。
  • struct sockaddr *addr: 这是一个指向套接字地址结构的指针,用于接收对方的地址信息。
    • 调用者需要提供一个足够大的缓冲区(例如 struct sockaddr_instruct sockaddr_in6)并将其地址传递给 addr
  • socklen_t *addrlen: 这是一个指向 socklen_t 类型变量的指针。
    • 输入: 在调用 getpeername 时,这个变量必须被初始化为 addr 指向的缓冲区的大小(以字节为单位)。例如,如果 addr 指向 struct sockaddr_in,则 *addrlen 应初始化为 sizeof(struct sockaddr_in)
    • 输出: getpeername 返回时,这个变量会被更新为实际存储在 addr 中的地址结构的大小

5. 返回值 見出しへのリンク

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

6. 相似函数,或关联函数 見出しへのリンク

  • getsockname: 用于获取本地(本端)套接字的地址信息。
  • connect: 客户端使用此函数连接到服务器。连接成功后,可以使用 getpeername 获取服务器地址。
  • accept: 服务器使用此函数接受客户端连接,返回一个已连接的新套接字,可以使用 getpeername 获取客户端地址。
  • socket: 创建套接字。

7. 示例代码 見出しへのリンク

示例 1:客户端使用 getpeername 确认服务器地址 見出しへのリンク

这个例子演示了 TCP 客户端在连接到服务器后,如何使用 getpeername 来获取它所连接的服务器的地址信息。

// getpeername_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 8093
#define SERVER_IP "127.0.0.1" // 可以换成域名测试,如 "localhost"

int main() {
    int sock;
    struct sockaddr_in server_addr, peer_addr;
    socklen_t peer_addr_len = sizeof(peer_addr);

    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);

    // 配置服务器地址并连接
    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: %s\n", SERVER_IP);
        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");

    // --- 关键: 使用 getpeername 获取服务器地址 ---
    printf("\n--- Getting peer (server) name with getpeername ---\n");
    // 重新初始化长度,以防被修改
    peer_addr_len = sizeof(peer_addr);
    if (getpeername(sock, (struct sockaddr *)&peer_addr, &peer_addr_len) == 0) {
        printf("Connected to server at:\n");
        printf("  Peer IP Address: %s\n", inet_ntoa(peer_addr.sin_addr));
        printf("  Peer Port: %d\n", ntohs(peer_addr.sin_port));
    } else {
        perror("getpeername failed");
        // 常见错误:ENOTCONN (如果套接字未连接)
    }

    // (对比) 使用 getsockname 获取自己的本地地址
    struct sockaddr_in local_addr;
    socklen_t local_addr_len = sizeof(local_addr);
    if (getsockname(sock, (struct sockaddr *)&local_addr, &local_addr_len) == 0) {
        printf("\nMy local address is:\n");
        printf("  Local IP Address: %s\n", inet_ntoa(local_addr.sin_addr));
        printf("  Local Port: %d\n", ntohs(local_addr.sin_port));
    } else {
        perror("getsockname failed");
    }

    printf("\nClient is connected and verified peer/local addresses.\n");

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

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

代码解释:

  1. 创建 TCP 套接字。
  2. 配置服务器地址并调用 connect 连接。
  3. 关键步骤: 连接成功后,调用 getpeername(sock, (struct sockaddr *)&peer_addr, &peer_addr_len)
    • sock: 已连接的套接字。
    • &peer_addr: 指向用于存储对端地址的 sockaddr_in 结构的指针。
    • &peer_addr_len: 指向 socklen_t 变量的指针,初始化为 sizeof(peer_addr)
  4. getpeername 成功后,打印出服务器的地址(IP 和端口)。
  5. 对比: 调用 getsockname 获取自己的本地地址。
  6. 最后关闭套接字。

示例 2:服务器使用 getpeername 获取客户端地址 見出しへのリンク

这个例子演示了 TCP 服务器在 accept 一个连接后,如何使用 getpeername 来获取连接进来的客户端的地址信息。

// getpeername_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 8093
#define BACKLOG 10

void handle_client(int client_fd) {
    struct sockaddr_in peer_addr;
    socklen_t peer_addr_len = sizeof(peer_addr);

    printf("Handling new client connection (fd: %d)\n", client_fd);

    // --- 关键: 使用 getpeername 获取客户端地址 ---
    if (getpeername(client_fd, (struct sockaddr *)&peer_addr, &peer_addr_len) == 0) {
        printf("  Client connected from: %s:%d\n",
               inet_ntoa(peer_addr.sin_addr), ntohs(peer_addr.sin_port));
    } else {
        perror("  getpeername on client socket failed");
    }

    // 为了演示,我们立即关闭连接
    // 实际应用中,这里会进行数据读写
    close(client_fd);
    printf("  Closed connection to client.\n");
}

int main() {
    int server_fd, client_fd;
    struct sockaddr_in address;
    socklen_t client_addr_len;
    struct sockaddr_in client_address;
    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);

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    if (listen(server_fd, BACKLOG) < 0) {
        perror("listen failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("Server listening on port %d\n", PORT);

    // 演示接受几个连接
    for (int i = 0; i < 3; ++i) {
        printf("\n--- Waiting for connection #%d ---\n", i + 1);
        client_addr_len = sizeof(client_address);
        client_fd = accept(server_fd, (struct sockaddr *)&client_address, &client_addr_len);
        if (client_fd < 0) {
            perror("accept failed");
            continue;
        }

        printf("New connection accepted (fd: %d)\n", client_fd);
        // accept 返回的 client_fd 已经包含了客户端地址信息,
        // 我们也可以用 getpeername 再次确认
        handle_client(client_fd);
    }

    close(server_fd);
    printf("Server socket closed.\n");
    return 0;
}

如何测试:

  1. 在一个终端编译并运行服务器:
    gcc -o getpeername_server getpeername_server.c
    ./getpeername_server
    
  2. 在另外几个终端运行客户端(可以多次运行):
    gcc -o getpeername_client getpeername_client.c
    ./getpeername_client
    

代码解释:

  1. 创建、绑定、监听标准的 TCP 服务器套接字。
  2. 进入一个循环,accept 三个客户端连接。
  3. 每次 accept 成功后,调用 handle_client(client_fd)
  4. handle_client 函数中:
    • 关键步骤: 调用 getpeername(client_fd, ...)
    • client_fdaccept 返回的、与特定客户端关联的已连接套接字。
    • getpeername 会返回该客户端的 IP 地址和端口号。
    • 打印出客户端的地址信息。
  5. 函数结束时关闭与该客户端的连接。

示例 3:对比 getsocknamegetpeername 見出しへのリンク

这个例子在一个已连接的套接字上同时使用 getsocknamegetpeername,清晰地展示它们的区别。

// compare_sock_peer_name.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 SERVER_PORT 8094
#define SERVER_IP "127.0.0.1"

void print_socket_addresses(int sock, const char* role) {
    struct sockaddr_in local_addr, peer_addr;
    socklen_t addr_len;

    printf("--- Addresses for %s socket (fd: %d) ---\n", role, sock);

    // 获取本地地址
    addr_len = sizeof(local_addr);
    if (getsockname(sock, (struct sockaddr *)&local_addr, &addr_len) == 0) {
        printf("  Local Address : %s:%d\n",
               inet_ntoa(local_addr.sin_addr), ntohs(local_addr.sin_port));
    } else {
        perror("  getsockname failed");
    }

    // 获取对端地址
    addr_len = sizeof(peer_addr);
    if (getpeername(sock, (struct sockaddr *)&peer_addr, &addr_len) == 0) {
        printf("  Peer Address  : %s:%d\n",
               inet_ntoa(peer_addr.sin_addr), ntohs(peer_addr.sin_port));
    } else {
        perror("  getpeername failed");
        // 如果套接字未连接,会打印错误,如 "Transport endpoint is not connected"
    }
    printf("----------------------------------------\n");
}

int main() {
    int server_fd, client_sock;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    int opt = 1;

    // --- 服务器端 ---
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == 0) {
        perror("server socket failed");
        exit(EXIT_FAILURE);
    }

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

    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(SERVER_PORT);

    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("server bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    if (listen(server_fd, 5) < 0) {
        perror("server listen failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("Server listening on port %d\n", PORT);

    // --- 客户端 ---
    client_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (client_sock < 0) {
        perror("client socket failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    memset(&client_addr, 0, sizeof(client_addr));
    client_addr.sin_family = AF_INET;
    client_addr.sin_port = htons(SERVER_PORT);
    if (inet_pton(AF_INET, SERVER_IP, &client_addr.sin_addr) <= 0) {
        fprintf(stderr, "Invalid server IP\n");
        close(server_fd);
        close(client_sock);
        exit(EXIT_FAILURE);
    }

    printf("\nClient connecting to server...\n");
    if (connect(client_sock, (struct sockaddr *)&client_addr, sizeof(client_addr)) < 0) {
        perror("client connect failed");
        close(server_fd);
        close(client_sock);
        exit(EXIT_FAILURE);
    }

    // --- 连接建立后,检查客户端套接字 ---
    print_socket_addresses(client_sock, "Client");

    // --- 服务器接受连接 ---
    int accepted_sock = accept(server_fd, NULL, NULL); // 忽略客户端地址
    if (accepted_sock < 0) {
        perror("server accept failed");
        close(server_fd);
        close(client_sock);
        exit(EXIT_FAILURE);
    }

    // --- 检查服务器端的已连接套接字 ---
    print_socket_addresses(accepted_sock, "Server-Accepted");

    // 客户端和服务器的本地地址应该等于对方的对端地址
    printf("\nNote:\n");
    printf("- Client's Local Address should equal Server's Peer Address.\n");
    printf("- Server's Local Address should equal Client's Peer Address.\n");

    close(client_sock);
    close(accepted_sock);
    close(server_fd);

    return 0;
}

代码解释:

  1. 程序同时扮演服务器和客户端的角色。
  2. 设置服务器套接字并开始监听。
  3. 创建客户端套接字并连接到服务器。
  4. 定义一个 print_socket_addresses 函数,它接受一个套接字和一个角色名称(“Client” 或 “Server-Accepted”)。
  5. 在该函数内部:
    • 调用 getsockname 获取并打印本地地址。
    • 调用 getpeername 获取并打印对端地址。
  6. main 函数中:
    • 客户端连接成功后,调用 print_socket_addresses(client_sock, "Client")
    • 服务器 accept 连接后,调用 print_socket_addresses(accepted_sock, "Server-Accepted")
  7. 通过打印的地址可以清楚地看到:
    • 客户端的本地地址 == 服务器 accept 套接字的对端地址。
    • 服务器的本地地址 == 客户端的对端地址。

重要提示与注意事项: 見出しへのリンク

  1. 仅对已连接套接字有效: getpeername 只能用于已连接的套接字。对于未连接的套接字(如刚创建的套接字,或未 connect 的 UDP 套接字),调用会失败,errno 通常为 ENOTCONN
  2. 区分 getsocknamegetpeername:
    • getsockname: 获取本地地址(我的地址)。
    • getpeername: 获取对端地址(对方的地址)。
  3. addrlen 的初始化: 与 getsockname 一样,在调用前,必须*addrlen 初始化为目标缓冲区的大小。调用后,它会被更新为实际的地址结构大小。
  4. 服务器常用: 服务器在 accept 后,经常使用 getpeername 来记录或显示是哪个客户端连接了进来。
  5. 客户端确认: 客户端可以用它来确认连接的服务器地址,尤其是在使用域名时。
  6. 错误处理: 始终检查返回值。最常见的错误是 ENOTCONN(套接字未连接)。

总结:

getpeername 是一个用于获取已连接套接字对端地址信息的系统调用。它与 getsockname 相辅相成,分别用于查询连接两端的地址。在服务器记录客户端信息和客户端确认服务器信息等场景中非常有用。理解其参数和使用条件对于进行有效的网络编程至关重要。