好的,我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 getsockname
函数,它用于获取本地(本端)套接字的地址信息,即套接字绑定的本地 IP 地址和端口号。
1. 函数介绍 链接到标题
getsockname
是一个 Linux 系统调用,其功能是查询一个套接字的本地协议地址(local protocol address)。这个地址包含了套接字绑定的本地 IP 地址和本地端口号。
你可以把它想象成查看自己电话的号码:
- 你有一个电话(套接字)。
- 你想知道这个电话的号码是多少(本地地址),以便告诉别人如何联系你。
getsockname
就是查看这个号码的功能。
这对于以下场景非常有用:
- 客户端: 客户端在调用
connect
之前通常不显式调用bind
。操作系统会自动为其分配一个临时的本地端口。客户端可以使用getsockname
来发现操作系统为它分配了哪个本地端口。 - 服务器: 服务器显式调用
bind
来指定监听的端口。getsockname
可以用来确认绑定的地址是否是预期的,尤其是在绑定到INADDR_ANY
(0.0.0.0) 时,可以查看具体绑定到了哪个接口。 - 调试: 在网络编程调试中,
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_in
或struct 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;
}
代码解释:
- 创建一个 TCP 套接字。
- 在
connect
之前调用getsockname
。此时行为未定义,通常返回未指定的地址(如0.0.0.0:0
)。 - 配置服务器地址并调用
connect
连接到服务器。 - 关键步骤:
connect
成功返回后,立即调用getsockname(sock, (struct sockaddr *)&local_addr, &local_addr_len)
。sock
: 要查询的套接字。&local_addr
: 指向用于存储本地地址的sockaddr_in
结构的指针。&local_addr_len
: 指向socklen_t
变量的指针,该变量在调用前被初始化为sizeof(local_addr)
。
getsockname
成功后,打印出本地地址(IP 和端口)。这个端口是操作系统自动分配的临时端口。- 对比: 调用
getpeername
获取对端(服务器)的地址。 - 最后关闭套接字。
示例 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;
}
代码解释:
- 创建、设置选项、配置地址结构(绑定到
INADDR_ANY
和PORT
)。 - 调用
bind
将套接字绑定到指定地址。 - 关键步骤: 调用
getsockname(server_fd, ...)
来获取绑定后的本地地址。 - 打印出通过
getsockname
获取的地址信息。- 注意:当绑定到
INADDR_ANY
时,getsockname
返回的 IP 地址通常仍然是0.0.0.0
。这表示它监听所有接口。要获取实际的、具体的本地 IP 地址,需要更高级的网络接口查询(如getifaddrs
)。
- 注意:当绑定到
- 调用
listen
开始监听。 - 程序挂起等待连接。
示例 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;
}
代码解释:
- 与上一个服务器示例类似,但这次将
address.sin_addr
设置为一个特定的 IP 地址 (127.0.0.1
)。 - 调用
bind
绑定到该特定地址。 - 调用
getsockname
。 - 打印结果。这次
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;
}
代码解释:
- 创建一个 UDP (
SOCK_DGRAM
) 套接字。 - 将其绑定到
INADDR_ANY
和UDP_PORT
。 - 调用
getsockname
获取并打印绑定的本地地址信息。
重要提示与注意事项: 链接到标题
- 适用于所有类型套接字:
getsockname
可以用于SOCK_STREAM
(TCP)、SOCK_DGRAM
(UDP) 以及其他类型的套接字。 - 区分
getsockname
和getpeername
:getsockname
: 获取本地地址(我的地址)。getpeername
: 获取对端地址(对方的地址),仅对已连接的套接字有效。
addrlen
的初始化: 在调用前,必须将*addrlen
初始化为目标缓冲区的大小。调用后,它会被更新为实际的地址结构大小。- 绑定到
INADDR_ANY
: 当套接字绑定到INADDR_ANY
(0.0.0.0) 时,getsockname
通常也返回0.0.0.0
,而不是具体的本地 IP 地址。 - 客户端自动分配: 对于客户端,即使没有显式调用
bind
,在connect
成功后,getsockname
也能返回操作系统自动分配的本地地址。 - 错误处理: 始终检查返回值。在套接字无效或未正确创建时调用会失败。
总结:
getsockname
是一个简单但非常实用的函数,用于查询套接字的本地地址信息。无论是在客户端发现自动分配的端口,还是在服务器端验证绑定的地址,它都是一个重要的调试和信息获取工具。理解其参数(特别是 addrlen
的输入输出特性)对于正确使用它至关重要。