linux 网络编程各种坑

TCP/IP协议相关

1、延时ack

当协议栈接受到TCP数据时,并不一定会立刻发送ACK响应,而是倾向于等待一个超时或者满足特殊条件时再发送。对于Linux实现,这些特殊条件如下:

1)收到的数据已经超过了 full frame size 
2)或者处于快速回复模式 
3)或者出现了乱序的包 
4)或者接收窗口的数据足够多

如果接收方有数据回写,则ACK也会搭车一起发送。当以上条件都不满足时,接收方会延迟40ms再回应ACK。

2、nagle 算法

Nagle 算法要求:

  • 一个 TCP 连接上最多只能有一个未被确认的未完成的小分组,在它到达目的地前,不能发送其它分组。
  • 在上一个小分组未到达目的地前,即还未收到它的 ack 前,TCP 会收集后来的小分组。当上一个小分组的 ack 收到后,TCP 就将收集的小分组合并成一个大分组发送出去。

 

通过延时ack和nagle算法我们思考,当RTT延时很小时,会出现什么情况?假如RTT是1ms,我们发送完‘l’字符之后需要等待(1ms + 40ms + 1ms)时间,反而数据传输效率降低了。对于这种情况通常有以下解决方法:

  • 使用 writev 函数,聚集写。
  • 在应用层将前 4 个字节和后 396 字节复制到单个缓冲区,然后调用一次 write.
  • 使用 TCP_NODELAY 关闭 Nagle 算法。 

read/write 函数

1、read/write函数是可以设置非阻塞的,默认是阻塞的。每个socket 在内核中都拥有自己的发送缓冲区和接收缓冲区,read/write函数只是把数据发送到内核缓冲区或者从内核缓冲区接收。至于数据什么时候发送到网络之中,我们是不知道的。

扫描二维码关注公众号,回复: 12774314 查看本文章

2、当内核接收缓冲区空/发送缓冲区满时,调用read/write 函数在非阻塞情况下,会立刻返回-1,errno = EAGAIN或者EWOULDBLOCK(这两个值是相等的)。

3、假设 A 机器上的一个进程 a 正在和 B 机器上的进程 b 通信:某一时刻 a 正阻塞在 socket 的 read 调用上(或者在nonblock下轮询socket),当 b 进程终止时,无论应用程序是否显式关闭了 socket(OS会负责在进程结束时关闭所有的文件描述符,对于socket,则会发送一个 FIN 包到对面)。但是事情远远没有想象中简单。优雅地(gracefully)关闭一个 TCP 连接,不仅仅需要双方的应用程序遵守约定,中间还不能出任何差错。

  3.1  假如 b 进程是异常终止的,发送 FIN 包是 OS 代劳的b 进程已经不复存在,当机器B再次收到该 socket 的消息时,会回应 RST(因为拥有该 socket 的进程已经终止)。a 进程对收到 RST 的 socket 调用 write 时,操作系统会给 a 进程发送 SIGPIPE,默认处理动作是终止进程。

  3.2 不同于 b 进程退出(此时 OS 会负责为所有打开的 socket 发送 FIN 包),当 B 机器的 OS 崩溃(注意不同于人为关机,因为关机时所有进程的退出动作依然能够得到保证)/主机断电/网络不可达时,a 进程根本不会收到 FIN 包作为连接终止的提示

  • 如果a进程阻塞在 read 上,那么结果只能是永远的等待。
  • 如果 a 进程先 write 然后阻塞在 read,由于收不到 B 机器 TCP/IP 栈的 ack,TCP会持续重传 12 次(时间跨度大约为9分钟),然后在阻塞的read调用上返回错误:ETIMEDOUT/EHOSTUNREACH/ENETUNREACH。假如 B 机器恰好在某个时候恢复和A机器的通路,并收到 a 某个重传的pack,因为不能识别所以会返回一个RST,此时 a 进程上阻塞的 read 调用会返回错误 ECONNREST。

