乐趣区

关于tcp:TCP之send-recv

关注公众号: 高性能架构摸索。后盾回复【材料】,能够收费支付

接触过网络开发的人,大抵都晓得,下层利用应用 send 函数发送数据,应用 recv 来接收数据,而 send 和 recv 的实现原理又是怎么的呢?

在后面的几篇文章中,咱们有提过,TCP 是个牢靠的、全双工协定。其流量管制或者拥塞管制依赖于滑动窗口和拥塞窗口的滑动来实现,而这两个窗口的滑动实现则是依赖于 TCP 中的两个 buffer,这两个 buffer 则是 TCP socket 在内核中的发送缓冲区 (send buffer) 和接收缓冲区(recv buffer)。

在本文中,咱们首先会简略介绍下 TCP 中发送缓冲区和接收缓冲区的作用(对于前面了解 send 和 recv 十分重要),而后解说 Linux 零碎下,TCP 发送和接收数据是如何实现的。

缓冲区


缓冲区,能够了解为是一个长期缓存。

对于发送端来说,socket 将数据拷贝到发送长期缓冲区,就立刻返回到应用层去做其余的事件,而剩下的将长期缓冲区的数据通过内核发送到对端,这就是 tcp 的事。

对于接收端来说,内核将网络中的数据拷贝到缓冲区,期待下层利用读取。

发送缓冲区

下面有讲,过程在调用 send()发送的数据的时候, 最简略状况(也是个别状况), 将数据拷贝进入 socket 的内核发送缓冲区之中,而后 send 便会立刻返回。

换句话说,在应用层调用 send()返回之时,数据不肯定会发送到对端去(和 write 写文件有点相似),send()仅仅是把应用层 buffer 的数据拷贝进 socket 的内核发送 buffer 中。

TCP socket 有两种模式,即阻塞模式和非阻塞模式。

  • 在阻塞模式下, send 函数的过程是将应用程序申请发送的数据拷贝到发送缓存中发送并失去确认后再返回. 但因为发送缓存的存在, 体现为: 如果发送缓存大小比申请发送的大小要大, 那么 send 函数立刻返回, 同时向网络中发送数据; 否则,send 向网络发送缓存中不能包容的那局部数据, 并期待对端确认后再返回(接收端只有将数据收到接管缓存中, 就会确认, 并不一定要期待应用程序调用 recv)
  • 在非阻塞模式下,send 函数的过程仅仅是将数据拷贝到协定栈的缓存区而已, 如果缓存区可用空间不够, 则尽能力的拷贝, 返回胜利拷贝的大小; 如缓存区可用空间为 0, 则返回 -1, 同时设置 errno 为 EAGAIN.

在 Linux 内核中,有两种形式能够查看 tcp 缓冲区 buffer 大小。

1、通过查看 /etc/sysctl.ronf 下的 net.ipv4.tcp_wmem 值

2、通过命令 ’cat /proc/sys/net/ipv4/tcp_wmem’

cat /proc/sys/net/ipv4/tcp_wmem
4096    16384    4194304

从下面能够看出,在笔者所在的服务器上,tcp send 缓冲区 buffer 有 3 个值,别离是 4096 16384 4194304。

  • 第一个值是 socket 的发送缓存区调配的起码字节数,
  • 第二个值是默认值(该值会被 net.core.wmem_default 笼罩), 缓存区在零碎负载不重的状况下能够增长到这个值
  • 第三个值是发送缓存区空间的最大字节数(该值会被 net.core.wmem_max 笼罩)

咱们能够通过程序,来批改以后 tcp socket 的发送缓冲区大小, 须要留神的是,如下的代码批改,只会批改以后特定的 socket。

int buffer_len = 10240;
setsockopt(fd, SOL_SOCKET, SO_SNDBUF, (void*)&buffer_len, buffer_len);
接收缓冲区

接收缓冲区被 TCP 用来缓存网络上来的数据,始终保留到利用过程读走为止。

