Java - day21 - 网络编程


Java - day21 - 网络编程

什么是网络编程?

  • 网络编程可以让程序与网络上的其他设备中的程序进行数据交互。

网络通信基本模式

  • 常见的通信模式有如下2种形式:Client-Server(Cs)、Browser/Server(BS)。

网络通信三要素

三要素概述、要素一:IP地址

实现网络编程关键的三要素(IP地址,端口,协议)

  • IP地址:设备在网络中的地址,是唯一的标识。
  • 端口:应用程序在设备中唯一的标识。
  • 协议:数据在网络中传输的规则,常见的协议有UDP协议和TCP协议。

IP地址

  • IP ( Internet Protocol):全称”互联网协议地址”,是分配给上网设备的唯一标志。
  • 常见的IP分类为:IPv4和IPv6
  • IPv6:128位(16个字节),号称可以为地球每一粒沙子编号。
  • IPv6分成8个整数,每个整数用四个十六进制位表示,数之间用冒号(:)分开。

IР地址形式

  • 公网地址、和私有地址(局域网使用)。
  • 192.168.开头的就是常见的局域网地址,范围即为192.168.0.0--192.168.255.255,专门为组织机构内部使用。

IP常用命令

  • ipconfig:查看本机IP地址
  • ping IP地址:检查网络是否连通

特殊IP地址

  • 本机IP:127.0.0.1或者localhost:称为回送地址也可称本地回环地址,只会寻找当前所在本机。

IP地址操作类 - InetAddress

lnetAddress的使用

  • 此类表示Internet协议(IP)地址。

实例

import java.net.InetAddress;
/**
 目标:InetAddress类概述(了解)
 一个该类的对象就代表一个IP地址对象。

 InetAddress类成员方法:
 static InetAddress getLocalHost()
 * 获得本地主机IP地址对象。
 static InetAddress getByName(String host)
 * 根据IP地址字符串或主机名获得对应的IP地址对象。
 String getHostName()
 * 获得主机名。
 String getHostAddress()
 * 获得IP地址字符串。
 */
public class InetAddressDemo01 {
    public static void main(String[] args) throws Exception {
        // 1.获取本机地址对象。
        InetAddress ip1 = InetAddress.getLocalHost();
        System.out.println(ip1.getHostName());
        System.out.println(ip1.getHostAddress());

        // 2.获取域名ip对象
        InetAddress ip2 = InetAddress.getByName("www.baidu.com");
        System.out.println(ip2.getHostName());
        System.out.println(ip2.getHostAddress());

        // 3.获取公网IP对象。
        InetAddress ip3 = InetAddress.getByName("112.80.248.76");
        System.out.println(ip3.getHostName());
        System.out.println(ip3.getHostAddress());

        // 4.判断是否能通: ping  5s之前测试是否可通
        System.out.println(ip3.isReachable(5000));
    }
}

要素二:端口号

端口号

  • 端口号:标识正在计算机设备上运行的进程(程序),被规定为一个16位的二进制,范围是0~65535。

端口类型

  • 周知端口:0~1023,被预先定义的知名应用占用(如:HTTP占用80,FTP占用21)。
  • 注册端口:1024~49151,分配给用户进程或某些应用程序。(如:Tomcat占用8080,MySQL占用3306)
  • 动态端口:49152到65535,之所以称为动态端口,是因为它一般不固定分配某种进程,而是动态分配。
  • 注意:我们自己开发的程序选择注册端口,且一个设备中不能出现两个程序的端口号一样,否则出错。

要素三:协议

通信协议

  • 连接和通信数据的规则被称为网络通信协议。

网络通信协议有两套参考模型

  • OSI参考模型:世界互联协议标准,全球通信规范,由于此模型过于理想化,未能在因特网上进行广泛推广。
  • TCP/IP参考模型(或TCP/IP协议):事实上的国际标准。

传输层的2个常见协议

  • TCP(Transmission Control Protocol):传输控制协议。
  • UDP(User Datagram Protocol):用户数据报协议。

TCP协议特点

  • 使用TCP协议,必须双方先建立连接,它是一种面向连接的可靠通信协议
  • 传输前,采用“三次握手”方式建立连接,所以是可靠的。
  • 在连接中可进行大数据量的传输。
  • 连接、发送数据都需要确认,且传输完毕后,还需释放已建立的连接,通信效率较低。

TCP协议通信场景

  • 对信息安全要求较高的场景,例如:文件下载、金融等数据通信。

TCP三次握手确立连接