4、socket是什么时候可读/写?

      4.1  socket 可读:

  • socket 内核接受缓存区中的字节数大于或等于其低水位标记SO_RCVLOWAT。此时我们可以无阻塞的读该socket,并且读操作返回的字节数大于0.
  • socket 通信的对方关闭连接,此时对该socket的读操作返回0.
  • 监听socket上有新的连接请求。
  • socket上有未处理的错误。此时我们可以使用getsockopt来读取和清除该错误。

      4.2  socket 可写

  • socket内核发送缓冲区中的可用字节数大于或等于其低水位标记SO_SNDLOWAT。此时我们可以无阻塞低写该socket,并且写操作返回的字节数大于0.
  • socket 的写操作被关闭。对写操作被关闭的socket执行写操作将触发一个SIGPIPE信号。
  • socket使用非阻塞connect连接成功或者失败(超时)之后。
  • socket上有未处理的错误。此时我们可以使用getsockopt来读取和清除该错误。

5、EINTR (error interrupt)

  read和write函数是可以被信号中断的函数,函数被中断时会返回错误(-1)并将errno变量置为EINTR。可以在sigaction注册时 ,将SA_RESTART 选项开启,函数遇到中断时函数不会返回错误而是自动重启。

accept 函数

1、在客户端和服务器成功建立三次握手即进入 ESTABLISHED 状态,在服务器accept接收之前,客户端关闭socket(需要将socket设置SO_LINGER 选项,close 时将会发送RST报文而不是正常的FIN报文)发送RST报文,然后服务器执行accept函数,根据不同的操作系统会发生如下情况:

  •  accept时悄无声息的把这个连接给 kill 掉。
  • 让 accept 返回 ECONNABORTED。
  • 成功接收。但是在执行读或者写的时候返回 ECONNRESET 错误。

close 函数

当我们用close函数关闭套接字的时候,接下来的行为和SO_LINGER 套接字选项相关。

设置SO_LINGER 选项时需要用到结构体

struct linger {
  l_onoff;   //开关,0为关闭SO_LINGER选项,不等于0为打开
  l_linger;  //延滞时间,单位为秒。
}

默认情况下,SO_LINGER 选项是被关闭的,此时成员 l_onoff = 0,这种情况下 l_linger 是没有用的。

  1. l_onoff == 0, {off, ~},这是系统的默认情况。此时 l_linger 被忽略,close 立即返回,如果发送缓冲还有残留数据,系统会在后台将这些数据发送给对端,最后发送 FIN 给对端。
  2. l_onff != 0 && l_linger == 0 , {on, 0},close 函数立即返回,将放弃发送缓冲区中残留数据,立即发送 RST 给对端。
  3. l_onff != 0 && l_linger > 0, {on, >0} , close 会阻塞,直到下面两种情况中的任何一种发生就返回,返回值是 0:

      a) 所有数据都已发送完(包含 FIN),且得到对端的 ack,这种情况为提前返回;

       b) 延滞时间到(l_linger 指定的时间,单位为秒)。

设置方法

struct linger lgr;
lgr.l_onoff = 1;
lgr.l_linger = 5;

setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &lgr, sizeof(lgr));

(accept)常见错误

  • EAGAIN: 11, Resource temporarily unavailable  原因是系统资源暂时不可用 解决方法是重试即可
  • ECONNABORTED: 103, Software caused connection abort   原因是当服务和客户进程在完成用于 TCP 连接的“三次握手”后,客户 TCP 却发送了一个 RST (复位)分节,在服务进程看来,就在该连接已由 TCP 排队,等着服务进程调用 accept 的时候 RST 却到达了。POSIX 规定此时的 errno 值必须 ECONNABORTED。 解决方法忽略,重新accept即可。
  • EINTR: 4, Interrupted system call   原因是执行读或者写的时候被系统信号中断。解决方法是继续执行读写即可。
  • EPROTO: 71, Protocol error   原因是协议错误   解决方法忽略
  • EPERM: 1, Operation not permitted    操作不允许  解决方法忽略
  • EMFILE: 24, Too many open files   打开的文件过多 解决方法忽略

猜你喜欢

转载自blog.csdn.net/u014608280/article/details/85211190