对于 TCP,如果利用过程始终没有读取,接收缓冲区满了之后,产生的动作是:收端告诉发端,接管窗口敞开(win=0)。这个便是滑动窗口的实现。保障 TCP 套接口接收缓冲区不会溢出,从而保障了 TCP 是牢靠传输。因为对方不容许收回超过所通告窗口大小的数据。这就是 TCP 的流量管制,如果对方忽视窗口大小而收回了超过窗口大小的数据,则接管方 TCP 将抛弃它。

与查看发送缓冲区大小的形式一样,接收缓冲区也是通过如上的两种形式。
1、通过查看 /etc/sysctl.ronf 下的 net.ipv4.tcp_rmem 值

2、通过命令 ’cat /proc/sys/net/ipv4/tcp_rmem’

cat /proc/sys/net/ipv4/tcp_rmem
4096    87380    4194304

TCP 接收缓冲区 buffer 有 3 个值,别离是 4096 87380 4194304。

  • 第一个值是 socket 的接管缓存区的起码字节数,
  • 第二个值是默认值(该值会被 net.core.rmem_default 笼罩), 缓存区在零碎负载不重的状况下能够增长到这个值
  • 第三个值是接管缓存区空间的最大字节数(该值会被 net.core.rmem_max 笼罩)

同样的,能够通过如下代码,批改接收缓冲区的大小。

int buffer_len = 10240;
setsockopt(fd, SOL_SOCKET, SO_RCVBUF, (void*)&buffer_len, buffer_len);

实现原理

为了便于咱们了解 TCP 的整个传输过程,咱们先理解下 TCP 的四层模型以及四册模型在数据传输中的流向。前面咱们将从四层模型的角度来剖析 send 和 recv 函数在每层中都做了什么。

send 原理
NAME
       send, sendto, sendmsg - send a message on a socket

SYNOPSIS
       #include <sys/types.h>
       #include <sys/socket.h>

       ssize_t send(int sockfd, const void *buf, size_t len, int flags);

DESCRIPTION
       The system calls send(), sendto(), and sendmsg() are used to transmit a message to another socket.

当调用该函数时,send 函数:
1、先比拟待发送数据的长度 len 和套接字 sockfd 的可用发送缓冲区的长度

  • 如果数据长度 len 大于发送缓冲区的长度,则分屡次发送
  • 如果果 len 小于或者等于 sockfd 的缓冲区长度,那么 send 先查看协定是否正在发送 sockfd 的发送缓冲中的数据

    • 如果是就期待协定把数据发送完
    • 否则,如果协定还没有开始发送 s 的发送缓冲中的数据或者 s 的发送缓冲中没有数据,那么 send 就比拟 sockfd 的发送缓冲区的残余空间和 len

      • 如果 len 大于残余空间大小,send 就始终期待协定把 s 的发送缓冲中的数据发送完
      • 如果 len 小于残余空间大小,send 就仅仅把 buf 中的数据 copy 到残余空间里。如果 send 函数 copy 数据胜利,就返回理论 copy 的字节数,如果 send 在 copy 数据时呈现谬误,那么 send 就返回 SOCKET_ERROR;如果 send 在期待协定传送数据时网络断开的话,那么 send 函数也返回 SOCKET_ERROR。
        须要留神 send 函数把 buf 中的数据胜利 copy 到 s 的发送缓冲的残余空间里后它就返回了,然而此时这些数据并不一定马上被传到连贯的另一端。如果协定在后续的传送过程中呈现网络谬误的话,那么下一个 socket 函数就会返回 SOCKET_ERROR.(每一个除 send 外的 socket 函数在执行的最开始总要先期待套接字的发送缓冲中的数据被协定传送结束能力持续,如果在期待时呈现网络谬误,那么该 socket 函数就返回 SOCKET_ERROR)。

如果对具体实现不是很感兴趣,可间接此局部

