本博文,保证不用装B的话语和太多专业的语言,保证简单易懂,只要懂JAVAEE开发的人都可以看懂。 本博文发表目的是,目前网上针对Websocket的资料太散乱,导致初学者的知识体系零零散散,学习困难加大。本博加以整理,并且实践。
所用核心技术选型:
Tomcat + Spring 4.0.3 + Mongodb(高并发数据库)
+ SpringQueue(消息队列)+ ActiveMQ (消息队列)
+ Spring-data-Mongo + Servlet 3.0
+Spring-Websocket
+ Maven
注:以下Websocket 均省略成 WB
先说Websocket 的原理。 Websocket 是全双工通讯(说白了就是俩都可以通讯,服务器也可以给客户端发消息,客户端也能给服务器发消息)。也是基于TCP的,效率是很高的,首先这个技术的底层选用,就决定了完全可以用wb这个技术做高并发应用,而且开发非常快!!代码非常简单!!最重要的是稳定性,扩展性等等都有保证,等会儿说为什么说都有保证。
WB 不同于TCP的三次握手。 WB是先进行一次HTTP请求,这个请求头不同于普通HTTP请求,等会贴出来讲解。然后服务器开始辨认请求头,如果是WB的请求头,则开始进行普通的TCP连接,即三次握手(不懂的TCP的,出门百度)。如果不是WB的HTTP请求头,那就是按普通的HTTP请求处理。
流程梳理: 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
4.0.0
com.mendao
websocket
war
1.0.0
门道的即时通讯服务器
1.1.39
3.1.0
4.0.3.RELEASE
3.6.10.Final
5.1.30
1.1.2
2.0.0
5.7.0
2.23.0
8.0.5
com.esotericsoftware.kryo
kryo
${kryo.version}
org.apache.activemq
activemq-core
${activemq.version}
org.apache.xbean
xbean-spring
3.17
javax.servlet
javax.servlet-api
${servlet.version}
provided
javax.annotation
jsr250-api
1.0
provided
javax.persistence
persistence-api
1.0.2
provided
org.aspectj
aspectjweaver
1.7.4
xerces
xercesImpl
2.11.0
antlr
antlr
2.7.7
dom4j
dom4j
1.6.1
org.springframework
spring-aop
${spring.version}
org.springframework
spring-context
${spring.version}
org.springframework
spring-beans
${spring.version}
org.springframework
spring-core
${spring.version}
org.springframework
spring-tx
${spring.version}
org.springframework
spring-web
${spring.version}
org.springframework
spring-webmvc
${spring.version}
org.springframework
spring-jdbc
${spring.version}
org.springframework
spring-expression
${spring.version}
org.springframework
spring-orm
${spring.version}
org.springframework
spring-websocket
${spring.version}
org.springframework
spring-jms
${spring.version}
org.springframework.data
spring-data-mongodb
1.4.1.RELEASE
org.springframework.data
spring-data-redis
1.2.1.RELEASE
mysql
mysql-connector-java
${mysql.version}
com.alibaba
fastjson
${fastjson.version}
ch.qos.logback
logback-classic
${logback.version}
ch.qos.logback
logback-core
${logback.version}
com.googlecode.xmemcached
xmemcached
${xmemcached.version}
commons-lang
commons-lang
2.6
commons-fileupload
commons-fileupload
1.3
commons-codec
commons-codec
1.9
commons-httpclient
commons-httpclient
3.1
org.apache.tomcat
tomcat-websocket
${tomcat.version}
test
org.apache.tomcat
tomcat-coyote
${tomcat.version}
provided
ROOT
org.apache.maven.plugins
maven-compiler-plugin
2.3.2
1.7
1.7
UTF-8
org.apache.maven.plugins
maven-resources-plugin
2.5
UTF-8
org.apache.maven.plugins
maven-war-plugin
2.2
false
然后,开始进入正题。web.xml配置。我贴上我的配置。
welcome.html
springmvc
org.springframework.web.servlet.DispatcherServlet
contextClass
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
contextConfigLocation
com.mendao.config.WebConfig
springmvc
/
org.springframework.web.context.ContextLoaderListener
org.springframework.web.context.request.RequestContextListener
encodingFilter
org.springframework.web.filter.CharacterEncodingFilter
encoding
UTF-8
forceEncoding
true
encodingFilter
/*
org.springframework.web.util.IntrospectorCleanupListener
web.xml没什么可说的了。主要点已经标注。然后贴上
com.mendao.config.WebConfig
这个类让大家一看究竟!!完全可以直接复制下来
/*** WebSocket 配置类*/@Configuration//一定不能少
@ImportResource("classpath*:/applicationContext.xml") //重要!!加载spring的其他的xml配置文件,这种方式是注解方式+xml方式 相结合的配置方式!!
@EnableWebSocket //不能少
public class WebConfig extends WebMvcConfigurerAdapter implementsWebSocketConfigurer {
@Resourceprivate BootstrapHandler clientHandler; //注入实例
@Resourceprivate Bootstrapnterceptor interceptor; //注入实例
@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {//重要!处理器 URL地址 拦截器!! 都在这里加入!!//等会儿帖 处理器和 拦截器的代码//你需要更多处理器 或者URL 都在这里填就是了。其实一般一个就够了,一个核心处理器做请求中转。
registry.addHandler(clientHandler, "/bootstrap").addInterceptors(interceptor);
}//Allow serving HTML files through the default Servlet//完全可以无视下面的代码 @Override
public voidconfigureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
下面是BootstrapHandler 的代码!都有注释
@Servicepublic class BootstrapHandler implementsWebSocketHandler {private final Logger logger = LoggerFactory.getLogger(BootstrapHandler.class);
@ResourceprivateBootstrapHandlerService bootstrapHandlerService;
@ResourceprivateCached cached;/*** 双工通讯 连接后 并且在这里心跳
*
*@paramsession
*@throwsException*/@Overridepublic void afterConnectionEstablished(WebSocketSession session) throwsException {
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);
}/*** 处理发送过来的消息
*
*@paramsession
*@parammessage
*@throwsException*/@Overridepublic void handleMessage(WebSocketSession session, WebSocketMessage message) throwsException {try{
//如果连接成功!!这里面会不停的接收到心跳包!! 怎么处理~看你的了!!! 总之这个方法就是接受客户端发来消息的方法!!!
// message.getPayload()得到的是客户端发来的消息,比如“你好啊!” 之类的。得到后转成String就能处理了!
StringBuffer sb= newStringBuffer((String) message.getPayload());
//这个是我自己写的一个处理业务逻辑。你可以实现自己的业务逻辑
bootstrapHandlerService.handleMessage(session, sb);
}catch(Exception e) {
e.printStackTrace();
logger.error(e.getMessage());
}
}/*** 客户端 异常断开
*
*@paramsession
*@paramthrowable
*@throwsException*/@Overridepublic void handleTransportError(WebSocketSession session, Throwable throwable) throwsException {
logger.info(session.getId()+ " - 异常断开连接");
//所谓异常断开,例如:突然关闭HTML页面等等,总之不是用户正常关闭的!
//这个也是我自己实现的 异常处理的业务逻辑,你可以自己写
bootstrapHandlerService.handleError(session, throwable);
}/*** 连接已经断 开
*
*@paramsession
*@paramstatus
*@throwsException*/@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throwsException {
//只要是断开连接!不管是异常断开,还是普通正常断开,一定会进入这个方法。
String reason=status.getReason();if (reason == null) {
reason= "客户端 按指令正常退出";
}
logger.info(session.getId()+ " - 已经主动关闭连接 - 关闭码 - " + status.getCode() + " - 缘由 -" +reason);
//其实这里面封装了个session.close()释放了一些资源, 也是我自己实现的业务逻辑,你也可以自己写!
bootstrapHandlerService.connectionClose(session);
}/*** 握手成功 初始化操作在这里面进行
*
*@return
*/@Overridepublic booleansupportsPartialMessages() {
//一旦HTTP认证成功 这个方法先被调用 如果返回true 则进行上面那么N多方法的流程。如果返回的是false就直接拦截掉了。不会调用上面那些方法了!!
//就好像个构造器一样。这个是处理器 BootstrapHandler的构造器~return true;
}
}
然后贴上 Interceptor 拦截器的代码!! 实现的接口不能变!!里面没代码的原因是 我实在不知道在这里面做什么操作,感觉我的业务是用不到这两个方法。
@Servicepublic class Bootstrapnterceptor implementsHandshakeInterceptor {/*** 握手前
*
*@paramrequest
*@paramresponse
*@paramwebSocketHandler
*@paramstringObjectMap
*@return*@throwsException*/@Overridepublic boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler webSocketHandler, Map stringObjectMap) throwsException {return true;
}/*** 握手成功后
*
*@paramrequest
*@paramresponse
*@paramhandler
*@parame*/@Overridepublic voidafterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler, Exception e) {
}
}
然后我上一个HTML版的客户端测试程序!!
wvarheartbeat_timer= 0;varlast_health= -1;varhealth_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(){varmsg=$("#mysendbox").val();
alert(msg);
alert(ws);
ws.send(msg);
$("#mysendbox").val("");
});
});functionkeepalive( ws ){vartime= newDate();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
functionws_conn( to_url ){
to_url=to_url|| "";if( to_url== ""){return false;
}
clearInterval( heartbeat_timer );
$("#statustxt").html("Connecting...");varws= newWebSocket( to_url );
ws.onopen=function(){
$("#statustxt").html("connected.");
$("#send_btn").attr("disabled",false);
heartbeat_timer=setInterval(function(){keepalive(ws)},5000);
}
ws.οnerrοr=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){vartime= newDate();if( msg.data==('1') ){
last_health=time.getTime();return;
}
$("#chatbox").val( $("#chatbox").val()+msg.data+ "\n");
$("#chatbox").attr("scrollTop",$("#chatbox").attr("scrollHeight"));
}returnws;
}
web socket连接状态: 连接中...
心跳状态:检测中...
发送文本到Websocket服务器
核心的就这么多。
这些方法理解了,其他的,靠自己发挥想象~
对了,每个不同的连接都会有一个不同的WebSocketSession session 你可以把这个session存入一个全局的ConcurrentHashMap中!!作为连接池!!
用的时候 用 map.get(key); 然后就能用sendMessage(); 发送给他消息了!!!
什么时候存这个session,这就看你的业务需要了。总之每个WebSocketSession 标识一个完全不同的新的连接。客户句柄来形容,也可以~
然后虽然你用上了WB 但是还是要自己做出来。心跳包~ 数据分割处理~ 等等一些基本的业务逻辑~ 什么地方用消息队列分发,那就要看你业务怎么设计了。
离线消息怎么做,可以找我私聊QQ。631768417 有不懂的也可以私聊我QQ
最后!!最有用的!!websocket可以做移动端 (安卓IOS等)即时通讯服务器。但是需要用到一个jar包。在github上搜索 websocket client (websocket的客户端) 有java的实现也有object-c的实现
这个思路提供出来之后,你就知道websocket 的强大了吧。不但敏捷开发!而且跨平台!!可以做android推送解决方案!! 当然也可以整合ios做即时通讯!!当然!!HTML更可以!因为原生的就是HTML!!! 强大的websocket为企业即时通讯方案提供了更好的出路!!!
核心已经讲解!更多的发挥想象吧!!!