【JavaEE--网络编程】网络编程套接字

1. 网络编程基础

所谓的网络资源,其实就是在网络中可以获取的各种数据资源。
而所有的网络资源,都是通过网络编程来进行数据传输的。

网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输)。
在这里插入图片描述

当然,我们只要满足进程不同就行;所以即便是同一个主机,只要是不同进程,基于网络来传输数据,
也属于网络编程。特殊的,对于开发来说,在条件有限的情况下,一般也都是在一个主机中运行多个进程来完成网络编程。

但是,我们一定要明确,我们的目的是提供网络上不同主机,基于网络来传输数据资源:

  • 进程A:编程来获取网络资源
  • 进程B:编程来提供网络资源

关于发送端和接收端:
在一次网络数据传输时:

  • 发送端:数据的发送方进程,称为发送端。发送端主机即网络通信中的源主机。
  • 接收端:数据的接收方进程,称为接收端。接收端主机即网络通信中的目的主机。
  • 收发端:发送端和接收端两端,也简称为收发端。
  • 注意:发送端和接收端只是相对的,只是一次网络数据传输产生数据流向后的概念。

2. Socket套接字

概念:Socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程。

分类:
Socket套接字主要针对传输层协议划分为如下三类:

2.1 流套接字:使用传输层TCP协议

(TCP,即Transmission Control Protocol(传输控制协议),传输层协议。)
以下为TCP的特点:

  • 有连接 :类似于打电话,需要自己拨号和对方接电话
  • 可靠传输 :对方有没有收到数据是可知的
  • 面向字节流 :一个字节一个一节(字节数组)为单位进行数据传输
  • 有接收缓冲区,也有发送缓冲区
  • 大小不限

对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的情
况下,是无边界的数据,可以多次发送,也可以分开多次接收。

2.2 数据报套接字:使用传输层UDP协议

UDP,即User Datagram Protocol(用户数据报协议),传输层协议。
以下为UDP的特点:

  • 无连接 (不需要建立连接即可直接发送数据,类似于发微信)
  • 不可靠传输 (对方有没有接收到数据是不可知的)
  • 面向数据报 (以数据报为单位进行数据传输)
  • 有接收缓冲区,无发送缓冲区
  • 大小受限:一次最多传输64k

对于数据报来说,可以简单的理解为,传输数据是一块一块的,发送一块数据假如100个字节,必须一
次发送,接收也必须一次接收100个字节,而不能分100次,每次接收1个字节。

3. Java数据报套接字通信模型

对于UDP协议来说,具有无连接,面向数据报的特征,即每次都是没有建立连接,并且一次发送全部数
据报,一次接收全部的数据报。

java中使用UDP协议通信,主要基于 DatagramSocket 类来创建数据报套接字,并使用
DatagramPacket 作为发送或接收的UDP数据报。对于一次发送及接收UDP数据报的流程如下:
在这里插入图片描述

4. Java流套接字通信模型

在这里插入图片描述

5. UDP数据报套接字编程

5.1 DatagramSocket API

DatagramSocket 是UDP Socket,用于发送和接收UDP数据报。
DatagramSocket 构造方法:
在这里插入图片描述
DatagramSocket 方法:
在这里插入图片描述

5.2 DatagramPacket API

DatagramPacket是UDP Socket发送和接收的数据报。
DatagramPacket 构造方法:
在这里插入图片描述

DatagramPacket 方法:
在这里插入图片描述
构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创
建。

5.3 InetSocketAddress API

InetSocketAddressSocketAddress 的子类 )构造方法:
在这里插入图片描述

5.4 UdpEchoServer(回显模式下的udp服务器)

回显模式:客服端数据怎么发送给服务器的,服务器就怎么返回数据给客服端

package network;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

// 站在服务器的角度:
// 1. 源 IP: 服务器程序本机的 IP
// 2. 源端口: 服务器绑定的端口 (此处手动指定了 9090)
// 3. 目的 IP: 包含在收到的数据报中. (客户端的IP)
// 4. 目的端口: 包含在收到的数据报中. (客户端的端口)
// 5. 协议类型: UDP

public class UdpEchoServer {
    // 进行网络编程, 第一步就需要先准备好 socket 实例~ 这是进行网络编程的大前提
    private DatagramSocket socket=null;

