背景
一直没有写过JAVA Socket的代码,所以对JAVA Socket通讯这部分内容也一直停留在理论层面。最近项目上有Socket通讯的相关需求,也算是补充了一下这部分的空白。
基于JAVA实现Socket通讯起码有以下选择:
- 基于JDK1.4之前的BIO(Blocking I/O)的Socket实现,同步阻塞。
- 基于JDK1.4之后的NIO(New I/O或Non-Blocking I/O)的Socket实现,同步非阻塞。
- 基于AIO(Asynchronous I/O,NIO2.0)实现,异步非阻塞,JDK1.7之后才支持。
- 基于第三方框架实现,比如Netty。
具体采用哪一种方案其实取决于需求,并不是说有了NIO、AIO之后BIO就一文不值不能用了,比如我的项目需求就很简单,我需要实现的其实就是Socket Client,通过Socket Client向设备要数据,这种场景下,个人感觉BIO也很好,毕竟阻塞等待除了挂起一个线程之外也不会再有什么资源浪费,并非不可接受。也不是说一定就要用一个NB的框架才可以,毕竟,需求很简单以及、对框架不是很熟悉也没有太多时间研究框架的话,JDK Socket和Netty这种框架相比,前者学习成本低是显而易见的事情。
所以,先搞BIO,有空了再搞NIO,最后是Netty。
长连接 vs 短连接
我们都知道Socket通讯有长连接和短连接的区分,短连接就是每次通讯前建立连接、发送接收数据完成之后就关闭连接,需要通讯的时候就再次建立连接,如此反复。而长连接就是在连接建立、完成通讯之后,不关闭连接,通讯双方一直保持连接,在需要通讯的时候继续使用该连接收发数据。
所以长连接和短连接的区别就是是否长时间持有连接。需要频繁、长时间通讯的场景下,长连接更适合一点,因为建立连接的过程也是需要消耗资源的,频繁的创建、丢弃连接,会造成不必要的资源浪费。而通讯不太频繁的场景下,一次通讯完成之后比较长的时间内不会再通讯,则短连接更适合一点,因为长期持有连接而不收发数据,会造成线程资源的浪费。
代码示例
Socket Server
BIO模式是JDK支持的IO框架,所以不需要引入任何依赖,创建一个普通的POM工程即可。
创建SocketServer类,实例化的时候在指定端口创建socket服务:
public class SocketServer {
private int port=10086; //端口,写死的,可以参数配置
private ServerSocket serverSocket;
private Socket socket;
OutputStream outputStream;
private boolean sendrn=true;
private String HEARTBEAT="heartbeat"; //心跳信息,可以配置为参数
private int heartbeatPeriod=3000; //心跳周期,可以配置为参数
private ConcurrentHashMap<Socket,OutputStream> activedConnections = new ConcurrentHashMap();
public SocketServer(){
try{
serverSocket = new ServerSocket(port);
}catch (Exception e){
log.info("error in create serverSocket...");
}
}
创建一个等待客户端连接的方法,如果有客户端连接请求上来的话,把该请求缓存到activedConnections中,等待进行数据通讯(发送数据)。
serverSocket.accept()是等待连接的关键方法,BIO中的blocking阻塞就是在accept方法中实现的,如果没有客户端请求的话,accept方法就一直阻塞等待…直到有客户端发起连接请求:
public void waitingForConnection(){
while(true) {
try {
log.info("waiting for connection from client..."+Thread.currentThread().getId());
socket = serverSocket.accept();
outputStream = socket.getOutputStream();
activedConnections.put(socket, outputStream);
log.info("get connection,and actived connections:"+activedConnections.size());
} catch (Exception e) {
log.info("create connection error:" + e.getMessage());
}
}
}
接下来,创建一个发送数据的方法(项目中用来模拟磅秤有称重的场景,定时发送称重数据给客户端),遍历已经创建的连接,通过连接的输出流发送数据:
public void sendMessage(){
while(true){
ArrayList<Socket> needToRemove = new ArrayList<>();
log.info("active connections:"+activedConnections.size());
for(Map.Entry<Socket,OutputStream>entry:activedConnections.entrySet()){
Socket s = entry.getKey();
OutputStream os = entry.getValue();
try{
sendTscaleData(os);
}catch (Exception ex){
log.info("error in send message:"+ex.getMessage());
needToRemove.add(s);
}
}
//housekeeping
for(Socket s:needToRemove){
activedConnections.remove(s);
}
// ops.close();
try{
Thread.sleep(10000);
}catch (Exception e){
log.info("exception in accept..."+e.getMessage());
}
}
}
其中,sendTscaleData(os);是根据磅秤通讯协议模拟磅秤发送数据的方法,略。
最后是main方法:
public static void main(String[] args) {
SocketServer socketServer = new SocketServer();
log.info("main Thread:"+Thread.currentThread().getId());
Thread wfc = new Thread(()->{
socketServer.waitingForConnection();
});
wfc.start();
Thread hb=new Thread(()->{
socketServer.heartbeat();
}
);
hb.start();
socketServer.sendMessage();
}
main方法创建SocketServer,启动一个独立的线程等待客户端连接,这个线程会一直等待客户端连接,有连接过来之后就把连接放在缓存中,当前现成继续等待。所以,服务端是可以处理多个客户端连接的,当然也可以通过对activedConnections容量的控制来限制连接数。超过连接数之后可以拒绝连接、或者做连接排队等待等处理。
之后启动一个心跳连接,心跳连接的作用我们后面再说。
最后,主线程调用sendMessage方法,定时遍历activedConnections,获取到已创建的连接,发送数据给客户端。
socketServer有3个线程各司其职,主线程负责发送数据,等待连接线程负责处理客户端的连接请求,心跳线程负责心跳处理。
心跳
心跳是针对长连接的,也就是说,长连接才需要心跳,短连接不需要。原因是:心跳存在的原因是客户端向服务端请求连接、获取并持有连接之后,对于长连接而言,双方就会一直通过该连接收发数据。但是,网络是有可能出现波动的,而网络中断或出现波动后,某些特定场景下,Socket客户端和服务端双方都不能感知到。在此情况下,双方都认为网络状况依然良好,但实际情况是该连接已经不能正常收发数据了。
我们需要心跳的原因是,网络出现波动后的短时间内可能就恢复了,但是即使网络已经恢复,如果我们的应用并没有感知到网络的中断、也没有进行相应的处理的话,我们已经创建的连接其实已经不好使了,我们必须要感知到网络的中断情况,从而丢弃掉已经建立的连接,并且在网络重新恢复正常后能够及时快速的恢复连接,使得应用能够快速的、几乎不受影响的正常工作。
我们需要对网络中断后我们的应用没有感知这件事情做一个简单的理解,从而让我们能后理解心跳存在的底层原因。一般情况下,不管是服务端、还是客户端,如果我们直接拔掉网线的话,操作系统底层是可以感知到这一断网事件的,会通过底层事件的处理机制通知到应用层,所以我们也可以获取到断网通知的,从而可以做必要的重连,这种情况下其实我们是不需要心跳的,因为我们其实已经感知到断网事件了。
但是即使是上述场景下,也只有拔掉网线的这一段能感知到,比如Socket服务端拔掉网线了,也之后服务端自己能感知到断网了。客户端是无法感知到的,反之亦然。这种情况下客户端可能还有一直利用已经创建的连接请求数据,但是是加上该连接已经不可用了。
另外一种场景是,相对负责的网络场景,网络不稳定,可能交换机或者路由器侧短暂的网络中断,几秒钟之后网络就恢复正常了,这种情况下Socket通讯的双方都无法感知到网络中断过,网络中断后即使及时恢复了正常,先前已经建立的连接也已经不可用了,应用层必须感知到网络问题后及时恢复,才能确保应用的正常工作。
理解了心跳处理存在的必要性和原因之后,心跳本身的处理逻辑其实非常简单:Socket通讯双方按照约定好的频率和内容发送心跳数据给对方,双方定期检查是否能接收到对方发送的心跳数据,比如约定好每隔5秒钟服务端发送一个心跳包给客户端,客户端如果超过5秒没有接收到心跳包(或者超过两个心跳周期都没有收到心跳),就会丢弃掉当前连接、重新建立连接。网络短暂波动的情况下,重建连接会立刻成功,后续的通讯就可以在新连接上进行,因此,业务就不会受到(会尽可能少的受到)网络波动的影响。
Socket 服务端发送心跳的代码:
public void heartbeat(){
while(true){
ArrayList<Socket> needToRemove = new ArrayList<>();
for(Map.Entry<Socket,OutputStream>entry:activedConnections.entrySet()){
Socket s = entry.getKey();
OutputStream os = entry.getValue();
try{
log.info("send heartbeat to client...");
os.write(HEARTBEAT.getBytes());
if(sendrn) {
outputStream.write('\r');
outputStream.write('\n');
}
}catch (Exception ex){
log.info("error in send heartbeat:"+ex.getMessage());
needToRemove.add(s);
}
}
//housekeeping
for(Socket s:needToRemove){
activedConnections.remove(s);
}
// ops.close();
try{
Thread.sleep(heartbeatPeriod);
}catch (Exception e){
log.info("exception in accept..."+e.getMessage());
}
}
}
心跳发送失败的话,说明该连接已经发生异常了,就需要直接丢弃掉该连接(交给垃圾回收处理了)。
客户端的处理逻辑,下次再说。