前言
最近公司在预研设备app端与服务端的交互方案,主要方案有
服务端和app端通过阿里iot套件实现消息的收发;
服务端通过极光推送主动给app端推消息,app通过rest接口与服务端进行交互;
服务端与app通过mqtt消息队列来实现彼此的消息交互;
服务端与app通过原生socket长连接交互。
虽然上面的一些成熟方案肯定更利于上生产环境,但它们通讯基础也都是socket长连接,所以本人主要是预研了一下socket长连接的交互,写了个简单demo,采用了BIO的多线程方案,实现了自定义简单协议,心跳机制,socket客户端身份强制验证,socket客户端断线获知等功能,并暴露了一些接口,可通过接口简单实现客户端与服务端的socket交互。
Github 地址点此
IO通讯模型 IO通讯模型简介IO通讯模型主要包括阻塞式同步IO(BIO),非阻塞式同步IO,多路复用IO以及异步IO。大神博客请点此
1. 阻塞式同步IOBIO就是:blocking IO。最容易理解、最容易实现的IO工作方式,应用程序向操作系统请求网络IO操作,这时应用程序会一直等待;另一方面,操作系统收到请求后,也会等待,直到网络上有数据传到监听端口;操作系统在收集数据后,会把数据发送给应用程序;最后应用程序受到数据,并解除等待状态。
2. 非阻塞式同步IO这种模式下,应用程序的线程不再一直等待操作系统的IO状态,而是在等待一段时间后,就解除阻塞。如果没有得到想要的结果,则再次进行相同的操作。这样的工作方式,暴增了应用程序的线程可以不会一直阻塞,而是可以进行一些其他工作。
3. 多路复用IO(阻塞+非阻塞)目前流程的多路复用IO实现主要包括四种:select、poll、epoll、kqueue。下表是他们的一些重要特性的比较:
4. 异步IO异步IO则是采用“订阅-通知”模式:即应用程序向操作系统注册IO监听,然后继续做自己的事情。当操作系统发生IO事件,并且准备好数据后,在主动通知应用程序,触发相应的函数。
和同步IO一样,异步IO也是由操作系统进行支持的。微软的windows系统提供了一种异步IO技术:IOCP(I/O Completion Port,I/O完成端口);
Linux下由于没有这种异步IO技术,所以使用的是epoll(上文介绍过的一种多路复用IO技术的实现)对异步IO进行模拟。
Java对IO模型的支持Java对阻塞式同步IO的支持主要是java.net包中的Socket套接字实现;
Java中非阻塞同步IO模式通过设置serverSocket.setSoTimeout(100);即可实现;
Java 1.4中引入了NIO框架(java.nio包)可以构建多路复用、同步非阻塞IO程序;
Java 7中对NIO进行了进一步改进,即NIO2,引入了异步非阻塞IO方式。
由于是要实现socket长连接的demo,主要关注其一些实现注意点及方案,所以本demo采用了BIO的多线程方案,该方案代码比较简单、直观,引入了多线程技术后,IO的处理吞吐量也大大提高了。下面是BIO多线程方案server端的简单实现:
public static void main(String[] args) throws Exception{ ServerSocket serverSocket = new ServerSocket(83); try { while(true) { Socket socket = null; socket = serverSocket.accept(); //这边获得socket连接后开启一个线程监听处理数据 SocketServerThread socketServerThread = new SocketServerThread(socket); new Thread(socketServerThread).start(); } } catch(Exception e) { log.error("Socket accept failed. Exception:{}", e.getMessage()); } finally { if(serverSocket != null) { serverSocket.close(); } } } } @slf4j class SocketServerThread implements Runnable { private Socket socket; public SocketServerThread (Socket socket) { this.socket = socket; } @Override public void run() { InputStream in = null; OutputStream out = null; try { in = socket.getInputStream(); out = socket.getOutputStream(); Integer sourcePort = socket.getPort(); int maxLen = 2048; byte[] contextBytes = new byte[maxLen]; int realLen; StringBuffer message = new StringBuffer(); BIORead:while(true) { try { while((realLen = in.read(contextBytes, 0, maxLen)) != -1) { message.append(new String(contextBytes , 0 , realLen)); /* * 我们假设读取到“over”关键字, * 表示客户端的所有信息在经过若干次传送后,完成 * */ if(message.indexOf("over") != -1) { break BIORead; } } } //下面打印信息 log.info("服务器(收到来自于端口:" + sourcePort + "的信息:" + message); //下面开始发送信息 out.write("回发响应信息!".getBytes()); //关闭 out.close(); in.close(); this.socket.close(); } catch(Exception e) { log.error("Socket read failed. Exception:{}", e.getMessage()); } } } 注意点及实现方案 TCP粘包/拆包 1. 问题说明假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4种情况。
服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包;
服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包;
服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为TCP拆包;