PAYATHON 2026

如何从同一网络中的另一台机器连接自己的 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

备注:内容仅供参考。