Java网络编程

目录​​​​​​​

Ip

域名和端口号

 网络协议

TCP和UDP

InetAddress

socket

TCP网络编程

网络上传文件(TCP)

netstat指令相关的知识

一个知识点(TCP连接的秘密)

 UDP网络编程

网络编程作业 


dos层的一些指令:

ipconfig——查看当前计算机ip地址

ping 域名——查看域名对应的ip地址。

ping还有其他的指令自行百度

Ip

 ipv4:32位则有32/8 = 4个字节。每个字节的范围是0-255.(十进制表示)

ipv6:128位则有128/8 = 16个字节,每个字节的范围是0-255;(十六进制表示)

必须明白的点:

  1. 你要做网络编程必须要知道对方的ip地址,或者域名( 通过InetAdress.getByName() 转成ip )
  2. 每一个网络设备都有一个唯一的ip地址。
  3. ip地址分为ipv4和ipv6两类
  4. ipv4由4个10进制的字节表示,ipv6由16个16进制的字节表示。

域名和端口号

域名:简单来说域名就是通过http协议把ip地址映射成的一个名字。比如www.baidu.com就是由百度主机的ip地址映射而成的。域名为我们提供了ip难记的问题。

端口号:比如我们访问一个百度网址,首先我们通过ip地址可以定位到这个网址,这个网址上是不是有很多服务,比如邮件服务、网站服务、Tomcat服务。那如果只是有ip地址你确实可以定位到百度主机,但是我怎么知道你要在请求百度主机上面的什么服务,由此引出端口。每一个服务都对应着不同的端口号,我们通过程序在网络上请求任何不同的服务都需要ip+端口号才可以精准定位到对应的目标请求。

 网络协议

 

 协议的作用在互联网上来说是保证双方的程序能够识别传输的数据,只有双方都遵守同一个协议才能够实现数据的传输。这个协议有非常多,可以是自定义的协议。最最重要的协议是tcp/ip协议这个是计算机底层的一个协议,所有的计算机都必须遵守这个协议否则无法正确的识别和传输数据。

TCP和UDP

TCP的三次握手类似于下面这样

//TCP传输完数据必须释放这个连接,否则别的程序无法在通过TCP协议连接到kim,比如tom给kim打电话,打完以后不挂,那我现在又来了一个人jack需要给kim打电话,你tom不挂断我怎么了打?那这里就又体现了udp的效率高的一个点了,udp类似于发短信,你tom发短信我也可以发,互不影响。

UDP协议类似于下面这样

 

InetAddress

可以这么说,这个方法的作用就是获取本地ip或者通过域名获取远程服务器的ip地址

socket

 //socket需要通过TCP或UDP编程传输数据,socket最后一定要关闭。

//socket不是唯一的,不管是客户端还是服务端都可能有多个socket。

TCP网络编程

案例:

 执行步骤:首先两端的1、2都是准备工作,然后是客户端的3.再服务器的3.

代码演示:

        //服务端编写
        //思路:
        //1.在本机的客户端监听9999端口,等待连接
        //  前提是本机的9999客户端没有被其他服务占用(监听),不然会报错
        //ServerSocket(直接填端口号即可,因为ServerSocket会默认去本地ip找目标端口)
        ServerSocket serverSocket = new ServerSocket(9999);
        System.out.println("服务端已监听9999端口,等待客户端连接");

        //2.当没有客户端连接的时候程序会阻塞在下面这条语句,等待连接,
        //  连接后程序会返回一个Socket对象,然后程序才会再次往下执行
        Socket socket = serverSocket.accept();//accept——接收
        System.out.println("监听到客户端连接请求");


        //3.通过socket获取一个输入流InputStream,用来接收客户端发送的数据
        InputStream inputStream = socket.getInputStream();
        byte[] bytes = new byte[1024];
        int readLen = 0;
        while((readLen = inputStream.read(bytes)) != -1){
            System.out.println(new String(bytes,0,readLen));
        }

        //4.关闭流和socket、serverSocket
        inputStream.close();
        socket.close();
        serverSocket.close();
        //客户端编写
        //思路        
        //1.在客户端中通过socket对象连接目标端口(本机的9999端口),
        //若连接成功则返回一个Socket对象
        Socket socket = new Socket(InetAddress.getLocalHost(), 9999);

        //2.通过socket获取输出流OutPutStream
        OutputStream outputStream = socket.getOutputStream();
        //3.通过outputStream对象把数据写入到数据通道
        outputStream.write("hello,server~".getBytes());
        //关闭流和socket
        socket.close();
        outputStream.close();

一个端口在同一时间,只能被一个服务器(服务端)监听。服务端同一时间只能有一ServerSocket,但可以监听到很多请求,每监听到一个请求连接则会通过accept方法返回一个socket对象。

案例2

//服务端
        ServerSocket serverSocket = new ServerSocket(9999);
        Socket socket = serverSocket.accept();

        InputStream inputStream = socket.getInputStream();
        byte[] bytes = new byte[1024];
        int readLen = 0;
        while((readLen = inputStream.read(bytes)) != -1){
            System.out.println(new String(bytes,0,readLen));
        }

        OutputStream outputStream = socket.getOutputStream();
        outputStream.write("hello,client".getBytes());
        socket.shutdownOutput();
        //这里如果你不设置结束标记那么其实他就跟前面的01案例一样,一样不会报错
        //因为你下一步就是关闭流的,关闭输出流后整条数据通信将会停止传输数据
        //两个Socket端口被释放

        socket.close();
        inputStream.close();
        outputStream.close();
        serverSocket.close();
//客户端
        Socket socket = new Socket(InetAddress.getLocalHost(),9999);

        OutputStream outputStream = socket.getOutputStream();
        outputStream.write("hello,server".getBytes());

        socket.shutdownOutput();

        InputStream inputStream = socket.getInputStream();
        byte[] bytes = new byte[1024];
        int readLen = 0;
        while((readLen = inputStream.read(bytes)) != -1){
            System.out.println(new String(bytes,0,readLen));
        }

//你会发现跟案例一相比无非就是在服务端多了一个输出流,在客户端多了一个输入流。但是事实上里面有一个非常重要的细节,就是设置输出结束标记——socket.shutdownOutput();它的作用是结束当前输出数据。

它的作用是在双方传输数据的时候两边根本不知道你有没有说完,就是客户端再发送完“hello,server~”后,服务端确实是可以监听到你传输过来的数据并且打印,问题就是如果客户端此时不设置输出结束标记,那么服务端就会无法确认你后面是否还有数据需要传输过来,他会一直等待阻塞在这里,所以在客户端发完“hello,server~”后必须设置输出结束标记。

你可能会有一个疑惑:为什么案例1,也没有设置输出结束标记为啥它不会阻塞,因为案例一的客户端在发送完“hello,server~”后下一步就是关闭流或关闭socket对象的操作,无论你关闭输出流还是socket对象他都会结束数据的传输并且释放两端的socket。

此时另外一个问题又来了:那案例2在客户端发送完“hello,server~”后能否直接关闭输出流不使用socket.shutdownOutput方法,NO,不行!!!!因为你一旦关闭了输出流那么它会直接把数据通道关闭掉释放两端的socket对象,那你后面可是还有操作的啊,你的socket对象都释放了,服务端怎么发送“hello,client~” ???跟别提后面客户端还要接收“hello,client~” 了。

案例三(字符)

在案例二的基础上采用字符的方式传输

思想:其实这里面没啥思想,总的架构还是按照案例二那样,只不过我们需要用到转换流,因为你这个socket只能获取到字节流,那我们在传输的时候就需要把获取的字节流转换成字符流,再去传输即可。

//服务端        
        ServerSocket serverSocket = new ServerSocket(6666);
        Socket socket = serverSocket.accept();

        InputStream inputStream = socket.getInputStream();
        InputStreamReader isr = new InputStreamReader(inputStream);
        BufferedReader bfr = new BufferedReader(isr);
        String readerLine = bfr.readLine();
        System.out.println(readerLine);


        OutputStream outputStream = socket.getOutputStream();
        OutputStreamWriter osw = new OutputStreamWriter(outputStream);
        BufferedWriter bfw = new BufferedWriter(osw);
        bfw.write("hello,client~");
        socket.shutdownOutput();
        //可以使用这个bfw.newLine();为输出结束点,但是不推荐,局限性太多
        bfw.flush();

        bfw.close();
        bfr.close();
        socket.close();
        serverSocket.close();
