Socket网络编程(四)——点对点传输场景方案

目录

  • 场景
  • 如何去获取到TCP的IP和Port?
  • UDP的搜索IP地址、端口号方案
  • UDP搜索取消实现
    • 相关的流程:
    • 代码实现逻辑
      • 服务端实现
      • 客户端实现
      • UDP搜索代码执行结果
  • TCP点对点传输实现
    • 代码实现步骤
    • 点对点传输测试结果
  • 源码下载

场景

在一个局域网当中,不知道服务器的IP地址,仅仅知道服务器公共的UDP的端口,在这种情况下,想要实现TCP的连接。TCP是点对点的连接,所以需要知道TCP的连接IP地址和端口Port。

如何去获取到TCP的IP和Port?

可以通过UDP的搜索实现,

  1. 当我们的服务器与我们所有的客户端之间约定了搜索的格式之后,我们可以在客户端发起广播
  2. 然后服务器在收到广播之后判断一下这些收到的广播是否是需要处理的。那么服务器就会回送这些广播到对应的端口(地址)上去。
  3. 客户端就能收到服务器回送过来的UDP的包。收到的这些数据包,里面就包含了端口号、IP地址等。
  4. 根据以上的流程就能够UDP的搜索得到TCP服务器的IP地址和TCP的端口,然后使用这些信息来实现TCP的连接。

UDP的搜索IP地址、端口号方案

  1. 构建基础口令消息
    原理:如果要实现UDP的交互,就要约定一组公共的数据格式,也就是基础的口令头。如果没有约定口令消息,那么别人发送的消息到达我们的服务器后就会去回送,这就会导致我们自己的基本信息(比如IP\Port)的暴露。
  2. 局域网广播口令消息(指定端口)
  3. 接收指定端口回送消息(得到客户端IP、Port,这里的客户端IP指的是server端)

20240228-154508-t3.png
如上图,BroadCast发出广播,如果有设备(服务器)感兴趣就会回送到BroadCast。如果三台(服务器)都感兴趣,就都会回送到BroadCast。

UDP搜索取消实现

相关的流程:

  1. 异步线程接收回送消息
  2. 异步线程等待完成(定时)
  3. 关闭等待-终止线程等待

代码实现逻辑

服务端实现

  1. TCP/UDP基础信息字段
    TCPConstants.java
public class TCPConstants {
 
    // 服务器固化UDP接收端口
    public static int PORT_SERVER = 30401;
}
  1. UDP基础信息
    UDPConstants.java
public class UDPConstants {
 
    // 公用头部(8个字节都是7,就是可回复的)
    public static byte[] HEADER = new byte[]{7,7,7,7,7,7,7,7};
    // 服务器固化UDP接收端口
    public static int PORT_SERVER = 30201;
    // 客户端回送端口
    public static int PORT_CLIENT_RESPONSE = 30202;
}
  1. 工具类ByteUtils
    用于校验是否为正确的口令。即对HEADER进行校验。
public class ByteUtils {
    /**
     * Does this byte array begin with match array content?
     *
     * @param source Byte array to examine
     * @param match  Byte array to locate in <code>source</code>
     * @return true If the starting bytes are equal
     */
    public static boolean startsWith(byte[] source, byte[] match) {
        return startsWith(source, 0, match);
    }
 
    /**
     * Does this byte array begin with match array content?
     *
     * @param source Byte array to examine
     * @param offset An offset into the <code>source</code> array
     * @param match  Byte array to locate in <code>source</code>
     * @return true If the starting bytes are equal
     */
    public static boolean startsWith(byte[] source, int offset, byte[] match) {
 
        if (match.length > (source.length - offset)) {
            return false;
        }
 
        for (int i = 0; i < match.length; i++) {
            if (source[offset + i] != match[i]) {
                return false;
            }
        }
        return true;
    }
 
    /**
     * Does the source array equal the match array?
     *
     * @param source Byte array to examine
     * @param match  Byte array to locate in <code>source</code>
     * @return true If the two arrays are equal
     */
    public static boolean equals(byte[] source, byte[] match) {
 
        if (match.length != source.length) {
            return false;
        }
        return startsWith(source, 0, match);
    }
 
