如何从同一网络中的另一台机器连接自己的 C 服务器?
不需要额外调用“跨机器连接”函数。给出的程序已经可以用于局域网通信:服务器绑定可接收外部数据的地址,客户端使用服务器的局域网 IP,并指定相同的端口即可。
这份代码使用 UDP。仅凭“本机正常、跨机器失败、能 ping 通”,还不能确定故障原因。需要检查目标地址、绑定的地址族和防火墙,并确认数据报是否到达服务器。
为什么 localhost 能用,换一台机器就不行?
localhost、127.0.0.1 和 ::1 都指向运行客户端的机器。客户端移到另一台电脑后,如果继续使用这些地址,访问的仍是客户端本机。
ping 通常使用 ICMP,程序则通过 SOCK_DGRAM 使用 UDP。能收到 ICMP 回应,不代表指定的 UDP 端口也能通信。
服务器中的这两处设置可以用于接收局域网请求:
hints.ai_flags = AI_PASSIVE;
s = getaddrinfo(NULL, argv[1], &hints, &result);
节点参数为 NULL 且设置了 AI_PASSIVE 时,getaddrinfo() 会返回适合绑定的通配地址:IPv4 对应 0.0.0.0,IPv6 对应 ::。因此,这份服务器代码没有明确限制为只接收 localhost 的请求。
不过,代码在第一次 bind() 成功后就退出循环,只创建一个 socket。AF_UNSPEC 允许返回 IPv4 和 IPv6 候选地址,并不保证程序会同时接收两种协议的请求。
先用服务器的局域网 IPv4 地址运行
在服务器所在机器上查看地址:
ip -4 addr show
选择实际连接路由器的网卡地址。以下假设服务器地址为 192.168.1.10,客户端地址为 192.168.1.20,测试端口为 50000。使用时请替换为实际值。
在服务器机器上编译并启动程序:
cc -Wall -Wextra server.c -o server
./server 50000
保持服务器运行,再在客户端机器上执行:
cc -Wall -Wextra client.c -o client
./client 192.168.1.10 50000 hello
最后的 hello 不能省略。客户端从 argv[3] 开始发送消息,如果只提供主机和端口,原程序会直接结束,不会发送数据。
客户端的参数检查也应改为:
if (argc < 4) {
fprintf(stderr, "Usage: %s host port msg...\n", argv[0]);
exit(EXIT_FAILURE);
}
客户端应填写服务器的地址。0.0.0.0 是服务器绑定时使用的通配地址,不应作为客户端的目标地址。两台机器能直接通过局域网地址通信时,通常不需要配置路由器端口转发。
对原代码做几处有助于排查的修改
暂时统一使用 IPv4
为排除 IPv4、IPv6 不匹配的问题,将客户端和服务器中的:
hints.ai_family = AF_UNSPEC;
都改为:
hints.ai_family = AF_INET;
服务器的初始化代码如下,其余通信代码可以保留:
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_flags = AI_PASSIVE;
s = getaddrinfo(NULL, argv[1], &hints, &result);
客户端的初始化代码:
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_DGRAM;
s = getaddrinfo(argv[1], argv[2], &hints, &result);
这样,服务器会尝试绑定 IPv4 通配地址,客户端则访问指定的 IPv4 地址。
如果需要同时支持 IPv4 和 IPv6,可以分别创建并绑定 socket。若要用单个 IPv6 socket 接收 IPv4 请求,需要确认 IPV6_V6ONLY 设置,不能仅凭 AF_UNSPEC 判断其行为。
避免服务器在反向 DNS 查询上等待
服务器收到数据后,会先调用 getnameinfo(),再发送回应。原代码只设置了 NI_NUMERICSERV,仍可能尝试将客户端 IP 解析为主机名。如果名称解析迟迟没有完成,客户端就会一直等待回应。
排查时可以直接输出数字地址:
s = getnameinfo((struct sockaddr *) &peer_addr,
peer_addrlen,
host, sizeof(host),
service, sizeof(service),
NI_NUMERICHOST | NI_NUMERICSERV);
这能消除名称解析带来的等待,但当前故障是否由此引起,还需要确认。
给客户端设置接收超时
原客户端的 read() 默认会阻塞等待。如果 UDP 回应被丢弃,程序可能长时间没有输出。
在客户端增加头文件:
#include <sys/time.h>
成功创建并选定 sfd 后,在发送消息前设置超时:
struct timeval timeout = {
.tv_sec = 3,
.tv_usec = 0
};
if (setsockopt(sfd, SOL_SOCKET, SO_RCVTIMEO,
&timeout, sizeof(timeout)) == -1) {
perror("setsockopt");
close(sfd);
exit(EXIT_FAILURE);
}
在 Linux 上,接收超时且没有读到数据时,read() 会返回 -1,errno 为 EAGAIN 或 EWOULDBLOCK。原有的 perror("read") 可以输出错误信息。
UDP 的 connect() 成功代表什么?
这里创建的是 UDP socket:
hints.ai_socktype = SOCK_DGRAM;
connect() 为这个 socket 设置默认通信对端,使后续可以使用 write() 和 read(),并限制接收的对端。它不会执行 TCP 那样的连接握手,也不会验证服务器是否正在运行。
因此:
connect()成功,不代表服务器收到了请求。write()成功,不代表数据报已经送达。- 没有回应时,客户端可能阻塞或超时;某些情况下也可能收到异步网络错误,例如
ECONNREFUSED。
这份 UDP 服务器使用 bind()、recvfrom() 和 sendto() 就能工作,无须增加 TCP 服务器使用的 listen()、accept()。
仍然不通时,按数据流向检查
确认服务器绑定了预期端口
在服务器机器上执行:
ss -lunp
检查 UDP 端口 50000 对应的本地地址:
| 本地地址 | 含义 |
|---|---|
0.0.0.0:50000 | 绑定所有本地 IPv4 地址 |
192.168.1.10:50000 | 绑定指定的局域网 IPv4 地址 |
127.0.0.1:50000 | 只能通过本机 IPv4 回环地址访问 |
[::]:50000 | 绑定 IPv6 通配地址;是否兼容 IPv4 需要另行确认 |
普通用户可能看不到其他用户进程的完整信息,必要时使用 sudo ss -lunp。
检查 UDP 防火墙规则
确认服务器允许客户端访问 50000/udp。只开放 50000/tcp 对这份程序没有帮助。
具体命令取决于机器使用的是 nftables、firewalld、UFW,还是其他规则管理方式。检查实际生效的规则,并按需要允许客户端地址或所在网段访问该 UDP 端口。
抓包确认请求和回应的位置
如果安装了 tcpdump,先在服务器上运行:
sudo tcpdump -ni any 'udp port 50000 or icmp or icmp6'
再从客户端发送 hello,根据抓包结果继续检查:
- 服务器没有捕获到请求:检查客户端目标地址、客户端是否执行了发送,以及中间网络的过滤规则。
- 捕获到请求,但程序没有处理:检查服务器绑定地址、端口和本机防火墙。
- 程序收到了请求,却没有及时回应:检查
getnameinfo()是否阻塞,以及sendto()是否报错。 - 服务器捕获到回应,客户端仍收不到:继续在客户端抓包,检查返回路径和客户端防火墙。
原服务器会直接忽略 recvfrom() 的错误,其他一些错误信息也比较笼统。排查时,应在系统调用失败后及时使用 perror() 输出错误;getaddrinfo() 的错误则继续使用 gai_strerror()。
代码注意事项和相关 manpage
原客户端用 %s 打印收到的缓冲区,依赖数据中存在 '\0'。UDP 数据报不保证以空字符结尾,可以按实际收到的长度输出:
printf("Received %zd bytes: ", nread);
fwrite(buf, 1, (size_t) nread, stdout);
putchar('\n');
这段代码应放在确认 nread >= 0 之后。UDP 也不保证数据报一定送达。如果应用需要可靠传输,应设计超时、重试等机制,或根据需求选择 TCP。
相关接口可以查看本机手册:
man 3 getaddrinfo
man 2 bind
man 2 connect
man 7 udp
man 7 ipv6
man 7 socket
备注:内容仅供参考。