    public UdpEchoServer(int port) throws SocketException {
        socket=new DatagramSocket(port);
    }

    //启动服务器
    public void start() throws IOException {
        System.out.println("启动服务器!");
        // UDP 不需要建立连接, 直接接收从客户端来的数据即可
        while (true){
            //1.读取客服端发来的请求
            DatagramPacket requestPacket=new DatagramPacket(new byte[1024],1024);
            // 为了接受数据, 需要先准备好一个空的 DatagramPacket 对象, 由 receive 来进行填充数据
            socket.receive(requestPacket);
            // 把 DatagramPacket 解析成一个 String
            String request=new String(requestPacket.getData(),0,requestPacket.getLength(),"UTF-8");
            //2.根据请求计算相应(因为这是一个回显服务,此步骤省略)
            String response=process(request);
            //3.把响应写回到客服端
            //getSocketAddress,即包含了ip,又包含了端口号
            DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length
            ,requestPacket.getSocketAddress());
            socket.send(responsePacket);
            System.out.printf("[%s:%d] req:%s,resp:%s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),request,response);

        }
    }

    // 由于是回显服务, 响应就和请求一样了.
    // 实际上对于一个真实的服务器来说, 这个过程是最复杂的. 为了实现这个过程, 可能需要几万行, 几十万行代码....
    public String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        UdpEchoServer server=new UdpEchoServer(9090);
        server.start();
    }
}

5.5 UdpEchoClient (回显模式下的udp客服端)

package network;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Scanner;

public class UdpEchoClient {
    private DatagramSocket socket=null;
    private String serverIP;
    private int serverPort;

    // 站在客户端的角度:
    // 源 IP: 本机 IP
    // 源端口: 系统分配的端口
    // 目的 IP: 服务器的 IP
    // 目的端口: 服务器的端口
    // 协议类型: UDP
    public UdpEchoClient(String ip,int port) throws SocketException {
        // 此处的 port 是服务器的端口.
        // 客户端启动的时候, 不需要给 socket 指定端口. 客户端自己的端口是系统随机分配的
        socket=new DatagramSocket();
        serverIP=ip;
        serverPort=port;
    }

    public void start() throws IOException{
        Scanner scanner=new Scanner(System.in);
        while (true){
            // 1. 先从控制台读取用户输入的字符串
            System.out.print("-> ");
            String request=scanner.next();
            // 2. 把这个用户输入的内容, 构造成一个 UDP 请求, 并发送.
            //    构造的请求里包含两部分信息:
            //    1) 数据的内容. request 字符串
            //    2) 数据要发给谁~ 服务器的 IP + 端口
            DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length
            , InetAddress.getByName(serverIP),serverPort);
            socket.send(requestPacket);
            //3.从服务器读取响应数据,并解析
            DatagramPacket responsePacket=new DatagramPacket(new byte[1024],1024);
            socket.receive(responsePacket); //读取的内容放到创建好的这个数据报里
            String response=new String(responsePacket.getData(),0,responsePacket.getLength(),"UTF-8");
            //4.把响应结果显示到控制台上
            System.out.printf("req:%s,resp:%s\n",request,response);

        }
    }

    public static void main(String[] args) throws IOException {
        // 由于服务器和客户端在同一个机器上, 使用的 IP 仍然是 127.0.0.1 . 如果是在不同的机器上, 当然就需要更改这里的 IP 了
        UdpEchoClient client=new UdpEchoClient("127.0.0.1",9090);
        client.start();
    }
}

运行代码:
客服端:
在这里插入图片描述
服务器:(IP:端口号)
在这里插入图片描述
注意:勾选运行代码设置模块下的这个按钮就可以创建出多个不同端口号的客服端程序:
在这里插入图片描述
在这里插入图片描述
不同客服端程序被创建出来所被绑定的端口号也是不同的,此处由系统自动分配:
在这里插入图片描述

5.6 UdpDictServer (Udp服务器-简单实现翻译功能)

这个实现翻译功能的服务器直接继承自UdpEchoServer类,只需要重写一下process加工数据方法的业务逻辑即可:

package network;

import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;

public class UdpDictServer extends UdpEchoServer{

