Http是无状态的基于请求和响应的协议。客户端向服务端发送的所有请求之间都是没有半毛钱关系的(而且只能客户端向服务端发起请求),服务端不能判断请求是否都属于同一个客户端,由此诞生了cookie、session等技术用于保存用户状态。

http1.0客户端每次请求都会建立新的连接,服务端返回消息给客户端后,连接即刻断开,浪费资源。

http1.1增加了一个新的特性keep alive -- 客户端可以与服务端短时间内保持一个连接,在设置的时间内,客户端可以多次发起请求而服务端不会建立新连接,继续使用已有的连接进行处理。

如果使用http协议进行聊天程序开发,服务端无法向客户端主动推送消息,客户端就需要采用轮询等方法不断请求服务端来获取消息,此等操作显然会造成消息的延迟(要在下一次轮询才能获取到服务端的消息)和资源浪费(轮询查询后不一定会返回有效消息,每次请求都会携带大量头信息 -- 流量浪费)。

WebSocket真正实现了长连接的技术 -- 客户端与服务端建立连接后如果没有其他因素干预(服务端关闭、客户端退出等),此连接就会一直有效。websocket长连接一旦建立成功,客户端与服务端就成为了两个对等的存在,就可以随意进行发收消息(去除头信息的有效消息)。

那么http和websocket有什么关系呢,websocket是html5规范的一部分,基于http的升级版协议 -- 客户端与服务端的请求是标准的http请求,只是在请求头中携带了websocket相关的参数信息,服务端根据websocket参数进行长连接等处理后,客户端与服务端就可以进行双向数据传递 liao。


服务端

MyServer

之前服务端代码差不多

public class MyServer {
    public static void main(String[] args) throws Exception {
        EventLoopGroup boosGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try{
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(boosGroup,workerGroup).channel(NioServerSocketChannel.class).
                    handler(new LoggingHandler(LogLevel.INFO)).//添加日志处理器
                    childHandler(new WebSocketInit());

            //使用java.net包下的socket端口,用不用都可以 .bind(8899).sync();
            ChannelFuture channelFuture = serverBootstrap.bind(new InetSocketAddress(8899)).sync();
            channelFuture.channel().closeFuture().sync();
        } finally {
            boosGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

WebSocketInit

public class WebSocketInit extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline channelPipeline = ch.pipeline();

        //服务端简单的编解码器
        //最开始三个handler都是处理http滴
        channelPipeline.addLast(new HttpServerCodec());

        //adds support for writing a large data stream,消息一次性发送
        channelPipeline.addLast(new ChunkedWriteHandler());

        //A {@link ChannelHandler} that aggregates an {@link HttpMessage}
        // * and its following {@link HttpContent}s into a single {@link FullHttpRequest}
        // * or {@link FullHttpResponse}  
        //将内容聚合成一个完整的http请求/响应。客户端分段发送消息,不完整需聚合
        //参数:最大字节数长度,以此长度聚合成一个完整对象,内容超过则调用handleOversizedMessage方法处理(想知道怎么处理 -- 读源码解决)
        channelPipeline.addLast(new HttpObjectAggregator(8192));

        //does all the heavy lifting for you to run a websocket server 
        //websocket服务协议处理器。简化运行netty的websocket服务端一些繁重的工作,websocket必须加的处理器
        channelPipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
        channelPipeline.addLast(new MyServerHandler());
    }
}

MyServerHandler

相比之前的String类型数据,Netty使用TextWebSocketFrame类型定义Websocket的几个规范操作,比如心跳机制等(了解了解,读源码解决),只需要知道netty中websocket的数据是以frames进行传输

public class MyServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        System.out.println("收到的消息为:"+msg.text());

        ctx.channel().writeAndFlush(new TextWebSocketFrame("服务器时间:"+ LocalDateTime.now()));
    }

    //每个channel都独有一个id
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        System.out.println("handlerAdd "+ ctx.channel().id().asLongText());
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        System.out.println("handlerRemoved "+ctx.channel().id().asLongText());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("异常发送");
        cause.printStackTrace();
        ctx.channel();
    }
}

客户端

src下新建html文件 webapp/test.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>WebSocket客户端</title>
</head>
<body>

<script type="text/javascript">

    var socket;

    //判断浏览器是否支持websocket
    if(window.WebSocket) {
        socket = new WebSocket("ws://localhost:8899/ws");

        //对应服务端channelRead0方法
        socket.onmessage = function (event) {
            var tar = document.getElementById("responseText");
            tar.value = tar.value + "\n" + event.data;
        }

        //handlerAdded(ChannelHandlerContext ctx)
        socket.onopen = function (event) {
            var tar = document.getElementById("responseText");
            tar.value = "连接开启!";
        }

        //handlerRemoved(ChannelHandlerContext ctx)
        socket.onclose = function (event) {
            var tar = document.getElementById("responseText");
            tar.value = tar.value + "\n" + "连接关闭!";
        }
    } else {
        alert("浏览器不支持WebSocket")
    }

    function sendMessage(message) {
        if (!window.WebSocket) {
            return;
        }

        if(socket.readyState == WebSocket.OPEN) {
            socket.send(message);
        }else{
            alert("连接尚未开启!");
        }
    }

</script>

<form onsubmit="return false;">
    <textarea name="message" style="width: 400px;height: 200px"></textarea>
    <input type="button" value="发送数据" onclick="sendMessage(this.form.message.value)"><br>
    <b3>服务端输出:</b3><br>
    <textarea id="responseText" style="width: 400px;height: 200px"></textarea>
    <input type="button" onclick="javascript:document.getElementById('responseText').value = ''" value="清空内容">
</form>

</body>
</html>

program_result

启动服务端后打开客户端会调用服务端的channelRead0方法,判断返回连接开启与服务器时间

working_process_tips

IDEA浏览器浏览html文件

html文件中右键 Run 'xxx.html' 会自动跳转浏览器