导航菜单
首页 » 问答 » 正文

使用 WebSocket 实现一个网页版的聊天室(摸鱼更隐蔽)

简介

协议是完全重新设计的协议,旨在为Web上的双向数据传输问题提供一个切实可行的解决方案,使得客户端和服务器之间可以在任意时刻传输消息,因此,这也就要求它们异步地处理消息回执

特点:通信握手

在从标准的 HTTP 或者 HTTPS协议切换到时,将会使用一种称为握手的机制 ,因此,使用的应用程序将始终以HTTP/S作为开始,然后再执行升级。这个升级动作发生的确切时刻特定于应用程序;它可能会发生在启动时,也可能会发生在请求了某个特定的URL之后

下面是请求和响应的标识信息:

客户端的请求:服务器端响应:Netty为数据帧提供的支持

由 IETF 发布的 RFC,定义了6种帧,Netty为它们每种都提供了一个POJO实现

实战

首先,定义服务端,其中创建了一个Netty提供变量用来记录所有已经连接的客户端,而这个就是用来完成群发和单聊功能的

//定义websocket服务端
public class WebSocketServer {

 private static EventLoopGroup bossGroup = new NioEventLoopGroup(1);
 private static EventLoopGroup workerGroup = new NioEventLoopGroup();
    private static ServerBootstrap bootstrap = new ServerBootstrap();
 
 private static final int PORT =8761;

 //创建 DefaultChannelGroup,用来保存所有已经连接的 WebSocket Channel,群发和一对一功能可以用上
 private final static ChannelGroup channelGroup =
            new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
 
 public static void startServer(){
  try {
   bootstrap.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .childHandler(new WebSocketServerInitializer(channelGroup))
;
            Channel ch = bootstrap.bind(PORT).sync().channel();
            System.out.println("打开浏览器访问: http://127.0.0.1:" + PORT + '/');
            ch.closeFuture().sync();
  } catch (Exception e) {
   e.printStackTrace();
  }finally{
   bossGroup.shutdownGracefully();
         workerGroup.shutdownGracefully();
  }
 }
 public static void main(String[] args) {
  startServer();
 }
}

接下来,初始化,向当前中注册所有必需的,主要包括:用于处理HTTP请求编解码的、自定义的处理HTTP请求的、用于处理帧数据以及升级握手的以及自定义的处理数据帧和握手完成事件的er

public class WebSocketServerInitializer extends ChannelInitializer<SocketChannel>{

 /*websocket访问路径*/
    private static final String WEBSOCKET_PATH = "/ws";
 
    private ChannelGroup channelGroup;
 
 public WebSocketServerInitializer(ChannelGroup channelGroup){
  this.channelGroup=channelGroup;
 } 
    
 @Override
 protected void initChannel(SocketChannel ch) throws Exception {
  //用于HTTP请求的编解码
  ch.pipeline().addLast(new HttpServerCodec());
  //用于写入一个文件的内容
  ch.pipeline().addLast(new ChunkedWriteHandler());
  //用于http请求的聚合
  ch.pipeline().addLast(new HttpObjectAggregator(64*1024));
  //用于WebSocket应答数据压缩传输
  ch.pipeline().addLast(new WebSocketServerCompressionHandler());
  //处理http请求,对非websocket请求的处理
  ch.pipeline().addLast(new HttpRequestHandler(WEBSOCKET_PATH));
  //根据websocket规范,处理升级握手以及各种websocket数据帧
  ch.pipeline().addLast(new WebSocketServerProtocolHandler(WEBSOCKET_PATH, ""true));
  //对websocket的数据进行处理,主要处理TextWebSocketFrame数据帧和握手完成事件
  ch.pipeline().addLast(new WebSocketServerHanlder(channelGroup));
 }
}

用来处理HTTP请求,首先会先确认当前的HTTP请求是否指向了的URI,如果是那么将调用对象上的方法,并通过调用(msg)方法将它转发给下一个r(之所以调用方法,是因为调用方法完成之后,会进行资源释放)

接下来,读取磁盘上指定路径的index.html文件内容,将内容封装成对象,之后,构造一个响应对象,将添加进去,并设置请求头信息。最后,调用方法冲刷所有写入的消息