//客户端   
        Socket socket = new Socket(InetAddress.getLocalHost(), 6666);
        OutputStream outputStream = socket.getOutputStream();
        //转换流
        OutputStreamWriter osw = new OutputStreamWriter(outputStream);
        BufferedWriter bfw = new BufferedWriter(osw);
        bfw.write("hello,server~");
        socket.shutdownOutput();
        //可以使用这个bfw.newLine();为输出结束点,但是不推荐,局限性太多
        bfw.flush();

        InputStream inputStream = socket.getInputStream();
        InputStreamReader isr = new InputStreamReader(inputStream);
        BufferedReader bfr = new BufferedReader(isr);
        String readerLine = bfr.readLine();//按行读取,若读取不到返回null
        System.out.println(readerLine);
        
        bfr.close();
        bfw.close();
        socket.close();

注意点:

  1. 使用字符传输必须flush,否则无法写入到数据通道。
  2. 为什么说这个newLine();为输出结束点局限性多,是因为首先你用这个为结束点另外一端则必须用readeLine读取,且只能读取一行。无法通过循环读取。

网络上传文件(TCP)

需求:客户端发送本地文件给服务端,服务端接收图片后保存到src目录下,并且回复客户端“图片已收到”,服务端回复完后退出端口,客户端收到回复后退出端口

客户端


@SuppressWarnings("all")
public class TCP_Client {
    public static void main(String[] args) throws IOException {
        String str_img = "D:\\FileTest\\Kola.jpg";
        FileInputStream fileInputStream = new FileInputStream(str_img);
        byte[] bytes = new byte[1024];
        int readLen = 0;

        Socket socket = new Socket(InetAddress.getLocalHost(), 8888);
        OutputStream outputStream = socket.getOutputStream();
        while((readLen = fileInputStream.read(bytes)) != -1){
            outputStream.write(bytes,0,readLen);
        }
        socket.shutdownOutput();

        InputStream inputStream = socket.getInputStream();
        byte[] bytes1 = new byte[1024];
        inputStream.read(bytes1);
        System.out.println(new String(bytes1));


        outputStream.close();
        fileInputStream.close();
        socket.close();

    }
}

服务端 


public class TCP_Server {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8888);
        Socket socket = serverSocket.accept();

        InputStream inputStream = socket.getInputStream();
        File file = new File("src\\Kola.jpg");
        FileOutputStream fileOutputStream = new FileOutputStream(file);
        byte[] bytes = new byte[1024];
        int readLen = 0;
        while((readLen = inputStream.read(bytes)) != -1){
            fileOutputStream.write(bytes,0,readLen);
        }

        OutputStream outputStream = socket.getOutputStream();
        if(file.exists()){
            outputStream.write("收到图片".getBytes());
        }
        socket.shutdownOutput();

        outputStream.close();
        fileOutputStream.close();
        inputStream.close();
        socket.close();
        serverSocket.close();
    }
}

netstat指令相关的知识

本地地址一般是0.0.0.0开头和127.0.0.1开头,后面是接着的是端口号。

外部地址表示别的计算机等设备或者本地的某些服务,后面的状态是外部地址与本地地址连接的状态,LISTENING:正在监听状态。ESTABLEISHED:表示连接状态

比如:执行下面代码

 然后我再dos层用netstat -an指令去查看,可以看到

本地的8888端口正在被外部地址0.0.0.0.0(本地的服务)监听,一旦我的这个socket被外部socket访问连接,也就是双方的数据通道被打通后,这个外部地址就会发生变化。变成类似于下面这样

 只不过一般来说在开发当中这个外部地址都是别的机器的ip。

netstat -anb  还可以查看到是什么程序在监听

一个知识点(TCP连接的秘密)

