【Java】使用Socket手搓三次握手 从原理到实践

[复制链接]
发表于 2025-12-5 12:32:40 | 显示全部楼层 |阅读模式
【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;这是消息。
  编写数据结构的类

  1. package test2;
  2. import java.util.Arrays;
  3. public class SocketBean {
  4.     private final String type;
  5.     private final int seq;
  6.     private final int ack;
  7.     private final String content;
  8.     public static boolean isMsg(String data) {
  9.         if (data == null || data.isEmpty()) return false;
  10.         String[] dataArr = data.split("/");
  11.         return dataArr[0].equals(Constants.MSG_TYPE);
  12.     }
  13.     public SocketBean(String type, int seq, int ack, String content) {
  14.         this.type = type;
  15.         this.seq = seq;
  16.         this.ack = ack;
  17.         this.content = content;
  18.     }
  19.     public SocketBean(String data) {
  20.         String[] dataArr = data.split("/");
  21.         this.type = dataArr[0];
  22.         String[] dataArr2 = dataArr[1].split(";");
  23.         this.seq = Integer.parseInt(dataArr2[0]);
  24.         this.ack = Integer.parseInt(dataArr2[1]);
  25.         if (dataArr2.length == 3) this.content = dataArr2[2];
  26.         else this.content = "";
  27.     }
  28.     public String getType() {
  29.         return type;
  30.     }
  31.     public int getSeq() {
  32.         return seq;
  33.     }
  34.     public int getAck() {
  35.         return ack;
  36.     }
  37.     public String getContent() {
  38.         return content;
  39.     }
  40.     public String toString() {
  41.         return type + "/" + seq + ";" + ack + ";" + content;
  42.     }
  43. }
复制代码
界说一下常量

在上篇中的常量类中增补一下
  1.     // 三次握手消息类型
  2.     public static final String MSG_TYPE = "MSG";
  3.     public static final String SYNC_TYPE = "SYN";
  4.     public static final String ACK_TYPE = "ACK";
复制代码
三次握手的准备工作就完成了
思绪

然后就是三次握手的具体实现了
看看我们当前的工作:

毕竟上,完全可以让客户端发送的广播成为第一次握手。因此只须要完成接下来两次的握手就大功告成。

  • 客户端发送第一次握手的广播。
  • 服务器收到广播,像客户端发送第二次握手。
  • 客户端收到第二次握手,以为毗连乐成创建。制止发送广播,而且向服务器发送第三次握手。
  • 服务器收到第三次握手,以为毗连乐成创建。将客户端IP生存,并连续提供服务。
如图:

具体实现