    /**
     * Copies bytes from the source byte array to the destination array
     *
     * @param source      The source array
     * @param srcBegin    Index of the first source byte to copy
     * @param srcEnd      Index after the last source byte to copy
     * @param destination The destination array
     * @param dstBegin    The starting offset in the destination array
     */
    public static void getBytes(byte[] source, int srcBegin, int srcEnd, byte[] destination,
                                int dstBegin) {
        System.arraycopy(source, srcBegin, destination, dstBegin, srcEnd - srcBegin);
    }
 
    /**
     * Return a new byte array containing a sub-portion of the source array
     *
     * @param srcBegin The beginning index (inclusive)
     * @param srcEnd   The ending index (exclusive)
     * @return The new, populated byte array
     */
    public static byte[] subbytes(byte[] source, int srcBegin, int srcEnd) {
        byte destination[];
 
        destination = new byte[srcEnd - srcBegin];
        getBytes(source, srcBegin, srcEnd, destination, 0);
 
        return destination;
    }
 
    /**
     * Return a new byte array containing a sub-portion of the source array
     *
     * @param srcBegin The beginning index (inclusive)
     * @return The new, populated byte array
     */
    public static byte[] subbytes(byte[] source, int srcBegin) {
        return subbytes(source, srcBegin, source.length);
    }
}
  1. 服务器端接收约定数据包,解析成功并回送包的代码
    ServerProvider
public class ServerProvider {
 
    private static Provider PROVIDER_INSTANCE;
 
    static void start(int port){
        stop();
        String sn = UUID.randomUUID().toString();
        Provider provider = new Provider(sn, port);
        provider.start();
        PROVIDER_INSTANCE = provider;
    }
 
    static void stop(){
        if(PROVIDER_INSTANCE != null){
            PROVIDER_INSTANCE.exit();
            PROVIDER_INSTANCE = null;
        }
    }
 
 
    private static class Provider extends Thread{
        private final byte[] sn;
        private final int port;
        private boolean done = false;
        private DatagramSocket ds = null;
        // 存储消息的Buffer
        final byte[] buffer = new byte[128];
 
        public Provider(String sn, int port){
            super();
            this.sn = sn.getBytes();
            this.port = port;
        }
 
        @Override
        public void run() {
            super.run();
 
            System.out.println("UDDProvider Started.");
 
            try {
                // 监听20000 端口
                ds = new DatagramSocket(UDPConstants.PORT_SERVER);
                // 接收消息的Packet
                DatagramPacket receivePacket = new DatagramPacket(buffer,buffer.length);
                while(!done){
                    // 接收
                    ds.receive(receivePacket);
 
                    // 打印接收到的信息与发送者的信息
                    // 发送者的IP地址
                    String clientIp = receivePacket.getAddress().getHostAddress();
                    int clientPort = receivePacket.getPort();
                    int clientDataLen = receivePacket.getLength();
                    byte[] clientData = receivePacket.getData();
                    boolean isValid = clientDataLen >= (UDPConstants.HEADER.length + 2 + 4) && ByteUtils.startsWith(clientData,UDPConstants.HEADER);
                    System.out.println("ServerProvider receive from ip:" + clientIp + "\tport:" + clientIp +"\tport:"+clientPort+"\tdataValid:"+isValid);
 
                    if(!isValid){
                        //无效继续
                        continue;
                    }
 
                    // 解析命令与回送端口
                    int index = UDPConstants.HEADER.length;
                    short cmd = (short) ((clientData[index++] << 8) | (clientData[index++] & 0xff));
                    int responsePort = (((clientData[index++]) << 24) |
                            ((clientData[index++] & 0xff) << 16) |
                            ((clientData[index++] & 0xff) << 8) |
                            ((clientData[index++] & 0xff)));
 
                    // 判断合法性
                    if( cmd == 1 && responsePort > 0){
                        // 构建一份回送数据
                        ByteBuffer byteBuffer = ByteBuffer.wrap(buffer);
                        byteBuffer.put(UDPConstants.HEADER);
                        byteBuffer.putShort((short)2);
                        byteBuffer.putInt(port);
                        byteBuffer.put(sn);
                        int len = byteBuffer.position();
                        // 直接根据发送者构建一份回送信息
                        DatagramPacket responsePacket = new DatagramPacket(buffer,len,receivePacket.getAddress(),responsePort);
                        ds.send(responsePacket);
                        System.out.println("ServerProvider response to:" + clientIp + "\tport:"+responsePort + "\tdataLen: " + len);
                    }else {
                        System.out.println("ServerProvider receive cmd nonsupport; cmd:" + cmd + "\tport:" + port);
                    }
                }
            } catch (SocketException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
 
        private void close() {
            if( ds != null ){
                ds.close();
                ds = null;
            }
        }
 
        /**
         * 提供结束
         */
        void exit(){
            done = true;
            close();
        }
    }
}
  1. main方法启动类
public class Server {
 
