【Java】使用Socket手搓三次握手 从原理到实践
本身这次打算将三次握手、四次挥手都做出来。但发现内容越来越多了,所以就只实现了三次握手。但依然为后续操作做了大量的铺垫。
系列文章:
前情提要
强烈建议在阅读这篇博客之前阅读之前的文章
在前面两篇文章中,我们实现了广播和接收。并且在收到广播后对客户端发起连接。实现了一个从一对多通信转一对一通信的转换。
这篇博客将在此基础上实现更完善的功能
三次握手
在前面的文章中,我们也提到过了:使用UDP实现的数据传输是不保证安全性的。换句话说,如果在一个复杂的网络中,常常会出现一些不可预料的小问题。
比如:
当前的方案
发现问题了吧,客户端的小小一个广播,就让服务器认为成功通信,并且持续的为广播地址服务。而客户端收到一条数据后,就认为建立了连接,从而停止继续广播。
表面看上去似乎没有问题。但如果有一个恶意程序,不断的发送广播、发送数据,就会发生这样的情况。
这不仅仅对服务器造成了资源的浪费,还让客户端无法收到正确的消息。如果涉及到了业务,将会破坏功能,影响程序的正常使用。
三次握手
三次握手就是用来解决这个问题的。先来看一下三次握手的作用。
作用
-
确保连接的可靠性:通过三次握手,客户端和服务器都能确认对方已经准备好进行数据传输,从而确保连接的可靠性。
-
防止已失效的连接请求:通过三次握手,可以防止网络中的失效连接请求被误认为是有效的连接请求,从而浪费服务器资源。
-
流量控制和拥塞避免:三次握手不仅确保了连接的建立,还能通过协商数据传输的初始窗口大小,帮助控制流量和避免网络拥塞
一言以蔽之:三次握手就是为了告诉双方,你的请求我确实收到了,我们已经可以开始通信了。
过程
老生常谈啦,直接上图:
步骤
-
客户端向服务器:设置
seq = n
,发送第一次握手。此时客户端设置自己状态为SYN
。 -
服务器向客户端:设置
seq = m
,并且设置ack = n + 1
,发送第二次握手。此时服务器设置自己状态为SYN ACK
。 -
客户端向服务器:设置
ack = m + 1
,发送第三次握手。此时客户端进入就绪态。认为此时已经建立连接了。 -
服务器:收到第三次握手后,服务器进入就绪态。此时认为已经建立连接了。
原因
为什么客户端收到第二次握手后就认为成功建立连接,而服务器需要收到第三次才行呢?
面经:为什么要三次握手,两次行不行?
-
客户端向服务器:第一次握手。这一步没能获得任何信息。
-
服务器向客户端:第二次握手。这里主要分为两部分。客户端与服务器。
- 客户端:客户端收到了服务器发来的信息,意味着客户端知道了:服务器已经收到我的请求了、并且我收到了服务器的请求。前者代表服务器的接收能力、客户端的发送能力 都正常。后者代表客户端的接收能力正常。
- 服务器:服务器收到了客户端发来的信息,意味着服务器知道了:我收到了客户端的请求。代表客户端的发送能力正常、服务器的接收能力正常。
-
客户端向服务器:第三次握手。这里服务器收到了客户端的信息。服务器知道了:客户端收到了我的请求。意味着客户端的接收能力正常、服务器的发送能力正常。
至此,客户端和服务器都知道了双方的发送能力、接受能力都是正常的,可以进行数据传输了。
改造
知道了三次握手与三次握手的原理。就可以开始着手进行修改了。别小看这一步,手搓三次握手,实际上是在实现一个新的协议。就是没人用
定义数据结构
由于只实现三次握手部分的功能。因此只需要定义一个简单数据结构,方便我们使用就行了。上图
是不是有点太过于简单。但已经完完全全的足够工作了。
举个例子: 一条带消息的同步数据为
SYN/123;-1;这是消息
。
编写数据结构的类
package test2;
import java.util.Arrays;
public class SocketBean {
private final String type;
private final int seq;
private final int ack;
private final String content;
public static boolean isMsg(String data) {
if (data == null || data.isEmpty()) return false;
String[] dataArr = data.split("/");
return dataArr[0].equals(Constants.MSG_TYPE);
}
public SocketBean(String type, int seq, int ack, String content) {
this.type = type;
this.seq = seq;
this.ack = ack;
this.content = content;
}
public SocketBean(String data) {
String[] dataArr = data.split("/");
this.type = dataArr[0];
String[] dataArr2 = dataArr[1].split(";");
this.seq = Integer.parseInt(dataArr2[0]);
this.ack = Integer.parseInt(dataArr2[1]);
if (dataArr2.length == 3) this.content = dataArr2[2];
else this.content = "";
}
public String getType() {
return type;
}
public int getSeq() {
return seq;
}
public int getAck() {
return ack;
}
public String getContent() {
return content;
}
public String toString() {
return type + "/" + seq + ";" + ack + ";" + content;
}
}
定义一下常量
在上篇中的常量类中补充一下
// 三次握手消息类型
public static final String MSG_TYPE = "MSG";
public static final String SYNC_TYPE = "SYN";
public static final String ACK_TYPE = "ACK";
三次握手的准备工作就完成了
思路
然后就是三次握手的具体实现了
看看我们当前的工作:
事实上,完全可以让客户端发送的广播成为第一次握手。因此只需要完成接下来两次的握手就大功告成。
- 客户端发送第一次握手的广播。
- 服务器收到广播,像客户端发送第二次握手。
- 客户端收到第二次握手,认为连接成功建立。停止发送广播,并且向服务器发送第三次握手。
- 服务器收到第三次握手,认为连接成功建立。将客户端IP保存,并持续提供服务。
如图:
具体实现
这里的代码大多数在前面提到过了,如果有需要请移步历史文章查看
客户端
package test2;
import java.io.IOException;
import java.net.*;
public class ClientSocket {
private final int serverPort = Constants.SERVER_PORT;
private final int clientPort = Constants.CLIENT_PORT;
private final String serverIp = Constants.BROADCAST_IP;
private final int maxWaitTime = Constants.MAX_WAIT_TIME;
private boolean isReceiver = true;
private int seq = -1;
private int ack = -1;
/**
* 广播客户端消息到指定服务器
* 该方法在一个新线程中执行,不断向指定的服务器IP和端口发送广播消息
* 直到isReceiver标志被设置为false
*/
public void broadcast() {
// 模拟客户端发送广播的初始化过程
System.out.println("客户端发送广播:充当第一次握手");
System.out.println(serverIp + ":" + serverPort);
new Thread(() -> {
try {
DatagramSocket sock = new DatagramSocket();
InetAddress address = InetAddress.getByName(serverIp);
while (isReceiver) {
// 三次握手第一次!
seq = (int) (Math.random() * 1000 + 1);
String data = new SocketBean(Constants.SYNC_TYPE, seq, ack, "").toString();
byte[] bytes = data.getBytes();
DatagramPacket packet = new DatagramPacket(bytes, bytes.length, address, serverPort);
sock.send(packet);
System.out.println("三次握手第一次:" + data);
Thread.sleep(maxWaitTime);
}
sock.close();
System.out.println("客户端发送广播结束");
} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
}
/**
* 接收定向消息的函数
* 通过创建一个新的线程来监听指定端口,以便接收来自任何发送方的定向消息
* 此函数没有参数,也不返回任何值
*/
public void receiver() {
System.out.println("客户端接收定向消息");
new Thread(() -> {
DatagramSocket socket = null;
try {
socket = new DatagramSocket(clientPort);
byte[] data = new byte[1024];
DatagramPacket packet = new DatagramPacket(data, 1024);
while (true) {
socket.receive(packet);
if (isReceiver) {
// 收到第二次握手,此时认为连接可以顺利建立!发送第三次握手
String synData = new String(packet.getData(), 0, packet.getLength());
SocketBean socketBean = new SocketBean(synData);
// 如果ack不正确,则舍弃此数据包
if (socketBean.getAck() != seq + 1) continue;
ack = socketBean.getSeq()+1;
System.out.println("收到第二次握手,发送第三次握手");
sendPacket(new SocketBean(Constants.ACK_TYPE, seq, ack, "").toString(), packet.getAddress(), serverPort);
isReceiver = false;
continue;
}
// 顺利收到消息
String msg = new String(packet.getData(), 0, packet.getLength());
System.out.println("客户端收到消息: " + new SocketBean(msg).getContent());
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}).start();
}
public static void sendPacket(String data, InetAddress address, int serverPort) {
DatagramSocket socket = null;
try {
socket = new DatagramSocket();
byte[] bytes = data.getBytes();
DatagramPacket packet = new DatagramPacket(bytes, bytes.length, address, serverPort);
socket.send(packet);
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
ClientSocket clientSocket = new ClientSocket();
clientSocket.receiver();
clientSocket.broadcast();
}
}
服务器
package test2;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.List;
public class ServerSocket {
private final String serverIp = Constants.BROADCAST_IP;
private final int serverPort = Constants.SERVER_PORT;
private final int maxWaitTime = Constants.MAX_WAIT_TIME;
private final List<InetAddress> addresses = new ArrayList<>();
private int seq;
private int ack;
/**
* 启动服务器套接字
* 该方法初始化服务器套接字,并监听指定的IP地址和端口
* 同时,它启动一个新的线程来处理传入的广播消息
*/
public void open() {
System.out.println("ServerSocket open");
System.out.println("serverIp:" + serverIp + " serverPort:" + serverPort);
// 创建并启动一个新的线程来处理网络通信
new Thread(() -> {
try {
DatagramSocket socket = new DatagramSocket(serverPort);
// 无限循环等待并处理广播消息
while (true) {
DatagramPacket packet = new DatagramPacket(new byte[1024], 1024);
socket.receive(packet);
String msg = new String(packet.getData(), 0, packet.getLength());
System.out.println("服务器收到握手:" + msg + " 来自 " + packet.getAddress());
SocketBean socketBean = new SocketBean(msg);
// 在这里判断处于握手的状态!
switch (socketBean.getType()) {
case Constants.SYNC_TYPE:
// 三次握手第二次!
ack = socketBean.getSeq()+1;
seq = (int) (Math.random() * 1000 + 1);
String packetData = new SocketBean(Constants.ACK_TYPE, seq, ack, "").toString();
System.out.println("收到第一次握手!发送第二次握手");
sendPacket(packetData, packet.getAddress(), Constants.CLIENT_PORT);
break;
case Constants.ACK_TYPE:
// 如果ack不正确,则忽略该消息
if (socketBean.getAck() != seq + 1) continue;
// 三次握手第三次!此时认为顺利建立了连接!
addresses.add(packet.getAddress());
System.out.println("收到第三次握手!建立连接: " + packet.getAddress());
break;
default:
System.out.println("服务器收到其他类型消息:" + msg);
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}).start();
}
/**
* 发送数据报的方法
* 该方法在一个新的线程中执行,不断尝试向地址列表中的客户端发送数据
*/
public void send() {
new Thread(() -> {
DatagramSocket socket = null;
try {
socket = new DatagramSocket();
} catch (SocketException e) {
throw new RuntimeException(e);
}
while (true) {
try {
Thread.sleep(maxWaitTime);
if (!addresses.isEmpty()) {
for (InetAddress address : addresses) {
String data = new SocketBean(Constants.MSG_TYPE, seq, ack, "服务器的定向消息").toString();
byte[] bytes = data.getBytes();
DatagramPacket packet = new DatagramPacket(bytes, bytes.length, address, Constants.CLIENT_PORT);
socket.send(packet);
System.out.println("服务器发送定向数据:" + data);
}
}
} catch (InterruptedException | IOException e) {
throw new RuntimeException(e);
}
}
}).start();
}
public static void sendPacket(String data, InetAddress address, int serverPort) {
DatagramSocket socket = null;
try {
socket = new DatagramSocket();
byte[] bytes = data.getBytes();
DatagramPacket packet = new DatagramPacket(bytes, bytes.length, address, serverPort);
socket.send(packet);
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
ServerSocket serverSocket = new ServerSocket();
serverSocket.send();
serverSocket.open();
}
}
注意
- 实际上在三次握手之后
seq
、ack
就已经没有任何作用了。但我在这里依然做了保留。后续可以引入更多逻辑加入验证。 - 在收消息的时候,通过判断类型来确定这条消息数据什么状态。
- 如果判断是正在握手中的状态,则进一步判断
ack
是否正确。如果不正确,则忽略该消息。继续等待其他的消息。 - 引入了最大等待时间。未来将通过最大等待时间进行消息重传等控制。
总结
- 其实在搞明白握手的原理以及作用的时候,一切就已经明了了。
- 定义了各种类型,方便后续拓展更多功能(四次挥手)。
- 需要在接受消息的时候判断消息的类型。对各种类型分别进行处理。但依然要小心骚扰信息的存在。
三次握手实现完成,看看效果:
其中客户端:
其中服务端:
效果很好!晚安!