今天和大家分享一些基本套接口的编程函数,为帮助大家更好的理解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);
}
我们首先来看执行结果:
下面说明几个基本的函数:
SOCKET PASCAL FAR socket(int af, int type, int protocol);
(2)二是数据报式套接字(SOCK_DGRAM)提供了一个无连接服务。数据包以独立包形式被发送,不提供无错保证,数据可能丢失或重复,并且接收顺序混乱。网络文件系统(NFS)使用数据报式套接字。
(3)三是原始式套接字(SOCK_RAW)该接口允许对较低层协议,如IP、ICMP直接访问。常用于检验新的协议实现或访问现有服务中配置的新设备。
参数protocol说明该套接字使用的特定协议,如果调用者不希望特别指定使用的协议,则置为0,使用默认的连接模式。根据这三个参数建立一个套接字,并将相应的资源分配给它,同时返回一个整型套接字号。因此,socket()系统调用实际上指定了相关五元组中的“协议”这一元。
2 指定本地地址──bind()
当一个套接字用socket()创建后,存在一个名字空间(地址族),但它没有被命名。bind()将套接字地址(包括本地主机地址和本地端口地址)与所创建的套接字号联系起来,即将名字赋予套接字,以指定本地半相关。int PASCAL FAR bind(SOCKET s, const struct sockaddr FAR * name, int namelen);
3 建立套接字连接──connect()与accept()
这两个系统调用用于完成一个完整相关的建立,其中connect()用于建立连接。accept()用于使服务器等待来自某客户进程的实际连接。int PASCAL FAR connect(SOCKET s, const struct sockaddr FAR * name, int namelen);
如果没有错误发生,connect()返回0。否则返回值SOCKET_ERROR。在面向连接的协议中,该调用导致本地系统和外部系统之间连接实际建立。
由于地址族总被包含在套接字地址结构的前两个字节中,并通过socket()调用与某个协议族相关。因此bind()和connect()无须协议作为参数。
SOCKET PASCAL FAR accept(SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen);
4 监听连接──listen()
此调用用于面向连接服务器,表明它愿意接收连接。listen()需在accept()之前调用,int PASCAL FAR listen(SOCKET s, int backlog);
调用listen()是服务器接收一个连接请求的四个步骤中的第三步。它在调用socket()分配一个流套接字,且调用bind()给s赋于一个名字之后调用,而且一定要在accept()之前调用。
5 数据传输──send()与recv()
当一个连接建立以后,就可以传输数据了。常用的系统调用有send()和recv()。send()调用用于s指定的已连接的数据报或流套接字上发送输出数据,格式如下: