一.前言

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.1ConsoleUtils.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;}@Overridepublic 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) {//读取cmdbyte 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<-SDataPacket requestPacket = mDataPacketList.get(seq);callbackServerRequestResult(requestPacket, responsePacket);} else {//C->Sfinal 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() {@Overridepublic 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 {@Overridepublic 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) {//读取cmdbyte 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->SDataPacket requestPacket = mDataPacketList.get(seq);callbackClientRequestResult(requestPacket, responsePacket);} else {//C<-Sfinal DataPacket requestPacket = responsePacket;DataPacket replyResponsePacket = replyData(requestPacket, DataRes.SUCCESS, "Success");callbackServerRequestResult(requestPacket, replyResponsePacket);}} else {//异常应答,不回调callbackResultreplyData(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() {@Overridepublic 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") {@Overridepublic DataType geDataType() {return DataType.HEARTBEAT;}@Overridepublic int getCmd() {return 0x01;}@Overridepublic int getSeq() {int andDecrement = ai.getAndDecrement();if (andDecrement == Integer.MIN_VALUE) {ai.set(-1);}return andDecrement;}};timer.schedule(new TimerTask() {@Overridepublic 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;//80864108if (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() {@Overridepublic void onStarted() {showServerMsg("onStarted");}@Overridepublic void onStopped() {showServerMsg("onStopped");}@Overridepublic 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() {@Overridepublic void sendOnSuccess(int cmd, DataPacket requestPacket, DataPacket responsePacket) {//子线程ConsoleUtils.i("服务端监听客服端:cmd: " + cmd + " ,requestPacket: " + requestPacket + " responsePacket: " + responsePacket);}@Overridepublic 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() {@Overridepublic void onStarted() {}@Overridepublic void onStopped() {}@Overridepublic void onException(Exception e) {}});/*   apkClient.setHeartbeatListener(new HeartbeatListener() {@Overridepublic void heartBeatPacket(DataPacket requestPacket, DataPacket responsePacket, String source) {ConsoleUtils.i("心跳响应:" + source);}});*/apkClient.setFileSaveDir(FileUtils.currentWorkDir + "apk");//订阅服务端的请求信息apkClient.subscribeDataResponseListener(new DataResponseListener() {@Overridepublic void sendOnSuccess(int cmd, DataPacket requestPacket, DataPacket responsePacket) {ConsoleUtils.i("客服端监听服务端,cmd: " + cmd + " ,requestPacket: " + requestPacket + " responsePacket: " + responsePacket);}@Overridepublic void sendOnError(int cmd, Throwable t) {ConsoleUtils.e("接收服务端信息异常:" + cmd, t);}});apkClient.connect();
1.2.2往服务端发送数据
apkClient.sendData(dataReq, new DataResponseListener() {@Overridepublic void sendOnSuccess(int cmd, DataPacket requestPacket, DataPacket responsePacket) {ConsoleUtils.i("发送消息成功,cmd: " + cmd + " ,requestPacket: " + requestPacket + " responsePacket: " + responsePacket);}@Overridepublic 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

全网最全的Java Socket通讯例子相关推荐

  1. 推荐:全网最全的Java并发面试题及答案。

    转载自  推荐:全网最全的Java并发面试题及答案. 1.在java中守护线程和本地线程区别? java中的线程分为两种:守护线程(Daemon)和用户线程(User). 任何线程都可以设置为守护线程 ...

  2. java socket通讯_Java socket通讯实现过程及问题解决

    这篇文章主要介绍了Java socket通讯实现过程及问题解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 本来是打算验证java socket是 ...

  3. 可能是全网最全,JAVA日志框架适配/冲突解决方案,可以早点下班了

    点击关注公众号,Java干货及时送达 你是否遇到过配置了日志,但打印不出来的情况? 你是否遇到过配置了logback,启动时却提示log4j错误的情况?像下面这样: log4j:WARN No app ...

  4. 可能是全网最全的 Java 日志框架适配、冲突解决方案

    作者:空无 juejin.cn/post/6945220055399399455 前言 你是否遇到过配置了日志,但打印不出来的情况? 你是否遇到过配置了logback,启动时却提示log4j错误的情况 ...

  5. 可能是全网最全,JAVA日志框架适配、冲突解决方案,可以早点下班了!

    点击上方"Java基基",选择"设为星标" 做积极的人,而不是积极废人! 每天 14:00 更新文章,每天掉亿点点头发... 源码精品专栏 原创 | Java ...

  6. 全网最全的JAVA所有版本特性【JAVA 1.0 - JAVA 20】

    闲来想了解下各版本之间的特性,搜索没有最新的特性说明,故想写一份.废话不多说. PS:绝对全网最全最齐,若不是,请私聊我补充,哈哈哈哈! JDK Version 1.0 1996-01-23 Oak( ...

  7. Java Socket简单例子、readLine()、readUTF()

    转载请标明出处:http://blog.csdn.net/xx326664162/article/details/51752701 文章出自:薛瑄的博客 你也可以查看我的其他同类文章,也会让你有一定的 ...

  8. 全网最全的 Java 面试题汇总,爱了~

    点击关注公众号,实用技术文章及时了解 不断收集整理,汇总网上面试知识点,方便面试前刷题,希望对你有帮助!有哪些方面的内容缺失,欢迎留言,后续不断补充. 01-10期 [01期]Spring,Sprin ...

  9. 全网最全的 Java 语法糖指南

    写在前面 本文隶属于专栏<100个问题搞定Java虚拟机>,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢! 本专栏目录结构和文献引用请见100个问题搞定Java ...

最新文章

  1. Lync server 2013 之office web apps server 搭建步骤
  2. android sdk 如何重新生成debug.keystore
  3. 基于OHCI的USB主机 —— 寄存器(初始化)
  4. Nacos配置管理-多环境配置共享
  5. Python实现进度条和时间预估的示例代码
  6. 以太坊2.0存款合约地址余额28.87万ETH,进度达55%
  7. 大数据分析平台安全的重要性
  8. 优先队列——Priority_Queue 详解
  9. linux怎么看内存时序,内存速度和时序重要么
  10. 一文介绍完整:python猴子补丁python monkey patch 没听过?
  11. 大数据可视化(一)数据可视化概述
  12. 2018-04-21-linux-sources-list html-url、隐藏滚动条
  13. 2022-10-25 系统app提示Signature mismatch for shared user: SharedUserSetting,开机后无法安装
  14. 知识图谱本体建模之RDF、RDFS、OWL详解
  15. Python基础知识(二)基本数据结构list列表和dict字典
  16. 液晶显示器原理和应用
  17. 读后感: 懈寄生---走出软件作坊:三五个人十来条枪 如何成为开发正规军(十四)
  18. 中国人,怎样毁了 祖传中医
  19. 分类号检索不好用?那是因为你压根没用对分类
  20. 用户名密码等信息用星号显示

热门文章

  1. MySQL学生成绩表查询最大、最小、平均、80分以上、人数、
  2. php判断页面访问是移动端还是pc端
  3. 寄存器一般多大,cpu一级缓存一般多大
  4. 中国婚博会PHP高级工程师、安全顾问汤青松:浅析Web安全编程
  5. 2020-mac教程-sudo spctl --master-disable用不了
  6. android获取用户手机信息,Android – 使用AccountManager /手机所有者的姓氏和姓氏获取用户数据...
  7. win10重装系统后连不上公司服务器,Win10电脑重装系统后不能上网怎么办?
  8. 苹果手机点击事件无效
  9. 国内优秀的免费素材网站大比拼
  10. Webdings字体和Wingdings字体对照表