TCP四次挥手断开连接

UDP协议

  • UDP是一种无连接、不可靠传输的协议。
  • 将数据源lP、目的地IP和端口封装成数据包,不需要建立连接
  • 每个数据包的大小限制在64KB内.
  • 发送不管对方是否准备好,接收方收到也不确认,故是不可靠的.
  • 可以广播发送,发送数据结束时无需释放资源,开销小,速度快。

UDP协议通信场景

  • 语音通话,视频会话等。

UDP通信

UDP通信:快速入门

DatagramPacket:数据包对象

Datagramsocket:发送端和接收端对象(人)

实例

package d21;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;

/**
 发送端  一发 一收
 */
public class ClientDemo1 {
    public static void main(String[] args) throws Exception {
        System.out.println("=====客户端启动======");

        // 1、创建发送端对象:发送端自带默认的端口号(人)
        DatagramSocket socket = new DatagramSocket(6666);

        // 2、创建一个数据包对象封装数据(韭菜盘子)
        /**
         public DatagramPacket(byte buf[], int length,
         InetAddress address, int port)
         参数一:封装要发送的数据(韭菜)
         参数二:发送数据的大小
         参数三:服务端的主机IP地址
         参数四:服务端的端口
         */
        byte[] buffer = "我是一颗快乐的韭菜,你愿意吃吗?".getBytes();
        DatagramPacket packet = new DatagramPacket( buffer, buffer.length,
                InetAddress.getLocalHost() , 8888);

        // 3、发送数据出去
        socket.send(packet);

        socket.close();
    }
}
package d21;

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

/**
 接收端
 */
public class ServerDemo2 {
    public static void main(String[] args) throws Exception {
        System.out.println("=====服务端启动======");
        // 1、创建接收端对象:注册端口(人)
        DatagramSocket socket = new DatagramSocket(8888);

        // 2、创建一个数据包对象接收数据(韭菜盘子)
        byte[] buffer = new byte[1024 * 64];
        DatagramPacket packet = new DatagramPacket(buffer, buffer.length);

        // 3、等待接收数据。
        socket.receive(packet);

        // 4、取出数据即可
        // 读取多少倒出多少
        int len = packet.getLength();
        String rs = new String(buffer,0, len);
        System.out.println("收到了:" + rs);

        // 获取发送端的ip和端口
        String ip  =packet.getSocketAddress().toString();
        System.out.println("对方地址:" + ip);

        int port  = packet.getPort();
        System.out.println("对方端口:" + port);

        socket.close();
    }
}

UDP通信:多发多收

UDP的接收端为什么可以接收很多发送端的消息?

  • 接收端只负责接收数据包,无所谓是哪个发送端的数据包。

实例

package d3;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.util.Scanner;

/**
 发送端  多发 多收
 */
public class ClientDemo1 {
    public static void main(String[] args) throws Exception {
        System.out.println("=====客户端启动======");

        // 1、创建发送端对象:发送端自带默认的端口号(人)
        DatagramSocket socket = new DatagramSocket(7777);


        Scanner sc = new Scanner(System.in);
        while (true) {
            System.out.println("请说:");
            String msg = sc.nextLine();

            if("exit".equals(msg)){
                System.out.println("离线成功!");
                socket.close();
                break;
            }

            // 2、创建一个数据包对象封装数据(韭菜盘子)
            byte[] buffer = msg.getBytes();
            DatagramPacket packet = new DatagramPacket( buffer, buffer.length,
                    InetAddress.getLocalHost() , 8888);

            // 3、发送数据出去
            socket.send(packet);
        }

    }
}
package d3;

import java.net.DatagramPacket;
import java.net.DatagramSocket;

/**
 接收端
 */
public class ServerDemo2 {
    public static void main(String[] args) throws Exception {
        System.out.println("=====服务端启动======");
        // 1、创建接收端对象:注册端口(人)
        DatagramSocket socket = new DatagramSocket(8888);

        // 2、创建一个数据包对象接收数据(韭菜盘子)
        byte[] buffer = new byte[1024 * 64];
        DatagramPacket packet = new DatagramPacket(buffer, buffer.length);

        while (true) {
            // 3、等待接收数据。
            socket.receive(packet);
            // 4、取出数据即可
            // 读取多少倒出多少
            int len = packet.getLength();
            String rs = new String(buffer,0, len);
            System.out.println("收到了来自:" + packet.getAddress() +", 对方端口是" + packet.getPort() +"的消息:" + rs);
        }
    }
}

UDP通信 - 广播、组播

UDP的三种通信方式

  • 单播:单台主机与单台主机之间的通信。
  • 广播:当前主机与所在网络中的所有主机通信。
  • 组播:当前主机与选定的一组主机的通信。

UDP如何实现广播

  • 使用广播地址:255.255.255.255
  • 具体操作:
    发送端发送的数据包的目的地写的是广播地址、且指定端口。(255.255.255.255, 9999)

②本机所在网段的其他主机的程序只要匹配端口成功即就可以收到消息了。(9999)

UDP如何实现组播

  • 使用组播地址:224.0.0.0 ~ 239.255.255.255
  • 具体操作:
    ①发送端的数据包的目的地是组播IP(例如:224.0.1.1,端口:9999)

②接收端必须绑定该组播IP(224.0.1.1),端口还要对应发送端的目的端口9999,这样即可接收该组播消息。
③DatagramSocket的子类MulticastSocket可以在接收端绑定组播lP。

TCP通信 - 快速入门

  • 注意:在java中只要是使用java.net.Socket类实现通信,底层即是使用了TCP协议

TCP通信的客户端的代表类

  • Socket类
  • public Socket(String host , int port)

TCP通信如何使用Socket管道发送、接收数据

  • Outputstream getOutputStream():获得字节输出流对象(发)
  • InputStream getlnputStream():获得字节输入流对象(收)

TCP通信服务端用的代表类

  • ServerSocket类,注册端口。
  • 调用accept()方法阻塞等待接收客户端连接。得到Socket对象。## TCP通信的基本原理
  • 客户端怎么发,服务端就应该怎么收。
  • 客户端如果没有消息,服务端会进入阻塞等待。
  • Socket一方关闭或者出现异常、对方Socket也会失效或者出错。

编写客户端代码,编写服务端代码

实例

package d5;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;

/**
 目标:完成Socket网络编程入门案例的客户端开发,实现1发1收。
 */