学到这里我需要说一个知识点,就是你有没有想过客户端在通过指定的id和端口连接到了服务端,那客户端在连接的时候就是直接连接的吗?不是的,客户端也是通过端口来连接的,客户端的端口是通过TCP/IP协议随机分配的,也就是说他每一次连接可能端口都不一样,那么如果你再传输数据的过程中,是可以在服务端/客户端通过netstat -an来看到这个随机分配的端口号的,但是一旦连接关闭那么你这个随机分配的端口号也就断开了。

 UDP网络编程

udp网络编程首先请你抛弃客户端和服务端的概念。按照发送端和接收端的概念去想。发送端和接收端是可以灵活颠倒的。
必须要记住的两个关键字DatagramSocket/DatagramPacket

概念其实很好理解,如果你写这个UDP网络编程不熟悉,卡壳了先把这两个关键字写上你就懂基本的传输协议了

 DatagramSocket就相当于TCP中的Socket

案例1

/**
 * @author
 * @version 1.0
 * 接收端
 */
public class UDPReceiver01 {
    public static void main(String[] args) throws IOException {
        //创建类似于TCP的Socket对象DatagramSocket(指定接收端本地的端口号)
        DatagramSocket datagramSocket = new DatagramSocket(9999);
//创建一个接收数据的字节数组(因为数据包裹DatagramPacket底层使用的是字节数组来保存数据的)
        byte[] bytes = new byte[1024];
        //因为对方发送数据是以DatagramPacket对象(数据包裹)的形式来发送的,
        //所以接收端也需要定义一个DatagramPacket对象(数据包裹)来接收数据
        //DatagramPacket(接收数据的字节数组,数据包裹大小)
        DatagramPacket datagramPacket = new DatagramPacket(bytes,bytes.length);
        //调用DatagramSocket中的接收数据方法receive,一旦调用了这个方法,
        //就会开始阻塞,当有数据传入则接收数据执行后面的代码,没有数据传入就阻塞
        datagramSocket.receive(datagramPacket);
        System.out.println(new String(bytes));
    }
}
/**
 * @author 
 * @version 1.0
 * 发送端
 */
public class UDPSend01 {
    public static void main(String[] args) throws IOException {
        //创建类似于TCP的Socket对象DatagramSocket(指定发送端本地的端口号)
        DatagramSocket datagramSocket = new DatagramSocket(8888);

        //你要发送的内容,必须得转成字节数组,因为DatagramPacket数据包裹对象
        //只能按照字节数组的形式保存数据
        byte[] bytes = "明天晚上吃火锅".getBytes();

        //创建DatagramPacket用于发送数据,
//DatagramPacket(数据包数据,数据包数据偏移量,数据包数据长度,目标地址,目标端口号)
//目标地址可以是ip号或者域名
DatagramPacket datagramPacket = new DatagramPacket(bytes, 0, bytes.length,
                                    InetAddress.getByName("172.20.85.142"), 9999);    

        //调用datagramSocket的发送指令把数据包发送出去
        datagramSocket.send(datagramPacket);

        datagramSocket.close();
    }
}

网络编程作业 

//服务端
/**
 * @author 
 * @version 1.0
 * 使用字符流的方式,编写一个客户端程序和服务器端程序
 * 客户端发送“name”,服务器端接收到后,返回"我是 nova,nova 是你自己的名字
 * 客户端发送“hobby”,服务器端接收到后,返回“编写java程序”
 * 不是这两个问题,回复“你说啥呢“
 */
