由于項(xiàng)目要求TV端與移動(dòng)(手機(jī)/平板)端進(jìn)行離線通訊的需求同诫,所以我選擇了建立TCP連接來實(shí)現(xiàn)離線的功能。
那么問題來了:
1.再TV端輸入IP地址(這個(gè)界面也是需要的)泡孩,但是使用遙控機(jī)輸入麻煩采郎。
2.如何使TV端自動(dòng)獲取移動(dòng)端的IP。
本來考慮的通過移動(dòng)端上傳IP轩褐,TV端再進(jìn)行更新的方案。但是可能存在沒網(wǎng)情況下IP未及時(shí)更新的情況玖详,那么離線模式也將不可用把介,穩(wěn)定性不高。
后來想到可以用UDP廣播來實(shí)現(xiàn)獲取移動(dòng)端IP蟋座,TV端通過發(fā)送UDP廣播拗踢,如果移動(dòng)端在同一網(wǎng)段,那么接收到廣播后再把當(dāng)前的IP通過UDP發(fā)送給TV端蜈七,拿到了IP,那問題自然也就解決了莫矗。
第一步:建立TV端UDP接收器&發(fā)送器
UDP接收器
服務(wù)端要這邊定好自己的端口飒硅,客戶端通過這個(gè)端口發(fā)送(同網(wǎng)段),服務(wù)端就可以接收到客戶端發(fā)送的廣播了作谚。
public synchronized void initAndStart(){
byte[] message = new byte[100];
try {
datagramSocket = new DatagramSocket(TV_SERVER_PORT );
datagramSocket.setBroadcast(true);
DatagramPacket datagramPacket = new DatagramPacket(message , message.length);
while (!isThreadDisable){
try {
lock.acquire();
datagramSocket.receive(datagramPacket);
String receiveMsg =new String(datagramPacket.getData()).trim();
Log.e("UdpServer", "收到消息 " + receiveMsg);
if (receiveMsg.startsWith("server_ip")){
String[] serverIp = receiveMsg.split("#");
if (serverIp.length == 2 && listener != null){
listener.onGetServerIp(serverIp[1]);
}
}
lock.release();
} catch (IOException e) {
e.printStackTrace();
}
}
} catch (SocketException e) {
e.printStackTrace();
}
}
UDP發(fā)送器
值得注意的是三娩,在安卓上并不能接收到跨網(wǎng)段的UDP廣播(當(dāng)然在你知道IP的情況下跨網(wǎng)段通信是可以的,這一點(diǎn)和TCP通信沒什么區(qū)別)妹懒,所以只能在同網(wǎng)段下實(shí)現(xiàn)UDP廣播的收發(fā)雀监。
private synchronized void send(String message) {
if (TextUtils.isEmpty(message))
return;
if (datagramSocket == null || datagramSocket.isClosed()) {
InetAddress inetAddress = null;
try {
datagramSocket = new DatagramSocket();
} catch (SocketException e) {
e.printStackTrace();
}
try {
inetAddress = InetAddress.getByName(broadcastIp);
} catch (UnknownHostException e) {
e.printStackTrace();
}
int msg_length = message.length();
byte[] messageByte = message.getBytes();
datagramPacket = new DatagramPacket(messageByte, msg_length, inetAddress, UDP_QUEUE_SERVER_PORT);
}
try {
Log.e("UdpSender", "將要發(fā)送消息 " + message);
lock.acquire();
datagramSocket.send(datagramPacket);
lock.release();
} catch (IOException e) {
e.printStackTrace();
}
}
注:我這邊因?yàn)檠h(huán)調(diào)用上面發(fā)送器的原因,沒有把datagramSocket直接close掉眨唬。等到發(fā)送廣播結(jié)束時(shí)要注意調(diào)用datagramSocket.close()來釋放資源会前。
第二步:建立移動(dòng)端UDP接收器&發(fā)送器
在移動(dòng)端的收發(fā)和上面的大同小異,無非是發(fā)送和接收的內(nèi)容不同罷了匾竿,這里我就不再詳細(xì)貼代碼了瓦宜,參考第一步中的代碼可以自行根據(jù)自己的業(yè)務(wù)來構(gòu)造UDP的收發(fā)器。
第三步:在移動(dòng)端上建立TCP服務(wù)端
這邊我使用的xsocket的庫岭妖,里面封裝了一些接口比較方便临庇。
首先實(shí)現(xiàn)xsocket封裝的handler接口:
public class SocketServerHandler implements IDataHandler, IConnectHandler, IDisconnectHandler, IDestroyable, ISocketSender {
private final String TAG = "SocketServerHandler";
public SocketServerHandler(){
}
@Override
public boolean onData(INonBlockingConnection iNonBlockingConnection) throws IOException, BufferUnderflowException, ClosedChannelException, MaxReadSizeExceededException {
return true;
}
@Override
public boolean onConnect(INonBlockingConnection iNonBlockingConnection) throws IOException, BufferUnderflowException, MaxReadSizeExceededException {
Log.e(TAG, "消息服務(wù)器,客戶端連接上來了.onConnect" + iNonBlockingConnection);
}
@Override
public boolean onDisconnect(INonBlockingConnection iNonBlockingConnection) throws IOException {
Log.e(TAG, "消息服務(wù)器昵慌,客戶端斷開連接.onDisconnect");
return true;
}
@Override
public void destroy() {
}
@Override
public synchronized void send(String message) {
}
}
那么接下來我們要在客戶端連接上時(shí)假夺,去保存客戶端的連接,以便之后發(fā)消息給客戶端:
private Lock lock = new ReentrantLock();
private Set<INonBlockingConnection> connections = null;
@Override
public boolean onConnect(INonBlockingConnection iNonBlockingConnection) throws IOException, BufferUnderflowException, MaxReadSizeExceededException {
Log.e(TAG, "消息服務(wù)器斋攀,客戶端連接上來了.onConnect" + iNonBlockingConnection);
lock.lock();
try {
connections.add(iNonBlockingConnection);
} finally {
lock.unlock();
}
return true;
}
@Override
public boolean onDisconnect(INonBlockingConnection iNonBlockingConnection) throws IOException {
Log.e(TAG, "消息服務(wù)器已卷,客戶端斷開連接.onDisconnect");
lock.lock();
try {
connections.remove(iNonBlockingConnection);
} finally {
lock.unlock();
}
return true;
}
好了,接下來就是實(shí)現(xiàn)如何去發(fā)送消息了淳蔼。我選擇使用BlockingQueue來實(shí)現(xiàn)消息的存取悼尾,一種實(shí)現(xiàn)了阻塞接口的隊(duì)列柿扣,然后啟動(dòng)發(fā)送消息的線程去循環(huán)取這個(gè)隊(duì)列就可以了,不說了闺魏,上代碼:
private BlockingQueue<String> messageQueue = null;//消息隊(duì)列
private Thread writeThread;//發(fā)消息線程
private Timer timer = null;//用來發(fā)送心跳消息的輪詢?nèi)蝿?wù)
public SocketServerHandler() {
messageQueue = new LinkedBlockingDeque<>(100);
connections = new HashSet<>();
writeThread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
String msg;
try {
msg = messageQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
return;
}
List<INonBlockingConnection> tConnections = new ArrayList<>();
lock.lock();
try {
if (connections.isEmpty()) {
continue;
}
tConnections.addAll(connections);
} finally {
lock.unlock();
}
StringBuilder sb = new StringBuilder();
sb.append(msg).append(IQueueMessage.SPLIT);
String tMsg = sb.toString();
for (INonBlockingConnection connection : tConnections) {
try {
if (connection.isOpen()) {
Log.e(TAG, "客戶端信息:" + connection);
connection.write(tMsg);
}
} catch (Throwable t) {
t.printStackTrace();
}
}
}
}
});
writeThread.start();
timer = new Timer(TAG, true);
long splitTime = 20 * 1000L;
timer.schedule(new TimerTask() {
@Override
public void run() {
send(IQueueMessage.MESSAGE_HEART);
}
}, splitTime, splitTime);
}
在發(fā)送消息時(shí)未状,只需要向隊(duì)列里面塞消息就可以了:
@Override
public synchronized void send(String message) {
if (message != null) {
Log.e(TAG, "發(fā)送消息:" + message);
messageQueue.add(message);
}
}
最后,啟動(dòng)socket服務(wù)析桥,把剛才定義的handler放進(jìn)去就可以了:
private IServer iServer;
SocketServerMonitor monitor = new SocketServerMonitor(new SocketServerHandler());
handler.post(monitor);
class SocketServerMonitor implements Runnable {
SocketServerHandler serverHandler;
public SocketServerMonitor(SocketServerHandler serverHandler) {
this.serverHandler = serverHandler;
}
@Override
public void run() {
if (iServer != null && iServer.isOpen()) {
handler.postDelayed(this, 10 * 1000);
return;
}
new Thread(new Runnable() {
@Override
public void run() {
try {
iServer = new Server(IQueueMessage.SOCKET_MESSAGE_PORT, serverHandler);
iServer.start();
iServer.addListener(new IServerListener() {
@Override
public void onInit() {
Log.e("SocketServerMonitor", "消息服務(wù)器初始化...");
}
@Override
public void onDestroy() throws IOException {
Log.e("SocketServerMonitor", "消息服務(wù)器onDestroy...");
}
});
Log.e("SocketServerMonitor", "啟動(dòng)/重啟消息服務(wù)器成功");
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
handler.postDelayed(this, 10 * 1000);
}
}
第四步:在TV端上建立TCP客戶端司草,并實(shí)現(xiàn)socket連接監(jiān)聽器
客戶端使用INonBlockingConnection這個(gè)接口來實(shí)現(xiàn)socket的連接,同樣也需要自定義handler來處理消息泡仗÷窈纾基本和服務(wù)端一樣:
public class SocketClientHandler implements IDataHandler, IDisconnectHandler, IConnectHandler , IDestroyable , ISocketSender{
final private static String TAG = "SocketClientHandler";
/**
* <code>是否連接上了</code>.
*/
private boolean isConnected;
private INonBlockingConnection serverConnection;
private BlockingQueue<String> messageQueue = null;
private Thread writeThread;
public SocketClientHandler() {
messageQueue = new LinkedBlockingQueue<>();
writeThread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
String msg;
try {
msg = messageQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
return;
}
if (serverConnection == null)
continue;
StringBuilder sb = new StringBuilder();
sb.append(msg).append(IQueueMessage.SPLIT);
String sendMsg = sb.toString();
try {
if (serverConnection.isOpen()){
Log.e(TAG , "服務(wù)端信息:"+ serverConnection);
serverConnection.write(sendMsg);
}
}catch (Throwable t) {
t.printStackTrace();
}
}
}
});
writeThread.start();
}
@Override
public boolean onData(INonBlockingConnection arg) throws IOException, BufferUnderflowException, ClosedChannelException, MaxReadSizeExceededException {
String msg = arg.readStringByDelimiter(IQueueMessage.SPLIT).trim();
if (StringUtils.isBlank(msg))
return true;
if (IQueueMessage.MESSAGE_HEART.equals(msg)){
Log.e(TAG, "心跳消息:" + msg);
return true;
}
Log.e(TAG, "收到消息體:" + msg);
return true;
}
@Override
public boolean onConnect(INonBlockingConnection arg) throws IOException, BufferUnderflowException, MaxReadSizeExceededException {
Log.e(TAG , "onSocketConnected");
isConnected = true;
serverConnection = arg;
return true;
}
@Override
public boolean onDisconnect(INonBlockingConnection arg) throws IOException {
Log.e(TAG , "onSocketDisConnected -- " + arg);
isConnected = false;
serverConnection = null;
return true;
}
public boolean isConnected() {
return isConnected;
}
public void reset() {
isConnected = false;
}
@Override
public synchronized void send(String message) {
if (message != null) {
Log.e(TAG , "發(fā)送消息:" + message);
messageQueue.add(message);
}
}
@Override
public void destroy() {
Log.e(TAG, "客戶端銷毀");
writeThread.interrupt();
}
}
然后自己維護(hù)一個(gè)監(jiān)聽器,維護(hù)實(shí)現(xiàn)客戶端的socket連接娩怎。
public class SocketConnectionMonitor extends Handler {
final private static String TAG = "SocketConnectionMonitor";
private String serverIp;
private int port;
private SocketClientHandler socketClientHandler;
private INonBlockingConnection nonBlockingConnection;
private Application application;
private boolean connecting;
public SocketConnectionMonitor(String serverIp, int port,Application application) {
this.serverIp = serverIp;
this.port = port;
this.application = application;
socketClientHandler = new SocketClientHandler();
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case IMessageKey.KEY_MONITOR_SOCKET:
monitorConnection();
break;
}
}
public synchronized void monitorConnection() {
if (!isConnected() && !connecting) {
if (!NetWorkUtils.isNetworkActive(application))
return;
connecting = true;
try {
connect();
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
connecting = false;
}
}
}
private void connect() {
if (StringUtils.isBlank(serverIp) && !NetWorkUtils.isNetworkActive(application))
return;
if (nonBlockingConnection != null) {
try {
Log.e(TAG, "initConnection nonBlockingConnection.close();");
nonBlockingConnection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
try {
Log.e(TAG, "initConnection nonBlockingConnection.reset();");
socketClientHandler.reset();
Log.e(TAG, "initConnection" + serverIp + ":" + port);
String[] ipStr = serverIp.split("\\.");
byte[] ipBuf = new byte[4];
for (int i = 0; i < 4; i++) {
ipBuf[i] = (byte) (Integer.parseInt(ipStr[i]) & 0xff);
}
InetAddress inetAddress = InetAddress.getByAddress(ipBuf);
Log.e(TAG, "initConnection connect");
nonBlockingConnection = new NonBlockingConnection(inetAddress, port, socketClientHandler, 3000);
Log.e(TAG, "initConnection nonBlockingConnection.setIdleTimeoutMillis");
nonBlockingConnection.setIdleTimeoutMillis(32000);
Log.e(TAG, "initConnection - success" + serverIp + ":" + port);
} catch (Throwable t) {
t.printStackTrace();
Log.e(TAG, "initConnection - failed " + serverIp + ":" + port);
}
}
public boolean isConnected() {
return socketClientHandler.isConnected();
}
public void setServerIp(String serverIp) {
this.serverIp = serverIp;
socketClientHandler.reset();
}
}
最后啟動(dòng)建立線程去監(jiān)測socket的連接狀態(tài)搔课,TV端與移動(dòng)端的離線模式基本框架就已經(jīng)完成了。
if (!mMessageInited) {
final String serverIp = mPlatform.getServerIp();
mSocketMonitor = new SocketConnectionMonitor(serverIp, IQueueMessage.SOCKET_MESSAGE_PORT, this);
mMonitorThread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
mSocketMonitor.monitorConnection();
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
mMonitorThread.start();
mMessageInited = true;
}
當(dāng)然截亦,如果兩個(gè)設(shè)備不在同一網(wǎng)段的話爬泥,TV端這邊是無法自動(dòng)獲取移動(dòng)端的IP的,所以還是留了一個(gè)輸入IP的界面可以讓用戶自己輸入IP崩瓤。因?yàn)檫b控器上只有上下左右確認(rèn)鍵袍啡,輸入的時(shí)候還是比較繁瑣的,實(shí)現(xiàn)界面是要注意焦點(diǎn)的控制/(ㄒoㄒ)/~~却桶。