Android 基於TCP協議的Socket編程(自定義協議)

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

《道德經》第二章:
天下皆知美之爲美,惡已;皆知善,斯不善矣。有無之相生也,難易之相成也,長短之相刑也,高下之相盈也,音聲之相和也,先後之相隨,恆也。是以聖人居無爲之事,行不言之教,萬物作而弗始也,爲而弗志也,成功而弗居也。夫唯弗居,是以弗去。
譯文: 天下人都知道美之所以爲美,那是由於有醜陋的存在。都知道善之所以爲善,那是因爲有惡的存在。所以有和無互相轉化,難和易互相形成,長和短互相顯現,高和下互相充實,音與聲互相諧和,前和後互相接隨——這是永恆的。因此聖人用無爲的觀點對待世事,用不言的方式施行教化:聽任萬物自然興起而不爲其創始,有所施爲,但不加自己的傾向,功成業就而不自居。正由於不居功,就無所謂失去。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章