java基础-网络编程

赖卓成2023年7月4日
大约 13 分钟

12-1-网络编程和UDP

01-网络编程三要素-概述

网络编程

  • 在网络通讯协议下,不同计算机上运行的程序,可以进行数据传输。

思考:

A电脑的飞秋,发消息给B电脑的飞秋,需要知道哪些条件才能发送?

网络编程三要素:

  • ip地址

    设备在网络中的地址,是唯一标识。

  • 端口

    应用程序在设备中的唯一标识

  • 协议

    数据在网络传输中的规则,常见UDP、TCP。

02-网络编程三要素-IP

“互联网协议地址”,是分配给上网设备的数字标签,常见IPv6、IPv4。

平时访问网站,访问的其实是网站的ip,可以ping:

image-20230704224437887

过程:

  • 计算机将域名给dns服务器
  • 服务器返回域名对应的ip地址给计算机
  • 计算机访问ip对应的服务器
  • 服务器返回数据给计算机

image-20230704224733936

IPv4:每8位为一个字节,共4个字节。

image-20230704224811863

为了方便表示和记忆,将字节转成10进制,用点号分割:

image-20230704224936671

每个字节的范围是0~255,每个字节最多表示256个数字。导致IPv4最多能够表示256^4个IP。不够用了。

推出了IPv6:128位地址长度,分成8组,每组2个字节(16位)。

image-20230704225213133

可以省略前面的0:

image-20230704225237451

如果有多个连续的0:

image-20230704225258502

采用0位压缩表示法:读时,自动补全为8组。

image-20230704225329700

03-网络编程-常见命令

ipconfig、ping

04-网络编程-InetAddress类

为了方便我们对IP地址的获取和操作,Java提供了一个类InetAddress类供我们使用。此类表示Internet协议地址。

public class InetAddressDemo {
    public static void main(String[] args) {
        try {
            InetAddress address = InetAddress.getByName("DESKTOP-JCMLI7D");
            
            System.out.println("address.getHostName() = " + address.getHostName());
            System.out.println("address.getAddress() = " + address.getHostAddress());

        } catch (UnknownHostException e) {
            throw new RuntimeException(e);
        }
    }
}

image-20230704230521886

05-网络编程三要素-端口

应用程序在设备中唯一的标识。用两个字节表示的整数,取值范围是0~65535

0~1013之间的端口号用于一些知名的网络应用程序,自己写的程序用1024以上的端口号就可以了,注意不要超过范围、

一个端口号只能被一个应用程序使用。

06-网络编程三要素-协议

协议:计算机网络中,连接和通信的规则被称为网络通信协议。

UDP:

  • 用户数据报协议(User Datagram Protocol)
  • 面向无连接通信协议
    • 速度快
    • 大小限制64k
    • 数据不安全、易丢失

TCP:

  • 传输控制协议(Transmission Control Protocol)
  • 面向连接的通信协议
    • 速度慢
    • 没有大小限制
    • 数据安全

07-UDP-发送端

步骤:

  • 创建发送端DatagramSocket对象
  • 创建数据,并把数据打包(DatagramPacket)
  • 调用DatagramSocket对象的方法发送数据
  • 释放资源
public class UdpSendDemo {

    public static void main(String[] args) {
        DatagramSocket datagramSocket = null;
        try {
            // 创建DatagramSocket对象
            datagramSocket = new DatagramSocket();
            
            // 要发送的消息
            String msg = "你好,我是赖卓成";
            byte[] buf = msg.getBytes();
            
            // 发送到哪台主机、哪个端口号
            InetAddress address = InetAddress.getByName("127.0.0.1");
            int port = 10000;
            // 创建报文
            DatagramPacket datagramPacket = new DatagramPacket(buf,buf.length,address,port);
            // 发送
            datagramSocket.send(datagramPacket);
        } catch (SocketException e) {
            throw new RuntimeException(e);
        } catch (UnknownHostException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            // 释放资源
            if (Objects.nonNull(datagramSocket)){
                datagramSocket.close();
            }
        }
    }
}

运行后,没有任何结果,因为udp是面向无连接的。

08-UDP-接收端

