TCP入门使用

Posted by 麦子 on Saturday, 2020年01月11日

[TOC]

转载地址:https://juejin.im/post/5e12e2c55188253a937f435e

概述

TCP是面向连接的传输层层协议,可以为应用层提供可靠的数据传输服务。所谓的面向连接并不是真正意思上的连接,只不过是在发送数据之前,首先得相互握手,也就是说接收方知道你要发数据给它了。而UDP是面向无连接的传输层协议,并不提供可靠的数据传输。有一个很恰当的比喻:UDP传输就类似于写信,接收方事先并不知道你要写信给他;而TCP传输就像是打电话,必须等对方按了接听键你才能更他通话。

TCP(Transmission Control Protocol),又叫传输控制协议。 TCP协议是面向连接的,可靠的,基于字节流的传输协议。在基于 TCP 进行通信时,通信双方需要先建立一个 TCP 连接,建立连接需要经过三次握手,断开连接的时候需要经过四次挥手。

TCP协议

TCP头部

Xnip2020-01-13_11-25-00

对于 TCP 头部来说,以下几个字段是很重要的:

序列号(Sequence number)

这个序号保证了 TCP 传输的报文都是有序的,对端可以通过序号顺序的拼接报文

确认号(Acknowledgement Number)

这个序号表示数据接收端期望接收的下一个字节的编号是多少,同时也表示上一个序号的数据已经收到

窗口大小(Window Size)

表示还能接收多少字节的数据,用于流量控制

标识符

  • ACK=1 :该字段为一表示确认号字段有效。此外,TCP 还规定在连接建立后传送的所有报文段都必须把 ACK 置为一。
  • SYN=1:当SYN=1,ACK=0时,表示当前报文段是一个连接请求报文。当SYN=1,ACK=1时,表示当前报文段是一个同意建立连接的应答报文。
  • FIN=1:该字段为一表示此报文段是一个释放连接的请求报文。
  • URG=1 : 该字段为一表示本数据报的数据部分包含紧急信息,是一个高优先级数据报文,此时紧急指针有效。紧急数据一定位于当前数据包数据部分的最前面,紧急指针标明了紧急数据的尾部。
  • PSH=1 :该字段为一表示接收端应该立即将数据 push 给应用层,而不是等到缓冲区满后再提交。
  • RST=1:该字段为一表示当前 TCP 连接出现严重问题,可能需要重新建立 TCP 连接,也可以用于拒绝非法的报文段和拒绝连接请求。

三次握手

Xnip2020-01-13_11-49-16

简单的说:

第一次握手

  • SYN = 1, seq(client) = x
  • 客户端向服务端发送连接请求报文段。该报文段中包含自身的数据通讯初始序号。请求发送后,客户端便进入 SYN-SENT 状态。

第二次握手

  • SYN = 1,ACK = 1,确认序号 = x+1, seq(server) = y
  • 服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,该应答中也会包含自身的数据通讯初始序号,发送完成后便进入 SYN-RECEIVED 状态

第三次握手

  • ACK = 1,确认序号 = y+1, seq(client) = x + 1
  • 客户端收到连接同意的应答后,还要向服务端发送一个确认报文。客户端发完这个报文段后便进入ESTABLISHED 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时连接建立成功。

为什么不用两次握手?

主要是为了防止已经失效的连接请求报文突然又传送到了服务器,从而产生资源浪费。

采用两次握手,那么若Client向Server发起的包A1如果在传输链路上遇到的故障,导致传输到Server的时间相当滞后,在这个时间段由于Client没有收到Server的对于包A1的确认,那么就会重传一个包A2,假设服务器正常收到了A2的包,然后返回确认B2包。由于没有第三次握手,这个时候Client和Server已经建立连接了。再假设A1包随后在链路中传到了Server,这个时候Server又会返回B1包确认,但是由于Client已经清除了A1包,所以Client会丢弃掉这个确认包,但是Server会保持这个相当于“僵尸”的连接,造成Server的网络资源浪费。

如果你用过对讲机你就会明白:

  • C ->S: 你能听到吗?
  • S->C: 听到。你能听到我吗?
  • C->S:听到。

如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接,就不会建立新的连接造成资源浪费。

四次挥手

Xnip2020-01-13_11-57-03

TCP 是全双工的,在断开连接时两端都需要发送 FIN 和 ACK。

第一次挥手

  • 若客户端 A 认为数据发送完成,则它需要向服务端 B 发送连接释放请求。

第二次挥手

  • B 收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,表示 A 到 B 的连接已经释放,不接收 A 发的数据了。但是因为 TCP 连接时双向的,所以 B 仍旧可以发送数据给 A。

第三次挥手

  • B 如果此时还有没发完的数据会继续发送,完毕后会向 A 发送连接释放请求,然后 B 便进入LAST-ACK状态。
  • PS:通过延迟确认的技术(通常有时间限制,否则对方会误认为需要重传),可以将第二次和第三次挥手合并,延迟 ACK 包的发送。

第四次挥手

  • A 收到B返回的释放请求后,向 B 发送确认应答,此时 A 进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有 B 的重发请求的话,就进入 CLOSED 状态。当 B 收到确认应答后,也便进入 CLOSED 状态。

A:我开始断开了。 B:我收到, 你可以断开了。断开了输出流,输入流还没关闭。 B:发送完所有数据后, 我服务器开始断开了,持续发送FIN命令。 A:发送ACK命令后, 就真的断开了。