这里的代码大多数在前面提到过了,假如有须要请移步汗青文章查察
客户端

  1. package test2;
  2. import java.io.IOException;
  3. import java.net.*;
  4. public class ClientSocket {
  5.     private final int serverPort = Constants.SERVER_PORT;
  6.     private final int clientPort = Constants.CLIENT_PORT;
  7.     private final String serverIp = Constants.BROADCAST_IP;
  8.     private final int maxWaitTime = Constants.MAX_WAIT_TIME;
  9.     private boolean isReceiver = true;
  10.     private int seq = -1;
  11.     private int ack = -1;
  12.     /**
  13.      * 广播客户端消息到指定服务器
  14.      * 该方法在一个新线程中执行,不断向指定的服务器IP和端口发送广播消息
  15.      * 直到isReceiver标志被设置为false
  16.      */
  17.     public void broadcast() {
  18.         // 模拟客户端发送广播的初始化过程
  19.         System.out.println("客户端发送广播:充当第一次握手");
  20.         System.out.println(serverIp + ":" + serverPort);
  21.         new Thread(() -> {
  22.             try {
  23.                 DatagramSocket sock = new DatagramSocket();
  24.                 InetAddress address = InetAddress.getByName(serverIp);
  25.                 while (isReceiver) {
  26.                     // 三次握手第一次!
  27.                     seq = (int) (Math.random() * 1000 + 1);
  28.                     String data = new SocketBean(Constants.SYNC_TYPE, seq, ack, "").toString();
  29.                     byte[] bytes = data.getBytes();
  30.                     DatagramPacket packet = new DatagramPacket(bytes, bytes.length, address, serverPort);
  31.                     sock.send(packet);
  32.                     System.out.println("三次握手第一次:" + data);
  33.                     Thread.sleep(maxWaitTime);
  34.                 }
  35.                 sock.close();
  36.                 System.out.println("客户端发送广播结束");
  37.             } catch (IOException | InterruptedException e) {
  38.                 throw new RuntimeException(e);
  39.             }
  40.         }).start();
  41.     }
  42.     /**
  43.      * 接收定向消息的函数
  44.      * 通过创建一个新的线程来监听指定端口,以便接收来自任何发送方的定向消息
  45.      * 此函数没有参数,也不返回任何值
  46.      */
  47.     public void receiver() {
  48.         System.out.println("客户端接收定向消息");
  49.         new Thread(() -> {
  50.             DatagramSocket socket = null;
  51.             try {
  52.                 socket = new DatagramSocket(clientPort);
  53.                 byte[] data = new byte[1024];
  54.                 DatagramPacket packet = new DatagramPacket(data, 1024);
  55.                 while (true) {
  56.                     socket.receive(packet);
  57.                     if (isReceiver) {
  58.                         // 收到第二次握手,此时认为连接可以顺利建立!发送第三次握手
  59.                         String synData = new String(packet.getData(), 0, packet.getLength());
  60.                         SocketBean socketBean = new SocketBean(synData);
  61.                         // 如果ack不正确,则舍弃此数据包
  62.                         if (socketBean.getAck() != seq + 1) continue;
  63.                         ack =  socketBean.getSeq()+1;
  64.                         System.out.println("收到第二次握手,发送第三次握手");
  65.                         sendPacket(new SocketBean(Constants.ACK_TYPE, seq, ack, "").toString(), packet.getAddress(), serverPort);
  66.                         isReceiver = false;
  67.                         continue;
  68.                     }
  69.                     // 顺利收到消息
  70.                     String msg = new String(packet.getData(), 0, packet.getLength());
  71.                     System.out.println("客户端收到消息: " + new SocketBean(msg).getContent());
  72.                 }
  73.             } catch (IOException e) {
  74.                 throw new RuntimeException(e);
  75.             }
  76.         }).start();
  77.     }
  78.     public static void sendPacket(String data, InetAddress address, int serverPort) {
  79.         DatagramSocket socket = null;
  80.         try {
  81.             socket = new DatagramSocket();
  82.             byte[] bytes = data.getBytes();
  83.             DatagramPacket packet = new DatagramPacket(bytes, bytes.length, address, serverPort);
  84.             socket.send(packet);
  85.             socket.close();
  86.         } catch (IOException e) {
  87.             throw new RuntimeException(e);
  88.         }
  89.     }
  90.     public static void main(String[] args) {
  91.         ClientSocket clientSocket = new ClientSocket();
  92.         clientSocket.receiver();
  93.         clientSocket.broadcast();
  94.     }
  95. }