步骤:

  • 创建接收端DatagramSocket对象
  • 创建DatagramPacket对象,用于接收数据
  • 调用DatagramSocket的方法接受数据,并将数据放入DatagramPacket
  • 解析数据,打印
  • 释放资源
public class UdpAcceptDemo {
    public static void main(String[] args) {
        DatagramSocket datagramSocket = null;
        try {
            datagramSocket = new DatagramSocket(10000);
            // 创建字节数组用于接收数据
            byte[] buf = new byte[1024];
            DatagramPacket datagramPacket = new DatagramPacket(buf, buf.length);
            // 接收数据
            datagramSocket.receive(datagramPacket);
            byte[] data = datagramPacket.getData();
            String msg = new String(data,0,datagramPacket.getLength());
            System.out.println("msg = " + msg);
        } catch (SocketException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            if (Objects.nonNull(datagramSocket)){
                datagramSocket.close();
            }
        }
    }
}

先运行接收端,再运行发送端:

image-20230704233837241

测试之后,可以发到腾讯云服务器上,能接收到。

09-UDP-练习

发送数据,键盘录入,如果内容是886则结束,接收端不知道什么时候结束,所以一直死循环。

太简单,不写了。

10-UDP-三种通讯方式

  • 单播:一个发送端对应一个接收端

    image-20230705000415374

  • 组播:一对多

    image-20230705000506395

  • 广播:一对所有

    image-20230705000540274

11-UDP-组播代码实现

组播的发送端跟单播是类似的,区别在接收端。组播的接收端是多台计算机。

组播地址:224.0.0.0~239.255.255.255,其中224.0.0.0~224.0.0.255为预留的组播地址。

发送端:

/**
 * udp 组播发送端
 *
 * @author 赖卓成
 * @date 2023/07/05
 */
public class UdpMultiCastSendDemo {

    public static void main(String[] args) {
        DatagramSocket datagramSocket = null;
        try {
            // 组播地址
            InetAddress address = InetAddress.getByName("224.0.1.0");
            // 和单播一样、创建DatagramSocket对象
            datagramSocket = new DatagramSocket();
            int port = 10000;

            byte[] bytes = "你好,我是赖卓成".getBytes();

            // 数据包
            DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length, address, port);

            datagramSocket.send(datagramPacket);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            datagramSocket.close();
        }

    }

}

接收端:与单播不同的是需要创建MulticastSocket对象来接收,并且需要加入组播地址。

/**
 * udp组播接受
 *
 * @author 赖卓成
 * @date 2023/07/05
 */
public class UdpMultiCastAcceptDemo {

    public static void main(String[] args)  {

    while (true){
        int port = 10000;
        MulticastSocket multicastSocket = null;
        try {
            multicastSocket = new MulticastSocket(port);
            // 加入组播地址
            InetAddress address = InetAddress.getByName("224.0.1.0");
            multicastSocket.joinGroup(address);

            DatagramPacket datagramPacket = new DatagramPacket(new byte[1024], 1024);

            // 接收数据
            multicastSocket.receive(datagramPacket);
            byte[] data = datagramPacket.getData();
            String msg = new String(data,0, datagramPacket.getLength());
            System.out.println("msg = " + msg);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            multicastSocket.close();
        }
    }
    }
}

本地运行可以正常发送、接收数据包,但是不能发到服务器上(原因还没搞明白)。

12-UDP-广播代码实现

广播地址:255.255.255.255

需要注意的是,在发送时,需要指定广播地址,而在接受时,不需要指定广播地址。

/**
 * udp广播代码实现--发送端
 *
 * @author lzc
 * @date 2023/07/05
 */
public class UdpCastSendDemo {
    public static void main(String[] args) {
        DatagramSocket datagramSocket = null;
        try {
            // 广播地址 表示消息是广播的
            InetAddress address = InetAddress.getByName("255.255.255.255");
            int port = 10000;
            datagramSocket = new DatagramSocket();

            byte[] bytes = "你好,这是广播消息".getBytes();
            DatagramPacket datagramPacket = new DatagramPacket(bytes, 0, bytes.length, address, port);
            datagramSocket.send(datagramPacket);
        } catch (UnknownHostException e) {
            throw new RuntimeException(e);
        } catch (SocketException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            datagramSocket.close();
        }

    }
}
/**
 * udp广播--接收端
 *
 * @author lzc
 * @date 2023/07/05
 */
