网络编程
# 网络中的进程通信
操作系统为本地进程通信提供了相应设施
UNIX BSD有:管道(pipe)、命名管道(named pipe)软中断信号(signal)
UNIX system V有:消息(message)、共享存储区(shared memory)和信号量(semaphore)等.
首先要解决的是网间进程标识问题。同一主机上,不同进程可用进程号(process ID)唯一标识,TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了。
TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议,TCP/IP协议存在于OS中,网络服务通过OS提供,在OS中增加支持TCP/IP的系统调用——Berkeley套接字,如Socket,Connect,Send,Recv等
UDP(User Data Protocol,用户数据报协议)是与TCP相对应的协议。它是属于TCP/IP协议族中的一种
TCP/IP协议族包括运输层、网络层、链路层,而socket所在位置如图,Socket是应用层与TCP/IP协议族通信的中间软件抽象层。
# socket
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中属于门面模式,,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。
# socket接口函数
# socket
#include<sys/socket.h>
int socket(int protofamily, int type, int protocol);//返回sockfd描述符
2
对应普通文件的打开操作,他创建一个socket描述符,唯一标识一个socket,作为参数可以进行一些读写操作
protofamily
:即协议域,又称为协议族(family)。 常用的协议族有,AF_INET(IPV4)、AF_INET6(IPV6)、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。 协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。type
:指定socket类型。 常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等。protocol
:指定协议。 常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议
并不是type和protocol可以随意组合的,protocol为0时会自动选择type对应的默认协议
socket缓冲区
- 每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区
- I/O缓冲区在每个TCP套接字中单独存在;
- 即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
- 关闭套接字将丢失输入缓冲区中的数据。
- 通过 getsockopt() 函数获取缓冲区的默认大小,一般都是 8K
unsigned optVal;
int optLen = sizeof(int);
getsockopt(servSock, SOL_SOCKET, SO_SNDBUF, (char*)&optVal, &optLen);
printf("Buffer length: %d\\n", optVal);
2
3
4
# bind
调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想把一个地址族中的特定地址赋给socket,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd
:即socket描述字addr
:指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同addrlen
:对应的是地址的长度,即sizeof(addr)
struct sockaddr_in{
sa_family_t sin_family; //地址族,和socket函数传入的协议族是一样的
uint16_t sin_port; //16位TCP/UDP端口号
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //不使用
}
struct in_addr{
In_addr_t s_addr;//32位IPv4地址
}
2
3
4
5
6
7
8
9
通常服务器在启动的时候会绑定一个地址,用于提供服务,客户端就可以通过他来连接服务器,而客户端就不用指定,系统自动分配端口号和ip地址的组合(connect时系统随机生成一个)
在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian 主机字节序就是我们平常说的大端和小端模式:
a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
b) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。 网络字节序: 4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。
#inclde<arpa/inet.h>
uint32_t htonl(uint32_t hostlong)
将一个32位数从主机字节顺序转换成网络字节顺序。
uint16_t htons(uint16_t hostlong)
将一个16位数从主机字节顺序转换成网络字节顺序
uint16_t ntohs(uint16_t hostlong)
将一个16位数由网络字节顺序转换为主机字节顺序
uint32_t ntohs(uint32_t hostlong)
uint32_t ntohs(uint32_t hostlong)
例子:
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//让系统自动获取本机的IP地址。
servaddr.sin_port = htons(8000);//设置端口号
//将本地地址绑定到所创建的套接字上
if( bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){
printf("bind socket error: %s(errno: %d)\\n",strerror(errno),errno);
exit(0);
}
2
3
4
5
6
7
8
9
10
其中一些宏:
INADDR_ANY
转换过来就是0.0.0.0,表示本机的所有IP,多网卡的情况下,这个就表示所有网卡ip地址的意思。
比如一台电脑有3块网卡,对应3个ip地址了。如果绑定某个具体的ip地址,你只能监听你所设置的ip地址所在的网卡的端口,其它两块网卡无法监听端口,如果我需要三个网卡都监听,那就需要绑定3个ip,
为此出现INADDR_ANY,你只需绑定INADDR_ANY,管理一个套接字就行,不管数据是从哪个网卡过来的,只要是绑定的端口号过来的数据,都可以接收到。
# listen
int listen(int sockfd, int backlog);
sockfd
:socket描述字
backlog
:可以排队的最大连接个数
将套接字( sockfd )变成被动的连接监听套接字(被动等待客户端的连接),相当于开启一个被动等待的状态
并不阻塞,只是将该套接字和套接字对应的连接队列长度告诉 Linux 内核
# connect
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
客户端调用connect向服务端发出连接请求,通过三次握手建立TCP连接
sockfd
:socket描述字
addr
:服务器地址
addrlen
:为socket地址的长度
返回一个连接的socket描述字
这个过程由内核自动完成三次握手,默认一直阻塞直到连接上
这个过程也会被select函数接收为读操作
**底层实现:**从established状态的连接队列头部取出一个已经完成的连接,如果没有则一直阻塞。同样,如果队列满了客户端继续发出连接请求,并不会拒绝,只会延迟
connetc之前的数据转换:
int inet_aton(const char *cp, struct in_addr *inp);
转换网络主机地址ip(如192.168.1.10)为二进制数值,储存在结构体inp中
返回0表示成功,否则表示主机地址无效
(转换完,还需要调用函数将主机字节顺序转为网络字节顺序)
in_addr_t inet_addr(const char \*cp);
转换网络主机地址ip(如192.168.1.10)为二进制数值
如果参数无效返回-1,255.255.255.255也会返回-1
char \*inet_ntoa(struct in_addr in);
转换网络字节排序的地址为标准的ASCII以点分开的地址,该函数返回指向点分开的字符串地址(如192.168.1.10)的指针
#include <arpe/inet.h>
int inet_pton(int family, const char *strptr, void *addrptr);
将点分十进制的ip地址转化为用于网络传输的数值格式
返回值:若成功则为1,若输入不是有效的表达式则为0,若出错则为-1
const char * inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
将数值格式转化为点分十进制的ip地址格式
返回值:若成功则为指向结构的指针,若出错则为NULL
# accept
通过三次握手建立TCP连接
服务器用于监听指定的socket地址,TCP服务器监听到客户端的一个请求就会调用accept函数接收请求。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //返回连接connect_fd
sockfd
:socket描述字
addr
:结果参数,用于接收一个返回值,返回客户端的地址,可为NULL
addrlen
:结果参数,用于接收上诉addr的结构的大小
成功返回表示连接建立,返回与客户通信的套接字
注意:accept默认会阻塞进程,直到有个客户建立连接后返回一个新可用的连接套接字
int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, 标志符); //返回连接connect_fd
标志符
:
- 0,和accept没有区别
- SOCK_CLOEXEC,当用exec创建进程时,能自动先关闭当前进程已经有的文件描述符
# read、write等操作函数
建立号连接关系就可以调用网络I/O进行读写操作
- read/write
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
2
3
write成功返回,**只是buf中的数据被复制到了kernel中的TCP发送缓冲区。**至于数据什么时候被发往网络以及后面的内容,无法保证
只有每个socket的发送缓冲区满时会阻塞()每个socket有自己的发送接收缓冲区
- send/recv
#include <sys/types.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
2
3
TCP数据读写部分常用,失败返回-1,设置errno
- sendro/recvfrom
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
2
UDP常用的读写,UDP
通信没有连接的概念,所以我们读取数据都需要获取发送端的socket
地址,这里的flags
参数和上面的表格相同,失败返回-1
讲最后两个参数设置NULL也可以用于面向连接的socket数据读写
- sendmsg/recvmsg
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
2
通用数据读写
struct msghdr {
void* msg_name;// socket地址,面向连接的socket这里必须为NULL
socklen_t msg_namelen;// socket地址的长度
struct iovec* msg_iov;// 分散的内存块
int msg_iovlen;// 分散内存块的数量
void* msg_control;// 指向辅助数据的起始位置
socklen_t msg_controllen;// 辅助数据的大小
int msg_flags;// 复制函数中的flags参数,并且在调用过程当中更新
};
struct iovec {
void *iov_base; // 内存起始地址
size_t iov_len; // 这块内存的长度
}
2
3
4
5
6
7
8
9
10
11
12
13
TCP阻塞模式:
对于TCP套接字(默认情况下),当使用 write()/send() 发送数据时:
- 检查缓冲区大小,不够时阻塞,直到有空间才唤醒
- TCP协议在向网络发送数据时不能写入,发完后唤醒write/send函数
- 如果要写入的数据大于缓冲区的最大长度,那么将分批写入。
- 所有数据都写入缓冲区才返回
当使用 read()/recv() 读取数据时:
- 检查缓冲区,有数据则读,否则阻塞
- 要读的数据长度小于缓冲区长度,则剩余数据会不断积压直到再次读
- 读到数据后才返回,否则阻塞
# close函数
#include <unistd.h>
int close(int fd);
2
close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。
int inet_pton(int af, const char *src, void *dst);//转换字符串到网络地址:
inet_pton 是Linux下IP地址转换函数,可以在将IP地址在“点分十进制”和“整数”之间转换 ,是inet_addr的扩展。
第一个参数af是地址族,转换后存在dst中
af = AF_INET:src为指向字符型的地址,即ASCII的地址的首地址(ddd.ddd.ddd.ddd格式的),函数将该地址转换为in_addr的结构体,并复制在*dst中
af =AF_INET6:src为指向IPV6的地址,函数将该地址转换为in6_addr的结构体,并复制在*dst中
如果函数出错将返回一个负值,并将errno设置为EAFNOSUPPORT,如果参数af指定的地址族和src格式不对,函数将返回0。
# 阻塞和非阻塞模式下的read/write
默认为阻塞模式,通常的编程中阻塞代价较大,因此常采用不阻塞的方式
read函数:
- 阻塞模式下,没有数据阻塞,
- 非阻塞模式下,没有数据返回-1
write函数:
- 阻塞模式下,缓冲区写入全部数据时才返回,
- 阻塞模式下,收到socket的关闭,会将剩余缓冲区填满并返回所写字节数,再次调用write则写入失败
- 非阻塞模式下,返回能写入的字节数
# 对连接异常的处理
内核通过socket的read/write将双方的连接异常通知到应用层
对于收到对方发来的RST,无论读写都会立即停止
对于读:
进程收到FIN,则调用read函数将数据清空,如果正阻塞在read函数上(即缓冲区为空),则唤醒进程,read调用立即返回EOF
如果对面操作系统崩溃了,无法收到FIN,那么就会一直阻塞。如果是先write再read的,则之前的write收不到ACK,多次重传后,阻塞的read调用会返回错误
(当对面不可达时,如果做了一次write,而不是轮询或阻塞在read上,那么在重传的周期内能检测出错误)
对于写:
- 依然可以write(收到FIN只意味着对方不会再发 消息),如果对方发了FIN开始四次挥手,此时对方收到write的数据会回ACK但并不会交付给上层应用。
- 如果这边是用循环读写的方式,需要这边调用close函数,才会开始第三次挥手,如果迟迟不调用close,对方因收不到第三次挥手而终止,此时write函数发过去会收到RST,我方收到RST后,执行的读写函数都会返回错误。(socket对对方连接的中止感知能力有限)
# socket原理
服务器与客户端通信建立的流程:服务器调用accept
,等待连接请求到达监听套接字sockfd
(假设为3,会一直在服务器生命周期内存在),客户端调用connect
函数,发送请求到监听套接字sockfd
,服务器的accept
内打开一个新的已连接套接字(假设为4,只服务这次连接),在客户端和服务器间建立了连接,accept
返回给服务器,connect
返回给客户端,此时客户端和服务器端就可以通过套接字传送数据了
# 套接字
是一个整数,0、1、2对应的FILE *结构的表示就是stdin、stdout、stderr,即标准输入、标准输出、标准错误输出
当应用程序要为因特网通信而创建一个套接字(socket)时,操作系统就返回一个小整数作为描述符(descriptor)来标识这个套接字
linux下socket为文件,申请一个套接字就是打开socket文件,文件描述表中这个文件的文件描述符对应一个套接字
4元组来表示(clientip:clientport, serverip:serverport)
clientip 是客户端的IP地址,clientport 是客户端的端口,serverip 是服务器的IP地址,而 serverport 是服务器的端口。
套接字的内部数据结构包含很多字段,但是系统创建套接字后,大多数字字段没有填写。应用程序创建套接字后在该套接字可以使用之前,必须调用其他的进程来填充这些字段。
监听套接字: 监听套接字正如accept函数的参数sockfd,由listen函数将一个主动套接字转化为监听套接字。一个服务器通常只创建一个监听socket描述子,它在该服务器的生命周期内一直存在。
监听套接字
连接套接字:而accept函数等待客户端的连接请求到达监听套接字listenfd,accept函数返回的是连接socket描述字(一个连接套接字),它代表着一个网络已经存在的点点连接。已连接套接字是客户端与服务器之间已经建立起来了的连接的一个端点,服务器每次接受连接请求时都会创建一次已连接套接字,它只存在于服务器为一个客户端服务的过程中。
连接套接字socketfd_new依然使用的是与监听套接字socketfd_new一个类型的端口号,均只存在于服务器端
# socket
socket缓冲区
- 每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区
- I/O缓冲区在每个TCP套接字中单独存在;
- 即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
- 关闭套接字将丢失输入缓冲区中的数据。
- 通过 getsockopt() 函数获取缓冲区的默认大小,一般都是 8K
unsigned optVal;
int optLen = sizeof(int);
getsockopt(servSock, SOL_SOCKET, SO_SNDBUF, (char*)&optVal, &optLen);
printf("Buffer length: %d\\n", optVal);
2
3
4
# listen
int listen(int sockfd, int backlog);
这里的backlog参数:
内核为任何一个给定的监听套接口维护两个队列:
1、半连接队列(incomplete connection queue),已由某个客户发出并到达服务器,正在等待完成相应的 TCP 三次握手( SYN_RCVD 状态)
2、全连接队列(completed connection queue),已完成 TCP 三次握手过程的(ESTABLISHED 状态)