public class homework01Server {
    public static void main(String[] args) throws IOException {

        ServerSocket serverSocket = new ServerSocket(9999);//服务端在监听本地的9999端口
        Socket socket = serverSocket.accept();//等待接收数据

        InputStream inputStream = socket.getInputStream();
        InputStreamReader isr = new InputStreamReader(inputStream);
        BufferedReader br = new BufferedReader(isr);
        String s = br.readLine();
        System.out.println(s);

        OutputStream outputStream = socket.getOutputStream();
        OutputStreamWriter osw = new OutputStreamWriter(outputStream);
        switch (s){
            case "name":osw.write("我是nova");break;
            case "hobby":osw.write("编写Java程序");break;
            default:osw.write("你说啥");break;
        }
        osw.flush();
        socket.shutdownOutput();

        socket.close();
        br.close();

    }

}
//客户端        
        Socket socket = new Socket(InetAddress.getByName("172.20.85.142"), 9999);
        OutputStream outputStream = socket.getOutputStream();
        OutputStreamWriter osw = new OutputStreamWriter(outputStream);

        //随机发送问题
        int count = (int)(Math.random()*3);
        switch (count){
                case 0:osw.write("name");break;
                case 1:osw.write("hobby");break;
                case 2:osw.write("阿吧阿吧");break;
            }
        osw.flush();
        socket.shutdownOutput();

        InputStream inputStream = socket.getInputStream();
        InputStreamReader isr = new InputStreamReader(inputStream);
        BufferedReader br = new BufferedReader(isr);
        System.out.println(br.readLine());

        br.close();
        osw.close();
        socket.close();

案例二:

/**
 * @author 接收端
 * @version 1.0
 * 编写一个接收端A,和一个发送端B, 使用UDP协议完成1接收端在 8888端口等待接收数据(receive)
 * 发送端向接收端 发送 数据“四大名著是哪些"
 * 接收端接收到 发送端发送的 问题后,返回“四大名著是<<红楼梦>> ..."否则返回what?
 * 接收端和发送端程序退出
 */
public class homework02Receiver {
    public static void main(String[] args) throws IOException {
        DatagramSocket datagramSocket = new DatagramSocket(8888);

        byte[] bytes = new byte[1024];
        DatagramPacket receivePack = new DatagramPacket(bytes,0,bytes.length);
        datagramSocket.receive(receivePack);//接收指令,这里是阻塞点

        //UDP编程,如果你要对接收到的数据进行字符串转换,
        // 必须对接收到的数据必须进行拆包,否则你在后面使用equals
        // 判断无法得到正确的布尔值
        int length = receivePack.getLength();
        byte[] data = receivePack.getData();
        String s = new String(data,0, length);
      //String s1 = new String(bytes,0, bytes.length)
        //如果你前面没有拆包的动作,上述的字符串写成new String(bytes,0, bytes.length)
        //那么他会把整个字节数组1024个字节全部转成字符串,虽然直观上你最后输出的是具体的字符串
        //但是他后面其实还跟着很多null,你在进行equal比较的时候肯定会出大问题
        System.out.println(s);

        DatagramPacket sendPacket = null;
        String str = "四大名著是哪些";
        if(s.equals(str)){
            byte[] bytes1= "四大名著是<<红楼梦>> ...".getBytes();
            sendPacket = new DatagramPacket(bytes1,0,bytes1.length,
                    InetAddress.getByName("172.20.85.142"),9999);
        }else{
            byte[] bytes2 = "what???".getBytes();
            sendPacket = new DatagramPacket(bytes2,0,bytes2.length,
                    InetAddress.getByName("172.20.85.142"),9999);
        }
        datagramSocket.send(sendPacket);
        datagramSocket.close();
    }
}

/**
 * @author 发送端
 * @version 1.0
 */
public class homework02Send {
    public static void main(String[] args) throws IOException {
        DatagramSocket datagramSocket = new DatagramSocket(9999);
        System.out.println("请输入你的问题:");
        Scanner scanner = new Scanner(System.in);
        String str = scanner.next();
        byte [] bytes = str.getBytes();
        DatagramPacket sendPacket = new DatagramPacket(bytes,0,bytes.length,
                InetAddress.getByName("172.20.85.142"),8888);
        datagramSocket.send(sendPacket);//发送指令

        byte[] bytes1 = new byte[1024];
        DatagramPacket receivePack = new DatagramPacket(bytes1,0,bytes1.length);
        datagramSocket.receive(receivePack);//接收指令,这里是阻塞点
        String s = new String(bytes1);
        System.out.println(s);

        datagramSocket.close();
        
    }
}

这个案例中有一个非常重要的细节:

就是在必须要对接收到的数据进行拆包

        byte[] bytes = new byte[1024];    //缓冲池

        //拆包

        int length = receivePack.getLength();
        byte[] data = receivePack.getData();
        String s = new String(data,0, length);

    

        String s1 = new String(bytes,0, bytes.length) //没有拆包


       

