有关TCP 连接的退出,你该知道的事....(上)
前言
ok,这幅图熟悉吧(如果不熟悉,那么你可能不太适合看这篇博文)。 正常的整个流程还是比较简单的,然而在现实生活中,生活并不是总是一帆风顺,异常情况还是要时常考虑的。
ok,今天就来总结一波在socket编程中,有关tcp/ip 整个通信过程中的一些异常(尤其是异常退出)的情况。
由于要讨论的情况比较多,有些乱 。为了方便、清晰起见,我打算按照流程从上到下挨着分析。因此,对于上图做了一点小改动。
注意,下文中有些异常情况包括上图中好几个步骤(圈圈),所以不太好划分,所以下文中的阶段只是大概区分。
废话不多说,策马崩腾,我们走起。
一、建立连接阶段可能出现的异常
对于使用socket 建立的tcp连接来说,除了上图中描述的过程外,还有一些辅助性的过程,如绑定ip和端口号等,这个过程可能也会出现错误,这种情况是静态的情况,比较简单,这里就不考虑了。
正儿八经的建立连接(开始有数据交互)从connect开始, 这个阶段可能会在上图中的①②③出现异常。
1.1 在①②阶段的异常:服务器端口未打开
当客户端connect的第一阶段,内核(客户端的内核)会发送建立连接请求(发送SYN)给服务器端,服务器端如果没有对应的端口在接受服务,那么内核(服务器端的内核)会给客户端返回一个RST分节。 客户端内核接受到之后,知道发生了异常,会返回给上层Connection refused; 如下图所示
默认你都能看得懂哦,(* ^ ▽ ^ *)
1.2 在①②阶段的异常:客户端等待ACK超时
这种情况最经典一般出在非阻塞connect ,比较少见。【1】中有叙述,但是不太准确。
具体的场景是这样:客户端设置了非阻塞的connect 连接,并且设置了超时时间。 这样客户端在发送SYN分节后,如果在设定的超时时间内没有接收到服务器端的ACK+SYN,那么客户端的connect 就会立即返回一个EINPROGRESS(阻塞时的connect在成功或者失败之后才会返回)。 但是注意,如果客户端不做特殊处理的话(比如说关闭连接),那么内核会继续TCP的三路握手进行下去。
下图在connect返回EINPROGRESS后关闭了程序,第二个SYN是服务器端发来的SYN+ACK(已经超时了);第三个RST是我在程序中关闭了描述符,然后客户端内核发送的RST(作为对上一个SYN+ACK的回应:自己已经因为超时关闭socket了,服务器端还在那等着呢,无论怎么样,得给他一个回复啊,RST异常回复也是回复)
下图,是我在connect超时之后,没有立即关闭socket,三次握手得以继续进行下去。
1.3 在②③阶段的异常:服务器端等待ACK超时
这个就是著名的SYN Flood攻击。其基本原理是在握手的第三阶段,如果服务器要等待客户端的ACK。 如果客户端迟迟不发送这个ACK, 服务器就会重试,重试超过一定次数(或者说超过一定时间,也即SYN Timeout,一般是30s -2 min),服务器端才会丢弃这个未完成的连接。
如果有恶意的客户端不断伪造TCP连接请求(不发送第三阶段的ACK), 那么服务器负载就会相当巨大,造成正常用于的访问延迟加大,这就是服务端SYN Flood受攻击。
1.4 在③阶段的异常:服务器端accept异常
这种情况描述的是客户端和服务器端建立连接,但是当服务器端准备accept从连接队列中取出连接之前,因为各种原因客户端发送了一个断开连接的过程(比如说进程crash),导致客户端发送了一个RST分节,那么服务器端会出现什么状况?
我给这种情况形象的表述成:明明说好的一起牵手,当我准备伸出手时,你缺转头而去!
对于这种情况,不同的系统内核处理的方式可能不同。有常见的处理方式如下(来自【3】):
我在linux下做了个实验,发现在linux上accept不会报错,但是在在对取得的socket进行读写的时候,会报错“Connection reset by peer”。
在【4】中有对linux的这种现象做解释:
这里附上实验代码和对应的结果截图。
//客户端
#include <sys/socket.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdint.h>
#include <assert.h>
#include <fcntl.h>
#include <string.h>
#include <bits/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <errno.h>
const char* ip = "x.x.x.x";
uint16_t port = 9999;
int main()
{
int sock_fd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in sock_addr;
memset(&sock_addr, 0, sizeof(sock_addr));
sock_addr.sin_family = AF_INET;
sock_addr.sin_port = htons(port);
inet_pton(AF_INET, ip, &(sock_addr.sin_addr));
int ret = connect(sock_fd, (sockaddr*)&sock_addr, sizeof(sock_addr));
if(ret < 0)
{
printf("%s\n", strerror(errno));
}
struct linger my_linger;
memset(&my_linger, 0, sizeof(my_linger));
my_linger.l_onoff = 1;
my_linger.l_linger = 0;
setsockopt(sock_fd, SOL_SOCKET, SO_LINGER, &my_linger, sizeof(my_linger));
close(sock_fd);
return 0;
}
//服务器端
#include <sys/socket.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdint.h>
#include <assert.h>
#include <fcntl.h>
#include <string.h>
#include <bits/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <errno.h>
const char* ip = "0.0.0.0";
uint16_t port = 9999;
char buff[1024];
void make_sock_noblocking(int sock_fd)
{
int flags = fcntl(sock_fd, F_GETFL, 0);
flags |= O_NONBLOCK;
fcntl(sock_fd, F_SETFL, flags);
}
int main()
{
int listen_fd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in sock_addr;
memset(&sock_addr, 0, sizeof(sock_addr));
sock_addr.sin_family = AF_INET;
sock_addr.sin_port = htons(port);
inet_pton(AF_INET, ip, &(sock_addr.sin_addr));
int optval = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR,
(const void *)&optval , sizeof(int));
int ret = bind(listen_fd, (sockaddr*)&sock_addr, sizeof(sock_addr));
listen(listen_fd, 1024);
make_sock_noblocking(listen_fd);
sleep(10);
struct sockaddr_in client_addr;
memset(&sock_addr, 0, sizeof(client_addr));
socklen_t len = sizeof(client_addr);
int conn_fd = accept(listen_fd, (sockaddr*)&client_addr, &(len));
printf("%d--------%s\n", conn_fd, strerror(errno));
int read_bytes = read(conn_fd, buff, sizeof(buff));
printf("%d %s\n", read_bytes, strerror(errno));
return 0;
}
实验截图如下:
客户端:
服务器端:
enen…
我后来有深究了一下,如果客户端退出的时候发送的不是RST(异常关闭连接),而是FIN(正常关闭连接),那会有什么情况?
你猜猜…
我做了一个实验,正常连接,可以获取socket,但是read的时候会返回0(大概是对方连接已关闭)。 如下图所示,
服务器端:
客户端:
服务器端:
参考
【1】、TCP/IP详解–接收RST回应的几种情况
【2】、SYN Flood攻击原理与防范
【3】、TCP异常处理(accept返回前连接中止)与SO_LINGER选项
【4】、linux下accept之前异常终止连接问题?