网络编程套接字(二)——TCP

目录


TCP流套接字

TCP 是字节流的,读写的时候,以字节 byte 为基本单位

类似于文件的字节流,读写TCP代码,本质上和读写文件的代码是一致的【都是通过 InputStream / OutputStream 展开的】

TCP特点:

有连接 ,TCP 的通信双方会保存对方的信息

可靠传输,TCP 会尽可能保证数据报能够被对端收到

⾯向字节流,TCP 读写数据的基本单位,就是字节 (类似于文件操作)

有接收缓冲区,也有发送缓冲区

⼤⼩不限

API介绍

ServerSocket

创建一个这样的对象就相当于打开了一个 socket 文件

这个 socket 对象是给服务器专门使用的

这个类本身不负责发送接收,主要负责建立连接

ServerSocket 构造方法:

java 复制代码
ServerSocket(int port)    创建⼀个服务端流套接字Socket,并绑定到指定端⼝

方法:

java 复制代码
方法名                       说明
Socket accept()         开始监听指定端⼝(创建时绑定的端⼝),有客⼾端
                        连接后,返回⼀个服务端Socket对象,并基于该
                         Socket建⽴与客⼾端的连接,否则阻塞等待

void close()             关闭此套接字

Socket

创建一个这样的对象也就相当于打开了一个 socket 文件

Socket 服务器和客户端都会使用,这个类,它负责发送和接收数据

Socket 构造方法:

java 复制代码
Socket(String host, int port)    创建⼀个客⼾端流套接字Socket,并与对应IP的
                                 主机上,对应端⼝的进程建⽴连接

方法:

java 复制代码
方法名                           说明
InetAddress getInetAddress()      返回套接字所连接的地址
InputStream getInputStream()      返回此套接字的输⼊流
OutputStream getOutputStream()    返回此套接字的输出流

一个简单的代码

因为TCP是有连接的,不能一上来就读写数据,需要先处理连接

建立连接的过程,操作系统已经完成了(不需要代码实现)

在写代码的时候,只需要把系统内核中建立好的连接拿上来就行

SeverSocket 相当于销售,Socket 相当于给销售拉来的客户做讲解的人

PrintWriter(基于OutputStream 创建) 有一个println 的写法 ,把数据写出某个地方

还有一个flush 方法 ,用于刷新缓冲区【println 通常会攒一波数据之后再写出,但有时我们需要写了一点就返回,此时就使用 flush 把数据都打包送出去(无论数据攒没攒满)】
Scanner(基于 InputStream 创建)有一个 scanner.next 写 法,把数据读取

注意:每次打开一个 Socket 文件后,都要关闭一次,防止文件泄露

TCP协议段格式

4位首部长度:【报头的长度】报头中包含了 选项
TCP报头的最大长度是60字节
保留(6位):保留位,现在不使用,但是先占个位置,如果数据填满了就可以靠他扩容
6个关键标志位:

ACK【应答报文】:确认号是否有效

RST【复位报文】:对⽅要求重新建⽴连接(单方面放弃连接时)

SYN【同步报⽂】:请求建⽴连接

FIN【结束报文】:通知对⽅,本端要关闭了
16位检验和:确认数据是否正确传输【发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含TCP⾸部, 也包含TCP数据部分

URG:表示 "紧急指针" 有效------正常情况下,tcp 的数据都是"顺序传输",紧急指针意味着后面有些数据要先传输(插队)紧急指针的值表示,从当前位置往后多少个字节位置的部分,要进行插队

PSH:催促接收方,尽快把缓冲区的数据交给应用层

TCP的几大特性(核心机制)

确认应答

确认应答就是 TCP 实现可靠传输的最关键机制之一

TCP 是字节流传输,序号和确认序号是针对 字节 进行编号的
每一个ACK(应答报文)都带有对应的确认序列号,意思是告诉发送者,我已经收到了哪些数据;下⼀次你从哪里开始发

同一个TCP连接内,序号会持续累加,下一个数据报的序号会在上一个数据报最后一个字节的序号基础上继续递增
确认序号的含义

  1. 所有 < 确认序号的数据,接收方已经收到了
  2. 发送方接下来该从确认序号位置,继续发送数据

例如我发了一个消息 "你好",然后发一个消息 "吃了吗" ,序号列就会从 "你好" 开始读,读完后假设读到了1000,就从下一个消息"吃了吗"开始,记为1001,继续往后出发,这样根据序号发送消息,就能避免 "吃了吗" 发在 "你好" 前面

超时重传

