TCP协议及状态

一、TCP协议api

1.客户端接口

socket();connect();send();recv();close()
函数原型
	int sockfd=socket(int domain, int type, int protocol);
函数介绍
	domain:协议域,又称协议族(family)。常用的协议族有AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域Socket)、AF_ROUTE等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
	type:指定Socket类型。常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。流式Socket(SOCK_STREAM)是一种面向连接的Socket,针对于面向连接的TCP服务应用。数据报式Socket(SOCK_DGRAM)是一种无连接的Socket,对应于无连接的UDP服务应用。
	protocol:指定协议。常用协议有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。(注意:1.type和protocol不可以随意组合,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当第三个参数为0时,会自动选择第二个参数类型对应的默认协议。)
	返回值:如果调用成功就返回新创建的套接字的描述符,如果失败就返回INVALID_SOCKET(Linux下失败返回-1)。套接字描述符是一个整数类型的值。每个进程的进程空间里都有一个套接字描述符表,该表中存放着套接字描述符和套接字数据结构的对应关系。该表中有一个字段存放新创建的套接字的描述符,另一个字段存放套接字数据结构的地址,因此根据套接字描述符就可以找到其对应的套接字数据结构。每个进程在自己的进程空间里都有一个套接字描述符表但是套接字数据结构都是在操作系统的内核缓冲里。
		
函数原型
	int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函数介绍
	sockfd:标识一个未连接socket
	addr:指向要连接套接字的sockaddr结构体的指针
	addrlen 结构体的字节长度
	返回值:成功则返回0, 失败返回-1, 错误原因存于errno 中. 注意一般默认connect是阻塞的,意思是线程 调用的时候三次握手不成功会一直阻塞到那里,如果客户端的socket描述符为阻塞模式,则connect()会阻塞到连接建立成功或连接建立超时(linux内核中对connect的超时时间限制是75s, Soliris 9是几分钟,因此通常认为是75s到几分钟不等)。如果为非阻塞模式,则调用connect()后函数立即返回,如果连接不能马上建立成功(返回-1),则errno设置为EINPROGRESS,此时TCP三次握手仍在继续。此时可以调用select()检测非阻塞connect是否完成。select指定的超时时间可以比connect的超时时间短,因此可以防止连接线程长时间阻塞在connect处。		
		
函数原型
	#include <sys/socket.h>
	ssize_t send(int sockfd, const void *buf, size_t len, int flags);
函数介绍
	sockfd :用于通信的文件描述符(服务器:sockfd为accept返回的通信描述符)
	buf :应用缓存,用于存放要发送到数据,可以是任何类型:结构体,int , char,float,字符串
	len: buf的大小
	flags 一般设置为0,此时send为阻塞式发送即发送不成功会一直阻塞,直到被某个信号终端终止,或者直到发送成功为止。指定MSG_NOSIGNAL,表示当连接被关闭时不会产生;SIGPIPE信号指定;MSG_DONTWAIT 表示非阻塞发送
	返回值:成功则返回实际传送出去的字符数,失败返回-1,错误原因存于errno 中。
	
函数原型
	头文件 #include <sys/socket.h>
	ssize_t recv(int sockfd, void *buf, size_t len, int flags);
函数介绍
	sockfd :用于通信的文件描述符
	buf 应用缓存,用于存放要发送到数据可以是任何类型:结构体,int , char,float,字符串
    len :buf的大小
    flags 一般设置为0,此时send为阻塞式发送即发送不成功会一直阻塞,直到被某个信号终端终止,或者直到发送成功为止。指定MSG_NOSIGNAL,表示当连接被关闭时不会产生SIGPIPE信号;指定MSG_DONTWAIT 表示非阻塞发送;指定MSG_OOB 表示带外数据
	返回值:成功返回发送的字节数;失败返回-1,同时errno被设置
		
函数原型
	int close(int sockfd);
函数介绍
	sockfd :用于通信的文件描述符
	返回值 :返回成功为0,出错为-1

2.服务端接口

	socket();bind();listen();accept();recv();send();close()
	对于服务端来说,bind ,listen,和accept函数是与客户端不同的。其他的函数和客户端一样,这里着重只介绍这三个函数
函数原型
	int bind(int sockfd, const struct sockaddr addr,socklen_t addrlen);