    public static void main(String[] args) {
        ServerProvider.start(TCPConstants.PORT_SERVER);
        try{
            System.in.read();
        } catch (IOException e){
            e.printStackTrace();
        }
        ServerProvider.stop();
    }
}

客户端实现

客户端广播发送消息包代码

  1. 服务器端消息实体
    ServerInfo
public class ServerInfo {
 
    private String sn;
    private int port;
    private String address;
 
    public ServerInfo(int port, String address, String sn) {
        this.sn = sn;
        this.port = port;
        this.address = address;
    }
    
    省略set/get方法 ……
    
}
  1. 客户端启动main方法类
public class Client {
 
    public static void main(String[] args) {
        // 定义10秒的搜索时间,如果超过10秒未搜索到,就认为服务器端没有开机
        ServerInfo info = ClientSearcher.searchServer(10000);
        System.out.println("Server:" + info);
    }
}
  1. 客户端接收服务器端回送与广播发送的具体逻辑
    ClientSearcher
public class ClientSearcher {
 
    private static final int LISTENT_PORT = UDPConstants.PORT_CLIENT_RESPONSE;
 
    public static ServerInfo searchServer(int timeout){
        System.out.println("UDPSearcher Started.");
 
        //  成功收到回送的栅栏
        CountDownLatch receiveLatch = new CountDownLatch(1);
        Listener listener = null;
        try{
            // 监听
            listener = listen(receiveLatch);
            // 发送广播
            sendBroadCast();
            // 等待服务器返回,最长阻塞10秒
            receiveLatch.await(timeout, TimeUnit.MILLISECONDS);
        }catch (Exception e){
            e.printStackTrace();
        }
        // 完成
        System.out.println("UDPSearcher Finished.");
        if(listener == null){
            return null;
        }
        List<ServerInfo> devices = listener.getServerAndClose();
        if(devices.size() > 0){
            return devices.get(0);
        }
        return null;
    }
 
    /**
     * 监听服务器端回送的消息
     * @param receiveLatch
     * @return
     * @throws InterruptedException
     */
    private static Listener listen(CountDownLatch receiveLatch) throws InterruptedException {
        System.out.println("UDPSearcher start listen.");
        CountDownLatch startDownLatch = new CountDownLatch(1);
        Listener listener = new Listener(LISTENT_PORT, startDownLatch,receiveLatch);
        listener.start();   // 异步操作,开启端口监听
        startDownLatch.await();
        return listener;
    }
 
    /**
     * 发送广播逻辑
     * @throws IOException
     */
    private static void sendBroadCast() throws IOException {
        System.out.println("UDPSearcher sendBroadcast started.");
 
        // 作为搜索方,让系统自动分配端口
        DatagramSocket ds = new DatagramSocket();
 
        // 构建一份请求数据
        ByteBuffer byteBuffer = ByteBuffer.allocate(128);
        // 头部
        byteBuffer.put(UDPConstants.HEADER);
        // CMD命名
        byteBuffer.putShort((short)1);
        // 回送端口信息
        byteBuffer.putInt(LISTENT_PORT);
        // 直接构建Packet
        DatagramPacket requestPacket = new DatagramPacket(byteBuffer.array(), byteBuffer.position() + 1);
        // 广播地址
        requestPacket.setAddress(InetAddress.getByName("255,255.255.255"));
        // 设置服务器端口
        requestPacket.setPort(UDPConstants.PORT_SERVER);
 
        // 发送
        ds.send(requestPacket);
        ds.close();
 
        // 完成
        System.out.println("UDPSearcher sendBroadcast finished.");
 
    }
 
