tcpping(也常被称为 tcping)是一个网络诊断工具,它的功能类似于 ping,但使用的是 TCP 协议而不是 ICMP 协议,这对于那些防火墙可能阻止 ICMP 流量但允许 TCP 流量的网络环境非常有用。

(图片来源网络,侵删)
tcpping 的基本工作原理是:
- 尝试与目标主机的指定 TCP 端口建立一个 TCP 连接。
- 如果连接成功,说明目标端口是开放的,并记录连接建立所花费的时间(RTT - Round-Trip Time),然后立即关闭连接。
- 如果连接失败(目标主机无响应、端口被过滤或拒绝),则记录失败信息。
- 重复以上步骤,就像
ping一样发送多个“探测包”。
核心技术点
要实现一个 tcpping,我们需要掌握以下几个 C 语言和网络编程的关键点:
socket(): 创建一个套接字,这是进行网络通信的端点。struct sockaddr_in: 用于存储 IPv4 地址信息的结构体,包括 IP 地址和端口号。inet_addr()或inet_pton(): 将点分十进制的 IP 地址字符串(如 "8.8.8.8")转换为网络字节序的二进制格式。htons(): 将端口号从主机字节序转换为网络字节序,网络协议栈要求端口号必须使用网络字节序。connect(): 尝试与指定的 IP 地址和端口建立 TCP 连接,这是tcpping的核心函数。gettimeofday(): 获取高精度的时间戳,用于精确计算连接建立所需的时间。close(): 关闭套接字,释放资源。#include <sys/socket.h>,#include <netinet/in.h>, `#include <arpa/inet.h>,#include <unistd.h>,#include <sys/time.h>: 这些是实现网络编程所必需的头文件。
完整 C 语言代码示例
下面是一个功能完整的 tcpping 实现,它支持命令行参数,并会持续发送 TCP 探测包,直到被用户中断(如 Ctrl+C)。
// tcpping.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include <signal.h>
#include <errno.h>
// 全局变量,用于控制主循环
volatile sig_atomic_t keep_running = 1;
// 信号处理函数,当用户按下 Ctrl+C 时,设置 keep_running 为 0
void handle_sigint(int sig) {
(void)sig; // 避免未使用参数的警告
keep_running = 0;
}
// 打印使用说明
void print_usage(const char *prog_name) {
fprintf(stderr, "Usage: %s <host> <port>\n", prog_name);
fprintf(stderr, "Example: %s google.com 80\n", prog_name);
}
int main(int argc, char *argv[]) {
if (argc != 3) {
print_usage(argv[0]);
return 1;
}
const char *hostname = argv[1];
const char *port_str = argv[2];
int port = atoi(port_str);
if (port <= 0 || port > 65535) {
fprintf(stderr, "Error: Invalid port number %s\n", port_str);
return 1;
}
// 注册信号处理函数
struct sigaction sa;
sa.sa_handler = handle_sigint;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction");
return 1;
}
printf("TCPing %s on port %d...\n", hostname, port);
printf("Press Ctrl+C to stop.\n");
while (keep_running) {
int sock = -1;
struct sockaddr_in server_addr;
struct timeval start_time, end_time;
long long elapsed_us;
// 1. 创建套接字
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("socket");
break; // 创建失败,退出循环
}
// 2. 设置服务器地址信息
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
// 将主机名转换为IP地址
if (inet_pton(AF_INET, hostname, &server_addr.sin_addr) <= 0) {
// 如果不是有效的 IPv4 地址,尝试通过 DNS 解析
struct hostent *host = gethostbyname(hostname);
if (host == NULL) {
fprintf(stderr, "Error: Unknown host %s\n", hostname);
close(sock);
break;
}
memcpy(&server_addr.sin_addr, host->h_addr, host->h_length);
}
// 3. 记录开始时间
gettimeofday(&start_time, NULL);
// 4. 尝试连接
int connect_result = connect(sock, (struct sockaddr *)&server_addr, sizeof(server_addr));
// 5. 记录结束时间
gettimeofday(&end_time, NULL);
// 6. 计算耗时
elapsed_us = (end_time.tv_sec - start_time.tv_sec) * 1000000LL + (end_time.tv_usec - start_time.tv_usec);
// 7. 输出结果
if (connect_result == 0) {
// 连接成功
printf("Connected to %s:%d in %lld ms\n", hostname, port, elapsed_us / 1000);
} else {
// 连接失败
printf("Failed to connect to %s:%d. Error: %s (%lld ms)\n", hostname, port, strerror(errno), elapsed_us / 1000);
}
// 8. 关闭套接字
close(sock);
// 9. 等待 1 秒,避免过于频繁的探测
sleep(1);
}
printf("\nTCPing finished.\n");
return 0;
}
代码详解
-
信号处理 (
handle_sigint,sigaction):
(图片来源网络,侵删)volatile sig_atomic_t keep_running = 1;定义了一个原子变量,用于控制while循环。handle_sigint函数在接收到SIGINT信号(通常是Ctrl+C)时被调用,它将keep_running设为 0,从而优雅地终止主循环。- 这使得程序可以被用户正常中断,而不是被强制杀死。
-
命令行参数解析 (
main函数开头):- 检查
argc是否为 3,如果不是,打印用法并退出。 - 使用
atoi()将端口号字符串转换为整数,并进行简单的有效性检查。
- 检查
-
主循环 (
while (keep_running)):- 循环会一直执行,直到
keep_running变为 0。
- 循环会一直执行,直到
-
创建套接字 (
socket):AF_INET指定使用 IPv4 协议。SOCK_STREAM指定使用 TCP 协议。- 第三个参数通常为 0,表示让系统自动选择合适的协议。
-
设置服务器地址 (
struct sockaddr_in):memset将结构体清零,这是一个好习惯。server_addr.sin_family = AF_INET;设置地址族。htons(port)将端口号转换为网络字节序。inet_pton()是一个现代的、更安全的函数,用于将 IP 地址字符串(如 "192.168.1.1")转换为二进制格式,如果传入的是域名(如 "google.com"),inet_pton会失败,此时我们回退到gethostbyname进行 DNS 解析。
-
记录时间 (
gettimeofday):- 在
connect()调用之前和之后分别获取时间戳。 gettimeofday返回秒和微秒,我们手动计算总微秒数,然后除以 1000 得到毫秒,这是更直观的单位。
- 在
-
连接与结果处理 (
connect):connect()是阻塞调用,它会一直等待直到连接建立或失败。- 如果返回值为 0,表示连接成功。
- 如果返回值为 -1,表示连接失败,
errno会被设置成具体的错误码(如ECONNREFUSED连接被拒绝,ETIMEDOUT连接超时),我们可以用strerror(errno)打印出可读的错误信息。
-
清理 (
close,sleep):close(sock)关闭套接字,释放系统资源。sleep(1)让程序暂停 1 秒,避免对目标服务器造成过大压力,也模拟了ping的间隔。
如何编译和运行
-
保存代码: 将上面的代码保存为
tcpping.c。 -
编译: 使用 GCC 编译器进行编译。
- 在 Linux 或 macOS 终端中运行:
gcc tcpping.c -o tcpping
- 在 Windows (使用 MinGW 或 WSL) 中,命令类似。
- 在 Linux 或 macOS 终端中运行:
-
运行:
- 探测一个 IP 地址和端口 (探测百度网站的 80 端口):
./tcpping 180.101.49.12 80
- 探测一个域名和端口 (探测 Google 的 443 端口):
./tcpping google.com 443
- 按
Ctrl+C停止。
- 探测一个 IP 地址和端口 (探测百度网站的 80 端口):
预期输出示例:
TCPing google.com on port 443...
Press Ctrl+C to stop.
Failed to connect to google.com:443. Error: Connection timed out (0 ms)
Failed to connect to google.com:443. Error: Connection timed out (0 ms)
Connected to google.com:443 in 42 ms
Connected to google.com:443 in 38 ms
^C
TCPing finished.
(注意:第一次连接可能会失败,因为 TCP 三次握手需要时间,或者中间路由器有延迟。)
扩展功能
这个基础版本已经实现了核心功能,但还可以进一步扩展:
- 超时设置: 使用
setsockopt()和SO_RCVTIMEO/SO_SNDTIMEO来为connect()调用设置超时,避免无限等待。 - 命令行选项: 使用
getopt库来支持更多命令行选项,-c <count>: 发送指定次数后退出,而不是无限循环。-i <interval>: 自定义探测间隔时间(秒)。-t <timeout>: 自定义连接超时时间(毫秒)。
- ICMP 回显: 对于无法连接的情况,可以尝试发送一个 ICMP Echo Request (真正的
ping) 作为补充信息。 - IPv6 支持: 修改代码以支持
AF_INET6,使其能够探测 IPv6 地址。