从四层模型的角度来剖析 send 实现。

应用层

对于 TCP,应用程序在创立 socket 之后,调用 connect()函数,通过 socket 使客户端和服务端建设连贯。而后就能够调用 send 函数发送数据。

传输层

数据在传输层进行解决,以 TCP 协定为例,其次要有以下性能:

  • 1、结构 TCP 段
  • 2、计算校验和
  • 3、发送回复 (ACK) 包
  • 4、滑动窗口 (sliding windown) 等操作保障可靠性。

不同的协定有不同的发送函数,TCP 调用 tcp_sendmsg 函数,而 UDP 则调用的是 sock_sendmsg 函数。

tcp_sendmsg()的次要工作是传输用户层的数据,将数据放入 skb 中。而后调用 tcp_push()发送,tcp_push 函数调用 tcp_write_xmit() 函数,顺次调用发送函数 tcp_transmit_skb 将 skb 封装 tcp 头之后,回调 ip_queue_xmit。

网络层

ip_queue_xmit(skb)次要有路由查找校验、封装 ip 头和 ip 选项,最初通过 ip_local_out 发送数据包。

数据链路层

数据链路层在不牢靠的物理介质上提供牢靠的传输。该层的性能包含:物理地址寻址、数据成帧、流量管制、数据谬误检测、重发等。这一层的数据单位称为帧(frame)。


上图为 send 函数源码的调用逻辑图,对源码有趣味的话,能够在 net/tcp.c 找到对应的实现。

recv 原理
NAME
       recv, recvfrom, recvmsg - receive a message from a socket

SYNOPSIS
       #include <sys/types.h>
       #include <sys/socket.h>

       ssize_t recv(int sockfd, void *buf, size_t len, int flags);

       ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                        struct sockaddr *src_addr, socklen_t *addrlen);

       ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

DESCRIPTION
       The recvfrom() and recvmsg() calls are used to receive messages from a socket, and may be used to receive data on a socket whether or not it is connection-oriented.

       If  src_addr  is not NULL, and the underlying protocol provides the source address, this source address is filled in.  When src_addr is NULL, nothing is filled in; in this case, addrlen is not used, and should also be NULL.  The argument
       addrlen is a value-result argument, which the caller should initialize before the call to the size of the buffer associated with src_addr, and modified on return to indicate the actual size of the source address.  The returned address is
       truncated if the buffer provided is too small; in this case, addrlen will return a value greater than was supplied to the call.

       The recv() call is normally used only on a connected socket (see connect(2)) and is identical to recvfrom() with a NULL src_addr argument.

当调用该函数时候:

  • 先查看套接字 sockfd 的接收缓冲区
  • 如果 sockfd 接收缓冲区中没有数据或者协定正在接收数据,那么 recv 就始终期待,直到协定把数据接管结束。
  • 当协定把数据接管结束,recv 函数就把 sockft 的接管缓冲中的数据 copy 到 buf 中,recv 函数返回其理论 copy 的字节数。
  • 如果 recv 在 copy 时出错,那么它返回 SOCKET_ERROR;
  • 如果 recv 函数在期待协定接收数据时网络中断了,那么它返回 0。
  • 对方优雅的敞开 socket 并不影响本地 recv 的失常接收数据;
  • 如果协定缓冲区内没有数据,recv 返回 0,批示对方敞开;
  • 如果协定缓冲区有数据,则返回对应数据(可能须要屡次 recv),在最初一次 recv 时,返回 0,批示对方敞开。

如果对具体实现不是很感兴趣,可间接此局部

从四层模型的角度来剖析 recv 实现。

数据链路层

当数据包达到机器的物理网卡时会触发一个中断,中断处理程序调配 skb_buff 数据结构,并将从网卡 I / O 接管到的数据帧复制到 skb_buff 缓冲区,并设置 skb_buff 相应的参数。

而后收回软中断,告诉内核接管新的数据帧。进入软中断解决流程,调用 net_rx_action 函数。进入 netif _receive_skb 解决流程。

