Android设备能够使用互联网功能是因为系统底层实现了TCP/IP协议,可以使终端通过网络建立TCP连接。TCP协议是一个面向连接的传输控制协议,也就是说数据通信必须要建立在连接的基础上。建立一个TCP连接需要经过“三次握手”,通俗来讲就是:1.客户端向服务器发送一个含有同步序列号(SYN)的数据段给服务器,向服务器请求建立连接;2.服务器收到客户端的请求后,用一个带有确认应答(ACK)和同步序列号(SYN)的数据段响客户端;3客户端收到这个数据段后,再发送一个确认应答(ACK),确认已收到服务器的数据段。至此,“三次握手”就完成了,客户端和服务器就可以传输数据了。握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。通常状态下,TCP连接一旦建立,在任何一方主动关闭之前,连接都将一直保持。断开连接时服务器和客户端均可以主动发起断开请求,断开过程需要经过“四次握手”,原理类似,就是服务器和客户端交互,最终确定断开。
TCP通信主要通过Socket连接,Socket可以支持不同的传输层协议:TCP和UDP。通常我们对HTTP连接很熟悉,HTTP协议是应用层协议,更接近用户端,而上文提到,TCP是传输层协议,更接近底层, Socket是从传输层抽象出来的一层接口,他们的关系如下图:
那么HTTP连接与Socket连接有什么区别呢?1、HTTP连接是短连接,而Socket连接(基于TCP协议)是长连接,尽管HTTP1.1开始支持持久连接,但仍无法保证始终连接;2、HTTP连接采用“请求--响应”机制,在客户端未发消息给服务端前,服务端无法主动发消息给客户端,而Socket连接则一方随时可以向另一方发送消息。所以目前大部分即时通讯(IM)应用、实时监控应用、点对点应用、视频流播放应用等主要用的是Socket通信。
如何通信?
通常我们使用socket客户端编程情况比较多,但是安卓设备也是可以做服务器端的,比如中转一些智能设备数据或者做一些点对点的应用。下面简单阐述一下TCP如何创建客户端(服务器端类似),以及通讯过程常见的问题。代码如下:
1. try {
2. socket = new Socket(ip, port);
3. socket.setSoTimeout(30000);
4. inputStream = socket.getInputStream();
5. outputStream = socket.getOutputStream();
6. sendEmptyMessage(CREATE_SUCCESS);
7. } catch(Exception e) {
8. sendEmptyMessage(CREATE_FAIL);
9. }
10. byte[] buffer = new byte[8192];
11. byte[] remains = null;
12. while (socket != null && inputStream != null) {
13. try {
14. int length = inputStream.read(buffer);
15. if (length == -1) {
16. break;
17. }
18. byte[] temp = new byte[length];
19. System.arraycopy(buffer, 0, temp, 0, length);
20. if (remains != null && remains.length > 0) {
21. temp = mergePackage(remains, temp);
22. }
23. remains = handlePackage(temp);
24. Thread.sleep(10);
25. } catch(Exception e) {
26. sendEmptyMessage(RECEIVE_FAIL);
27. break;
28. }
29. }
以上代码必须放在子线程中执行,其中mergePackage和handlePackage方法为数据处理数据方法,因数据量可能较大,服务器端一次不能全部发送,则需要我们根据具体的业务协议对每次接收数据进行拆分和整合,比如对粘包问题的处理等。
假如我们现在有以上协议,客户端发送及服务器返回皆假定为此协议,编码规范为GBK格式,数据传输采用16进制byte数组方式。如图我们可以知道每条消息的长度为19+N个字节,包头默认为0X282A,包尾默认为0X2A29等。一般校验位会涉及到数据加解密算法或消息内容准确性判断,此处暂不做处理,默认赋以0X00,每条消息必须满足协议所有特性才是一条正确的消息。则我们可以这样打包:
1. /**
2. * 打包数据
3. * @param from
4. * 指令号
5. * @param content
6. * 发送的内容
7. * @return 打包好的字节数据
8. */
9. public byte[] generalPackage(int from, String content) {
10. byte[] msgByte = null;
11. try {
12. msgByte = content.getBytes("GBK");
13. } catch (Exception e) {
14. return null;
15. }
16. byte[] result = new byte[msgByte.length + 19];
17. // 包头
18. result[0] = 0x28;
19. result[1] = 0x2A;
20. // 包长
21. result[2] = (byte) (result.length / 255);
22. result[3] = (byte) (result.length % 255);
23. // 来源
24. result[4] = (byte) 0x91;
25. // 指令
26. result[5] = (byte) 0xF0;
27. result[6] = (byte) from;
28. // 用户ID
29. result[7] = 0x00;
30. result[8] = 0x00;
31. result[9] = 0x00;
32. result[10] = 0x00;
33. // 消息格式
34. result[11] = 0x00;
35. // 总包数
36. result[12] = 0x00;
37. result[13] = 0x01;
38. // 当前包数
39. result[14] = 0x00;
40. result[15] = 0x01;
41. System.arraycopy(msgByte, 0, result, 16, msgByte.length);
42. result[result.length - 3] = 0X00;
43. result[result.length - 2] = 0x2A;
44. result[result.length - 1] = 0x29;
45. return result;
46. }
同理,我们按照协议约束来将服务器返回数据进行解析,如果是返回是字符串数据或文本信息,我们可以这样封装:
1. /**
2. * 解包
3. * @param result 16进制消息数组
4. * @return String 字符串数据
5. */
6. public String parseBytes(byte[] result) {
7. String restult = null;
8. byte[] buffer = null;
9. byte[] buff = null;
10. for (int i = 0; i < result.length;) {
11. int length = new FormaterBytes().byteToInteger(result[i + 2], result[i + 3]);
12. if (buffer == null) {
13. buffer = new byte[length - 19];
14. System.arraycopy(result, i + 16, buffer, 0, buffer.length);
15. } else {
16. buff = buffer;
17. buffer = new byte[buffer.length + length - 19];
18. System.arraycopy(buff, 0, buffer, 0, buff.length);
19. System.arraycopy(result, i + 16, buffer, buff.length, length - 19);
20. }
21. i = i + length;
22. }
23. try {
24. restult = new String(buffer, "GBK");
25. } catch (UnsupportedEncodingException e) {
26. e.printStackTrace();
27. }
28. return restult;
29. }
如果返回的是类似H264的码流,此处我们可以把返回值类型改为byte[],在页面直接使用就行。
接下来我们就可以发送请求了,这个操作务必要在子线程进行:
1. /**
2. * 发送消息
3. *
4. * @param content 消息内容
5. */
6. public void sendMsg(byte[] content) {
7. if (socket == null || !socket.isConnected()) {
8. handler.sendEmptyMessage(CREATE_RECONNECT);
9. //此处判断后做常规关闭操作
10. try {
11. // 接口置空,停止回调
12. if (socket != null) {
13. socket.close();
14. socket = null;
15. }
16. if (inputStream != null) {
17. inputStream.close();
18. inputStream = null;
19. }
20. if (outputStream != null) {
21. outputStream.close();
22. outputStream = null;
23. }
24. } catch (IOException e) {
25. LogUtil.e("关闭连接失败" + e.toString());
26. } catch (NullPointerException e) {
27. LogUtil.e("关闭连接失败" + e.toString());
28. }
29. //此处重新初始化socket对象
30. initSocket();
31. } else {
32. try {
33. outputStream.write(content);
34. outputStream.flush();
35. socketResult.content(SEND_SUCCESS, null);
36. } catch (Exception e) {
37. socketResult.content(SEND_FAIL, null);
38. LogUtil.e(e.toString());
39. }
40. }
41. }
常见问题
1.大小端转换不当导致数据错乱
电脑CPU分为大端法和小端法两种。通常网络传输时都采用大端对齐法。对于AIX等系统,是大端对齐;而Windows、Linux等系统则是小端对齐。对于超过一个字节的short、int、int64及相应的unsigned数据类型,都需要进行大小字节序的转换。我们接着上面的协议分析,包头为0X282A,如果按小端发送,则需要发送0X2A28,后面的包长、指令、用户ID等大于一字节的字段全部都需要转换为小端模式再发送,比如用户ID为0XFAFBFCFD,则我们需要转换为0XFDFCFBFA,同理接收到数据也要做相应处理后再解析和使用。当我们开发时发现返回的数据有问题,而我们用的是小端传输,那么就应该仔细检查一下转换工具类的代码了。下面列举一个常用的方法:
1. /**
2. * 整形转小端16进制int数组
3. *
4. * @param value 整形
5. * @param length 字节数(2字节、4字节等)
6. * @return int数组
7. */
8. public static int[] intToSmallHex(int value, int length) {
9. String hexString = Integer.toHexString(value);
10. for (int i = 0; i < length * 2 - hexString.length(); ) {
11. hexString = "0" + hexString;
12. }
13. int byteLength = hexString.length() / 2;
14. int[] smallHexs = new int[byteLength];
15. for (int j = 0; j < byteLength; j++) {
16. smallHexs[j] = Integer.valueOf(hexString.substring(byteLength * 2 - j * 2 - 2, byteLength * 2 - j * 2), 16);
17. }
18. return smallHexs;
19. }
2.粘包问题导致数据错乱
TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。此处应强调一下,TCP是一种流模式的协议,通过字节流进行传播,此处所说的包是指服务器单次发送的数据,便于我们理解。通常数据包可分为以下几类:半条数据、一条完整数据、1.x条数据、x.x条数据。可参考下图:
我们可以这样处理:
1. /**
2. * 解决粘包问题
3. * @param unHandledPkg 源数据
4. * @return 处理数据
5. */
6. private final byte[] handlePackage(byte[] unHandledPkg) {
7. // 调用一次read,从Server收到的数据包(可能是半包、1个包、1.x、2.x....)
8. // 数据包长度
9. int pkgLen = unHandledPkg.length;
10.
11. // 一个完整数据包的长度
12. FormaterBytes forBytes = new FormaterBytes();
13. int completePkgLen = forBytes.byteToInt(unHandledPkg[2], unHandledPkg[3]);
14.
15. if (completePkgLen > pkgLen) {
16. // 当前收到的数据不到一个完整包,则直接返回,等待下一个包
17. return unHandledPkg;
18. } else if (completePkgLen == pkgLen) {
19. // 一个完整包
20. // 此处可做具体业务逻辑分类,如:登录包判断
21. if ((unHandledPkg[5] & 0xFF) == 0xf1 && (unHandledPkg[6] & 0xFF) == 0x01) {
22. if (new MakePackage().parseBytes(unHandledPkg).equals("1")) {
23. sendEmptyMessage(CreateSuccess);
24. return null;
25. } else if (new MakePackage().parseBytes(unHandledPkg).equals("0")) {
26. sendEmptyMessage(CreateFail);
27. return null;
28. }
29. }
30.
31. // 实时数据包或者详情包
32. if ((unHandledPkg[5] & 0xFF) == 0xf1 && (unHandledPkg[6] & 0xFF) == 0x02 ||
33. (unHandledPkg[6] & 0xFF) == 0x03|| (unHandledPkg[6] & 0xFF) == 0x04
34. || (unHandledPkg[6] & 0xFF) == 0x05 || (unHandledPkg[6] & 0xFF) == 0x06) {
35. Message msg = new Message();
36. msg.what = ReceiveDone;
37. msg.obj = unHandledPkg;
38. sendMessage(msg);
39. }
40. return null;
41. } else {
42. // 有多个包,那么就递归解析
43. int pkgAmount = forBytes.byteToInt(unHandledPkg[12], unHandledPkg[13]);// 总包数
44. int pkgCurrent = forBytes.byteToInt(unHandledPkg[14], unHandledPkg[15]);// 当前包序号
45. boolean condition = (pkgCurrent >= pkgAmount);
46. if (condition) {
47. Message msg = new Message();
48. msg.what = ReceiveDone;
49. byte[] temp = new byte[completePkgLen];
50. System.arraycopy(unHandledPkg, 0, temp, 0, completePkgLen);
51. msg.obj = temp;
52. sendMessage(msg);
53. temp = null;
54. }
55. // 截取除完整包后的剩余部分
56. byte[] remain = getSubBytes(unHandledPkg, completePkgLen, pkgLen - completePkgLen);
57. return handlePackage(remain);
58. }
59. }
总结
Tcp开发不像Http通信那样有很多成熟优秀的网络封装库,协议的解析也比较复杂,还要做断线重连机制,心跳包维持等,因篇幅原因这里就不一一列举了。本文主要阐述了数据发送和接收的大概流程,希望没有做过相关开发的同学能有一个宏观的概念,如有理解不到位的地方请多担待。有对安卓开发某块知识点感兴趣或有疑惑的朋友,可在文章底部留言,本人根据自己的经验写些相关的文章。