public class UdpCastAcceptDemo {
    public static void main(String[] args) {
        DatagramSocket datagramSocket = null;
        try {
            // 接受广播消息不需要指定ip地址
            datagramSocket = new DatagramSocket(10000);
            // 定义数组用于接收数据
            byte[] bytes = new byte[1024];

            DatagramPacket datagramPacket = new DatagramPacket(bytes, 0, 1024);

            datagramSocket.receive(datagramPacket);
            String s = new String(bytes, 0, datagramPacket.getLength());
            System.out.println(s);

        } catch (SocketException e) {
            throw new RuntimeException(e);
        } catch (UnknownHostException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            datagramSocket.close();
        }

    }
}

12-1-TCP通讯程序

13-TCP-发送端

TCP通信协议是一种可靠的网络协议,在通信的两端,各建立一个Socket对象。

在传输数据之前,需要保证连接已经建立。

通过Socket产生IO流来进行网络通信。

image-20230706000130655

步骤:

  • 创建客户端Socket对象与指定服务端连接,指定host、port
  • 获取输出流(OutputStream)
  • 释放资源

代码:

public class TcpSend {
    public static void main(String[] args)  {
        while (true) {
            try {
                // 创建连接
                Socket socket = new Socket("127.0.0.1", 9118);
                // 获得输出流
                OutputStream outputStream = socket.getOutputStream();

                // 写数据
                Scanner scanner = new Scanner(System.in);
                String msg = scanner.nextLine();
                outputStream.write(msg.getBytes());

                // 关闭流
                outputStream.close();

                // 关闭Socket
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
                throw new RuntimeException(e);
            }
        }
    }
}

这样写是没问题,但运行会报一次,因为TCP通信前,需要建立好连接,此时我们还没有编写服务器端代码。

14-TCP-服务器端

步骤:

  • 创建服务器端的Socket对象(ServerSocket)
  • 监听客户端连接,返回一个Socket对象,accept方法,返回一个Socket对象,如果没有客户端来连接,就会一直等待。
  • 获得输入流对象,InputStream,读取数据。
  • 释放资源
public class TcpAccept {
    public static void main(String[] args) {
        while (true) {
            try {
                // 创建服务端Socket对象
                ServerSocket serverSocket = new ServerSocket(9118);
                // 等待连接,连接成功以后,会返回一个Socket对象
                Socket socket = serverSocket.accept();

                // 获取输入流
                InputStream inputStream = socket.getInputStream();
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
                String s;
                while (( s= bufferedReader.readLine())!=null&&s.length()>0) {
                    System.out.println(s);
                }
                inputStream.close();
                serverSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
                throw new RuntimeException(e);
            }
        }
    }
}

运行结果:

image-20230706003329391

15-TCP-原理分析

上面的例子中,需要先执行服务端,再执行发送端。在执行服务端的时候,accept方法,会阻塞,等待发送端来连接,当客户端创建Socket的时候,就会进行连接。发送数据(字节输出流写数据)会通过建立好的通道,将数据发送到服务端,进行接收(字节输入流读数据)。

  • accept方法是阻塞的,作用是等待客户端的连接,如果连接上就返回Socket对象,否则一直等待。

  • 客户端创建对象,连接服务器,会通过三次握手协议,保证连接成功。

  • 针对客户端,是往外写,所以是字节输出流。

    针对服务端,是往里读,所以是字节输入流。

  • read方法也是阻塞的,如果客户端没有关闭字节输出流,服务端就会一直等待。

  • 客户端在关闭OutPutStream时,会往服务器发一个结束标记。

  • 最后一步关闭连接,会通过四次挥手协议,进行断开连接。

16-TCP-三次握手

image-20230706101056177

17-TCP-四次挥手

image-20230706101512273

四次挥手,多了一个数据处理的过程。

18-TCP-练习1

需求:

客户端:发送数据,接收服务器反馈