netif_receive_skb 依据在全局数组 ptype_all 和 ptype_base 中注册的网络层数据报类型,将数据报发送到不同的网络层协定接管函数(INET 域次要是 ip_rcv 和 arp_rcv)。

网络层

ip_rcv 函数为网络层的入口函数。该函数做的第一件事就是数据校验,而后调用 ip_rcv_finish 这个函数。

ip_rcv_finish 函数会调用 ip_route_input 函数来更新路由,而后寻找路由,决定音讯是发送到本地机器,转发还是抛弃。

如果发送到本机,则调用 ip_local_deliver 函数,能够进行碎片整顿(合并多个包),并调用 ip_local_deliver_finish。最初调用下一层接口,包含 tcp_v4_rcv(TCP)、udp_rcv(UDP)、icmp_rcv(ICMP)、igmp_rcv(IGMP)。如果须要转发,则进入转发流程,调用 dev_queue_xmit,进入链路层解决流程。如果不是发送到本机,应该是转发,调用 ip_forward 进行转发。

传输层

在该层,咱们会做一些完整性检查,如果发现问题就丢包。如果是 tcp,则调用 tcp_v4_do_rcv。

而后 sk->sk_state == TCP_ESTABLISHED,调用 tcp_rcv_builted,调用 tcp_data_queue 办法将音讯放入队列。而后应用 tcp_ofo_queue 办法将音讯插入接管到 Queued。

应用层

应用程序调用读取或者 recv 的时候,该调用被映射到 /net/socket.c 中的 sys_recv 零碎调用,而后调用 sock_recvmsg 函数。

TCP 会调用 tcp_recvmsg。该函数从套接字缓冲区复制数据到缓冲区。

上述过程,咱们总结下就是:
1、数据帧从内部网络达到网卡
2、网卡把帧 DMA 到内存 Ring Buffer 中
3、硬中断告诉 CPU
4、CPU 响应硬中断,简略解决后发憷软中断
5、软中断过程解决软中断,调用网卡驱动注册的 pool 函数开始收包
6、帧被从 Ring Buffer 中摘下来,存储到 skb 中
7、协定层开始解决网络帧,并将解决实现后的数据放入 socket 的接收缓冲区中


上图为整个网络数据接管的函数调用过程,对月接收端来说,当有数据来的时候,都是通过终端来告诉内核,最终通过回调,调用零碎函数。

下图是 send 和 recv 残缺的函数调用过程

常见问题

在理论利用中, 如果发送端是非阻塞发送, 因为网络的阻塞或者接收端解决过慢, 通常呈现的状况是, 发送应用程序看起来发送了 10k 的数据, 然而只发送了 2k 到对端缓存中, 还有 8k 在本机缓存中(未发送或者未失去接收端的确认). 那么此时, 接管应用程序可能收到的数据为 2k. 如果接管应用程序调用 recv 函数获取了 1k 的数据在解决, 在这个霎时, 产生了以下状况之一, 单方体现为:

  1. 发送应用程序认为 send 完了 10k 数据, 敞开了 socket:

发送主机作为 tcp 的被动敞开者, 连贯将处于 FIN_WAIT1 的半敞开状态(期待对方的 ack), 并且, 发送缓存中的 8k 数据并不革除, 仍然会发送给对端. 如果接管应用程序仍然在 recv, 那么它会收到余下的 8k 数据(这个前题是, 接收端会在发送端 FIN_WAIT1 状态超时前收到余下的 8k 数据.), 而后失去一个对端 socket 被敞开的音讯(recv 返回 0). 这时, 应该进行敞开.

  1. 发送应用程序再次调用 send 发送 8k 的数据:

如果发送缓存的空间为 20k, 那么发送缓存可用空间为 20-8=12k, 大于申请发送的 8k, 所以 send 函数将数据做拷贝后, 并立刻返回 8192;