public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest>{

 private static final File INDEX = new File("D:/学习/index.html");
 
 private String websocketUrl;
 
 public HttpRequestHandler(String websocketUrl)
 
{
  this.websocketUrl = websocketUrl;
 }
 
 @Override
 protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
  if(websocketUrl.equalsIgnoreCase(msg.getUri())){
   //如果该HTTP请求指向了websocketUrl的URL,那么直接交给下一个ChannelInboundHandler进行处理
   ctx.fireChannelRead(msg.retain());
  }else{
   //生成index页面的具体内容,并送往浏览器
   ByteBuf content = loadIndexHtml(); 
   FullHttpResponse res = new DefaultFullHttpResponse(
                      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);
  }
 }
 
 public static ByteBuf loadIndexHtml(){
  FileInputStream fis = null;
  InputStreamReader isr = null;
  BufferedReader  raf = null;
  StringBuffer content = new StringBuffer();
  try {
     fis = new FileInputStream(INDEX);
     isr = new InputStreamReader(fis);
     raf = new BufferedReader(isr);
     String s = null;
     // 读取文件内容,并将其打印
     while((s = raf.readLine()) != null) {
     content.append(s);
     }
   } catch (Exception e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  } finally {
   try {
    fis.close();
    isr.close();
    raf.close();
   } catch (IOException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
   }
  }
  return Unpooled.copiedBuffer(content.toString().getBytes());
 }
  /*发送应答*/
    private static void sendHttpResponse(ChannelHandlerContext ctx,
                                         FullHttpRequest req,
                                         FullHttpResponse res)
 
{
        // 错误的请求进行处理 (code<>200).
        if (res.status().code() != 200) {
            ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(),
                    CharsetUtil.UTF_8);
            res.content().writeBytes(buf);
            buf.release();
            HttpUtil.setContentLength(res, res.content().readableBytes());
        }

        // 发送应答.
        ChannelFuture f = ctx.channel().writeAndFlush(res);
        //对于不是长连接或者错误的请求直接关闭连接
        if (!HttpUtil.isKeepAlive(req) || res.status().code() != 200) {
            f.addListener(ChannelFutureListener.CLOSE);
        }
    }
}

前面的处理器只是用来管理HTTP请求和响应的,而实际对传输的数据帧的处理是交由er 进行(其中只对类型的数据帧进行处理)。

er 处理时通过重写方法,并监听握手成功的事件,当新客户端的握手成功之后,它将通过把通知消息写到中的所有来通知所有已经连接的客户端,然后它将这个新的加入到该中,并且还为每个随机生成了一个用户

之后,如果接收到了消息时,会先根据当前拿到用户,并解析发送的文本帧信息,确认是群聊还是单聊,最后,构造响应内容,通过进行冲刷

/**
 * 对websocket的文本数据帧进行处理
 *
 */

public class WebSocketServerHanlder extends SimpleChannelInboundHandler<TextWebSocketFrame>{

 
 private ChannelGroup channelGroup;
 
 public WebSocketServerHanlder(ChannelGroup channelGroup){
  this.channelGroup=channelGroup;
 }
 
 @Override
 protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
  //获取当前channel用户名
  String userName=UserMap.getUser(ctx.channel().id().asLongText());
     //文本帧
  String content= msg.text();
  System.out.println("Client: "+ userName+" received [ "+content+" ]");
  String toName = null;
  //判断是单聊还是群发(单聊会通过  user@ msg 这种格式进行传输文本帧)
  if(content.contains("@")){
   String[] str= content.split("@");
   content=str[1];
   //获取单聊的用户
   toName = str[0];
  }
  if(null!=toName){
   Iterator it=channelGroup.iterator();
   while(it.hasNext()){
    Channel channel=it.next();
    //找到指定的用户
    if(UserMap.getUser(channel.id().asLongText()).equals(toName)){
     //单聊
     channel.writeAndFlush(new TextWebSocketFrame(userName+"@"+content));
    }
   }
  }else{
   channelGroup.remove(ctx.channel());
   //群发实现
   channelGroup.writeAndFlush(new TextWebSocketFrame(userName+"@"+content));
   channelGroup.add(ctx.channel());
  }
 }
 @Override
 public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
  //检测事件,如果是握手成功事件,做点业务处理
  if(evt==WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE){
   String channelId = ctx.channel().id().asLongText();
   //随机为当前channel指定一个用户名
   UserMap.setUser(channelId);
   System.out.println("新的客户端连接:"+UserMap.getUser(channelId));
   //通知所有已经连接的 WebSocket 客户端新的客户端已经连接上了
   channelGroup.writeAndFlush(new TextWebSocketFrame(UserMap.getUser(channelId)+"加入群聊"));
   //将新的 WebSocket Channel 添加到 ChannelGroup 中
   channelGroup.add(ctx.channel());
  }else{
   super.userEventTriggered(ctx, evt);
  }
 }
}