服务端:接收数据,给出反馈

image-20230706101826777

注意两个方法:

shutdownInputshutdownOutput

客户端:

public class TcpClientDemo01 {

    public static void main(String[] args) throws IOException {

        Socket socket = new Socket("127.0.0.1", 2975);
        OutputStream outputStream = socket.getOutputStream();
        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
        bufferedWriter.write("你好,我是赖卓成");
        // 将缓冲区数据写入字节流
        // 强制将缓冲区的数据写入到输出流中。
        // 在写入数据时,数据并不是立即发送到对方,而是先存储在缓冲区,等到缓冲区满了或者手动调用了 flush() 方法时才会将缓冲区的数据发送出去
        bufferedWriter.flush();
        // 告诉服务端,客户端已经发送完数据,不再向服务端发送数据了。这样服务端就可以通过读取输入流来获取完整的客户端请求
        // 否则服务端可能会一直等待客户端发送数据,导致阻塞。
        socket.shutdownOutput();

        InputStream inputStream = socket.getInputStream();
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        bufferedReader.lines().forEach(System.out::println);
        bufferedReader.close();
        socket.close();
    }
}

服务端:

public class TcpServerDemo01 {

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

        InputStream inputStream = socket.getInputStream();
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        bufferedReader.lines().forEach(System.out::println);
        // 告诉客户端,服务端已经接收完数据,不再接收客户端发送的数据了。
        // 这样客户端就可以通过读取输入流来获取完整的服务端响应
        socket.shutdownInput();

        // 响应客户端
        OutputStream outputStream = socket.getOutputStream();
        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
        bufferedWriter.write("你好,我是服务端");
        bufferedWriter.close();
        socket.close();
        serverSocket.close();
    }
}

image-20230706153636426

19-TCP-练习2

需求:客户端发送文件,并接收服务器的反馈。服务端接收客户端发送的文件,给出反馈。

image-20230706220826686

/**
 * TCP练习2-客户端
 *
 * @author lzc
 * @date 2023/07/06
 */
public class TcpClientDemo02 {
    public static void main(String[] args) throws IOException {
        // 创建连接
        Socket socket = new Socket("127.0.0.1", 2975);

        // 读本地文件
        File file = new File("D:\\dev\\workfile\\TcpClientDemo02.txt");
        if (!file.exists()&&!file.isFile()){
            throw new RuntimeException("文件不存在");
        }
        // 创建文件输入流
        FileInputStream fileInputStream = new FileInputStream(file);

        // 获取socket的输出流
        OutputStream outputStream = socket.getOutputStream();
        BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream);
        BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
        byte[] bytes = new byte[8192];
        int len;
        // 使用缓冲字节流读数据,使用缓冲字节流写数据
        while ((len = bufferedInputStream.read(bytes))!=-1){
            bufferedOutputStream.write(bytes,0,len);
        }
        // 写完后关闭流
        bufferedInputStream.close();

        // 告诉服务端 写完了
        System.out.println("客户端已发送文件!");
        bufferedOutputStream.flush();
       socket.shutdownOutput();

       // 获取socket的输出流  输出服务端响应
        InputStream inputStream = socket.getInputStream();
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        Stream<String> lines = bufferedReader.lines();
        lines.forEach(System.out::println);

        // 关闭输入流
        bufferedReader.close();
        socket.close();

    }

}

public class TcpServerDemo02 {
    public static void main(String[] args) throws IOException {
        // 创建ServerSocket对象
        ServerSocket serverSocket = new ServerSocket(2975);

        // 等待客户端连接,连接后返回Socket对象
        Socket socket = serverSocket.accept();

        // 获取socket的输入流对象
        InputStream inputStream = socket.getInputStream();


        // 创建文件字节输出流,用于保存文件
        FileOutputStream fileOutputStream = new FileOutputStream("D:\\dev\\workfile\\TcpServerDemo01-copy.txt");

        // 创建缓冲字节输出流
        BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
        BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);

        // 创建数组,用于读数据,临时存储
        byte[] bytes = new byte[8192];

