1.Socket简介
网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。
建立网络通信连接至少要一对端口号(socket)。socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口;HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。
Socket的英文原义是“孔”或“插座”。通常也称作”套接字”,用于描述IP地址和端口,是一个通信链的句柄,可以用来实现不同虚拟机或不同计算机之间的通信。
理论上两台主机能互相ping通,则可以互相socket通信。
2.需求描述
一台具有固定ip的android设备(作为Socket服务端),客户端(可以多个)需要给它发送一些命令来即时播放文本、图片、视频等信息。
3.需求分析
一般来说,Android开发时像一般简单的需求可以通过http或者webservice进行通信,对即时性要求不高的通信也可以折中选择轮询服务的机制(但耗流量)。但是要实现上述需求所说的即时播放多媒体信息的话,显然用Socket来实现最佳。
服务端: Java中能接收其他通信实体连接请求的类是ServerSocket,ServerSocket对象用来监听来自客户端的Socket连接,如果没有连接,它将一直处于等待状态。ServerSocket类提供如下构造器:
- ServerSocket(int port); 使用指定端口来创建,port范围:0-65535。注意,在选择端口时,必须小心。每一个端口提供一种特定的服务,只有给出正确的端口,才能获得相应的服务。0~1023的端口号为系统所保留,例如http服务的端口号为80,telnet服务的端口号为21,ftp服务的端口号为23, 所以我们在选择端口号时,最好选择一个大于1023的数以防止发生冲突。
- ServerSocket(int port, int backlog); 增加一个用来改变连接队列长度的参数backlog。
- ServerSocket(int port, int backlog, InetAddress bindAddr);在机器存在多个IP地址的情况下,bindAddr参数指定将ServerSocket绑定到指定IP。
客户端 通常使用Socket来连接指定服务器,Socket类提供如下构造器:
- Socket(InetAddress/String remoteAddress, int port); 创建连接到指定远程主机、远程端口的Socket,本地IP地址和端口使用默认值。
- Socket(InetAddress/String remoteAddress, int port, InetAddress localAddr, int localPort); 绑定本地IP地址和端口,适用于本地主机有多个IP地址的情形。
Socket通信需要服务端和客户端,刚开始分析时把Android端当客户端,发现这样行不通啊,Android设备是负责监听某个端口,有消息来时就作出响应(播放多媒体信息),这应该是服务端做的事情。所以让Android端作为服务端。
需要自定义协议来播放文本、图片、视频消息。
4.编程实现
4.1自定义协议很简单,按字节来定义如下(编码utf-8):
开始标志(1byte) 业务数据包 结束标志(1byte)
0x02 0x03
业务数据包定义:顺序定义(各域间用"|"分隔,第一个域为消息类型)
1)文本通知
2001|优先级|每次时长(秒)|播放次数(默认1)|字体大小|通知内容
2)图片通知
2071|优先级|每次时长(秒)|播放次数(默认1)|是否全屏|图片URL
5)视频通知(1为全屏播放)
2051|优先级|播放次数|是否全屏|视频URL
通知内容中不能包含分隔符|,字体大小默认60(大于0才有效)
播放次数:<=0则为默认1次,大于0才有效
优先级:同类通知中数字越大表示优先级越高,<=0表示放到队列末尾。
普通文本>图片>视频
返回客户端:
OK -- 成功
ERROR:MSG -- 失败 例如 ERROR:消息类型不正确
4.2 服务端实现
Android设备作为服务端,需要开一个端口监听。很容易想到用Service来实现,这里采用它的一个子类IntentService来实现(该类的销毁由系统管理,非常方便),为了可以多个客户端连接,需要启动多个线程分别与客户端建立连接。
4.2.1 负责监听端口的服务类SocketService.java
import android.app.IntentService;
import android.content.Intent;
import android.support.annotation.Nullable;
import android.util.Log;
import com.jykj.departure.util.SocketHelper;
import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SocketService extends IntentService {
private ServerSocket server;
private static final int PORT = 54321;
private List<Socket> mList = new ArrayList<>();
/**
* Creates an IntentService. Invoked by your subclass's constructor.
*
* @param name Used to name the worker thread, important only for debugging.
*/
public SocketService(String name) {
super(name);
}
public SocketService() {
super("Socket Service");
System.out.println("Create SocketService!");
}
@Override
protected void onHandleIntent(@Nullable Intent intent) {
ExecutorService es;
try {
server = new ServerSocket(PORT);
server.setReuseAddress(true);
es = Executors.newCachedThreadPool();
} catch (IOException e) {
e.printStackTrace();
return;
}
Log.e("WS", "begin client connected");
Socket client;
while (true) {
Log.e("WS", "线程连接数:" + mList.size());
try {
client = server.accept();
Log.e("WS", "client connected:" + client.getInetAddress() + ":" + client.getPort());
mList.add(client);
es.execute(new ServiceThread(client));
} catch (IOException e) {
e.printStackTrace();
break;
}
}
}
//每个客户端连接开启一个线程处理
class ServiceThread implements Runnable {
private Socket socket;
ServiceThread(Socket s) {
socket = s;
}
@Override
public void run() {
try {
InputStream is = socket.getInputStream();
byte[] bytes = SocketHelper.readBytes(is);
Log.e("WS", "字节长度:" + bytes.length + "," + new String(bytes, SocketHelper.CHARSET));
String s = SocketHelper.checkBytes(bytes);
Log.e("WS", "校验结果:" + s);
if (SocketHelper.RETURN_OK.equals(s)) {
Intent i = new Intent(MainActivity.ACTION_SOCKET);
i.putExtra(MainActivity.EXTRA_INFO, SocketHelper.getContent(bytes));
sendBroadcast(i);//校验成功将 消息命令 通过广播发送给主界面MainActivity
}
mList.remove(socket);
PrintWriter writer = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(),SocketHelper.CHARSET), true);
writer.println(s);
writer.flush();
writer.close();
is.close();
socket.close();//记得关闭socket(关闭与客户端的连接)
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
4.2.2 与协议有关的辅助类SocketHelper.java
import android.util.Log;
import com.jykj.departure.entity.Notice;
import com.jykj.departure.entity.Pictures;
import com.jykj.departure.entity.Videos;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
/**
* socket协议接口
*/
public class SocketHelper {
public static final String CHARSET = "UTF-8";
private static final byte BEGIN_BYTE=0x02;
private static final byte END_BYTE=0x03;
public static final String SPLITOR = "\\|";//分隔符 "|"
public static final String RETURN_OK = "OK";
private static final String RETURN_ERROR = "ERROR:";
public static final String TYPE_GENERAL="2001";//普通消息
public static final String TYPE_VIDEO_GENERAL = "2051";//视频普通
public static final String TYPE_PICTURE_GENERAL = "2071";//图片普通
//从输入流中读取所有字节
public static byte[] readBytes(InputStream is) throws IOException {
BufferedInputStream bis = new BufferedInputStream(is);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buff = new byte[128];
int len = -1;
boolean flag = true;
while (flag&&(len=bis.read(buff))!=-1){
for(int i=len-1;i>=0;i--){
if(buff[i]==END_BYTE){
flag = false;
len = i+1;
break;
}
}
baos.write(buff,0,len);
}
byte[] b = baos.toByteArray();
baos.close();
//bis.close();//此处如果把bis关闭的话(则会顺带关闭底层is),这样会将socket关闭,这样就不能写出数据了!!!
return b;
}
/**
* 校验 消息格式
* 目前只有两种消息格式 播放次数为0表示停止播放,为-1表示一直播放
* 优先级:数字越大表示优先级越高,<=0表示放到队列末尾,班次通知列表优先级最高
音,发车时间,车牌号,车型名称,经停,晚点
* @param bytes 字节流
* @return OK
*/
public static String checkBytes(byte[] bytes){
if(bytes==null||bytes.length<=0) return RETURN_ERROR+"未读取到数据";
byte b1= bytes[0];//起始标志
byte b2 = bytes[bytes.length-1];//结束标志
if(b1!=BEGIN_BYTE) return RETURN_ERROR+"协议起始标志不对";
if(b2!=END_BYTE) return RETUR