    private HashMap<String,String> dict=new HashMap<>();
    public UdpDictServer(int port) throws SocketException {
        super(port);

        // 简单构造几个词
        dict.put("cat", "小猫");
        dict.put("dog", "小狗");
        dict.put("fuck", "卧槽");
        dict.put("pig", "小猪");

    }

    @Override
    public String process(String request) {
        return dict.getOrDefault(request,"该词无法被翻译");
    }

    public static void main(String[] args) throws IOException {
        UdpDictServer server=new UdpDictServer(9090);
        server.start();
    }
}

客服端服务器通用上一个:
在这里插入图片描述

6. TCP流套接字编程

6.1 ServerSocket API

ServerSocket 是创建TCP服务端Socket的API。
ServerSocket 构造方法:
在这里插入图片描述
ServerSocket 方法:
在这里插入图片描述

ServerSocket API 只用于接收从客服端传过来的程序(类似于只完成接电话这一操作)

6.2 Socket API

Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端
Socket。
不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。

Socket 构造方法:
在这里插入图片描述

Socket 方法:
在这里插入图片描述

6.3TcpEchoServer(回显模式下的Tcp服务器)

package network;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class TcpEchoServer {
    private ServerSocket serverSocket=null ;//建立连接专用socket
    public TcpEchoServer(int port) throws IOException{
        serverSocket=new ServerSocket(port);
    }

    public void start() throws IOException{
        System.out.println("服务器启动!");
        while (true){
            // 由于 TCP 是有连接的, 不能一上来就读数据, 而要先建立连接. (类似于接电话)
            // accept 就是在 "接电话", 接电话的前提是, 有人给你打了, 如果当前没有客户端尝试建立连接, 此处的 accept 就会阻塞.
            // accept 返回了 一个 socket 对象, 称为 clientSocket. 后续和客户端之间的沟通, 都是通过 clientSocket 来完成的.
            // 进一步讲, serverSocket 就干了一件事, 建立连接(接电话)
            Socket clientSocket=serverSocket.accept();
            processConnection(clientSocket);
        }
    }

    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s:%d] 客户端建立连接!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
        // 接下来来处理请求和响应
        // 这里的针对 TCP socket 的读写就和文件读写是一模一样的
        try(InputStream inputStream=clientSocket.getInputStream()) {
            try(OutputStream outputStream=clientSocket.getOutputStream()) {
                // 循环的处理每个请求, 分别返回响应
                Scanner scanner=new Scanner(inputStream);
                while (true){
                    //1.读取请求
                    if (!scanner.hasNext()){
                        System.out.printf("%s:%d 客户端断开连接!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
                        break;
                    }
                    // 此处用 Scanner 更方便. 如果不用 Scanner 就用原生的 InputStream 的 read 也是可以的
                    String request=scanner.next();
                    // 2. 根据请求, 计算响应
                    String response=process(request);
                    // 3. 把这个响应返回给客户端
                    // 为了方便起见, 可以使用 PrintWriter 把 OutputStream 包裹一下
                    PrintWriter printWriter=new PrintWriter(outputStream);
                    printWriter.println(response);
                    // 刷新缓冲区, 如果没有这个刷新, 可能客户端就不能第一时间看到响应结果.
                    printWriter.flush();

                    System.out.printf("[%s:%d] req:%s,resp:%s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort()
                    ,request,response);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            //最后记得关闭生命周期较短的clientSocket
            try {
                clientSocket.close();
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }

    private String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        TcpEchoServer server=new TcpEchoServer(9090);
        server.start();
    }
}

6.4 TcpEchoClient(回显模式下的Tcp客服端)

package network;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;

public class TcpEchoClient {
    // 用普通的 socket 即可, 不用 ServerSocket 了
    // 此处也不用手动给客户端指定端口号, 让系统自由分配.
    private Socket socket=null;

    public TcpEchoClient(String serverIP,int serverPort) throws IOException {
        // 其实这里是可以给顶IP+端口号的. 但是这里给了之后, 含义是不同的.
        // 这里传入的 ip 和 端口号 的含义表示的不是自己绑定, 而是表示和这个 ip 端口建立连接!!
        // 调用这个构造方法, 就会和服务器建立连接 (打电话拨号了)
        socket=new Socket(serverIP,serverPort);
    }

    public void start(){
        System.out.println("和服务器建立连接成功!");
        Scanner scanner=new Scanner(System.in);
        try(InputStream inputStream=socket.getInputStream()) {
            try(OutputStream outputStream=socket.getOutputStream()) {
                while (true){
                    // 要做的事情, 仍然是四个步骤
                    // 1. 从控制台读取字符串
                    System.out.print("-> ");
                    String request=scanner.next();
                    // 2. 根据读取的字符串, 构造请求, 把请求发给服务器
                    PrintWriter printWriter=new PrintWriter(outputStream);
                    printWriter.println(request);
                    printWriter.flush();// 如果不刷新, 可能服务器无法及时看到数据.
                    // 3. 从服务器读取响应, 并解析
                    Scanner respScanner=new Scanner(inputStream);
                    String response=respScanner.next();
                    // 4. 把结果显示到控制台上.
                    System.out.printf("req:%s ,resp:%s",request,response);
                }
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        TcpEchoClient client=new TcpEchoClient("127.0.0.1",9090);
        client.start();

    }
}

运行程序:
(相当于传给服务器两条数据,一条hello,一条tcp,服务器回显给客服端原样数据)
客服端:
在这里插入图片描述
服务器:
在这里插入图片描述

6.5 TcpThreadEchoServer (多线程实现可以允许有多个客服端发生请求)

由于上面的Tcp服务器存在一定的问题,上述服务器存在两个循环,外层循环用来建立连接,建立连接成功之后就会进入内层循环来读取客服端请求,那么内层的读取客服端请求的循环不结束,外层循环就不会进行,这样就导致了这个服务器只能与一个客服端建立连接成功,这显然是不合常理的;

解决这个问题的方法就是运用多线程,在主线程中进行与客服端建立连接,一旦连接建立成功,接下来的读取客服端请求的代码就交给一个线程执行,这样两个线程并发执行,互不干扰,就实现了可以同时读取多个客服端的请求,解决了问题~

下面的运用创建多个线程进行读取客服端请求的代码:

package network;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class TcpThreadEchoServer {
    private ServerSocket serverSocket=null ;//建立连接专用socket
    public TcpThreadEchoServer(int port) throws IOException {
        serverSocket=new ServerSocket(port);
    }

    public void start() throws IOException{
        System.out.println("服务器启动!");
        while (true){
            // 由于 TCP 是有连接的, 不能一上来就读数据, 而要先建立连接. (类似于接电话)
            // accept 就是在 "接电话", 接电话的前提是, 有人给你打了, 如果当前没有客户端尝试建立连接, 此处的 accept 就会阻塞.
            // accept 返回了 一个 socket 对象, 称为 clientSocket. 后续和客户端之间的沟通, 都是通过 clientSocket 来完成的.
            // 进一步讲, serverSocket 就干了一件事, 建立连接(接电话)
            Socket clientSocket=serverSocket.accept();
            // [改进方法] 在这个地方, 每次 accept 成功, 都创建一个新的线程, 由新线程负责执行这个 processConnection 方法
            Thread t=new Thread(()->{
                processConnection(clientSocket);
            });
            t.start();
        }
    }

    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s:%d] 客户端建立连接!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
        // 接下来来处理请求和响应
        // 这里的针对 TCP socket 的读写就和文件读写是一模一样的
        try(InputStream inputStream=clientSocket.getInputStream()) {
            try(OutputStream outputStream=clientSocket.getOutputStream()) {
                // 循环的处理每个请求, 分别返回响应
                Scanner scanner=new Scanner(inputStream);
                while (true){
                    //1.读取请求
                    if (!scanner.hasNext()){
                        System.out.printf("%s:%d 客户端断开连接!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
                        break;
                    }
                    // 此处用 Scanner 更方便. 如果不用 Scanner 就用原生的 InputStream 的 read 也是可以的
                    String request=scanner.next();
                    // 2. 根据请求, 计算响应
                    String response=process(request);
                    // 3. 把这个响应返回给客户端
                    // 为了方便起见, 可以使用 PrintWriter 把 OutputStream 包裹一下
                    PrintWriter printWriter=new PrintWriter(outputStream);
                    printWriter.println(response);
                    // 刷新缓冲区, 如果没有这个刷新, 可能客户端就不能第一时间看到响应结果.
                    printWriter.flush();

                    System.out.printf("[%s:%d] req:%s,resp:%s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort()
                            ,request,response);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            //最后记得关闭生命周期较短的clientSocket
            try {
                clientSocket.close();
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }

    private String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        TcpThreadEchoServer server=new TcpThreadEchoServer(9090);
        server.start();
    }
}

6.6 TcpThreadPoolEchoServer(线程池实现可以允许有多个客服端发生请求)

这个与上面创建多个线程执行读取客服端请求的代码类似,只是把创建多个线程改成了运用线程池,提升了效率 ~

代码如下:

package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TcpThreadPoolEchoServer {
    // 但是在 Java socket 中是体现不出来 "监听" 的含义的~~
    // 之所以这么叫, 其实是 操作系统原生的 API 里有一个操作叫做 listen
    // private ServerSocket listenSocket = null;
    private ServerSocket serverSocket = null;

    public TcpThreadPoolEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动!");
        ExecutorService pool=Executors.newCachedThreadPool();

        while (true) {
            // 由于 TCP 是有连接的, 不能一上来就读数据, 而要先建立连接. (接电话)
            // accept 就是在 "接电话", 接电话的前提是, 有人给你打了~~, 如果当前没有客户端尝试建立连接, 此处的 accept 就会阻塞.
            // accept 返回了 一个 socket 对象, 称为 clientSocket. 后续和客户端之间的沟通, 都是通过 clientSocket 来完成的.
            // 进一步讲, serverSocket 就干了一件事, 接电话~~
            Socket clientSocket = serverSocket.accept();
            // [改进方法] 在这个地方, 每次 accept 成功, 都创建一个新的线程, 由新线程负责执行这个 processConnection 方法~
            // 通过线程池来实现
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    processConnection(clientSocket);
                }
            });
        }
    }

    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s:%d] 客户端建立连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
        // 接下来来处理请求和响应
        // 这里的针对 TCP socket 的读写就和文件读写是一模一样的!!
        try (InputStream inputStream = clientSocket.getInputStream()) {
            try (OutputStream outputStream = clientSocket.getOutputStream()) {
                // 循环的处理每个请求, 分别返回响应
                Scanner scanner = new Scanner(inputStream);
                while (true) {
                    // 1. 读取请求
                    if (!scanner.hasNext()) {
                        System.out.printf("[%s:%d] 客户端断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
                        break;
                    }
                    // 此处用 Scanner 更方便. 如果不用 Scanner 就用原生的 InputStream 的 read 也是可以的
                    String request = scanner.next();
                    // 2. 根据请求, 计算响应
                    String response = process(request);
                    // 3. 把这个响应返回给客户端
                    // 为了方便起见, 可以使用 PrintWriter 把 OutputStream 包裹一下
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    printWriter.println(response);
                    // 刷新缓冲区, 如果没有这个刷新, 可能客户端就不能第一时间看到响应结果.
                    printWriter.flush();

                    System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress().toString(),
                            clientSocket.getPort(), request, response);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 此处要记得来个关闭操作.
            try {
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        TcpThreadPoolEchoServer server = new TcpThreadPoolEchoServer(9090);
        server.start();
    }
}

执行:

客服端1
在这里插入图片描述
客服端2
在这里插入图片描述
服务器在这里插入图片描述
通过运用多线程的改进,服务器就可以同时与多个客服端建立连接并读取返回客服端请求了,tcp服务器实现成功 ~

6.7 TcpDicServer (Tcp服务器-简单实现翻译功能)

package network;

import java.io.IOException;
import java.util.HashMap;

public class TcpDicServer extends TcpEchoServer{

    private HashMap<String,String> dict=new HashMap<>();

    public TcpDicServer(int port) throws IOException {
        super(port);

        // 简单构造几个词
        dict.put("cat", "小猫");
        dict.put("dog", "小狗");
        dict.put("fuck", "卧槽");
        dict.put("pig", "小猪");
    }

    @Override
    public String process(String request) {
        return dict.getOrDefault(request,"该词无法被翻译");
    }

    public static void main(String[] args) throws IOException {
        TcpDicServer tcpDicServer=new TcpDicServer(9090);
        tcpDicServer.start();
    }
}

在这里插入图片描述
在这里插入图片描述
测试成功,简单tcp翻译服务器实现成功~

  • over ~ ✨
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值