以前的话呢,学过 网络编程 那里,写了一系列的 笔记。但是 那时候 没租 服务器,根本 就不算是 真正的 学习 网络编程。
这次的话,我把写的 服务器 程序 搭在 我租的阿里云服务器上。然后 我们 进行了 一系列的 尝试。其中 我认为 我写的 最好的,就是 UDP 利用 云服务器 弄一个 简易 聊天室。当然 这个 聊天室 只 限于 一对一。也就是 我们 在 本机 上 打开 客户端,然后 服务器上 打开服务端,可以 进行 一对一的 信息 传输。
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
端口,就实现了 云服务器 和 本机 的 数据包通讯。