Netty之Http与Websocket

分类专栏:
Netty相关

文章标签:
Netty
原创

HTTP(应用层协议)

默认是80端口,最早推出于1991年

请求/响应

客户端:

HttpResponseDecoder 解码器,处理服务端的响应
HttpRequestEncoder编码器,处理服务端请求

服务端:

HttpRequestDecoder 解码器,处理客户端的请求
HttpResponseEncoder编码器,处理客户端的响应

编解码器

客户端编码解码器HttpClientCodeC:HttpRequestEncoder+HttpResponseDecoder
服务端编码解码器HttpServerCodeC:HttpRequestDecoder+HttpResponseEncoder

压缩

HttpContentCompressor压缩,用于服务端
HttpContentDecompressor解压缩,用于客户端

聚合

由于http的请求和响应,可能由很多部分组成,需要聚合成一个完整的消息
HttpObjectAggregator ->
FullHttpRequest /
FullHttpResponse

发展历程

1) 0.9版本

GET /index.html
服务端只能返回html格式,传输过程只能处理文字

2) 1.0版本

支持任何格式的内容,包括图像、视频、二进制等等
引入了POST命令、HEAD命令
增加了请求头、状态码,以及权限、缓存等
GET / HTTP/1.0
User-Agent:Mozilla/1.0
Accept: */*
HTTP/1.0 200 OK
Content-Type: text/plain
Content-Encoding: gzip
<html>
  <body> hello world </body>
</html>

a、 Content-Type

服务端通知客户端,当前数据的格式
示例: text/html 、 image/png 、 application/pdf 、 video/mp4
前面是一级类型,后面是二级类型,用斜杠分隔; 还可以增加其他参数,如编码格式
Content-Type: text/plain; charset=utf-8

b、Content-Encoding

表示数据压缩的方式,gzip、compress、deflate
对应客户端的字段为 Accept-Encoding,代表接收哪些压缩方式

c、缺点和问题

每个TCP连接只能发送一个请求,发送完毕连接关闭,使用成本很高,性能较差
Connection: keep-alive   - 非标准字段

3) 1.1版本

GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.8
Accept: */*
Postman-Token: be754386-04ec-4a76-9817-bdd27ddd4b93
Host: cn.bing.com
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
HTTP/1.1 200 OK
Cache-Control: private, max-age=0
Content-Length: 45542
Content-Type: text/html; charset=utf-8
Content-Encoding: br
Vary: Accept-Encoding
P3P: CP="NON UNI COM NAV STA LOC CURa DEVa PSAa PSDa OUR IND"
Set-Cookie: SRCHD=AF=NOFORM; domain=.bing.com; expires=Thu, 15-Dec-2022 08:16:19 GMT; path=/
Set-Cookie: SRCHUID=V=2&GUID=55418C8D9DA04D47B721FCE030EB283B&dmnchg=1; domain=.bing.com; expires=Thu, 15-Dec-2022 08:16:19 GMT; path=/
Set-Cookie: SRCHUSR=DOB=20201215; domain=.bing.com; expires=Thu, 15-Dec-2022 08:16:19 GMT; path=/
Set-Cookie: _SS=SID=1DEB2B301FB66F20330024911EF56EF5; domain=.bing.com; path=/
Set-Cookie: _EDGE_S=F=1&SID=1DEB2B301FB66F20330024911EF56EF5; path=/; httponly; domain=bing.com
Set-Cookie: _EDGE_V=1; path=/; httponly; expires=Sun, 09-Jan-2022 08:16:19 GMT; domain=bing.com
Set-Cookie: MUID=03DC0275D7906F4E3BE10DD4D6D36EAA; samesite=none; path=/; secure; expires=Sun, 09-Jan-2022 08:16:19 GMT; domain=bing.com
Set-Cookie: MUIDB=03DC0275D7906F4E3BE10DD4D6D36EAA; path=/; httponly; expires=Sun, 09-Jan-2022 08:16:19 GMT
X-MSEdge-Ref: Ref A: 835BA58441E44FAAA96A2E12620C9519 Ref B: BJ1EDGE0608 Ref C: 2020-12-15T08:16:19Z
Date: Tue, 15 Dec 2020 08:16:18 GMT
a、持久连接,含义为默认不关闭tcp连接,可以被多个请求复用。大多时候,浏览器对同一个域名,允许同时建立6个连接
b、管道机制,支持客户端发送多个请求,管理请求的顺序的。服务器还是按照接受请求的顺序,返回对应的响应结果
c、Content-Length, 用来区分数据包的重要字段
d、支持PUT、DELETE、PATCH等命令
缺点和问题
当部分请求耗时较长时,仍会阻塞后续请求的处理速度,这种现象叫做“队头阻塞”/"线头阻塞"

4) 2.0版本

解决队头阻塞的问题,使用的是多路复用的方式

代码Demo

public class HttpServer {

    public static void main(String[] args) {

        // 可以自定义线程的数量
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        // 默认创建的线程数量=CPU处理器数量 * 2
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .handler(new LoggingHandler())
                // 当连接被阻塞时  BACKLOG代表的是 阻塞队列的长度
                .option(ChannelOption.SO_BACKLOG, 128)
                // 设置连接为保持活动的状态
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                .childHandler(new MyHttpInitializer());
        try {
            ChannelFuture future = serverBootstrap.bind(9988).sync();
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}
public class MyHttpInitializer extends ChannelInitializer<Channel> {

    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        //先对请求解码,后对响应编码
        pipeline.addLast("codec", new HttpServerCodec());
        // 压缩数据
        pipeline.addLast("compressor", new HttpContentCompressor());
        // 聚合成完整的消息,参数代表可以处理的最大值(此时是512kb)
        pipeline.addLast("aggregator", new HttpObjectAggregator(512 * 1024));
        pipeline.addLast(new MyHttpHandler());
    }
}
/**
 * 泛型需要设置为 FullHttpRequest
 * 筛选msg为此类型的消息才处理
 */
public class MyHttpHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {

        /**
         * DefaultFullHttpResponse是一个默认的完整http响应
         * 设定版本号、响应码、响应数据
         * 设置响应头,HttpHeaders来接收
         * 设置请求或响应头字段时,可以使用HttpHeaderNames
         * 设置字段值时,可以使用HttpHeaderValues
         * 设置包的大小时,调用readableBytes方法
         */
        DefaultFullHttpResponse response = new DefaultFullHttpResponse(
                HttpVersion.HTTP_1_1,
                HttpResponseStatus.OK,
                Unpooled.wrappedBuffer("This is http netty demo".getBytes())
        );
        HttpHeaders headers = response.headers();
        headers.add(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN + ";charset=UTF-8");
        headers.add(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
        ctx.write(response);
    }

    /**
     * 刷新
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
//        super.channelReadComplete(ctx);
    }
}
运行服务端打开浏览器或者postman 输入ip+端口

liulanqi

postman


WebSocket

websocket

websocket是由浏览器发起的
协议标识符 http://127.0.0.1:8080 ws://127.0.0.1:7777
GET ws://127.0.0.1:7777 HTTP/1.1
Host: 127.0.0.1
Upgrade: websocket    # 升级为ws   
Connection: Upgrade   # 此链接需要升级
Sec-WebSocket-key: client-random-string ...  # 标识加密相关信息
HTTP/1.1 101
Upgrade: websocket
Connection: Upgrade
响应码 101 代表本次协议需要更改为websocket,连接建立后,支持文本信息及二进制信息

Websocket实现的原理:

http的缺陷:通信只能由客户端发起。因此需要一种服务端能够主动推送的能力---websocket
通过http协议进行连接的建立(握手和回答),建立连接后不再使用http,而tcp自身是支持双向通信的,所以能达到“全双工”的效果
通信使用的单位叫帧 frame

客户端:发送时将消息切割成多个帧

服务端:接收时,将关联的帧重新组装

WebSocket客户端

kehuduan

var ws = new WebSocket("ws://127.0.0.1:7777/hello");
ws.onopen = function(ev){
     ws.send("hello"); //建立连接后发送数据
}

代码Demo

设计一个样式,左右两个各有一个文本框,中间放一个发送按钮
左侧文本框用来发送数据,右侧文本框用来显示数据
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Hello WebSocket</title>
</head>
    <body>
    <script>
        var socket;
        // 判断当前浏览器是否支持websockt
        if (!window.WebSocket) {
            alert("不支持websocket")
        } else {
            socket = new WebSocket("ws://127.0.0.1:7777/hello");
            //设置开启连接的方法
            socket.onopen = function (ev) {
                var tmp = document.getElementById("respText");
                tmp.value = "连接已开启";
            }
            //设置关闭连接的方法
            socket.onclose = function (ev) {
                var tmp = document.getElementById("respText");
                tmp.value = tmp.value + "\n" + "连接已关闭";
            }
            //设置接收数据的方法
            socket.onmessage = function (ev) {
                var tmp = document.getElementById("respText");
                tmp.value = tmp.value + "\n" + ev.data;
            }
        }

        function send(message) {
            //先判断socket是否已经创建
            if (!window.socket) {
                return
            }
            //判断socket的状态
            //CONNECTING正在连接 CLOSING正在关闭
            //CLOSED已经关闭或打开连接失败
            //OPEN连接成功可以正常通信
            if (socket.readyState == WebSocket.OPEN) {
                socket.send(message);
            } else {
                alert("连接未开启");
            }
        }
    </script>

    <!--防止表单自动提交-->
    <form onsubmit="return false">
        <textarea name="message" style="height: 400px;width: 400px"></textarea>
        <input type="button" value="发送" onclick="send(this.form.message.value)">
        <textarea id="respText" style="height: 400px;width: 400px"></textarea>
    </form>

    </body>
</html>
public class WebSocketServer {

    public static void main(String[] args) {
        //可以自定义线程的数量
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        //默认创建的线程数量=CPU处理器数量 * 2
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .handler(new LoggingHandler())
                //当连接被阻塞时BACKLOG代表的是阻塞队列的长度
                .option(ChannelOption.SO_BACKLOG, 128)
                //置连接为保持活动的状态
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                .childHandler(new WebSocketInitializer());
        try {
            ChannelFuture future = serverBootstrap.bind(7777).sync();
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}
public class WebSocketInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        //增加编解码器的另一种方式
        pipeline.addLast(new HttpServerCodec());
        //块方式写的处理器  用于处理较大数据
        pipeline.addLast(new ChunkedWriteHandler());
        //聚合
        pipeline.addLast(new HttpObjectAggregator(512 * 1024));
        //声明websocket的请求路径
        //ws://127.0.0.1:7777/hello
        //是将http协议升级为websocket协议,并且使用101作为响应码
        pipeline.addLast(new WebSocketServerProtocolHandler("/hello"));
        pipeline.addLast(new WebSocketHandler());
    }
}
/**
 * 泛型
 * 代表的是处理数据的单位
 * TextWebSocketFrame是文本信息帧
 */
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        System.out.println("msg : " + msg.text());
        Channel channel = ctx.channel();
        TextWebSocketFrame resp = new TextWebSocketFrame("hello client from websocket server");
        channel.writeAndFlush(resp);
    }
}
run 上面那个HTML文件 在打开服务端 刷新

hello

fuwuduan

jieguo

图片来自本人CSDN

  • 作者:潘震
  • 评论

    留言