假设对方传过来的数据为“哈哈哈哈”,下面是拆包和没有拆包的区别。没有拆包后面多了N个null,那么一旦你后面要多“哈哈哈哈”进行equals比较的话,你没有拆包你后面多了这么多null你怎么可能比较得到正确的布尔值
     

 

 

案例三:


/**
 * @author 服务端
 * @version 1.0
 * 编写客户端程序和服务器端程序
 * (2)客户端可以输入一个音乐文件名,比如 高山流水, 服务端 收到音乐名后,
 * 可以给客户端 返回这个 音乐文件,如果服务器没有这个文件,
 * 返回 一个默认的音乐即可.
 * (3) 客户端收到文件后,保存到本地 e:\\
 */
public class homework03Server {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8888);
        System.out.println("等待接收客户端指令...");
        Socket socket = serverSocket.accept();
        //获取输入流接收客户端指令
        InputStream inputStream = socket.getInputStream();
        InputStreamReader isr = new InputStreamReader(inputStream);//转换流
        BufferedReader bfr = new BufferedReader(isr);//包装流
//        System.out.println(bfr.readLine());

        //判断从客户端收到的指令,对应去音乐名去找本地音乐文件夹(src\\)
        //如果有则发送音乐文件给客户端。否,则发送默认的音乐文件.
        //发送文件的形式是本地服务器边取文件边发送文件
        //目标音乐文件path
        String musicPath = bfr.readLine()+".mp3";
        File file = new File("src\\" + musicPath);
        //默认音乐文件path
        File file1 = new File("src\\无名.mp3");
        //读取文件流
        FileInputStream fis = null;
        BufferedInputStream bis = null;
        //发送文件流
        OutputStream outputStream = socket.getOutputStream();
        BufferedOutputStream bos = new BufferedOutputStream(outputStream);
        //设立缓冲区
        byte[] bytes = new byte[1024];
        int readLen = 0;
        if(file.exists()){
            fis = new FileInputStream(file);
            bis = new BufferedInputStream(fis);
            while((readLen = bis.read(bytes)) != -1){
                bos.write(bytes,0,readLen);
            }
            System.out.println("发送完毕");
        }else{
            fis = new FileInputStream(file1);
            bis = new BufferedInputStream(fis);
            while((readLen = bis.read(bytes)) != -1){
                bos.write(bytes,0,readLen);
            }
        }

        bos.close();
        bis.close();
        serverSocket.close();
        socket.close();
        bfr.close();
    }

}

/**
 * @author 客户端
 * @version 1.0
 * 编写客户端程序和服务器端程序
 * 客户端可以输入一个音乐文件名,比如 高山流水, 服务端 收到音乐名后,
 * 可以给客户端 返回这个 音乐文件,如果服务器没有这个文件,
 * 返回 一个默认的音乐即可.
 * 客户端收到文件后,保存到本地 e:\\
 */
public class homework03Client {
    public static void main(String[] args) throws IOException {
        /**思想:
         * 1.客户端发送音乐文件名
         * 2.
         */
        Socket socket = new Socket(InetAddress.getByName("172.20.85.142"), 8888);
        OutputStream outputStream = socket.getOutputStream();
        OutputStreamWriter osw = new OutputStreamWriter(outputStream);//转换流
        BufferedWriter bfw = new BufferedWriter(osw);//包装流
        //音乐的名字可自由输入
        System.out.println("请输入要下载的音乐名:");
        Scanner scanner = new Scanner(System.in);
        String next = scanner.next();
        bfw.write(next);
        bfw.flush();//字符流必须刷新
        socket.shutdownOutput();

        //接收服务端的音乐文件
        InputStream inputStream = socket.getInputStream();
        File file = new File("d:\\"+next+".mp3");
        FileOutputStream fos = new FileOutputStream(file);
        byte[] bytes = new byte[1028];
        int readLen = 0;
        while((readLen = inputStream.read(bytes)) != -1){
            fos.write(bytes,0,readLen);
        }


        fos.close();
        inputStream.close();
        socket.close();
        bfw.close();

    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

new麻油叶先生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值