本博文,保證不用裝B的話語和太多專業的語言,保證簡單易懂,只要懂JAVAEE開發的人均可以看懂。 本博文發表目的是,目前網上針對Websocket的資料太散亂,致使初學者的知識體系零零散散,學習困難加大。本博加以整理,而且實踐。javascript
所用核心技術選型:php
Tomcat + Spring 4.0.3 + Mongodb(高併發數據庫)css
+ SpringQueue(消息隊列)+ ActiveMQ (消息隊列)html
+ Spring-data-Mongo + Servlet 3.0java
+Spring-Websocketmysql
+ Mavenjquery
注:如下Websocket 均省略成 WB android
先說Websocket 的原理。 Websocket 是全雙工通信(說白了就是倆均可以通信,服務器也能夠給客戶端發消息,客戶端也能給服務器發消息)。也是基於TCP的,效率是很高的,首先這個技術的底層選用,就決定了徹底能夠用wb這個技術作高併發應用,並且開發很是快!!代碼很是簡單!!最重要的是穩定性,擴展性等等都有保證,等會兒說爲何說都有保證。 ios
WB 不一樣於TCP的三次握手。 WB是先進行一次HTTP請求,這個請求頭不一樣於普通HTTP請求,等會貼出來說解。而後服務器開始辨認請求頭,若是是WB的請求頭,則開始進行普通的TCP鏈接,即三次握手(不懂的TCP的,出門百度)。若是不是WB的HTTP請求頭,那就是按普通的HTTP請求處理。 git
流程梳理: HTTP特殊請求(有個特殊的頭) ---- 》 服務請接收判斷 ----- 》 認出來了,確實是WB請求頭,開啓TCP 三次握手,創建鏈接後,和TCP同樣了就------》沒有認出來,不是WB的請求頭,按普通HTTP請求處理。
很清楚了吧。這是個基礎,先理解了,下面寫程序纔好搞。下面這段是Webscoket的請求頭。 GET請求
GET ws://localhost:12345/websocket/test.html HTTP/1.1
Origin: http://localhost
Connection: Upgrade
Host: localhost:12345
Sec-WebSocket-Key: JspZdPxs9MrWCt3j6h7KdQ== //主要這個字段,這個叫「夢幻字符串」,這個加密規則能夠去百度,是有規則的。這個也是個密鑰,只有有這個密鑰 服務器才能經過解碼 認出來,哦~這是個WB的請求,我要創建TCP鏈接了!!!若是這個字符串沒有按照加密規則加密,那服務端就認不出來,就會認爲這整個協議就是個HTTP請求。更不會開TCP。其餘的字段均可以隨便設置,可是這個字段是最重要的字段,標識WB協議的一個字段。
Upgrade: websocket
Sec-WebSocket-Version: 13
下面這段是服務端迴應消息:
HTTP/1.1 101 Web Socket Protocol Handshake
WebSocket-Location: ws://localhost:12345/websocket/test.php
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: zUyzbJdkVJjhhu8KiAUCDmHtY/o= //這個字段,叫「夢幻字符串」,和上面那個夢幻字符串做用同樣。不一樣的是,這個字符串是要讓客戶端辨認的,客戶端拿到後自動解碼。而且辨認是否是一個WB請求。而後進行相應的操做。這個字段也是重中之重,不可隨便修改的。加密規則,依然是有規則的,能夠去百度一下。
WebSocket-Origin: http://localhost
好了,一去一回的HTTP請求, 若是他們的夢幻字符串都對上了,客戶端服務端都肯定是一次WB請求了。。那就開始創建TCP鏈接了。
關於Tcp編程,爲何沒有選用Netty或者Mina框架,而選用以上的技術。 其實我感受仍是他們太複雜。而且,咱們用Netty的話,集羣規則,負載均衡,JVM優化都須要本身作。集羣規則,負載均衡這塊兒,就是另外一個大的研究方向,一我的根本搞不下來。
不如放在容器裏。好比Tomcat,你要真嫌棄Tomcat過低端。換Jboss也不是不行,他們都作了N年的優化和開發,穩定性絕對OK,集羣規則,負載均衡,JVM等等都有現成的解決方案,還有其餘的一些優化 ,能夠說世界頂尖。知足你的高併發一點問題都沒。
不要重複造輪子!!別人(JBoss)的集羣規則好,負載均衡穩定,就用就是了!!!!因此,小的WB應用推薦tomcat,高併發的WB應用,推薦Jboss。而且合理設置集羣規則,合理配置負載均衡,合理優化JVM,我保證,知足你的高併發websocket需求徹底不是問題。。
加上咱們的數據庫選型和消息隊列,都是爲高併發添火的技術,因此代碼寫的乾淨的話,高併發徹底不是問題。不用糾結,WB的效率如何,集羣怎麼作~負載均衡是否是要本身寫。。答案是NO。 解決方案是 用高端點的應用容器!!
這就是WB和TCP比的優點!!他能夠在容器裏搞~ 集羣方案,負載均衡方案都是人家作好的。
原生的TCP協議,你必須本身去解決這些問題。這真是一個大問題,想一想就知道了,單單集羣這塊兒,有幾個能作的好的。。
先貼pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.mendao</groupId> <artifactId>websocket</artifactId> <packaging>war</packaging> <version>1.0.0</version> <description>門道的即時通信服務器</description> <properties> <!--Fast Json--> <fastjson.version>1.1.39</fastjson.version> <!-- Servlet 版本號 --> <servlet.version>3.1.0</servlet.version> <!-- spring版本號 3.2.8.RELEASE --> <spring.version>4.0.3.RELEASE</spring.version> <!-- Hibernate版本號 4.3.5.Final --> <hibernate.version>3.6.10.Final</hibernate.version> <!-- mysql版本號 --> <mysql.version>5.1.30</mysql.version> <!--logback--> <logback.version>1.1.2</logback.version> <!-- xmemcached 版本號 --> <xmemcached.version>2.0.0</xmemcached.version> <!--Activemq --> <activemq.version>5.7.0</activemq.version> <!-- 高速序列化框架 --> <kryo.version>2.23.0</kryo.version> <!--tomcat--> <tomcat.version>8.0.5</tomcat.version> </properties> <dependencies> <!-- 高性能 序列化框架 --> <dependency> <groupId>com.esotericsoftware.kryo</groupId> <artifactId>kryo</artifactId> <version>${kryo.version}</version> </dependency> <!--異步消息隊列--> <dependency> <groupId>org.apache.activemq</groupId> <artifactId>activemq-core</artifactId> <version>${activemq.version}</version> </dependency> <dependency> <groupId>org.apache.xbean</groupId> <artifactId>xbean-spring</artifactId> <version>3.17</version> </dependency> <!-- 必須包 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>${servlet.version}</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.annotation</groupId> <artifactId>jsr250-api</artifactId> <version>1.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.persistence</groupId> <artifactId>persistence-api</artifactId> <version>1.0.2</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.7.4</version> </dependency> <dependency> <groupId>xerces</groupId> <artifactId>xercesImpl</artifactId> <version>2.11.0</version> </dependency> <dependency> <groupId>antlr</groupId> <artifactId>antlr</artifactId> <version>2.7.7</version> </dependency> <dependency> <groupId>dom4j</groupId> <artifactId>dom4j</artifactId> <version>1.6.1</version> </dependency> <!-- spring --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-expression</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-websocket</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jms</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-mongodb</artifactId> <version>1.4.1.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-redis</artifactId> <version>1.2.1.RELEASE</version> </dependency> <!-- mysql驅動包 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <!-- FastJson 來處理JSON數據 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>${fastjson.version}</version> </dependency> <!-- logback--> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>${logback.version}</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> <version>${logback.version}</version> </dependency> <!-- xmemcached 緩存服務器 --> <dependency> <groupId>com.googlecode.xmemcached</groupId> <artifactId>xmemcached</artifactId> <version>${xmemcached.version}</version> </dependency> <!--Common 系列--> <dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>2.6</version> </dependency> <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.3</version> </dependency> <!--加密解密,編碼解碼--> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.9</version> </dependency> <dependency> <groupId>commons-httpclient</groupId> <artifactId>commons-httpclient</artifactId> <version>3.1</version> </dependency> <!--Tomcat 環境支持--> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-websocket</artifactId> <version>${tomcat.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-coyote</artifactId> <version>${tomcat.version}</version> <scope>provided</scope> </dependency> </dependencies> <build> <finalName>ROOT</finalName> <plugins> <!-- 使用JDK1.7編譯java源文件 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.3.2</version> <configuration> <source>1.7</source> <target>1.7</target> <encoding>UTF-8</encoding> </configuration> </plugin> <!-- 使用UTF-8編碼資源文件 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <version>2.5</version> <configuration> <encoding>UTF-8</encoding> </configuration> </plugin> <!--WAR 打包插件--> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>2.2</version> <configuration> <failOnMissingWebXml>false</failOnMissingWebXml> </configuration> </plugin> </plugins> </build> </project>
而後,開始進入正題。web.xml配置。我貼上個人配置。
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:javaee="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javeee/web-app_3_0.xsd" version="3.0"> <!--關鍵點!servlet要是3.0不能再是2.5了--> <welcome-file-list> <welcome-file>welcome.html</welcome-file> </welcome-file-list> <servlet> <servlet-name>springmvc</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextClass</param-name> <param-value> org.springframework.web.context.support.AnnotationConfigWebApplicationContext </param-value> </init-param> <init-param> <param-name>contextConfigLocation</param-name> <param-value> com.mendao.config.WebConfig <!--關鍵點!咱們須要用spring的全註解配置方式來配置websocket,因此這塊兒須要指向你的對應的類--> </param-value> </init-param> </servlet> <!-- Spring MVC --> <servlet-mapping> <servlet-name>springmvc</servlet-name> <url-pattern>/</url-pattern> <!--關鍵點!一旦你使用wb 就不能使用普通的springmvc裏的HTTP請求了,也就是要麼這個程序使用WB的技術要麼使用springmvc的技術,兩者不可兼得!!因此WB和Springmvc必須分開程序寫--> </servlet-mapping> <!-- Spring 監聽器 --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!--Spring Request 監聽器--> <listener> <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class> </listener> <!-- 編碼強轉 --> <filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- Spring 刷新Introspector防止內存泄露 請求多了會內存泄露,加上他就行了--> <listener> <listener-class>org.springframework.web.util.IntrospectorCleanupListener</listener-class> </listener> </web-app>
web.xml沒什麼可說的了。主要點已經標註。而後貼上
com.mendao.config.WebConfig
這個類讓你們一看究竟!!徹底能夠直接複製下來
/** * WebSocket 配置類 */ @Configuration //必定不能少 @ImportResource("classpath*:/applicationContext.xml") //重要!!加載spring的其餘的xml配置文件,這種方式是註解方式+xml方式 相結合的配置方式!! @EnableWebSocket //不能少 public class WebConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer { @Resource private BootstrapHandler clientHandler; //注入實例 @Resource private Bootstrapnterceptor interceptor; //注入實例 @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {//重要!處理器 URL地址 攔截器!! 都在這裏加入!!//等會兒帖 處理器和 攔截器的代碼 //你須要更多處理器 或者URL 都在這裏填就是了。其實通常一個就夠了,一個核心處理器作請求中轉。 registry.addHandler(clientHandler, "/bootstrap").addInterceptors(interceptor); } // Allow serving HTML files through the default Servlet // 徹底能夠無視下面的代碼 @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { configurer.enable(); } }
下面是BootstrapHandler 的代碼!都有註釋
@Service public class BootstrapHandler implements WebSocketHandler { private final Logger logger = LoggerFactory.getLogger(BootstrapHandler.class); @Resource private BootstrapHandlerService bootstrapHandlerService; @Resource private Cached cached; /** * 雙工通信 鏈接後 而且在這裏心跳 * * @param session * @throws Exception */ @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { TextMessage textMessage; try { HttpHeaders headers = session.getHandshakeHeaders(); String userAgent = headers.get("user-agent").get(0); logger.info("LOGIN : " + userAgent); //構造迴應的消息,每次鏈接成功後要回應消息吖!告訴客戶端已經鏈接成功了!消息就在這裏面構造
textMessage = new TextMessage(「鏈接成功」); } catch (Exception e) { e.printStackTrace(); textMessage = new TextMessage(「鏈接失敗」); } //這樣就發送給客戶端了~ 很簡單!!
session.sendMessage(textMessage); } /** * 處理髮送過來的消息 * * @param session * @param message * @throws Exception */ @Override public void handleMessage(WebSocketSession session, WebSocketMessage message) throws Exception { try { //若是鏈接成功!!這裏面會不停的接收到心跳包!! 怎麼處理~看你的了!!! 總之這個方法就是接受客戶端發來消息的方法!!!
// message.getPayload()獲得的是客戶端發來的消息,好比「你好啊!」 之類的。獲得後轉成String就能處理了!
StringBuffer sb = new StringBuffer((String) message.getPayload()); //這個是我本身寫的一個處理業務邏輯。你能夠實現本身的業務邏輯
bootstrapHandlerService.handleMessage(session, sb); } catch (Exception e) { e.printStackTrace(); logger.error(e.getMessage()); } } /** * 客戶端 異常斷開 * * @param session * @param throwable * @throws Exception */ @Override public void handleTransportError(WebSocketSession session, Throwable throwable) throws Exception { logger.info(session.getId() + " - 異常斷開鏈接");
//所謂異常斷開,例如:忽然關閉HTML頁面等等,總之不是用戶正常關閉的! //這個也是我本身實現的 異常處理的業務邏輯,你能夠本身寫
bootstrapHandlerService.handleError(session, throwable); } /** * 鏈接已經斷 開 * * @param session * @param status * @throws Exception */ @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { //只要是斷開鏈接!不論是異常斷開,仍是普通正常斷開,必定會進入這個方法。
String reason = status.getReason(); if (reason == null) { reason = "客戶端 按指令正常退出"; }
logger.info(session.getId() + " - 已經主動關閉鏈接 - 關閉碼 - " + status.getCode() + " - 原因 -" + reason);
//其實這裏面封裝了個session.close()釋放了一些資源, 也是我本身實現的業務邏輯,你也能夠本身寫! bootstrapHandlerService.connectionClose(session); } /** * 握手成功 初始化操做在這裏面進行 * * @return */ @Override public boolean supportsPartialMessages() { //一旦HTTP認證成功 這個方法先被調用 若是返回true 則進行上面那麼N多方法的流程。若是返回的是false就直接攔截掉了。不會調用上面那些方法了!!
//就好像個構造器同樣。這個是處理器 BootstrapHandler的構造器~
return true; } }
而後貼上 Interceptor 攔截器的代碼!! 實現的接口不能變!!裏面沒代碼的緣由是 我實在不知道在這裏面作什麼操做,感受個人業務是用不到這兩個方法。
@Service public class Bootstrapnterceptor implements HandshakeInterceptor { /** * 握手前 * * @param request * @param response * @param webSocketHandler * @param stringObjectMap * @return * @throws Exception */ @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler webSocketHandler, Map<String, Object> stringObjectMap) throws Exception { return true; } /** * 握手成功後 * * @param request * @param response * @param handler * @param e */ @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler, Exception e) { } }
而後我上一個HTML版的客戶端測試程序!!
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=gb2312" /> <title>w</title> <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script> <script type="text/javascript"> var heartbeat_timer = 0; var last_health = -1; var health_timeout = 3000; $(function(){ //ws = ws_conn( "ws://211.100.41.186:9999" ); ws = ws_conn( "ws://127.0.0.1:12345/bootstrap"); $("#send_btn").click(function(){ var msg = $("#mysendbox").val(); alert(msg); alert(ws); ws.send(msg); $("#mysendbox").val(""); }); }); function keepalive( ws ){ var time = new Date(); if( last_health != -1 && ( time.getTime() - last_health > health_timeout ) ){ //此時便可以認爲鏈接斷開,但是設置重連或者關閉 $("#keeplive_box").html( "服務器沒有響應." ).css({"color":"red"}); //ws.close(); } else{ $("#keeplive_box").html( "鏈接正常" ).css({"color":"green"}); if( ws.bufferedAmount == 0 ){ ws.send( '1'); } } } //websocket function function ws_conn( to_url ){ to_url = to_url || ""; if( to_url == "" ){ return false; } clearInterval( heartbeat_timer ); $("#statustxt").html("Connecting..."); var ws = new WebSocket( to_url ); ws.onopen=function(){ $("#statustxt").html("connected."); $("#send_btn").attr("disabled", false); heartbeat_timer = setInterval( function(){keepalive(ws)}, 5000 ); } ws.onerror=function(){ $("#statustxt").html("error."); $("#send_btn").attr("disabled", true); clearInterval( heartbeat_timer ); $("#keeplive_box").html( "鏈接出錯." ).css({"color":"red"}); } ws.onclose=function(){ $("#statustxt").html("closed."); $("#send_btn").attr("disabled", true); clearInterval( heartbeat_timer ); $("#keeplive_box").html( "鏈接已關閉." ).css({"color":"red"}); } ws.onmessage=function(msg){ var time = new Date(); if( msg.data == ( '1' ) ){ last_health = time.getTime(); return; } $("#chatbox").val( $("#chatbox").val() + msg.data + "\n" ); $("#chatbox").attr("scrollTop",$("#chatbox").attr("scrollHeight")); } return ws; } </script> </head> <body> <p>web socket鏈接狀態: <span id="statustxt">鏈接中...</span></p> <p>心跳狀態:<span id="keeplive_box">檢測中...</span></p> <p> <textarea name="chatbox" id="chatbox" cols="55" rows="20" readonly="readonly"></textarea> </p> <p> <p>發送文本到Websocket服務器</p> <input name="mysendbox" type="text" id="mysendbox" size="50" /> <input type="button" name="send_btn" id="send_btn" value="Send" disabled="disabled" /> <input type="button" onclick="javascript:ws.close()" value="Close"/> </p> </body> </html>
核心的就這麼多。
這些方法理解了,其餘的,靠本身發揮想象~
對了,每一個不一樣的鏈接都會有一個不一樣的WebSocketSession session 你能夠把這個session存入一個全局的ConcurrentHashMap中!!做爲鏈接池!!
用的時候 用 map.get(key); 而後就能用sendMessage(); 發送給他消息了!!!
何時存這個session,這就看你的業務須要了。總之每一個WebSocketSession 標識一個徹底不一樣的新的鏈接。客戶句柄來形容,也能夠~
而後雖然你用上了WB 可是仍是要本身作出來。心跳包~ 數據分割處理~ 等等一些基本的業務邏輯~ 什麼地方用消息隊列分發,那就要看你業務怎麼設計了。
最後!!最有用的!!websocket能夠作移動端 (安卓IOS等)即時通信服務器。可是須要用到一個jar包。在github上搜索 websocket client (websocket的客戶端) 有java的實現也有object-c的實現
這個思路提供出來以後,你就知道websocket 的強大了吧。不但敏捷開發!並且跨平臺!!能夠作android推送解決方案!! 固然也能夠整合ios作即時通信!!固然!!HTML更能夠!由於原生的就是HTML!!! 強大的websocket爲企業即時通信方案提供了更好的出路!!!
核心已經講解!更多的發揮想象吧!!!