    /**
     * 广播消息的接收逻辑
     */
    private static class Listener extends Thread {
        private final int listenPort;
        private final CountDownLatch startDownLatch;
        private final CountDownLatch receiveDownLatch;
        private final List<ServerInfo> serverInfoList = new ArrayList<>();
        private final byte[] buffer = new byte[128];
        private final int minLen = UDPConstants.HEADER.length + 2 + 4; // 2:CMD命令长度  4:TCP端口号长度
        private boolean done = false;
        private DatagramSocket ds = null;
 
        private Listener(int listenPort,CountDownLatch startDownLatch,CountDownLatch receiveDownLatch){
            super();
            this.listenPort = listenPort;
            this.startDownLatch = startDownLatch;
            this.receiveDownLatch = receiveDownLatch;
        }
 
       @Override
       public void run(){
            super.run();
 
            // 通知已启动
            startDownLatch.countDown();
            try{
                // 监听回送端口
                ds = new DatagramSocket(listenPort);
                // 构建接收实体
                DatagramPacket receivePacket = new DatagramPacket(buffer,buffer.length);
                while( !done){
                    // 接收
                    ds.receive(receivePacket);
                    // 打印接收到的信息与发送者的信息
                    // 发送者的IP地址
                    String ip = receivePacket.getAddress().getHostAddress();
                    int port = receivePacket.getPort();
                    int dataLen = receivePacket.getLength();
                    byte[] data = receivePacket.getData();
                    boolean isValid = dataLen >= minLen
                            && ByteUtils.startsWith(data, UDPConstants.HEADER);
 
                    System.out.println("UDPSearch receive form ip:" + ip + "\tport:" + port + "\tdataValid:" + isValid);
 
                    if( !isValid ) {
                        // 无效继续
                        continue;
                    }
                    // 跳过口令字节,从具体数据开始
                    ByteBuffer byteBuffer = ByteBuffer.wrap(buffer,UDPConstants.HEADER.length, dataLen);
                    final short cmd = byteBuffer.getShort(); // 占据2个字节
                    final int serverPort = byteBuffer.getInt(); // 占据4个字节
                    if(cmd != 2 || serverPort <= 0){
                        System.out.println("UDPSearcher receive cmd:" + cmd + "\tserverPort:" + serverPort);
                        continue;
                    }
 
                    String sn = new String(buffer,minLen,dataLen - minLen);
                    ServerInfo info = new ServerInfo(serverPort,ip,sn);
                    serverInfoList.add(info);
                    // 成功接收到一份
                    receiveDownLatch.countDown();
                }
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                close();
            }
           System.out.println("UDPSearcher listener finished.");
       }
 
       private void close(){
           if(ds != null){
               ds.close();
               ds = null;
           }
       }
 
        List<ServerInfo> getServerAndClose() {
            done = true;
            close();
            return serverInfoList;
        }
    }
}

UDP搜索代码执行结果

服务端启动接收的结果:

UDDProvider Started.
ServerProvider receive from ip:169.254.178.74	port:169.254.178.74	port:61968	dataValid:true
ServerProvider response to:169.254.178.74	port:30202	dataLen: 50

客户端监听并发起广播的执行结果:

UDPSearcher Started.
UDPSearcher start listen.
UDPSearcher sendBroadcast started.
UDPSearcher sendBroadcast finished.
UDPSearch receive form ip:169.254.178.74	port:30201	dataValid:true
UDPSearcher Finished.
Server:ServerInfo{sn='ed4ab162-5d5c-49eb-b80e-6ddeb8b223e0', port=30401, address='169.254.178.74'}
UDPSearcher listener finished.
 
Process finished with exit code 0

由以上结果可知,启动服务端后,客户端在启动listen监听后,向服务器端发送数据包,并获得服务器端的回送,经解析后,该回送的数据包中可以获得 ip/port,可用于TCP连接使用。在UDP解析数据包过程中,通过口令保证了客户端与服务端对消息发送、接收、回送的有效,避免不必要的回应。

TCP点对点传输实现

基于前面UDP广播-搜索的机制,Server-Client获得了建立Socket链接的IP\Port信息。
可以接着使用该信息进行建立TCP的Socket连接,实现点对点的数据收发。

代码实现步骤

