协议栈
协议栈,指的是TCP/IP协议栈。linux系统中,协议栈是内核实现的。
协议,是通信双方对包格式的一种约定。
为什么是栈呢?因为对于包的组织,类似于栈的数据结构。发送端组织包的顺序是应用层->传输层->网络层->数据链路层,之后通过网卡将数字信号转换成光电信号,发送给接收端;接收端的网卡将光电信号转换成数字信号,解包的顺序是数据链路层->网络层->传输层->应用层。

如何拿到最原始的数据?
- raw socket,socket的第二个参数,可以设置SOCK_STREAM,SOCK_DGRAM. SOCK_RAW就可以拿到以太网数据。tcpdump、wireshark就是利用这种方法。
- netmap
- dpdk
网卡的作用
Client发送数据给server,数据首先到达网卡,经过两步到达应用程序
1)将数据从网卡的内存copy到内核协议栈,内核协议栈对数据包进行解析;
2)应用程序通过调用recv函数,将数据从内核copy进用户空间,得到应用层的数据包。
网卡的作用,接收的时候,是将光电信号转换成数字信号;发送的时候,将数字信号转换成光电信号。
什么是用户态协议栈?
就是将协议栈,做到应用程序。为什么要这么做呢?减少了一次数据copy的过程,绕过内核,数据可以直接从网卡copy到应用程序,对于性能会有很大的提升。

为什么要有用户态协议栈呢?
是为了解决C10M的问题。
之前说过C10K的问题,使用epoll可以解决C10K的问题。现在epoll已经可以支持两三百万的并发了。
什么是C10M问题?
实现10M(即1千万)的并发连接挑战意味着什么:(网上找的)
1)1千万的并发连接数;
2)100万个连接/秒:每个连接以这个速率持续约10秒;
3)10GB/秒的连接:快速连接到互联网;
4)1千万个数据包/秒:据估计目前的服务器每秒处理50K数据包,以后会更多;
5)10微秒的延迟:可扩展服务器也许可以处理这个规模(但延迟可能会飙升);
6)10微秒的抖动:限制最大延迟;
7)并发10核技术:软件应支持更多核的服务器(通常情况下,软件能轻松扩展到四核,服务器可以扩展到更多核,因此需要重写软件,以支持更多核的服务器).
我们来计算一下,单机承载1000万连接,需要的硬件资源:
内存:1个连接,大概需要4k recvbuffer,4k sendbuffer,一共需要10M * 8k = 80G
CPU:10M 除以 50K = 200核
只是支持这么多连接,还没有做其他事情,就需要这么多的资源,如果在加上其他的限制,加上业务的处理,资源肯定会更多。使用用户态协议栈,可以减少一次数据的copy,可以节省很大一部分资源。
要实现用户态协议栈,很关键的一个问题,是网络数据怎么才能绕过内核,直接到达用户空间?netmap、dpdk为用户态协议栈的实现,提供了可能。
这次我们使用了netmap实现用户态协议栈,后面会介绍dpdk。
netmap原理
netmap主要利用了mmap,将网卡中数据,直接映射到内存。netmap直接接管网卡数据,可以绕过内核协议栈。我们直接在应用程序中实现协议栈,对协议进行解析,就可以获取到网络数据了。
零拷贝,使用的是mmap方式,本质是DMA的方式,不需要CPU参与。普通copy,从磁盘copy数据到内存,需要CPU的move指令。sendfile使用的是mmap方式。**零拷贝主要是说CPU有没有参与,而不是说有没有copy。**是由主板上的DMA芯片将外设的数据copy到内存。

netmap可以在github上下载,按照上面的readme编译安装,使用比较方便。
https://github.com/luigirizzo/netmap
利用netmap实现用户态协议栈
以太网头定义

typedef struct _ethhdr {
unsigned char h_dst[ETH_ADDR_LENGTH];
unsigned char h_src[ETH_ADDR_LENGTH];
unsigned short h_proto;
} ethhdr;
IP头定义

typedef struct _iphdr {
unsigned char hdrlen:4, // ip头长度,最大15*4=60字节
version:4;
unsigned char tos;
unsigned short length;
unsigned short id;
unsigned short flag_offset;
unsigned char ttl; // time to live ping的ttl就是ip头里面的ttl
unsigned char type;
unsigned short check;
unsigned int sip;
unsigned int dip;
} iphdr;
UDP头定义

UDP比TCP要简单很多,没有序列号,无法保证消息必达;也做不了重传;没有拥塞控制。
typedef struct _udphdr {
unsigned short sport;
unsigned short dport;
unsigned short length;
unsigned short check;
} udphdr;
UDP包定义
UDP包组成:以太网头 + IP头 + UDP头 + UDP数据。
以太网头、IP头、UDP头都已经定义好了,这里有一个问题,UDP数据怎么定义?
用指针是不合适的,这里引入了零长数组,也叫柔性数组。
typedef struct _udppkt {
ethhdr eh; // 14
iphdr ip; // 20
udphdr udp; // 8
unsigned char data[0];
} udppkt;
柔性数组的好处,是不占空间,只是占了一个位置,作用相当于标签。
零长数组使用条件:
- 不关心长度,可以通过某种方法计算出它的长度,比如通过udp头的length能够计算出来用户数据的长度;
- 它的内存是提前分配好的,不会越界。
因为4字节对齐的关系,sizeof(udppkt)的结果是44,所以需要使用单字节对齐,结果就是42。
#pragma pack(1)
使用netmap处理UDP包
nm_open主要做了两个事情:
- 把网卡的内存映射到内存;
- 把fd指向eth0对应的设备文件。
通过检测fd,就可以判断网卡是否有数据,有数据就可以直接操作内存。

如果来了一个数据,如果数据很多,网卡需要进行模拟信号和数字信号转换,DMA也需要不断把数据映射到内存,怎么把多个数据包给组织起来?比如一下来100个包,怎么把这100个包组织起来?
使用ringbuff。
对于大量数据,从网卡将数据取到内存中,CPU有两种做法:
- 轮询
- 事件
对于大量数据,使用轮询方式比较好。这就是网络这一层,从网卡里面取数据的两种方法。事件的方式针对稀疏型的数据。
nm_nextpkt是操作内存,取出来的就是一个完整的包。
之后通过去掉以太网头,IP头,UDP头,得到用户数据。

int main() {
struct nm_pkthdr h; // ringbuff的指针
struct nm_desc *nmr = nm_open("netmap:eth1", NULL, 0, NULL);
if (nmr == NULL) return -1;
struct pollfd pfd = {
0};
pfd.fd = nmr->fd;
pfd.events = POLLIN;
while (1) {
int ret = poll(&pfd, 1, -1);
if (ret < 0) continue;
if (pfd.events & POLLIN) {
unsigned char *stream = nm_nextpkt(nmr, &h); // ringbuff
ethhdr *eh = (ethhdr*)stream;
if (ntohs(eh->h_proto) == PROTO_IP) {
udppkt *udp = (udppkt*)stream;
if (udp->ip.type == PROTO_UDP)

本文介绍了用户态协议栈的概念,以及为何需要它来解决C10M问题。通过使用Netmap库,可以绕过内核协议栈,减少数据拷贝,提高性能。详细讲解了Netmap的工作原理,如利用mmap实现零拷贝,以及如何处理UDP和ARP包。还展示了利用Netmap实现用户态协议栈的代码示例,包括解析和注入网络包。最后提到了ARP攻击的可能性及防范措施。
最低0.47元/天 解锁文章
1440





