点击关注公众号,利用碎片时间学习
WebSocket简介
WebSocket
协议是完全重新设计的协议,旨在为Web上的双向数据传输问题提供一个切实可行的解决方案,使得客户端和服务器之间可以在任意时刻传输消息,因此,这也就要求它们异步地处理消息回执
WebSocket特点:
HTML5
中的协议,实现与客户端与服务器双向,基于消息的文本或二进制数据通信
适合于对数据的实时性要求比较强的场景,如通信、直播、共享桌面,特别适合于客户端与服务端频繁交互的情况下,如实时共享、多人协作等平台
采用新的协议,后端需要单独实现
客户端并不是所有浏览器都支持
WebSocket通信握手
在从标准的HTTP
或者HTTPS
协议切换到WebSocket
时,将会使用一种称为握手的机制 ,因此,使用WebSocket
的应用程序将始终以HTTP/S
作为开始,然后再执行升级。这个升级动作发生的确切时刻特定于应用程序;它可能会发生在启动时,也可能会发生在请求了某个特定的URL之后
下面是WebSocket
请求和响应的标识信息:
客户端的请求:
Connection
属性中标识Upgrade
,表示客户端希望连接升级
Upgrade
属性中标识为Websocket
,表示希望升级成Websocket
协议
Sec-WebSocket-Key
属性,表示随机字符串,服务器端会用这些数据来构造出一个 SHA-1 的信息摘要。把 “Sec-WebSocket-Key
” 加上一个特殊字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11
”,然后计算 SHA-1 摘要,之后进行 BASE-64 编码,将结果做为 “Sec-WebSocket-Accept
” 头的值,返回给客户端。如此操作,可以尽量避免普通 HTTP 请求被误认为 Websocket 协议。
Sec-WebSocket-Version
属性,表示支持的Websocket
版本,RFC6455
要求使用的版本是 13,之前草案的版本均应当弃用
服务器端响应:
Upgrade
属性中标识为websocket
Connection
告诉客户端即将升级的是Websocket
协议
Sec-WebSocket-Accept
这个则是经过服务器确认,并且加密过后的Sec-WebSocket-Key
Netty为WebSocket数据帧提供的支持
由 IETF 发布的WebSocket RFC
,定义了6种帧,Netty为它们每种都提供了一个POJO实现
实战
首先,定义WebSocket服务端,其中创建了一个Netty提供ChannelGroup
变量用来记录所有已经连接的客户端channel,而这个ChannelGroup
就是用来完成群发和单聊功能的
//定义websocket服务端publicclassWebSocketServer{privatestaticEventLoopGroupbossGroup=newNioEventLoopGroup(1);privatestaticEventLoopGroupworkerGroup=newNioEventLoopGroup();privatestaticServerBootstrapbootstrap=newServerBootstrap();privatestaticfinalintPORT=8761;//创建DefaultChannelGroup,用来保存所有已经连接的WebSocketChannel,群发和一对一功能可以用上privatefinalstaticChannelGroupchannelGroup=newDefaultChannelGroup(ImmediateEventExecutor.INSTANCE);publicstaticvoidstartServer(){try{bootstrap.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class).childHandler(newWebSocketServerInitializer(channelGroup));Channelch=bootstrap.bind(PORT).sync().channel();System.out.println("打开浏览器访问:http://127.0.0.1:"+PORT+'/');ch.closeFuture().sync();}catch(Exceptione){e.printStackTrace();}finally{bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}publicstaticvoidmain(String[]args){startServer();}}
接下来,初始化Pipeline,向当前Pipeline
中注册所有必需的ChannelHandler
,主要包括:用于处理HTTP请求编解码的HttpServerCodec
、自定义的处理HTTP请求的HttpRequestHandler
、用于处理WebSocket帧数据以及升级握手的WebSocketServerProtocolHandler
以及自定义的处理TextWebSocketFrame
数据帧和握手完成事件的WebSocketServerHanlder
publicclassWebSocketServerInitializerextendsChannelInitializer<SocketChannel>{/*websocket访问路径*/privatestaticfinalStringWEBSOCKET_PATH="/ws";privateChannelGroupchannelGroup;publicWebSocketServerInitializer(ChannelGroupchannelGroup){this.channelGroup=channelGroup;}@OverrideprotectedvoidinitChannel(SocketChannelch)throwsException{//用于HTTP请求的编解码ch.pipeline().addLast(newHttpServerCodec());//用于写入一个文件的内容ch.pipeline().addLast(newChunkedWriteHandler());//用于http请求的聚合ch.pipeline().addLast(newHttpObjectAggregator(64*1024));//用于WebSocket应答数据压缩传输ch.pipeline().addLast(newWebSocketServerCompressionHandler());//处理http请求,对非websocket请求的处理ch.pipeline().addLast(newHttpRequestHandler(WEBSOCKET_PATH));//根据websocket规范,处理升级握手以及各种websocket数据帧ch.pipeline().addLast(newWebSocketServerProtocolHandler(WEBSOCKET_PATH,"",true));//对websocket的数据进行处理,主要处理TextWebSocketFrame数据帧和握手完成事件ch.pipeline().addLast(newWebSocketServerHanlder(channelGroup));}}
HttpRequestHandler
用来处理HTTP请求,首先会先确认当前的HTTP请求是否指向了WebSocket
的URI,如果是那么HttpRequestHandler
将调用FullHttpRequest
对象上的retain方法,并通过调用fireChannelRead(msg)
方法将它转发给下一个ChannelInboundHandler
(之所以调用retain方法,是因为调用channelRead0
方法完成之后,会进行资源释放)
接下来,读取磁盘上指定路径的index.html
文件内容,将内容封装成ByteBuf对象,之后,构造一个FullHttpResponse
响应对象,将ByteBuf
添加进去,并设置请求头信息。最后,调用writeAndFlush
方法冲刷所有写入的消息
publicclassHttpRequestHandlerextendsSimpleChannelInboundHandler<FullHttpRequest>{privatestaticfinalFileINDEX=newFile("D:/学习/index.html");privateStringwebsocketUrl;publicHttpRequestHandler(StringwebsocketUrl){this.websocketUrl=websocketUrl;}@OverrideprotectedvoidchannelRead0(ChannelHandlerContextctx,FullHttpRequestmsg)throwsException{if(websocketUrl.equalsIgnoreCase(msg.getUri())){//如果该HTTP请求指向了websocketUrl的URL,那么直接交给下一个ChannelInboundHandler进行处理ctx.fireChannelRead(msg.retain());}else{//生成index页面的具体内容,并送往浏览器ByteBufcontent=loadIndexHtml();FullHttpResponseres=newDefaultFullHttpResponse(HTTP_1_1,OK,content);res.headers().set(HttpHeaderNames.CONTENT_TYPE,"text/html;charset=UTF-8");HttpUtil.setContentLength(res,content.readableBytes());sendHttpResponse(ctx,msg,res);}}publicstaticByteBufloadIndexHtml(){FileInputStreamfis=null;InputStreamReaderisr=null;BufferedReaderraf=null;StringBuffercontent=newStringBuffer();try{fis=newFileInputStream(INDEX);isr=newInputStreamReader(fis);raf=newBufferedReader(isr);Strings=null;//读取文件内容,并将其打印while((s=raf.readLine())!=null){content.append(s);}}catch(Exceptione){//TODOAuto-generatedcatchblocke.printStackTrace();}finally{try{fis.close();isr.close();raf.close();}catch(IOExceptione){//TODOAuto-generatedcatchblocke.printStackTrace();}}returnUnpooled.copiedBuffer(content.toString().getBytes());}/*发送应答*/privatestaticvoidsendHttpResponse(ChannelHandlerContextctx,FullHttpRequestreq,FullHttpResponseres){//错误的请求进行处理(code<>200).if(res.status().code()!=200){ByteBufbuf=Unpooled.copiedBuffer(res.status().toString(),CharsetUtil.UTF_8);res.content().writeBytes(buf);buf.release();HttpUtil.setContentLength(res,res.content().readableBytes());}//发送应答.ChannelFuturef=ctx.channel().writeAndFlush(res);//对于不是长连接或者错误的请求直接关闭连接if(!HttpUtil.isKeepAlive(req)||res.status().code()!=200){f.addListener(ChannelFutureListener.CLOSE);}}}
前面的HttpRequestHandler
处理器只是用来管理HTTP请求和响应的,而实际对传输的WebSocket
数据帧的处理是交由WebSocketServerHanlder
进行(其中只对TextWebSocketFrame
类型的数据帧进行处理)。
WebSocketServerHanlder
处理时通过重写userEventTriggered
方法,并监听握手成功的事件,当新客户端的WebSocket
握手成功之后,它将通过把通知消息写到ChannelGroup
中的所有channel来通知所有已经连接的客户端,然后它将这个新的channel加入到该ChannelGroup
中,并且还为每个channel随机生成了一个用户
之后,如果接收到了TextWebSocketFrame
消息时,会先根据当前channel拿到用户,并解析发送的文本帧信息,确认是群聊还是单聊,最后,构造TextWebSocketFrame
响应内容,通过writeAndFlush
进行冲刷
/***对websocket的文本数据帧进行处理**/publicclassWebSocketServerHanlderextendsSimpleChannelInboundHandler<TextWebSocketFrame>{privateChannelGroupchannelGroup;publicWebSocketServerHanlder(ChannelGroupchannelGroup){this.channelGroup=channelGroup;}@OverrideprotectedvoidchannelRead0(ChannelHandlerContextctx,TextWebSocketFramemsg)throwsException{//获取当前channel用户名StringuserName=UserMap.getUser(ctx.channel().id().asLongText());//文本帧Stringcontent=msg.text();System.out.println("Client:"+userName+"received["+content+"]");StringtoName=null;//判断是单聊还是群发(单聊会通过user@msg这种格式进行传输文本帧)if(content.contains("@")){String[]str=content.split("@");content=str[1];//获取单聊的用户toName=str[0];}if(null!=toName){Iterator<Channel>it=channelGroup.iterator();while(it.hasNext()){Channelchannel=it.next();//找到指定的用户if(UserMap.getUser(channel.id().asLongText()).equals(toName)){//单聊channel.writeAndFlush(newTextWebSocketFrame(userName+"@"+content));}}}else{channelGroup.remove(ctx.channel());//群发实现channelGroup.writeAndFlush(newTextWebSocketFrame(userName+"@"+content));channelGroup.add(ctx.channel());}}@OverridepublicvoiduserEventTriggered(ChannelHandlerContextctx,Objectevt)throwsException{//检测事件,如果是握手成功事件,做点业务处理if(evt==WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE){StringchannelId=ctx.channel().id().asLongText();//随机为当前channel指定一个用户名UserMap.setUser(channelId);System.out.println("新的客户端连接:"+UserMap.getUser(channelId));//通知所有已经连接的WebSocket客户端新的客户端已经连接上了channelGroup.writeAndFlush(newTextWebSocketFrame(UserMap.getUser(channelId)+"加入群聊"));//将新的WebSocketChannel添加到ChannelGroup中channelGroup.add(ctx.channel());}else{super.userEventTriggered(ctx,evt);}}}
index.html内容
<html><head><metahttp-equiv="Content-Type"content="text/html;charset=utf-8"/><title>基于WebSocket实现网页版群聊</title></head><body><scripttype="text/javascript">varuserName=null;varsocket;varmyDate=newDate();if(!window.WebSocket){window.WebSocket=window.MozWebSocket;}if(window.WebSocket){socket=newWebSocket("ws://127.0.0.1:8761/ws");socket.onmessage=function(event){varinfo=document.getElementById("jp-container");vardataObj=event.data;if(dataObj.indexOf("@")!=-1){vararr=dataObj.split('@');varsendUser;varacceptMsg;for(vari=0;i<arr.length;i++){if(i==0){sendUser=arr[i];}else{acceptMsg=arr[i];}}if(userName==sendUser){return;}vartalk=document.createElement("div");talk.setAttribute("class","talk_recordboxme");talk.innerHTML=sendUser+':';varrecordtext=document.createElement("div");recordtext.setAttribute("class","talk_recordtextbg");talk.appendChild(recordtext);vartalk_recordtext=document.createElement("div");talk_recordtext.setAttribute("class","talk_recordtext");varh3=document.createElement("h3");h3.innerHTML=acceptMsg;talk_recordtext.appendChild(h3);varspan=document.createElement("span");span.innerHTML=myDate.toLocaleTimeString();span.setAttribute("class","talk_time");talk_recordtext.appendChild(span);talk.appendChild(talk_recordtext);}else{vartalk=document.createElement("div");talk.style.textAlign="center";varfont=document.createElement("font");font.color='#212121';font.innerHTML=dataObj+':'+myDate.toLocaleString();talk.appendChild(font);}info.appendChild(talk);};socket.onopen=function(event){console.log("Socket已打开");};socket.onclose=function(event){console.log("Socket已关闭");};}else{alert("YourbrowserdoesnotsupportWebSocket.");}functionsend(message){if(!window.WebSocket){return;}if(socket.readyState==WebSocket.OPEN){varinfo=document.getElementById("jp-container");vartalk=document.createElement("div");talk.setAttribute("class","talk_recordbox");varuser=document.createElement("div");user.setAttribute("class","user");talk.appendChild(user);varrecordtext=document.createElement("div");recordtext.setAttribute("class","talk_recordtextbg");talk.appendChild(recordtext);vartalk_recordtext=document.createElement("div");talk_recordtext.setAttribute("class","talk_recordtext");varh3=document.createElement("h3");h3.innerHTML=message;talk_recordtext.appendChild(h3);varspan=document.createElement("span");span.innerHTML=myDate.toLocaleTimeString();span.setAttribute("class","talk_time");talk_recordtext.appendChild(span);talk.appendChild(talk_recordtext);info.appendChild(talk);socket.send(message);}else{alert("Thesocketisnotopen.");}}</script><br><br><divclass="talk"><divclass="talk_title"><span>群聊</span></div><divclass="talk_record"style="background:#EEEEF4;"><divid="jp-container"class="jp-container"></div></div><formonsubmit="returnfalse;"><divclass="talk_word"> <inputclass="add_face"id="facial"type="button"title="添加表情"value=""/><inputclass="messagesemotion"autocomplete="off"name="message"value="在这里输入文字"onFocus="if(this.value=='在这里输入文字'){this.value='';}"onblur="if(this.value==''){this.value='在这里输入文字';}"/><inputclass="talk_send"type="button"title="发送"value="发送"onclick="send(this.form.message.value)"/></div></form></div>
样式
body{font-family:verdana,Arial,Helvetica,"宋体",sans-serif;font-size:12px;}body,div,dl,dt,dd,ol,li,h1,h2,h3,h4,h5,h6,pre,form,fieldset,input,P,blockquote,th,td,img,INS{margin:0px;padding:0px;border:0;}ol{list-style-type:none;}img,input{border:none;}a{color:#198DD0;text-decoration:none;}a:hover{color:#ba2636;text-decoration:underline;}a{blr:expression(this.onFocus=this.blur())}/*去掉a标签的虚线框,避免出现奇怪的选中区域*/:focus{outline:0;}.talk{height:480px;width:335px;margin:0auto;border-left-width:1px;border-left-style:solid;border-left-color:#444;}.talk_title{width:100%;height:40px;line-height:40px;text-indent:12px;font-size:16px;font-weight:bold;color:#afafaf;background:#212121;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#434343;font-family:"微软雅黑";}.talk_titlespan{float:left}.talk_title_c{width:100%;height:30px;line-height:30px;}.talk_record{width:100%;height:398px;overflow:hidden;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#434343;margin:0px;}.talk_word{line-height:40px;height:40px;width:100%;background:#212121;}.messages{height:24px;width:240px;text-indent:5px;overflow:hidden;font-size:12px;line-height:24px;color:#666;background-color:#ccc;border-radius:3px;-moz-border-radius:3px;-webkit-border-radius:3px;}.messages:hover{background-color:#fff;}.talk_send{width:50px;height:24px;line-height:24px;font-size:12px;border:0px;margin-left:2px;color:#fff;background-repeat:no-repeat;background-position:0px0px;background-color:transparent;font-family:"微软雅黑";}.talk_send:hover{background-position:0px-24px;}.talk_recordul{padding-left:5px;}.talk_recordli{line-height:25px;}.talk_word.controlbtna{margin:12px;}.talk.talk_word.order{float:left;display:block;height:14px;width:16px;background-repeat:no-repeat;background-position:0px0px;}.talk.talk_word.loop{float:left;display:block;height:14px;width:16px;background-repeat:no-repeat;background-position:-30px0px;}.talk.talk_word.single{float:left;display:block;height:14px;width:16px;background-repeat:no-repeat;background-position:-60px0px;}.talk.talk_word.order:hover,.talk.talk_word.active{background-position:0px-20px;text-decoration:none;}.talk.talk_word.loop:hover{background-position:-30px-20px;text-decoration:none;}.talk.talk_word.single:hover{background-position:-60px-20px;text-decoration:none;}/*讨论区*/.jp-container.talk_recordbox{min-height:80px;color:#afafaf;padding-top:5px;padding-right:10px;padding-left:10px;padding-bottom:0px;}.jp-container.talk_recordbox:first-child{border-top:none;}.jp-container.talk_recordbox:last-child{border-bottom:none;}.jp-container.talk_recordbox.talk_recordtextbg{float:left;width:10px;height:30px;display:block;background-repeat:no-repeat;background-position:lefttop;}.jp-container.talk_recordbox.talk_recordtext{-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px;background-color:#b8d45c;width:240px;height:auto;display:block;padding:5px;float:left;color:#333333;}.jp-container.talk_recordboxh3{font-size:14px;padding:2px05px0;text-transform:uppercase;font-weight:100;}.jp-container.talk_recordbox.user{float:left;display:inline;height:45px;width:45px;margin-top:0px;margin-right:5px;margin-bottom:0px;margin-left:0px;font-size:12px;line-height:20px;text-align:center;}/*自己发言样式*/.jp-container.talk_recordboxme{display:block;min-height:80px;color:#afafaf;padding-top:5px;padding-right:10px;padding-left:10px;padding-bottom:0px;}.jp-container.talk_recordboxme.talk_recordtextbg{float:right;width:10px;height:30px;display:block;background-repeat:no-repeat;background-position:lefttop;}.jp-container.talk_recordboxme.talk_recordtext{-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px;background-color:#fcfcfc;width:240px;height:auto;padding:5px;color:#666;font-size:12px;float:right;}.jp-container.talk_recordboxmeh3{font-size:14px;padding:2px05px0;text-transform:uppercase;font-weight:100;color:#333333;}.jp-container.talk_recordboxme.user{float:right;height:45px;width:45px;margin-top:0px;margin-right:10px;margin-bottom:0px;margin-left:5px;font-size:12px;line-height:20px;text-align:center;display:inline;}.talk_time{color:#666;text-align:right;width:240px;display:block;}
测试
首先,启动三个窗口
群聊
单聊
总结
本文,基于Netty实战了一个WebSocket
协议实现的网页版聊天室服务器,从代码上可以看出,基于Netty的WebSocket
的实现还是非常简单、容易实现的。
但是WebSocket
协议使用上还是存在局限的,比如需要浏览器的支持。但是毕竟WebSocket
代表了Web技术的一种重要进展,可以扩宽我们的视野,在一些特定的工作场景中,可以帮助我们解决一些问题
来源:/wzljiayou/article/details/110506164
推荐:最全的java面试题库PS:因为公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。点“在看”支持我们吧!