全网最全的Java Socket通讯例子

一.前言

Socket通讯在银行、图书馆,物联网应用较多,日常都是Http/Https居多。网上关于Java的比较完整的Socket编程例子屈指可数,参考价值不大。要么是短连接且只支持纯文本通讯;要么是短连接且只支持文件通讯;要么是一个短连接文本通讯和一个短连接文件通讯;前面这些基本是单向通讯的短连接例子,而且长连接的例子比较少,主要存在如下特点:残缺,排版错乱,容易引起读者走火入魔。当然也有比较著名的socket框架,如Netty。但这些框架高度封装,对于入门和理解Socket基础编程,未免显得吃力。本文介绍Java Socket(长连接)原始通讯例子,可基于例子自由扩展和完善,例子主要有以下特点:

  1. 自定义数据报文包格式,解决粘包问题
  2. 全双工通讯,客户端和服务端互发消息 ,互相监听消息包
  3. 支持文本和大文件通讯
  4. 引入心跳机制,主要保持连接,因为设备在网络通讯之间有大量中间设备,无法直接通过判断socket的连接状态判断设备连接

介绍之前先简单说下socket相关的几个概念知识

二.概念知识

通信模式

单工,就是两者通信单向进行,只能一个主动发信号一个被动去接受,不能角色互换。
举例:行人只能接受红绿灯的信号但是不能向红绿灯发信号,红绿灯只能发出信号不能接收信号。

半双工,两个事物都可以发信号,但是不能同时进行。
举例:类似于踢足球,只能一个传给另一个人,两个人不能同时传球,球只有一个,信道只有一个。

全双工,两个事物可以同时发送和接受信息。
举例:两个人互相打电话,你可以说也可以听电话。在Java里套接字Socket就是全双工的

三种通讯模式如下图:
在这里插入图片描述

套接字Socket和Socket编程方式

Socket是应用层与TCP/IP协议族(Socket处于TCP/IP五层模型中的传输层)通信的中间软件抽象层,它是一组接口。平时Socket编程都是使用soket接口来实现自己的业务和协议。
Socket编程有两个典型的接收发送方式:轮询方式和select侦听及管道中断方式

我们平时都是采用轮询阻塞方式创建socket,本篇是采用轮询方式演示例子。
其工作流程结构图如下:
在这里插入图片描述

Socket长连接和短连接

Socket短连接,连接一次,进行一次读写操作,然后关闭socekt;
Socket长连接,连接一次,进行多次读写数据,进程退出时或不需要时关闭socekt

上面毫无疑问是长连接占用资源更少,效率更高。

Socket时间参数设置

客户端连接超时时间:

Socket s=new Socket(); 
s.connect(new InetSocketAddress("127.0.0.1",8090),10000);

不设置连接超时时间的情况下,Socket 默认大概是20s连接超时

客户端读超时时间:

Socket s=new Socket("127.0.0.1",8090);
s.setSoTimeout(10000);

不设置setSoTimeout,默认120s超时

上面设置时间情况,皆抛出异常SocketTimeoutException,但是第一种连接超时,出现这种异常一般是ip或者port填错(类似打电话过去每没有人接)。
而第二读取数据超时,说明连接成功了,跟服务端通讯read超时(类似接通了电话,对方不说话挂掉)。

Socket读取阻塞问题

参考我之前写的文章:https://blog.csdn.net/u011082160/article/details/100779231

Socket粘包问题

粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。本次通过自定义数据数据包和读写互斥解决粘包问题

说了这么多概念知识,开始正式入门Socket编程。

三.Socket编程

1.数据包定义

通常实际socket通讯中,需要定义一个数据交换格式,约定客户端和服务端的数据包格式,解决粘包问题,易于人阅读和编写,排除问题,易于机器解析和生成,并有效地提升网络传输效率。

约定数据包格式如下:

数据包=类型+标志+命令+包序列号+消息体+结束符

类型: 发送文本或文本和文件,占用1字节
标志: 0客服端发起的请求,1服务端发起的请求,响应请求时把flag带回去
命令: 由用户定义,如100表示登陆,101表示注册之类,响应请求时把cmd带回去
包序列号: 数据包的唯一索引,响应请求时把seq带回去
消息体: 通讯数据
结束符: 表示报文结束标志,本次使用换行符‘’ \n‘’
其中消息体分为两大类:

1.纯文本:

 包类型byte,标志byte,命令int,包序列号int,文本长度 int,文本体 byte[],结束符 byte

2.文本和文件:

 包类型byte,标志byte,命令int,包序列号int,文本长度 int,文本体 byte[],分隔符 byte,文件名长度 int,文件名byte[],
 分隔符 byte,文件数据长度 long,文件数据 byte[],结束符 byte

分隔符:分割文本和文件的标志,本次使用 ‘’?‘’

