基于 UDP 协议的 C/S 通信案例

因为在 Android 的通信这一块有点吃瘪,所以重新学习 Java 的网络编程。因为即时通讯中 UDP 的大放异彩,这里就来实现一个简单的 UDP 通信的案例。一开始不太熟悉,只能够实现客户端与服务端的单次通信,程序就会结束。也就是说客户端在向服务端发送一次数据,然后接收一次来自服务端的数据就会退出了。这对于我来说并没有什么意义。我想要达成的是服务端可以和客户端实现不间断的通信过程,其中可以是单向的连续发送或者连续接收。

因为暂且还是在命令行的模式下,在同一个线程中我还不能做到在 DatagramSocket 接收数据报的同时,还能够随意发送数据报,因为 DatagramSocket 的 receive() 方法在接收到数据报之前是会阻塞当前进程的。因此我想应该可以在主线程进行数据报的监听与接收操作,而在子线程中实现监听用户的输入并打包发送的过程。这一原理也可以用在客户端,这样双方都可以随时进行数据发送与接收了。

首先来看服务端的实现,因为考虑到以后会进行多客户端的同时监听,我已经把监听数据报的逻辑放到了一个分线程中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package udp;

import java.io.IOException;
import java.net.DatagramSocket;
import java.net.InetAddress;

/**
* Created by alpha on 16-11-18.
* 基于 udp 的服务端
*/
public class Server {
public static void main(String[] args) throws IOException {
//DatagramSocket socket = new DatagramSocket(3467);//直接监听 localhost
//服务器端,指定端口,这里监听局域网 IP 地址
DatagramSocket socket = new DatagramSocket(3467,
InetAddress.getByAddress(new byte[]{(byte) 192, (byte) 168, 99, 100}));
//socket.setBroadcast(true);
System.out.println("服务器已经启动,等待客户端连接。。。");
//在子线程完成数据接收与发送
ServerThread thread = new ServerThread(socket);
while (true) {
thread.run();
}

//以下全部为注释
/*//字节数组,指定接收数据包大小
byte[] data = new byte[1024];
//创建数据报,用于接收客户端发送的信息
DatagramPacket packet = new DatagramPacket(data, data.length);
System.out.println("服务器已经启动,等待服务端发送数据。。。");
//接收客户端信息
socket.receive(packet);//在接收到数据报之前会阻塞进程
//读取接收的数据
String info = new String(data, 0, packet.getLength());
System.out.println("客户端说:" + info);*/

/**
* 响应客户端
*/
/*InetAddress address = packet.getAddress();
int port = packet.getPort();
System.out.println("客户端端口号是:" + port);
byte[] data2 = "欢迎您!".getBytes();
DatagramPacket packet2 = new DatagramPacket(data2, data2.length, address, port);
socket.send(packet2);
socket.close();*/
}
}

可以看出这里我监听的是局域网的 IP 地址,当然也可以改成监听 localhost 。对应的写法也在源码中有所体现了。

子线程源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package udp;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

