前言
这是我参与「第三届青训营 -后端场」笔记创作活动的的第3篇笔记。
TCP协议特点
TCP的连接与断开
TCP 协议提供的是:面向连接、可靠的、字节流服务。
使用 TCP 协议通信的双发必须先建立连接,然后才能开始数据的读写。双方都必须为该连接分配必要的内核资源,以管理连接的状态和连接上数据的传输。TCP 连接是全双工的,双方的数据可以通过一个连接进行读写。完成数据交换之后,通信双方都必须断开连接以释放系统资源。
-
三次握手发生在客户端执行 connect()的时候,该方法返回成功,则说明三次握手已经建立。
- 四次挥手发生在客户端或服务端执行 close()关闭连接的时候
TCP状态转移图
TCP 连接的任意一端在任一时刻都处于某种状态,当前状态可以通过 netstat 命令查看,下图是 TCP 连接从建立到关闭整个过程中通信两端状态的变化。其中 CLOSED 是假想的起始点,并不是一个实际的状态。
应答确认和超时重传
TCP 发送的报文段是交给 IP 层传送的。但 IP 层只能提供尽最大努力的服务,也就是说,TCP 下面的网络所提供的是不可靠的传输。因此,TCP 必须采用适当的措施才能使两个运输层之间的通信变得可靠。TCP 的可靠传输是通过使用应答确认和超时重传来完成。
无差错时候,数据交互的流程: 发送端发送数据 m1 给接收端,接收端收到数据后会给发送端一个确认信息,以表明数据已经被成功收到。在发送方未收到确认信息前,M1应继续被保留,直到确认信息到达才能丢弃。
应答确认图示
超时重传:
滑动窗口
TCP 协议是利用滑动窗口实现流量控制的。一般来说,我们总是希望数据传输得更快一些,不会一次只发一个字节。但是如果发送方把数据发得过快,接受方就可能来不及接收,这就会造成数据的丢失。所谓流量控制就是让发送方的发送速率不要太快,要让接收方来的及接收。
在 TCP 的报头中有一个字段叫做接收通告窗口,这个字段由接收端填充,是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。所以发送端就会有一个发送窗口,这个发送窗口的大小是由接收端填充的接收通告窗口的大小决定的,并且窗口的位置会随着发送端数据的发送和接收到接收端对数据的确认而不断的向右滑动,将之称为滑动窗口。
当收到 36 的 ack,并发出 46-51 的字节后,窗口滑动的示意图如下:
拥塞控制
TCP模块还有一个重要任务,就是提高网络利用率,降低丢包率,并保证网络资源对每条数据流的公平性,这就是所谓的拥塞控制。
拥塞控制是一个全局性的过程,涉及到所有主机, 所有路由器,以及与降低网络传输性能有关的所有因素。
几种拥塞控制的方法:
- 慢启动
- 拥塞避免
- 快速重传
- 快速恢复
图示
TCP编程流程
服务端示例
// TCP Server.c
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<netinet/in.h>
int main(void)
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
assert(sockfd != -1);
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(sockfd, (struct sockaddr*)&saddr, sizeof(saddr));
assert(res != -1);
res = listen(sockfd, 5);
assert(res != -1);
while (1)
{
struct sockaddr_in caddr;
socklen_t len = sizeof(caddr);
int c = accept(sockfd, (struct sockaddr*)&caddr, &len);
if (c == -1)
{
perror("accept error");
continue;
}
printf("accept c = %d\n", c);
char data[128] = {0};
int n = recv (c, data, 127, 0);
printf("n = %d, buff = %s\n", n, data);
send(c, "OK", 2, 0);
close(c);
}
close(sockfd);
exit(0);
}
客户端示例
//TCP Client.c
#include<stdio.h>
#include<assert.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<netinet/in.h>
int main(void)
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
assert(sockfd != -1);
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = connect(sockfd, (struct sockaddr*)&saddr, sizeof(saddr));
assert(res != -1);
printf("Please input data:\n");
char buff[128] = {0};
fgets(buff, 128, stdin);
send(sockfd, buff, strlen(buff) - 1, 0);
char data[128] = {0};
int n = recv(sockfd, data, 127, 0);
printf("%s\n", data);
close(sockfd);
exit(0);
}