对应DataPacket下的代码:

  public class DataPacket {
    public static final String ENCODE = "UTF-8";

    public static final byte CLIENT_REQUEST = 0x00;
    public static final byte SERVER_REQUEST = 0x01;

    /**
     * 报文类型
     */
    public byte dataType;
    /**
     * 标志:0客服端发起的请求,1服务端发起的请求,响应请求时把flag带回去
     */
    public byte flag;
    /**
     * 命令,响应请求时把cmd带回去
     */
    public int cmd;

    /**
     * 包序列号,数据包的唯一索引,响应请求时把seq带回去
     */
    public int seq;

    /**
     * 长度
     */
    public int textLength;
    /**
     * 文本体
     */
    public byte[] textData;


    /**
     * 文件分隔符
     */
    public static final byte Spilt = (byte) 0x63;


    /**
     * 文件名长度
     */
    public int fileNameLength;
    /**
     * 文件名
     */
    public byte[] fileNameData;

    /**
     * 长度
     */
    public long fileLength;

    /**
     * 文件,暂时支持单个文件传输
     */
    public File file;
    /**
     * 暂时不需要,因为边读编写,一次写文件容易oom
     */
    public byte[] fileData;
    /**
     * 结束符
     */
    public byte end;

    /**
     * 报文结束符
     */
    public static final byte End = (byte) 0xA;
	// ....
}

2.数据包进一步封装

上层可以直接通过DataPacket组包发送,但这样过于繁琐,可以进步封装上层数据调用,定义请求DataReq,如下:

public abstract class DataReq {
    public abstract DataType geDataType();

    public Object data;
    public File file;
    public String fileName;


    /**
     * 1已经占用,作为心跳的命令号
     *
     * @return
     */
    public abstract int getCmd();

    /**
     * 报文序列号,调用一次增加一次
     *
     * @return
     */
    public abstract int getSeq();


    public DataReq(Object data) {
        this.data = data;
    }

    public DataReq(Object data, File file, String fileName) {
        this.data = data;
        this.file = file;
        this.fileName = fileName;
    }


}


其中data对应消息体的文本,file对应消息体的文件,两者是否有值看发送的数据类型DataType

public enum DataType {
    TEXT_ONLY((byte) 0x01), FILE_AND_TEXT((byte) 0x02),HEARTBEAT((byte) 0x99);
    //...
}

命令号cmd、序列号seq则由用户定义。根据数据包定义数据包=类型+标志+命令+包序列号+消息体+结束符,还差标志,结束符,但这两个是内定的,这样一个数据包定义好了。

下面开始简单封装两端通讯流程,提供应用层若干API,简化应用层调用

2.服务端开发

关键类:SimpleServer

2.1启动服务并绑定端口,阻塞监听客户端连接到来

 public void startup() {
     running = false;
     serverReceiverThread = new Thread(new MyServerThread());
     serverReceiverThread.start();
 }
 
private class MyServerThread implements Runnable {


        public void run() {
            try {
              //绑定端口
                serverSocket = new ServerSocket(port);
                ConsoleUtils.i("Server is running");
                running = true;
                if (serverListener != null) {
                    serverListener.onStarted();
                }
                while (true) {
                    if (!serverSocket.isClosed()) {
                    	//阻塞监听客户端连接
                        Socket socket = serverSocket.accept();
    					//客户端连接成功
                        ConsoleUtils.i("client is connected");
                        InetAddress inetAddress = socket.getInetAddress();
                        String ip = inetAddress.getHostAddress();
                        //inet address: 127.0.0.1
                        ConsoleUtils.i("inet address: " + inetAddress.toString());
                        ServerClient oldServerClient = getClientConnection(ip);
                        if (oldServerClient != null && oldServerClient.isConnected()) {
                            oldServerClient.disconnect();
                        }
                        ServerClient serverClient = new ServerClient(socket, ip);
                        Thread clientThread = new Thread(serverClient);
                        clientThread.start();
                        //同一个客户端ip,只保留一个最新的长连接
                        clientConnectionList.put(ip, serverClient);

                    }

                }
            } catch (Exception e) {
                ConsoleUtils.e("服务端监听异常", e);
                running = false;
                if (serverListener != null) {
                    serverListener.onException(e);
                }
            }
        }
    }

2.2创建服务端和客户端数据通讯线程

在独立的线程中,进行服务端和客户端数据交互,不影响服务端监听其它客户端连接的到来。

  /**
     * ServerClient即客户端连接服务端的Client
     */
    public class ServerClient implements Runnable {
        private Socket socket;
        private String ip;
        private DataOutputStream out;
        private Object obj = new Object();
        private Map<Integer, DataPacket> mDataPacketList = new ConcurrentHashMap<>();
        /**
         * 数据回调
         */
        private Map<Integer, DataResponseListener> mDataCallbacks = new ConcurrentHashMap<>();

        public ServerClient(Socket socket, String ip) {
            this.socket = socket;
            this.ip = ip;
        }

        @Override
        public void run() {
            try {
                OutputStream os = socket.getOutputStream();
                out = new DataOutputStream(os);
                parseDataPacket(socket);
            } catch (Exception e) {
                ConsoleUtils.e("客服端连接服务端中断", e);
            } finally {
                disconnect();
            }
        }

        public void disconnect() {
            if (socket != null && !socket.isClosed()) {
                try {
                    socket.close();
                } catch (IOException e) {
//                    e.printStackTrace();
                }
            }
        }

        public boolean isConnected() {
            if (socket != null && socket.isConnected() && !socket.isClosed()) {
                return true;
            }
            return false;
        }
        //...
}
2.3服务端阻塞监听客户端发来的数据