产生丢包之后,重新传输数据

两种情况:数据丢了;ack 丢了

如果是数据丢了,就重新传输

如果是 ack 丢了,接收方就按照序号对数据去重

超时时间:根据超时重传的轮次增加,每次重传的时间都会逐渐增加

连接管理

连接:虚拟的,抽象的连接

Q1:连接是如何建立的

答:三次握手

TCP中的握手,就是传输一个"打招呼"的数据包,这个数据包不带任何"业务数据"【只有报头,没有报文】(没有应用层的载荷)

主要流程:

建立连接是相互的过程

从流程/逻辑上是四次交互,但是中间两次可以合并成一个 TCP数据包

网络上实际只传输了 三个数据包
三次握手的意义

1.三次握手,相当于"投石问路",验证通信链路是否畅通 (火车载客之前会提前来回跑一遍,确保路线正常)

2.三次握手,也是在验证通信双方发送能力和接收能力是否正常

3.通过三次握手,让通信双方协商关键信息 (和可靠不是很相关,但很有用)

------TCP 建立连接过程中,有一个信息是需要协商的,就是TCP 数据的起始序号

TCP的序号会针对载荷部分按照字节编号.

当TCP 连接建立好了之后,传输的第一个数据包的第一个字节的编号,不是从1/0 开始编排的,而是在三次握手阶段协商出这样的数字,作为起始编号
为什么不从0或1开始编排呢

例如,当客户端服务器建立一次连接后断开重连了,那么上一次连接还没发的信息在第二次连接会核对序号,如果信息的序号和当前连接的起始序号相差很大,那么判断出此为上次连接的信息,就不会让它发出去了,因此每次连接都要写上一个序号

三次挥手详细流程:

人话:LISTEN: 手机开机,信号良好,随时可以打电话

ESTABLESHED:连接已建立,电话接听,可以说话了

注:三次握手是和可靠传输有关系的只不过是"前提条件""辅助操作"
真正传输数据的时候,靠的是确认应答和超时重传

Q2:连接是如何断开的

答:四次挥手

断开连接的四次挥手,可能是客户端主动发起,也可能是服务器主动发起的

主要流程:

1.客户端告诉服务器,我要和你断开连接

请你把我删了

2.服务器回应收到

3.服务器告诉客户端,我也要和你断开连接

请你把我也删了

4.客户端回应收到

客户端和服务器都会把对方的信息(之前保存的对方的IP和端口) 删除掉

删除完了,连接就断开了
称为四次挥手的原因:中间的两次交互,不一定能触发合并 (因为ack和fin的触发时间不一样,ack比fin快)

fin 是通过代码(close() )触发,而ack是通过内核负责的

触发合并的情况:通过TCP的机制【延时应答】

把应答ack的时机往后拖一段时间

四次握手的详细流程:

CLOSE_WAIT:被动断开连接的一方进入的状态

收到对方发来的FIN 的时候 就会返回 ACK, 同时进入 CLOSE._WAIT状态。正常情况下,存在的时间会比较短.

(可以理解成 wait close, 等待关闭, 等待应用程序代码,调用 close)
TIME_WAIT:等待连接彻底释放

(类似于线程 TIMED_WAITING, 有时间限制的等待)

理论上服务器发送fin时,客户端就能断开连接了,像这样等一段时间是为了确保最后的ack能顺利传输(如果ack丢包了但客户端已经结束,则服务器无法正常断开连接了)

滑动窗口

滑动窗口是 TCP 协议实现可靠传输、流量控制、拥塞控制的核心机制,通过 "窗口" 动态调整数据发送 / 接收的范围,平衡传输效率与网络稳定性

前面几个机制,是用于实现TCP的可靠性

而滑动窗口,是为了提高效率的

可靠传输是有代价的,每次发送一个数据,都要等 ack,这样单位时间能传输的数据就少了

解决方案:把每次发送都等待的 ack 变为 批量送一波,再等 ack(花一份等待的时间,等待多个 ack ),此时总的等待时间就变少了

这样,就把批量发送多少数据不需要等待称为"窗口大小"

当收到一个ack,就立即往后发送一组数据,这就是滑动窗口

每次收到一个ack,窗口都会往后平移一个格子

如果收到 ack的速度很快,平移的过程就好像"滑动"的过程

注意:TCP的滑动窗口虽然效率提高了不少,但还是比不上UDP不可靠传输的传输效率的

在滑动窗口的机制下,出现丢包怎么办?

情况1:数据包已经抵达,ack 被丢了

