制作和淘宝商城一样网站,wordpress 切换语言,国内建设地铁的公司网站,鄂尔多斯 网站制作在开发应用时#xff0c;我们使用 socket 实现网络数据的收发。以tcp为例#xff0c;server端通过 socket, bind, listen来创建服务端#xff0c;然后通过 accept接收客户端连接#xff1b;客户端通过 socket和 connect系统调用来创建客户端。用于数据收发的系统调用包括 s…在开发应用时我们使用 socket 实现网络数据的收发。以tcp为例server端通过 socket, bind, listen来创建服务端然后通过 accept接收客户端连接客户端通过 socket和 connect系统调用来创建客户端。用于数据收发的系统调用包括 send, recv, sendto, recvfrom等。除了上述系统调用之外另外还有多路复用技术 selectpoll, epoll也常常在网络应用中使用。 // tcp 服务端int fd socket(AF_INET, SOCK_STREAM, 0);bind(listen_fd,(struct sockaddr *)server_addr, sizeof(server_addr))}listen(listen_fd, 5);int accetp_fd accept(listen_fd, (struct sockaddr*)client_addr, client);// tcp 客户端sock_fd socket(AF_INET, SOCK_STREAM, 0);connect(sock_fd, (struct sockaddr *)addr_serv,sizeof(struct sockaddr));在做应用开发时使用上述系统调用比较简单和简洁。对于 linux 网络来说这些系统调用只是冰山一角在 linux 网络收发包的过程中报文要经过协议栈网卡驱动以及网卡硬件本身的处理。
1网络分层
如下是 linux 收发包示意图 上图中包括的环节有网卡网卡驱动网络层传输层socket 层应用层。收包时报文的流动方向是从下到上发包时反之。其中网络应用工作在用户态传输层、网络层以及网卡驱动工作在内核态socket 层是 linux 提供的系统调用是用户态和内核态的桥梁。应用层、socket、传输层网络层是软件部分网卡是硬件部分网卡驱动是软件与硬件的桥梁。
1.1网卡
dma
dma即直接内存访问直接内存访问的意思是硬件来访问内存数据读写过程中不需要cpu参与。在收包方向网卡收到包之后会通过dma引擎将数据保存在主机内存中然后将收包事件通知给 cpu之后cpu便可以处理这个报文在发包方向cpu将报文放到对应的内存buffer之后将信息通知网卡之后网卡便会通过dma引擎将buffer中的数据取出并发送出去。dma过程中, cpu和网卡之间只需要一些通知指令cpu不需要参与数据的读写过程。设想一下如果没有dma, 那么在收发包时就需要cpu不断地从网卡中读写数据进行收发包这种效率会非常低下。
环形队列
环形队列中的元素称为bd(buffer descriptor)是一个描述符该描述符中存储的并不是真正的报文数据而是报文的元数据。考虑最简单的情况bd中应包括一个buffer指针指向实际存储数据的内存地址另外还有一个变量是内存buffer的长度。实际中bd的结构会更复杂比如一个报文太长在一个buffer中存不下这样就需要有其它的信息比如报文存了几个buffer另外网卡收到数据之后并不一定从buffer的开始处存储报文数据这样就需要在buffer的开始处留出一段空间有时还需要在buffer的末尾处保留一段空间。环形队列是cpu软件和网卡硬件通信的桥梁网卡硬件和cpu都可以访问环形队列。
struct buffer_desc {char *buffer;int length;
};
1.2网卡驱动
网卡驱动与网卡通信的方式有两种中断和轮询这也是软件和硬件交互的两种方式。以收包为例对于中断方式即网卡收到数据包之后会触发一个中断然后网卡驱动注册的中断处理程序便会从网卡中读取数据并做后续的处理轮询方式是cpu不断地查询网卡的相关寄存器判断当前有没有新的包需要处理有则处理没有则这次轮询空转。当流量较小的时候在没有数据时轮询方式会导致cpu空跑浪费cpu资源所以轮询方式适用于流量较大的场景。
中断提高实时性(因为在linux中中断的优先级最高高于线程和软中断中断到来之后会立即得到响应)但是如果在流量比较大的场景网卡产生中断的速度就会非常快这样会让cpu为处理中断事件浪费所有的时间。
轮询方式适用于网络流量比较大的场景比如路由器路由器是通信专用设备功能比较单一核心功能就是处理网络流量所以可以使用轮询方式(即使短时间内流量小导致cpu空转也不会造成其它影响因为路由器上也没有其它任务来抢cpu)。DPDK中就使用了轮询方式。
linux内核中提供了napi方式该方式既不是纯中断方式也不是纯轮询方式而是集合了中断和轮询发挥了两种方式的优点。当中断到来时napi便会关中断然后处理包因为关了中断如果在napi处理包的过程中又来了新的报文napi处理过程就不会被打断。napi每一次处理均是批量处理不是处理一个报文就返回也就是说在napi处理过程中虽然新到的数据包没有中断napi也会处理它。
napi的退出机制napi并不是一直在轮询当接收队列中的报文都被处理完毕当然就会返回但是如果接收队列中的报文非常多短时间内没有处理完会一直处理报文吗 不会的为了防止处理网络报文的任务一直占用这cpunapi有主动退出机制一个是时间维度一个是报文数量维度时间维度是 napi 处理时间超过某个时间便会返回未处理的报文等到下次调度时再次处理数量维度是处理的报文数量达到一定的数量时也会主动返回未处理的包等到下次调度时再次处理。
napi —— linux 网卡驱动收包机制-CSDN博客
1.3网络层
网络层最常用的是ip。
ip层主要的作用即路由在收包方向上根据目的ip决定报文是接收并上传到传输层(目的ip是本地 ip)还是转发(目的ip不是本地ip)。
另外 ip 层也需要处理分片之所以处理分片是因为报文的长度大于 mtu。
linux中的netfilter功能也是在ip层实现。
netfilter_netfilter模块报文处理-CSDN博客 1.4传输层
传输层包括两个协议tcp和udp。
tcp有如下3个特点
1.4.1面向连接
tcp的服务端和客户端通信之前需要建立连接建立连接之后才可以收发数据收发数据结束之后需要断开连接。建立连接过程需要3次握手断开连接过程中需要4次挥手。
tcp断开连接时可能是4次挥手也可能是3次挥手。3次挥手的时候第二次挥手和第三次挥手合并到了一个报文中。 1.4.2延时ack
如果收到数据之后就立即发送ack的话那么会导致网络带宽利用率低因为ack报文没有有效数据只有tcp, ip协议头。
延时ack是在收到数据之后不立即发送ack而是等待一定的时间(最小等待时间是40ms最大等待时间是200ms) 再发送ack如果在等待期间本端有数据要发送那么ack也会跟着数据一块发送出去不会受40ms和200ms的约束比如收到数据之后开始等待等待了10ms还没到最小等待时间 40ms如果这个时候要发送数据那么就会停止等待(停止定时器)ack随数据一块发送出去。
延时ack通过定时器来实现收到数据之后启动一个定时器定时器的超时时间最小是40ms最大是200ms如果定时器超时就会发送ack如果在定时器超时之前ack随着本端数据一块发送了出去那么定时器就会被取消。
延时ack的最小等待时间和最大等待时间用两个宏来表示。
#define TCP_DELACK_MAX ((unsigned)(HZ/5)) /* maximal time to delay before sending an ACK */
#define TCP_DELACK_MIN ((unsigned)(HZ/25)) /* minimal time to delay before sending an ACK */
因为延时ack的存在被动关闭的一方在收到FIN报文的时候并不会立即发送ack而是会有一定的延时。如果在等待的这段时间之内没有数据要发送本端也没有做其它动作(比如关闭连接)那么超时之后就会单独发送一个ack如果本端还有数据发送那么ack就会跟着数据一块发送出去。这种情况下就是4次挥手。
收到FIN之后说明对端不会再发送新的数据到来如果这个时候本端接收完缓存的数据之后也调用了close将连接关闭并且这个时候延时ack定时器还没有超时那么ack就会和FIN一块发送出去。这种情况下就是3次挥手。
1.4.3FIN
当tcp连接的一方要关闭本端的发送时便会向对端发送FIN报文FIN报文通过函数tcp_send_fin发送。
如果本端调用了close或者shutdown(fd, SHUT_WR) 之后就表示本端已经停止了发送如果之后再调用send函数那么会受到SIGPIPE信号在这种情况下应用会被SIGPIPE杀死如果不想应用被直接杀死可以在send函数的最后一个参数中带上 MSG_NOSIGNAL 标志这样就不会被杀死而是返回错误码 “Broken pipe”。
3 次挥手
// server 端
// 收到 FIN 之后说明对端已经停止了发送数据
// 这个时候 read 返回 0 之后说明没有数据需要处理
// 此时立即关闭连接调用 close 时会发送 FIN
// 因为延时 ack 还没有超时, 所以 ACK 和 FIN 一块发送出去
#include stdlib.h
#include stdio.h
#include errno.h
#include string.h
#include netdb.h
#include unistd.h
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include netinet/tcp.h#define PORT (12345)
#define DATA_MAX_LEN 1024int main(int argc, char *argv[])
{int listen_fd socket(AF_INET, SOCK_STREAM, 0);if(listen_fd 0){printf(create listen socket error : %s\n, strerror(errno));return -1;}printf(tcp server listen fd: %d\n, listen_fd);struct sockaddr_in server_addr;memset(server_addr, 0, sizeof(struct sockaddr_in));server_addr.sin_family AF_INET;server_addr.sin_addr.s_addr htonl(INADDR_ANY);server_addr.sin_port htons(PORT);if(bind(listen_fd, (struct sockaddr *)(server_addr), sizeof(struct sockaddr)) 0){printf(bind error: %s\n, strerror(errno));return -1;}if(listen(listen_fd, 32)){printf(listen error: %s\n\a, strerror(errno));return -1;}struct sockaddr_in client_addr;socklen_t client_addrlen sizeof(client_addr);int client_fd accept(listen_fd, (struct sockaddr *)client_addr, client_addrlen);if(client_fd 0) {printf(accept error: %s\n\a, strerror(errno));return -1;}printf(tcp server accept fd: %d\n, client_fd);char data[DATA_MAX_LEN] {0};while(1) {int n read(client_fd, data, DATA_MAX_LEN);if(n 0) {printf(read error: %s\n\a, strerror(errno));break;} else if(n 0) {// 读取数据之后立即关闭连接这个时候延时 ack 还没有超时// close 会导致本端也会向对端发送 FIN所以此时 ACK 和 FIN 就会合并到一个报文中发送// 如果打开睡眠函数让睡眠时间超过延时 ack 的时间那么延时 ack 超时之后便会发送 ack// close 时的 FIN 会单独发送// usleep(220 * 1000);int value 1;// 设置 QUICKACK, 的时候会立即发送一个 ack// QUICKACK 模式不是永久生效的如果要想要保证每次收到数据之后都立即返回 ack需要在每次收到数据之后都要做一次这个设置// if (setsockopt(client_fd, IPPROTO_TCP, TCP_QUICKACK, (char *)value, sizeof(int)) 0) {// printf(set quick ack error\n);// return -1;// }close(client_fd);printf(client fd closed\n);break;}data[n] 0;printf(received %d bytes: %s\n, n, data);}close(listen_fd);return 0;
}// client 端
#include stdlib.h
#include stdio.h
#include errno.h
#include string.h
#include netdb.h
#include unistd.h
#include sys/types.h
#include netinet/in.h
#include sys/socket.h#define PORT (12345)int main(int argc, char *argv[])
{int connect_fd socket(AF_INET, SOCK_STREAM, 0);if(connect_fd 0){printf(create socket error: %s\n, strerror(errno));return -1;}struct sockaddr_in server_addr;memset(server_addr, 0, sizeof(struct sockaddr_in));server_addr.sin_family AF_INET;server_addr.sin_addr.s_addr inet_addr(127.0.0.1);server_addr.sin_port htons(PORT);if(connect(connect_fd, (struct sockaddr *)(server_addr), sizeof(server_addr)) 0){printf(connect error: %s\n, strerror(errno));return -1;}char data[64] hello, server;int ret send(connect_fd, data, strlen(data), 0);if(ret ! strlen(data)) {printf(send data error: %s\n, strerror(errno));return -1;}sleep(2);close(connect_fd);return 0;
}
这是使用 tcpdump 抓包的结果截图从下图中可以看到
第 6 个报文是客户端向服务端发送的 FIN第一次挥手
第 7 个报文是服务端向客户端发送的 ACK FIN 报文第二次挥手
第 8 个报文是客户端向服务端发送的 ACK, 第三次挥手 4 次挥手
如果我们将上述服务端的代码做一下修改把第 69 行的 usleep(220 * 1000) 打开这样的话当服务端 read 返回 0 之后不立即关闭 fd而是延时一段时间再调用 close。延时时间是 220ms超过了延时 ack 的最大时间这样的话在 usleep() 期间本端就会返回 ACK。之后调用 close向对端发送 FIN。这样的话第二次挥手和第 3 次挥手就不会合并。
抓包结果如下图所示
第 6 个报文是客户端向服务端发送的 FIN第一次挥手
第 7 个报文是服务端向客户端发送的 ACK第二次挥手从第 6 和第 7 个报文的时间可以看出来延时 ack 这次的延时在 45ms 左右
第 8 个报文服务端向客户端发送的 FIN第三次挥手
第 9 个报文客户端向服务端发送的 ACK第四次挥手 如果把上述服务端的代码做一下修改把第 74 - 77 行的代码打开这样在设置 quickack 的时候会发送 ack。然后再调用 close发送 FIN这样也是 4 次挥手。
从下图可以看出第 6、7、8、9 报文是 4 次挥手的报文。 1.4.4字节流
tcp 是字节流协议也就是说在 tcp 这一层收发数据的边界与用户收发数据的边界可能会出现不一致。比如用户发送了一个长度为 2000 字节的报文tcp 发送的时候可能会分两次发送一次发送 1000; 如果用户先后发送了两个报文长度分别是 1000 字节和 2000 字节tcp 也可能第一次发送 500字节的报文第二次发送 1000 字节的报文第 3 次发送 1500 字节的报文 tcp 分 3 次包数据发送出去。
1.4.5可靠
tcp 最大的特点就是可靠性。丢包乱序是导致不可靠的原因tcp 可以通过序列号重传等技术解决这样的问题从而保证传输是可靠的。
从上边 3 个方面来说udp 与 tcp 是相反的
1没有连接
udp 的通信双方在通信之前不需要建立连接。从这个角度来看tcp 是面向连接的协议是一对第一的通信方式那么 tcp 只能支持单播不支持多播和广播因为多播和广播是一对多的通信方式udp 支持多播和广播。
2数据报
udp 是数据报协议也就是说数据收发的边界就是用户收发数据的边界比如用户发送了 1000 字节的数据那么 udp 就会发送 1000 字节的数据udp 不会改变报文的边界。
3不可靠
udp 是不可靠的协议如果报文在传输过程中出现了丢包或者乱序udp 协议无法发现这些问题。如果使用 udp 协议还要达到可靠的目标可以在应用层实现。
1.4.6socket 层和应用层
本片文章值关注使用 socket 进行 tcp 通信。比较简单不做太多记录。 2struct sk_buff
在内核网络代码中随处可见的一个变量名skb 数据结构就是struct sk_buff(本文中也会使用skb来表示一个sk_buff)。无论是在哪一层一个报文在 linux 内核中就用sk_buff 来表示这方便在各个网络层之间交换数据而不需要复制数据。
sk_buff 中的字段比较多本文中只关注两类字段一类用于报文数据管理一类用于 sk_buff 管理。
1报文数据管理
sk_buff 中有 4 个指针分别指向数据缓存的不同位置数据缓存就是实际存放报文数据的一段内存。
head: 数据缓存的起始位置
end: 数据缓存的结束位置
data: 数据存放的起始位置
tail: 数据存放的结束位置
也就是说head和end指向缓存的起止位置缓存申请好之后这两个指针指向的位置就保持不变data和tail指向实际数据存储的起止位置随着报文在不同的层次之间传递会出现添加协议头(发送方向tcp, ip, mac 头逐层添加)或者删除报文头(接收方向mac, ip, tcp 头逐层删除)的情况这个时候通过操作data来实现当需要向缓存中追加数据的时候需要移动tail。headroom是head 和data之间的空间tailroom是tail和end之间的空间这两个room的大小 0。
另外还有3个成员 transport_header, network_header, mac_header分别表示 tcp 头ip 头 mac 头相对于 head 的偏移量。这 3 个成员是每层协议头相对于 head 的偏移量不是指针。 从上图中可以看到sk_buff-end 是报文数据可用空间的结尾但不是内存 buffer 的结尾内存 buffer 的结尾处还有一个数据结构 struct skb_shared_info。struct skb_shared_info 两个重要的成员是 frags 和 frag_list。frags 是用在 SG, 网卡支持这种方式才能向这个里边放数据frag_list 是 ip 分片。
2sk_buff 管理
tcp有发送缓冲区也有接收缓冲区缓冲区中的元素是一个skb缓冲区是一个双向链表tcp接收侧有一个乱序队列tcp解决乱序问题主要使用这个队列这个队列使用红黑树实现。
以 tc 发送缓冲区为例tcp_sendmsg 中调用tcp_sendmsg_locked, tcp_sendmsg_locked中调用skb_entail将skb入队到发送缓冲区中。tcp发送缓冲保存在struct sock 中成员struct sk_buff_head sk_write_queue。
下图是 tcp 发送缓冲区的示意图 tcp接收缓冲区在 struct sock 中成员是sk_receive_queue 在函数tcp_queue_rcv中入队。
接收时乱序队列的入队函数是tcp_data_queue_ofo乱序队列在数据结构struct tcp_sock 中成员是 out_of_order_queue。