我们现在考虑如下情况:
局域网中,服务端出于安全考虑,
不主动暴露其特定服务的IP地址。
但是服务端给出了一个获取其服务IP,端口的方法:服务端制定一个UDP通信协议,能够让客户端发送符合协议的广播,服务端接收到之后,返回自己服务的IP地址和端口给客户端。
注意,服务器的协议端口默认是对协议开放的,也就是说知道此协议的客户端必定会知道服务端暴露的协议端口
实现示意图
定制简单UDP协议
这里我们简单的使用如下的协议字段:
定义8 bytes的头
,2 bytes的Cmd(命令,通信的功能类型,short大小)
,4 bytes的端口号(Int大小)
,Data部分为具体的数据
。
现在定义头字段为:Byte{ 7, 7, 7, 7, 7, 7, 7, 7 }
定义服务器UDP协议
的监听端口:8888
。
定义客户端的UDP协议
回送端口:8889
。
于是,对于这些常数,我们专门放到一个类中:UDPConstants类
public class UDPConstants {
// 公用头部
public static byte[] HEADER = new byte[]{7, 7, 7, 7, 7, 7, 7, 7};
// 服务器固化UDP接收端口
public static int PORT_SERVER = 8888;
// 客户端回送端口
public static int PORT_CLIENT = 8889;
}
UDP:搜索服务器客户端
根据服务器提供的协议,客户端(Client)定义一个UDPSearcher类,用来搜索服务器。
下面是UDPSearcher类的具体实现:
/**
* 发送广播搜寻Server服务器
*/
public class UDPSearcher {
private static final int LISTEN_PORT = UDPConstants.PORT_CLIENT;
public static ServerInfo searchServer(int timeout){
System.out.println("UDP Searcher started...");
// 是否收到了广播返回的信息,收到就CountDown
CountDownLatch receiveLatch = new CountDownLatch(1);
Listener listener = null;
try {
// 1. 先监听端口
listener = listen(receiveLatch);
// 2. 再发送广播
sendBroadCast();
// 等待timeout时间。
// 等待结束时间:收到消息时;或者,timeout时间已过
receiveLatch.await(timeout, TimeUnit.MILLISECONDS);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("UDP Searcher finished...");
if(listener == null) {
return null;
}
List<ServerInfo> devices = listener.getServerAndClose();
if(devices.size() > 0){
return devices.get(0);
}
return null;
}
private static Listener listen(CountDownLatch receiveLatch) throws InterruptedException {
System.out.println("UDP Searcher start to listen...");
// Listen线程是否已经开始了
CountDownLatch startDownLatch = new CountDownLatch(1);
Listener listener = new Listener(LISTEN_PORT, startDownLatch, receiveLatch);
listener.start();
// 必须要等到Listener线程开始运行,才继续后面的代码
startDownLatch.await();
return listener;
}
private static class Listener extends Thread{
private final int listenPort;
private final CountDownLatch startDownLatch;
private final CountDownLatch receiveDownLatch;
private final List<ServerInfo> serverInfoList = new ArrayList<>();
private final byte[] buffer = new byte[128];
private final int minLen = UDPConstants.HEADER.length + 2 + 4;
private boolean done = false;
private DatagramSocket ds = null;
public Listener(int listenPort, CountDownLatch startDownLatch, CountDownLatch receiveDownLatch) {
super();
this.listenPort = listenPort;
this.startDownLatch = startDownLatch;
this.receiveDownLatch = receiveDownLatch;
}
@Override
public void run() {
super.run();
// Listener线程已经开始了
startDownLatch.countDown();
try {
// 监听会送端口
ds = new DatagramSocket(listenPort);
// 接收数据包
DatagramPacket datagramPacket = new DatagramPacket(buffer, buffer.length);
// 循环接收数据包
while(!done) {
ds.receive(datagramPacket);
String ip = datagramPacket.getAddress().getHostAddress();
int port = datagramPacket.getPort();
int dataLen = datagramPacket.getLength();
byte[] data = datagramPacket.getData();
boolean isValid = dataLen >= minLen &&
ByteUtils.startsWith(data, UDPConstants.HEADER);
System.out.println("UDPSearcher receive form ip:" + ip
+ "\tport:" + port + "\tdataValid:" + isValid);
// 如果收到的数据, 不遵循协议
if(!isValid){
continue;
}
// 获得相关字段,数据
ByteBuffer byteBuffer = ByteBuffer.wrap(buffer, UDPConstants.HEADER.length, dataLen);
final short cmd = byteBuffer.getShort();
final int serverPort = byteBuffer.getInt();
if(cmd != 2 || serverPort <= 0){
System.out.println("UDPSearcher receive cmd:" + cmd + "\tserverPort:" + serverPort);
continue;
}
String sn = new String(buffer, minLen, dataLen-minLen);
ServerInfo info = new ServerInfo(serverPort, ip, sn);
serverInfoList.add(info);
// 已经收到了服务端的数据
receiveDownLatch.countDown();
}
} catch (Exception e) {
//e.printStackTrace(); // ignored
} finally {
close();
}
System.out.println("UDP Searcher finish to listen...");
}
public List<ServerInfo> getServerAndClose(){
done = true;
close();
return serverInfoList;
}
private void close(){
if(ds != null){
ds.close();
ds = null;
}
}
}
// 发送广播,数据为UDP协议字段
private static void sendBroadCast() throws IOException {
System.out.println("UDP Seacher start to broadcast...");
DatagramSocket ds = new DatagramSocket();
// 获取一个分配特定大小内存的ByteBuffer
ByteBuffer byteBuffer = ByteBuffer.allocate(128);
// UDP协议,头
byteBuffer.put(UDPConstants.HEADER);
// UDP协议,命令(功能)
byteBuffer.putShort((short)1);
// UDP协议,端口
byteBuffer.putInt(LISTEN_PORT);
// 构建UDP Packet数据包
DatagramPacket dp = new DatagramPacket(byteBuffer.array(), 0, byteBuffer.position()+1);
dp.setAddress(InetAddress.getByName("255.255.255.255"));
dp.setPort(UDPConstants.PORT_SERVER);
// 发送数据包
ds.send(dp);
// 关闭socket资源
ds.close();
System.out.println("UDP Seacher finish to broadcast...");
}
}
其中有一个工具类ByteUtils
在例子中的实现:
public class ByteUtils {
public static boolean startsWith(byte[] source, byte[] match) {
return startsWith(source, 0, match);
}
public static boolean startsWith(byte[] source, int offset, byte[] match) {
if (match.length > (source.length - offset)) {
return false;
}
for (int i = 0; i < match.length; i++) {
if (source[offset + i] != match[i]) {
return false;
}
}
return true;
}
}
另外,我们返回的ServerInfo为一个java bean类:
/**
*ServerInfo,搜索后得到的服务器信息。
**/
public class ServerInfo{
private String sn;
private int port;
private String addrss;
public ServerInfo(int port, String address, String sn) {
this.sn = sn;
this.port = port;
this.addrss = addrss;
}
public String getSn() {
return sn;
}
public void setSn(String sn) {
this.sn = sn;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public String getAddrss() {
return addrss;
}
public void setAddrss(String addrss) {
this.addrss = addrss;
}
@Override
public String toString() {
return "ServerInfo{" +
"sn='" + sn + '\'' +
", port=" + port +
", addrss='" + addrss + '\'' +
'}';
}
在客户端,我们只需要调用UDPSearcher 类
的searchServer(int timeout)
方法,就能开始搜索局域网中的服务端。这里默认只返回一个服务端信息。
UDPSearcher 类的整体代码框架如文章开头图的上半部分。先监听端口,等监听端口的线程开始之后,开始发送协议广播,并等待timeout时间来获取服务端的数据。在timeout时间内,如果有服务端返回信息,那么就返回此信息;如果timeout时间后,仍然没有服务端返回信息,那么我们返回的服务端信息为null。
UDP:服务器信息提供端
接着,实现服务端的UDP数据的接收并处理。
UDPProvider类:
public class UDPProvider {
private static Provider PROVIDER_INSTANCE;
static void start(int port){
stop();
String sn = UUID.randomUUID().toString();
Provider provider = new Provider(sn, port);
provider.start();
PROVIDER_INSTANCE = provider;
}
static void stop(){
if(PROVIDER_INSTANCE != null){
PROVIDER_INSTANCE.exit();
PROVIDER_INSTANCE.close();
}
}
private static class Provider extends Thread{
private final int port;
private final byte[] sn;
private boolean done = false;
private DatagramSocket ds = null;
private byte[] buffer = new byte[128];
public Provider(String sn, int port) {
super();
this.sn = sn.getBytes();
this.port = port;
}
@Override
public void run() {
super.run();
System.out.println("UDPProvider Started.");
try{
ds = new DatagramSocket(UDPConstants.PORT_SERVER);
DatagramPacket datagramPacket = new DatagramPacket(buffer, buffer.length);
while(!done){
ds.receive(datagramPacket);
String clientIP = datagramPacket.getAddress().getHostAddress();
int clientPort = datagramPacket.getPort();
int clientDataLen = datagramPacket.getLength();
byte[] clientData = datagramPacket.getData();
boolean isValid = clientDataLen >= UDPConstants.HEADER.length+2+4 &&
ByteUtils.startsWith(clientData, UDPConstants.HEADER);
System.out.println("UDPProvider receive form ip:" + clientIP
+ "\tport:" + clientPort + "\tdataValid:" + isValid);
if(!isValid){
continue;
}
int index = UDPConstants.HEADER.length;
short cmd = (short) ((clientData[index++] << 8) | (clientData[index++] & 0xff));
int responsePort = (((clientData[index++]) << 24) |
((clientData[index++] & 0xff) << 16) |
((clientData[index++] & 0xff) << 8) |
((clientData[index] & 0xff)));
if(cmd == 1 && responsePort > 0){
// 构建一份回送数据
ByteBuffer byteBuffer = ByteBuffer.wrap(buffer);
byteBuffer.put(com.shengid.socket.mcourse.demo5tpc.constants.UDPConstants.HEADER);
byteBuffer.putShort((short) 2);
byteBuffer.putInt(port);
byteBuffer.put(sn);
int len = byteBuffer.position();
// 直接根据发送者构建一份回送信息
DatagramPacket responsePacket = new DatagramPacket(buffer,
len,
datagramPacket.getAddress(),
responsePort);
ds.send(responsePacket);
System.out.println("UDPProvider response to:" + clientIP + "\tport:" + responsePort + "\tdataLen:" + len);
} else {
System.out.println("UDPProvider receive cmd nonsupport; cmd:" + cmd + "\tport:" + port);
}
}
} catch (Exception e){
} finally{
close();
}
// 完成
System.out.println("UDPProvider Finished.");
}
private void close() {
if (ds != null) {
ds.close();
ds = null;
}
}
/**
* 提供结束
*/
void exit() {
done = true;
close();
}
}
}
主要的思路就是,接受客户端的请求,判断是否符合协议。
然后获取协议字段。
重点在协议的字段获取的位运算
。
UDP协议测试
客户端作简单测试:
ServerInfo serverInfo = UDPSearcher.searchServer(10000);
System.out.println("Server information: " + serverInfo);
服务端测试:
UDPProvider.start(UDPConstants.PORT_SERVER);
TCP:C/S通信协议
在我们上面使用UDP获取到服务器信息之后,客户端就可以开始使用TCP socket来连接服务器并且与之通信。
这里我们的通信例子仍然是简单的echo例子
。
下面是客户端的请求接收处理:
TCPClient类:
public class TCPClient {
public static void linkWith(ServerInfo serverInfo) throws IOException {
Socket socket = new Socket();
socket.setSoTimeout(3000);
socket.connect( new InetSocketAddress(Inet4Address.getByName(serverInfo.getAddrss()), serverInfo.getPort()), 3000);
System.out.println("已发起服务器连接...");
System.out.println("客户端信息:" + socket.getLocalAddress() + " P:" + socket.getLocalPort());
System.out.println("服务器信息:" + socket.getInetAddress() + " P:" + socket.getPort());
try{
requestSocket(socket);
} catch (Exception e){
System.out.println("异常!!!正在关闭...");
}
// 释放socket资源
socket.close();
// 客户端退出
System.out.println("客户端退出...");
}
private static void requestSocket(Socket socket) throws IOException {
// 获取socket流
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
// 将socket流,转化成特定形式的流
PrintStream socketPrintStream = new PrintStream(out);
BufferedReader socketBufferedReader = new BufferedReader(new InputStreamReader(in));
// 键盘输入流
BufferedReader userInputReader = new BufferedReader(new InputStreamReader(System.in));
boolean Waiting = true;
do{
// 用户输入一行用于发送数据
String inputStr = userInputReader.readLine();
socketPrintStream.println(inputStr);
// 从socket中读取一行数据
String responseStr = socketBufferedReader.readLine();
if("bye".equalsIgnoreCase(responseStr)){
Waiting = false;
}
System.out.println("Response string: " + responseStr);
}while(Waiting);
// 释放读写socket的相关资源
socketPrintStream.close();
socketBufferedReader.close();
}
}
客户端程序只需要调用TCPClient.linkWith(ServerInfo serverInfo)
即可连接服务端进行通信,函数传入的参数为我们之前通过UDP获取的服务器连接信息serverInfo
。
分析一下客户端的代码:
首先创建一个socket对象,来连接特定的服务器
。连接成功之后,开始进行通信,通信的一来一往与【网络编程】——Java实现(9)系列文章
中的方式大同小异。这里就不一一介绍了。
下面介绍服务端的实现。
TCPServer类:
上代码:
public class TCPServer {
private final int port;
private ClientListener mListener;
public TCPServer(int port) {
this.port = port;
}
public boolean start(){
try {
mListener = new ClientListener(port);
mListener.start();
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
public void stop(){
if(mListener != null){
mListener.exit();
}
}
private static class ClientListener extends Thread{
private boolean done = false;
private ServerSocket server;
private ClientHandler clientHandler;
public ClientListener(int port) throws IOException {
server = new ServerSocket(port);
System.out.println("服务端信息, IP: " + server.getInetAddress().getHostAddress() +
", Port: " + server.getLocalPort());
}
@Override
public void run() {
super.run();
System.out.println("服务端准备就绪...");
do{
Socket client;
try {
client = server.accept();
} catch (IOException e) {
continue;
}
// 启动处理客户端的线程
clientHandler = new ClientHandler(client);
clientHandler.start();
} while(!done);
System.out.println("服务器关闭...");
}
void exit(){
done = true;
try {
server.close();
clientHandler.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private static class ClientHandler extends Thread{
private Socket client;
private boolean done = false;
public ClientHandler(Socket client) {
this.client = client;
}
@Override
public void run() {
super.run();
System.out.println("新客户端连接. IP: " + client.getInetAddress().getHostAddress() +
", Port: " + client.getPort());
try {
// 写socket
PrintStream out = new PrintStream(client.getOutputStream());
// 读socket
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
do {
// socket中读取一行
String str = in.readLine();
System.out.println("接收信息: " + str);
// socket中写入一行,echo
System.out.println("回送信息:" + str);
out.println(str);
if("bye".equalsIgnoreCase(str)){
close();
}
} while (!done);
in.close();
out.close();
} catch (IOException e) {
System.out.println("连接异常断开");
} finally {
// 关闭连接
close();
}
System.out.println("客户端退出. IP: " + client.getInetAddress().getHostAddress() +
", Port: " + client.getPort());
}
private void close(){
if(client != null){
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
done = true;
}
}
}
}
一个构造函数,只需传入端口号,即socket只需要监听制定端口就ok。start与stop
函数在分别为开启服务
和关闭服务
的函数。
start函数
生成一个ClientListener对象来监听指定端口的连接。
ClientListener类
的核心代码:
一个do-while循环,不停地接收客户端的连接请求
(client = server.accept();
),接收到后连接之后,new一个ClientHandler对象处理此客户端的请求
。之后进入新的循环,继续等待新的客户端连接
。
System.out.println("服务端准备就绪...");
do{
Socket client;
try {
client = server.accept();
} catch (IOException e) {
continue;
}
// 启动处理客户端的线程
clientHandler = new ClientHandler(client);
clientHandler.start();
} while(!done);
System.out.println("服务器关闭...");
clientHandler 类就是处理socket io的相关操作了。很简单
不过此类的重要地方,在于各个类的exit和close方法
。
在stop方法,调用ClientListener 类的 mListener.exit()函数
。
看到ClientListener类的exit方法,不仅仅要设置done=true结束监听循环,server.close()释放监听socket的资源;更要调用clientHandler.close();来关闭已经在处理与客户端通信的线程。
这样当我们调用TCPServer类的stop方法时,才算将服务器的所有资源都释放关闭掉
。
整体UDP,TCP程序测试:
Client类:
public class Client {
public static void main(String[] args) {
ServerInfo serverInfo = UDPSearcher.searchServer(10000);
System.out.println("Server information: " + serverInfo);
if(serverInfo != null){
try {
// 连接服务端
TCPClient.linkWith(serverInfo);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Server类:
public class Server {
public static void main(String[] args) {
// 先监听客户端的信息,再开启UDP协议服务
TCPServer server = new TCPServer(TCPConstants.PORT_SERVER);
boolean isStart = server.start();
if(!isStart){
System.out.println("Server TCP Server failed!");
return;
}
UDPProvider.start(TCPConstants.PORT_SERVER);
// 服务端程序只要在控制太按任意键回车,就会退出程序
try {
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
// 关闭
UDPProvider.stop();
server.stop();
}
}
ok,代码就是上面所说的了。
可以按下面的方式组织java项目目录
|Project Name
|client
--|bean
----ServerInfo.java
--|Client.java
--|TCPClient.java
--|TCPSearcher.java
|server
--|Server.java
--|TCPServer.java
--|UDPProvider.java
|constants
--|TCPConstants.java
--|UDPConstants.java
|lib
--|--|utils
--|--|--ByteUtils.java