如果只是 ack 丢包,在滑动窗口机制下,不需要做任何处理

因为 ack 的确认序号的设定规则

ack 的确认序号,表示该序号前的所有数据都已经收到.

ack =>1001,1001 之前的数据都收到了,假设你1000的ack丢了,但是2000的数据收到了,TCP就会默认你2001之前的数据都收到了,下一个ack就直接从2001开始

情况2:数据包丢了

B 反复向 A 索要没有收到的1001 这个数据

A 感知到 B 连续多次索要 1001 之后,就会认为 1001 丢包,触发重传 1001.

一旦 1001-2000 数据 B 收到了,此时 B 观察发现,自己的接收缓冲区里,已经有2001 - 7000 这些数据了

接下来从 7001 索要即可

这就是快速重传 ,是滑动窗口机制下的重传机制,相当于超时重传的变种

如果使用 TCP 传输较大量的数据的时候,自然就会触发 滑动窗口,重传机制采取快速重传。

如果使用 TCP 传输较少的数据,此时就仍然是按照 确认应答和超时重传 方式来进行

类似于牛顿经典力学和爱因斯坦相对论

如果是最后一个ack丢失了,那么证明批量传输已经结束了,仍然要靠超时重传接管,此时最后一波数据就是按照超时重传机制

流量控制

虽然窗口越大,传输速度就越快

但是也不能无限大,太大了,对于可靠性会有影响

比如发送方以极快的速度发送,接收方的处理速度跟不上...也就会导致有些数据被接收方丢弃了
流量控制,就是根据接收方的处理能力,干预到发送方的发送速度 (通过调整滑动窗口大小)

这个过程类似于生产者消费者模型

发送方是生产者,接收方的应用程序是消费者

所谓"接收方的处理能力 ",就是接收方应用程序调用read的速度(调用read 有多快,每次 read 读多少)

注:read的数据,都是在接收缓冲区中排好序的

要衡量 read 的速度,可以通过根据接收缓冲区的剩余空间的大小

以上述指标反向制约发送方的发送速度

接收方返回 ack 报文 的时候,在TCP 报头中把接收缓冲区剩余空间大小 数值的放到 ack 的报头中 等发送方收到 ack 就知道接收方的处理速度了

注:TCP 的滑动窗口大小,并不是只局限于16位(64KB),选项中有一个项"窗口扩展因子 "

发送方收到 ack 之后,设置的滑动窗口大小= 16位窗口大小<<窗口扩展因子

即左移一位 相当于*2,属于指数级增长

这样窗口大小的取值范围是非常非常大的

拥塞控制

限制滑动窗口的发送速率

虽然TCP有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据. 但是如果在刚开始阶段就发送大量的数据, 仍然可能引发问题,因为网络上有很多的计算机, 可能当前的⽹络状态就已经⽐较拥堵.

发送方的速率,不光要考虑接收方的速率,还要考虑传输路径整个过程中所有中间节点的情况, 在不清楚当前网络状态下, 贸然发送⼤量的数据, 是很有可能引起雪上加霜的。

TCP引⼊ 慢启动 机制, 先发少量的数据, 探探路, 摸清当前的⽹络拥堵状态, 再决定按照多⼤的速度传输数据

先按照比较小的速度发送数据看一下,是否丢包.

如果丢包,说明中间链路已经有节点顶不住了,减小窗口大小,减小速度.

如果不丢包,说明中间链路已经有节点顶不住了,增大窗口大小,增加速度
拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中⽅案

发送方的 发送窗口的大小,同时取决于 流量控制拥塞控制 (谁小谁说了算)
流量控制:接收方通过 ack 告诉发送方流量控制窗口
拥塞控制:发送方自己维护一个变量作为拥塞控制的窗口

快速记忆:拥塞控制的过程就像热恋的感觉

延时应答

承接滑动窗口,让传输效率提高一些.让窗口尽量大一些 (在可靠性的前提下)

方法:不立即返回 ack, 而是稍微等一等【为了给接收方留出一些时间,好能够多消费一些,让接收缓冲区的剩余空间更大一点】

延时应答不光提高效率,还能减少ack的次数

捎带应答

在延时应答的基础上, 我们发现, 很多情况下, 客⼾端服务器在应⽤层也是 "⼀发⼀收" 的. 意味着客户端给服务器说了 "How are you", 服务器也会给客⼾端回⼀个 "Fine, thank you";

那么这个时候ACK就可以搭顺⻛⻋, 和服务器回应的 "Fine, thank you" ⼀起回给客⼾端【即把响应和ack一起发出