parseDataPacket方法主要负责解析客户端发来的数据包,主要解析三大类数据类型:

  1. 客户端发来的纯文本数据
  2. 客户端发来的文本和文件的混合数据
  3. 客户端向服务端保持连接的心跳数据
   /**
         * 解析数据包
         *
         * @param client
         * @throws IOException
         */
        private synchronized void parseDataPacket(Socket client) throws IOException {
            InputStream ins = client.getInputStream();
            DataInputStream inputStream = new DataInputStream(ins);
            //服务端解包过程
            while (true) {
                //读取cmd
                byte type = inputStream.readByte();
                if (type == DataPacket.End) {
                    continue;
                }
                DataType dataType = DataType.parseType(type);
                if (dataType != null) {
                    DataPacket responsePacket = new DataPacket();
                    switch (dataType) {
                        case TEXT_ONLY: {
                            //解析文本
                            responsePacket.readText(type, inputStream);
                            ConsoleUtils.i("====文本end");
                            handleResponse(responsePacket);
                            break;
                        }
                        case FILE_AND_TEXT: {
                            //解析文本
                            responsePacket.readTextAndFile(type, inputStream, fileSaveDir);
                            ConsoleUtils.i("文件大小:" + responsePacket.fileLength + ",格式化大小:" + FileUtils.formatFileSize(responsePacket.fileLength)
                                    + ",文件写入成功:" + responsePacket.file.getName());
                            ConsoleUtils.i("====文本和文件end");

                            handleResponse(responsePacket);
                            break;
                        }
                        case HEARTBEAT: {
                            responsePacket.readHeart(type, inputStream);
                            ConsoleUtils.i("====收到客服端心跳end");
                            handleResponse(responsePacket);
                            break;
                        }
                        default:
                            break;
                    }
                } else {
                    ConsoleUtils.e("客服端非法消息type:" + type);
                }

            }

        }

解析数据成功后,服务端会立马给客户端发送一个应答数据包,证明服务端接收成功

 private void handleResponse(DataPacket responsePacket) {
            byte flag = responsePacket.flag;
            int seq = responsePacket.seq;
            byte end = responsePacket.end;
            if (end == DataPacket.End) {
                if (flag == REQUEST_FLAG) { //C<-S
                    DataPacket requestPacket = mDataPacketList.get(seq);
                    callbackServerRequestResult(requestPacket, responsePacket);
                } else {
                    //C->S
                    final DataPacket requestPacket = responsePacket;
                    DataPacket replyResponsePacket = replyData(requestPacket, DataRes.SUCCESS, "Success");
                    callbackClientRequestResult(requestPacket, replyResponsePacket);
                }
            } else {
                replyData(responsePacket, DataRes.FAIL, "Fail");
            }
        }

        /**
         * 应答数据
         *
         * @param requestDataPacket
         * @param code
         * @param msg
         * @return
         */
        private DataPacket replyData(DataPacket requestDataPacket, int code, String msg) {
            try {
                synchronized (obj) {
                    DataRes dataRes = getDataRes();
                    dataRes.code = code;
                    dataRes.msg = msg;
                    String text = gson.toJson(dataRes);
                    byte[] dataByte = text.getBytes(DataPacket.ENCODE);
                    byte dataType = requestDataPacket.dataType;
                    if (requestDataPacket.dataType != DataType.HEARTBEAT.getDataType()) {
                        dataType = DataType.TEXT_ONLY.getDataType();
                    }
                    DataPacket responsePacket = new DataPacket(dataType, requestDataPacket.flag, requestDataPacket.cmd, requestDataPacket.seq, dataByte);
                    responsePacket.writeText(out);
                    return responsePacket;
                }
            } catch (Exception e) {
                ConsoleUtils.e("应答异常", e);
            }

            return null;
        }
2.4服务端往客户端推送数据

public boolean pushDataTpClient(String clientIp, DataReq dataReq) {
        SimpleServer.ServerClient clientConnectionList = getClientConnection(clientIp);
        if (clientConnectionList != null) {
            try {
                clientConnectionList.sendData(dataReq);
                return true;
            } catch (Exception e) {
                ConsoleUtils.e("推送-->" + clientIp + "失败", e);
            }
        }
        return false;

    }

    /**
     * 异步推送数据到指定客户端
     *
     * @param clientIp
     * @param dataReq
     * @param dataResponseListener
     */
    public void pushDataTpClient(String clientIp, DataReq dataReq, DataResponseListener dataResponseListener) {
        SimpleServer.ServerClient clientConnectionList = getClientConnection(clientIp);
        if (clientConnectionList != null) {
            clientConnectionList.sendData(dataReq, dataResponseListener);
        } else {
            if (dataResponseListener != null) {
                dataResponseListener.sendOnError(dataReq.getCmd(), new RuntimeException("客户端未连接,推送失败"));
            }
        }
    }

    public void pushDataTpAllClient(DataReq dataReq) {
        Map<String, ServerClient> clientConnectionList = getClientConnectionList();
        for (Map.Entry<String, ServerClient> entry : clientConnectionList.entrySet()) {
            String key = entry.getKey();
            ServerClient serverClient = entry.getValue();
            if (serverClient.isConnected()) {
                try {
                    serverClient.sendData(dataReq);
                } catch (Exception e) {
                    ConsoleUtils.e("推送-->" + key + "失败", e);
                }
            }

        }
    }
    

