Effective TCP/IP Programming读书笔记

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/yueguanghaidao/article/details/47068975

TCP/IP深入思考


这是我读Effective TCP/IP Programming的 读书笔记和思考,以及做的一些实验。强烈建议后端工程师有空读一读这本书,有些细节的确是我们平时没有注意的,读了最好自己动手做一些实验加深理解。

fin的含义

对端发送fin会导致read()返回,但send()照样可以发送,也就是单纯的fin其实相当于shutdown(SHUT_WR)。

当对端应用程序崩溃后(未close),本段一直调用send(),对端会返回RST,此时read()会返回Connection reset by peer。

当对端应用程序崩溃后或close连接,本段调用read()返回0,
调用send()会导致EPIPE(Broken pipe),第二次send()才会出错。

对端主机崩溃

由于主机崩溃,或不结束应用程序关机,或直接关闭modern,导致对方接受不到fin,rst等,recv()会永久阻塞。解决办法:

1.开启keepalive机制,默认情况下(2+9*75/60)小时后超时
2.recv()超时机制
3.心跳,其实就是发送send(),一段时间会检测到连接断了

SO_LINGER深入理解

如果应用程序执行主动关闭,最后会在TIME_WAIT状态保持2MSL时间,而SO_LINGER选项能迫使连接立刻关闭,不走正常4步关闭流程。
server

from socket import *
s = socket(AF_INET,SOCK_STREAM)
s.bind(("localhost",9123))
s.listen(10)
cc = s.accept()
print cc #查看零时端口为47128

client

from socket import *
c = socket(AF_INET,SOCK_STREAM)
c.connect(("localhost",9123))
c.close()

由于client主动关闭,这时client应该处于FIN_WAIT2,server对应处于CLOSE_WAIT

[root@10-9-22-239 ~]# netstat -an | grep 47128
tcp        1      0 127.0.0.1:9123          127.0.0.1:47128         CLOSE_WAIT 
tcp        0      0 127.0.0.1:47128         127.0.0.1:9123          FIN_WAIT2  

注意netstat要加n参数,不然会根据/etc/services解析成服务名。
当server端再调用cc[0].close()时,此时clinet将处于TIME_WAIT状态。我们将client的socket加个SO_LINGER选项。

from struct import *
c.setsockopt(SOL_SOCKET,SO_LINGER,pack("ii",1,0))

此时我们查看发现没有对应状态,也就是未新户型FIN_WAIT2的状态,直接关闭,不走正常四步关闭连接流程。

当然强壮的应用服务器不应该干扰TIME_WAIT状态,这是TCP可靠机制的重要组成部分。
SO_LINGER选项还有其它作用,通常当close()一条连接时,即使发送缓冲区仍然有数据要发送,close()也会立即返回。当然TCP还是会尝试发送未发送出去的数据,但应用程序并不知道是否发送成功,SO_LINGER就是为解决这个问题的。

struct linger{
    int l_onoff; // on/off选项
    int l_linger; //逗留时间
};

1.l_onoff为0,linger不启用,行为和默认close()相同
2.l_onoff非0,如果l_linger非0,close()在等待数据发送完毕或逗留时间超时,如果逗留时间到,仍然有未发送数据,close()返回EWOULDBLOCK;如果linger为0,就将丢弃连接,也就是上面我们展示的那样。

Nagle算法和延迟ACK

Nagle算法有助于预防网络中的小报文泛洪,但与延迟ACK交互可能会导致200ms延迟。BSD实现ACK延迟200ms,RFC规定不能大于500ms,我在Linux上做的实验发现都是40ms延迟ACK。

关闭Nagle算法

const int on = 1;
setsocket(s, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on));
//from tornado
try:
    self.socket.setsockopt(socket.IPPROTO_TCP,socket.TCP_NODELAY, 1)
except socket.error as e:
    # Sometimes setsockopt will fail if the socket is closed
    # at the wrong time.  This can happen with HTTPServer
    # resetting the value to false between requests.
    if e.errno not in (errno.EINVAL, errno.ECONNRESET):
        raise

应对方法:

1.将写操作合并,可使用writev()
2.多用buff,如go中bufio.Reader,bufio.Writer

非阻塞connect

非阻塞connect()会立即返回EINPROGRESS,如果连接成功套接字会立刻可写,那如何检查是否连接成功呢?

status = connect( sock, ......); // 返回0,成功连接
if ( status != 0 && errno != EINPROGRESS) {
    close(sock);
}

可以使用getsockopt检查是否出错,当然之前必须等待该socket状态变化

int error;
socklen_t len = sizeof(error);  
int code = getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &len);  
if (code < 0 || error) {  //返回值小于0或error不等于0说明连接错误
    close(s);
    return SOCKET_ERROR;
}

用python模拟如下:

import socket
import select
import errno

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.setblocking(False)
try:
  s.connect(("www.google.com",80))
except socket.error as e:
  if e.errno == errno.EINPROGRESS:
    print("INprocess")
  else:
    raise

r,w,x = select.select([s],[s],[s])
err = s.getsockopt(socket.SOL_SOCKET,socket.SO_ERROR)
if err != 0:
  print("error",errno.errorcode[err])
  s.close()
else:
  print("connect ok")

很明显,不翻墙www.google.com暂时连接不了,出错信息

INprocess
('error', 'ETIMEDOUT')

理解udp connect

使用udp connect好处:

1.使用connect()对性能有较大提高,BSD实现中sendto()是connect的特列,内核暂时让套接字与目的地址连接,发送数据包,然后解除连接。
2.可以接受异步错误,由于udp无状态,数据发送出去,系统就会忘记,所以即使返回icmp报文,也不知道是哪个应用程序发送的数据包,使用connect会记录对应信息。

s=socket(AF_INET,SOCK_DGRAM)
s.connect(("localhost",9999))
s.send("1111")
s.recv() //error: [Errno 111] Connection refused

send()之后,icmp报文报告不可达,再次recv或recvfrom会报错。

缓冲区大小

发送缓冲区设置为MSS的3倍大,可有效减少Nagle算法与延迟ACK导致的交互问题。
Linux默认发送缓冲区为4K,以太网mss是1460,还是很合理的。

发送缓冲区大小大于等于对方的接受缓冲区,因为发送方只能收到ACK才会释放已发送数据,所以发送缓冲区容易满,不发送新的数据,就会产生40ms延迟ACK。

猜你喜欢

转载自blog.csdn.net/yueguanghaidao/article/details/47068975
今日推荐