Java实现Udp网络编程

作者: Louis_陆 | 来源:发表于2016-04-28 10:07 被阅读906次

    在看到本文之前,如果读者没看过笔者的上一个系列 Java实现Socket网络编程,建议先翻阅。

    笔者将在上期Demo的基础上,进一步修改和扩展,达到本次Demo的运行效果。

    首先展示Demo的演示效果:
    初始状态:1个服务器,2个客户端

    Paste_Image.png

    检测通信正常:

    Paste_Image.png

    断开服务器,再次检测通信正常:

    Paste_Image.png

    服务器重新启动,自动刷新:

    Paste_Image.png

    添加客户端:

    Paste_Image.png

    关于 C(客户端)和 S(服务器)之间的TCP通信,以及 C 检测 S 状态,自动重连等机制,笔者在上期Demo的实现过程中已详细阐述,此处就不再赘述。

    我们来看看本次案例的实现需求:
    1、服务器支持多客户端访问
    2、C和S之间使用TCP连接
    3、C和C之间使用UDP直接通信

    由于案例需求的步骤1、2已实现,我们对步骤3作如下设计思路:
    1、客户端创建监听线程,建立UDP监听端口,并发消息告诉服务器,指定自己的服务端口。
    2、服务器得知客户端的服务端口后,广播通知其他客户端,现已登录的客户端服务端口列表。
    3、客户端之间直接通过UDP,向指定服务端口发送消息。

    值得注意的是,C与C之间要求直接通信,所以必须满足“在服务器关闭的情况下,C与C之间仍能通信”的情况,而不是借助服务器完成间接通信

    首先,我们创建客户端监听线程,并发消息告诉服务器

    public void run() {
    
            try {
    
                DatagramSocket server = new DatagramSocket(0);// 随机分配一个端口号
    
                // 向服务器发送接收客户端的DatagramSocket的端口号
    
                String message = Common.SPECIAL;
    
                String t = "" + server.getLocalPort();
    
                ClientMain.frame.setTitle("client " + t);
                String c = "" + t.length();
                if (c.length() < 2) {
                    c = "000" + c;
                } else if (c.length() < 3) {
                    c = "00" + c;
                } else if (c.length() < 4) {
                    c = "0" + c;
                }
                message += c + t;
    
                OutputStreamWriter outstream = null;
    
                // 将信息发送给服务器
                try {
                    outstream = new OutputStreamWriter(mSocket.getOutputStream(),
                            "GBK");
                    outstream.write(message);
                    outstream.flush();
    
                } catch (IOException e1) {
                    ClientMain.jlConnect.setText("Out Of Connect.");
                    ClientMain.isConnected = false;
                    if (outstream != null)
                        try {
                            outstream.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    e1.printStackTrace();
                }
    
                while (true) {
    
                    byte[] recvBuf = new byte[1024];// 定义接收消息的缓冲区
                    DatagramPacket recvPacket = new DatagramPacket(recvBuf,
                            recvBuf.length);// 数据包
                    server.receive(recvPacket);
    
                    // 接收到的消息
                    String recvStr = new String(recvPacket.getData(), 0,
                            recvPacket.getLength());
    
                    ClientMain.jtaReceivedMessage.append(recvStr + "\n");
                    // 滚动到底端
                    ClientMain.jtaReceivedMessage
                            .setCaretPosition(ClientMain.jtaReceivedMessage
                                    .getText().length());
                }
    
            } catch (Exception e) {
    
                e.printStackTrace();
            }
    }
    

    服务器得知客户端的服务端口后,广播通知其他客户端

    else if (s.startsWith(Common.SPECIAL) && s.length() > 10
                                && count == Integer.parseInt((s.substring(6, 10)))) {
                            // 存储客户端监听端口
                            /**
                             * 一定要注意使用前初始化,否则在IDE在这里检测不到空指针错误
                             */
                            HashMap<Socket, String> map = new HashMap<Socket, String>();
                            map.put(mSocket, s.substring(10));
                            ServerMain.clientMonitorPortList.add(map);
                            // 发送更新列表信息给客户端
                            sendUpdateToClient();
                            count = -10;
                            s = "";
                        }
    

    sendUpdateToClient方法如下:

    // 发送更新列表信息给所有客户端
        private void sendUpdateToClient() {
            String message = Common.SEND_TO_CLIENT;
            String t = "";
    
            for (int i = 0; i < ServerMain.clientMonitorPortList.size(); i++) {
                HashMap<Socket, String> map = ServerMain.clientMonitorPortList
                        .get(i);
                Iterator iter1 = map.entrySet().iterator();
                Map.Entry entry = (Map.Entry) iter1.next();
                Socket key = (Socket) entry.getKey();
                int localPort = key.getPort();
                String port = (String) entry.getValue();
                if (i != ServerMain.clientMonitorPortList.size() - 1)
                    t += localPort + " " + port + " ";
                else
                    t += localPort + " " + port;
            }
    
            String c = "" + t.length();
            if (c.length() < 2) {
                c = "000" + c;
            } else if (c.length() < 3) {
                c = "00" + c;
            } else if (c.length() < 4) {
                c = "0" + c;
            }
            message += c + t;
    
            OutputStreamWriter outstream = null;
    
            // 将信息发送给每个客户端
            for (int i = 0; i < ListenThread.clientSockets.size(); i++) {
                try {
                    HashMap<Socket, Boolean> map = ListenThread.clientSockets
                            .get(i);
                    // 用迭代器获取HashMap的Key,即所选中的Socket
                    Iterator iter = map.entrySet().iterator();
                    Map.Entry<Socket, Boolean> entry = (Entry<Socket, Boolean>) iter
                            .next();
    
                    Socket key = (Socket) entry.getKey();
                    outstream = new OutputStreamWriter(key.getOutputStream(), "GBK");
                    outstream.write(message);
                    outstream.flush();
    
                } catch (IOException e1) {
                    if (outstream != null)
                        try {
                            outstream.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    e1.printStackTrace();
                }
            }
        }
    

    最后,客户端通过UDP向指定服务端口发送消息
    当选中JList的项时,向选中的项发送消息,如果没有选中项,则向服务器发送消息

    // 设置监听
            jbSendMessage.addActionListener(new ActionListener() {
    
                @Override
                public void actionPerformed(ActionEvent e) {
    
                    if (jtaSendMessage.getText().equals("")) {
                        JOptionPane.showMessageDialog(null, "发送内容不能为空!");
                        return;
                    }
    
                    // 取得要发送的消息
                    String message = Common.SIMPLE;
    
                    String t = "client " + Common.IP + ":" + mSocket.getLocalPort()
                            + " " + jtaSendMessage.getText();
                    String c = "" + t.length();
                    if (c.length() < 2) {
                        c = "000" + c;
                    } else if (c.length() < 3) {
                        c = "00" + c;
                    } else if (c.length() < 4) {
                        c = "0" + c;
                    }
                    message += c + t;
    
                    OutputStreamWriter outstream = null;
    
                    // 如果没有选中,则向服务器发送消息
                    if (selecteds == null || selecteds.length == 0) {
                        try {
                            outstream = new OutputStreamWriter(mSocket
                                    .getOutputStream(), "GBK");
                            outstream.write(message);
                            outstream.flush();
                        } catch (IOException e1) {
                            if (outstream != null)
                                try {
                                    outstream.close();
                                } catch (IOException e2) {
                                    e2.printStackTrace();
                                }
                            e1.printStackTrace();
                        }
                    } else {
                        String sendPort = "";
    
                        // 检测现在进行发送行为的是哪个客户端
                        for (int i = 0; i < clientPortList.size(); i++) {
                            HashMap<String, String> map = (HashMap<String, String>) clientPortList
                                    .get(i);
                            Iterator iter1 = map.entrySet().iterator();
                            Map.Entry entry = (Map.Entry) iter1.next();
                            String sendSocketPort = (String) entry.getKey();
                            // mSocket.getLocalPort()是int类型,要注意加""
                            if (sendSocketPort.equals(mSocket.getLocalPort() + "")) {
                                sendPort = (String) entry.getValue();
                            }
                        }
    
                        // 向选中的客户端发送消息
                        for (int i = 0; i < selecteds.length; i++) {
                            // 获取选中的端口
                            HashMap<String, String> map = (HashMap<String, String>) clientPortList
                                    .get(selecteds[i]);
                            Iterator iter1 = map.entrySet().iterator();
                            Map.Entry entry = (Map.Entry) iter1.next();
                            String port = (String) entry.getValue();
    
                            try {
                                // 生成一个临时发送端口
                                DatagramSocket client = new DatagramSocket(0);
                                // 要发送的数据
                                String sendMessage = "client " + Common.IP + ":"
                                        + sendPort + " " + jtaSendMessage.getText();
                                byte[] buf = sendMessage.getBytes();
                                // 定义发送信息的目的地
                                InetAddress destination = InetAddress
                                        .getByName(Common.IP);
                                // 生成数据包
                                DatagramPacket dp = new DatagramPacket(buf,
                                        buf.length, destination, Integer
                                                .valueOf(port));
                                client.send(dp);
                            } catch (Exception e1) {
                                e1.printStackTrace();
                            }
                        }
                    }
    
                    // 清空文本
                    jtaSendMessage.setText(null);
    
                }
    
            });
    

    本次实验步骤看似简单,但也有几个不得不注意的地方:
    1、在读写数据的循环里,是检测不到空指针错误的,只会检测到读写错误后不断尝试重连。读者在开发过程中一定要注意把相应的控件初始化,而发现不断重连,重复读写时,应首先考虑是否在读写循环里引用了未初始化的控件。
    2、mSocket.getLocalPort()方法返回的是int类型,使用equals比较时要注意加双引号"",以转换成String类型,否则IDE不会编译报错,但结果并未如意。
    3、使用UDP端口容易混乱:读者在开发过程中应尽量避免更新UI时整体删除再添加剩余项,而改用“只删除关闭项,只增加新增项”,前种方法在开发过程中容易造成端口混乱。同时,笔者建议读者在涉及JList操作时,多用ArrayList替代HashMap存储,因为ArrayList是插入有序的,能减少混乱的发生。
    4、注意在视图model中删除了项,也要同时在列表List中删除对应项,以做到真正的删除,而不是假删除。
    5、删除List中的所有项:
    for(int i=0;i<list.size();)list.remove(i);
    注意!这里不能添加i++,因为每次remove后,list.size()会自动减小,如果添加了i++,则不能完全删除List中的元素,从而导致二次混乱

    最后,笔者在github上给出了两次实验的Demo源码,供读者学习和思考,感谢关注!

    相关文章

      网友评论

      本文标题:Java实现Udp网络编程

      本文链接:https://www.haomeiwen.com/subject/jhxhrttx.html