Bootstrap

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

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

本身这次打算将三次握手、四次挥手都做出来。但发现内容越来越多了,所以就只实现了三次握手。但依然为后续操作做了大量的铺垫。

系列文章:

  1. 使用Socket在局域网中进行广播
  2. 【Java】使用Socket实现查找IP并建立连接?手把手教你
  3. 【Java】使用Socket手搓三次握手 从原理到实践

在这里插入图片描述


前情提要

强烈建议在阅读这篇博客之前阅读之前的文章

在前面两篇文章中,我们实现了广播和接收。并且在收到广播后对客户端发起连接。实现了一个从一对多通信一对一通信的转换。

这篇博客将在此基础上实现更完善的功能

三次握手

在前面的文章中,我们也提到过了:使用UDP实现的数据传输是不保证安全性的。换句话说,如果在一个复杂的网络中,常常会出现一些不可预料的小问题。

比如:

当前的方案

在这里插入图片描述

发现问题了吧,客户端的小小一个广播,就让服务器认为成功通信,并且持续的为广播地址服务。而客户端收到一条数据后,就认为建立了连接,从而停止继续广播。

表面看上去似乎没有问题。但如果有一个恶意程序,不断的发送广播、发送数据,就会发生这样的情况。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这不仅仅对服务器造成了资源的浪费,还让客户端无法收到正确的消息。如果涉及到了业务,将会破坏功能,影响程序的正常使用。

三次握手

三次握手就是用来解决这个问题的。先来看一下三次握手的作用。

作用
  • 确保连接的可靠性‌:通过三次握手,客户端和服务器都能确认对方已经准备好进行数据传输,从而确保连接的可靠性。

  • 防止已失效的连接请求‌:通过三次握手,可以防止网络中的失效连接请求被误认为是有效的连接请求,从而浪费服务器资源。

  • 流量控制和拥塞避免‌:三次握手不仅确保了连接的建立,还能通过协商数据传输的初始窗口大小,帮助控制流量和避免网络拥塞

一言以蔽之:三次握手就是为了告诉双方,你的请求我确实收到了,我们已经可以开始通信了。

过程

老生常谈啦,直接上图:

在这里插入图片描述

步骤
  1. 客户端向服务器:设置seq = n,发送第一次握手。此时客户端设置自己状态为SYN

  2. 服务器向客户端:设置seq = m,并且设置ack = n + 1,发送第二次握手。此时服务器设置自己状态为SYN ACK

  3. 客户端向服务器:设置ack = m + 1,发送第三次握手。此时客户端进入就绪态。认为此时已经建立连接了。

  4. 服务器:收到第三次握手后,服务器进入就绪态。此时认为已经建立连接了。

原因

为什么客户端收到第二次握手后就认为成功建立连接,而服务器需要收到第三次才行呢?

面经:为什么要三次握手,两次行不行?

  1. 客户端向服务器:第一次握手。这一步没能获得任何信息。

  2. 服务器向客户端:第二次握手。这里主要分为两部分。客户端与服务器。

    • 客户端:客户端收到了服务器发来的信息,意味着客户端知道了:服务器已经收到我的请求了并且我收到了服务器的请求。前者代表服务器的接收能力、客户端的发送能力 都正常。后者代表客户端的接收能力正常。
    • 服务器:服务器收到了客户端发来的信息,意味着服务器知道了:我收到了客户端的请求。代表客户端的发送能力正常、服务器的接收能力正常。
  3. 客户端向服务器:第三次握手。这里服务器收到了客户端的信息。服务器知道了:客户端收到了我的请求。意味着客户端的接收能力正常、服务器的发送能力正常。

至此,客户端和服务器都知道了双方的发送能力、接受能力都是正常的,可以进行数据传输了。

改造

知道了三次握手与三次握手的原理。就可以开始着手进行修改了。别小看这一步,手搓三次握手,实际上是在实现一个新的协议。就是没人用

定义数据结构

由于只实现三次握手部分的功能。因此只需要定义一个简单数据结构,方便我们使用就行了。上图

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

是不是有点太过于简单。但已经完完全全的足够工作了。

举个例子: 一条带消息的同步数据为 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";

三次握手的准备工作就完成了

思路

然后就是三次握手的具体实现了

看看我们当前的工作:

在这里插入图片描述

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

  1. 客户端发送第一次握手的广播。
  2. 服务器收到广播,像客户端发送第二次握手。
  3. 客户端收到第二次握手,认为连接成功建立。停止发送广播,并且向服务器发送第三次握手。
  4. 服务器收到第三次握手,认为连接成功建立。将客户端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();
    }
}

注意

  • 实际上在三次握手之后seqack就已经没有任何作用了。但我在这里依然做了保留。后续可以引入更多逻辑加入验证。
  • 在收消息的时候,通过判断类型来确定这条消息数据什么状态。
  • 如果判断是正在握手中的状态,则进一步判断ack是否正确。如果不正确,则忽略该消息。继续等待其他的消息。
  • 引入了最大等待时间。未来将通过最大等待时间进行消息重传等控制。

总结

  • 其实在搞明白握手的原理以及作用的时候,一切就已经明了了。
  • 定义了各种类型,方便后续拓展更多功能(四次挥手)。
  • 需要在接受消息的时候判断消息的类型。对各种类型分别进行处理。但依然要小心骚扰信息的存在。

三次握手实现完成,看看效果:

在这里插入图片描述
其中客户端:

在这里插入图片描述
其中服务端:

在这里插入图片描述

效果很好!晚安!


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

;