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 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 picsList=new ArrayList<>();//图片通知
    private List videosList=new ArrayList<>();//视频通知
    private List 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;iif(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;iif(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 set =  spLongterm.getStringSet(SocketHelper.SP_KEY_LONGTERM_PICTURES,new HashSet());
                        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;iif(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 map)
            throws IOException {
        if(map==null) return requestPost(urlString);
        // 正文,正文内容其实跟get的URL中'?'后的参数字符串一致
        // String content =
        // "[email protected]"
        // + "&activatecode=" + URLEncoder.encode("中国聚龙", "utf-8");
        String content = "";
        for (Entry 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

《道德经》第二章:
天下皆知美之为美,恶已;皆知善,斯不善矣。有无之相生也,难易之相成也,长短之相刑也,高下之相盈也,音声之相和也,先后之相随,恒也。是以圣人居无为之事,行不言之教,万物作而弗始也,为而弗志也,成功而弗居也。夫唯弗居,是以弗去。
译文: 天下人都知道美之所以为美,那是由于有丑陋的存在。都知道善之所以为善,那是因为有恶的存在。所以有和无互相转化,难和易互相形成,长和短互相显现,高和下互相充实,音与声互相谐和,前和后互相接随——这是永恒的。因此圣人用无为的观点对待世事,用不言的方式施行教化:听任万物自然兴起而不为其创始,有所施为,但不加自己的倾向,功成业就而不自居。正由于不居功,就无所谓失去。

你可能感兴趣的:(Socket)