TCP三次握手和四次挥手与Java Socket
简介
想要理解 TCP 的三次握手和四次挥手和 Java Socket,首先需要掌握 TCP 的报头结构(传送门)。如下图所示:
00~31 表示 32 个比特位,即 32 个二进制位。
- 序列号 Seq:当前数据段的第一个字节的序列号。
- 确认编号 Ack:期望接收到下一个数据段的序列号。
- 紧急指针 Urgent Pointer:指向紧急数据序列中最后一个字节的序列号。
标识 | 描述 |
---|---|
URG | 紧急指针标识位。 |
ACK | 确认编号标识位。 |
PSH | 提示接收端应用程序立即从TCP缓冲区把数据取走 |
RST | 发送方要求重新建立连接,复位 |
SYN | 请求建立连接。 |
FIN | 希望断开连接。 |
- URG 设置为 1,紧急指针需要被优先处理
- ACK 设置为 1,表示 确认编号 Ack 有效
- SYN 设置为 1,表示 序列号 Seq 设定为随机初始值
何为套接字
在 TCP 术语中,一个 IP 地址和一个端口号的组合有时被称为 端点 (endpoint)或者 套接字 (socket)。
每个 TCP 连接由一对套接字或者端点(四元组,客户端IP,客户端端口,服务端IP,服务端端口)唯一地标识。
摘自《TCP/IP 详解卷 1》——12章第3节;另外 RFC0793 有英文原文描述
简单来说,套接字 = IP地址 + 端口号。
实战
启动 wireshark
启动 Java 服务端
public class EchoServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("服务器等待连接...");
Socket clientSocket = serverSocket.accept();
System.out.println("服务端正在接收信息...");
InputStream inStream = clientSocket.getInputStream();
OutputStream outStream = clientSocket.getOutputStream();
Scanner in = new Scanner(inStream);
PrintWriter out = new PrintWriter(outStream, true /*autoFlush*/);
out.println("Hello! Enter BYE to exit");
System.out.println("服务端正在读取信息...");
boolean done = false;
while (!done && in.hasNextLine()) {
String line = in.nextLine();
// 回声
String echo = "Echo:" + line;
out.println(echo);
if (line.trim().equals("BYE")) done = true;
}
System.out.println("服务器关闭连接...");
inStream.close();
serverSocket.close();
}
}
首先在 IDEA 中启动该 Java 服务端:
如上图所示,服务端代码阻塞在了 serverSocket.accept()
等待客户端的连接
启动 Windows Telnet 客户端
Win+R
打开运行,输入cmd
打开命令行提示符- 输入
telnet 127.0.0.1 8080
- 连接成功,接收到服务端发来的消息 Hello! Enter BYE to exit
- 同时按下
Ctrl
+]
,进入 Microsoft Telnet Client
- 输入 close, 按下回车,主动关闭客户端连接
实验结果
关于实验结果的疑问
-
问题一:为什么发出去的
Hello! Enter BYE to exit
是 24 个字节,但是却显示长度为 26 呢?怎么多了两个字节?
答:查询 ASCii 码表,得知0d
表示 CR 回车,0a
表示 LF 换行/新行。out.println
在原来的基础上追加了回车和换行字符\r\n
。 -
问题二:除了客户机第一个发起连接请求的 SYN 报文外,其他每个报文都有 ACK 置位?
RFC0793 明确规定,除了第一个握手报文SYN除外,其它所有报文必须将ACK = 1。
追问:
TCP作为一个可靠传输协议,其可靠性就是依赖于收到对方的数据,发送ACK给对方,这样对方就可以释放缓存的数据,因为对方确信数据已经被接收到了。但TCP报文是在IP网络上传输,丢包是家常便饭,接收方要抓住一切的机会,把消息告诉发送方。最方便的方式就是,任何我方发送的TCP报文,都要捎带着ACK状态位。 -
问题三:我们发现,带数据(长度 Len > 0)的包都将 PSH 置位?
该标志表示发送端缓存为空。也就是说,当 PSH 置位的数据包发送完成以后,发送端没有其他数据包需要传送。
三次握手
三段握手,发生在建立连接的阶段。
- CLOSED:无连接状态
- LISTEN:等待任意远程 TCP 和对应端口发来的连接请求
- SYN-SENT:在发出连接请求后,等待匹配的连接请求
- SYN-RECEIVED:已经收到和发出连接请求,正在等待连接请求的确认
- ESTABLISHED:已建立连接,可以传送数据给用户。TCP 连接的数据传输阶段的正常状态。
四次挥手
- FIN-WAIT-1:等待来自远程主机的连接终止请求或先前发送的连接终止请求的确认。
- FIN-WAIT-2:等待来自远程主机的连接终止请求。
- CLOSE-WAIT:等待来自远程主机的连接终止请求。
- LAST-ACK:等待先前发送给远程主机的连接终止请求的确认(其中包括其连接终止请求的确认)。
- TIME-WAIT:等待足够的时间以确保远程主机收到其连接终止请求的确认。
FIN-WAIT-2 半关闭状态
已经接收到先前发送给远程主机的连接终止请求的确认,等待来自远程主机的连接终止请求,当前客户机就进入了 FIN-WAIT-2 阶段。
这是在关闭连接时,客户端和服务器两次挥手之后的状态,是著名的半关闭的状态了,在这个状态下,应用程序还有接受数据的能力,但是已经无法发送数据,但是也有一种可能是,客户端一直处于 FIN-WAIT-2 状态,而服务器则一直处于 CLOSE-WAIT 状态,而直到应用层来决定关闭这个状态。
Java 半关闭客户端
public class HalfCloseClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket();
socket.connect(new InetSocketAddress(InetAddress.getLocalHost(), 8080));
PrintWriter out = new PrintWriter(socket.getOutputStream());
Scanner in = new Scanner(socket.getInputStream());
out.print("nice to meet you");
out.flush();
socket.shutdownOutput();
while (in.hasNextLine()) {
String line = in.nextLine();
System.out.println(line);
}
socket.close();
}
}
结果
半关闭 (half-close) 提供了这样一种能力:套接字连接的一端可以终止其输出,同时仍就可以接收来自另一端的数据。
如上图所示,客户端执行 socket.shutdownOutput()
之后,客户端进入了 FIN-WAIT-2 阶段,但是此时客户端仍然可以接收服务端传输的数据,并且还可以发送确认。