一.IP地址:标识了网络上设备所在的位置
端口号:标识了一个具体的应用程序
二.认识协议:发送方和接收方双方进行的一种约定,发送方约好了我发的数据是啥样的,接收方按照这个固定的格式来进行解析。
如果这个协议比较大,学习成本、使用成本、理解成本、维护成本都会非常高。需要我们把这个协议分成一些小的协议(每个协议负责一部分功能),此时就发现,某些协议之间,起到的功能和作用是相似的。
协议分层有很多的好处:1.降低了学习和维护成本 2.灵活的针对这里的某一层协议进行替换。
协议分层有两种风格:
OSI七层网络模型:(教科书上的,实际上并不存在)
TCP/IP五层(四层)网络模型(实际上的情况)
TCP/IP是OSI简化的实现方式
越往下的,越接近硬件设备,越往上,就越接近用户,网络分层这里,相当于上层协议要调用下层协议,下层协议给上层协议提供服务。
物理层:约定了网络通信中基础的硬件设备是咋样的(比如像通信使用的网线、网口等设备),因此我们使用的网线、网口都是相同规格的。
数据链路层:主要负责相邻的两个节点之间,具体怎么进行传输
网络层:主要负责路径的规划,走哪条路比较划算,比如我买了个快递,是从浙江发货,发货到山东,网络层就是规划从浙江怎样发货到山东。
传输层:站在我和商家的角度,我们不关心快递传输的过程,而是关心起点和终点,这个过程就是传输层做的工作,端到端之间的传输,
应用层:应用程序,描述了数据的传输,用户要怎样来使用,比如(你买了个东西,这个东西用它干嘛取决于你)
应用层协议我们可以自定义,其他四层都是在系统内核/驱动程序/硬件中已经实现好了的。我们只能了解,不能修改,应用层协议,我们可以自定义,自定义协议主要做两件事:1.明确协议数据要传递那些信息(根据需求来的).2.明确数据组织形式 比如按照纯文本的方式,也可以使用xml,json,protobuffer.
任何连入网络的电脑、交换机、路由器都可以看成是一个节点,相邻结点:通过一根线连在一起的节点,对于网络层考虑的就是数据从电脑1到电脑2,有几条路径,而对于数据链路层,则考虑相邻两个节点之间怎样传输,是通过网线传输,还是光纤传输,还是通过wifi无线传输。
网络数据传输的基本流程
比如以微信为例:A给B发送一个新年好
发送方:用户在 输入框中输入新年好这个字符串,微信这个应用程序,就把这个字符串,给构造成一个应用层数据报。
一个假设的应用层协议格式:发送方微信号;发送时间;接收方微信号;消息 内容
传输层(进入系统内核了)
在传输层中,就要把上述应用层数据,构造成传输层的数据报,传输层应用到的协议,最知名的就是UDP和TCP,比如此处是使用UDP,就需要构造出UDP数据报(在应用层数据报上,加上个UDP报头)
传输层就把这个UDP数据报,交给网络层
网络层.最知名的协议,IP协议,IP协议基于上述数据,打包成一个IP数据报
网络层数据报准备好,进一步的交给数据链路层
数据链路层 最知名的协议,叫做“以太网”,基于上述数据,还要打包成一个“以太网数据帧”
接下来把这个数据继续往下传输,交给物理层
物理层
把上述二进制的数据转换成电信号/光信号,此时就把真正的数据给发送出去了,上述过程,从应用层到物理层,层层加码,这个过程称为“封装”。
接收方的工作:
物理层:网卡接收到的是光信号和电信号,是在物理层,把这个光电信号转换回二进制的数据,转换回的这个数据,其实是一个以太网数据帧
把这个数据交给数据链路层解析
数据链路层 需要去掉帧头帧尾,取出中间的载荷,交给上层的网络层
以太网数据帧头里面也会记录,这个数据是不是IP数据报
网络层 IP协议针对这里进行解析,解析出IP报头,取出IP协议的载荷,把这里得到的传输层数据报,交给上层传输层。
IP报头里会记录载荷是UDP还是TCP
传输层 UDP再进行解析,取出报头,取出载荷,再把这个载荷交给对应的应用层程序,
UPD报头里有一个重要的字段“目的端口”,目的端口是一个具体的应用程序,关联在一起的,因此就可以根据这个端口把数据交给应用程序了
应用层:微信应用程序 微信会针对应用层协议进行解析,显示到界面上
从下到上,层层解析,这个过程称为分用。
整个的网络协议中,协议分成了很多层,上层协议要调用下层协议(上层协议把数据交给下层,继续封装),下层协议要给上层协议提供支持(下层协议解析好数据,交给上层),这里的几层协议之间有明确的层级关系,只有相邻的两层之间才进行交互。
三.传输层两个重要的协议:UDP和TCP,UDP 无连接,不可靠传输 面向数据报 全双工
TCP 有连接,可靠传输 面向字节流 全双工
给大家举个简单的例子:打电话就是有连接的,需要建立连接才能通信,连接建立需要对方来“接受”,如果连接没建立好,通信不了
发短信/微信是无连接的,直接发就行
打电话其实是可靠传输,因为你说的话、传递的信息你能知道对方有没有接收到
发短信/发微信是不可靠传输,你不能确定对方是否看到,可靠不可靠和有没有连接,没有任何关系
面向字节流:数据传输就和文件读写类似"流式“的
面向数据报:数据传输以一个个的“数据报”为基本单位(一个数据报可能是若干个字节,带有一定格式的)
全双工 一个通信通道,可以双向传输
基于UDP来编写一个简单的客户端服务器的网络通信程序
最简单的UDP版本的客户端服务器程序,称之为 回显服务器
一个普通的服务器:收到请求,根据请求,计算响应,返回响应
public class UdpServe {
private DatagramSocket socket=null;
public UdpServe(int port) throws SocketException {
socket=new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
//服务器不是只给一个客户端提供服务,需要服务很多客户端
while(true) {
//只要有客户端过来,就可以提供服务
//1.读取客户端发来的请求是啥
//receive方法的参数是一个输出型参数,需要先构造好个空白的DatagramPacket对象,交给receive来进行填充
DatagramPacket requestPacket = new DatagramPacket(new byte[5000], 5000);
socket.receive(requestPacket);
//2.根据请求计算响应,由于此处是回显服务器,响应和请求相同
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
//3.把响应写回到客户端,send的参数也是DatagramPacket需要把这个Packet对象构造好
//此处构造的响应对象,不能是用空的字节数组构造了,而是使用响应数据来构造
String response=process(request);
DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());
socket.send(responsePacket);
//打印当前服务器ip和端口,请求和响应
System.out.printf("[%s:%d] req:%s;resp:%s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),request,
response);
}
}
public String process(String request)
{
return request;
}
public static void main(String[] args) throws IOException {
UdpServe serve=new UdpServe(9090);//端口号需要在1024-65535
serve.start();
}
}
public class Udpclient {
private DatagramSocket socket=null;
public String serveip=null;
public int serveport=0;
//一次通信,需要有两个ip,两个端口
//客户端的ip是已知的
//客户端的port是系统自动分配的
//服务器的ip和端口也是需要告诉客户端的,才能把消息顺利发给服务器
public Udpclient (String serveip,int serveport) throws SocketException
{
socket=new DatagramSocket();
this.serveip=serveip;
this.serveport=serveport;
}
public void start() throws IOException {
System.out.println("客户端启动");
Scanner scaner=new Scanner(System.in);
while(true) {
//1.从控制台读取要发送的数据
String request = scaner.next();
if(request.equals("Exit"))
{
System.out.println("拜拜了");
break;
}
//2.构造成UDP请求,并发送
//构造这个Packet的时候,需要把serveip和port都传入过来,此时ip地址需要填写32位的整数形式
//ip地址是一个字符串,需要使用InetAddress.getByName 来进行一个转换
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName(serveip),serveport);
socket.send(requestPacket);
//3.读取服务器的Udp响应,并解析
DatagramPacket respon = new DatagramPacket(new byte[6000], 6000);
socket.receive(respon);
String result=new String(respon.getData(),0,respon.getLength());
//4.把解析好的结果响应出来
System.out.println(result);
}
}
public static void main(String[] args) throws IOException {
Udpclient client=new Udpclient("192.168.1.101",9090);
client.start();
}
}
我们来看一下客户端和服务器里面执行过程的先后顺序:
我们来看一下程序运行结果:
基于udp实现的简单的字典服务器和字典客户端
其实我们只需要让一个类继承这个实现服务器这个类,然后重写process方法即可
public class UdpServe1 extends UdpServe{
private HashMap<String,String>dict=new HashMap<>();
public UdpServe1(int port) throws SocketException {
super(port);
dict.put("cat","小猫");
dict.put("money","小钱");
}
@Override
public String process(String request) {
return dict.getOrDefault(request,"字典中没有查到");
}
public static void main(String[] args) throws IOException {
UdpServe1 serve1=new UdpServe1(9090);
serve1.start();
}
}
基于Tcp实现的客户端和服务器
Tcp提供的API主要是两个类:
ServerSocket 专门给服务器使用的Socket对象
Socket是给客户端使用,也会给服务器使用
Tcp不需要使用一个类来表示Tcp数据报,不是以数据报为单位进行传输,是以字节的方式,流式传输。
Socket
在服务器这边,是由accept返回的,在客户端这里,构造的时候指定一个IP和端口号(此处指定的IP和端口号是服务器的IP和端口)
Socket 在客户端 和服务器都会用到
getInputStream
getOutputStream
我们把当前服务器的代码给展示出来
public class TcpServe1 {
private ServerSocket tcpserve = null;
public TcpServe1(int port) throws IOException {
tcpserve = new ServerSocket(port);
}
public void start() throws IOException {
//ExecutorService poll= Executors.newCachedThreadPool();
System.out.println("服务器启动");
while (true) {
//使用这个socket1和具体的客户端进行交流
Socket socket = tcpserve.accept();// //客户端在构造Socket对象的时候,就会指定服务器的IP和端口,如果没有客户端来连接,此时accept
//就会阻塞,也就不会有socket这个对象
connection(socket);//此方法连接一个客户端
}
}
public void connection(Socket socket) throws IOException {
//System.out.println("服务器启动");
System.out.printf("[%s:%d] 客户端上线!\n", socket.getInetAddress().toString(), socket.getPort());
//由于要处理多个请求和响应,使用循环来进行
try (OutputStream outputStream = socket.getOutputStream(); InputStream inputStream = socket.getInputStream()) {
//Scanner scanner=new Scanner(inputStream);
while (true) {
//1.读取请求
Scanner scanner = new Scanner(inputStream);
if (!scanner.hasNext()) {
//System.out.println("服务器退出");
//没有下个数据。说明读完了(客户端关闭了连接)
System.out.printf("[%s:%d] 客户端下线!", socket.getInetAddress().toString(), socket.getPort());
break;
}
// 此处使用 next 是一直读取到换行符/空格/其他空白符结束, 但是最终返回结果里不包含上述 空白符
String request = scanner.next();
String response = process(request);
PrintWriter writer = new PrintWriter(outputStream);
writer.println(response);
//此处使用println来写入,让结果中带有一个\n换行,方便对端来接收解析
writer.flush();
//flush 用来刷新缓冲区,保证当前写入的数据发送出去了
System.out.printf("[%s:%d],req:%s;resp:%s\n", socket.getInetAddress().toString(), socket.getPort(),
request, response);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
socket.close();
}
}
public String process(String request)
{
return request;
}
public static void main(String[] args) throws IOException {
TcpServe1 tcpServe1=new TcpServe1(10001);
tcpServe1.start();
}
}
当前代码还存在一个重要的问题,同一时刻只能处理一个连接(只能给一个客户端,提供服务),当有客户端连上服务器之后,代码就执行到了process这个方法里面的while循环里了,只要循环不结束,就无法调用到accept,即使把循环去了,也不行,等待客户端请求的时候是会阻塞等待的,因此我们可以考虑使用多线程,主线程负责进行accept,每次收到一个连接,创建新的线程,由这个新的线程负责处理这个新的客户端
public void start() throws IOException {
while(true)
{
//使用这个socket1和具体的客户端进行交流
Socket socket1=socket.accept();
//客户端在构造Socket对象的时候,就会指定服务器的IP和端口,如果没有客户端来连接,此时accept
//就会阻塞,也就不会有socket1这个对象
Thread t=new Thread(()->{
try {
connection(socket1);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
//使用这个方法来建立一个连接,这个连接对应到一个客户端
}
}
但是我们知道线程的创建和销毁是需要开销的,这种方法还是不好,因此我们考虑使用线程池。
public void start() throws IOException {
ExecutorService poll= Executors.newCachedThreadPool();
System.out.println("服务器启动");
while (true) {
Socket socket = tcpserve.accept();
poll.submit(()->{
try {
connection(socket);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}
但是这样还不够,如果客户端非常多,而且客户端连接都迟迟不断开,就会导致机器上有很多线程,我们的机器承担不了这样的负担,是否有一个办法,处理很多客户端连接呢?是存在的,我们采用IO多路复用,IO多路转接,给这个线程安排个集合,这个集合就放了一堆连接,这个线程就负责监听这个集合,那个连接有数据来了,线程就来处理那个连接,因为连接的请求并非严格意义上的同时,总还是有先后顺序的。
接下来我们再来看一下客户端的实现代码
public class TCpClient1 {
private Socket socket=null;
public TCpClient1(String tcpServe1,int port) throws IOException {
//Socket 构造方法,能够识别 点分十进制格式的IP地址,比DatagramPacket 更方便
//new 这个对象的时候,会进行TCP连接操作
socket=new Socket(tcpServe1,port);
}
public void start()
{
System.out.println("客户端开启");
try(InputStream inputStream= socket.getInputStream(); OutputStream outputStream= socket.getOutputStream()) {
Scanner scanner=new Scanner(System.in);
while(true)
{
//1.从键盘上读取用户输入的请求内容
System.out.print('>');
String request=scanner.next();
if(request.equals("exit"))
{
System.out.println("goodbye");
break;
}
//2.把读到的请求内容发送给服务器
PrintWriter printWriter=new PrintWriter(outputStream);
printWriter.println(request);
printWriter.flush();
//flush确保数据发送出去
//3.读取服务器响应
Scanner scanner1=new Scanner(inputStream);
String response=scanner1.next();
//4.把响应内容显示到界面上
System.out.println(response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TCpClient1 cpClient1=new TCpClient1("192.168.43.90",10001);
cpClient1.start();
}
}
端口号:端口号是传输层协议的概念,要求在同一个主机上,一个端口号不能被多个进程绑定,例如进程A绑定了端口号4000,此时进程B也尝试绑定4000,进程B绑定操作就会失败(抛出异常)。
TCP和UDP协议报头中都会包含源端口和目的端口,这两个都是使用两个字节,16个bit来表示的,一个端口号的取值范围0->65535,但是我们自己写程序的时候,绑定的端口,得从1024起,0->1023这个范围的端口号,称为“知名端口号/具名端口号”,这些端口号属于已经分配给了一些知名的广泛使用的应用程序,1023以下的端口,也不是完全不能用,这些端口被分配给了特定的程序,但是这个程序在电脑上是否运行着,也是不一定的。
UDP报头:应用层的数据传入到传输层的时候,会被加上UDP报头,
UDP 会把载荷数据(通过UDP socket,send()方法拿来的数据,在它的基础上前面拼装几个字节的报头)相当于字符串拼接(此处是二进制的,不是文本的)
UDP报头里包含了一些特定的属性,不同的协议,功能不同,报头中带有的属性信息就不同,对于UDP来说,报头一共8个字节,分成4个部分(源端口、目的端口、UDP报文长度、校验和),每部分两个字节。源IP:相当于发送方的地址,源端口:相当于发送方的名字 目的IP:相当于接收方的地址 目的端口:
相当于接收方的名字。UDP报文长度,也是2个字节表示的,2字节表示的范围是0->65535,换算为64KB,一个UDP数据报,最大只能传输64KB的数据,如果应用层数据报,超过64KB,我们有两种解决方案:1.在应用层通过代码的方式针对 应用层数据进行手动的分包,拆成多个包通过多个UDP数据进行传输(本来send一次,现在需要send多次)2.不用UDP,换成TCP,这两种方法第二种更好,因为第一种的话我们需要写很多代码,比较麻烦。
校验和:验证传输的数据是否正确
网络传输过程中,可能会受到一些干扰,网络传输本质上就是光信号/电信号,这些可能会受到一些物理环境的影响,比如我们想传输的数据是110011 ,受一些物理环境的影响变成110001,这样难免会产生影响,因此就引入了校验和来进行鉴别 。针对数据内容进行一系列数学运算,得到一个比较短的结果(比如2字节),如果数据内容一定,得到的校验结果就一定,如果数据变了,得到的校验和也就变了
针对网络传输的数据来说,生成的校验算法有很多种:其中比较知名的几个:
CRC 循环冗杂校验:简单粗暴,把数据的每个字节,循环网上累加,如果累加溢出,高位就不要了,这样好算但是校验结果不是特别理想,万一我们的数据同时变动了两个bit位,前一个字节少1,后一个字节多1,这样就出现了内容变了,CRC没变这种情况。
MD5:MD5不是简单相加,,有一系列公式,来进行更加复杂的数学运算(数学问题)
MD5 算法的特点:
定长:不论你原始数据多长,得到的md5值都是固定长度(4字节版本,也有8字节版本)
冲突概率很小:原始数据哪怕只变动一个地方,算出来的MD5值都会差别很大,会大大减少数据内容变了但是CRC没变这种情况
不可逆:通过原始数据计算MD5很容易,通过MD5还原成原始数据很难,比较安全
MD5这样的特点:1.校验和 2.作为计算hash的值的方式 3.加密
TCP
我们来看一下TCP的报文结构:
TCP报文=TCP报头(首部)+TCP载荷,一个TCP报头,长度是可变的,不是像UDP一样固定8个字节,首部长度决定了TCP报头具体多长,选项之前的部分是固定长度(20字节)首部长度-20字节得到的就是选项部分的长度,此处的首部长度的单位是4bit,而不是1bit,如果首部长度值是15,整个TCP报头是60字节。
四.TCP内部的十大工作机制
TCP是一个复杂的协议,里面有很多机制。
TCP:我们知道TCP的有连接、面向字节流和全双工的特点在代码中是可以体现的,但是可靠传输(这里的可靠传输并不是说发送方%100把消息发送给接收方,只是尽可能的把数据传输过去,如果传输不过去,至少我们可以知道)又是怎样体现的呢?可靠传输是TCP最核心的特点,确认应答和超时重传是构成可靠传输的基石,
1.确认应答:实现可靠传输的最核心的机制,
TCP进行可靠性传输,最主要就是靠这个应答机制,A给B发了个消息,B收到之后就会返回一个应答报文,此时A收到报文之后,就知道刚才发的数据已经顺利到达B了,
考虑更复杂的情况:
注意此处我发的可能是连续发两条消息,我发第二条消息,不需要等待第一个消息的回应,妈妈收到消息就会立即回应。
但是注意!!网络上可能存在“后发先至”,也就是这种情况:
由于“后发先至”,我先收到“不可以”,后收到“可以”,很明显此处的应答就错乱了,此时表示的含义就出现歧义了,网络先发后至这个现象客观存在,无法避免.
如何解决上述先发后至的问题?方法其实也很简单,给传输的数据和应答报文,都进行编号,就可以了
TCP的字节序号是依次累加的,这个依次累加的过程对于后一条数据来说,起始字节的序号就是上一个数据 的最后一个字节的序号+1,每个TCP的数据报报头填写的序号只需要填写TCP数据的头一个字节的序号即可,TCP知道了头一个字节的序号,再根据TCP报文长度,很容易知道每个字节的序号。
确认序号的取值,是收到数据的最后一个字节的序号+1,
2.超时重传:丢包,涉及到两种情况:1.发的数据丢了2.返回的ack丢了。发送方看到的结果就是没有收到ack,区分不了是那种情况,这两种情况会一视同仁,都认为是丢包了。
TCP引入了重传机制,引入了一个时间阈值,发送方发送了一个数据之后,就会等待ack,此时开始计时,如果在时间阈值之内,如果超过了时间阈值,也没有收到ack,甭管此时ack是不是还在路上,还是彻底丢了,都视为是丢包了。
也是因为这样,接收方可能收到了很多重复的消息,假设这个数据是一个支付请求,会造成不可估量的损失,TCP对于这种重复数据的传输,是有特殊处理,去重,TCP存在一个“接收缓冲区”这样的存储空间(接收方操作系统内核里的一段内存)每个TCP的socket对象,都有一个接收缓冲区(其实也有发送缓冲区),主机B收到主机A的数据,其实是B的网卡读到数据了,然后把这个数据放到B的对应socket的接收缓冲区中,根据数据的序号,TCP很容易识别当前接收缓冲区里的两条数据是重复的,如果重复,则把后来的这份数据直接丢弃了,保证了应用程序调用read读取到的数据一定是不重复的,TCP使用这个接收缓冲区,对收到的数据(根据序号)重新进行排序,使应用程序read到的数据是保证有序的(和发送顺序是一致的)。
可靠传输是TCP最核心的部分,TCP的可靠性就是通过确认应答+超时重传来进行体现的,其中确认应答描述的是传输顺利的情况,超时重传描述的是传输出现问题的情况,这两者相互配合,共同支撑整体的TCP可靠性。
3.连接管理
什么叫做连接,给大家举个例子,结婚证,我的这份结婚证里有她的信息,她的结婚证里有我的信息,那么我们两个人就建立连接,正式结婚了
TCP建立连接:
A
A这里需要有一个空间存储他的老婆是谁 (B的ip和端口)
B 这里也需要一个空间存储她的老公是谁(A的ip和端口)
当这两部分信息都被维护好了之后,此时连接就有了,此时也把保存这部分信息的这个空间(数据结构)称为连接
断开连接:A和B把自己存储的连接信息(数据结构)删了,连接就断开了
建立连接(三次握手)通信双方各自要记录对方的信息,彼此之间要相互认同。
所谓的“三次握手”本质上是四次交互,通信双方,各自要向对方发起一个“建立连接”的请求,同时,再各自向对方回应一个ack,这里其实是一共有四次信息交互,但是中间两次交互,是可以合并成一次交互的,因此就构成了“三次握手”,中间这两次不合并行不行?答案是不行,因为封装分用两次一定比封装分用一次成本更高。
三次握手中间两次交互之所以能够合并,是因为他们两是同一个时机,具体来说,三次握手这三次交互过程,是纯内核完成的(应用程序感知不到,也干预不了)服务器的系统内核收到syn之后,就会立即发送ack,也会立即发送syn.
三次握手另外一个重要作用,验证通信双方各自的发送能力和接受能力是否正常
三次握手,一定程度上保证了TCP传输的可靠性(起到的不是关键作用,辅助作用)。
三次握手的意义:1.让通信双方各自建立对对方的认同
2.验证通信双方各自的发送能力和接收能力是否ok
3.在握手的过程中,双方来协商一些重要的参数
建立连接阶段主要认识两个状态:1.LISTEN 服务器的状态 表示服务器应经准备就绪,随时可以有客户端来建立连接 2.ESTABLISHED 客户端和服务器都有 ,连接建立完成,接下来可以正常通信了
TCP断开连接:
四次挥手和三次握手非常相似,都是通信双方发起一个断开连接的请求,再各自给对方一个回应。
在上述代码中,当前是循环一结束,就立刻close发起了FIN,此时ACK和FIN之间的时间间隔就比较短,此时可以合并成一个,但是如果时间长了,比如在close之前做别的工作
如果是这样,发送FIN的时机和ACK的时机就间隔的比较久了,此时就无法合并成一个了。
1.CLOSE_WAIT出现在被动断开连接的一方,等待关闭(等待调用close方法关闭socket)建立连接一定是客户端主动发起请求,断开连接,可能是客户端主动发起,也可能是服务器主动发起
TIME_WAIT出现在主动发起断开连接的一方,假设是客户端主动断开连接,当客户端进入TIME_WAIT状态的时候,相当于四次挥手已经挥完了,但是此时这里的TIME_WAIT要保持当前的TCP连接状态不要立即就释放,为啥不要释放连接?为啥会以TIME_WAIT保留一会连接呢?是因为如果最后一个ack刚刚发出去,还没到,如果这个ack丢包,TIME_WAIT会等,如果等了一段时间,也没收到重传的FIN,此时就会认为最后一个ack没丢,于是就彻底地释放连接了。
4.滑动窗口
确认应答、超时重传、连接管理都是给TCP的可靠性提供的支持,引入了可靠性,其实就降低了效率,TCP竭尽可能的提高传输效率,我们进行IO操作的时候,其实时间成本主要是两个部分1.等2.数据传输(数据拷贝)大多数情况下,IO花的时间成本大头都是在等,具体怎么缩短,批量发送,批量等待,把多份等待时间合并成一份。
上述情况下,如果丢包了咋办?
数据包丢了
5.流量控制:
这是一种干预发送的窗口大小的机制
滑动窗口,窗口越大,传输效率就越高(一份时间,等的ack就越多),但是窗口也不能无限大
窗口太大,会消耗大量的系统资源
发送速度太快,接收方处理不过来,发了也白发
完全不等ack,会影响可靠性
接收方的处理能力,是一个很重要的约束依据,发送方发的速度,不能超出接收方的处理能录流量控制要做的工作就是这个根据接收方的处理能力,协调发送方的发送速率
如何衡量接收方的处理能力?
简单的方法:直接看接收方接收缓冲区的剩余大小。
6.拥塞控制:
流量控制和拥塞控制共同决定发送方的窗口大小是多少
其中流量控制考虑的是接收方的处理能力,拥塞控制考虑的是传输过程中间结点的处理能力,
7.延时应答,也是提高效率的机制要做的就是在接收方能处理得了的前提下,尽可能把窗口大小放大一点,延时:受到数据之后不是立即返回ack了,而是稍微等会再返回,等待的时间里,接收方的应用程序,就能够把接收缓冲区给读取一波,此时剩余空间就更大了
8.捎带应答:
也是提高效率的方式,在延时应答的基础上,引入的捎带应答
9.面向字节流
面向字节流,引入了一个麻烦事,黏包问题
由于TCP是字节流的,一次读一个字节,读N个字节,都是可以的,这就导致一次读到的数据,可能是半个应用层数据报,可能是一个应用程数据报,也可能是多个应用层数据报,我们期望读到的是整个应用层数据报,解决方案就是我们约定好应用层数据协议,尤其是明确应用层数据报和应用层数据报之间的边界就好了,1.约定好分隔符2.约定好每个包的长度
10.异常情况
进程崩溃
主机关机
这两种情况看做是一种情况,进程没了,对应的pcb就没了,对应的文件描述符表就释放了,相当于socket.close(),此时内核内会继续完成四次挥手,此时其实仍然是一个正常断开的流程,主机关机要先杀死进程,然后才正式关机,这种情况和第一种一样,也是和上面一样触发四次挥手
主机掉电
网线断开
这两种情况看做是一种情况,假设是接收方掉电了,发送方会继续发送数据,发完数据要等待ack,ack等不到,超时重传,再怎模重传,也收不到ack,重传几次没有应答,尝试重置tcp连接,显然这个重置也会失败,此时也就只能放弃连接了
如果是发送方掉电了,接收方发现没数据了,没数据是发送方挂了,还是数据在来的路上,接收方不知道,此时接收方需要周期性的给发送方发送一个消息,确认下对方是否还在正常工作。