PageRenderTime 56ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

/modules/cpr/src/main/java/org/atmosphere/websocket/WebSocket.java

http://github.com/Atmosphere/atmosphere
Java | 431 lines | 256 code | 53 blank | 122 comment | 52 complexity | ad3713b29d0644c5b38911a91bc945cf MD5 | raw file
Possible License(s): Apache-2.0
  1. /*
  2. * Copyright 2008-2021 Async-IO.org
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package org.atmosphere.websocket;
  17. import org.atmosphere.cpr.ApplicationConfig;
  18. import org.atmosphere.cpr.AsyncIOWriter;
  19. import org.atmosphere.cpr.AtmosphereConfig;
  20. import org.atmosphere.cpr.AtmosphereInterceptorWriter;
  21. import org.atmosphere.cpr.AtmosphereRequest;
  22. import org.atmosphere.cpr.AtmosphereRequestImpl;
  23. import org.atmosphere.cpr.AtmosphereResource;
  24. import org.atmosphere.cpr.AtmosphereResourceImpl;
  25. import org.atmosphere.cpr.AtmosphereResponse;
  26. import org.atmosphere.cpr.KeepOpenStreamAware;
  27. import org.atmosphere.util.ByteArrayAsyncWriter;
  28. import org.slf4j.Logger;
  29. import org.slf4j.LoggerFactory;
  30. import java.io.IOException;
  31. import java.nio.Buffer;
  32. import java.nio.ByteBuffer;
  33. import java.nio.CharBuffer;
  34. import java.util.Map;
  35. import java.util.concurrent.ConcurrentHashMap;
  36. import java.util.concurrent.atomic.AtomicBoolean;
  37. import static org.atmosphere.cpr.HeaderConfig.X_ATMOSPHERE_ERROR;
  38. /**
  39. * Represent a portable WebSocket implementation which can be used to write message.
  40. *
  41. * @author Jeanfrancois Arcand
  42. */
  43. public abstract class WebSocket extends AtmosphereInterceptorWriter implements KeepOpenStreamAware {
  44. protected static final Logger logger = LoggerFactory.getLogger(WebSocket.class);
  45. public final static String WEBSOCKET_INITIATED = WebSocket.class.getName() + ".initiated";
  46. public final static String WEBSOCKET_SUSPEND = WebSocket.class.getName() + ".suspend";
  47. public final static String WEBSOCKET_RESUME = WebSocket.class.getName() + ".resume";
  48. public final static String WEBSOCKET_ACCEPT_DONE = WebSocket.class.getName() + ".acceptDone";
  49. public final static String NOT_SUPPORTED = "Websocket protocol not supported";
  50. public final static String CLEAN_CLOSE = "Clean_Close";
  51. private AtmosphereResource r;
  52. protected long lastWrite;
  53. protected boolean binaryWrite;
  54. private final AtomicBoolean firstWrite = new AtomicBoolean(false);
  55. private final AtmosphereConfig config;
  56. private WebSocketHandler webSocketHandler;
  57. protected ByteBuffer bb;
  58. protected CharBuffer cb;
  59. protected String uuid = "NUll";
  60. private Map<String, Object> attributesAtWebSocketOpen;
  61. private Object attachment;
  62. public WebSocket(AtmosphereConfig config) {
  63. String s = config.getInitParameter(ApplicationConfig.WEBSOCKET_BINARY_WRITE);
  64. if (s != null && Boolean.parseBoolean(s)) {
  65. binaryWrite = true;
  66. } else {
  67. binaryWrite = false;
  68. }
  69. int bufferSize = 8192;
  70. String bufferSizeParam = config.getInitParameter(ApplicationConfig.WEBSOCKET_BUFFER_SIZE);
  71. if (bufferSizeParam != null) {
  72. bufferSize = Integer.parseInt(bufferSizeParam);
  73. }
  74. bb = ByteBuffer.allocate(bufferSize);
  75. cb = CharBuffer.allocate(bufferSize);
  76. this.config = config;
  77. }
  78. public AtmosphereConfig config() {
  79. return config;
  80. }
  81. protected WebSocket webSocketHandler(WebSocketHandler webSocketHandler) {
  82. this.webSocketHandler = webSocketHandler;
  83. return this;
  84. }
  85. /**
  86. * Switch to binary write, or go back to text write. Default is false.
  87. *
  88. * @param binaryWrite true to switch to binary write.
  89. * @return
  90. */
  91. public WebSocket binaryWrite(boolean binaryWrite) {
  92. this.binaryWrite = binaryWrite;
  93. return this;
  94. }
  95. public WebSocketHandler webSocketHandler() {
  96. return webSocketHandler;
  97. }
  98. /**
  99. * Associate an {@link AtmosphereResource} to this WebSocket
  100. *
  101. * @param r an {@link AtmosphereResource} to this WebSocket
  102. * @return this
  103. */
  104. public WebSocket resource(AtmosphereResource r) {
  105. // Make sure we carry what was set at the onOpen stage.
  106. if (this.r != null && r != null) {
  107. // TODO: This is all over the place and quite ugly (the cast). Need to fix this in 1.1
  108. AtmosphereResourceImpl.class.cast(r).cloneState(this.r);
  109. }
  110. this.r = r;
  111. if (r != null) uuid = r.uuid();
  112. return this;
  113. }
  114. /**
  115. * Copy {@link AtmosphereRequestImpl#localAttributes()} that where set when the websocket was opened.
  116. *
  117. * @return this.
  118. */
  119. public synchronized WebSocket shiftAttributes() {
  120. attributesAtWebSocketOpen = new ConcurrentHashMap<String, Object>();
  121. attributesAtWebSocketOpen.putAll(AtmosphereResourceImpl.class.cast(r).getRequest(false).localAttributes().unmodifiableMap());
  122. return this;
  123. }
  124. /**
  125. * Return the attribute that was set during the websocket's open operation.
  126. *
  127. * @return
  128. */
  129. public Map<String, Object> attributes() {
  130. return attributesAtWebSocketOpen;
  131. }
  132. /**
  133. * Return the an {@link AtmosphereResource} used by this WebSocket, or null if the WebSocket has been closed
  134. * before the WebSocket message has been processed.
  135. *
  136. * @return {@link AtmosphereResource}
  137. */
  138. public AtmosphereResource resource() {
  139. return r;
  140. }
  141. /**
  142. * The last time, in milliseconds, a write operation occurred.
  143. *
  144. * @return this
  145. */
  146. public long lastWriteTimeStampInMilliseconds() {
  147. return lastWrite == -1 ? System.currentTimeMillis() : lastWrite;
  148. }
  149. protected byte[] transform(byte[] b, int offset, int length) throws IOException {
  150. return transform(r.getResponse(), b, offset, length);
  151. }
  152. protected byte[] transform(AtmosphereResponse response, byte[] b, int offset, int length) throws IOException {
  153. AsyncIOWriter a = response.getAsyncIOWriter();
  154. // NOTE #1961 for now, create a new buffer par transform call and release it after the transform call.
  155. // Alternatively, we may cache the buffer in thread-local and use it while this thread invokes
  156. // multiple writes and release it when this thread invokes the close method.
  157. ByteArrayAsyncWriter buffer = new ByteArrayAsyncWriter();
  158. try {
  159. response.asyncIOWriter(buffer);
  160. invokeInterceptor(response, b, offset, length);
  161. return buffer.stream().toByteArray();
  162. } finally {
  163. buffer.close(null);
  164. response.asyncIOWriter(a);
  165. }
  166. }
  167. @Override
  168. public WebSocket write(AtmosphereResponse r, String data) throws IOException {
  169. firstWrite.set(true);
  170. if (data == null) {
  171. logger.error("Cannot write null value for {}", resource());
  172. return this;
  173. }
  174. if (!isOpen()) throw new IOException("Connection remotely closed for " + uuid);
  175. logger.trace("WebSocket.write() {}", data);
  176. boolean transform = !filters.isEmpty() && r.getStatus() < 400;
  177. if (binaryWrite) {
  178. byte[] b = data.getBytes(resource().getResponse().getCharacterEncoding());
  179. if (transform) {
  180. b = transform(r, b, 0, b.length);
  181. }
  182. if (b != null) {
  183. write(b, 0, b.length);
  184. }
  185. } else {
  186. if (transform) {
  187. byte[] b = data.getBytes(resource().getResponse().getCharacterEncoding());
  188. data = new String(transform(r, b, 0, b.length), r.getCharacterEncoding());
  189. }
  190. if (data != null) {
  191. write(data);
  192. }
  193. }
  194. lastWrite = System.currentTimeMillis();
  195. return this;
  196. }
  197. @Override
  198. public WebSocket write(AtmosphereResponse r, byte[] data) throws IOException {
  199. if (data == null) {
  200. logger.error("Cannot write null value for {}", resource());
  201. return this;
  202. }
  203. return write(r, data, 0, data.length);
  204. }
  205. @Override
  206. public WebSocket write(AtmosphereResponse r, byte[] b, int offset, int length) throws IOException {
  207. firstWrite.set(true);
  208. if (b == null) {
  209. logger.error("Cannot write null value for {}", resource());
  210. return this;
  211. }
  212. if (!isOpen()) throw new IOException("Connection remotely closed for " + uuid);
  213. if (logger.isTraceEnabled()) {
  214. logger.trace("WebSocket.write() {}", new String(b, offset, length, "UTF-8"));
  215. }
  216. boolean transform = !filters.isEmpty() && r.getStatus() < 400;
  217. if (binaryWrite || resource().forceBinaryWrite()) {
  218. if (transform) {
  219. b = transform(r, b, offset, length);
  220. }
  221. if (b != null) {
  222. write(b, 0, b.length);
  223. }
  224. } else {
  225. String data = null;
  226. String charset = r.getCharacterEncoding() == null ? "UTF-8" : r.getCharacterEncoding();
  227. if (transform) {
  228. data = new String(transform(r, b, offset, length), charset);
  229. } else {
  230. data = new String(b, offset, length, charset);
  231. }
  232. if (data != null) {
  233. write(data);
  234. }
  235. }
  236. lastWrite = System.currentTimeMillis();
  237. return this;
  238. }
  239. /**
  240. * Broadcast, using the {@link org.atmosphere.cpr.AtmosphereResource#getBroadcaster()} the object to all
  241. * {@link WebSocket} associated with the {@link org.atmosphere.cpr.Broadcaster}. This method does the same as
  242. * websocket.resource().getBroadcaster().broadcast(o).
  243. *
  244. * @param o An object to broadcast to all WebSockets.
  245. */
  246. public WebSocket broadcast(Object o) {
  247. if (r != null) {
  248. r.getBroadcaster().broadcast(o);
  249. } else {
  250. logger.debug("No AtmosphereResource Associated with this WebSocket.");
  251. }
  252. return this;
  253. }
  254. @Override
  255. public WebSocket writeError(AtmosphereResponse r, int errorCode, String message) throws IOException {
  256. super.writeError(r, errorCode, message);
  257. if (!firstWrite.get()) {
  258. logger.debug("The WebSocket handshake succeeded but the dispatched URI failed with status {} : {} " +
  259. "The WebSocket connection is still open and client can continue sending messages.", errorCode + " " + message, uuid());
  260. } else {
  261. logger.warn("Unable to write {} {}", errorCode, message);
  262. }
  263. return this;
  264. }
  265. @Override
  266. public WebSocket redirect(AtmosphereResponse r, String location) throws IOException {
  267. logger.error("WebSocket Redirect not supported");
  268. return this;
  269. }
  270. @Override
  271. public void close(AtmosphereResponse r) throws IOException {
  272. logger.trace("WebSocket.close() for {}", uuid);
  273. try {
  274. // Never trust underlying server.
  275. // https://github.com/Atmosphere/atmosphere/issues/1633
  276. if (r.request() != null && r.request().getAttribute(CLEAN_CLOSE) == null) {
  277. close();
  278. }
  279. } catch (Exception ex) {
  280. logger.trace("", ex);
  281. }
  282. try {
  283. ((Buffer)bb).clear();
  284. ((Buffer)cb).clear();
  285. // NOTE #1961 if the buffer is cached at thread-local, it needs to be released here.
  286. } catch (Exception ex) {
  287. logger.trace("", ex);
  288. }
  289. }
  290. @Override
  291. public WebSocket flush(AtmosphereResponse r) throws IOException {
  292. return this;
  293. }
  294. /**
  295. * Is the underlying WebSocket open.
  296. *
  297. * @return true is opened
  298. */
  299. abstract public boolean isOpen();
  300. /**
  301. * Use the underlying container's websocket to write the String.
  302. *
  303. * @param s a websocket String message
  304. * @return this
  305. * @throws IOException
  306. */
  307. abstract public WebSocket write(String s) throws IOException;
  308. /**
  309. * Use the underlying container's websocket to write the byte.
  310. *
  311. * @param b a websocket byte message
  312. * @param offset start
  313. * @param length end
  314. * @return this
  315. * @throws IOException
  316. */
  317. abstract public WebSocket write(byte[] b, int offset, int length) throws IOException;
  318. /**
  319. * Use the underlying container's websocket to write the byte.
  320. *
  321. * @param b a websocket byte message
  322. * @return this
  323. * @throws IOException
  324. */
  325. public WebSocket write(byte[] b) throws IOException {
  326. return write(b, 0, b.length);
  327. }
  328. /**
  329. * Close the underlying WebSocket
  330. */
  331. abstract public void close();
  332. /**
  333. * Allow the underlying WebSocket to close with a code and a reason.
  334. */
  335. public void close(int statusCode, String reasonText) {
  336. }
  337. public String uuid() {
  338. return uuid;
  339. }
  340. public static void notSupported(AtmosphereRequest request, AtmosphereResponse response) throws IOException {
  341. response.addHeader(X_ATMOSPHERE_ERROR, WebSocket.NOT_SUPPORTED);
  342. response.sendError(501, WebSocket.NOT_SUPPORTED);
  343. logger.trace("{} for request {}", WebSocket.NOT_SUPPORTED, request);
  344. }
  345. /**
  346. * Send a WebSocket Ping
  347. *
  348. * @param payload the bytes to send
  349. * @return this
  350. */
  351. public WebSocket sendPing(byte[] payload) {
  352. throw new UnsupportedOperationException();
  353. }
  354. /**
  355. * Send a WebSocket Pong
  356. *
  357. * @param payload the bytes to send
  358. * @return this
  359. */
  360. public WebSocket sendPong(byte[] payload) {
  361. throw new UnsupportedOperationException();
  362. }
  363. /**
  364. * Attach an object. Be careful when attaching an object as it can cause memory leak
  365. *
  366. * @oaram object
  367. */
  368. public WebSocket attachment(Object attachment) {
  369. this.attachment = attachment;
  370. return this;
  371. }
  372. /**
  373. * Return the attachment
  374. */
  375. public Object attachment() {
  376. return attachment;
  377. }
  378. }