@backlog参数的意义
backlog指的是accept队列的长度,不能超过内核定义的上限(somaxconn)
# tcp在socket的位置
建立:
- 客户端发送SYN包,包含发送客户端的序列号,进入SYN_SENT状态
- 服务端收到后,回ACK+SYN包,包含客户端的+1,服务端的序列号,进入SYN_RECD状态
- 客户端收到ACK后,connect调用返回,回复ACK,包括服务端序列号+1,进入established状态
- ack到达服务端后进入established状态,并返回accept函数
断开:
- 客户端调用close发送FIN报文,进入FIN_WAIT_1
- 服务端收到后,为FIN包插入一个文件结束符EOF到接收缓冲区(放在末尾),发出ACK报文,进入CLOSE_WAIT
- 客户端收到ACK报文,close函数返回
- 应用程序通过read感知这个FIN包,于是调用close,发出FIN包,进入LAST_ACK状态
- 客户端收到FIN包发送ACK确认包,进入TIME_WAIT状态
- 服务端收到ACK进入close状态
# 没有accept,能建立tcp连接吗
可以,accept只是从TCP全连接队列中取出一个已经建立连接的socket
# 没有listen,能建立TCP连接吗
可以,两个客户端之间可以建立连接
# socket编程实例
服务器端:一直监听本机的8000号端口,如果收到连接请求,将接收请求并接收客户端发来的消息,并向客户端返回消息
服务端
/* File Name: server.c */
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#define DEFAULT_PORT 8000
#define MAXLINE 4096
int main(int argc, char** argv)
{
int socket_fd, connect_fd;
struct sockaddr_in servaddr;
char buff[4096];
int n;
//初始化Socket
if( (socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ){
printf("create socket error: %s(errno: %d)\\n",strerror(errno),errno);
exit(0);
}
//初始化
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//IP地址设置成INADDR_ANY,让系统自动获取本机的IP地址。
servaddr.sin_port = htons(DEFAULT_PORT);//设置的端口为DEFAULT_PORT
//将本地地址绑定到所创建的套接字上
if( bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){
printf("bind socket error: %s(errno: %d)\\n",strerror(errno),errno);
exit(0);
}
//开始监听是否有客户端连接
if( listen(socket_fd, 10) == -1){
printf("listen socket error: %s(errno: %d)\\n",strerror(errno),errno);
exit(0);
}
printf("======waiting for client's request======\\n");
while(1){
//阻塞直到有客户端连接,不然多浪费CPU资源。
if( (connect_fd = accept(socket_fd, (struct sockaddr*)NULL, NULL)) == -1){
printf("accept socket error: %s(errno: %d)",strerror(errno),errno);
continue;
}
//接受客户端传过来的数据
n = recv(connect_fd, buff, MAXLINE, 0);
//向客户端发送回应数据
if(!fork()){ /*紫禁城*/
if(send(connect_fd, "Hello,you are connected!\\n", 26,0) == -1){
perror("send error");
close(connect_fd);
exit(0);
}
buff[n] = '\\0';
printf("recv msg from client: %s\\n", buff);
close(connect_fd);
}
close(socket_fd);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
客户端
/* File Name: client.c */
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#define MAXLINE 4096
int main(int argc, char** argv)
{
int sockfd, n,rec_len;
char recvline[4096], sendline[4096];
char buf[MAXLINE];
struct sockaddr_in servaddr;
if( argc != 2){
printf("usage: ./client <ipaddress>\\n");
exit(0);
}
if( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
printf("create socket error: %s(errno: %d)\\n", strerror(errno),errno);
exit(0);
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8000);
if( inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0){
printf("inet_pton error for %s\\n",argv[1]);
exit(0);
}
if( connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){
printf("connect error: %s(errno: %d)\\n",strerror(errno),errno);
exit(0);
}
printf("send msg to server: \\n");
fgets(sendline, 4096, stdin);
if( send(sockfd, sendline, strlen(sendline), 0) < 0){
printf("send msg error: %s(errno: %d)\\n", strerror(errno), errno);
exit(0);
}
if((rec_len = recv(sockfd, buf, MAXLINE,0)) == -1) {
perror("recv error");
exit(1);
}
buf[rec_len] = '\\0';
printf("Received : %s ",buf);
close(sockfd);
exit(0);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
客户端去连接server
./client 127.0.0.1
# 高性能网络编程
对于网络数据传输,从网卡到业务经过路径太长,内核时性能瓶颈
xdp
基于ebpf动态挂载,驻足于内核空间,可以绕过内核协议栈,较早的拿到包,常用于快速识别丢弃包,
从硬件中断的方式变为主动轮询,减少中断处理的事件消耗,位于数据链路层
无需专门的硬件,基于linux的,所以支持的硬件更多
主流的linux发行版已经内置XDP
请求先被xdp过滤一波,从xdp拿到的包可能再经过napi机制再进入内核网络,走传统的linux内核网络栈流程
dpdk
intel的,需要专门的硬件支持,只对指定网卡生效
取代内核网络栈和网卡驱动,数据通过网卡直接流入应用层的dpdk
缺点是对网络的监控全面交给dpdk,应用无法监控和感知物理网卡,dpdk出故障将导致系统无法服务,