上面数据推送,支持指定客户端推送,推送所有客户端,数据发送都调用了方法sendData

发送数据和接收数据一样,也是支持三大类数据类型

 /**
     * 异步发送数据,带响应数据
     *
     * @param dataReq
     * @param dataResponseListener
     */
    public void sendData(DataReq dataReq, DataResponseListener dataResponseListener) {
        if (dataResponseListener == null) {
            return;
        }

        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    int seq = sendData(dataReq);
                    mDataCallbacks.put(seq, dataResponseListener);
                    //10s等待服务端,返回信息
                    int readWaitTimeout = 10 * 1000;
                    long start = System.currentTimeMillis();
                    boolean success = false;
                    while (System.currentTimeMillis() - start <= readWaitTimeout) {
                        DataResponseListener temp = mDataCallbacks.get(seq);
                        if (temp == null) {
                            //说明被移除成功,证明接收成功
                            success = true;
                            break;
                        }
                    }
                    if (!success) {
                        mDataPacketList.remove(seq);
                        mDataCallbacks.remove(seq);
                        dataResponseListener.sendOnError(dataReq.getCmd(), new RuntimeException("超时读取数据"));
                    }
                } catch (Exception e) {
                    dataResponseListener.sendOnError(dataReq.getCmd(), e);
                }

            }
        });
    }
    
   /**
     * @param dataReq
     * @return 返回报文的序列号
     * @throws Exception
     */
    public int sendData(DataReq dataReq) throws Exception {
        if (dataReq == null) {
            throw new NullPointerException("dataReq为空");
        }
        synchronized (obj) {
            DataType dataType = dataReq.geDataType();
            if (out == null) {
                ConsoleUtils.e("未连接服务端");
                return 0;
            }
            if (dataType == DataType.TEXT_ONLY) {
                return sendTextData(out, dataType.getDataType(), dataReq);
            } else if (dataType == DataType.FILE_AND_TEXT) {
                return sendTextAndFileData(out, dataType.getDataType(), dataReq);
            } else if (dataType == DataType.HEARTBEAT) {
                return sendHeartData(out, DataType.HEARTBEAT.getDataType(), dataReq);
            } else {
                ConsoleUtils.e("不支持命令:" + dataType);
                throw new IllegalArgumentException("非法数据包发送");
            }
        }

    }

    private int sendHeartData(DataOutputStream out, byte dataType, DataReq dataReq) throws Exception {
        DataPacket dataPacket = new DataPacket(dataType, REQUEST_FLAG, dataReq.getCmd(), dataReq.getSeq());
        dataPacket.sendHeart(out);
        cacheMsg(dataPacket);
        return dataPacket.seq;
    }

    private void cacheMsg(DataPacket dataPacket) {
        mDataPacketList.put(dataPacket.seq, dataPacket);
    }

    private int sendTextData(DataOutputStream out, byte dataType, DataReq dataReq) throws Exception {
        String text = gson.toJson(dataReq.data);
        ConsoleUtils.i("发送文本:" + text);
        byte[] dataByte = text.getBytes(DataPacket.ENCODE);
        DataPacket dataPacket = new DataPacket(dataType, REQUEST_FLAG, dataReq.getCmd(), dataReq.getSeq(), dataByte);
        dataPacket.writeText(out);
        cacheMsg(dataPacket);
        return dataPacket.seq;
    }


    private int sendTextAndFileData(DataOutputStream out, byte dataType, DataReq dataReq) throws Exception {
        File file = dataReq.file;
        if (file.exists() && file.isFile()) {
            String text = gson.toJson(dataReq.data);
            ConsoleUtils.i("发送文本:" + text);
            byte[] dataByte = text.getBytes(DataPacket.ENCODE);
            byte[] fileNameData = dataReq.fileName.getBytes(DataPacket.ENCODE);
            DataPacket dataPacket = new DataPacket(dataType, REQUEST_FLAG, dataReq.getCmd(), dataReq.getSeq(), dataByte, fileNameData, file);
            dataPacket.writeTextAndFile(out);
            cacheMsg(dataPacket);
            return dataPacket.seq;
        } else {
            throw new IllegalArgumentException("不存在该文件或不是文件,发送失败:" + dataReq.toString());
        }
    }

备注:发送文件时边读编写,不是一次性发送整个文件的字节数据,原因是发送超大文件容易oom。

2.5服务端关闭