挥手和握手相关问题

1. 为什么 A 要进入 TIME-WAIT 状态,等待 2MSL 时间后才进入 CLOSED 状态?

为了保证 B 能收到 A 的确认应答。若 A 发完确认应答后直接进入 CLOSED 状态,如果确认应答因为网络问题一直没有到达,那么会造成 B 不能正常关闭。

如果A发送完ACK应答之后直接进入CLOSED状态的话,如果因为网络延迟问题这个应答丢失或在2MSL内还没有到达B的话,那么B等待超时之后就会重新发送一个FIN包,但是此时A已经关闭了,永远得不到A的响应,从而导致B永远不能正常关闭

2. 为什么需要TIME_WAIT状态

第一:为实现TCP这种全双工连接的可靠释放

这样可让TCP再次发送最后的ACK以防这个ACK丢失(另一端超时并重发最后的FIN)这种2MSL等待的另一个结果是这个TCP连接在2MSL等待期间,定义这个连接的插口(客户的IP地址和端口号,服务器的IP地址和端口号)不能再被使用。这个连接只能在2MSL结束后才能再被使用。

第二:为使旧的数据包在网络因过期而消失

每个具体TCP实现必须选择一个报文段最大生存时间MSL。它是任何报文段被丢弃前在网络内的最长时间。

3. 为什么建立连接是三次握手,关闭连接确是四次挥手呢?

  1. 建立连接的时候, 服务器在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。
  2. 而关闭连接时,服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必全部数据都发送给对方了。

拥塞处理

拥塞处理包括了四个算法,分别为:慢开始,拥塞避免,快速重传,快速恢复。

为什么TCP这么复杂?

因为既要保证可靠性, 同时又要尽可能提高性能。

保证可靠性的机制

  • 校验和
  • 序列号(按序到达)
  • 确认应答
  • 超时重传
  • 连接管理
  • 流量控制
  • 拥塞控制

提高性能的机制

  • 滑动窗口
  • 快速重传
  • 延迟应答
  • 捎带应答

定时器

  • 超时重传定时器
  • 保活定时器
  • TIME_WAIT定时器

基于 TCP 的应用层协议

  • HTTP
  • HTTPS
  • SSH
  • Telnet
  • FTP
  • SMTP

TCP 包大小

我们在用Socket编程时,UDP协议要求包小于64K。TCP没有限定,TCP包头中就没有“包长度”字段,而完全依靠IP层去处理分帧。这就是为什么TCP常常被称作一种“流协议”的原因,开发者在使用TCP服务的时候,不必去关心数据包的大小,只需讲SOCKET看作一条数据流的入口,往里面放数据就是了,TCP协议本身会进行拥塞/流量控制。

TCP的核心API

转载地址: https://blog.csdn.net/Robot__Man/article/details/80657727

获取本地地址和端口号

java.net.Socket为套接字类,其提供了很多方法,其中我们可以通过Socket获取本地的地址以及端口号。

- int getLocalPort()该方法用于获取本地使用的端口号
- InetAddress getLocalAddress()该方法用于获取套接字绑定的本地地址

使用InetAddress获取本地的地址方法:

- String getCanonicalHostName()获取此IP地址的完全限定域名
- String getHostAddress()返回IP地址字符串以文本表现形式)。

获取远端地址和端口号

通过Socket获取远端的地址以及端口号。

- int getPort()该方法用于获取远端使用的端口号
- InetAddress getInetAddress()该方法用于获取套接字绑定的远端地址

获取网络输入流和网络输出流

通过Socket获取输入流与输出流,这两个方法是使用Socket通讯的关键方法。

- InputStream getInputStream()该方法用于返回此套接字的输入流
- OutputStream getOutputStream()该方法用于返回此套接字的输出流

close方法

当使用Socket进行通讯完毕后,要关闭Socket以释放系统资源。

- close()关闭此套接字当关闭了该套接字后也会同时关闭由此获取的输入流与输出流

Socket通信模型

Server端ServerSocket监听

java.net.ServerSocket是运行于服务端应用程序中。通常创建ServerSocket需要指定服务端端口号,之后监听Socket的连接:

 //创建ServerSocket并申请服务端口8088
 ServerSocket server = new ServerSocket(8088);
 //方法会产生阻塞,直到某个Socket连接,并返回请求连接的Socket
 Socket socket = server.accept();

Client端Socket连接

当服务端创建ServerSocket并通过accept()方法侦听后,我们就可以通过在客户端应用程序中创建Socket来向服务端发起连接。 需要注意的是,创建Socket的同时就发起连接,若连接异常会抛出异常。

//参数1:服务端的IP地址,参数2:服务端的服务端口
Socket socket = new Socket("localhost",8088);

Nagle 算法

TCP/IP协议中,无论发送多少数据,总是要在数据前面加上协议头,同时,对方接收到数据,也需要发送ACK表示确认。为了尽可能的利用网络带宽,TCP总是希望尽可能的发送足够大的数据。Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。

// 但是对于实时交互性高的程序,建议其改为 true,即关闭 Nagle 算法,客户端每发送一次数据,无论数据包大小都会将这些数据发送出去
clientSocket.setTcpNoDelay(true);

「真诚赞赏,手留余香」

真诚赞赏,手留余香

使用微信扫描二维码完成支付