【Java】使用Socket手搓三次握手 从原理到实践
本身这次计划将三次握手、四次挥手都做出来。但发现内容越来越多了,以是就只实现了三次握手。但依然为后续操纵做了大量的铺垫。
系列文章:
- 使用Socket在局域网中举行广播
- 【Java】使用Socket实现查找IP并创建毗连?手把手教你
- 【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是否准确。假如不准确,则忽略该消息。继承等候其他的消息。
- 引入了最大等候时间。将来将通过最大等候时间举行消息重传等控制。
总结
- 着实在搞明确握手的原理以及作用的时间,统统就已经明白了。
- 界说了各种范例,方便后续拓展更多功能(四次挥手)。
- 须要在担当消息的时间判定消息的范例。对各种范例分别举行处理惩罚。但依然要警惕骚扰信息的存在。
三次握手实现完成,看看效果:
此中客户端:
此中服务端:
效果很好!晚安!
上一篇:【Java】使用Socket实现查找IP并创建毗连?手把手教你
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |