在看到本文之前,如果读者没看过笔者的上一个系列 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源码,供读者学习和思考,感谢关注!
作者:陆嘉杰
链接:https://www.jianshu.com/p/700e95a45206