服务端关闭会断开服务端与所有客户端的连接

 public void shutdown() {
        running = false;
        for (Map.Entry<String, ServerClient> entry : clientConnectionList.entrySet()) {
            String key = entry.getKey();
            ServerClient serverClient = entry.getValue();
            serverClient.disconnect();
            ConsoleUtils.i("断开-->" + key + "连接");
        }
        if (serverSocket != null && !serverSocket.isClosed()) {
            try {
                serverSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (serverListener != null) {
            serverListener.onStopped();
        }
    }

3.客户端开发

客户端流程和服务端基本相同,只不过服务端是监听n个客户端连接,客户端只能连接一个服务端(不同客户端和服务端除外)。
客户端的代码和服务端大部分相同,尤其是通讯部分,本次为了说明流程,不做抽取,方便理解。
服务端写好了,现在开始写客户端流程。

3.1客户端绑定IP和端口,连接服务端

连接也是阻塞的,连接成功才能拿到OutputStreamInputStream ,连接成功后,主要做两方面的工作,一是保持与服务端连接的定时心跳数据发送,二是阻塞监听服务端发来的数据。


    public void connect() {
        if (isRunning()) {
            if (clientListener != null) {
                clientListener.onStarted();
            }
        } else {
            running = false;
            MyClient myClient = new MyClient();
            Thread thread = new Thread(myClient);
            thread.start();
        }
    }
  public class MyClient implements Runnable {


        @Override
        public void run() {
            try {
                socket = new Socket(ip, port);
                //连接成功
                running = true;

                OutputStream os = socket.getOutputStream();
                out = new DataOutputStream(os);
                if (clientListener != null) {
                    clientListener.onStarted();
                }
                //启动心跳
                startHeartbeat();
                parseDataPacket(socket);
            } catch (Exception e) {
                ConsoleUtils.e("客户端IO异常", e);
                if (clientListener != null) {
                    clientListener.onException(e);
                }
            } finally {
                disconnect();
            }
        }


    }

3.2客户端阻塞监听服务端发来的数据

parseDataPacket方法主要负责解析服务端发来的数据包,主要解析三大类数据类型:

  1. 服务端发来的纯文本数据
  2. 服务端发来的文本和文件的混合数据
  3. 服务端应答客户端的心跳数据
/**
     * 解析服务端数据包
     *
     * @param server
     * @throws IOException
     */
    private synchronized void parseDataPacket(Socket server) throws IOException {
        InputStream ins = server.getInputStream();
        DataInputStream inputStream = new DataInputStream(ins);
        while (true) {
            //读取cmd
            byte type = inputStream.readByte();
            if (type == DataPacket.End) {
                continue;
            }
            DataType dataType = DataType.parseType(type);
            if (dataType != null) {
                DataPacket responsePacket = new DataPacket();
                switch (dataType) {
                    case TEXT_ONLY: {
                        //解析文本
                        responsePacket.readText(type, inputStream);
                        ConsoleUtils.i("====文本end");
                        handleResponse(responsePacket);
                        break;
                    }
                    case FILE_AND_TEXT: {
                        //解析文本
                        responsePacket.readTextAndFile(type, inputStream, fileSaveDir);
                        ConsoleUtils.i("文件大小:" + responsePacket.fileLength + ",格式化大小:" + FileUtils.formatFileSize(responsePacket.fileLength)
                                + ",文件写入成功:" + responsePacket.file.getName());
                        ConsoleUtils.i("====文本和文件end");
                        handleResponse(responsePacket);
                        break;
                    }
                    case HEARTBEAT: {
                        //解析服务端应答包
                        responsePacket.readText(type, inputStream);
                        ConsoleUtils.i("收到服务端心跳应答end");
                        handleResponse(responsePacket);
                        break;
                    }
                    default:
                        break;
                }
            } else {
                ConsoleUtils.e("服务端非法消息type:" + type);
            }

        }
    }

解析数据成功后,客户端会立马给服务端发送一个应答数据包,证明客户端接收成功

    private void handleResponse(DataPacket responsePacket) {
        byte flag = responsePacket.flag;
        int seq = responsePacket.seq;
        byte end = responsePacket.end;
        if (end == DataPacket.End) {
            if (flag == REQUEST_FLAG) { //C->S
                DataPacket requestPacket = mDataPacketList.get(seq);
                callbackClientRequestResult(requestPacket, responsePacket);
            } else {
                //C<-S
                final DataPacket requestPacket = responsePacket;
                DataPacket replyResponsePacket = replyData(requestPacket, DataRes.SUCCESS, "Success");
                callbackServerRequestResult(requestPacket, replyResponsePacket);
            }
        } else {
            //异常应答,不回调callbackResult
            replyData(responsePacket, DataRes.FAIL, "Fail");
        }
    }


    /**
     * 应答数据
     *
     * @param requestDataPacket
     * @param code
     * @param msg
     * @return
     */
    private DataPacket replyData(DataPacket requestDataPacket, int code, String msg) {
        try {
            synchronized (obj) {
                DataRes dataRes = getDataRes();
                dataRes.code = code;
                dataRes.msg = msg;
                String text = gson.toJson(dataRes);
                byte[] dataByte = text.getBytes(DataPacket.ENCODE);
                DataPacket responsePacket = new DataPacket(DataType.TEXT_ONLY.getDataType(), requestDataPacket.flag, requestDataPacket.cmd, requestDataPacket.seq, dataByte);
                responsePacket.writeText(out);
                return responsePacket;
            }
        } catch (Exception e) {
            ConsoleUtils.e("应答异常", e);
        }
        return null;
    }

3.3客户端往服务端发送数据

发送数据和接收数据一样,也是支持三大类数据类型,其中一种是定时心跳数据,另外两种数据是文本数据、文本和文件数据

/**
     * 异步发送数据
     *
     * @param dataReq
     * @param dataResponseListener
     */
    public void sendData(DataReq dataReq, DataResponseListener dataResponseListener) {
        if (dataResponseListener == null) {
            return;
        }

        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    int seq = sendData(dataReq);
                    mDataCallbacks.put(seq, dataResponseListener);
                    //10s等待服务端,返回信息
                    int readWaitTimeout = 10 * 1000;
                    long start = System.currentTimeMillis();
                    boolean success = false;
                    while (System.currentTimeMillis() - start <= readWaitTimeout) {
                        DataResponseListener temp = mDataCallbacks.get(seq);
                        if (temp == null) {
                            //说明被移除成功,证明接收成功
                            success = true;
                            break;
                        }
                    }
                    if (!success) {
                        mDataPacketList.remove(seq);
                        mDataCallbacks.remove(seq);
                        dataResponseListener.sendOnError(dataReq.getCmd(), new RuntimeException("超时读取数据"));
                    }
                } catch (Exception e) {
                    dataResponseListener.sendOnError(dataReq.getCmd(), e);
                }

            }
        });
    }


    /**
     * @param dataReq
     * @return 返回报文的序列号
     * @throws Exception
     */
    public int sendData(DataReq dataReq) throws Exception {
        if (dataReq == null) {
            throw new NullPointerException("dataReq为空");
        }
        synchronized (obj) {
            DataType dataType = dataReq.geDataType();
            if (out == null) {
                ConsoleUtils.e("未连接服务端");
                return 0;
            }
            if (dataType == DataType.TEXT_ONLY) {
                return sendTextData(out, dataType.getDataType(), dataReq);
            } else if (dataType == DataType.FILE_AND_TEXT) {
                return sendTextAndFileData(out, dataType.getDataType(), dataReq);
            } else if (dataType == DataType.HEARTBEAT) {
                return sendHeartData(out, DataType.HEARTBEAT.getDataType(), dataReq);
            } else {
                ConsoleUtils.e("不支持命令:" + dataType);
                throw new IllegalArgumentException("非法数据包发送");
            }
        }

    }

    private int sendHeartData(DataOutputStream out, byte dataType, DataReq dataReq) throws Exception {
        DataPacket dataPacket = new DataPacket(dataType, REQUEST_FLAG, dataReq.getCmd(), dataReq.getSeq());
        dataPacket.sendHeart(out);
        cacheMsg(dataPacket);
        return dataPacket.seq;
    }

    private int sendTextData(DataOutputStream out, byte dataType, DataReq dataReq) throws Exception {
        String text = gson.toJson(dataReq.data);
        ConsoleUtils.i("发送文本:" + text);
        byte[] dataByte = text.getBytes(DataPacket.ENCODE);
        DataPacket dataPacket = new DataPacket(dataType, REQUEST_FLAG, dataReq.getCmd(), dataReq.getSeq(), dataByte);
        dataPacket.writeText(out);
        cacheMsg(dataPacket);
        return dataPacket.seq;
    }


    private int sendTextAndFileData(DataOutputStream out, byte dataType, DataReq dataReq) throws Exception {
        File file = dataReq.file;
        if (file.exists() && file.isFile()) {
            String text = gson.toJson(dataReq.data);
            ConsoleUtils.i("发送文本:" + text);
            byte[] dataByte = text.getBytes(DataPacket.ENCODE);
            byte[] fileNameData = dataReq.fileName.getBytes(DataPacket.ENCODE);
            DataPacket dataPacket = new DataPacket(dataType, REQUEST_FLAG, dataReq.getCmd(), dataReq.getSeq(), dataByte, fileNameData, file);
            dataPacket.writeTextAndFile(out);
            cacheMsg(dataPacket);
            return dataPacket.seq;
        } else {
            throw new IllegalArgumentException("不存在该文件或不是文件,发送失败:" + dataReq.toString());
        }
    }

