TCP学习目录
TCP
传输控制协议(英语:Transmission Control Protocol,缩写为TCP)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。在简化的计算机网络OSI模型中,它完成第四层传输层所指定的功能,用户数据包协议(UDP)是同一层内另一个重要的传输协议。
在因特网协议族(Internet protocol suite)中,TCP层是位于IP层之上,应用层之下的中间层。不同主机的应用层之间经常需要可靠的、像管道一样的连接,但是IP层不提供这样的流机制,而是提供不可靠的包交换。
以上是我在维基百科抄的。
认识TCP
- 传输层协议
传输层有两个协议,各有不同的应用场景,如何使用取决于应用场景,因此两个协议特点尤为重要,相比于UDP协议,TCP用的更加广泛,目前大部分你能使用到的软件(QQ是UDP)基本都是TCP传输协议 - 有连接
TCP具有三次握手来建立连接,相比于UDP多了连接这一步,UDP无需连接便可发送接收数据 - 可靠传输
可靠性通过发送方检测到丢失的传输数据并重传这些数据。包括超时重传与重复累计确认 - 面向字节流
比较灵活,数据无明显边界,但是容易产生粘包问题
TCP协议段格式
- 源/目标端口:表明数据从哪个进程来去哪个进程
- 序列号码:32位序列号
- 如果含有同步化旗标(SYN),则此为最初的序列号;第一个数据比特的序列码为本序列号加一。
- 如果没有同步化旗标(SYN),则此为第一个数据比特的序列码。
- 确认号码:期望收到数据开始的序列号,也就是已接收数据序列号+1
- 数据偏移:4位TCP报头长度,表示TCP头部有多少个32位bit,以4字节来计算数据开始的偏移地址,4位最大可表示15,4字节为1个单位,所以TCP的头部最长为60字节
- 标志位:6位标志
- NS:ECN-nonce。
- CWR:Congestion Window Reduced。
- ECE:ECN-Echo有两种意思,取决于SYN标志的值。
- URG:为1表示高优先级数据包,紧急指针字段有效。
- ACK:为1表示确认号字段有效
- PSH:为1表示是带有PUSH标志的数据,提示接收端应该尽快将这个报文读取走
- RST:为1表示出现严重差错。可能需要重现创建TCP连接。还可以用于拒绝非法的报文段和拒绝连接请求。
- SYN:为1表示这是连接请求或是连接接受请求,用于创建连接和使顺序号同步,我们把带SYN表示的称为同步报文段。
- FIN:为1表示发送方没有数据要传输了,要求释放连接,我们称携带FIN的报文称为结束报文段。
- 窗口:16位长,表示从确认号开始,本报文的接受方可以接收的字节数,即接收窗口大小。用于流量控制。
- 校验和:16位长,对整个的TCP报文段,包括TCP头部和TCP数据,以16位字进行计算所得。这是一个强制性的字段,接收端校验不通过,则认为数据有问题。
- 紧急指针:16位长,本报文段中的紧急数据的最后一个字节的序号。
- 选项字段:最多40字节。每个选项的开始是1字节的kind字段,说明选项的类型。
客户端和服务端连接过程
首先来看大致的连接示意图
服务端初始化
服务端初始化可分为以下几个部分:
- 调用socket,创建套接字文件
- 调用bind绑定,绑定服务端地址信息,将套接字文件ip、port、网络类型全部参数给予结构体,绑定到套接字
- 调用listen开始监听,声明当前套接字文件作为一个接收连接服务端的套接字文件
- 调用accept开始接受连接,没有连接就阻塞等待连接
三次挥手
三次握手建立连接,客户端在创建socket网络套接字后,会通过三次握手这个过程来进行连接:
- 调用connect向服务端发起连接请求,发送了一个SYN,阻塞等待服务端回复(第一次握手)
- 服务端收到客户端发送的SYN,为了让客户端知道他这准备好了,于是回了一个ACK告诉客户端我准备好了,并且发了一个SYN问客户端准备好没(第二次握手)
- 客户端收到服务端回复的ACK,进入ESTABLELISHED状态,准备就绪,并回了一个ACK给服务端说我也准备好了,服务端收到ACK确认了对方状态,于是也进入ESTABLELISHED状态,连接建立(第三次握手)
发送/接收数据
对于第一条数据的发送和接收,服务端永远是等待请求的那个,客户端永远是先发请求的那个。
于是便有了连接建立成功后,便有了服务端调用recv(或调用read)阻塞等待数据接收,接收请求后调用send(或write)回复;客户端则是先调用send(或write)发送一个请求,然后调用recv(或read)等待请求。
重复上面的过程便是发送和接收数据的过程。
TCP提供全双工通信服务,也就是一端读的同时,另一端可以写,与之相对的是半双工,只能有一端读或者一端写。
四次挥手
如果客户端没有请求了,主动关闭,那么便会调用close关闭连接
- 客户端调用close,发送了一个FIN包给服务端,告诉服务端要断开了(第一次挥手)
- 服务端收到FIN后,告诉客户端我知道了要断开连接了,于是发了一个ACK给客户端,同时recv(或read)会返回0(第二次挥手)
- recv和read返回0,于是服务端也调用了close关闭套接字问价,并给客户端发了一个FIN包,告诉客户端我也要关了(第三次挥手)
- 客户端收到FIN,回了一个ACK,说我知道了,断了吧(第四次挥手)
以上过程称为TCP的四次挥手,断开连接
TCP可靠传输如何保证
- 确认应答机制——发送的每条数据都要确认回复
- 超时重传机制——等待回复的过程中,如果没有收到接收端的回复,则会重传数据,超时等待的时间递增,每次等待时间都会较前一次更长,但是有重传次数,重传超过次数便会断开连接
- 序号确认机制——为了保证数据接收有序,每段数据都会带有序号信息,如果碰到网络问题,后面的数据先到了,前面数据后道了,仍能通过序号保证有序
TCP服务端和客户端实现代码
服务端流程
- 创建套接字(socket)
- 绑定地址信息(bind)
- 开始监听(listen)
- 接收连接(accept)
- 接收(recv或read)/发送(send或write)数据
- 关闭套接字(close)
客户端流程
- 创建套接字(socket)
- 绑定地址(不推荐绑定,推荐直接系统分配)
- 开始连接(connect)
- 发送(send或write)/接收(recv或read)数据
- 关闭套接字(close)
代码
github地址:TCP服务端单线程版本
github地址:TCP服务端多线程版本
github地址:TCP客户端
下面贴出的是TCP单线程服务端简单实现代码
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <errno.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char* argv[])
{
//1.创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sockfd < 0)
{
perror("socket failed");
return -1;
}
//2.绑定地址
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[2]));
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t server_addr_len = sizeof(server_addr);
int ret = bind(sockfd, (struct sockaddr*)&server_addr, server_addr_len);
if (ret < 0)
{
perror("bind failed");
return -1;
}
//3.开始监听(listen)
ret = listen(sockfd, 5);
if (ret < 0)
{
perror("listen failed");
return -1;
}
char buf[1024] = {0};
while (1)
{
int new_sockfd;
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
//4.连接成功后获取新的套接字文件描述符
new_sockfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_addr_len);
if (new_sockfd < 0)
{
perror("accept failed");
continue;
}
while (1)
{
memset(buf, 0, 1024);
//5.接收数据
ssize_t recv_len = recv(new_sockfd, buf, 1023, 0);
if (recv_len < 0)
{
perror("recv error");
continue;
}
else if (recv_len == 0)
{
printf("client link to termitate\n");
close(new_sockfd);
break;
}
else
{
printf("client[ip:%s, prot:%d]\n%s\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buf);
memset(buf, 0, 1024);
scanf("%s", buf);
//6.发送数据
send(new_sockfd, buf, strlen(buf), 0);
}
}
close(new_sockfd);
}
//7.关闭套接字
close(sockfd);
return 0;
}