UNP——原始套接字
1.原始套接字的用处
使用原始套接字可以构造或读取网际层及其以上报文。
具体来说,可以构造 ICMP, IGMP 协议报文,通过开启 IP_HDRINCL 套接字选项,进而自定义 IPv4首部。
2. 创建原始套接字
2.1 使用 SOCK_RAW 创建原始套接字
sockfd = socket(AF_INET, SOCK_RAW, protocol);
protocol 定义在 <netinet/in.h>,如 IPPROTO_xxx。
2.2 初始化原始套接字的常用步骤
2.2.1 IP_HDRINCL
int on = 1; setsockopt(sockfd, IPPOTO_IP, IP_HDRINCL, &on, sizeof(on));
2.2.2 bind, connect
原始套接字不存在端口的概念(TCP报文只是原始套接字报文的数据部分,而非首部),所以调用 bind, connect,分别设置 报文的源地址和目标地址。
对应bind而言,没有启用 IP_HDRINCL 选项,则bind会设置套接字源地址,若没有进行bind,内核会 根据数据包出口设置源地址。
对于connect,若进行connect,则设置套接字对端地址,从而可以用send/write而非sendto。
3. 原始套接字的输出
3.1 sendto 和 send
如果套接字已经设置了对端地址,则可以使用send
3.2 内核对发送数据的处理
(1)没启动 IP_HDRINCL,内核构造IPv4首部,应用进程传来的数据会被当成 IPv4数据部分,内核会将自己构造的首部和应用传来的数据并接,其中IPv4协议号按照 socket 第三个参数即 IPPROTO_XXX 设置。
(2)开启了 IP_HDRINCL,IPv4首部由应用程序构造, 内核会将应用进程传来的数据当成包含IPv4首部和IPv4数据部分的完整IPv4报文,但 IPv4首部的校验和字段总是由内核计算并存储。
即,根据IPROTO_XXX是否启用,传递给原始套接字的数据报可能不完整(不包含IP首部)。
3.3 内核会对超出 MTU 的分组进行分片。
3.4 内核总会对IPv4首部求校验并存储,而对于其他校验如ICMP,TCP,需要自己求的。
4. 原始套接字的输入
原始套接字的输入来自于内核,内核会将哪些数据传给原始套接字呢?
(1)内核不会将TCP和UDP分组传给原始套接字,如果希望获得TCP和UDP分组,必须使用链路层套接字。
(2)大多数的ICMP消息和所有的IGMP消息在内核处理完其中信息后,会传递给原始套接字
(3)内核不认识的协议字段的所有IP数据报将传给原始套接字
(4)如果数据包以分片形式到达,则在数据包完整前,内核不会传给原始套接字
当IP数据报通过上面的筛选,内核会找到匹配该数据包的原始套接字,并将数据包拷贝给该套接字,那么匹配的规则是什么呢?
(1)数据包的协议和套接字的协议匹配
(2)数据包的目的地址和套接字的源地址匹配
(3)数据包的源地址和套接字的目的地址匹配
(4)若套接字的协议和目的地址和源地址都是 void的,即协议设置为0,没有调用 connect 设置目的地址,没有调用bind设置 源地址。则该套接字会接受所有的IP数据包。
从原始套接字,收到的数据报,一定是包含IP首部的完整数据包。
5.实际测试
5.1 ping程序
实际发现 connect 没有。
#include <stdlib.h> #include <stdio.h> #include <sys/time.h> #include <arpa/inet.h> #include "wrap_common.h" #include <string.h> #include <sys/time.h> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <netinet/ip_icmp.h> void proc_v4(char *ptr, ssize_t len, struct timeval *tvrecv); uint16_t in_cksum(uint16_t *addr, int len) { int nleft = len; uint32_t sum = 0; uint16_t *w = addr; uint16_t answer = 0; while (nleft > 1) { sum += *w++; nleft -= 2; } if (nleft == 1) { *(unsigned char *)(&answer) = *(unsigned char *)w ; sum += answer; } /* 4add back carry outs from top 16 bits to low 16 bits */ sum = (sum >> 16) + (sum & 0xffff); /* add hi 16 to low 16 */ sum += (sum >> 16); /* add carry */ answer = ~sum; /* truncate to 16 bits */ return(answer); } struct icmp* get_icmp(int seq) { static struct icmp icmp_buf; static int datalen = 56; int len; icmp_buf.icmp_type = ICMP_ECHO; icmp_buf.icmp_code = 0; icmp_buf.icmp_id = getpid(); icmp_buf.icmp_seq = seq; memset(icmp_buf.icmp_data, 0xa5, datalen); gettimeofday((struct timeval *)icmp_buf.icmp_data, NULL); len = 8 + datalen; icmp_buf.icmp_cksum = 0; icmp_buf.icmp_cksum = in_cksum((unsigned short *)&icmp_buf, len); return &icmp_buf; } int main(int argc, char **argv) { int sockfd; const struct sockaddr_in dst_addr = {0}, addr = {0}, src_addr = {0}; char *dst_ip; struct icmp *icmp; struct timeval tval; int recv_len, i; char buf[128]; dst_ip = argv[1]; sockfd = Socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); Inet_pton(AF_INET, dst_ip, (void *)&dst_addr.sin_addr); for (i = 0; ; i++) { icmp = get_icmp(i); if (sendto(sockfd, icmp, 8+56,0, (struct sockaddr *)&dst_addr, sizeof(struct sockaddr_in)) < 0) perror("sendto"); if ((recv_len = recv(sockfd, &buf, sizeof(buf), 0)) < 0) perror("recv"); gettimeofday(&tval, NULL); proc_v4(buf, recv_len, &tval); sleep(1); } return 0; } void tv_sub(struct timeval *out, struct timeval *in) { if ( (out->tv_usec -= in->tv_usec) < 0 ) { /* out -= in */ --out->tv_sec; out->tv_usec += 1000000; } out->tv_sec -= in->tv_sec; } void proc_v4(char *ptr, ssize_t len, struct timeval *tvrecv) { int hlen1, icmplen; double rtt; struct ip *ip; struct icmp *icmp; struct timeval *tvsend; ip = (struct ip *) ptr; /* start of IP header */ hlen1 = ip->ip_hl << 2; /* length of IP header */ if (ip->ip_p != IPPROTO_ICMP) return; /* not ICMP */ icmp = (struct icmp *) (ptr + hlen1); /* start of ICMP header */ if ( (icmplen = len - hlen1) < 8 ) return; /* malformed packet */ if (icmp->icmp_type == ICMP_ECHOREPLY) { if (icmp->icmp_id != getpid()) { printf("icmp id error : %d != %d \n", icmp->icmp_id, getpid()); return; /* not a response to our ECHO_REQUEST */ } if (icmplen < 16) return; /* not enough data to use */ tvsend = (struct timeval *) icmp->icmp_data; tv_sub(tvrecv, tvsend); rtt = tvrecv->tv_sec * 1000.0 + tvrecv->tv_usec / 1000.0; printf("from %s ", inet_ntoa(ip->ip_src)); printf("%d bytes, seq=%u, ttl=%d, rtt=%.3f ms\n", icmplen, icmp->icmp_seq, ip->ip_ttl, rtt); } }
5.2 使用多进程,和多线程测试
发现只要一个套接字发包,所有套接字都会收到回复,所以在读包时应该将所有包读出。
while ((recv_len = recv(sockfd, &buf, sizeof(buf), MSG_DONTWAIT)) >= 0) { gettimeofday(&tval, NULL); proc_v4(buf, recv_len, &tval); }
5.3 ping程序的recv必须即使处理,才知道接受包时的准确时间。所以应该用多路转接IO,或者专门用个线程来阻塞接受。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?