备注:发送文件时也是边读编写,原因同服务端一样。

通过上面发现,两端通讯数据发和收流程基本一样,只是数据来源不一样

3.3.1客户端定时往发送服务端发送心跳数据

心跳数据包比较简单,由cmd为0x01,序号-1自减组成,当然也可以定义别的。

 private void startHeartbeat() {
        timer = new Timer();
        DataReq dataReq = new DataReq("heart") {
            @Override
            public DataType geDataType() {
                return DataType.HEARTBEAT;
            }

            @Override
            public int getCmd() {
                return 0x01;
            }

            @Override
            public int getSeq() {
                int andDecrement = ai.getAndDecrement();
                if (andDecrement == Integer.MIN_VALUE) {
                    ai.set(-1);
                }
                return andDecrement;
            }
        };
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                if (!running) {
                    return;
                }
                if (isConnected()) {
                    try {
                        sendData(dataReq);
                    } catch (Exception e) {
                        ConsoleUtils.e("心跳异常", e);
                    }
                }
            }
        }, 1000, HEART_TIME);
    }

3.4客户端断开与服务端的连接

断开连接会停止心跳发送,套接字关闭。

   public void disconnect() {
        running = false;
        stopHeartbeat();
        if (socket != null && !socket.isClosed()) {
            try {
                socket.close();
            } catch (IOException e) {
//                e.printStackTrace();
            }
        }
        if (clientListener != null) {
            clientListener.onStopped();
        }
    }

4.数据发送和读取

上面数据的发送和读取都封装在DataPacket类里面,发送和读取都按数据包格式处理

public class DataPacket {
    
	//...
	