/**
* Created by alpha on 16-11-19.
* 服务端通信线程
*/
public class ServerThread implements Runnable {

private DatagramSocket socket;
//public static int count = 0;
private DatagramPacket receivePacket;
private DatagramPacket sendPacket;
private String sendMsg;
private String receivedMsg;

public ServerThread(DatagramSocket socket) {
this.socket = socket;
}

@Override
public void run() {
byte[] data = new byte[1024];
receivePacket = new DatagramPacket(data, data.length);
try {
//接收客户端的验证信息
socket.receive(receivePacket);
//System.out.println("当前链接客户端数量:" + ++count);
receivedMsg = new String(data, 0, receivePacket.getLength());
System.out.println("这个客户端说:" + receivedMsg);

InetAddress address = receivePacket.getAddress();
int port = receivePacket.getPort();
System.out.println("来自:" + address + ",端口为:" + port);
/**
* 响应客户端
*/
byte[] data2 = "欢迎您!".getBytes();
sendPacket = new DatagramPacket(data2, data2.length, address, port);
socket.send(sendPacket);

//另开一个子线程用于接收服务端输入信息并发送出去
new Thread(){
@Override
public void run() {
try {
while (!"bye".equals(receivedMsg)) {
//继续向客户端发送信息
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
sendMsg = in.readLine();
sendPacket = new DatagramPacket(sendMsg.getBytes(),
sendMsg.getBytes().length, address, port);
socket.send(sendPacket);
System.out.println("我说:" + sendMsg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
//循环接收客户端发送的信息
while (!"bye".equals(receivedMsg)) {
socket.receive(receivePacket);
receivedMsg = new String(receivePacket.getData(), 0, receivePacket.getLength());
System.out.println("客户端说:" + receivedMsg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

正如一开始所说,我在这个处理数据接收与发送的线程中又开了一个子线程专门用于监听用户在控制台的输入和发送,而在这个原本的子线程中除了一开始的主动回应,就只有数据报的接收操作了。

客户端源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
package udp;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.*;

/**
* Created by alpha on 16-11-18.
* 基于 udp 的客户端
*/
public class Client implements Runnable {
private int port = 3467;
private DatagramPacket receivePacket;
private InetAddress address;
private byte[] inData;
private String receivedMsg;
private DatagramPacket sendPacket;
private DatagramSocket socket;
private String sendMsg = "";

public static void main(String[] args) {
Client client = new Client();
client.run();
}

@Override
public void run() {
try {
//向服务端发送第一次连接信息
//address = InetAddress.getByName("localhost");
address = InetAddress.getByAddress(new byte[]{(byte) 192, (byte) 168, 99, 100});
byte[] data = "用户名:alpha,密码:123456".getBytes();
sendPacket = new DatagramPacket(data, data.length, address, 3467);
socket = new DatagramSocket(3468);
socket.send(sendPacket);

/**
* 接收服务端反馈信息
*/
inData = new byte[1024];
receivePacket = new DatagramPacket(inData, inData.length, address, port);
socket.receive(receivePacket);
receivedMsg = new String(inData, 0, receivePacket.getLength());
System.out.println("服务器说:" + receivedMsg);

//新开线程监听输入并发送
new Thread() {
@Override
public void run() {
while (!"bye".equals(sendMsg)) {
try {
//监听控制台输入
BufferedReader in = new BufferedReader(new
InputStreamReader(System.in));
sendMsg = in.readLine();
//打包输入信息
sendPacket = new DatagramPacket(sendMsg.getBytes(),
sendMsg.getBytes().length, address, port);
socket.send(sendPacket);
System.out.println("我说:" + sendMsg);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}.start();

//循环监听来接收服务端信息
while ( !"bye".equals(sendMsg)) {
//inData = new byte[1024];
//receivePacket = new DatagramPacket(inData, inData.length);
socket.receive(receivePacket);
receivedMsg = new String(receivePacket.getData(), 0,
receivePacket.getLength());
System.out.println("服务器说:" + receivedMsg);
}
} catch (SocketException e) {
e.printStackTrace();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}

看一下实际的效果:

首先开启服务端:

服务器已经启动,等待客户端连接。。。

再开启客户端:

服务器说:欢迎您!

这个时候服务端也接收到了来自客户端的问候信息:

这个客户端说:用户名:alpha,密码:123456
来自:/192.168.99.100,端口为:3468

在客户端发送一条信息:

你好
我说:你好

服务端就收到信息了:

客户端说:你好

在服务端回复一条信息:

你也好啊
我说:你也好啊

客户端也就收到了:

服务器说:你也好啊

如果要结束通信,那么在客户端输入 “bye” :

由于客户端的发送信息的代码是写在接收信息的上方,所以此时虽然客户端下的发送线程已经退出,但是客户端并没有结束,需要继续从服务端接收任何一条信息即可退出:

在服务端随意发送一条信息:

bye
我说:bye

此时再看客户端:

Process finished with exit code 0

客户端就关闭了。

完整服务端控制台内容:

服务器已经启动,等待客户端连接。。。
这个客户端说:用户名:alpha,密码:123456
来自:/192.168.99.100,端口为:3468
客户端说:你好
你也好啊
我说:你也好啊
客户端说:bye
bye
我说:bye

完整客户端:

服务器说:欢迎您!
你好
我说:你好
服务器说:你也好啊
bye
我说:bye
服务器说:bye
Process finished with exit code 0

尽管通过多线程的方式实现了双方不间断的任意通信,但就是我自己也感觉这个示例是非常粗糙的,不仅没有断开重连,也没有超时机制,就连客户端验证也只是一个模拟的信息而已。双方都是位于本机,并没有进行真正对的局域网测试,更不要说实现互联网通信了。不过这是一个良好的开端,再次基础之上,继续增加 GUI 窗口,多客户端同时连接,客户端之间的通信,甚至发送图片,表情,文件等,都是接下来需要完成的。