复制代码
服务器

  1. package test2;
  2. import java.io.IOException;
  3. import java.net.DatagramPacket;
  4. import java.net.DatagramSocket;
  5. import java.net.InetAddress;
  6. import java.net.SocketException;
  7. import java.util.ArrayList;
  8. import java.util.List;
  9. public class ServerSocket {
  10.     private final String serverIp = Constants.BROADCAST_IP;
  11.     private final int serverPort = Constants.SERVER_PORT;
  12.     private final int maxWaitTime = Constants.MAX_WAIT_TIME;
  13.     private final List<InetAddress> addresses = new ArrayList<>();
  14.     private int seq;
  15.     private int ack;
  16.     /**
  17.      * 启动服务器套接字
  18.      * 该方法初始化服务器套接字,并监听指定的IP地址和端口
  19.      * 同时,它启动一个新的线程来处理传入的广播消息
  20.      */
  21.     public void open() {
  22.         System.out.println("ServerSocket open");
  23.         System.out.println("serverIp:" + serverIp + " serverPort:" + serverPort);
  24.         // 创建并启动一个新的线程来处理网络通信
  25.         new Thread(() -> {
  26.             try {
  27.                 DatagramSocket socket = new DatagramSocket(serverPort);
  28.                 // 无限循环等待并处理广播消息
  29.                 while (true) {
  30.                     DatagramPacket packet = new DatagramPacket(new byte[1024], 1024);
  31.                     socket.receive(packet);
  32.                     String msg = new String(packet.getData(), 0, packet.getLength());
  33.                     System.out.println("服务器收到握手:" + msg + " 来自 " + packet.getAddress());
  34.                     SocketBean socketBean = new SocketBean(msg);
  35.                     // 在这里判断处于握手的状态!
  36.                     switch (socketBean.getType()) {
  37.                         case Constants.SYNC_TYPE:
  38.                             // 三次握手第二次!
  39.                             ack = socketBean.getSeq()+1;
  40.                             seq = (int) (Math.random() * 1000 + 1);
  41.                             String packetData = new SocketBean(Constants.ACK_TYPE, seq, ack, "").toString();
  42.                             System.out.println("收到第一次握手!发送第二次握手");
  43.                             sendPacket(packetData, packet.getAddress(), Constants.CLIENT_PORT);
  44.                             break;
  45.                         case Constants.ACK_TYPE:
  46.                             // 如果ack不正确,则忽略该消息
  47.                             if (socketBean.getAck() != seq + 1) continue;
  48.                             // 三次握手第三次!此时认为顺利建立了连接!
  49.                             addresses.add(packet.getAddress());
  50.                             System.out.println("收到第三次握手!建立连接: " + packet.getAddress());
  51.                             break;
  52.                         default:
  53.                             System.out.println("服务器收到其他类型消息:" + msg);
  54.                     }
  55.                 }
  56.             } catch (IOException e) {
  57.                 throw new RuntimeException(e);
  58.             }
  59.         }).start();
  60.     }
  61.     /**
  62.      * 发送数据报的方法
  63.      * 该方法在一个新的线程中执行,不断尝试向地址列表中的客户端发送数据
  64.      */
  65.     public void send() {
  66.         new Thread(() -> {
  67.             DatagramSocket socket = null;
  68.             try {
  69.                 socket = new DatagramSocket();
  70.             } catch (SocketException e) {
  71.                 throw new RuntimeException(e);
  72.             }
  73.             while (true) {
  74.                 try {
  75.                     Thread.sleep(maxWaitTime);
  76.                     if (!addresses.isEmpty()) {
  77.                         for (InetAddress address : addresses) {
  78.                             String data = new SocketBean(Constants.MSG_TYPE, seq, ack, "服务器的定向消息").toString();
  79.                             byte[] bytes = data.getBytes();
  80.                             DatagramPacket packet = new DatagramPacket(bytes, bytes.length, address, Constants.CLIENT_PORT);
  81.                             socket.send(packet);
  82.                             System.out.println("服务器发送定向数据:" + data);
  83.                         }
  84.                     }
  85.                 } catch (InterruptedException | IOException e) {
  86.                     throw new RuntimeException(e);
  87.                 }
  88.             }
  89.         }).start();
  90.     }
  91.     public static void sendPacket(String data, InetAddress address, int serverPort) {
  92.         DatagramSocket socket = null;
  93.         try {
  94.             socket = new DatagramSocket();
  95.             byte[] bytes = data.getBytes();
  96.             DatagramPacket packet = new DatagramPacket(bytes, bytes.length, address, serverPort);
  97.             socket.send(packet);
  98.             socket.close();
  99.         } catch (IOException e) {
  100.             throw new RuntimeException(e);
  101.         }
  102.     }
  103.     public static void main(String[] args) {
  104.         ServerSocket serverSocket = new ServerSocket();
  105.         serverSocket.send();
  106.         serverSocket.open();
  107.     }
  108. }
复制代码
注意



  • 实际上在三次握手之后seq、ack就已经没有任何作用了。但我在这里依然做了生存。后续可以引入更多逻辑到场验证。
  • 在收消息的时间,通过判定范例来确定这条消息数据什么状态。
  • 假如判定是正在握手中的状态,则进一步判定ack是否准确。假如不准确,则忽略该消息。继承等候其他的消息。
  • 引入了最大等候时间。将来将通过最大等候时间举行消息重传等控制。
总结



  • 着实在搞明确握手的原理以及作用的时间,统统就已经明白了。
  • 界说了各种范例,方便后续拓展更多功能(四次挥手)。
  • 须要在担当消息的时间判定消息的范例。对各种范例分别举行处理惩罚。但依然要警惕骚扰信息的存在。
三次握手实现完成,看看效果:

此中客户端:

此中服务端:

效果很好!晚安!

上一篇:【Java】使用Socket实现查找IP并创建毗连?手把手教你

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表