  1. TCP服务端main启动方法
public class Server {
 
    public static void main(String[] args) {
 
        TCPServer tcpServer = new TCPServer(TCPConstants.PORT_SERVER);
        boolean isSucceed = tcpServer.start();
        if(!isSucceed){
            System.out.println("Start TCP server failed.");
        }
        UDPProvider.start(TCPConstants.PORT_SERVER);
 
        try{
            System.in.read();
        } catch (IOException e){
            e.printStackTrace();
        }
 
        UDPProvider.stop();
        tcpServer.stop();
    }
}

在UDP搜索的基础上,我们获得了TCP的链接IP。创建tcpServer对相应的端口进行监听客户端链接请求。

  1. 服务端异步线程处理Socket
    TCPServer
public class TCPServer {
    private final int port;
    private ClientListener mListener;
 
    /**
     * 构造
     * @param port
     */
    public TCPServer(int port){
        this.port = port;
    }
 
    /**
     * 开始
     * @return
     */
    public boolean start(){
        try{
            ClientListener listener = new ClientListener(port);
            mListener = listener;
            listener.start();
        }catch (Exception e){
            e.printStackTrace();
            return false;
        }
        return true;
    }
 
    /**
     * 结束
     */
    public void stop(){
        if(mListener != null){
            mListener.exit();
        }
    }
 
    /**
     * 监听客户端链接
     */
    private static class ClientListener extends Thread {
        private ServerSocket server;
        private boolean done = false;
 
        private ClientListener(int port) throws IOException {
            server = new ServerSocket(port);
            System.out.println("服务器信息: " + server.getInetAddress() + "\tP:" + server.getLocalPort());
        }
 
        @Override
        public void run(){
            super.run();
 
            System.out.println("服务器准备就绪~");
            // 等待客户端连接
            do{
                // 得到客户端
                Socket client = null;
                try {
                    client = server.accept();
                }catch (Exception e){
                    e.printStackTrace();
                }
                // 客户端构建异步线程
                ClientHandler clientHandler = new ClientHandler(client);
                // 启动线程
                clientHandler.start();
            }while (!done);
            System.out.println("服务器已关闭!");
        }
        void exit(){
            done = true;
            try {
                server.close();
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }
 
    /**
     * 客户端消息处理
     */
    private static class ClientHandler extends Thread{
        private Socket socket;
        private boolean flag = true;
 
        ClientHandler(Socket socket ){
            this.socket = socket;
        }
 
        @Override
        public void run(){
            super.run();
            System.out.println("新客户链接: " + socket.getInetAddress() + "\tP:" + socket.getPort());
            try {
                // 得到打印流,用于数据输出;服务器回送数据使用
                PrintStream socketOutput = new PrintStream(socket.getOutputStream());
                // 得到输入流,用于接收数据
                BufferedReader socketInput = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                do {
                    // 客户端拿到一条数据
                    String str = socketInput.readLine();
                    if( "bye".equalsIgnoreCase(str)){
                        flag = false;
                        // 回送
                        socketOutput.println("bye");
                    }else {
                        // 打印到屏幕,并回送数据长度
                        System.out.println(str);
                        socketOutput.println("回送: " + str.length());
                    }
                }while (flag);
                socketInput.close();
                socketOutput.close();
            }catch (IOException e){
                System.out.println("连接异常断开");
            }finally {
                // 连接关闭
                try {
                    socket.close();
                }catch (IOException e){
                    e.printStackTrace();
                }
            }
            System.out.println("客户端已退出:" + socket.getInetAddress() + "\tP:" + socket.getPort());
        }
    }
}

accept() 监听到客户端的链接后,通过输入流读取客户端数据,并通过输出流回送数据长度。

