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 RETURN_ERROR+"協議結束標誌不對";
try {
String content = new String(Arrays.copyOfRange(bytes,1,bytes.length-1),CHARSET);
Log.e("WS","內容:"+content);
if(content.length()<=0) return RETURN_ERROR+"消息內容爲空";
String[] ss = content.split(SPLITOR);
Log.e("WS","消息類型:"+ss[0]);
String t = ss[0];
if(TYPE_GENERAL.equals(t)){
if(ss.length<6) return RETURN_ERROR+"文本通知格式需要6個字段";
else return RETURN_OK;
}elseif(TYPE_VIDEO_GENERAL.equals(t)){
if(ss.length<5) return RETURN_ERROR+"視頻通知格式需要5個字段";
else return RETURN_OK;
}else if(TYPE_PICTURE_GENERAL.equals(t)){
if(ss.length<6) return RETURN_ERROR+"圖片通知格式需要6個字段";
else return RETURN_OK;
} else
return RETURN_ERROR+"消息類型不正確"+t;
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return RETURN_ERROR+"不支持的編碼類型"+CHARSET;
}
}
//先校驗,再調用此方法,獲取有效內容(不包含開始結束的兩個字節)
public static String getContent(byte[] bytes){
try {
return new String(Arrays.copyOfRange(bytes,1,bytes.length-1),CHARSET);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return null;
}
}
public static Videos parseToVideo(String content){
String[] ss = content.split(SPLITOR);
Videos n = new Videos();
n.setType(ss[0]);
n.setOrder(ss[1].isEmpty()?0:Integer.parseInt(ss[1]));
n.setCount(ss[2].isEmpty()?0:Integer.parseInt(ss[2]));
n.setFull(ss[3].equals("1"));
n.setUrl(ss[4]);
return n;
}
public static Pictures parseToPic(String content){
String[] ss = content.split(SPLITOR);
Pictures n = new Pictures();
n.setType(ss[0]);
n.setOrder(ss[1].isEmpty()?0:Integer.parseInt(ss[1]));
n.setDuration(ss[2].isEmpty()?0:Integer.parseInt(ss[2]));
n.setCount(ss[3].isEmpty()?0:Integer.parseInt(ss[3]));
n.setFull(ss[4].equals("1"));
n.setUrl(ss[5]);
return n;
}
public static Notice parseToNotice(String content){
String[] ss = content.split(SPLITOR);
Notice n = new Notice();
n.setType(ss[0]);
n.setOrder(ss[1].isEmpty()?0:Integer.parseInt(ss[1]));
n.setDuration(ss[2].isEmpty()?0:Integer.parseInt(ss[2]));
n.setCount(ss[3].isEmpty()?0:Integer.parseInt(ss[3]));
n.setSize(ss[4].isEmpty()?0:Integer.parseInt(ss[4]));
n.setContent(ss[5]);
return n;
}
}
讀取輸入流中的字節需要注意的是在遇到結束標誌時結束while循環,否則會一直阻塞,另外輸入流bis不能關閉(關閉了會將socket關閉而無法寫出數據了),因爲還要在SocketService中寫出數據,所以流的關閉都放在SocketService中。
4.2.3 幾個實體類
public class Notice {
private int order;//優先級
private int duration;//時長,單位秒
private int count;//次數
private String content;//消息內容
private int size;//字體大小
private String type;//通知類型
...省略 get set方法
}
public class Pictures {
private int order;//優先級
private int count;//次數
private String url;//url
private int duration;//時長,單位秒
private boolean full;//是否全屏
private String type;//通知類型
...省略 get set方法
}
public class Videos {
private int order;//優先級
private int count;//次數
private String url;//url
private boolean full;//是否全屏
private String type;//通知類型 2061 長期,2051 短期,請查看SocketHelper定義的類型
...省略 get set方法
}
4.2.3 主界面類MainActivity.java
public class MainActivity extends ListActivity {
...省略很多不相關的代碼
public static final String ACTION_SOCKET = "com.abcdefg.socket";
public static final String EXTRA_INFO = "EXTRA_INFO";
private TextView socketTV;
private ScrollView socketScollView;
private VideoView socketVideo;
private ImageView socketImageView;
private View socketRegularView, socketLayoutVideo;
private List<Pictures> picsList=new ArrayList<>();//圖片通知
private List<Videos> videosList=new ArrayList<>();//視頻通知
private List<Notice> noticeList=new ArrayList<>();//消息列表,當檢票列表爲空時顯示消息列表,當二者都爲空時,顯示副表第一條記錄
Handler handler = new Handler();
@Override
protected void onPause() {
super.onPause();
unregisterReceiver(mReceiver);
}
@Override
protected void onResume() {
super.onResume();
registerReceiver(mReceiver, new IntentFilter(ACTION_SOCKET));
startService(new Intent(this, SocketService.class));
}
//播放圖片
private void setPicture(String url, boolean needStore){
Uri uri = ApplicationHelper.checkFileExist(ApplicationHelper.getPicDir(this),ApplicationHelper.getNetworkFileName(url));
Log.e("WS","圖片數:"+picsList.size()+",圖片name:"+ApplicationHelper.getNetworkFileName(url)+",Uri:"+uri);
if(uri!=null){
socketImageView.setImageURI(uri);
return;
}
new DownloadPictureTask(new DownloadPictureTask.OnTaskFinishCallback() {
@Override
public void onTaskFinish(Bitmap result, String exceptionInfo) {
if(exceptionInfo==null){
if(result==null){
Toast.makeText(MainActivity.this,"獲取圖片異常!",Toast.LENGTH_LONG).show();
handler.removeCallbacks(playRnnable);
handler.postDelayed(playRnnable,100);
return;
}
socketImageView.setImageBitmap(result);
}else {
Toast.makeText(MainActivity.this,"獲取圖片異常:"+exceptionInfo,Toast.LENGTH_LONG).show();
}
}
},url,needStore,ApplicationHelper.getPicDir(this)).execute();
}
//播放視頻
private void playVideo(final String url,boolean needStore){
Uri uri = ApplicationHelper.checkFileExist(ApplicationHelper.getVideoDir(this),ApplicationHelper.getNetworkFileName(url));
Log.e("WS","視頻數:"+videosList.size()+",視頻name:"+ApplicationHelper.getNetworkFileName(url)+",Uri:"+uri);
if(uri == null){
uri = Uri.parse( url );//網絡視頻
//下載它
if(needStore){
Log.e("WS","開始下載視頻:"+uri);
new Thread(){
@Override
public void run() {
try {
HttpHelper.downloadVideo(url,ApplicationHelper.getVideoDir(MainActivity.this));
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();}
}
//接收廣播的Socket消息
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (ACTION_SOCKET.equals(intent.getAction())) {
String content = intent.getStringExtra(EXTRA_INFO);
if (content == null || content.length() <= 0) return;
String[] ss = content.split(SocketHelper.SPLITOR);
if (SocketHelper.TYPE_GENERAL.equals(ss[0])) {//普通或長期
Notice n = SocketHelper.parseToNotice(content);
if(n.getOrder()<=0||noticeList.size()==0) noticeList.add(n);
else {
for(int i=0;i<noticeList.size();i++){
if(n.getOrder()>=noticeList.get(i).getOrder()){
noticeList.add(i,n);
break;
}
}
}
}else if(SocketHelper.TYPE_VIDEO_GENERAL.equals(ss[0])){//視頻
//if(socketVideo.isPlaying()) socketVideo.suspend();
Videos v = SocketHelper.parseToVideo(content);
if(v.getOrder()<=0||videosList.size()==0) videosList.add(v);
else {
for(int i=0;i<videosList.size();i++){
if(v.getOrder()>=videosList.get(i).getOrder()){
videosList.add(i,v);
break;
}
}
}
}else if(SocketHelper.TYPE_PICTURE_GENERAL.equals(ss[0])||SocketHelper.TYPE_PICTURE_LONGTERM.equals(ss[0])){//圖片
//長期通知則存儲
if(SocketHelper.TYPE_PICTURE_LONGTERM.equals(ss[0])){
Set<String> set = spLongterm.getStringSet(SocketHelper.SP_KEY_LONGTERM_PICTURES,new HashSet<String>());
if(!set.add(content)) return;//重複通知則不處理
spLongterm.edit().putStringSet(SocketHelper.SP_KEY_LONGTERM_PICTURES,set).apply();
}
Pictures v = SocketHelper.parseToPic(content);
if(v.getOrder()<=0||picsList.size()==0) picsList.add(v);
else {
for(int i=0;i<picsList.size();i++){
if(v.getOrder()>=picsList.get(i).getOrder()){
picsList.add(i,v);
break;
}
}
}
}
//顯示消息
handler.removeCallbacks(playRnnable);
handler.postDelayed(playRnnable,200);
}
}
};
//總播放任務,策略:普通文本=長期文本>圖片臨時=圖片長期>視頻臨時=視頻長期
Runnable playRnnable = new Runnable() {
@Override
public void run() {
boolean tb =noticeList.size()>0);//notice
boolean pb = noticeList.size()==0&&picsList.size()>0;//picture
boolean vb = noticeList.size()==0&&picsList.size()==0&&videosList.size()>0;//video
socketLayoutVideo.setVisibility(vb?View.VISIBLE:View.GONE);
if(!vb) socketVideo.suspend();
socketImageView.setVisibility(pb?View.VISIBLE:View.GONE);
socketTV.setVisibility(tb?View.VISIBLE:View.GONE);
socketScollView.setVisibility(!vb&&!tb&&!pb?View.VISIBLE:View.GONE);
if(noticeList.size()>0){
Notice n = noticeList.get(0);
socketTV.setTextSize(n.getSize()>0?n.getSize():NOTICE_TEXT_SIZE);
socketTV.setText(Html.fromHtml(n.getContent()));
socketTV.scrollTo(0,0);
noticeList.remove(0);
if(n.getType().equals(SocketHelper.TYPE_LONGTERM)){
noticeList.add(n);//長期通知保證不清除
}else {
if(n.getCount()>1){
n.setCount(n.getCount()-1);
n.setOrder(0);
noticeList.add(n);
}
}
handler.postDelayed(this,n.getDuration()>0?n.getDuration()*1000:20);
}else if(picsList.size()>0){
Pictures v = picsList.get(0);
socketRegularView.setVisibility(v.isFull()?View.GONE:View.VISIBLE);
String url = v.getUrl();
boolean needStore = v.getType().equals(SocketHelper.TYPE_PICTURE_LONGTERM);
setPicture(url,needStore);
picsList.remove(0);
if(v.getType().equals(SocketHelper.TYPE_PICTURE_LONGTERM)){
picsList.add(v);//長期通知保證不清除
}else {
if(v.getCount()>1){
v.setCount(v.getCount()-1);
v.setOrder(0);
picsList.add(v);
}
}
handler.postDelayed(this,v.getDuration()>0?v.getDuration()*1000:5);
}else if(videosList.size()>0){
Videos v = videosList.get(0);
socketRegularView.setVisibility(v.isFull()?View.GONE:View.VISIBLE);
playVideo(v.getUrl(),v.getType().equals(SocketHelper.TYPE_VIDEO_LONGTERM));
videosList.remove(0);
if(v.getType().equals(SocketHelper.TYPE_VIDEO_LONGTERM)){
videosList.add(v);//長期通知保證不清除
}else {
if(v.getCount()>1){
v.setCount(v.getCount()-1);
v.setOrder(0);
videosList.add(v);
}
}
}else {
setCurrentInfo(mList.size() > 0 ? mList.get(0) : null);
handler.postDelayed(this, 60);//如果只有一項,換頁頻率可以調成請求頻率
}
}
};
}
上面代碼比較多,但理清思路後並不複雜,原代碼中有9種消息分類代碼量更多,上面只是抽取了一部分。比如緊急通知優先級最高,還有一些長期通知類型的文本圖片視頻需要存儲在設備上,每次啓動應用後可以自動加載播放。
4.2.4 其他一些工具類
package com.jykj.departure.util;
import android.app.DownloadManager;
import android.app.DownloadManager.Request;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Environment;
import android.widget.Toast;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Map;
import java.util.Map.Entry;
/**
* 通用的與Http相關的輔助類
*/
public class HttpHelper {
private static String JSESSIONID; //定義一個靜態的字段,保存sessionID 例如JSESSIONID=AD5F5C9EEB16C71EC3725DBF209F6178
/**
* 利用android 下載管理器 下載文件
*
* @param context 上下文
* @param fileName 文件名
* @param uri
* http或https
* @return 如果已經下載過返回-1,否則返回Download ID
*/
public static long download(Context context, String fileName, Uri uri) {
Toast.makeText(context, "已經轉入後臺下載!", Toast.LENGTH_SHORT).show();
DownloadManager manager = (DownloadManager) context
.getSystemService(Context.DOWNLOAD_SERVICE); // 初始化下載管理器
Request request = new Request(uri);// 創建請求
//request.setAllowedNetworkTypes(Request.NETWORK_MOBILE | Request.NETWORK_WIFI);// 設置允許使用的網絡類型,這裏是移動網絡和wifi都可以
request.setNotificationVisibility(Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
request.setAllowedOverRoaming(false);// 漫遊
/*request.setDestinationInExternalPublicDir(
Environment.DIRECTORY_DOWNLOADS, fileName);*/
//判斷是否有SD卡,如果有設置路徑,沒有則使用默認路徑
if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED))
request.setDestinationInExternalFilesDir(context,
Environment.DIRECTORY_DOWNLOADS, fileName);
/* File file = new File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),fileName);
request.setDestinationUri(Uri.fromFile(file));*/
return manager.enqueue(request);// 將下載請求放入隊列
}
/**
* http POST 請求
*
* @param urlString
* http請求
* @param content
* 請求正文,正文內容其實跟get的URL中'?'後的參數字符串一致
* @return str
* @throws IOException IOException
*/
public static String requestPost(String urlString, String content)
throws IOException {
System.out.println("session:"+JSESSIONID+",URL:"+urlString+"?"+content);
URL url = new URL(urlString);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(6 * 1000);
// Read from the connection. Default is true.
conn.setDoInput(true);
// Output to the connection. Default is
// false, set to true because post
// method must write something to the
// connection
// 設置是否向connection輸出,因爲這個是post請求,參數要放在
// http正文內,因此需要設爲true
conn.setDoOutput(true);
// Post cannot use caches
conn.setUseCaches(false);
// Set the post method. Default is GET
conn.setRequestMethod("POST");
// This method takes effects to every instances of this class. URLConnection.setFollowRedirects是static函數,作用於所有的URLConnection對象。
// connection.setFollowRedirects(true);
// This methods only takes effacts to this instance.
conn.setInstanceFollowRedirects(true);
// Set the content type to urlencoded,because we will write some URL-encoded content to the
// connection. Settings above must be set before connect!
// 配置本次連接的Content-type,配置爲application/x-www-form-urlencoded的
// 意思是正文是urlencoded編碼過的form參數,下面我們可以看到我們對正文內容使用URLEncoder.encode
// 進行編碼
conn.setRequestProperty("Content-Type",
"application/x-www-form-urlencoded");
if(null != JSESSIONID){
conn.setRequestProperty("Cookie",JSESSIONID);
}
// 連接,從postUrl.openConnection()至此的配置必須要在connect之前完成,
// 要注意的是connection.getOutputStream會隱含的進行connect。
conn.connect();
DataOutputStream out = new DataOutputStream(conn.getOutputStream());
// The URL-encoded contend DataOutputStream.writeBytes將字符串中的16位的unicode字符以8位的字符形式寫道流裏面
out.writeBytes(content);
out.flush();
out.close(); // flush and close
//System.out.println(conn.getResponseMessage());
BufferedReader reader = new BufferedReader(new InputStreamReader(
conn.getInputStream(), "utf-8"));// 設置編碼,否則中文亂碼
String line;
StringBuilder sb = new StringBuilder();
while ((line = reader.readLine()) != null) {
sb.append(line);
}
String cookieval = conn.getHeaderField("set-cookie");
System.out.println("set-cookie:"+cookieval);
if(cookieval != null) {
JSESSIONID = cookieval.substring(0, cookieval.indexOf(";"));
}
reader.close();
conn.disconnect();
return sb.toString();
}
/**
* http Post請求 ,參考 {@link #requestPost(String,String) requestPost(String,String)}
*
* @param urlString
* 請求url
* @param map
* 封裝了正文內容的map
* @return str
* @throws IOException IOException
*
*/
public static String requestPost(String urlString, Map<String, String> map)
throws IOException {
if(map==null) return requestPost(urlString);
// 正文,正文內容其實跟get的URL中'?'後的參數字符串一致
// String content =
// "key=j0r53nmbbd78x7m1pqml06u2&type=1&[email protected]"
// + "&activatecode=" + URLEncoder.encode("中國聚龍", "utf-8");
String content = "";
for (Entry<String, String> en : map.entrySet()) {
String key = en.getKey();
String value = URLEncoder.encode(en.getValue(), "utf-8");
if (!content.isEmpty()) {
content += "&";
}
content += key + "=" + value;
}
return requestPost(urlString, content);
}
/**
* http Post請求,不帶content ,參考 {@link #requestPost(String,String) requestPost(String,String)}
* @param urlString 請求url
* @return str
* @throws IOException IOException
*/
public static String requestPost(String urlString)
throws IOException {
return requestPost(urlString, "");
}
/**
* http GET 請求
*
* @param url url
* @return str
* @throws IOException IOException
*/
public static String requestGet(String url) throws IOException {
URL getUrl = new URL(url);
// 根據拼湊的URL,打開連接,URL.openConnection函數會根據URL的類型,
// 返回不同的URLConnection子類的對象,這裏URL是一個http,因此實際返回的是HttpURLConnection
HttpURLConnection connection = (HttpURLConnection) getUrl
.openConnection();
connection.setConnectTimeout(6 * 1000);
// 進行連接,但是實際上get request要在下一句的connection.getInputStream()函數中才會真正發到
// 服務器
if(null != JSESSIONID){
connection.setRequestProperty("Cookie", JSESSIONID);
}
connection.connect();
// 取得輸入流,並使用Reader讀取
BufferedReader reader = new BufferedReader(new InputStreamReader(
connection.getInputStream(), "utf-8"));// 設置編碼,否則中文亂碼
String line;
StringBuilder sb = new StringBuilder();
while ((line = reader.readLine()) != null) {
sb.append(line);
}
String cookieval = connection.getHeaderField("set-cookie");
System.out.println("set-cookie:"+cookieval);
if(cookieval != null) {
JSESSIONID = cookieval.substring(0, cookieval.indexOf(";"));
}
reader.close();
// 斷開連接
connection.disconnect();
return sb.toString();
}
/**
* 讀取圖片
* @param url 網絡圖片url
* @param needStore 是否存儲 File dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
* @return Bitmap
* @throws IOException IOException
*/
public static Bitmap getBitmap(String url,boolean needStore,File dir) throws IOException {
// 獲得連接
HttpURLConnection conn = (HttpURLConnection) new URL(url)
.openConnection();
// 設置超時時間爲6000毫秒,conn.setConnectionTiem(0);表示沒有時間限制
conn.setConnectTimeout(6000);
// 連接設置獲得數據流
conn.setDoInput(true);
// 不使用緩存
conn.setUseCaches(false);
// 這句可有可無,沒有影響
conn.connect();
// 得到數據流
InputStream is = conn.getInputStream();
// 解析得到圖片
Bitmap bitMap = BitmapFactory.decodeStream(is);
if(needStore){
if(!dir.exists()){
dir.mkdir();
}
OutputStream os = new FileOutputStream(new File(dir,ApplicationHelper.getNetworkFileName(url)));
bitMap.compress(Bitmap.CompressFormat.JPEG,90,os);
os.flush();
os.close();
}
is.close();
return bitMap;
}
/**
* 下載視頻文件
* @param url url
* @param dir 下載目錄
* @throws IOException io
*/
public static void downloadVideo(String url,File dir) throws IOException {
// 獲得連接
HttpURLConnection conn = (HttpURLConnection) new URL(url)
.openConnection();
// 設置超時時間爲6000毫秒,conn.setConnectionTiem(0);表示沒有時間限制
conn.setConnectTimeout(6000);
// 連接設置獲得數據流
conn.setDoInput(true);
// 不使用緩存
conn.setUseCaches(false);
// 這句可有可無,沒有影響
conn.connect();
// 得到數據流
InputStream is = conn.getInputStream();
if(!dir.exists()){
dir.mkdir();
}
OutputStream os = new FileOutputStream(new File(dir,ApplicationHelper.getNetworkFileName(url)));
byte[] buff = new byte[4*1024];
int len = -1;
while ((len=is.read(buff))!=-1){
os.write(buff,0,len);
}
os.flush();
os.close();
is.close();
}
}
package com.jykj.departure.util;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningTaskInfo;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.Uri;
import android.os.Environment;
import android.util.Log;
import java.io.File;
import java.util.Calendar;
public class ApplicationHelper {
public static final CharSequence TOAST_NO_NET = "很遺憾,木有網絡";
/**
*
* @param context 上下文
* @param packageName 包名
* @return bool
*/
public static boolean checkApkInstalled(Context context, String packageName) {
if (packageName == null || packageName.isEmpty())
return false;
try {
context.getPackageManager().getApplicationInfo(packageName,
PackageManager.GET_UNINSTALLED_PACKAGES);
return true;
} catch (NameNotFoundException e) {
return false;
}
}
/**
* 檢查文件在默認下載目錄是否已存在
*
* @param fileName 文件名,如 xxx.apk
* @return 如果已經存在,則返回該文件的Uri,否則返回null
*/
public static Uri checkFileExist(String fileName) {
File dir =Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
File file = new File(dir, fileName);
if (file.exists()) {
return Uri.fromFile(file);
}
return null;
}
/**
*
* @param dir 目錄
* @param fileName 文件名稱
* @return Uri
*/
public static Uri checkFileExist(File dir,String fileName) {
if(dir==null) return null;
File file = new File(dir, fileName);
if (file.exists()) {
return Uri.fromFile(file);
}
return null;
}
//本應用的視頻存儲目錄
public static File getVideoDir(Context context){
return context.getExternalFilesDir(Environment.DIRECTORY_MOVIES);
}
//本應用的圖片存儲目錄
public static File getPicDir(Context context){
return context.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
}
//判斷某個文件是否爲apk文件
public static boolean isApkFile(String fileName){
fileName = fileName.toLowerCase();
int idx = fileName.lastIndexOf(".apk");
return idx >=0;
}
/**
* 獲得AndroidManifest.xml中的versionCode
*
* @param context 上下文
* @return int
*/
public static int getLocalVersionCode(Context context) {
try {
PackageInfo pi = context.getPackageManager().getPackageInfo(
context.getPackageName(), 0);
return pi.versionCode;
} catch (NameNotFoundException e) {
e.printStackTrace();
}
return 0;
}
/**
* 獲得AndroidManifest.xml中的versionName
*
* @param context 上下文
* @return str
*/
public static String getLocalVersionName(Context context) {
try {
PackageInfo pi = context.getPackageManager().getPackageInfo(
context.getPackageName(), 0);
return pi.versionName;
} catch (NameNotFoundException e) {
e.printStackTrace();
}
return null;
}
/**
* 安裝apk文件
*
* @param context 上下文
* @param uri uri
*/
public static void installApk(Context context, Uri uri) {
Intent installIntent = new Intent(Intent.ACTION_VIEW);
installIntent.setDataAndType(uri,
"application/vnd.android.package-archive");
installIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(installIntent);
}
/**
* 卸載apk程序
*
* @param context 上下文
* @param uri uri
*/
public static void uninstallApk(Context context, Uri uri, String packageName) {
Uri packageURI = Uri.parse("package:" + packageName);
Intent uninstallIntent = new Intent(Intent.ACTION_DELETE, packageURI);
context.startActivity(uninstallIntent);
}
//刪除某文件夾下所有文件
public static void deleteFile(File file) {
if (file.isDirectory()) {
File[] files = file.listFiles();
Log.e("WS","刪除的文件夾"+file.getAbsolutePath()+",數量:"+files.length);
for (File f :files){
deleteFile(f);
}
//file.delete();//如要保留文件夾,只刪除文件,請註釋這行
} else if (file.exists()) {
file.delete();
}
}
}
4.2.5 圖片下載任務DownloadPictureTask.java
package com.jykj.departure.task;
import android.app.ProgressDialog;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import com.jykj.departure.util.HttpHelper;
import java.io.File;
import java.io.IOException;
import java.util.Map;
public class DownloadPictureTask extends AsyncTask<Void, Void, Bitmap> {
private String mUrl;
private OnTaskFinishCallback mOnTaskFinishCallback;
private String exceptionInfo;
private boolean mStore;
private File mDir;
/**
*
* @param onTaskFinishCallback 成功執行任務的回調接口O
* @param url http url
* @param needStore 是否需要存儲
*/
public DownloadPictureTask(OnTaskFinishCallback onTaskFinishCallback, @NonNull String url, boolean needStore, File dir) {
mOnTaskFinishCallback = onTaskFinishCallback;
mUrl = url;
mStore = needStore;
mDir = dir;
}
@Override
protected Bitmap doInBackground(Void... params) {
try {
return HttpHelper.getBitmap(mUrl,mStore,mDir);
} catch (IOException e) {
e.printStackTrace();
exceptionInfo = e.getMessage();
return null;
}
}
@Override
protected void onPostExecute(Bitmap result) {
mOnTaskFinishCallback.onTaskFinish(result,
exceptionInfo);
}
/**
* 任務執行完成的回調接口,用於將執行結果返回給調用者
*/
public interface OnTaskFinishCallback {
/**
* 任務執行完成的回調接口,用於將執行結果返回給調用者
* @param result if null 則表示任務執行出現異常
* @param exceptionInfo 出現異常時的異常信息,否則爲null
*/
void onTaskFinish(Bitmap result, String exceptionInfo);
}
}
如果覺得上面的代碼太過於繁瑣,不妨先實現一種消息類型如文本的,後面再慢慢加。
4.2.6 客戶端
開發過程中肯定是要邊調試邊開發的,這邊省略了客戶端的開發,可以從網絡上直接下載一大堆的TCP調試工具。下面推薦使用的一款爲SocketTestDlg:
SocketTestDlg官網下載:SocketTestDlg官網下載地址
針對本應用,採用16進制發送消息,然後需要一個將utf8轉16進制的工具
Utf8ToHex下載:Utf8ToHex下載地址
或者Utf8ToHex下載地址二
下面是一些消息例子
1)普通通知:2001|3|5|2|0|簡單的通知hello時間
02\x32\x30\x30\x31\x7C\x33\x7C\x35\x7C\x32\x7C\x30\x7C\xE7\xAE\x80\xE5\x8D\x95\xE7\x9A\x84\xE9\x80\x9A\xE7\x9F\xA5\x68\x65\x6C\x6C\x6F\xE6\x97\xB6\xE9\x97\xB403
2)圖片臨時通知:2071|1|10|2|1|http://p4.so.qhmsg.com/t01c8f12eb94284bb19.jpg
02\x32\x30\x37\x31\x7C\x31\x7C\x31\x30\x7C\x32\x7C\x31\x7C\x68\x74\x74\x70\x3A\x2F\x2F\x70\x34\x2E\x73\x6F\x2E\x71\x68\x6D\x73\x67\x2E\x63\x6F\x6D\x2F\x74\x30\x31\x63\x38\x66\x31\x32\x65\x62\x39\x34\x32\x38\x34\x62\x62\x31\x39\x2E\x6A\x70\x6703
3)視頻臨時通知:2051|1|2|0|http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4
02\x32\x30\x35\x31\x7C\x31\x7C\x32\x7C\x30\x7C\x68\x74\x74\x70\x3A\x2F\x2F\x63\x6C\x69\x70\x73\x2E\x76\x6F\x72\x77\x61\x65\x72\x74\x73\x2D\x67\x6D\x62\x68\x2E\x64\x65\x2F\x62\x69\x67\x5F\x62\x75\x63\x6B\x5F\x62\x75\x6E\x6E\x79\x2E\x6D\x70\x3403
《道德經》第二章:
天下皆知美之爲美,惡已;皆知善,斯不善矣。有無之相生也,難易之相成也,長短之相刑也,高下之相盈也,音聲之相和也,先後之相隨,恆也。是以聖人居無爲之事,行不言之教,萬物作而弗始也,爲而弗志也,成功而弗居也。夫唯弗居,是以弗去。
譯文: 天下人都知道美之所以爲美,那是由於有醜陋的存在。都知道善之所以爲善,那是因爲有惡的存在。所以有和無互相轉化,難和易互相形成,長和短互相顯現,高和下互相充實,音與聲互相諧和,前和後互相接隨——這是永恆的。因此聖人用無爲的觀點對待世事,用不言的方式施行教化:聽任萬物自然興起而不爲其創始,有所施爲,但不加自己的傾向,功成業就而不自居。正由於不居功,就無所謂失去。