I'm trying to create a very basic example of a socket server in Spring Boot.
I have the following classes
WebSocketConfigurer:
#Configuration
#EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
#Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/path");
}
#Bean
public WebSocketHandler myHandler() {
return new SimpleWebSocketHandler();
}
}
TextWebSocketHandler:
public class SimpleWebSocketHandler extends TextWebSocketHandler {
private static Logger logger = LoggerFactory.getLogger(SimpleWebSocketHandler.class);
#Override
public void afterConnectionEstablished(WebSocketSession session) {
logger.debug("Opened new session in instance " + this);
}
#Override
public void handleTextMessage(WebSocketSession session, TextMessage message)
throws Exception {
String echoMessage = message.getPayload();
logger.debug(echoMessage);
}
#Override
public void handleTransportError(WebSocketSession session, Throwable exception)
throws Exception {
session.close(CloseStatus.SERVER_ERROR);
}
}
Application:
#SpringBootApplication
public class WebSocketServerApplication {
public static void main(String[] args) {
SpringApplication.run(WebSocketServerApplication.class, args);
}
}
Then I have a C program that will open a socket and send simple text to a server.
When I try to send just plain text (e.g. “Hello”) over the socket, I get the following error message back to the client:
HTTP/1.1 400 Bad Request
Server: Apache-Coyote/1.1
Transfer-Encoding: chunked
Date: Tue, 18 Aug 2015 09:22:05 GMT
Connection: close
And the server displays a “Error parsing HTTP request header” message. I assume that is to be expected as the server is running in an embedded tomcat server and is only configured for HTTP.
When I then try to send a HTTP POST message as text from the C application e.g.
POST /path HTTP/1.0\r\n Content-Type: text/plain\r\n Content-Length: 5\r\n \r\n Hello
I get nothing on the server side and the connection on the client eventually just times out.
I’m missing something obvious?
Do I need to specify a controller class? Is that not covered when I register the web socket handler at a path?
Related
I am running wiremock standalone using wiremockserver.
The application does not work in PCF because of the header Transfer-Encoding: chunked.
(If this header is present then in PCF I get the error 502 Bad Gateway: Registered endpoint failed to handle the request. and in headers x-cf-routererror endpoint_failure endpoint_failure (net/http: http/1.x transport connection broken: too many transfer encodings: ["chunked" "chunked"]).)
I don't want this header so I tried to disable the chunked encoding in wiremock:
#SpringBootApplication
public class MockApplication implements CommandLineRunner {
private final int serverPort;
private final ResourceLoader resourceLoader;
public MockApplication(#Value("${server.port}") final int serverPort, final ResourceLoader resourceLoader) {
this.serverPort = serverPort;
this.resourceLoader = resourceLoader;
}
public static void main(final String[] args) {
SpringApplication.run(MockApplication.class, args); //start springboot application
}
#Override
public void run(final String... args) {
final WireMockServer wireMockServer = new WireMockServer(options().port(serverPort+2)
.useChunkedTransferEncoding(Options.ChunkedEncodingPolicy.NEVER)
.extensions(new ResponseTemplateTransformer(false)));
wireMockServer.start(); //start wiremock server
}
Spring boot app posts data to wiremock app using restTemplate. sample request:
{
"request": {
"url": "/some-wiremock-endpoint",
"method": "POST",
"bodyPatterns": [
{
"contains": "12345"
}
]
},
"response": {
"status": 200,
"headers": null,
"body": "{\"books\":[{\"book_number\":\"12345\",\"book_code\":\"FICTION\"}]}"
}
}
When the customer uses http://localhost:8080/some-wiremock-endpoint then the springboot app connects to wiremock url http://localhost:8082/some-wiremock-endpoint to retrieve the stub data from wiremock server:
ResponseEntity<String> exchange = null;
try {
exchange = restTemplate.exchange(wiremockUrlAndUri, HttpMethod.POST, request, String.class);
//http://localhost:8082/some-wiremock-endpoint is the wiremockUrlAndUri value in this case.
When I see exchange.getHeaders() then I still see Transfer-Encoding: chunked header.
How do I disable this header in Springboot application?
How can I set the comet event timeout on NIO2 protocol?
How to well handle the socket connection on NIO2 protocol?(e.g., close connection)
We have a simple servlet which implements Apache CometEvent for long polling connection on tomcat8. It works well when we used org.apache.coyote.http11.Http11NioProtocol, however, we have now change to using org.apache.coyote.http11.Http11Nio2Protocol and it will not work properly.
On NIO, the client can make a comet connection to a Connect servlet by POST and the other client can send message by POST to Trigger servlet. Every 300 seconds we will timeout the comet and the client app will make comet connection again.
The Connect servlet as below
public class Connect extends HttpServlet implements CometProcessor {
...
public void event(CometEvent event) throws IOException, ServletException {
HttpServletRequest request = event.getHttpServletRequest();
HttpServletResponse response = event.getHttpServletResponse();
if (event.getEventType() == CometEvent.EventType.BEGIN) {
String deviceid = request.getParameter("id");
MessageSender.getInstance().addConnection(deviceid, event);
request.setAttribute("org.apache.tomcat.comet.timeout", 300 * 1000);
event.setTimeout(300 * 1000);
} else if (event.getEventType() == CometEvent.EventType.ERROR) {
MessageSender.getInstance().removeConnection(event);
event.close();
} else if (event.getEventType() == CometEvent.EventType.END) {
MessageSender.getInstance().removeConnection(event);
event.close();
} else if (event.getEventType() == CometEvent.EventType.READ) {
throw new UnsupportedOperationException("This servlet does not accept data");
}
}
}
And we have another Trigger servlet for sending message to client:
public class Trigger extends HttpServlet {
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
byte[] receieveByteArray = ByteUtil.getHttpServletRequestBody(req);
sendTrigger(req, resp, receieveByteArray);
}
private void sendTrigger(HttpServletRequest req, HttpServletResponse resp, byte[] trigger) throws IOException, ServletException
{
try
{
MessageSender.getInstance().sendTrigger(deviceId, trigger);
} catch (Exception e)
{
logger.error("Send trigger has thrown exception: ", e);
}
}
}
And the MessageSender class as below
public class MessageSender
{
private static final Map<String, CometEvent> connections = new ConcurrentHashMap<String, CometEvent>();
public void addConnection(String deviceId, CometEvent event) {
connections.put(deviceId, event);
}
public void removeConnection(CometEvent event) {
while (connections.values().remove(event)) {
}
public static MessageSender getInstance() {
return instance;
}
public void sendTrigger(String deviceId, byte[] triggerMessage) throws IOException, ConnectionNotFoundException {
CometEvent comet = connections.get(deviceId);
HttpServletResponse response = comet.getHttpServletResponse();
response.addHeader("Content-Length", Integer.toString(triggerMessage.length));
response.addHeader("Content-Language", "en-US");
ServletOutputStream servletOutputStream = response.getOutputStream();
servletOutputStream.write(triggerMessage);
servletOutputStream.flush();
servletOutputStream.close();
comet.close(); // add for NIO2
connections.remove(deviceId);
}
}
After we have changed the connector setting of tomcat http protocol to NIO2 as below
<Connector port="8443" protocol="org.apache.coyote.http11.Http11Nio2Protocol"
maxThreads="150" SSLEnabled="true" scheme="https" secure="true"
clientAuth="false" sslProtocol="TLS" connectionTimeout="60000"
keystoreFile="D:\localhost.jks" keystorePass="******" />
The timeout of event will not work as we have set it to 300 seconds, the comet connection will be disconnected after 60 seconds which I believe is the connector connection timeout. And there will have thrown an exception as below
28-Oct-2016 15:04:33.748 SEVERE [http-nio2-8443-exec-5] org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process Error reading request, ignored
java.lang.IllegalStateException: Reading not allowed due to timeout or cancellation
at sun.nio.ch.AsynchronousSocketChannelImpl.read(AsynchronousSocketChannelImpl.java:249)
at sun.nio.ch.AsynchronousSocketChannelImpl.read(AsynchronousSocketChannelImpl.java:297)
at org.apache.tomcat.util.net.SecureNio2Channel.read(SecureNio2Channel.java:792)
at org.apache.tomcat.util.net.Nio2Endpoint.awaitBytes(Nio2Endpoint.java:871)
at org.apache.coyote.http11.Http11Nio2Protocol$Http11ConnectionHandler.release(Http11Nio2Protocol.java:180)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:722)
at org.apache.tomcat.util.net.Nio2Endpoint$SocketProcessor.doRun(Nio2Endpoint.java:1073)
at org.apache.tomcat.util.net.Nio2Endpoint$SocketProcessor.run(Nio2Endpoint.java:1032)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)
If the client make the comet connection again after this, and the other client try to send message to Trigger servlet. The comet will be END immediately and connection disconnected.
Any help is appreciated
I have been migrating an existing application over to Spring Cloud's service discovery, Ribbon load balancing, and circuit breakers. The application already makes extensive use of the RestTemplate and I have been able to successfully use the load balanced version of the template. However, I have been testing the situation where there are two instances of a service and I drop one of those instances out of operation. I would like the RestTemplate to failover to the next server. From the research I have done, it appears that the fail-over logic exists in the Feign client and when using Zuul. It appears that the LoadBalancedRest template does not have logic for fail-over. In diving into the code, it looks like the RibbonClientHttpRequestFactory is using the netflix RestClient (which appears to have logic for doing retries).
So where do I go from here to get this working?
I would prefer to not use the Feign client because I would have to sweep A LOT of code.
I had found this link that suggested using the #Retryable annotation along with #HystrixCommand but this seems like something that should be a part of the load balanced rest template.
I did some digging into the code for RibbonClientHttpRequestFactory.RibbonHttpRequest:
protected ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException {
try {
addHeaders(headers);
if (outputStream != null) {
outputStream.close();
builder.entity(outputStream.toByteArray());
}
HttpRequest request = builder.build();
HttpResponse response = client.execute(request, config);
return new RibbonHttpResponse(response);
}
catch (Exception e) {
throw new IOException(e);
}
}
It appears that if I override this method and change it to use "client.executeWithLoadBalancer()" that I might be able to leverage the retry logic that is built into the RestClient? I guess I could create my own version of the RibbonClientHttpRequestFactory to do this?
Just looking for guidance on the best approach.
Thanks
To answer my own question:
Before I get into the details, a cautionary tale:
Eureka's self preservation mode sent me down a rabbit hole while testing the fail-over on my local machine. I recommend turning self preservation mode off while doing your testing. Because I was dropping nodes at a regular rate and then restarting (with a different instance ID using a random value), I tripped Eureka's self preservation mode. I ended up with many instances in Eureka that pointed to the same machine, same port. The fail-over was actually working but the next node that was chosen happened to be another dead instance. Very confusing at first!
I was able to get fail-over working with a modified version of RibbonClientHttpRequestFactory. Because RibbonAutoConfiguration creates a load balanced RestTemplate with this factory, rather then injecting this rest template, I create a new one with my modified version of the request factory:
protected RestTemplate restTemplate;
#Autowired
public void customizeRestTemplate(SpringClientFactory springClientFactory, LoadBalancerClient loadBalancerClient) {
restTemplate = new RestTemplate();
// Use a modified version of the http request factory that leverages the load balacing in netflix's RestClient.
RibbonRetryHttpRequestFactory lFactory = new RibbonRetryHttpRequestFactory(springClientFactory, loadBalancerClient);
restTemplate.setRequestFactory(lFactory);
}
The modified Request Factory is just a copy of RibbonClientHttpRequestFactory with two minor changes:
1) In createRequest, I removed the code that was selecting a server from the load balancer because the RestClient will do that for us.
2) In the inner class, RibbonHttpRequest, I changed executeInternal to call "executeWithLoadBalancer".
The full class:
#SuppressWarnings("deprecation")
public class RibbonRetryHttpRequestFactory implements ClientHttpRequestFactory {
private final SpringClientFactory clientFactory;
private LoadBalancerClient loadBalancer;
public RibbonRetryHttpRequestFactory(SpringClientFactory clientFactory, LoadBalancerClient loadBalancer) {
this.clientFactory = clientFactory;
this.loadBalancer = loadBalancer;
}
#Override
public ClientHttpRequest createRequest(URI originalUri, HttpMethod httpMethod) throws IOException {
String serviceId = originalUri.getHost();
IClientConfig clientConfig = clientFactory.getClientConfig(serviceId);
RestClient client = clientFactory.getClient(serviceId, RestClient.class);
HttpRequest.Verb verb = HttpRequest.Verb.valueOf(httpMethod.name());
return new RibbonHttpRequest(originalUri, verb, client, clientConfig);
}
public class RibbonHttpRequest extends AbstractClientHttpRequest {
private HttpRequest.Builder builder;
private URI uri;
private HttpRequest.Verb verb;
private RestClient client;
private IClientConfig config;
private ByteArrayOutputStream outputStream = null;
public RibbonHttpRequest(URI uri, HttpRequest.Verb verb, RestClient client, IClientConfig config) {
this.uri = uri;
this.verb = verb;
this.client = client;
this.config = config;
this.builder = HttpRequest.newBuilder().uri(uri).verb(verb);
}
#Override
public HttpMethod getMethod() {
return HttpMethod.valueOf(verb.name());
}
#Override
public URI getURI() {
return uri;
}
#Override
protected OutputStream getBodyInternal(HttpHeaders headers) throws IOException {
if (outputStream == null) {
outputStream = new ByteArrayOutputStream();
}
return outputStream;
}
#Override
protected ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException {
try {
addHeaders(headers);
if (outputStream != null) {
outputStream.close();
builder.entity(outputStream.toByteArray());
}
HttpRequest request = builder.build();
HttpResponse response = client.executeWithLoadBalancer(request, config);
return new RibbonHttpResponse(response);
}
catch (Exception e) {
throw new IOException(e);
}
//TODO: fix stats, now that execute is not called
// use execute here so stats are collected
/*
return loadBalancer.execute(this.config.getClientName(), new LoadBalancerRequest<ClientHttpResponse>() {
#Override
public ClientHttpResponse apply(ServiceInstance instance) throws Exception {}
});
*/
}
private void addHeaders(HttpHeaders headers) {
for (String name : headers.keySet()) {
// apache http RequestContent pukes if there is a body and
// the dynamic headers are already present
if (!isDynamic(name) || outputStream == null) {
List<String> values = headers.get(name);
for (String value : values) {
builder.header(name, value);
}
}
}
}
private boolean isDynamic(String name) {
return name.equals("Content-Length") || name.equals("Transfer-Encoding");
}
}
public class RibbonHttpResponse extends AbstractClientHttpResponse {
private HttpResponse response;
private HttpHeaders httpHeaders;
public RibbonHttpResponse(HttpResponse response) {
this.response = response;
this.httpHeaders = new HttpHeaders();
List<Map.Entry<String, String>> headers = response.getHttpHeaders().getAllHeaders();
for (Map.Entry<String, String> header : headers) {
this.httpHeaders.add(header.getKey(), header.getValue());
}
}
#Override
public InputStream getBody() throws IOException {
return response.getInputStream();
}
#Override
public HttpHeaders getHeaders() {
return this.httpHeaders;
}
#Override
public int getRawStatusCode() throws IOException {
return response.getStatus();
}
#Override
public String getStatusText() throws IOException {
return HttpStatus.valueOf(response.getStatus()).name();
}
#Override
public void close() {
response.close();
}
}
}
I had the same problem but then, out of the box, everything was working (using a #LoadBalanced RestTemplate). I am using Finchley version of Spring Cloud, and I think my problem was that I was not explicity adding spring-retry in my pom configuration. I'll leave here my spring-retry related yml configuration (remember this only works with #LoadBalanced RestTemplate, Zuul of Feign):
spring:
# Ribbon retries on
cloud:
loadbalancer:
retry:
enabled: true
# Ribbon service config
my-service:
ribbon:
MaxAutoRetries: 3
MaxAutoRetriesNextServer: 1
OkToRetryOnAllOperations: true
retryableStatusCodes: 500, 502
There is a good example for sharing HttpSession between Websocket and Rest service. (Spring DispatchServlet cannot find resource within Jetty) But it doesn't work for me. I'm not sure is there any thing I'm missing?
I'm using Jetty as websocket server and also I created a WebApp as well which injected by SpringConfig.
private void init() throws Exception
{
Server server = new Server();
// Create SSL Connector
ServerConnector serverConnector = getSSLConnector(server);
// Bundle to server
server.setConnectors(new Connector[] { serverConnector });
// Create request handler collection
HandlerCollection handlers = new HandlerCollection();
// Add WebSocket handler
final ServletContextHandler servletContextHandler = getWebSocketContextHandler();
handlers.addHandler(servletContextHandler);
// Add Servlet handler
handlers.addHandler(getWebAppServletContextHandler());
server.setHandler(handlers);
// Initial WebSocket
WebSocketServerContainerInitializer.configureContext(servletContextHandler);
// Start Jetty
server.start();
server.join();
}
Both WebSocket and Rest are working under same port perfectly, of course, with different context paths.
Now, I created a Rest service:
#RequestMapping(value = "/login", method = RequestMethod.POST)
#Consumes({ MediaType.APPLICATION_JSON_VALUE })
#Produces({ MediaType.APPLICATION_JSON_VALUE })
public #ResponseBody Message login(#RequestBody Credential credential, #Context HttpServletRequest servlerRequest)
{
...
HttpSession session = servlerRequest.getSession(true);
session.setAttribute("userName", credential.getUserName());
...
Message message = new Message();
...
return message;
}
In this service I created a HttpSession and stored something in. As I said, it works, and so does the session.
Rest client:
public void login() throws KeyManagementException, NoSuchAlgorithmException
{
final String loginServiceUri = HTTP_SERVICE_BASE_URI + "/login";
ClientConfig clientConfig = new DefaultClientConfig();
...
Client client = Client.create(clientConfig);
WebResource webResource = client.resource(loginServiceUri);
ClientResponse response = webResource
.type("application/json")
.post(ClientResponse.class, new Credential("user","pass"));
if (response.getStatus() != 200) {
throw new RuntimeException("Failed : HTTP error code : " + response.getStatus());
}
List<NewCookie>cookies = response.getCookies();
ClientEndpointConfigurator.setCookies(cookies); <== Store cookies as well as session to ClientEndpointConfigrator class
Message message = response.getEntity(Message.class);
...
}
ClientEndpointConfigrator class has a static list for all cookies which like this:
public class ClientEndpointConfigurator extends ClientEndpointConfig.Configurator {
private static List<NewCookie> cookies = null;
public static void setCookies(List<NewCookie> cookies) {
ClientEndpointConfigurator.cookies = cookies;
}
...
#Override
public void beforeRequest(Map<String, List<String>> headers) {
...
if(null != cookies)
{
List<String> cookieList = new ArrayList<String>();
for(NewCookie cookie: cookies)
{
cookieList.add(cookie.toString());
}
headers.put("Cookie", cookieList);
}
...
}
}
beforeRequest() method will put all cookies to request header. If you inspect the cookieList, you will see:
[JSESSIONID=tvum36z6j2bc1p9uf2gumxguh;Version=1;Path=/rs;Secure]
Things looks prefect.
Finally, create a server end ServerEndpointConfigurator class, and override the modifyHandshake() method to retrieve the session and cookies
public class SpringServerEndpointConfigurator extends ServerEndpointConfig.Configurator {
#Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
super.modifyHandshake(sec, request, response);
httpSession = (HttpSession)request.getHttpSession(); <== **It returns null here!**
...
}
}
}
I can't get my HttpSession back! and if you print headers out, you will see the cookie has been changed:
Cookie: JSESSIONID="tvum36z6j2bc1p9uf2gumxguh";$Path="/rs"
Any one knows what's the reason?
All right, I figured it out, it's because I put WebSocket and Rest to different context handler. Jetty keeps handlers isolate to each other. To share session information, you have to put them together.
But if someone does want to separate them, it is still possible done by sharing SessionManager or SessionHandler. There are many ways to achieve this, you can inject SessionHandler to each ServletContext or just define it as a static variable and put it on somewhere every one can reach, each way works.
Is there any way to read the header information received by GWT client, on the RPC response?
Response header
Server Apache-Coyote/1.1
Set-Cookie JSESSIONID=3379B1E57BEB2FE227EDC1F57BF550ED; Path=/GWT
Content-Encoding gzip
Content-Disposition attachment
Content-Type application/json;charset=utf-8
Content-Length 209
Date Fri, 05 Nov 2010 13:07:31 GMT
I'm particularly interest in identifying when client receives the Set-Cookie attribute on its header.
Is there any way to do that on GWT?
I found that on
com.google.gwt.user.client.rpc.impl.RequestCallbackAdapter<T>
exist the method
public void onResponseReceived(Request request, Response response) { ... }
On the parameter Response seems to have the information I need. The this is, exist some way to get that without "racking" the GWT compiler code?
thanks
JuDaC
You may try to override the RpcRequestBuilder.doSetCallback method and force your service to use it:
MyServiceAsync service = GWT.create(MyService.clas);
((ServiceDefTarget) service).setRpcRequestBuilder(new RpcRequestBuilder() {
#Override
protected void doSetCallback(RequestBuilder rb, final RequestCallback callback) {
super.doSetCallback(rb, new RequestCallback() {
#Override
public void onResponseReceived(Request request, Response response) {
String headerValue = response.getHeader("my-header");
// do sth...
callback.onResponseReceived(request, response);
}
#Override
public void onError(Request request, Throwable exception) {
callback.onError(request, exception);
}
});
}
});
Inspired by http://stuffthathappens.com/blog/2009/12/22/custom-http-headers-with-gwt-rpc/
If you declare your async service method to return a RequestBuilder you should be able to set a RequestCallback that will be notified when the Response is received. I haven't tried this myself, but it looks like what you need.