如果发送缓存的空间为 12k, 那么此时发送缓存可用空间还有 12-8=4k,send()会返回 4096, 应用程序发现返回的值小于申请发送的大小值后, 能够认为缓存区已满, 这时必须阻塞(或通过 select 期待下一次 socket 可写的信号), 如果应用程序不理睬, 立刻再次调用 send, 那么会失去 - 1 的值, 在 linux 下体现为 errno=EAGAIN.

  1. 接管应用程序在解决完 1k 数据后, 敞开了 socket:
    接管主机作为被动敞开者, 连贯将处于 FIN_WAIT1 的半敞开状态(期待对方的 ack). 而后, 发送应用程序会收到 socket 可读的信号(通常是 select 调用返回 socket 可读), 但在读取时会发现 recv 函数返回 0, 这时应该调用 close 函数来敞开 socket(发送给对方 ack);

如果发送应用程序没有解决这个可读的信号, 而是在 send, 那么这要分两种状况来思考, 如果是在发送端收到 RST 标记之后调用 send,send 将返回 -1, 同时 errno 设为 ECONNRESET 示意对端网络已断开, 然而, 也有说法是过程会收到 SIGPIPE 信号, 该信号的默认响应动作是退出过程, 如果疏忽该信号, 那么 send 是返回 -1,errno 为 EPIPE(未证实); 如果是在发送端收到 RST 标记之前, 则 send 像平常一样工作;

以上说的是非阻塞的 send 状况, 如果 send 是阻塞调用, 并且正好处于阻塞时(例如一次性发送一个微小的 buf, 超出了发送缓存), 对端 socket 敞开, 那么 send 将返回胜利发送的字节数, 如果再次调用 send, 那么会同上一样.

  1. 交换机或路由器的网络断开:

接管应用程序在解决完已收到的 1k 数据后, 会持续从缓存区读取余下的 1k 数据, 而后就体现为无数据可读的景象, 这种状况须要应用程序来解决超时. 个别做法是设定一个 select 期待的最大工夫, 如果超出这个工夫仍然没有数据可读, 则认为 socket 已不可用.

发送应用程序会一直的将余下的数据发送到网络上, 但始终得不到确认, 所以缓存区的可用空间继续为 0, 这种状况也须要应用程序来解决.

如果不禁应用程序来解决这种状况超时的状况, 也能够通过 tcp 协定自身来解决, 具体能够查看 sysctl 项中的:
net.ipv4.tcp_keepalive_intvl
net.ipv4.tcp_keepalive_probes
net.ipv4.tcp_keepalive_time

论断

  • TCP 协定自身是为了保障牢靠传输, 并不等于应用程序用 tcp 发送数据就肯定是牢靠的,必须要容错;
  • send()只负责拷贝,拷贝到内核就返回
  • 此次 send()调用所触发的程序谬误,可能会在本次返回,也可能在下次调用网络 IO 函数的时候被返回。
  • 在进行 TCP 协定传输的时候,要留神数据流传输的特点,recv 和 send 不肯定是一一对应的(个别状况下是一一对应),也就是说并不是 send 一次,就肯定 recv 一次就接管完,有可能 send 一次,recv 屡次才接管完,也可能 send 屡次,一次 recv 就接管完了。TCP 协定会保证数据的有序残缺的传输,然而如何去正确残缺的解决每一条信息,是开发人员的事件。

服务器在循环 recv,recv 的缓冲区大小为 100byte,客户端在循环 send,每次 send 6byte 数据,则 recv 每次收到的数据可能为 6byte,12byte,18byte,这是随机的,编程的时候留神正确的解决。

参考

https://slidetodoc.com/networ…
https://www.programmersought….
https://www.programmersought….
https://www.fatalerrors.org/a…
https://blog.csdn.net/w839687…
http://lkml.iu.edu/hypermail/…
https://linux-kernel-labs.git…
https://git.kernel.org/pub/sc…

退出移动版