如何不绑定端口使用WSASendTo/WSASendMsg发送指定端口UDP数据报
Windows下从不绑定的指定端口发送UDP数据报(WSASendTo实现)
要实现从不绑定的指定端口(即使该端口已被其他套接字占用)发送UDP数据报,核心思路是直接构造包含自定义源端口的IP和UDP头部,通过原始套接字发送,绕过系统自动处理的端口绑定逻辑。以下是具体实现步骤和代码示例:
关键前提
- 程序必须以管理员权限运行(Windows下创建原始套接字需要该权限)。
- 需要自行构造IP头部和UDP头部,包括计算正确的校验和。
实现步骤
- 初始化Winsock:调用
WSAStartup初始化套接字环境。 - 创建原始套接字:创建类型为
SOCK_RAW、协议为IPPROTO_UDP的套接字,用于手动构造IP和UDP头部。 - 启用IP_HDRINCL选项:设置套接字选项
IP_HDRINCL,告知系统我们将自行构造IP头部,而非由系统自动添加。 - 构造IP和UDP头部:手动填写IP头部的源IP、目标IP等字段,UDP头部的源端口(指定的目标端口)、目标端口等字段,并计算IP头部校验和与UDP校验和。
- 发送数据包:使用
WSASendTo发送构造好的完整数据包(IP头部 + UDP头部 + 数据内容)。
代码示例
#include <winsock2.h> #include <ws2tcpip.h> #include <stdio.h> #include <stdlib.h> #pragma comment(lib, "ws2_32.lib") // IP头部结构体 typedef struct ip_hdr { unsigned char ip_hl:4, ip_v:4; // 版本和头部长度 unsigned char ip_tos; // 服务类型 unsigned short ip_len; // 总长度 unsigned short ip_id; // 标识 unsigned short ip_off; // 分片偏移 unsigned char ip_ttl; // 生存时间 unsigned char ip_p; // 协议 unsigned short ip_sum; // 校验和 struct in_addr ip_src, ip_dst; // 源IP和目标IP } IP_HDR; // UDP头部结构体 typedef struct udp_hdr { unsigned short uh_sport; // 源端口 unsigned short uh_dport; // 目标端口 unsigned short uh_ulen; // UDP长度 unsigned short uh_sum; // 校验和 } UDP_HDR; // 计算校验和的辅助函数 unsigned short checksum(unsigned short *buf, int len) { unsigned long sum = 0; while (len > 1) { sum += *buf++; len -= 2; } if (len == 1) { sum += *(unsigned char*)buf; } sum = (sum >> 16) + (sum & 0xFFFF); sum += (sum >> 16); return (unsigned short)(~sum); } int main() { WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { printf("WSAStartup failed\n"); return 1; } // 创建原始套接字 SOCKET sock = socket(AF_INET, SOCK_RAW, IPPROTO_UDP); if (sock == INVALID_SOCKET) { printf("socket failed: %d\n", WSAGetLastError()); WSACleanup(); return 1; } // 设置IP_HDRINCL选项,自行构造IP头部 BOOL optval = TRUE; if (setsockopt(sock, IPPROTO_IP, IP_HDRINCL, (char*)&optval, sizeof(optval)) == SOCKET_ERROR) { printf("setsockopt IP_HDRINCL failed: %d\n", WSAGetLastError()); closesocket(sock); WSACleanup(); return 1; } // 构造目标地址 sockaddr_in dest_addr; memset(&dest_addr, 0, sizeof(dest_addr)); dest_addr.sin_family = AF_INET; dest_addr.sin_port = htons(8080); // 目标端口 inet_pton(AF_INET, "192.168.1.100", &dest_addr.sin_addr); // 目标IP // 构造IP头部 IP_HDR ip_hdr; memset(&ip_hdr, 0, sizeof(ip_hdr)); ip_hdr.ip_v = 4; ip_hdr.ip_hl = sizeof(IP_HDR) / 4; ip_hdr.ip_tos = 0; ip_hdr.ip_len = htons(sizeof(IP_HDR) + sizeof(UDP_HDR) + 10); // 总长度:IP头+UDP头+数据长度(这里数据长度10) ip_hdr.ip_id = htons(12345); ip_hdr.ip_off = 0; ip_hdr.ip_ttl = 64; ip_hdr.ip_p = IPPROTO_UDP; inet_pton(AF_INET, "192.168.1.50", &ip_hdr.ip_src); // 源IP ip_hdr.ip_dst = dest_addr.sin_addr; ip_hdr.ip_sum = checksum((unsigned short*)&ip_hdr, sizeof(IP_HDR)); // 构造UDP头部 UDP_HDR udp_hdr; memset(&udp_hdr, 0, sizeof(udp_hdr)); udp_hdr.uh_sport = htons(9999); // 指定的源端口(即使已被占用) udp_hdr.uh_dport = htons(8080); udp_hdr.uh_ulen = htons(sizeof(UDP_HDR) + 10); // UDP长度:UDP头+数据长度 // 构造UDP伪头部(用于计算UDP校验和) struct { struct in_addr src_ip; struct in_addr dst_ip; unsigned char zero; unsigned char proto; unsigned short udp_len; } udp_pseudo_hdr; udp_pseudo_hdr.src_ip = ip_hdr.ip_src; udp_pseudo_hdr.dst_ip = ip_hdr.ip_dst; udp_pseudo_hdr.zero = 0; udp_pseudo_hdr.proto = IPPROTO_UDP; udp_pseudo_hdr.udp_len = udp_hdr.uh_ulen; // 拼接伪头部、UDP头部和数据,计算校验和 char udp_checksum_buf[sizeof(udp_pseudo_hdr) + sizeof(UDP_HDR) + 10]; memcpy(udp_checksum_buf, &udp_pseudo_hdr, sizeof(udp_pseudo_hdr)); memcpy(udp_checksum_buf + sizeof(udp_pseudo_hdr), &udp_hdr, sizeof(UDP_HDR)); const char* data = "testdata12"; // 10字节数据 memcpy(udp_checksum_buf + sizeof(udp_pseudo_hdr) + sizeof(UDP_HDR), data, 10); udp_hdr.uh_sum = checksum((unsigned short*)udp_checksum_buf, sizeof(udp_checksum_buf)); // 拼接完整数据包:IP头 + UDP头 + 数据 char packet[sizeof(IP_HDR) + sizeof(UDP_HDR) + 10]; memcpy(packet, &ip_hdr, sizeof(IP_HDR)); memcpy(packet + sizeof(IP_HDR), &udp_hdr, sizeof(UDP_HDR)); memcpy(packet + sizeof(IP_HDR) + sizeof(UDP_HDR), data, 10); // 发送数据包 int send_len = WSASendTo(sock, (LPWSABUF)&packet, 1, NULL, 0, (SOCKADDR*)&dest_addr, sizeof(dest_addr), NULL, NULL); if (send_len == SOCKET_ERROR) { printf("WSASendTo failed: %d\n", WSAGetLastError()); } else { printf("Packet sent successfully\n"); } closesocket(sock); WSACleanup(); return 0; }
注意事项
- 校验和计算:IP头部和UDP头部的校验和必须准确,否则数据包会被网络设备或目标主机丢弃。UDP校验和计算需要借助伪头部,包含源IP、目标IP、协议类型和UDP长度。
- 权限问题:Windows下创建
SOCK_RAW套接字必须以管理员身份运行程序,否则会创建失败。 - 网络兼容性:部分防火墙或网络设备可能拦截原始套接字发送的数据包,需要确保网络环境允许此类流量。
- 端口冲突风险:这种方式绕过了系统的端口绑定机制,可能导致端口复用引发的网络异常,仅在特殊场景下使用。
内容的提问来源于stack exchange,提问作者Vega4
相关产品推荐
相关产品推荐