    public void sendHeart(DataOutputStream out) throws IOException {
        //占用1个字节
        out.writeByte(dataType);
        out.writeByte(flag);
        out.writeInt(cmd);
        out.writeInt(seq);
        //结束符,占用1个字节
        out.writeByte(End);
        out.flush();
    }

    public void writeText(DataOutputStream out) throws IOException {
        //占用1个字节
        out.writeByte(dataType);
        out.writeByte(flag);
        out.writeInt(cmd);
        out.writeInt(seq);
        out.writeInt(textLength);
        out.write(textData);

        //结束符,占用1个字节
        out.writeByte(End);
        out.flush();
    }

    public void writeTextAndFile(DataOutputStream out) throws IOException {
        //占用1个字节
        out.writeByte(dataType);
        out.writeByte(flag);
        out.writeInt(cmd);
        out.writeInt(seq);
        out.writeInt(textLength);
        out.write(textData);

        if (fileLength > 0) {
            //文件分隔符和文本数据
            out.writeByte(DataPacket.Spilt);//占用1个字节
            //发送文件名
            out.writeInt(fileNameLength);
            out.write(fileNameData);
            //发送文件
            out.writeByte(DataPacket.Spilt);//占用1个字节
            out.writeLong(fileLength);//文件的长度,占用8个字节
            writeFile(out, file);
        }
        //结束符,占用1个字节
        out.writeByte(End);
        out.flush();
    }

    private void writeFile(DataOutputStream out, File file) throws IOException {
        InputStream is = new FileInputStream(file.getPath());
        byte[] c = new byte[1024 * 4];
        int b;
        while ((b = is.read(c)) > 0) {
            out.write(c, 0, b);
        }
        is.close();
    }


    public void readHeart(byte dataType, DataInputStream inputStream) throws IOException {
        byte flag = inputStream.readByte();
        int cmd = inputStream.readInt();
        int seq = inputStream.readInt();
        byte end = inputStream.readByte();

        this.dataType = dataType;
        this.flag = flag;
        this.cmd = cmd;
        this.seq = seq;
        this.end = end;
    }

    /**
     * 按顺序解析
     *
     * @param dataType
     * @param inputStream
     * @throws IOException
     */
    public void readText(byte dataType, DataInputStream inputStream) throws IOException {
        byte flag = inputStream.readByte();
        int cmd = inputStream.readInt();
        int seq = inputStream.readInt();
        int textLength = inputStream.readInt();
        byte[] data = new byte[textLength];
        inputStream.readFully(data);
        byte end = inputStream.readByte();

        this.dataType = dataType;
        this.flag = flag;
        this.cmd = cmd;
        this.seq = seq;
        this.textLength = textLength;
        this.textData = data;
        this.end = end;
    }


    public void readTextAndFile(byte dataType, DataInputStream inputStream, File dir) throws IOException {
        long start = System.currentTimeMillis();
        byte flag = inputStream.readByte();
        int cmd = inputStream.readInt();
        int seq = inputStream.readInt();
        //解析文本
        int textLength = inputStream.readInt();
        byte[] data = new byte[textLength];
        inputStream.readFully(data);

        byte spiltChar = inputStream.readByte();
        if (spiltChar != DataPacket.Spilt) {
            throw new IllegalArgumentException("非法字节,无法解析文件名:" + spiltChar);
        }
        //解析文件名
        int fileNameLength = inputStream.readInt();
        byte[] fileNameData = new byte[fileNameLength];
        inputStream.readFully(fileNameData);

        spiltChar = inputStream.readByte();
        if (spiltChar != DataPacket.Spilt) {
            throw new IllegalArgumentException("非法字节,无法文件数据:" + spiltChar);
        }
        //解析文件数据
        long fileLength = inputStream.readLong();
        ConsoleUtils.i("fileLength:" + fileLength);

        String fileName = new String(fileNameData);
        File file = new File(dir, fileName);
        if (file.exists()) {
            file.delete();
        }
        FileOutputStream os = new FileOutputStream(file);

        byte[] buffer = new byte[1024 * 10];
        int ret;
        int readLength = 0;
        int surplus = buffer.length;
        //80864108
        if (fileLength <= buffer.length) {
            surplus = (int) fileLength;
        }
        //非堵塞读取,读多一个字节都会卡死
        while ((ret = inputStream.read(buffer, 0, surplus)) != -1) {
            os.write(buffer, 0, ret);
            readLength += ret;
            surplus = (int) (fileLength - readLength);
            if (surplus >= buffer.length) {
                surplus = buffer.length;
            }
            ConsoleUtils.i("readLength:" + readLength);
            if (readLength == fileLength) {
                ConsoleUtils.i("读取文件完毕");
                break;
            }
        }
        os.close();
        ConsoleUtils.i("文件读取耗时:" + (System.currentTimeMillis() - start) / 1000.0 + "s");
        byte end = inputStream.readByte();

        this.dataType = dataType;
        this.flag = flag;
        this.cmd = cmd;
        this.seq = seq;
        this.textLength = textLength;
        this.textData = data;
        this.fileNameLength = fileNameLength;
        this.fileNameData = fileNameData;
        this.fileLength = fileLength;
        this.file = file;
        this.end = end;
        ConsoleUtils.i("发送文件成功:" + fileName);
    }

   	//...
}

四.应用层调用