        // 每次读取的长度
        int len;
        while((len  = bufferedInputStream.read(bytes))!=-1){
            bufferedOutputStream.write(bytes,0,len);
        }
        // 读完关闭文件字节输出流,并调用socket方法告诉客户端,读完了
        socket.shutdownInput();
        bufferedOutputStream.close();


        // 获取socket的输出流
        OutputStream outputStream = socket.getOutputStream();
        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
        bufferedWriter.write("服务端已收到文件!");
        bufferedWriter.close();

        // 关闭socket
        socket.close();
        serverSocket.close();

    }
}

image-20230706223920251

image-20230706223936212

20-TCP-服务端的优化-循环

上面写的代码, 服务端每次接受到文件,都会关闭socket,只能使用一次,所以加个循环:

image-20230707000528885

21-TCP-服务端优化-UUID

每次上次文件名都一样,加个UUID

22-TCP-服务端优化-多线程

仅仅使用循环,无法同时处理多个客户端的请求。

创建类,实现Runnable接口,每次accept,如果没有接受到连接,会一直阻塞,接收到就返回Socket对象,我们就可以在每次接受到不同的请求,返回不同的Socket对象后,交给不同的线程去处理,就能实现同时处理多个客户端的请求。

public class MySocketRunnable implements Runnable {

    private final Socket socket;

    public MySocketRunnable(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        BufferedOutputStream bufferedOutputStream = null;
        try {

            // 获取socket的输入流对象
            InputStream inputStream = socket.getInputStream();

            // 创建文件字节输出流,用于保存文件
            File dir = new File("D:\\dev\\workfile");
            if (!dir.exists()) {
                dir.mkdirs();
            }
            File file = new File(dir, UUID.randomUUID() +".txt");
            FileOutputStream fileOutputStream = new FileOutputStream(file);

            // 创建缓冲字节输出流
            bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
            BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);

            // 创建数组,用于读数据,临时存储
            byte[] bytes = new byte[8192];

            // 每次读取的长度
            int len;
            while ((len = bufferedInputStream.read(bytes)) != -1) {
                bufferedOutputStream.write(bytes, 0, len);
            }
            // 读完关闭文件字节输出流,并调用socket方法告诉客户端,读完了
            socket.shutdownInput();


            // 获取socket的输出流
            OutputStream outputStream = socket.getOutputStream();
            BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
            bufferedWriter.write("服务端已收到文件!");
            bufferedWriter.flush();


        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (Objects.nonNull(socket)) {
                // 关闭socket
                try {
                    socket.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }

            if (Objects.nonNull(bufferedOutputStream)){
                try {
                    bufferedOutputStream.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }

        }
    }
}

public class TcpServerDemo03 {
    public static void main(String[] args) throws IOException {
        // 创建ServerSocket对象
        ServerSocket serverSocket = new ServerSocket(2975);

        while (true) {
            // 等待客户端连接,连接后返回Socket对象
            Socket socket = serverSocket.accept();
            MySocketRunnable mySocketRunnable = new MySocketRunnable(socket);

            new Thread(mySocketRunnable).start();
        }

    }
}

23-TCP-服务端优化-线程池

每次接受到请求,都重新开启线程,很消耗资源,我们可以使用线程池,减少创建和销毁线程的性能消耗。

public class TcpServerDemo03 {
    public static void main(String[] args) throws IOException {
        // 创建ServerSocket对象
        ServerSocket serverSocket = new ServerSocket(2975);

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                // 核心线程
                3,
                // 最大线程
                10,
                // 非核心线程空闲多少秒销毁
                60,
                TimeUnit.SECONDS
                // 阻塞队列
                ,
                new ArrayBlockingQueue<>(5),
                // 创建线程的方式 -默认
                Executors.defaultThreadFactory()
                ,
                // 拒绝策略--
                new ThreadPoolExecutor.AbortPolicy()
        );

        while (true) {
            // 等待客户端连接,连接后返回Socket对象
            Socket socket = serverSocket.accept();
            MySocketRunnable mySocketRunnable = new MySocketRunnable(socket);
            // 一旦有请求连接,就提交给线程池处理
            threadPoolExecutor.submit(mySocketRunnable);
        }
    }
}
Loading...