  1. 基于UDP回送结果建立的TCP客户端
    Client main方法
public class Client {
 
    public static void main(String[] args) {
        // 定义10秒的搜索时间,如果超过10秒未搜索到,就认为服务器端没有开机
        ServerInfo info = UDPSearcher.searchServer(10000);
        System.out.println("Server:" + info);
 
        if( info != null){
            try {
                TCPClient.linkWith(info);
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }
}

获得UDP的回送后,我们知道了建立TCP的ip、port。也就是serverInfo不为null,取出相关参数建立Socket 链接。

  1. 建立客户端连接类
    TCPClient
public class TCPClient {
 
    public static void linkWith(ServerInfo info) throws IOException {
        Socket socket = new Socket();
        // 超时时间
        socket.setSoTimeout(3000);
        // 端口2000;超时时间300ms
        socket.connect(new InetSocketAddress(Inet4Address.getByName(info.getAddress()),info.getPort()));//
 
        System.out.println("已发起服务器连接,并进入后续流程~");
        System.out.println("客户端信息: " + socket.getLocalAddress() + "\tP:" + socket.getLocalPort());
        System.out.println("服务器信息:" + socket.getInetAddress() + "\tP:" + socket.getPort());
 
        try {
            // 发送接收数据
            todo(socket);
        }catch (Exception e){
            System.out.println("异常关闭");
        }
 
        // 释放资源
        socket.close();
        System.out.println("客户端已退出~");
    }
 
    private static void todo(Socket client) throws IOException {
        // 构建键盘输入流
        InputStream in = System.in;
        BufferedReader input = new BufferedReader(new InputStreamReader(in));
 
        // 得到Socket输出流,并转换为打印流
        OutputStream outputStream = client.getOutputStream();
        PrintStream socketPrintStream = new PrintStream(outputStream);
 
        // 得到Socket输入流,并转换为BufferedReader
        InputStream inputStream = client.getInputStream();
        BufferedReader socketBufferedReader = new BufferedReader(new InputStreamReader(inputStream));
 
        boolean flag = true;
        do {
            // 键盘读取一行
            String str = input.readLine();
            // 发送到服务器
            socketPrintStream.println(str);
 
            // 从服务器读取一行
            String echo = socketBufferedReader.readLine();
            if("bye".equalsIgnoreCase(echo)){
                flag = false;
            }else {
                System.out.println(echo);
            }
        }while(flag);
        // 资源释放
        socketPrintStream.close();
        socketBufferedReader.close();
    }
}

建立Socket链接,从键盘读取一行发送到服务器;并从服务器读取一行。以上就是基于UDP广播-搜索实现TCP点对点传输的逻辑。

点对点传输测试结果

基于UDP实现的TCP服务端日志

服务器信息: 0.0.0.0/0.0.0.0	P:30401
服务器准备就绪~
UDDProvider Started.
ServerProvider receive from ip:169.254.178.74	port:169.254.178.74	port:51322	dataValid:true
ServerProvider response to:169.254.178.74	port:30202	dataLen: 50
新客户链接: /169.254.178.74	P:57172
ping
pong

基于UDP实现的TCP客户端日志:

UDPSearcher start listen.
UDPSearcher sendBroadcast started.
UDPSearcher sendBroadcast finished.
UDPSearch receive form ip:169.254.178.74	port:30201	dataValid:true
UDPSearcher Finished.
Server:ServerInfo{sn='10595790-14d1-44dc-a068-4c64c956a944', port=30401, address='169.254.178.74'}
UDPSearcher listener finished.
已发起服务器连接,并进入后续流程~
客户端信息: /169.254.178.74	P:57172
服务器信息:/169.254.178.74	P:30401
ping
回送: 4
pong
回送: 4

源码下载

下载地址:https://gitee.com/qkongtao/socket_study/tree/master/src/main/java/cn/kt/socket/SocketDemo_L5_UDP

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/421815.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

使用Python改造一款外星人入侵小游戏

目录 引言 游戏概述 准备工作 游戏设计 1. 初始化设置 2. 创建飞船和外星人 3. 创建子弹 4. 游戏循环 5. 优化和扩展 总结 引言 当我们提及Python编程语言时&#xff0c;很多人首先想到的是数据分析、机器学习或网络爬虫等高级应用。然而&#xff0c;Python同样适用…

Windows系统误删文件恢复

最近很多用户反馈误删文件的场景比较多.下面华仔将讲解数据恢复的原理和过程.以及一些注意事项。 建议的数据恢复软件 1.EaseUS Data Recovery Wizard(易我数据恢复)需要断网使用 2.Wondershare Recoverit(万兴数据恢复)&#xff0c; Windows系统删除文件原理&#xff1a;如果是…

PageHelper开源框架解读

在使用springboot开发系统时&#xff0c;列表查询经常会用PageHelper来进行分页。使用起来很方便&#xff0c;但从未想过它的实现原理&#xff0c;所以对其进行解读。 Service public class ScUserServiceImpl extends ServiceImpl<ScUserMapper, ScUser> implements IS…

ABAP - SALV教程02 - 开篇:打开SALV的三种方式之二

全屏模式生成SALV的方式&#xff1a;http://t.csdnimg.cn/CzNLz本文讲解生成可控模式的SALV&#xff0c;该方式需要依赖自己创建屏幕的自定义控件区域&#xff08;Custom Control&#xff09;实现步骤&#xff1a;需要注意的点是SALV的实例对象和dispaly方法一定是在屏幕PBO事件…

CrossOver 24下载-CrossOver 24 for Mac下载 v24.0.0中文永久版

CrossOver 24是一款可以让mac用户能够自由运行和游戏windows游戏软件的虚拟机类应用&#xff0c;虽然能够虚拟windows但是却并不是一款虚拟机&#xff0c;也不需要重启系统或者启动虚拟机&#xff0c;类似于一种能够让mac系统直接运行windows软件的插件。它以其出色的跨平台兼容…

Spring Initializer环境问题

1.基于jdk8与本地 环境准备 1)下载jdk8并安装 2&#xff09;下载maven 3.6.3并解压放入D盘maven目录下&#xff0c;去掉外层 设置阿里源 打开settings.xml,在mirrors标签之内增加&#xff0c;注意粘贴后</id>中的/有可能被删掉&#xff0c;要自己补上 <mirror>&l…

前端导出word文件的多种方式、前端导出excel文件

文章目录 纯前借助word模板端导出word文件 &#xff08;推荐&#xff09;使用模板导出 前端通过模板字符串导出word文件前端导出 excel文件&#xff0c;node-xlsx导出文件&#xff0c;行列合并 纯前借助word模板端导出word文件 &#xff08;推荐&#xff09; 先看效果&#xf…

HCIP-VLAN综合实验(VLAN的Access接口、Trunk接口、Hybrid 接口、dot1q封装,DHCP设置)

VLAN&#xff08;Virtual Local Area Network&#xff09;即虚拟局域网&#xff0c;是将一个物理的LAN在逻辑上划分成多个广播域的通信技术。每个VLAN是一个广播域&#xff0c;VLAN内的主机间通信就和在一个LAN内一样&#xff0c;而VLAN间则不能直接互通&#xff0c;这样&#…

2024环境工程、能源系统与化学材料国际会议(ICEEESCM 2024)

2024环境工程、能源系统与化学材料国际会议&#xff08;ICEEESCM 2024) 一、【会议简介】 2024环境工程、能源系统与化学材料国际会议&#xff08;ICEEESCM 2024)将于2024年在西安举行。会议将围绕环境工程、能源系统与化学材料等议题展开讨论&#xff0c;旨在为从事环境工程…

idea使用maven创建springboot项目

按照图片中的流程来&#xff0c;就可以创建springboot项目&#xff0c;我这个主要是想做一个JavaWeb项目 有用的话&#xff0c;点个小赞赞再走呀~

IPD MM流程之业务策略工具:安索夫矩阵

IPD市场管理流程&#xff0c;华为内部称为“MM流程”&#xff08;Market Management&#xff0c;MM&#xff09;。华为市场管理是通过对市场和细分市场的分析&#xff0c;制定细分市场的策略&#xff0c;形成商业计划&#xff0c;把商业计划落实在日常工作当中。MM流程其中一个…

【Django】执行查询—跨关系查询中的跨多值关联问题

跨多值查询 跨越 ManyToManyField 或反查 ForeignKey &#xff08;例如从 Blog 到 Entry &#xff09;时&#xff0c;对多个属性进行过滤会产生这样的问题&#xff1a;是否要求每个属性都在同一个相关对象中重合。 filter() 先看filter()&#xff0c;通过一个例子看&#xf…

【three.js】搭建本地静态服务器 查询API文档使用

目录 一、为什么要搭建本地静态服务器呢&#xff1f; 二、下载three.js文件包 三、vscode配置live-server插件 一、为什么要搭建本地静态服务器呢&#xff1f; 平时学习Three.js&#xff0c;如果你想预览代码3D效果、查询API文档等&#xff0c;咱们需要提供一个本地静态服务…

基于JAVA的快递投保管理系统 开源项目

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、系统设计2.1 软件2.2 布局2.3 技术框架 三、功能模块3.1 保险类型模块3.2 快递管理模块3.3 保险订单模块 四、系统展示五、核心代码5.1 查询快递5.2 查询快递保险5.3 新增快递保险5.4 快递投保5.5 查询保险订单 六、免责说明 一、摘…

LVGL常用部件使用总结之图片部件

图片部件可用于显示图片&#xff0c;图片源可以是 C 语言数组格式的文件、二进制的.bin 文件以及图标字体。值得注意的是&#xff0c;图片部件要显示 BMP、JPEG 等格式的图片&#xff0c;则必须经过解码。 图片部件的组成部分仅有一个&#xff1a;主体&#xff08;LV_PART_MAIN…

硬盘坏了怎么把数据弄出来?数据恢复方法推荐

在数字化时代电脑硬盘中的数据承载着我们的工作成果、生活回忆和珍贵资料。然而一旦硬盘出现故障&#xff0c;数据的安全就变得岌岌可危。那么当电脑硬盘出现问题时&#xff0c;我们真的无法挽回那些重要数据了吗&#xff1f;答案是&#xff1a;不一定&#xff01;本文将为您介…

【Python】【函数】详解Python函数概念

1. 函数定义 函数是组织好的&#xff0c;可重复使用的&#xff0c;用来实现单一&#xff0c;或相关联功能的代码段。 函数能提高应用的模块性&#xff0c;和代码的重复利用率。你已经知道Python提供了许多内建函数&#xff0c;比如print()。但你也可以自己创建函数&#xf…

docker配置数据默认存储路径graph已过时,新版本中是data-root

错误信息 我在修改/etc/docker/daemon.json文件中&#xff0c;添加存储路径graph字段。然后sudo systemctl restart docker包如下错误&#xff1a;使用journalctl -xeu docker.service错误信息&#xff0c;发现不能匹配graph字段。 原因 我的docker版本&#xff1a; 在doc…

RocketMQ安装

这里写目录标题 mq服务端安装配置启动 mq管理界面安装配置启动 mq服务端 安装 RocketMQ下载地址 配置 ROCKETMQ_HOME D:\google-d\rocketmq-all-5.2.0-bin-release启动 # bin目录cmd输入 start mqnamesrv.cmdmq管理界面 安装 下载地址 配置 #修改rocketmq-console\src\…

简单数据类型和复杂数据类型

1. 简单数据类型 null是个特例: 2. 复杂数据类型 3. 堆和栈 注意&#xff1a; JavaScript 中是没有堆和栈的概念的&#xff0c;通过堆栈的概念可以更好的理解代码的一些执行方式&#xff0c;便于将来学习其他语言。 4. 简单数据类型传参 总结&#xff1a;简单数据类型传参传…