因为ack是在收到请求后立即返回,而响应是要花时间计算的,于是就让ack等响应好了之后一起发回去

捎带应答,既取决于延时应答,又和应用程序的处理逻辑有关,捎带应答不是100% 会触发的,但是TCP会尽可能这么做

面向字节流

创建⼀个TCP的socket, 同时在内核中创建⼀个 发送缓冲区 和⼀个 接收缓冲区

粘包问题

粘包,粘的是应用层的数据包

TCP字节流的特性,收到多个 TCP数据报的时候把所有的载荷都给混到一起,放到接收缓冲区里

这样包的边界比较模糊,就好像"粘上了"一样

接收方的应用程序 read 的时候,就有很多种 read 的可能

• 在TCP的协议头中, 没有如同UDP⼀样的 "报⽂长度" 这样的字段, 但是有⼀个序号这样的字段.

• 站在传输层的角度, TCP是⼀个⼀个报⽂过来的. 按照序号排好序放在缓冲区中.

• 站在应用层的角度, 看到的只是⼀串连续的字节数据.

• 那么应⽤程序看到了这么⼀连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是⼀个完整的应用层数据包.

解决粘包,就要从应用层入手,合理设计应用层协议,让包的边界能比较清晰

方案:

  1. 通过特殊的分隔符来作为包的边界(如;)【aaa;bbb;ccc】
  2. 在应用侧数据包开头的地方,通过固定长度,约定整个应用层数据包的长度

粘包问题只针对字节流的传输,对于 文件操作,(使用文件存储多个结构化数据...也是可能涉及到粘包的)

UDP 就没有这种粘包问题,因为UDP的接收缓冲区是类似链表的结构

异常情况

  1. 进程崩溃【正常的流程】
    进程崩溃,意味着对应的文件描述符就被关闭了 (调用 close, 干掉进程),只要是进程退出, 都会释放 PCB, 释放文件描述符表【触发FIN】
    TCP 的连接并不会因为进程的结束立即结束,会保留一会
  2. 主机关机(正常流程)
    正常流程下的主机关机,就会先杀死所有的进程,此时也会触发 FIN, 进而进入四次挥手(有可能挥不完,无伤大雅)
    (1)如果关机的速度比较慢,有很大可能四次挥手挥完了;
    (2)如果关机的速度比较快,刚发 FIN, 机器就关了,此时对端可以正常返回 ack, 也会继续正常发送 FIN,这里的 FIN 就没有 收到 ack, 尝试重传几次 FIN 后还是没有 ack, 对端就直接放弃连接 (删除之前保存的对端信息)
  3. 主机掉电(直接拔电源)/ 网线断开
    接收方会周期性的和发送方交换"心跳包"
    A 给B发一个无业务数据的报文
    B 给A返回一个ack
    如果对方有应答,就可以认为对方是正常工作的,如果心跳包也没有应答,就可以认为对方挂了
    TCP⾃⼰也内置了⼀个保活定时器,会定期询问对⽅是否还在. 如果对方不在, 也会把连接释放.(但是TCP的心跳时间有点长,通常是好几秒)

TCP/UDP 对比

  1. TCP有连接,可靠传输,面向字节流
    还有一系列的机制 ...
    大部分情况优先考虑 TCP
  2. UDP 无连接,不可靠传输,面向数据报
    UDP 传输效率高
    通常用于机房内部的传输,不太会丢包,效率要求更高
相关推荐
冰暮流星4 小时前
javascript的switch语句介绍
java·前端·javascript
有梦想的攻城狮4 小时前
Java中的Double类型的存在精度丢失详解
java·开发语言·bigdecimal·double
废墟乌托邦4 小时前
实验10 路由器的基本配置 实验报告
网络·智能路由器
初听于你4 小时前
IP地址与路由器地址
linux·运维·服务器·网络·tcp/ip·计算机网络·智能路由器
《七》跷4 小时前
VLAN实验
网络·智能路由器
ME10104 小时前
计算机三级网络技术知识点全面总结
网络·计算机网络
brucelee1864 小时前
Window访问 小米路由器的共享文件夹 设置
网络·智能路由器
Lxyand14 小时前
OSPF 全网最详解(理论及配置)
网络
funnycoffee1234 小时前
遵循 TCP/IP 四层模型,详细描述一台终端访问 www.taobao.com 的完整过程
网络·网络协议·tcp/ip
m0_748249544 小时前
Java 语言提供了八种基本类型【文123】
java·开发语言·python