上面简单说明了两端通讯过程和简单封装,下面贴出应用层的调用过程,比较简单,通俗易懂。

1.1服务端调用
1.1.1启动服务端
  SimpleServer  simpleServer = new SimpleServer(port);
        simpleServer.setServerListener(new SimpleServer.ServerListener() {
            @Override
            public void onStarted() {
                showServerMsg("onStarted");
            }

            @Override
            public void onStopped() {
                showServerMsg("onStopped");
            }

            @Override
            public void onException(Exception e) {
                showServerMsg("onException:" + e.getMessage());

            }
        });
        //服务端文件保存目录
        File dir = new File(SimpleClientTest.getApkDir(), "apk" + File.separator + "download");
        simpleServer.setFileSaveDir(dir.getAbsolutePath());
        //订阅客户端的请求信息
        simpleServer.subscribeDataResponseListener(new DataResponseListener() {
            @Override
            public void sendOnSuccess(int cmd, DataPacket requestPacket, DataPacket responsePacket) {
                //子线程
             
                ConsoleUtils.i("服务端监听客服端:cmd: " + cmd + " ,requestPacket: " + requestPacket + " responsePacket: " + responsePacket);
              
            }

            @Override
            public void sendOnError(int cmd, Throwable t) {
                ConsoleUtils.e("接收客户端信息异常:" + cmd, t);
            }

        });


//启动服务端
        simpleServer.startup();
1.1.2 往客户端推送数据
        //定向客户端推送数据
        simpleServer.pushDataTpClient("","");
        //往所有有客户端推送数据
       simpleServer.pushDataTpAllClient();
1.1.3 关闭服务端
  simpleServer.shutdown();
1.2客户端调用
1.2.1连接服务端
  apkClient = new SimpleClient(ip, port);
        apkClient.setClientListener(new SimpleClient.ClientListener() {
            @Override
            public void onStarted() {
            }

            @Override
            public void onStopped() {

            }

            @Override
            public void onException(Exception e) {

            }
        });
     /*   apkClient.setHeartbeatListener(new HeartbeatListener() {
            @Override
            public void heartBeatPacket(DataPacket requestPacket, DataPacket responsePacket, String source) {
                ConsoleUtils.i("心跳响应:" + source);
            }
        });*/
        apkClient.setFileSaveDir(FileUtils.currentWorkDir + "apk");
        //订阅服务端的请求信息
        apkClient.subscribeDataResponseListener(new DataResponseListener() {
            @Override
            public void sendOnSuccess(int cmd, DataPacket requestPacket, DataPacket responsePacket) {
                ConsoleUtils.i("客服端监听服务端,cmd: " + cmd + " ,requestPacket: " + requestPacket + " responsePacket: " + responsePacket);
            
            }

            @Override
            public void sendOnError(int cmd, Throwable t) {
                ConsoleUtils.e("接收服务端信息异常:" + cmd, t);
            }

        });
        apkClient.connect();
1.2.2往服务端发送数据
apkClient.sendData(dataReq, new DataResponseListener() {
            @Override
            public void sendOnSuccess(int cmd, DataPacket requestPacket, DataPacket responsePacket) {
                ConsoleUtils.i("发送消息成功,cmd: " + cmd + " ,requestPacket: " + requestPacket + " responsePacket: " + responsePacket);
              
            }

            @Override
            public void sendOnError(int cmd, Throwable t) {
                
            }
        });
1.2.3断开连接
 apkClient.disconnect();

五.测试

测试其实就是上面的应用层调用
工程说明:
在这里插入图片描述
上面箭头所在工程都是Java类库。本工程是Android工程,建议使用IDEA导入四个module测试

本次测试提供三种测试方式,应对不同用户和不同环境的场景测试。

1.1测试场景1

说明:手机作为服务端,PC运行客户端连接(Java main程序),示例工程在module-demo下和app目录下
网络环境:同一局域网内ip或广域网的ip
操作
启动手机服务端:
在这里插入图片描述
运行main程序连接:
在这里插入图片描述
注意:配置的的IP和端口号

1.2测试场景2

说明:PC Android 模拟器(Genymotio 模拟器)作为服务端,PC运行客户端连接(Java main程序),示例工程在module-demo下和app目录下
网络环境:本地测试
操作
跟测试场景2差不多,唯一不同的是:连接之前执行如命令(CMD窗口执行):

//把PC电脑端TCP端口12580的数据转发到与电脑通过adb连接的Android设备的TCP端口8090上。
adb forward tcp:12580 tcp:8090
1.3测试场景3(建议)

说明:PC作为服务端和客户端,示例工程在module-demo下,适合Java开发人员测试,提供一个Java Swing窗口辅助测试
网络环境:本地测试
操作
在这里插入图片描述

上面三种测试:只是文本发送,如果需要同时发送文件和文件数据,需要:
在这里插入图片描述

六.总结

本文主要介绍Socket编程流程,对入门和理解Socket起到事半功倍的作用,同时对理解第三方客户端/服务器框架有较好的辅助作用,实际开发一般不选择原生Socket开发,因为工作量大,难度大,考虑的问题实际更多,实际选择业界流行,强大,稳定的Netty居多,上面如有错误请指出纠正。

七.项目地址

https://github.com/kellysong/Android-Socket 欢迎star,follow

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值