淘先锋技术网

首页 1 2 3 4 5 6 7

前言

最近需要做一个新功能,要求在浏览器可以看到服务器上的日志文件的内容,并且实时显示,也就是相当于要在浏览器实现Linux下的tail -f 的功能。
最开始的思路是使用Ajax定时向后端请求数据并进行展示,但是这样做效率不高,而且请求过于频繁,这个方案就被否决掉了;因此就想到了需要一个全双工的通信方式,后端可以直接向前端发送请求,那么采用WebSocket就是再合适不过的方案了。
关于WebSocket如何实现全双工通信的原理在此就不进行赘述,如果不太了解可以自行学习。
本文将采用Spring WebSocket 实现Linux 上tail -f 的效果,在浏览器端进行日志实时显示的功能。

准备

首先需要引入Spring WebSocket的支持

        <!-- spring websocket-->
        <dependency>
            <groupId>javax.websocket</groupId>
            <artifactId>javax.websocket-api</artifactId>
            <version>1.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-websocket</artifactId>
            <version>4.2.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-messaging</artifactId>
            <version>4.2.5.RELEASE</version>
        </dependency>

前端同样需要WebSocket的支持,在这里我采用的是sock.js和stomp.js的支持,可以在下面的网址下载对应的JS文件,也可以直接引入:

sockJS
stompJS

同时也需要引入Spring的支持,在这里就不具体给出了,可以根据自己项目所用的Spring版本进行选择。

实现

WebSocket配置:

@Configuration
@EnableWebSocketMessageBroker
public class WebsocketConfiguration extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/gs-guide-websocket").setAllowedOrigins("*").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic");
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
        registration.setMessageSizeLimit( * );
    }
}

这里需要进行一下WebSocket的配置,
registerStompEndpoints方法设置一个端点的配置,用于握手连接使用;
configureMessageBroker方法设置广播的前缀地址与订阅的前缀地址信息;
configureWebSocketTransport方法设置消息最大大小。

Controller:

@Controller
@RequestMapping(value = "/websocketDemo")
public class GreetingController {

    @Autowired
    private WebSocketService webSocketService;

    @RequestMapping(value = "/greeting.do", method = RequestMethod.POST)
    public ModelAndView greeting(HelloMessage message) throws Exception {
        webSocketService.sendMessage();
        ModelAndView model = new ModelAndView();
        model.setViewName("/demo/webSocketDemo.jsp");
        return model;
    }

}

Service:

@Service
public class WebSocketService {

    @Autowired
    private SimpMessageSendingOperations simpMessageSendingOperations;

    public void sendMessage() {
                // 通过Process和Runtime执行linux命令
        String cmd = "tail -f /var/test.log";
        try {
            Process process = Runtime.getRuntime().exec(cmd);
            //需要另外启动线程进行读取,防止输入流阻塞当前线程
            Thread inputThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        BufferedReader br = new BufferedReader(
                                new InputStreamReader(process.getInputStream(), "UTF-8"));

                        StringBuffer line = new StringBuffer();
                        String lineOne = null;
                        int count = ;
                        int lineNum = ;
                        while ((lineOne = br.readLine()) != null) {

                            if (count == ) {

                                simpMessageSendingOperations.convertAndSend("/topic/greeting", line.toString());
                                line = new StringBuffer();
                                count = ;
                                //控制线程执行速度 防止推送过快 导致浏览器卡屏
                                Thread.sleep();
                            } else {
                                line.append(lineOne + "</br>");
                                count++;
                                lineNum++;
                            }
                        }

                        simpMessageSendingOperations.convertAndSend("/topic/greeting", line.toString());
                        br.close();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            inputThread.start();

            //主线程读取错误输出流数据
            BufferedReader br = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8"));
            StringBuffer line = new StringBuffer();
            String lineOne = null;
            int count = ;
            int lineNum = ;
            while ((lineOne = br.readLine()) != null) {
                if (count == ) {
                    this.simpMessageSendingOperations.convertAndSend("/topic/greeting", line.toString());
                    line = new StringBuffer();
                    count = ;
                } else {
                    line.append(lineOne + "</br>");
                    count++;
                    lineNum++;
                }
            }

            this.simpMessageSendingOperations.convertAndSend("/topic/greeting", line.toString());
            //等待正常输出流线程读取完成后,统一销毁进程
            inputThread.join();
            // 返回码 0 表示正常退出 1表示异常退出
            int extValue = process.waitFor(); 
            if ( == extValue) {
                logger.info("Exit Success!");
                br.close();
                process.destroy();
            } else {
                logger.info("Exit failure!");
                br.close();
                process.destroy();
            }
        } catch (Exception e) {
            e.printStackTrace();
            logger.error("execute shell fail!", e);
            logger.info("Exit failure!");
        }
    }
}

这里使用了Spring的SimpMessageSendingOperations操作类,这个类来自于Spring对Websocket的支持,可以通过此类直接发送请求。

由于tail -f命令的输入流会阻塞当前线程,所以一定要创建一个新的线程来读取tail -f命令的返回结果。

前端页面展示:

<!DOCTYPE html>
<html>
<head>
    <title>Hello WebSocket</title>
    <script src="/webjars/jquery/jquery.min.js"></script>
    <script src="/webjars/sockjs-client/sockjs.min.js"></script>
    <script src="/webjars/stomp-websocket/stomp.min.js"></script>
    <script src="/app.js"></script>
</head>
<body>
<div class="conWrap">
     <div id="log-container" style="height: 450px; overflow-y: scroll; background: #333; color: #aaa; padding: 10px;">
      <div></div>
     </div>
</div>
</body>
</html>

app.js:

var stompClient = null;

function connectWebSocket() {
    var socket = new SockJS('/gs-guide-websocket');
    stompClient = Stomp.over(socket);
    stompClient.connect({}, function (frame) {
        console.log('Connected: ' + frame);
        stompClient.subscribe('/topic/greeting', function (greeting) {
            showGreeting(greeting.body);
        });
    });
}

function disconnectWebSocket() {
    if (stompClient !== null) {
        stompClient.disconnect();
    }
    console.log("Disconnected");
    alert("Disconnected success");
}

function showGreeting(message) {
    $("#log-container div").append(message);
    // 滚动条滚动到最低部
    $("#log-container").scrollTop($("#log-container div").height() - $("#log-container").height());
}

在JS中需要订阅后端的地址:/topic/greeting,这样后端发送数据到该地址时,JS可以接收到数据。

需要注意的点

在本人进行调试的过程中,遇见两点问题,值得注意

1、当第一次进行WebSocket握手时,出现握手失败的问题,这个问题最后发现原因是因为拦截器导致的,WebSocket请求会包装成一个HTTP发送至服务器,如果拦截器没有拦截到这个请求,就会导致握手失败,因此建议在web.xml中将filter配置成:

    <servlet>
        <servlet-name>springMvcServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!-- 加载spring 核心配置文件 -->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring/spring-mvc.xml</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>springMvcServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

同时需要注意静态资源被拦截的问题,需要在SpringMVC中进行对应的配置

2、读取文件流速的问题,在调试过程中,遇见文件中文本写入速度很快,读取速度也很快,导致了想浏览器发送速度过快,浏览器卡屏的情况,这里建议根据实际情况,设置好流速,可以加大每多少条数据一输出,也可以让线程sleep以达到控制流速的目的。

效果图

这里写图片描述