以前的话呢,学过 网络编程 那里,写了一系列的 笔记。但是 那时候 没租 服务器,根本 就不算是 真正的 学习 网络编程。
这次的话,我把写的 服务器 程序 搭在 我租的阿里云服务器上。然后 我们 进行了 一系列的 尝试。其中 我认为 我写的 最好的,就是 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 {
socket = new DatagramSocket(10000);
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 端口,就实现了 云服务器 和 本机 的 数据包通讯。