public class ClientDemo1 {
    public static void main(String[] args) {
        try {
            System.out.println("====客户端启动===");
            // 1、创建Socket通信管道请求有服务端的连接
            // public Socket(String host, int port)
            // 参数一:服务端的IP地址
            // 参数二:服务端的端口
            Socket socket = new Socket("127.0.0.1", 7777);

            // 2、从socket通信管道中得到一个字节输出流 负责发送数据
            OutputStream os = socket.getOutputStream();

            // 3、把低级的字节流包装成打印流
            PrintStream ps = new PrintStream(os);

            // 4、发送消息
            ps.println("我是TCP的客户端,我已经与你对接,并发出邀请:约吗?");
            ps.flush();

            // 关闭资源。
            // socket.close();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
package d5;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

/**
 目标:开发Socket网络编程入门代码的服务端,实现接收消息
 */
public class ServerDemo2 {
    public static void main(String[] args) {
        try {
            System.out.println("===服务端启动成功===");
            // 1、注册端口
            ServerSocket serverSocket = new ServerSocket(7777);
            // 2、必须调用accept方法:等待接收客户端的Socket连接请求,建立Socket通信管道
            Socket socket = serverSocket.accept();
            // 3、从socket通信管道中得到一个字节输入流
            InputStream is = socket.getInputStream();
            // 4、把字节输入流包装成缓冲字符输入流进行消息的接收
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            // 5、按照行读取消息
            String msg;
            if ((msg = br.readLine()) != null){
                System.out.println(socket.getRemoteSocketAddress() + "说了:: " + msg);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

TCP通信 - 多发多收消息

本次多发多收是如何实现的

  • 客户端使用循环反复地发送消息。
  • 服务端使用循环反复地接收消息。

现在服务端为什么不可以同时接收多个客户端的消息。

  • 目前服务端是单线程的,每次只能处理一个客户端的消息。

实例

package d6;

import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;

/**
 目标:实现多发和多收
 */
public class ClientDemo1 {
    public static void main(String[] args) {
        try {
            System.out.println("====客户端启动===");
            // 1、创建Socket通信管道请求有服务端的连接
            // public Socket(String host, int port)
            // 参数一:服务端的IP地址
            // 参数二:服务端的端口
            Socket socket = new Socket("127.0.0.1", 7777);

            // 2、从socket通信管道中得到一个字节输出流 负责发送数据
            OutputStream os = socket.getOutputStream();

            // 3、把低级的字节流包装成打印流
            PrintStream ps = new PrintStream(os);

            Scanner sc =  new Scanner(System.in);
            while (true) {
                System.out.println("请说:");
                String msg = sc.nextLine();
                // 4、发送消息
                ps.println(msg);
                ps.flush();
            }

            // 关闭资源。
            // socket.close();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
package d6;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

/**
 目标:开发Socket网络编程入门代码的服务端,实现接收消息
 */
public class ServerDemo2 {
    public static void main(String[] args) {
        try {
            System.out.println("===服务端启动成功===");
            // 1、注册端口
            ServerSocket serverSocket = new ServerSocket(7777);
            while (true) {
                // 2、必须调用accept方法:等待接收客户端的Socket连接请求,建立Socket通信管道
                Socket socket = serverSocket.accept();
                // 3、从socket通信管道中得到一个字节输入流
                InputStream is = socket.getInputStream();
                // 4、把字节输入流包装成缓冲字符输入流进行消息的接收
                BufferedReader br = new BufferedReader(new InputStreamReader(is));
                // 5、按照行读取消息
                String msg;
                while ((msg = br.readLine()) != null){
                    System.out.println(socket.getRemoteSocketAddress() + "说了:: " + msg);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

TCP通信 - 同时接受多个客户端消息【重点】

之前我们的通信是否可以同时与多个客户端通信,为什么?

  • 不可以的,单线程每次只能处理一个客户端的Socket通信。

如何才可以让服务端可以处理多个客户端的通信需求?

  • 引入多线程。

本次是如何实现服务端接收多个客户端的消息的。

  • 主线程定义了循环负责接收客户端Socket管道连接。
  • 每接收到一个Socket通信管道后分配一个独立的线程负责处理它。

实例

package d7;

import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;

/**
 目标:实现服务端可以同时处理多个客户端的消息。
 */
public class ClientDemo1 {
    public static void main(String[] args) {
        try {
            System.out.println("====客户端启动===");
            // 1、创建Socket通信管道请求有服务端的连接
            // public Socket(String host, int port)
            // 参数一:服务端的IP地址
            // 参数二:服务端的端口
            Socket socket = new Socket("127.0.0.1", 7777);

            // 2、从socket通信管道中得到一个字节输出流 负责发送数据
            OutputStream os = socket.getOutputStream();

            // 3、把低级的字节流包装成打印流
            PrintStream ps = new PrintStream(os);

            Scanner sc =  new Scanner(System.in);
            while (true) {
                System.out.println("请说:");
                String msg = sc.nextLine();
                // 4、发送消息
                ps.println(msg);
                ps.flush();
            }

            // 关闭资源。
            // socket.close();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
package d7;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

/**
 目标:实现服务端可以同时处理多个客户端的消息。
 */
public class ServerDemo2 {
    public static void main(String[] args) {
        try {
            System.out.println("===服务端启动成功===");
            // 1、注册端口
            ServerSocket serverSocket = new ServerSocket(7777);
            // a.定义一个死循环由主线程负责不断的接收客户端的Socket管道连接。
            while (true) {
                // 2、每接收到一个客户端的Socket管道,交给一个独立的子线程负责读取消息
                Socket socket = serverSocket.accept();
                System.out.println(socket.getRemoteSocketAddress()+ "它来了,上线了!");
                // 3、开始创建独立线程处理socket
                new ServerReaderThread(socket).start();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
package d7;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;

public class ServerReaderThread extends Thread{
    private Socket socket;
    public ServerReaderThread(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        try {
            // 3、从socket通信管道中得到一个字节输入流
            InputStream is = socket.getInputStream();
            // 4、把字节输入流包装成缓冲字符输入流进行消息的接收
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            // 5、按照行读取消息
            String msg;
            while ((msg = br.readLine()) != null){
                System.out.println(socket.getRemoteSocketAddress() + "说了:: " + msg);
            }
        } catch (Exception e) {
            System.out.println(socket.getRemoteSocketAddress() + "下线了!!!");
        }
    }
}

TCP通信 - 使用线程池优化

目前的通信架构存在什么问题?

  • 客户端与服务端的线程模型是N-N的关系。
  • 客户端并发越多,系统瘫痪的越快。

本次使用线程池的优势在哪里?

  • 服务端可以复用线程处理多个客户端,可以避免系统瘫痪。
  • 适合客户端通信时长较短的场景。

实例

package d8;

import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;

/**
    拓展:使用线程池优化:实现通信。
 */
public class ClientDemo1 {
    public static void main(String[] args) {
        try {
            System.out.println("====客户端启动===");
            // 1、创建Socket通信管道请求有服务端的连接
            // public Socket(String host, int port)
            // 参数一:服务端的IP地址
            // 参数二:服务端的端口
            Socket socket = new Socket("127.0.0.1", 6666);

            // 2、从socket通信管道中得到一个字节输出流 负责发送数据
            OutputStream os = socket.getOutputStream();

            // 3、把低级的字节流包装成打印流
            PrintStream ps = new PrintStream(os);

            Scanner sc =  new Scanner(System.in);
            while (true) {
                System.out.println("请说:");
                String msg = sc.nextLine();
                // 4、发送消息
                ps.println(msg);
                ps.flush();
            }
            // 关闭资源。
            // socket.close();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
package d8;


import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.*;

/**
   目标:实现服务端可以同时处理多个客户端的消息。
 */
public class ServerDemo2 {

    // 使用静态变量记住一个线程池对象
    private static ExecutorService pool = new ThreadPoolExecutor(300,
            1500, 6, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(2)
    , Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

    public static void main(String[] args) {
        try {
            System.out.println("===服务端启动成功===");
            // 1、注册端口
            ServerSocket serverSocket = new ServerSocket(6666);
            // a.定义一个死循环由主线程负责不断的接收客户端的Socket管道连接。
            while (true) {
                // 2、每接收到一个客户端的Socket管道,
                Socket socket = serverSocket.accept();
                System.out.println(socket.getRemoteSocketAddress()+ "它来了,上线了!");

                // 任务对象负责读取消息。
                Runnable target = new ServerReaderRunnable(socket);
                pool.execute(target);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
package d8;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;

public class ServerReaderRunnable implements Runnable{
    private Socket socket;
    public ServerReaderRunnable(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        try {
            // 3、从socket通信管道中得到一个字节输入流
            InputStream is = socket.getInputStream();
            // 4、把字节输入流包装成缓冲字符输入流进行消息的接收
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            // 5、按照行读取消息
            String msg;
            while ((msg = br.readLine()) != null){
                System.out.println(socket.getRemoteSocketAddress() + "说了:: " + msg);
            }
        } catch (Exception e) {
            System.out.println(socket.getRemoteSocketAddress() + "下线了!!!");
        }
    }
}

TCP通信实战案例 - 即时通信

即时通信是什么含义,要实现怎么样的设计?

  • 即时通信,是指一个客户端的消息发出去,其他客户端可以接收到。
  • 即时通信需要进行端口转发的设计思想。
  • 服务端需要把在线的Socket管道存储起来
  • 一旦收到一个消息要推送给其他管道

实例

  • <--d9_chat-->

TCP通信实战案例 - 模拟BS系统【了解】

TCP通信如何实现BS请求网页信息回来呢?

  • 客户端使用浏览器发起请求(不需要开发客户端)
  • 服务端必须按照浏览器的协议规则响应数据。
  • 浏览器使用HTTP协议

实例

package d10;

import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.*;

public class BSserverDemo {
    // 使用静态变量记住一个线程池对象
    private static ExecutorService pool = new ThreadPoolExecutor(3,
            5, 6, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(2)
            , Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

    public static void main(String[] args) {
        try {
            // 1.注册端口
            ServerSocket ss = new ServerSocket(8080);
            // 2.创建一个循环接收多个客户端的请求。
            while(true){
                Socket socket = ss.accept();
                // 3.交给一个独立的线程来处理!
                pool.execute(new ServerReaderRunnable(socket));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
package d10;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;

public class ServerReaderRunnable implements Runnable{
    private Socket socket;
    public ServerReaderRunnable(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        try {
            // 浏览器 已经与本线程建立了Socket管道
            // 响应消息给浏览器显示
            PrintStream ps = new PrintStream(socket.getOutputStream());
            // 必须响应HTTP协议格式数据,否则浏览器不认识消息
            ps.println("HTTP/1.1 200 OK"); // 协议类型和版本 响应成功的消息!
            ps.println("Content-Type:text/html;charset=UTF-8"); // 响应的数据类型:文本/网页

            ps.println(); // 必须发送一个空行

            // 才可以响应数据回去给浏览器
            ps.println("<span style='color:red;font-size:90px'>《最牛的149期》 </span>");
            ps.close();
        } catch (Exception e) {
            System.out.println(socket.getRemoteSocketAddress() + "下线了!!!");
        }
    }
}

声明:三二一的一的二|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - Java - day21 - 网络编程


三二一的一的二