WebSockets

Another kind of optimization we can think about is sending the data to the client as it becomes available to the server. Since we fetch results of the search in multiple threads, the data will come in multiple chunks. We could send them bit by bit instead of waiting for all the results.

Spring has excellent support for WebSockets, which is a protocol that allows clients to maintain a long-running connection to the server. Data can be pushed in web sockets on both ends of the connection and consumers will get the data in real-time.

We will use a JavaScript library called SockJS to ensure compatibility with all browsers. Sockjs will transparently fall back on another strategy if our users have an outdated browser.

We will also use StompJS to connect to our message broker.

Add the following library to your build:

compile 'org.springframework.boot:spring-boot-starter-websocket'
compile 'org.springframework:spring-messaging'

compile 'org.webjars:sockjs-client:1.0.0'
compile 'org.webjars:stomp-websocket:2.3.3'

Add the WebJars to our default Thymeleaf template:

<script src="/webjars/sockjs-client/1.0.0/sockjs.js"></script>
<script src="/webjars/stomp-websocket/2.3.3/stomp.js"></script>

To configure WebSockets in our application, we need to add a bit of configuration as well:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfiguration extends AbstractWebSocketMessageBrokerConfigurer {

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

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/twitterSearch").withSockJS();
    }

}

This will configure the different channels available in our application. SockJS clients will connect to the twitterSearch endpoint and will push data to the server on /ws/ channel and be able to listen to /topic/ for changes.

This will allow us to inject a SimpMessagingTemplate in a new controller to push data to the client in the /topic/searchResult channel, as follows:

@Controller
public class SearchSocketController {
    private CachedSearchService searchService;
    private SimpMessagingTemplate webSocket;

    @Autowired
    public SearchSocketController(CachedSearchService searchService, SimpMessagingTemplate webSocket) {
        this.searchService = searchService;
        this.webSocket = webSocket;
    }

    @MessageMapping("/search")
    public void search(@RequestParam List<String> keywords) throws Exception {
        Consumer<List<LightTweet>> callback = tweet -> webSocket.convertAndSend("/topic/searchResults", tweet);
        twitterSearch(SearchParameters.ResultType.POPULAR, keywords, callback);
    }

    public void twitterSearch(SearchParameters.ResultType resultType, List<String> keywords, Consumer<List<LightTweet>> callback) {
        keywords.stream()
            .forEach(keyword -> {
                searchService.search(resultType, keyword)
                    .addCallback(callback::accept, Throwable::printStackTrace);
            });
    }
}

In our resultPage, the JavaScript code is really simple:

var currentLocation = window.location.href;
var search = currentLocation.substr(currentLocation.lastIndexOf('=') + 1);

function connect() {
  var socket = new SockJS('/hello');
  stompClient = Stomp.over(socket);
  // stompClient.debug = null;
  stompClient.connect({}, function (frame) {
      console.log('Connected: ' + frame);

      stompClient.subscribe('/topic/searchResults', function (result) {
          displayTweets(JSON.parse(result.body));
      });

      stompClient.send("/app/search", {}, JSON.stringify(search.split(',')));
  });
}

The displayTweets function remains essentially the same as before:

function displayTweets(tweets) {
    $.each(tweets, function (index, tweet) {
        addTweet(tweet);
    })
}

function addTweet(tweet) {
    var template = _.template('<li class="collection-item avatar">' +
        '<img class="circle" src="${tweet.profileImageUrl}" />' +
        '<span class="title">${tweet.userName}</span>' +
        '<p>${tweet.text}</p>' +
        '</li>');

    $('#tweets').append(template({tweet: tweet}));
}

Here you go! The client will now receive the results of all the searches in the application-- live!

Before pushing this to production, it will require a little bit more work. Here are some ideas:

  • Create subchannels for clients to privately listen to changes
  • Close the channel when a client is done using it
  • Add CSS transitions to the new tweets so the user can feel that it's real-time
  • Use a real broker, such as RabbitMQ, to allow the backend to scale with connections

There is much more to WebSocket than just this simple example. Don't forget to have a look at the documentation at http://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html for more information.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.34.39