函数介绍
	当用socket()函数创建套接字以后,套接字在名称空间(网络地址族)中存在,但没有任何地址给它赋值。bind()把用addr指定的地址赋值给用文件描述符代表的套接字sockfd。addrlen指定了以addr所指向的地址结构体的字节长度。一般来说,该操作称为“给套接字命名”
	sockfd :用于通信的文件描述符
	sockaddr:结构定义为如下格式: struct sockaddr { sa_family_t sa_family; char sa_data[14]; }
	addrlen:指定了以addr所指向的地址结构体的字节长度
	返回值 :成功,返回0;出错,返回-1,相应地设定全局变量errno。

函数原型
	int listen(int sockfd, int backlog)
函数介绍
	listen函数使用主动连接套接字变为被连接套接口,使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。在TCP服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接。
	sockfd: 一个已绑定未被连接的套接字描述符
	backlog:规定了内核应该为相应套接口排队的最大连接个数
	返回值 :无错误,返回0,否则,返回-1
	
函数原型
	int accept(int sockfd, struct sockaddr addr, socklen_t len)
函数介绍
	accept一般默认是阻塞的,意思就是一直等客户端完成三次握手建立连接。
	sockfd, 利用系统调用socket()建立的套接字描述符,通过bind()绑定到一个本地地址(一般为服务器的套接字),并且通过listen()一直在监听连接;
	addr, 指向struct sockaddr的指针,该结构用通讯层服务器对等套接字的地址(一般为客户端地址)填写,返回地址addr的确切格式由套接字的地址类别(比如TCP或UDP)决定;若addr为NULL,没有有效地址填写,这种情况下,addrlen也不使用,应该置为NULL;备注:addr是个指向局部数据结构sockaddr_in的指针,这就是要求接入的信息本地的套接字(地址和指针)。
	addrlen, 一个值结果参数,调用函数必须初始化为包含addr所指向结构大小的数值,函数返回时包含对等地址(一般为服务器地址)的实际数值;备注:addrlen是个局部整形变量,设置为sizeof(struct sockaddr_in)。
	返回值:非负描述字——成功,这个值就是连接套接字,然后数据传输就是利用该返回值, -1——失败,具体错误可以查看error值

3.服务端客户端连接通信

在这里插入图片描述
C语言代码实现
代码如下(示例):

//客户端代码
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
 
#define MAXLINE 2048
#define PORT 8080
 
int main(void)
{
	int sockfd = -1; //定义客户端套接字
	struct sockaddr_in servaddr; //定义想连接的服务器的套接字地址
	char sendbuf[MAXLINE], recbuf[MAXLINE];//发送和接收数据的缓冲区
	memset(&servaddr, 0, sizeof(servaddr)); //初始化服务器套接字地址
	servaddr.sin_family = AF_INET;//IPv4
	servaddr.sin_port = htons(PORT);//想连接的服务器的端口
	servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");//服务器的IP地址
	if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)//创建套接字
	{
		printf("create socket error: %s(error: %d)\n", strerror(errno), errno);
		exit(0);
	}
	if(connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1) //向服务器发送连接请求
	{
		//连接失败
		printf("connect socket error: %s(error: %d)\n", strerror(errno), errno);
		exit(0);
	}
	while(1)
	{
		printf("向服务器发送信息:");
		fgets(sendbuf, sizeof(sendbuf), stdin);
		send(sockfd, sendbuf, sizeof(sendbuf), 0); //向服务器发送信息		
		ssize_t len = recv(sockfd, recbuf, sizeof(recbuf),0); //从服务器接收信息
		if(len < 0)
		{
			if(errno == EINTR)
			{
				continue;
			}
			exit(0);
		}
		printf("服务器回应:%s\n", recbuf);
	}
	close(sockfd); //关闭套接字
}


//服务器端代码
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
 
#define MAXLINE 4096
#define PORT 8000
 
int main(void)
{
	
	int listen_fd = -1, connect_fd = -1;//定义服务器监听套接字和连接套接字,初始化为-1
	struct sockaddr_in servaddr;//定义服务器对应的套接字地址	
	char sendbuf[MAXLINE], recbuf[MAXLINE]; //服务器接收和发送缓冲区
	memset(&servaddr, 0, sizeof(servaddr)); //初始化套接字地址结构体
	servaddr.sin_family = AF_INET;//IPv4
	servaddr.sin_port = htons(PORT);//设置监听端口
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//表示接收任意IP的连接请求
	if((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) //创建套接字
	{
		printf("create socket error: %s(error: %d)\n", strerror(errno), errno); //如果创建套接字失败,返回错误信息,strerror(int errnum)获取错误的描述字符串
		exit(0);
	}
	if(bind(listen_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1) //绑定套接字和本地IP地址和端口
	{
		printf("bind socket error: %s(error: %d)\n", strerror(errno), errno); //绑定出现错误
		exit(0);
	}
	if(listen(listen_fd, 10) == -1) //使得listen_fd变成监听描述符
	{
		printf("listen socket error: %s(error: %d)\n", strerror(errno), errno);
		exit(0);
	}
	printf("等待客户端发起连接\n");
	while(1)
	{
		if((connect_fd = accept(listen_fd, (struct sockaddr*)NULL, NULL)) == -1) //accept阻塞等待客户端请求
		{
			printf("accept socket error: %s(error: %d)\n", strerror(errno), errno);
			continue;
		}
		
		while(1) //可以一直保持连接
		{
			ssize_t len = recv(connect_fd, recbuf, sizeof(recbuf),0); //读取客户端发来的信息
			if(len < 0)
			{
				if(errno == EINTR)
				{
					continue;
				}
				exit(0);
			}
			printf("接收客户端的请求:%s\n", recbuf);
			printf("回复客户端信息:");
			fgets(sendbuf, sizeof(sendbuf), stdin);
			send(connect_fd, sendbuf, sizeof(sendbuf), 0); //向客户端发送信息
		}
		close(connect_fd); //关闭连接套接字
	}
	close(listen_fd); //关闭监听套接字
}

二、TCP的三次握手及状态

1.图形简单介绍

在这里插入图片描述
三次握手(Three-way Handshake),是指建立一个 TCP 连接时,需要客户端和服务器总共发送3个包,客户端执行 connect() 时,将触发三次握手,服务器端是在listen()和accept()之间

第一次握手(SYN=1, seq=x):
	客户端发送一个 TCP 的 SYN 标志位置1的包,指明客户端打算连接的服务器的端口,以及初始序号 X,保存在包头的序列号(Sequence Number)字段里。发送完毕后,客户端进入 SYN_SEND 状态。
第二次握手(SYN=1, ACK=1, seq=y, ACKnum=x+1):
	服务器发回确认包(ACK)应答。即 SYN 标志位和 ACK 标志位均为1。服务器端选择自己 ISN 序列号,放到 Seq 域里,同时将确认序号(Acknowledgement Number)设置为客户的 ISN 加1,即X+1。 发送完毕后,服务器端进入 SYN_RCVD 状态。

第三次握手(ACK=1,ACKnum=y+1)
	客户端再次发送确认包(ACK),SYN 标志位为0,ACK 标志位为1,并且把服务器发来 ACK 的序号字段+1,放在确定字段中发送给对方,并且在数据段放写ISN的+1发送完毕后,客户端进入 ESTABLISHED 状态,当服务器端接收到这个包时,也进入 ESTABLISHED 状态,TCP 握手结束。

三、TCP的四次挥手及状态

1.图形简单介绍

在这里插入图片描述
TCP 的连接的拆除需要发送四个包,因此称为四次挥手(Four-way handshake),也叫做改进的三次握手。客户端或服务器均可主动发起挥手动作,在 socket 编程中,任何一方执行 close() 操作即可产生挥手操作。

第一次挥手(FIN=1,seq=x)
	假设客户端想要关闭连接,客户端发送一个 FIN 标志位置为1的包,表示自己已经没有数据可以发送了,但是仍然可以接受数据。发送完毕后,客户端进入 FIN_WAIT_1 状态。

第二次挥手(ACK=1,ACKnum=x+1)
	服务器端确认客户端的 FIN 包,发送一个确认包,表明自己接受到了客户端关闭连接的请求,但还没有准备好关闭连接。发送完毕后,服务器端进入 CLOSE_WAIT 状态,客户端接收到这个确认包之后,进入 FIN_WAIT_2 状态,等待服务器端关闭连接。

第三次挥手(FIN=1,seq=y)
	服务器端准备好关闭连接时,向客户端发送结束连接请求,FIN 置为1。发送完毕后,服务器端进入 LAST_ACK 状态,等待来自客户端的最后一个ACK。

第四次挥手(ACK=1,ACKnum=y+1)
	客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入 TIME_WAIT状态,等待可能出现的要求重传的 ACK 包。服务器端接收到这个确认包之后,关闭连接,进入 CLOSED 状态。客户端等待了某个固定时间(两个最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,没有收到服务器端的 ACK ,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入 CLOSED 状态。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值