/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nonblocking;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import javax.servlet.AsyncContext;
import javax.servlet.ReadListener;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* This doesn't do anything particularly useful - it just counts the total
* number of bytes in a request body while demonstrating how to perform
* non-blocking reads.
*/
public class ByteCounter
extends HttpServlet
{
private static final long serialVersionUID = 1L
;
@
Override
protected void doGet
(HttpServletRequest req, HttpServletResponse resp
)
throws ServletException,
IOException {
resp.
setContentType("text/plain");
resp.
setCharacterEncoding("UTF-8");
resp.
getWriter().
println("Try again using a POST request.");
}
@
Override
protected void doPost
(HttpServletRequest req, HttpServletResponse resp
)
throws ServletException,
IOException {
resp.
setContentType("text/plain");
resp.
setCharacterEncoding("UTF-8");
// Non-blocking IO requires async
AsyncContext ac = req.
startAsync();
// Use a single listener for read and write. Listeners often need to
// share state to coordinate reads and writes and this is much easier as
// a single object.
@
SuppressWarnings("unused")
CounterListener listener =
new CounterListener
(
ac, req.
getInputStream(), resp.
getOutputStream());
}
/**
* Keep in mind that each call may well be on a different thread to the
* previous call. Ensure that changes in values will be visible across
* threads. There should only ever be one container thread at a time calling
* the listener.
*/
private static class CounterListener
implements ReadListener, WriteListener
{
private final AsyncContext ac
;
private final ServletInputStream sis
;
private final ServletOutputStream sos
;
private volatile boolean readFinished =
false;
private volatile long totalBytesRead =
0;
private byte[] buffer =
new byte[8192];
private CounterListener
(AsyncContext ac, ServletInputStream sis,
ServletOutputStream sos
) {
this.
ac = ac
;
this.
sis = sis
;
this.
sos = sos
;
// In Tomcat, the order the listeners are set controls the order
// that the first calls are made. In this case, the read listener
// will be called before the write listener.
sis.
setReadListener(this);
sos.
setWriteListener(this);
}
@
Override
public void onDataAvailable
() throws IOException {
int read =
0;
// Loop as long as there is data to read. If isReady() returns false
// the socket will be added to the poller and onDataAvailable() will
// be called again as soon as there is more data to read.
while (sis.
isReady() && read
> -
1) {
read = sis.
read(buffer
);
if (read
> 0) {
totalBytesRead += read
;
}
}
}
@
Override
public void onAllDataRead
() throws IOException {
readFinished =
true;
// If sos is not ready to write data, the call to isReady() will
// register the socket with the poller which will trigger a call to
// onWritePossible() when the socket is ready to have data written
// to it.
if (sos.
isReady()) {
onWritePossible
();
}
}
@
Override
public void onWritePossible
() throws IOException {
if (readFinished
) {
// Must be ready to write data if onWritePossible was called
String msg =
"Total bytes written = [" + totalBytesRead +
"]";
sos.
write(msg.
getBytes(StandardCharsets.
UTF_8));
ac.
complete();
}
}
@
Override
public void onError
(Throwable throwable
) {
// Should probably log the throwable
ac.
complete();
}
}
}