浅谈Android应用开发之TCP协议

       Android设备能够使用互联网功能是因为系统底层实现了TCP/IP协议,可以使终端通过网络建立TCP连接。TCP协议是一个面向连接的传输控制协议,也就是说数据通信必须要建立在连接的基础上。建立一个TCP连接需要经过三次握手,通俗来讲就是:1.客户端向服务器发送一个含有同步序列号(SYN)的数据段给服务器,向服务器请求建立连接;2.服务器收到客户端的请求后,用一个带有确认应答(ACK)和同步序列号(SYN)的数据段响客户端;3客户端收到这个数据段后,再发送一个确认应答(ACK),确认已收到服务器的数据段。至此,三次握手就完成了,客户端和服务器就可以传输数据了。握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。通常状态下,TCP连接一旦建立,在任何一方主动关闭之前,连接都将一直保持。断开连接时服务器和客户端均可以主动发起断开请求,断开过程需要经过四次握手,原理类似,就是服务器和客户端交互,最终确定断开。


        TCP通信主要通过Socket连接,Socket可以支持不同的传输层协议:TCPUDP。通常我们对HTTP连接很熟悉,HTTP协议是应用层协议,更接近用户端,而上文提到,TCP是传输层协议,更接近底层, Socket是从传输层抽象出来的一层接口,他们的关系如下图:

      那么HTTP连接与Socket连接有什么区别呢?1HTTP连接是短连接,而Socket连接(基于TCP协议)是长连接,尽管HTTP1.1开始支持持久连接,但仍无法保证始终连接;2HTTP连接采用“请求--响应”机制,在客户端未发消息给服务端前,服务端无法主动发消息给客户端,而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.	}   

以上代码必须放在子线程中执行,其中mergePackagehandlePackage方法为数据处理数据方法,因数据量可能较大,服务器端一次不能全部发送,则需要我们根据具体的业务协议对每次接收数据进行拆分和整合,比如对粘包问题的处理等。

      假如我们现在有以上协议,客户端发送及服务器返回皆假定为此协议,编码规范为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通信那样有很多成熟优秀的网络封装库,协议的解析也比较复杂,还要做断线重连机制,心跳包维持等,因篇幅原因这里就不一一列举了。本文主要阐述了数据发送和接收的大概流程,希望没有做过相关开发的同学能有一个宏观的概念,如有理解不到位的地方请多担待。有对安卓开发某块知识点感兴趣或有疑惑的朋友,可在文章底部留言,本人根据自己的经验写些相关的文章。



  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值