什么是协议栈
我认为是按照相反的顺序解析数据报,打包数据时候从应用层下到物理层,接受到数据的时候解包从物理层到应用层。
数据从网线上到应用程序的流程如下:
上图表示每一个TCP连接的读写缓冲区大小
实现一个协议栈怎么做?
-
获取最原始的数据(以太网的数据
-
raw socket
-
(本次课采用netmap)旁路(自己写一个driver来获得数据,让原来dirver停止工作)netmap, dpdk(其中driver都是用igx_uio,并不是自己实现的)
注:netmap是一个高效的收发报文的 I/O 框架
-
hook(bpf/ebpf)
-
本文工作
自己实现一个UDP协议栈
报文头格式
udp数据帧
以太网协议头
IP报文格式
首部四位,故最大值为15,表示头部最大长度为 15 * 4 = 60B;
标志位一般缺省
UDP协议头
传输层是不知道应用层的信息,链路层解析时就知道网络层协议,网络层解析报文时就知道传输层的协议,传输层解析报文时不知道应用层协议;
UDP长度位表示UDP整个报文的长度;
协议结构体实现
以太网头、ip头和udp头实现
struct etherhdr {
unsigned char dst_mac[ETHER_ADDR_LEN];
unsigned char src_mac[ETHER_ADDR_LEN];
unsigned short protocol;
};
struct iphdr {
//将8bit用位域进行划分
unsigned char version:4,
hdrlen:4;
unsigned char tos;
unsigned short totlen; // 16bit
unsigned short id;
unsigned short flag:3,
offset:13;
unsigned char ttl;
unsigned char protocol;
unsigned short checksum;
unsigned int sip;
unsigned int dip;
};
struct udphdr {
unsigned short sport;
unsigned short dport;
unsigned short length;
unsigned short check;
};
udp数据包结构体
struct udppkt {
struct etherhdr eth;
struct iphdr ip;
struct udphdr udp;
unsigned char payload[0]; //零长数组,直接使得头后面就是数据数组,即数组名不占空间
};
零长数组一般用于:1长度不确定但可计算出来;2内存已分配出来。
常用在 memory pool中
udp数据报的数据部分不能如下定义
unsigned char *payload; //会占用4B空间,没法达到头部后面直接跟数据的效果
unsigned char payload[65535]; //会出现越界的问题
使用netmap进行接管网卡数据
安装完netmap之后,输入下方代码使其工作:
insmod netmap.ko
此时工作流程如下,网卡数据映射到内存上
c表示
/dev/netmap
为字符设备
int main()
{
//printf("sizeof(struct ether_hdr) = %ld\n", sizeof(struct ether_hdr));
struct nm_pkthdr h;
struct nm_desc *nmr = nm_open("netmap:eth0", NULL, 0, NULL);
if(nmr == NULL) return -1;
struct pollfd pfd = {0};
pfd.fd = nmr->fd; //nmr->指向/dev/netmap的fd
pfd.events = POLLIN;
while(1) {
//第二个参数:用来指定第一个参数数组元素个数
//第三个参数:timeout ,-1等待直到事件发生,0是非阻塞直接返回
int ret = poll(&pfd, 1, -1);
if(ret < 0) continue;
if(pfd.revents & POLLIN) {
//接受网卡上到来的数据包
//会将所有rx环都检查一遍,当发现一个rx环有需要接受的数据包时,
//得到这个包的地址,并返回。所以nm_nextpkt()每次只能取一个数据包。
//返回数据包的地址
unsigned char *stream = nm_nextpkt(nmr, &h);
struct etherhdr *eh = (struct etherhdr *)stream;
if(ntohs(eh->protocol) == PROTO_IP) {
struct udppkt *pkt = (struct udppkt *)stream;
if(pkt->ip.protocol == PROTO_UDP) {
int length = ntohs(pkt->udp.length);
//hdr|data,其中hdr占8B,payload相当于|, pkt->payload[length - 8]相当于数据的末尾
pkt->payload[length - 8] = '\0';
printf("pkt : %s\n", pkt->payload);
}
}
}
}
return 0;
}
对nm_open()的解释:
利用netmap,可以使得网卡直接改道,通过nmap的方式(即DMA方式),直接将数据传输进入内存,也就是接管了网卡的数据,/dev/netmap这个fd的写事件到来就置为可读作为一个“数据到来的信号“,并不从这个文件中读数据
read:一般指从外存读到内存
杂项
- 客户端宕机,服务端send返回什么?
send本身返回成功,send把用户态数据拷贝到内核空间
-
七层网络模型
-
以太网 ether
-
ip协议
-
icmp ,ping 用到的
-
arp -a,arp攻击(不管是谁的arp请求都回复,导致arp表比较混乱)
-
网卡不工作在七层模型中,只做A/D and D/A转换
-
如何高效的发送tcp报文?
1. **到底在网络上发多少** 1. 通过往返时间确定有没有超时,从而放缓发送 1. 超时计算 rtt_new = 0.9 * rtt_old + 0.1rtt; 2. 确定包 **i. 慢启动,拥塞控制**
- 能够接收多少
- TCP的window size字段
- 能够接收多少
8.一个面试题:
若对方一直不recv()数据,对方发来的报文中window size最后变为0,导致本方send()返回-1,表示不能发了
问题
- 发送数据给eth0的ip一段时间后就不能发送:
观察arp表,发现数据接受不到的时候,arp表中eth0对应的ip项就没了。问题在于发送数据的时候也有发arp的请求,但是我们的接受程序没有对arp请求处理。所以我们需要实现arp - 不能ping通,是因为icmp协议没实现
- dpdk方式待了解