index.html内容

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>基于WebSocket实现网页版群聊title>
head>
<body>
<script type="text/javascript">   
           var userName= null;        
           var socket;        
           var myDate = new Date();
           if (!window.WebSocket) {
                window.WebSocket = window.MozWebSocket;
            }
            if (window.WebSocket) {
                socket = new WebSocket("ws://127.0.0.1:8761/ws");
                socket.onmessage = function(event
                   var info = document.getElementById("jp-container");
                   var dataObj=event.data;
                   if(dataObj.indexOf("@")!=-1){
                        var arr = dataObj.split('@');
                        var sendUser;
                        var acceptMsg;
                        for(var i=0;i                            if(i==0){
                                 sendUser = arr[i];
                            }else{
                                 acceptMsg =arr[i];
                            }
                        }
                      if(userName==sendUser){
                             return;
                      }        
                      var talk= document.createElement("div");
                      talk.setAttribute("class""talk_recordboxme");
                      talk.innerHTML = sendUser+':';
                      var recordtext= document.createElement("div");
                      recordtext.setAttribute("class""talk_recordtextbg");
                      talk.appendChild(recordtext);
                      var talk_recordtext=document.createElement("div");
                      talk_recordtext.setAttribute("class"" talk_recordtext");
                       var h3=document.createElement("h3");
                       h3.innerHTML =acceptMsg;
                       talk_recordtext.appendChild(h3);
                       var span=document.createElement("span");
                       span.innerHTML =myDate.toLocaleTimeString();
                       span.setAttribute("class""talk_time");
                       talk_recordtext.appendChild(span);
                       talk.appendChild(talk_recordtext);
                   }else{
                       var talk= document.createElement("div");
                       talk.style.textAlign="center";
                       var font = 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("Your browser does not support Web Socket.");
            }
                function send(message{
                    if (!window.WebSocket) { return; }
                       if (socket.readyState == WebSocket.OPEN) {
                         var info = document.getElementById("jp-container");

                   var talk= document.createElement("div");
                   talk.setAttribute("class""talk_recordbox");

                    var user = document.createElement("div");
                    user.setAttribute("class""user");
                    talk.appendChild(user);
                     var recordtext= document.createElement("div");
     
                   recordtext.setAttribute("class""talk_recordtextbg");
                    talk.appendChild(recordtext);

                      var talk_recordtext=document.createElement("div");
                      talk_recordtext.setAttribute("class"" talk_recordtext");

                       var h3=document.createElement("h3");
                      h3.innerHTML =message;
                      talk_recordtext.appendChild(h3);
                     var span=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("The socket is not open.");
                      }
                }
script>

<br>
<br>
<div class="talk">
 <div class="talk_title"><span>群聊span>div>
 <div class="talk_record" style="background: #EEEEF4;">
  <div id="jp-container" class="jp-container">
  div>
 
 div>
  <form onsubmit="return false;">
       <div class="talk_word">
   
  <input class="add_face" id="facial" type="button" title="添加表情" value="" />
  <input class="messages emotion" autocomplete="off" name="message" value="在这里输入文字" onFocus="if(this.value=='在这里输入文字'){this.value='';}"  onblur="if(this.value==''){this.value='在这里输入文字';}"  />
  <input class="talk_send" type="button" title="发送" value="发送"  onclick="send(this.form.message.value)" />
       div>
               form
div>

样式

body{
 font-family:verdana, Arial, Helvetica, "宋体", sans-serif;
 font-size12px;
}

body ,div ,dl ,dt ,dd ,ol ,li ,h1 ,h2 ,h3 ,h4 ,h5 ,h6 ,pre ,form ,fieldset ,input ,P ,blockquote ,th ,td ,img,
INS {
 margin0px;
 padding0px;
 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{
 height480px;
 width335px;
 margin:0 auto;
 border-left-width1px;
 border-left-style: solid;
 border-left-color#444;
}
.talk_title{
 width100%;
 height:40px;
 line-height:40px;
 text-indent12px;
 font-size16px;
 font-weight: bold;
 color#afafaf;
 background:#212121;
 border-bottom-width1px;
 border-bottom-style: solid;
 border-bottom-color#434343;
 font-family"微软雅黑";
}
.talk_title span{float:left}
.talk_title_c {
 width100%;
 height:30px;
 line-height:30px;
}
.talk_record{
 width100%;
 height:398px;
 overflow: hidden;
 border-bottom-width1px;
 border-bottom-style: solid;
 border-bottom-color#434343;
 margin0px
}
.talk_word {
 line-height40px;
 height40px;
 width100%;
 background:#212121;
}
.messages {
 height24px;
 width240px;
 text-indent:5px;
 overflow: hidden;
 font-size12px;
 line-height24px;
 color#666
 background-color#ccc;
 border-radius3px;
 -moz-border-radius3px;
 -webkit-border-radius3px;
}
.messages:hover{background-color#fff;}
.talk_send{
 width:50px;
 height:24px;
 line-height24px;
 font-size:12px;
 border:0px;
 margin-left2px;
 color#fff;
 background-repeat: no-repeat;
 background-position0px 0px;
 background-color: transparent;
 font-family"微软雅黑";
}
.talk_send:hover {
 background-position0px -24px;
}
.talk_record ulpadding-left:5px;}
.talk_record li {
 line-height25px;
}
.talk_word .controlbtn a{
 margin12px;
}
.talk .talk_word .order {
 float:left;
 display: block;
 height14px;
 width16px;   
 background-repeat: no-repeat;
 background-position0px 0px;
}

.talk .talk_word .loop {
 float:left;
 display: block;
 height14px;
 width16px;
 background-repeat: no-repeat;
 background-position: -30px 0px;
}
.talk .talk_word .single {
 float:left;
 display: block;
 height14px;
 width16px;
 background-repeat: no-repeat;
 background-position: -60px 0px;
}
.talk .talk_word .order:hover,.talk .talk_word .active{
 background-position0px -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-top5px;
 padding-right10px;
 padding-left10px;
 padding-bottom0px;
}

.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: left top;}
.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;
 padding5px;
 float:left;
 color:#333333;
}
.jp-container .talk_recordbox h3{
 font-size:14px;
 padding:2px 0 5px 0;
 text-transform:uppercase;
 font-weight100;
 
}
.jp-container .talk_recordbox .user {
 float:left;
 display:inline;
 height45px;
 width45px;
 margin-top0px;
 margin-right5px;
 margin-bottom0px;
 margin-left0px;
 font-size12px;
 line-height20px;
 text-align: center;
}
/*自己发言样式*/
.jp-container .talk_recordboxme{
 display:block;
 min-height:80px;
 color#afafaf
 padding-top5px;
 padding-right10px;
 padding-left10px;
 padding-bottom0px;
}
.jp-container .talk_recordboxme .talk_recordtextbg{
 float:right;
 width:10px;
 height:30px;
 display:block;
 background-repeat: no-repeat;
 background-position: left top;}

.jp-container .talk_recordboxme .talk_recordtext{
 -moz-border-radius:5px;
 -webkit-border-radius:5px;
 border-radius:5px;
 background-color:#fcfcfc;
 width:240px;
 height:auto;
 padding5px;
 color:#666;
 font-size:12px;
 float:right;
 
}
.jp-container .talk_recordboxme h3{
 font-size:14px;
 padding:2px 0 5px 0;
 text-transform:uppercase;
 font-weight100;
 color:#333333;
 
}
.jp-container .talk_recordboxme .user{
 float:right;
 height45px;
 width45px;
 margin-top0px;
 margin-right10px;
 margin-bottom0px;
 margin-left5px;
 font-size12px;
 line-height20px;
 text-align: center;
 display:inline;
}
.talk_time{
 color#666;
 text-align: right;
 width240px;
 display: block;
}

测试

首先,启动三个窗口

群聊

单聊

总结

本文,基于Netty实战了一个协议实现的网页版聊天室服务器,从代码上可以看出,基于Netty的的实现还是非常简单、容易实现的。

但是协议使用上还是存在局限的,比如需要浏览器的支持。但是毕竟代表了Web技术的一种重要进展,可以扩宽我们的视野,在一些特定的工作场景中,可以帮助我们解决一些问题

来源:////

推荐:

最全的java面试题库

PS:因为公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。“在看”支持我们吧!

评论(0)

二维码