Java 复习 02 UDP 利用云服务器写简易聊天室

2.1 UDP 利用云服务器写简易聊天室


以前的话呢,学过 网络编程 那里,写了一系列的 笔记。但是 那时候 没租 服务器,根本 就不算是 真正的 学习 网络编程。

这次的话,我把写的 服务器 程序 搭在 我租的阿里云服务器上。然后 我们 进行了 一系列的 尝试。其中 我认为 我写的 最好的,就是 UDP 利用 云服务器 弄一个 简易 聊天室。当然 这个 聊天室 只 限于 一对一。也就是 我们 在 本机 上 打开 客户端,然后 服务器上 打开服务端,可以 进行 一对一的 信息 传输。

  1. 服务器端
import java.io.BufferedReader;
import java.io.DataOutput;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.*;

public class 服务端 {
    public static int daPort = 0; // NAT 动态分配 的 端口
    public static InetAddress  publicIP = null; // NAT 转换而成 的 公网 IP
    public static DatagramSocket socket = null;
    public static void main(String[] args) throws IOException {
        /* 由 端口 10000 创建 一个 套接字UDP 服务 */
        socket = new DatagramSocket(10000);

        /* 创建 一个 SendMsg 线程 并设为 该线程为 守护线程!开启 该 线程 进行 发送消息的 监视 */
        SendMsg sendMsg = new SendMsg();
        sendMsg.setDaemon(true);
        sendMsg.start();

        /* 以下 是 接收 消息的监视,由 主线程 管理 */
        while(true){
            byte[] container = new byte[1024];

            DatagramPacket packet = new DatagramPacket(container,0,container.length);

            // 阻塞 接收 UDP 发送来的 数据包
            socket.receive(packet);

            // 把数据包的 数据 存储到 相应类型 的 容器中
            byte[] data = packet.getData();

            // 进行 String 的 转换,这样方便 输出
            String dataString = new String(data,0,getSize(data));

            // 如果 接收到的 是 心跳包,我们 就 不去 输出它 而是 更新 我们的 动态分配端口 和 公网IP
            if(dataString.equals("HeartBeat")){
                //System.out.println("接收到 HeartBeat");
                daPort = packet.getPort();
                publicIP = packet.getAddress();
            }else if(dataString.equals("bye")){
                // 当 接收到 bye 字符串的时候,我们就应该 退出了!要去 关闭 套接字服务了。这算是一个 字符串口令。
                break;
            }else{
                // 发送 正常接收到的 数据
                System.out.println(dataString);
            }

        }
        // 其实 有没有 bye 口令 都无所谓,我们只要 在 客户端 做一个 断开连接 就行的。
        socket.close();
    }


    // 读取 byte[] 数组 有效的 数据 位数,以便于 更好 的 转换 为 String 类型
    public static int getSize(byte[] data){
        int i = 0;
        for(byte x: data){
            if(x != (byte)0){
                i++;
            }
        }
        return i;
    }

    // SendMsg 线程 为了省事,而且本来 线程 开的 也少,就 采用了 继承 Thread 的方式
    public static class SendMsg extends Thread{

        @Override
        public void run() {
            super.run();

            while(true){
                BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));

                String data = null;
                try {
                    data = reader.readLine();
                } catch (IOException e) {
                    e.printStackTrace();
                }

                byte[] datas = data.getBytes();

                DatagramPacket packet  = new DatagramPacket(datas,0,datas.length,publicIP, daPort);

                try {
                    socket.send(packet);
                    System.out.println("发送数据包完毕!");
                    //socket.send(packet);
                } catch (IOException e) {
                    e.printStackTrace();
                }

                if(data.equals("bye")){
                    System.out.println("已关闭socket服务!");
                    break;
                }
            }
            socket.close();
        }
    }
}

这里 有几个 细节 部分 需要注意。我们 无时无刻 都在 接收 数据包,但是 却 判断了 一个 字符串 HeartBeat,这是什么呢? 这个 其实 就是 我们 发送 的 心跳包。即 每隔一段时间 就要发送 一个 数据包过来,俗称 心跳包。

① 那么为什么客户端 要发送 心跳包呢 ?
答:因为 UDP 的 连接 保持 活性的 时间 是 很短的。所以 我们 要 每隔一段时间 发送 一个数据包,来保持 连接的活性。

② 为什么 要 把 发送过来的数据包 IP 和 Port 获取下来呢 ?
答:因为 我们的本机 属于内网,它 要 通过 NAT 进行 端口的动态分配,还有 转换为 外网 IP,这样的话 我们 暂时 认为在基于 这个外网IP 和 动态分配端口的情况下,双方的连接通讯 是 安全的。可以进行 有效的 数据发送和接收。

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ContainerEvent;
import java.awt.event.ContainerListener;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketException;

public class 客户端 {
    // 提高 作用域
    public static JTextArea textArea = null;
    public static DatagramSocket socket = null;
    public static void main(String[] args) throws IOException {
        // 绘制 简单 JFrame GUI
        JFrame frame = new JFrame("UDP 客户端");
        frame.setBounds(500,500,500,500);
        Container container = frame.getContentPane();
        container.setLayout(new GridLayout(3,1));
        JTextArea textAreaA = new JTextArea("请输入文字内容",10,40);
        JPanel jPanel1 = new JPanel();
        JPanel jPanel2 = new JPanel();
        JPanel jPanel3 = new JPanel();
        jPanel3.setLayout(new GridLayout(1,2));
        JTextArea textAreaB = new JTextArea("服务器端发来的数据:\n",10,40);
        textAreaA.setLineWrap(true);
        textAreaB.setLineWrap(true);
        JScrollPane jScrollPaneA = new JScrollPane(textAreaA);
        JScrollPane jScrollPaneB = new JScrollPane(textAreaB);
        textAreaB.setEditable(false);
        jPanel1.add(jScrollPaneA);
        jPanel2.add(jScrollPaneB);
        container.add(jPanel1);
        container.add(jPanel2);

        JButton bthSend = new JButton("点击发送消息");
        JButton bthClose = new JButton("安全关闭连接");

        jPanel3.add(bthSend);jPanel3.add(bthClose);
        container.add(jPanel3);

        // 指定一个 端口 开启 套接字 UDP 服务
        socket = new DatagramSocket(10000);

        // 获取到 textAreaB
        textArea = textAreaB;
        // 创建一个 receiveMsg 线程,用来 监视 接收 到的数据
        ReceiveMsg receiveMsg = new ReceiveMsg();
        // 设为 守护线程
        receiveMsg.setDaemon(true);
        receiveMsg.start();

        // 创建一个 heartBeat 线程,每隔 三十秒 发送一个 心跳包
        HeartBeat heartBeat = new HeartBeat();
        // 设为 守护线程
        heartBeat.setDaemon(true);
        heartBeat.start();

        // 初次连接,我们 一定要 发送一个 数据包 进行 连通验证,当我们 在 服务端 接收到这条 信息的时候 才能证明 我们连通了
        byte[] datas = "连接成功".getBytes();
        DatagramPacket packet = new DatagramPacket(datas, 0, datas.length, new InetSocketAddress("云服务器IP", 10000));
        socket.send(packet);

        // 发送按钮的 触发 监听
        bthSend.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                String msg = textAreaA.getText();
                byte[] datas = msg.getBytes();

                DatagramPacket packet = new DatagramPacket(datas,0,datas.length,
                        new InetSocketAddress("云服务器IP", 10000));
                try {
                    socket.send(packet);
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
                System.out.println(packet.getPort());

                textAreaA.setText("");
            }
        });

        // 关闭 服务 和 线程 按钮的 触发 监听
        bthClose.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if(!socket.isClosed()){
                    socket.close();
                }
                receiveMsg.stop();

                frame.setTitle("连接已全部断开!线程已关闭!");
            }
        });


        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true);
    }

    public static class ReceiveMsg extends Thread{
        @Override
        public void run() {
            super.run();
            while(true){
                byte[] containner = new byte[1024];
                DatagramPacket packet = new DatagramPacket(containner,0,containner.length);

                try {
                    socket.receive(packet);
                    //System.out.println(packet.getPort());
                } catch (IOException e) {
                    e.printStackTrace();
                }

                byte[] data = packet.getData();

                String dataString = new String(data,0,getSize(data));
                //System.out.println(dataString);
                textArea.append(dataString+"\n");
            }
        }
        // 读取 byte[] 数组 有效的 数据 位数,以便于 更好 的 转换 为 String 类型
        public static int getSize(byte[] data) {
            int i = 0;
            for(byte x:data)
            {
                if(x != (byte)0)
                {
                    i++;
                }
            }
            return i;
        }
    }

    public static class HeartBeat extends Thread{
        @Override
        public void run() {
            super.run();
            String heartBeat = "HeartBeat";
            while(true){
                DatagramPacket packet = new DatagramPacket(heartBeat.getBytes(), 0,heartBeat.length(),new InetSocketAddress("云服务器IP", 10000));
                try {
                    socket.send(packet);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                try {
                    // 建议 每 三十秒 发送 一次 心跳包,因为 UDP 协议 生命周期 很短。必须 隔一段时间 发送心跳包 保持活性
                    Thread.sleep(30000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

然后 我们 整体的 思路 就是,把 云服务器的 这个 10000 端口 开放,在 客户端 和 服务器那里 各开 一个 线程,客户端的子线程 用来 接收 服务器端 发送的 数据包,服务器端的 子线程 用来 发送 给客户端 数据包。

但是 我们客户端 还得再 建一个 线程发送 心跳包。保持 连接的活性。

这样,我们 只在 云服务器上 开放了 一个 10000 端口,就实现了 云服务器 和 本机 的 数据包通讯。

Java 复习 02 UDP 利用云服务器写简易聊天室
Java 复习 02 UDP 利用云服务器写简易聊天室
Java 复习 02 UDP 利用云服务器写简易聊天室

上一篇:Mac 终端重启后~/.bash_profile不生效问题


下一篇:【Python】终端输入python3,提示“command not found“。歪??明明已经下载了