理解TCP连接,需要首先记住以下几点:
- TCP是双向连接。两个方向的连接可以独立关闭。
- TCP是基于字节流的连接。每个tcp socket在内核里有接收缓冲区和发送缓冲区。
- 应用程序只能操纵缓冲区数据,而不能干扰实际的数据发送过程。应用程协议可能有自己的协议格式,但在TCP看来全是一个一个的字节。
下面基于以上3点,谈一下TCP连接的建立、数据传输和终止过程。
建立连接
正常的连接过程需要3路握手,不再赘述。这里主要讲一下连接失败以及异步连接的情况。
正常来讲,在客户端调用connect之前,服务端应该已经调用socket, listen创建好监听套接口了。这样客户端connect应该可以很快返回。在连接建立完成后,服务端调用accept就可以立即返回,否则就阻塞到有客户端连接进来。
如果服务端在相应端口上没有监听socket,那么对于客户端发过来的SYN,服务端就会发送RST,连接失败;
如果客户端connect之前设置socket为O_NONBLOCK,那么调用connect立即返回,并设置errno为EINPROSESS。注意,这并不属于错误,因为这时候内核在为你建立连接。当3步握手完成之后,再调用connect就成功返回了。一般在select/epoll里面,需要监听多个fd的时候,可以使用非阻塞connect,避免应用程序阻塞在建立连接上。
传输数据
发送数据
write/send将数据写入到发送缓冲区。
阻塞型write/send会一直阻塞,直到所有的数据全部写入到发送缓冲区,并返回写入的字节数。
write的行为取决于是否被设置为O_NONBLOCK。
- 默认为阻塞型,那么write会一直阻塞,直到指定的所有字节全部写入内核缓冲区,除非出错;
- 如果设置为nonblock,那么一次write只会写入当前可以写入的字节数,并返回已写入字节数;如果缓冲区满,那么返回-1,并设置errno为EAGAIN。
我们知道tcp会对每个发送的字节进行确认,因此被对方ACK的数据就能从发送缓冲区删除,这样缓冲区就又可写了。
write返回,仅表示数据已经写到发送缓冲区。至于数据是否从网卡发送出去了,以及对方是否能够接收到数据,这些还都是未知数。TCP有流控制,每次发送数据的大小,取决于对方ACK里携带的窗口大小,也就是对方接收缓冲区的空闲大小。因此,如果对方应用程序一直不read,它的接收缓冲区就会一直处于饱和状态,那么发送方就不能发送新的数据。这样,发送方的write也就会阻塞。
但是,当对方连接关闭(对方进程死掉了,或者调用了close),那么我方在发送数据过去的时候就会收到对方发过来的RST。这时候,应用程序再调用write/send就会出错,errno是EPIPE,并且会收到SIGPIPE信号。此信号默认会终止进程,因此应用程序不能通过 if (ret < 0) { perror(…);}的形式解析errno,除非捕获SIGPIPE信号。
此外,被中断了,write也会出错,errno为EINTR。此种情况下,一般是重新write一遍,因此需要特殊处理。
接收数据
接收数据,会将接收缓冲区的数据拷贝到用户空间。
可以想象,如果接收缓冲区没有数据,那么read/recv就会阻塞。
对于接收方来说,一次完整的TCP连接,应该从SYN分节开始,以FIN分节结束。每次read都会返回从接收缓冲区拷贝的数据字节数。可能大于0,也可能等于0(收到FIN)。
如果在read FIN之前连接出错,那么read就会返回错误,并可以从errno获得错误信息。例如,被中断了(EINTR)。
注意,以上讨论的都是阻塞型的socket,这也是默认的形式。如果设置socket为O_NONBLOCK,那么缓冲区没数据的话read会返回EWOULDBLOCK或EAGAIN。
对于接受到RST的TCP连接,如果已经收到FIN分节了,那么read会返回0,而不是出错。因为FIN已经表示对方所有的数据都被收到了,没有理由返货错误给应用程序。一般的,应用程序在read返回0时就应该自行处理善后工作了。