本篇学习TCP协议的原理以及基础编程
TCP
TCP(Transmission Control Protocol)是一种用于在计算机网络中传输数据的协议。它是互联网协议套件中的一个重要协议之一,通常与IP(Internet Protocol)一起使用,形成TCP/IP协议栈。
TCP是有连接,可靠传输,面向字节流,全双工
TCP协议格式
- 源/目的端口号:表示数据是从哪个进程来,到哪个进程去;
- 32位序号/32位确认号:后面详细讲;
- 4位TCP报头长度:表示该TCP头部有多少个32位bit(有多少个4字节);所以TCP头部最大长度是15 * 4 = 60
- 6位标志位:
URG:紧急指针是否有效
ACK:确认号是否有效
PSH:提示接收端应用程序立刻从TCP缓冲区把数据读走
RST:对方要求重新建立连接;我们把携带RST标识的称为复位报文段
SYN:请求建立连接;我们把携带SYN标识的称为同步报文段
FIN:通知对方,本端要关闭了,我们称携带FIN标识的为结束报文段 - 16位窗口大小
- 16位校验和:发送端填充,CRC校验。接收端校验不通过,则认为数据有问题。此处的检验和不光包含TCP首部,也包含TCP数据部分。
- 16位紧急指针:标识哪部分数据是紧急数据;
- 40字节头部选项:忽略;
TCP原理
TCP对数据传输提供的管控机制,主要体现在两个方面:安全和效率。
这些机制和多线程的设计原则类似:保证数据传输安全的前提下,尽可能的提高传输效率。
确认应答机制(安全机制)
TCP怎样实现的可靠传输
可靠传输并不是说,100%可以传输过去,而是尽可能的传输过去,如果传输不过去,发发送方至少可以知道自己有没有传过去。
核心机制,确认应答,实现可靠性的最核心的机制,核心机制在于,接收方,收没收到会有个应答
应答报文(ACK acknowledge)
而为了解决网络中后发先至的问题,我们就就需要针对消息进行编号!!给发送的消息分配一个“序号”,同时应答报文,给出“确认序号”
由于TCP是面向字节流,TCP针对每个字节都去编号(并没有“一条消息,两条消息的说法”从前往后每个字节进行编号)
确认序号的规则:
并不是发送方的序号是什么,确实序号就是什么,而是取的是发送方发过来的所以数据,最后一个字节的下一个字节的序号:含义为确定序号前的数据已经收到,可以接受从确认序号开始的数据
TCP自身有一个接收缓冲区(一块内核中的内存空间),每个socket都有一份自己的缓冲区,TCP可以按照序号针对消息进行整队(也是TCP的一个重要用途),这样可以使应用程序读到的数据一定是有序的,和发送顺序一样
超时重传(安全机制)
在数据传输过程中,要发送给发送方的数据丢失,接收方收不到数据,就无法返回ack。
发送方就迟迟拿不到应答报文,等待一段时间后,还是没有收到应答报文,发送方就会认为刚才的数据丢包了,就会重发一遍。
网络丢包是概率事件
发送方对于丢包的判定,是一段时间,没有收到ack
- 数据直接丢了,接受方没有收到,自然不会发ack
- 接收方收到数据了,返回的ack丢了
这两种操作都会触发超时重传
而在TCP中,数据缓冲区域会根据收到的数据的序号,自动去重,保证了应用数据读到的数据仍有一份。
而TCP针对连续多个包丢失,虽然会继续超时重传,但是每丢包一个,超时等待时间,就会变长,(重传的频率降低了)连续多次重传,都无法得到ACK,TCP就会尝试重置连接,如果连接也失效,TCP就会关闭连接,放弃网络通信。
一切顺利,使用确认应答保证可靠性;出现丢包,使用超时重传进行补充。
确认应答和超时重传这两个机制,是TCP可靠性的基石。
连接管理(安全机制)
TCP建议连接:三次握手
握手(handsha)指的是通信双发,进行一次网络交互
相当于客户端与服务器之间,通过三次交互建立了连接关系(连接:双发各自记录了对方的信息)
syn称为同步报文段,意思是一方要向另一方,申请建立连接
在tcp中有6给个特殊的比特位
这几位默认是0,如果设为1,则有特殊含义
第二位为ACK,如果这一位为1,则表示这个TCP数据报为应答报文,同时16位窗口大小就会生效,这里的值就是建议发送方发送的窗口大小。
第五位为SYN,如果这一位为1,则表示这个TCP数据报为同步报文
第六位为FIN,如果这一位为1,则表示这个TCP数据报为结束报文
如果一个TCP数据报,第二位和第五位都是1,则这个报文为syn+ack报文
为什么要三次握手?
验证了客户端和服务器,各自的发送能力和接收能力是否正常
这是后续可靠传输的基础
TCP断开连接:四次挥手
通信双方,各自给对方发送一个FIN(结束报文),再各自给对方返回ACK
客户端和服务器都可以先给对方发送FIN
通常情况下,ACK和FIN是不能合并的
三次挥手,ack和syn是同一个时机触发的(都是内核完成的)等连接建立了,服务器accept把建立好的连接从内核拿到应用程序中
四次挥手,ack和fin则是不同时间触发的(ack由内核完成,收到find时候会第一时间返回,fin则是由应用程序代码控制的,在调用到socket的close方法的时候才会出发fin)
滑动窗口(效率机制)
在TCP数据传输过程中,发送数据等待应答,虽然可靠性提高,但是效率有所降低,
所以,在传输数据的过程中,一次发送多个数据,进行批量的传数数据,这个过程称为滑动窗口
批量不是无限发送,是发送到一定程度,就等待ACK,不等待直接发送的数据是有上限的,而是回来一个ACK就立即发下一条,相当于总的要批量等待是一致的,把批量等待数据的数量,就称为“窗口大小”
滑动是,收到一个ACK就立即发下一条
批量发送,丢包的处理:
在传输过程中,中间的ACK丢失,由于ack的性质,在确认序号前的数据都被认为传输成功,所以在后面传输的ack有确认序号,就会忽略,最后一位丢失,会对前一个ACK进行包含,不影响可靠传输。后一位ACK,则会超时重传
在传输过程中,数据丢失,在返回的ack中,会在丢失数据位置进行返回,在数据缓冲区会把除丢失数据外的数据进行缓存,丢失数据返回ACK进行索要,直到数据完整,没有进行余操作,也被称为快速重传
流量控制(安全机制)
通过流量控制,本质上就是让接收方来限制一些发送方的速度,本质上也是保证可靠性
接收方如何计算窗口大小,直接拿接受缓冲区的剩余空间大小作为窗口大小
发送方发送数据,接收方返回报文的时候返回剩余的空间大小,再次接受数据,直至缓冲区剩余空间为0,发送方停止发送数据,但会发送时窗口探测报文,查询是否有剩余空间,当应用程序socket读数据就会消费接受缓冲区的内容就,就会有剩余空间,当有空间后继续发送
拥塞控制(安全机制)
滑动窗口大小取决于流量控制和阻塞控制
做的事情就是衡量中间节点,传输能力
拥塞控制通过实验的方式,找到一个合适的速率,开始的时候,按照一个小的速率进行传输(慢开始),如果不丢包就提高一下速率(扩大一下窗口大小),如果出现丢包就会立即把速率降低,并且重复上述过程,这样的拥塞控制也是为了更好的适应时刻变化的网络环境
拥塞窗口(拥塞控制,实验出来的窗口)
流量控制的窗口
实际发送方的窗口大小 =min(拥塞窗口,流控窗口),谁小按谁
在开始传输过程中,会有一个慢开始的过程,由于开始速率十分小,会按照指数规律增长,当到达设定ssthresh的初始值时,就会进行线性增长,避免一下超过上限很多,可以时传输速度,逐渐接近传输上限,当增长到一定程度,出现丢包,就会认为当前窗口太大,会立即把窗口回归到一个较小的初始值,重复上述过程
延迟应答(效率机制)
通过延时,让接收方应用程序,可以多消费一些数据,此时反馈的窗口大小就会更大一些,发送方的发送速率也能更快一些(同时也能满足接收方能够处理过来)
延时应答的处理方法:
数量限制:每隔N个包就应答一次;
时间限制:超过最大延迟时间就应答一次;
捎带应答(效率机制)
客户端和服务器的通信模型
1.一问一答,绝大部分服务器都是这样
2.多问一答,上传大文件
3.一问多答,下载大文件
4.多问多答,游戏串流
当发送方发送数据,接收方会返回ack的同时也会返回其他数据,但是ack时内核负责,会立即返回,而应用程序必须通过一系列代码执行才能返回,
两者的数据发送时机不会相同,但是因为延时应答,ack就可能等待一段时间再发送,和应用程序合并成一个数据报,称为捎带应答
其他特性
粘包问题
当A给B发送了多个应用层数据报之后,这些数据报累积到B的数据缓冲区中,紧紧的挨在一起,此时B的应用程序在读取数据时,就难以区分从哪到哪是一个完整的应用层数据报,很容易多出半个/多个包
解决问题:定义分割符或者约定整个数据报的长度,同时这俩给解决方案都是自定义应用层协议的注意事项
那么如何避免粘包问题呢?归根结底就是一句话,明确两个包之间的边界。
解决
- 对于定长的包,保证每次都按固定大小读取即可;例如上面的Request结构,是固定大小的,那么就从缓冲区从头开始按sizeof(Request)依次读取即可;
- 对于变长的包,可以在包头的位置,约定一个包总长度的字段,从而就知道了包的结束位置;
- 对于变长的包,还可以在包和包之间使用明确的分隔符(应用层协议,是程序猿自己来定的,只要保证分隔符不和正文冲突即可);
对于UDP协议来说,是否也存在 “粘包问题” 呢?
对于UDP,如果还没有上层交付数据,UDP的报文长度仍然在。同时,UDP是一个一个把数据交付给应用层。就有很明确的数据边界。
站在应用层的站在应用层的角度,使用UDP的时候,要么收到完整的UDP报文,要么不收。不会出现"半个"的情况。
异常情况
进程关闭/进程崩溃
进程没了,但是socket是文件,随之被关闭,虽然进程没了,但是连接还在,仍然可以进行四次挥手。
主机关机(正常流程关机)
先杀死所以的用户进程,也会触发四次挥手,如果挥手没有挥完,比如fin发过来了,ack没有收到,此时对端就会重传fin,重传几次之后,依旧没有ack,就会尝试重新连接,如果还不行,就会直接释放连接。
主机掉电
来不及进行挥手操作
- 对端是发送方,发送方收不到ack=>超时重传=>重置连接=>释放连接。
- 对端是接收方,没有办法立即知道,是直接断开,还是没有发来新的数据。
TCP内置了心跳包 保活机制,虽然对端是接收方,但是对端会周期性给发送方发一个心跳包,如果心跳包返回正常说明对端状态良好,如果没有正常返回说明对端出现问题 。
网线断开
同上原理
TCP编程
TCP是面向字节流的
TCP两个核心类
ServerSocket:服务器使用的
Socket:既会给客户端使用,也会给服务器使用
TCP的api
ServerSocket 构造方法
ServerSoket 方法
accept意思就是接受
tcp在连接的接受在内核里已经完成了,而这里的accept是应用层序的接受
accept是会阻塞的(没有客户端连接)
socket 构造方法
两个参数,服务器的ip和端口号
TCP是由连接的,在客户端new Socket对象的时候,就会尝试和指定ip端口的目标建立连接了
Socket 方法
TCP编程时面向字节流的,可以通过上述字节流对象,进行数据传输,
从InputStream这里读数据,就相当于从网卡接受
从OutputSteam这里写数据,就相当于从网卡发送
处理客户端的连接过程中:发来一个请求结束的为短连接
发来多个请求,进行多个响应的为长连接
基于TCP编程的回显服务器
import java.io.*;
import java.net.*;
public class EchoServer {
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(9999);
System.out.println("服务器已启动,等待客户端连接...");
while (true) {
Socket clientSocket = serverSocket.accept();
System.out.println("客户端已连接:" + clientSocket.getInetAddress().getHostAddress());
// 创建输入输出流
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
// 读取客户端发送的数据并回显
String message;
while ((message = in.readLine()) != null) {
System.out.println("收到客户端消息:" + message);
out.println("服务器回显:" + message);
}
// 关闭连接
in.close();
out.close();
clientSocket.close();
System.out.println("客户端连接已断开");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
执行流程
1.服务器先运行start,阻塞在accept这里
2.客户端启动时,会调用socket的构造方法,与服务器建立连接,连接成功之后,服务器的accept就会返回
3.客户端往下执行到,从控制台读取用户输入,也会阻塞
3.服务器进入processConnection了,由于客户端没有发请求,此时读取操作也会阻塞
4.当用户真的输入内容,客户端真正发送了请求出去,同时往下执行到,读取服务器响应,再次阻塞
5.服务器收到客户端的请求之后,从next这里返回,执行process执行printin把响应写回客户端
6.服务器重新回到上述位置,尝试继续读取请求,并阻塞
6.客户端收到服务器的响应,就可以把结果显示出来,同时进入下次循环,再次等待用户的输入了
为了提高IO效率(读写一盘,读写网卡都可以视为IO操作),引入了缓冲区,减少缓冲区使用IO次数,就可以提高整体效率
缓存只能用来读,缓冲区既可以拿来读,也可以拿来写,缓冲区的策略更类似于线程池
在浏览器地址栏输入URL后,按下回车之后会经历哪些流程?
- 浏览器向DNS服务器请求解析该URL中的域名和对应的IP地址;
- 解析出IP地址后,根据该IP地址和默认端口号80,和服务器建立TCP连接,发起三次握手;
- 连接建立完成后,浏览器向服务器发送HTTP请求;
- 服务器对请求作出响应,并把对应的响应结果返回给浏览器;
- 释放TCP连接
- 浏览器根据其请求得到的资源渲染页面,最终向用户呈现一个完整的页面。