在一个TCP套接口被设置为非阻塞之后调用connect,connect会立即返回EINPROGRESS错误,表示连接操作正在进行中,但是仍 未完成;同时TCP的三路握手操作继续进行;在这之后,我们可以调用select来检查这个链接是否建立成功;非阻塞connect有三种用途: 1.我们可以在三路握手的同时做一些其它的处理.connect操作要花一个往返时间完成,而且可以是在任何地方,从几个毫秒的局域网到几百毫秒或几秒的广域网.在这段时间内我们可能有一些其他的处理想要执行; 2.可以用这种技术同时建立多个连接.在Web浏览器中很普遍; 3. 由于我们使用select来等待连接的完成,因此我们可以给select设置一个时间限制,从而缩短connect的超时时间.在大多数实现 中,connect的超时时间在75秒到几分钟之间.有时候应用程序想要一个更短的超时时间,使用非阻塞connect就是一种方法; 非阻塞connect听起来虽然简单,但是仍然有一些细节问题要处理: 1.即使套接口是非阻塞的,如果连接的服务器在同一台主机上,那么在调用connect建立连接时,连接通常会立即建立成功.我们必须处理这种情况; 2.源自Berkeley的实现(和Posix.1g)有两条与select和非阻塞IO相关的规则: A:当连接建立成功时,套接口描述符变成可写; B:当连接出错时,套接口描述符变成既可读又可写; 注意:当一个套接口出错时,它会被select调用标记为既可读又可写;
非阻塞connect有这么多好处,但是处理非阻塞connect时会遇到很多可移植性问题;
处理非阻塞connect的步骤: 第一步:创建socket,返回套接口描述符; 第二步:调用fcntl把套接口描述符设置成非阻塞; 第三步:调用connect开始建立连接; 第四步:判断连接是否成功建立; A:如果connect返回0,表示连接简称成功(服务器可客户端在同一台机器上时就有可能发生这种情况); B:调用select来等待连接建立成功完成; 如果select返回0,则表示建立连接超时;我们返回超时错误给用户,同时关闭连接,以防止三路握手操作继续进行下去; 如果select返回大于0的值,则需要检查套接口描述符是否可读或可写;如果套接口描述符可读或可写,则我们可以通过调用getsockopt来得到套 接口上待处理的错误(SO_ERROR),如果连接建立成功,这个错误值将是0,如果建立连接时遇到错误,则这个值是连接错误所对应的errno值(比 如:ECONNREFUSED,ETIMEDOUT等). "读取套接口上的错误"是遇到的第一个可移植性问题;如果出现问 题,getsockopt源自Berkeley的实现是返回0,等待处理的错误在变量errno中返回;但是Solaris会让getsockopt返回 -1,errno置为待处理的错误;我们对这两种情况都要处理;
这样,在处理非阻塞connect时,在不同的套接口实现的平台中存在的移植性问题,首先,有可能在调用select之前,连接就已经建立成功,而 且对方的数据已经到来.在这种情况下,连接成功时套接口将既可读又可写.这和连接失败时是一样的.这个时候我们还得通过getsockopt来读取错误 值;这是第二个可移植性问题; 移植性问题总结: 1.对于出错的套接口描述符,getsockopt的返回值源自Berkeley的实现是返回0,待处理的错误值存储在errno中;而源自Solaris的实现是返回0,待处理的错误存储在errno中;(套接口描述符出错时调用getsockopt的返回值不可移植) 2.有可能在调用select之前,连接就已经建立成功,而且对方的数据已经到来,在这种情况下,套接口描述符是既可读又可写;这与套接口描述符出错时是一样的;(怎样判断连接是否建立成功的条件不可移植)
这样的话,在我们判断连接是否建立成功的条件不唯一时,我们可以有以下的方法来解决这个问题: 1.调用getpeername代替getsockopt.如果调用getpeername失败,getpeername返回ENOTCONN,表示连接建立失败,我们必须以SO_ERROR调用getsockopt得到套接口描述符上的待处理错误; 2.调用read,读取长度为0字节的数据.如果read调用失败,则表示连接建立失败,而且read返回的errno指明了连接失败的原因.如果连接建立成功,read应该返回0; 3.再调用一次connect.它应该失败,如果错误errno是EISCONN,就表示套接口已经建立,而且第一次连接是成功的;否则,连接就是失败的;
被中断的connect: 如果在一个阻塞式套接口上调用connect,在TCP的三路握手操作完成之前被中断了,比如说,被捕获的信号中 断,将会发生什么呢?假定connect不会自动重启,它将返回EINTR.那么,这个时候,我们就不能再调用connect等待连接建立完成了,如果再 次调用connect来等待连接建立完成的话,connect将会返回错误值EADDRINUSE.在这种情况下,应该做的是调用select,就像在非 阻塞式connect中所做的一样.然后,select在连接建立成功(使套接口描述符可写)或连接建立失败(使套接口描述符既可读又可写)时返回;
socket -> connect -> recv/send -> close
其中socket没有什么可疑问的,主要是创建一个套接字用于与服务端交换数据,并且通常它会迅速返回,此时并没有数据通过网卡发送出去,而紧随其后的 connect函数则会产生网络数据的发送,TCP的三次握手也正是在此时开始,connect会先发送一个SYN包给服务端,并从最初始的CLOSED 状态进入到SYN_SENT状态,在此状态等待服务端的确认包,通常情况下这个确认包会很快到达,以致于我们根本无法使用netstat命令看到 SYN_SENT状态的存在,不过我们可以做一个极端情况的模拟,让客户端去连接一个随意指定服务器(如IP地址为88.88.88.88),因为该服务 器很明显不会反馈给我们SYN包的确认包(SYN ACK),客户端就会在一定时间内处于SYN_SENT状态,并在预定的超时时间(比如3分钟)之后从connect函数返回,connect调用一旦失 败(没能到达ESTABLISHED状态)这个套接字便不可用,若要再次调用connect函数则必须要重新使用socket函数创建新的套接字。
下面结合实例分析,客户端代码如下:
[cpp] view plain copy zhou@neptune:~/data/source$ ./client 88.88.88.88此时程序会在connect函数中阻塞等待,约180秒之后输出:
[cpp] view plain copy zhou@neptune:~/data/source$ sudo netstat -natp |grep 20000 tcp 0 1 192.168.0.4:44203 88.88.88.88:20000 SYN_SENT 5954/client 可以看到此时的TCP连接状态为SYN_SENT,也就意味着发送了SYN包之后一直未得到服务端回馈SYN ACK包。接下来我们使用这个客户端程序来连接自己的机器,测试时我的IP地址是192.168.0.4,是一个无线局域网,结果如下:
[cpp] view plain copy zhou@neptune:~/data/source$ ./client 192.168.0.188 can not connect to 192.168.0.188, exit! No route to host因为本地局域网中的该主机并不存在,ARP请求得不到回应,网关会回应主机不可达的ICMP报文,connect返回EHOSTUNREACH。
至此connect函数的分析就结束了,由于本人水平有限,博客中的不妥或错误之处在所难免,殷切希望读者批评指正。同时也欢迎读者共同探讨相关的内容,如果乐意交流的话请留下您宝贵的意见,谢谢。
原来我们实现connect()超时基本上都使用unix网络编程一书的非阻塞方式(connect_nonb),今天在网上看到一篇文章,觉得很有意思,转载如下:
读Linux内核源码的时候偶然发现其connect的超时参数竟然和用SO_SNDTIMO操作的参数一致: File: net/ipv4/af_inet.c
559 timeo = sock_sndtimeo(sk, flags & O_NONBLOCK); 560 561 if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) { 562 /* Error code is set above */ 563 if (!timeo || !inet_wait_for_connect(sk, timeo)) 564 goto out; 565 566 err = sock_intr_errno(timeo); 567 if (signal_pending(current)) 568 goto out; 569 } 这意味着: 在Linux平台下,可以通过在connect之前设置SO_SNDTIMO来达到控制连接超时的目的。简单的写了份测试代码:#include <stdlib.h> #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <errno.h> int main(int argc, char *argv[]) { int fd; struct sockaddr_in addr; struct timeval timeo = {3, 0}; socklen_t len = sizeof(timeo); fd = socket(AF_INET, SOCK_STREAM, 0); if (argc == 4) timeo.tv_sec = atoi(argv[3]); setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeo, len); addr.sin_family = AF_INET; addr.sin_addr.s_addr = inet_addr(argv[1]); addr.sin_port = htons(atoi(argv[2])); if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) { if (errno == EINPROGRESS) { fprintf(stderr, "timeout/n"); return -1; } perror("connect"); return 0; } printf("connected/n"); return 0; }
socket -> connect -> recv/send -> close
其中socket没有什么可疑问的,主要是创建一个套接字用于与服务端交换数据,并且通常它会迅速返回,此时并没有数据通过网卡发送出去,而紧随其后的 connect函数则会产生网络数据的发送,TCP的三次握手也正是在此时开始,connect会先发送一个SYN包给服务端,并从最初始的CLOSED 状态进入到SYN_SENT状态,在此状态等待服务端的确认包,通常情况下这个确认包会很快到达,以致于我们根本无法使用netstat命令看到 SYN_SENT状态的存在,不过我们可以做一个极端情况的模拟,让客户端去连接一个随意指定服务器(如IP地址为88.88.88.88),因为该服务 器很明显不会反馈给我们SYN包的确认包(SYN ACK),客户端就会在一定时间内处于SYN_SENT状态,并在预定的超时时间(比如3分钟)之后从connect函数返回,connect调用一旦失 败(没能到达ESTABLISHED状态)这个套接字便不可用,若要再次调用connect函数则必须要重新使用socket函数创建新的套接字。
下面结合实例分析,客户端代码如下:
[cpp] view plain copy zhou@neptune:~/data/source$ ./client 88.88.88.88此时程序会在connect函数中阻塞等待,约180秒之后输出:
[cpp] view plain copy zhou@neptune:~/data/source$ sudo netstat -natp |grep 20000 tcp 0 1 192.168.0.4:44203 88.88.88.88:20000 SYN_SENT 5954/client 可以看到此时的TCP连接状态为SYN_SENT,也就意味着发送了SYN包之后一直未得到服务端回馈SYN ACK包。接下来我们使用这个客户端程序来连接自己的机器,测试时我的IP地址是192.168.0.4,是一个无线局域网,结果如下:
[cpp] view plain copy zhou@neptune:~/data/source$ ./client 192.168.0.188 can not connect to 192.168.0.188, exit! No route to host因为本地局域网中的该主机并不存在,ARP请求得不到回应,网关会回应主机不可达的ICMP报文,connect返回EHOSTUNREACH。
至此connect函数的分析就结束了,由于本人水平有限,博客中的不妥或错误之处在所难免,殷切希望读者批评指正。同时也欢迎读者共同探讨相关的内容,如果乐意交流的话请留下您宝贵的意见,谢谢。
OK,闲话少扯,我们进入主题,下面结合一个简单的TCP服务端与客户端代码,借助tcpdump命令来分析一下TCP建立连接时的三次握手过程 (Three-way handshake process)。
服务端代码如下:
[cpp] view plain copy /** * client.c * * TCP client program, it is a simple example only. * * Writen By: Zhou Jianchun * Date: 2011.08.12 * * Compiled With: gcc -o client client.c * Tested On: Ubuntu 11.04 LTS * * gcc version: 4.5.2 * */ #include <stdio.h> #include <sys/socket.h> #include <unistd.h> #include <sys/types.h> #include <netinet/in.h> #include <stdlib.h> #include <string.h> #define SERVER_PORT 20000 #define CLIENT_PORT ((20001 + rand()) % 65536) #define BUFFER_SIZE 255 #define REQUEST_MESSAGE "welcome to connect the server.\n" void usage(char *name) { printf("usage: %s IP\n", name); } int main(int argc, char **argv) { int server_fd, client_fd, length = 0; struct sockaddr_in server_addr, client_addr; socklen_t socklen = sizeof(server_addr); char buf[BUFFER_SIZE]; if(argc < 2) { usage(argv[0]); exit(1); } if((client_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { printf("create socket error, exit!\n"); exit(1); } srand(time(NULL)); bzero(&client_addr, sizeof(client_addr)); client_addr.sin_family = AF_INET; //client_addr.sin_port = htons(CLIENT_PORT); client_addr.sin_port = htons(40000); client_addr.sin_addr.s_addr = htons(INADDR_ANY); bzero(&server_addr, sizeof(server_addr)); server_addr.sin_family = AF_INET; inet_aton(argv[1], &server_addr.sin_addr); server_addr.sin_port = htons(SERVER_PORT); /*if(bind(client_fd, (struct sockaddr*)&client_addr, sizeof(client_addr)) < 0) { printf("bind to port %d failed, exit!\n", CLIENT_PORT); exit(1); }*/ if(connect(client_fd, (struct sockaddr*)&server_addr, socklen) < 0) { printf("can not connect to %s, exit!\n", argv[1]); exit(1); } /*length = recv(client_fd, buf, BUFFER_SIZE, 0); if(length < 0) { printf("recieve data from %s error, exit!\n", argv[1]); exit(1); } */ char *tmp = buf; while((length = read(client_fd, tmp, BUFFER_SIZE)) > 0) { tmp += length; } printf("frome server %s:\n\t%s", argv[1], buf); close(client_fd); return 0; }代码逻辑十分简单,服务端程序启动后监听在20000端口,等待外部连接,客户端启动后连接过来,服务端发送一串字符串信息给客户端,然后退出,客户端在读取完信息后也退出。
运行程序之前先在另一个终端下输入如下命令:
tcpdump 'port 20000' -i lo -S
待两端程序退出后可以看到该命令输出如下信息:
<div bg_cpp"=""> [cpp] view plain copy 17:05:35.358403 IP neptune.local.49493 > neptune.local.20000: Flags [S], seq 1317094743, win 32792, options [mss 16396,sackOK,TS val 7083694 ecr 0,nop,wscale 6], length 0 17:05:35.358439 IP neptune.local.20000 > neptune.local.49493: Flags [S.], seq 1311370954, ack 1317094744, win 32768, options [mss 16396,sackOK,TS val 7083694 ecr 7083694,nop,wscale 6], length 0 17:05:35.358468 IP neptune.local.49493 > neptune.local.20000: Flags [.], ack 1311370955, win 513, options [nop,nop,TS val 7083694 ecr 7083694], length 0 17:05:35.358871 IP neptune.local.20000 > neptune.local.49493: Flags [P.], seq 1311370955:1311371210, ack 1317094744, win 512, options [nop,nop,TS val 7083694 ecr 7083694], length 255 17:05:35.358890 IP neptune.local.49493 > neptune.local.20000: Flags [.], ack 1311371210, win 530, options [nop,nop,TS val 7083694 ecr 7083694], length 0 17:05:35.358913 IP neptune.local.20000 > neptune.local.49493: Flags [F.], seq 1311371210, ack 1317094744, win 512, options [nop,nop,TS val 7083694 ecr 7083694], length 0 17:05:35.359419 IP neptune.local.49493 > neptune.local.20000: Flags [F.], seq 1317094744, ack 1311371211, win 530, options [nop,nop,TS val 7083694 ecr 7083694], length 0 17:05:35.359441 IP neptune.local.20000 > neptune.local.49493: Flags [.], ack 1317094745, win 512, options [nop,nop,TS val 7083694 ecr 7083694], length 0下面我们逐条进行分析:
1.客户端通过49493端口向服务端的20000端口发送一个SYN同步请求包,展开第一次握手,其中Flags [S]表求数据包的类型为SYN, 即同步请求包,seq字段标识数据包序列号。
2. 服务端发送ACK确认包,同时附代一个SYN请求包,在确认客户端同步请求的同时 向客户端发送同步请求,其中Flags [S.]中的点号表示这是个确认包(ACK),S表示它同时又是一个SYN请求包。因为TCP是双工通信协议,连接建立之后双方可以同时收发数据,所以双 方都发送了SYN包请求同步。
3.客户端发送ACK包确认服务端的SYN同步请求,可以看到此时Flags中只有一个小数点,表示这个包只是用来做确认的。
到此为止,三次握手过程就结束了,双方如果都收到了ACK包,则都进入到ESTABLISHED状态,表明此时可以进行数据发送了。
4.服务端向客户端发送一个数据包,包中的内容就是一个字符串,可以看到此时的Flags标识中有个字母P,意为PUSH DATA,就是发送数据的意思。
至此TCP三次握手过程的分析就结束了,由于本人水平有限,博客中的不妥或错误之处在所难免,殷切希望读者批评指正。同时也欢迎读者共同探讨相关的内容,如果乐意交流的话请留下您宝贵的意见,谢谢。
(1)客户端发送一个带SYN标志的TCP报文到服务器。这是三次握手过程中的报文1。
(2) 服务器端回应客户端的,这是三次握手中的第2个报文,这个报文同时带ACK标志和SYN标志。因此它表示对刚才客户端SYN报文的回应;同时又标志SYN给客户端,询问客户端是否准备好进行数据通讯。
(3) 客户必须再次回应服务段一个ACK报文,这是报文段3。
2、连接终止协议(四次握手) 由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
(1) TCP客户端发送一个FIN,用来关闭客户到服务器的数据传送(报文段4)。 (2) 服务器收到这个FIN,它发回一个ACK,确认序号为收到的序号加1(报文段5)。和SYN一样,一个FIN将占用一个序号。 (3) 服务器关闭客户端的连接,发送一个FIN给客户端(报文段6)。 (4) 客户段发回ACK报文确认,并将确认序号设置为收到序号加1(报文段7)。
CLOSED: 这个没什么好说的了,表示初始状态。
LISTEN: 这个也是非常容易理解的一个状态,表示服务器端的某个SOCKET处于监听状态,可以接受连接了。
SYN_RCVD: 这个状态表示接受到了SYN报文,在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂,基本上用netstat你是很难看到这种状态的,除非你特意写了一个客户端测试程序,故意将三次TCP握手过程中最后一个ACK报文不予发送。因此这种状态时,当收到客户端的ACK报文后,它会进入到ESTABLISHED状态。
SYN_SENT: 这个状态与SYN_RCVD遥想呼应,当客户端SOCKET执行CONNECT连接时,它首先发送SYN报文,因此也随即它会进入到了SYN_SENT状态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT状态表示客户端已发送SYN报文。
ESTABLISHED:这个容易理解了,表示连接已经建立了。
FIN_WAIT_1: 这个状态要好好解释一下,其实FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。而这两种状态的区别是:FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET即进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态,当然在实际的正常情况下,无论对方何种情况下,都应该马上回应ACK报文,所以FIN_WAIT_1状态一般是比较难见到的,而FIN_WAIT_2状态还有时常常可以用netstat看到。
FIN_WAIT_2:上面已经详细解释了这种状态,实际上FIN_WAIT_2状态下的SOCKET,表示半连接,也即有一方要求close连接,但另外还告诉对方,我暂时还有点数据需要传送给你,稍后再关闭连接。
TIME_WAIT: 表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL后即可回到CLOSED可用状态了。如果FIN_WAIT_1状态下,收到了对方同时带FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。
CLOSING: 这种状态比较特殊,实际情况中应该是很少见,属于一种比较罕见的例外状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什么情况下会出现此种情况呢?其实细想一下,也不难得出结论:那就是如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。
CLOSE_WAIT: 这种状态的含义其实是表示在等待关闭。怎么理解呢?当对方close一个SOCKET后发送FIN报文给自己,你系统毫无疑问地会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,实际上你真正需要考虑的事情是察看你是否还有数据发送给对方,如果没有的话,那么你也就可以close这个SOCKET,发送FIN报文给对方,也即关闭连接。所以你在CLOSE_WAIT状态下,需要完成的事情是等待你去关闭连接。
LAST_ACK: 这个状态还是比较容易好理解的,它是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,也即可以进入到CLOSED可用状态了。
最后有2个问题的回答,我自己分析后的结论(不一定保证100%正确)
1、 为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?
这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。
2、 为什么TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态?
这是因为:虽然双方都同意关闭连接了,而且握手的4个报文也都协调和发送完毕,按理可以直接回到CLOSED状态(就好比从SYN_SEND状态到ESTABLISH状态那样);但是因为我们必须要假想网络是不可靠的,你无法保证你最后发送的ACK报文会一定被对方收到,因此对方处于LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文,并保证于此。