今天和大家分享一些基本套接口的编程函数,为帮助大家更好的理解connect、accept和close函数并使用netstat调试TCP应用程序,我们需要了解如何建立和终止TCP连接以及TCP的状态转换图。这样能够帮助我们编写网络程序的例子。
《一》三次握手:
下述步骤建立一个TCP连接:
1.服务器必须准备好接受外来的连接。这通过调用socket、bind和listen函数来完成,称为被动打开(passive open)。
2.客户端通过调用connect进行主动打开(active open)。这引起客户TCP发送一个SYN分节(表示同步),它告诉服务器客户将在(待建立的)连接中发送的数据的初始序列号。一般情况下SYN分节不携带数据,它只含有一个IP头部、一个TCP头部及可能有的TCP选项。
3.服务器必须确认客户的SYN,同时自己也得发送一个SYN分节,它含有服务器将在同一连接中发送的数据的初始序列号。服务器以单个分节向客户发送SYN和对客户SYN的ACK。
4.客户必须确认服务器的SYN。
连接过程至少需要交换三个分组,因此称之为TCP的三路握手(three-way hand-shake)。其具体的过程如图所示:
TCP选项:
每一个SYN可以含有若干个TCP选项。常用的选项:
a>MSS选项:TCP发送的SYN中带有这个选项是通知对方它的最大分节大小MSS(maximum segment size),即它能接受的每个TCP 分节中的最大数据量。
b>窗口规模选项:TCP双方能够通知对方的最大窗口大小是65536,因为TCP的头部相应的字段只占16位。
c>时间戳选项:这个选项对高速连接是必要的,它可以防止失而复的的分组可能造成的数据损坏。
《二》TCP连接终止:
TCP用三个分节建立一个连接,终止一个连接则需四个分节:
1.某个应用程序首先调用close,我们称这一端执行主动关闭(active close)。这一端的TCP于是发送一个FIN分节,表示数据发送完毕。
2.接到FIN的另一端执行被动关闭(passive close)。这个FIN由TCP 确认。它的接收也作为文件的结束符传递给接收方应用进程(放在已排队等待该应用进程接收的任何其它数据之后),因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据。
3.一段时间过后,接收到文件结束符的应用进程将调用close关闭它的套接口。这导致它的TCP也发送一个FIN。
4.接收到这个FIN的原发送方TCP(即执行主动关闭的那一端)对它进行确认。
《三》TCP转换图:
图中,为一个连接定义了11中状态,,并且TCP规则决定如何从一个状态转换到另一个状态,这种转换基于当前状态及在该状态下所接收的分节。例如,当应用进程在CLOSED状态下执行一个主动打开时,TCP将发送一个SYN并从CLOSED状态转换成SYN_SENT状态。如果该TCP接着收到一个附带ACK的SYN,它将发送一个ACK并转换成ESTABLISHED状态。这个最终状态是数据传送状态。
TIME_WAIT状态:在图中可以看出执行主动关闭的那一端进入这种状态。这个端点留在该状态的持续时间是最长分节生命期MSL(maximum segment lifetime)的两倍,有时称2MSL。
每个TCP实现都必须选择一个MSL值,TIME_WAIT状态的持续时间在1分钟到4分钟之间。MSL是IP数据报能在互联网中生存的最长时间。
存在TIME_WAIT的状态有两个原因:
(1)实现终止TCP全双工连接的可靠性;
(2)允许老的重复字节在网络中消逝。
第一个理由解释如下:如图,假设最终ACK丢失服务器将重发最终的FIN。因此客户必须维护状态信息以允许它发送最终的ACK。如果不维护状态信息,它将响应以RST(另外一个类型的TCP分节),而服务器则把该分节解释成一个错误。如果TCP打算执行所有必要的工作以彻底终止某个连接上的两个方向数据流(即全双工关闭),那么它必须正确处理连接终止序列四个分节中任何一个分节的丢失情况。
要理解存在TIME_WAIT状态的第二个理由,我们假设206.62.226.33端口1500和192.69.10.2端口21之间有一个TCP连接。我们关闭这个连接后,在以后的某个时候又重新建立起相同的IP地址和端口之间TCP连接。后一个连接称为前一个连接的化身,因为它们的IP地址和端口号相同。TCP必须防止来自某个连接的老重复分组在连接终止后再现,从而被误解成属于同一个连接的化身。要实现这个功能,TCP不能给处于TIME_WAIT状态的连接启动新的化身。既然TIME_WAIT状态的持续时间是2MLS,这就足够让某个方向的分组最多存活MSL秒即被丢弃,另一个方向上的应答最多存活MSL秒也被丢弃。
下面我们来给出一个从简单的程序,该程序是客户建立与服务器的TCP连接并读取服务器送回的当前时间的和日期:
config.h ;
#ifndef _CONFIG_H_ #define _CONFIG_H_ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <sys/types.h> #include <unistd.h> #include <errno.h> #include <netinet/in.h> #include <netdb.h> #include <arpa/inet.h> const int MAX_LINE = 4096; #define SA struct sockaddr #define LISTENQ 6666 #endif服务器端程序:
#include <time.h> #include "config.h" int main(int argc,char **argv) { int listenfd,connfd; struct sockaddr_in servaddr; char buff[MAX_LINE]; time_t ticks; if((listenfd = socket(AF_INET,SOCK_STREAM,0)) < 0){ perror("socket error"); exit(1); } bzero(&servaddr,sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(15); if(bind(listenfd,(SA *)&servaddr,sizeof(servaddr)) < 0){ perror("bind error"); exit(1); } if(listen(listenfd,LISTENQ) < 0){ perror("listen error"); exit(1); } for(;;){ if(connfd = accept(listenfd,(SA *)NULL,NULL)){ perror("accept error"); exit(1); } ticks = time(NULL); snprintf(buff,sizeof(buff),"%.24s\r\n",ctime(&ticks)); if(write(connfd,buff,strlen(buff)) != strlen(buff)){ perror("write error"); } close(connfd); } }
#include "config.h" int main(int argc,char **argv) { int sockfd,n; char recvline[MAX_LINE + 1]; struct sockaddr_in servaddr; if(argc != 2){ perror("usage : tcpcli<IPaddress>"); exit(1); } //创建套接字 if((sockfd = socket(AF_INET,SOCK_STREAM,0)) < 0){ perror("socket error"); exit(1); } //设置链接服务器的地址结构 bzero(&servaddr,sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(13); if(inet_pton(AF_INET,argv[1],&servaddr.sin_addr) < 0){ printf("inet_pton error for %s\n",argv[1]); exit(1); } //发送链接服务器的请求 if(connect(sockfd,(SA *)&servaddr,sizeof(servaddr)) < 0){ perror("connect error"); exit(1); } while((n = read(sockfd,recvline,MAX_LINE)) > 0){ recvline[n] = 0; if(fputs(recvline,stdout) == EOF){ perror("fputs error"); exit(1); } } //关闭套接字 close(sockfd); }我们首先来看执行结果:
下面说明几个基本的函数: