TCP协议是UDP协议的改进版,TCP协议可以保证传输的可靠性,TCP协议的优势如下:
1、TCP协议提供可靠的传输服务(不会出现错误、乱码、丢失、误码、重复)
2、面向连接的传输方式
3、一条TCP只能进行两个端点一对一通信,通信双方固定,就是连接的两个点。如果想实现多机则需要创建多条TCP。
4、面向字节流传输(数据只有先后之分,也就是一个发数据一个收数据错开,不可能同时收发数据)半双工模式
5、可靠传输:有拥塞控制、流量控制、超时重传。
上面是一些TCP协议的优点,下面来介绍以下TCP通信如何确保数据的可靠的,它的传输形式又是怎么样的。
TCP协议传输数据的组成是 TCP头+用户数据
上图就是TPC的头,由很多部分构成:(加粗部分用于建立连接和释放连接所需要用到的)
源端口 16bit 发送数据方的端口号(也就是自己的),标识发送该TCP报文的应用程序
目标端口 16bit 接收方的端口号(发送到那的),标识接收该TCP报文的应用程序
序号 32bit 该TCP报文的数据的第一个字节的序号
确认号 32bit 这是接收收到数据,返回的内容〈TCP报文〉,标识接收到多少内容,如确认号为n,准确接收到n-1个字节
数据偏移:4bit 以四字节为单位,当前这个TCP报文距离整个TCP报文开始有多远(看TCP头有多大,向后偏移就多少)
保留:6bit 无用
确认ACK 1bit 只有确认ACK为1时,才表示确认号有效
同步:1bit 在建立连接时可以让客户端和服务端同步,只有在建立的时候才用
终止:1bit 在断开连接、释放TCP连接时需要将这位为1,平时为0
复位:1bit 复位TCP连接使用,
窗口:发送TCP报文对应接收方的窗口大小
校验和:用于校验信息是否正确
TCP通信:
TCP通信为了保证传输可靠,必须在通信双方间建立连接,只有建立了连接之后TCP通信才是可靠通信,且确定唯一的通信双方 。所以TCP要进行通信的双方操作就存在着区别,一个进程应该主动的去进行连接,另一个进程应该被动等待连接请求,然后建立连接进行通信,客户端的通信流程和服务端的通信流程不相同。
TCP通信大致流程如上图所示,在理论通信中,在建立连接时采用三次握手进行连接,四次挥手进行断开连接。
何以为三次握手?
就是为了确保在建立TCP通信连接时,能够成功的连接上,在上面图中写了TCP传输时需要将信息与TCP头进行捆绑,而在连接时也和TCP头有关。在建立连接时主要解决:知道双方、协商一部分数据、双方支援进行分配。下面开始说,如何进行三次握手:
第一次握手:TCP客户端打算建立连接,向服务端发送请求连接报文,客户端请求建立连接,客户端进入同步发送状态(SYN-SENT),既在发送的报文中把SYN 设置为1,表示这是一个请求连接报文把seq(序号)设置为x(这是一个初始值),作为TCP客户端的初始序号。
第二次握手:TCP服务端接收到客户端连接请求后,如果同意进行连接,则会向客户端发送确认请求报文,服务端进入同步接收状态(SYN-RECV),在返回客户端的报文中把SYN和ACK位都设置为1,把序号seq设置为一个初始值y,作为服务端的初始序号并将确认号字段ack = x + 1,作为对客户端的确认。
第三次握手:TCP客户端在接收了服务端的确认后,还要向服务端发送一个确认报文,并进入已连接状态(ESTABLISHED),在发送的报文中ACK设置为1,seq设置为x + 1,确认号ack =y+1,这是对tcp服务端的确认的确认,做到这里服务端也将进入已连接状态(ESTABLISHED)
什么叫四次挥手?
关闭(释放)连接步骤有四步,叫做四次挥手 。
第一次挥手:TCP客户端主动通知服务端要断开连接,关闭TCP连接;TCP客户端发送连接释放(关闭)报文,并进入终止等待1状态。发送的报文当中需要将TCP头中部分置位,把FIN(终止位)设置为1(表示断开),把ACK设置为1,对之前的报文做确认,序号seq设置为u,等于之前传送的数据的最后一个字节+1确认号ack设置为v,等于之前接收的数据最后一个字节+1
第二次挥手:TCP服务端接收到TCP客户端断开连接请求,返回一个普通的确认报文给客户端,服务端并进入关闭等待状态,客户端进入终止等待2状态(只能服务端发送,客户端接收)。返回的报文中将确认号ACK设置为1,表示这是一个普通的确认报文,服务端序号seq设置为v,表示服务端最后发送的一个字节序号+1,确认号ack = u+1,继续把之前没有发送完的数据继续完成发送。
第三次挥手:服务端给客户端发送连接释放报文,并进入最后确认状态,发送断开请求报文:设置FIN(终止)设置为1,表示这是一个TCP连接释放报文 ,ACK设置为1,表示对之前的报文做确认 ,seq序号设置w,服务器进入半关闭状态 ,确认号ack设置为u+1,表示释放的重复确认。
第四次挥手:tcp客户端收到连接释放请求,发送普通确认报文,进入时间等待状态。ACK设置为1,表示是一个普通确认 ,序号设置为u+1,表示这是一个释放报文 ,确认号ack设置为w+1,表示是一个确认报文。
而实际通信中TCP通信客户端和服务端操作流程如下:
1.客户端
2.服务端
也就是如下操作流程,上面是案例描述:
在这里需要学习一些新的API函数:
1、请求建立连接(TCP协议属于面向连接方式进行通信,这样才提供可靠性)
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
使用当前进程的套接字与某个服务端建立连接 建立连接:与服务端建立连接,把套接字与服务端建立连接
int connect(int socket, const struct sockaddr *addr,socklen_t addrlen);
参数1:客户端套接字,表示用客户端哪个套接字(ip、port)与服务端建立连接, 进行通信。
参数2:客户端套接字要与哪个服务端建立建立,结构体中是服务端的ip、prot。
参数3:结构体大小(参数2的结构体)
成功返回0 失败返回-1
2、监听套接字
服务端监视自己的ip、port,看是否有客户端连接 等待客户端的连接请求,只要做监视操作,就让内核创建一个队列,把所有的客户端连接请求都放在队列 去(作为等待,等待接受),之后如果也有客户端连接请求,也会放入等待队列.
int listen(int sockfd, int backlog);
参数1:要监视的套接字,服务端的网络套接字
参数2:等待客户端个数,也就是允许最多连接多少个。好确定队列的大小
成功返回0 失败返回-1
3、接收客户端连接
从之前listen监听等待的客户端连接中,取出第一个,进行接受,进行通信,如果在刚才listen监视队 列中没有客户端连接请求,就会阻塞等待,一定要取一个客户端连接请求进行通信.
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数1:监视的套接字
参数2:客户端的ip、port
参数3:结构体大小
成功:返回客户端进行通信的套接字文件描述符,之前listen的套接字被用来做监视作用
失败:返回-1
示例:
客户端
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0)
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.a_addr = inet_addr("xxx.xxx.xxx.xxx"); 自己的网络地址
bind(sockfd,(struct sockaddr *)&addr,sizeof(addr)); 将自己ip、port绑定到sockfd上
struct sockaddr_in serveraddr; 要连接的服务端信息
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(1111);
serveraddr.sin_addr.a_addr = inet_addr("xxx.xxx.xxx.xxx"); 要连接的的网络地址
//sockfd就是当前进程的套接字(有当前进程网络信息),创建连接,失败打印
if( connect(sockfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr)) < 0 )
{
perror("connect error\n");
return -1;
}
char buf[20];
while(1)
{
//要进行做的事情
read(sockfd,buf,20); //读取服务端传过来的数据
write(sockfd,buf,20); //往服务端写内容
}
}
服务端
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in addr;//网络信息结构体 //服务端的网络信息
addr.sin_family = AF_INET;
addr.sin_port = htons(9999);
addr.sin_addr.s_addr = inet_addr("xxx.xxx.xxx.xxx");
bind(sockfd,(struct sockaddr *)&addr,sizeof(addr));
listen(sockfd,10);//不会阻塞,告诉内核,要监视这个ip、port,把客户端的连接请求存放等待
//接受客户端连接请求
struct sockaddr_in addr_in;
socklen_t len;
int fd = accept(sockfd,(struct sockaddr *)&addr_in,&len); //fd是接收客户端的文件描述符
char buf[20];
while(1)
{
int num = read(fd,buf,20);
printf("server : %s\n",buf);
write(fd,"ok",3);
}
return 0;
}
配置好这些后就可以进行。有些函数是在UDP里面进行学习的。