java netty实现websocket记录

netty实现socket服务器 处理websocket请求

最近有两个都用到netty做服务端的项目,第一个是c直接发起socket建立连接的请求,第二个是react框架的app,用websocket协议发起连接请求,netty处理稍有不同,记录一下。

netty高性能:https://www.infoq.cn/article/netty-high-performance

netty调优:https://blog.csdn.net/C_J33/article/details/80737053

#### 先来看第一个项目:

Springboot版本是1.5.10,点进去发现默认依赖没有netty,加入netty依赖。

maven依赖:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.10.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
    
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.10.Final</version>
</dependency>


spring启动完成后,新建线程指定端口启动socket

public class SocketService {
    private int port;

    public SocketService(int port) {
        this.port = port;
    }

    public SocketService() {
    }

    public int getPort() {
        return port;
    }

    public void setPort(int port) {
        this.port = port;
    }

    public void startSocket() throws Exception{
        // 接受socket链接循环器
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        // 处理业务逻辑循环器
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try{
            ServerBootstrap bs = new ServerBootstrap();
            bs.group(bossGroup,workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel socketChannel) throws Exception {
                    socketChannel.pipeline()
                            // 回车换行作为消息分隔符,消息最大长度设置1024
                            .addLast(new LineBasedFrameDecoder(1024))
                            .addLast(new StringDecoder())
                            .addLast(new StringEncoder(CharsetUtil.UTF_8))
                            .addLast(new MyServerHandler());
                }
            })
                    // 请求处理线程满时,临时存放完成握手队列的大小,默认50
                    .option(ChannelOption.SO_BACKLOG, 1024);
                    // 是否启用心跳保活机制,若链接建立并2小时左右无数据传输,此机制才会被激活(tcp机制)。
                    //.childOption(ChannelOption.SO_KEEPALIVE, true);
            // 同步等待socket链接结果,用户线程waite,直到连接完成被notify,继续执行用户代码
            ChannelFuture future = bs.bind(port).sync();
            future.channel().closeFuture().sync();
        }finally {
            // 优雅的释放资源
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}

 

> netty使用很简单,只要加入相应处理handle即可。
>
> 传输层为了效率,tcp协议发送数据包时有可能合并发送,对接收方来说会产生粘包问题,需要在应用层解决拆包,收发数据时协商设计分割点,一般而言有四种分割收到包的方法:
>
> 1. 发送方在发送每段数据后拼接回车换行符,接收方读到“\r\n”则认为是一个独立的数据包。netty默认解析实现是LineBasedFrameDecoder,加入解码handle即可。
> 2. 其他自定义分割符号,如“#”。netty实现handle是DelimiterBasedFrameDecoder.
> 3. 无论数据大小,每次发送固定长度,如1024字节,不够的0补位,超出的截断。缺点是比较生硬,数据小的时候浪费带宽资源。netty实现的handle是FixedLengthFrameHandle.
> 4. 数据分为消息头,消息体,消息头定义消息体长度,接收端解析出长度后只读取指定的长度。需要自己实现decoder。

_上述DecoderHandle全部继承ByteToMessageDecoder,是netty封装的解析二进制数据的处理类,只要将相应handle添加到pipeline中即可,解析完成后传输给自定义的逻辑处理类MyServerHandler。此项目中与c端约定传输json字符串格式数据,每段数据手动增加换行分割符。_ 

 

#### 第二个项目(netty与websocket)

springboot版本2.0.6.RELEASE,点进去发现默认依赖<netty.version>4.1.29.Final</netty.version>。

maven依赖:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.6.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>



SocketService与上一个项目相同,只是把匿名内部类单独创建为ChildChannelInit类,具体实现为:

public class ChildChannelInit extends ChannelInitializer<SocketChannel> {
    private Logger logger = LoggerFactory.getLogger(ChildChannelInit.class);
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
//        logger.debug("有客戶端鏈接新建一條chennel ......");
        SSLEngine sslEngine = SslUtil.generateSSLContext().createSSLEngine();
        sslEngine.setUseClientMode(false); //服务器端模式
        sslEngine.setNeedClientAuth(false); //不需要验证客户端
        ch.pipeline().addLast("ssl", new SslHandler(sslEngine));

        ch.pipeline().addLast("http-codec", new HttpServerCodec());
//        ch.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
        // 把多个httpmessagge组装成一个的默认实现
        ch.pipeline().addLast("aggregator", new HttpObjectAggregator(65536));
        ch.pipeline().addLast("ping", new IdleStateHandler(20,0,0, TimeUnit.SECONDS));
        ch.pipeline().addLast("handler", new MyNettyHandler());
    }
}


上面是几条是为了给socket加入ssl功能,SslUtil类的主要方法:

private static volatile SSLContext ssl_Context = null;
    public static SSLContext generateSSLContext() {
        if (null == ssl_Context){
            synchronized (SslUtil.class){
                if (null == ssl_Context){
                    try {
                        KeyStore ks = KeyStore.getInstance("JKS");
                        InputStream ksInputStream = new FileInputStream(APP_CONFIG.getKeyStorePath());
                        ks.load(ksInputStream, APP_CONFIG.getKeyStorePass().toCharArray());
                        KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
                        kmf.init(ks, APP_CONFIG.getKeyStoreKeyPass().toCharArray());

                        SSLContext sslContext = SSLContext.getInstance("TLS");
                        sslContext.init(kmf.getKeyManagers(), null, null);
                        ssl_Context = sslContext;
                    }catch (Exception e){
                        logger.info("load ssl context failed, error:{}",e.getLocalizedMessage());
                    }
                }
            }
        }
        return ssl_Context;
    }



下面的Handle方法则是因为websocket协议是通过http协议握手,然后切换(升级)到socket协议,主要是用来处理http协议的编解码添加的netty自定义实现的handle。

这里有个问题,也是本篇要记录的初衷,在接收消息的handle中,后期测试发现,客户端发来10000条数据,内容是json,每次解析出json中的cmd指令回复相应数据,总会少回6-7条,想到了是粘包导致的问题,但无论是加分割符的编解码还是自定义二进制decoder,pipeline中都不会加载,也就没有任何作用。

__后来查看netty源码,发现在http发送握手后,netty会自动添加及调整websocket的编解码。__

// handshake方法内的部分原码,
p.addBefore(ctx.name(), "wsdecoder", newWebsocketDecoder());
p.addBefore(ctx.name(), "wsencoder", newWebSocketEncoder());


> websocketDecoder继承WebSocketFrameDecoder,会处理编解码,并把二进制数据转换成binaryFrame或Textframe,其中frame有个isFinalFragment方法可以判断是否是一条数据的最后一段,如果不是,会通过ContinuationWebSocketFrame消息类型发送剩下的数据,自己在代码逻辑中可以拼接出完整的数据,避免了拆包不清的问题。
>
> 这里处理的是text消息类型,binary同理,用byte数组存就可以了。


自定义处理类:

@ChannelHandler.Sharable
public class MyNettyHandler extends ChannelInboundHandlerAdapter {
    private Logger logger = LoggerFactory.getLogger(MyNettyHandler.class);
    private WebSocketServerHandshaker handshaker;
    private String appendStr = "";
    private String currentUserId = "";
    String wsFactroyUri = "";
    
    /**客户端链接建立,即为活跃*/
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        logger.info("new connect active !! channelId:{}",ctx.channel().id().asShortText());
    }
    /**客户端断开链接,通道不活跃*/
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        if (currentUserId != null && NettyManager.channelGroupMap.containsKey(currentUserId)){
            NettyManager.channelGroupMap.get(currentUserId).remove(ctx.channel());
            logger.debug("client disconnect!! channelId:{}  map user size:{}  current user connCount:{}",ctx.channel().id().asShortText(),NettyManager.channelGroupMap.size(), NettyManager.channelGroupMap.get(currentUserId).size());
        }
    }
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        logger.error("!!!!EXCEPTION:{}",cause.toString());
        ctx.close();
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //1.第一次握手请求消息由HTTP协议承载,所以它是一个HTTP消息,执行handleHttpRequest方法来处理WebSocket握手请求。
        //2.客户端通过socket提交请求消息给服务端,WebSocketServerHandler接收到的是已经解码后的WebSocketFrame消息。
        if (msg instanceof FullHttpRequest){
            handleHttpRequest(ctx,(FullHttpRequest) msg);
        }else if (msg instanceof WebSocketFrame){
            handleSocketFrame(ctx, (WebSocketFrame) msg);
        }
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent){
            IdleStateEvent event = (IdleStateEvent) evt;
            if (event.state().equals(IdleState.READER_IDLE)){
                logger.info("Can't get client msg or ping in idle time,channel will be closed, channelId:{}  ", ctx.channel().id().asLongText());
                ctx.channel().close();
            }else {
                super.userEventTriggered(ctx, evt);
            }
        }
    }

    private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) throws UnsupportedEncodingException{
        // 利用http协议完成握手后升级到webSocket
        if (!req.decoderResult().isSuccess() || (!"websocket".equals(req.headers().get("Upgrade")))) {
            sendHttpResponse(ctx, req, new DefaultFullHttpResponse( HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
            return;
        }

        WebSocketServerHandshakerFactory handShakerFac = new WebSocketServerHandshakerFactory( wsFactroyUri, null, false);
        handshaker = handShakerFac.newHandshaker(req);
        if (handshaker == null) {
            WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
        } else {
            // 通过它构造握手响应消息返回给客户端
            // 同时将WebSocket相关的编码和解码类动态添加到ChannelPipeline中,用于WebSocket消息的编解码,
            // 添加WebSocketEncoder和WebSocketDecoder之后,服务端就可以自动对WebSocket消息进行编解码了
            handshaker.handshake(ctx.channel(), req);
        }
    }
    private void handleSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame){
        // 判断是否为关闭链接
        if (frame instanceof CloseWebSocketFrame){
            logger.info("get close socket command");
            handshaker.close(ctx.channel(),(CloseWebSocketFrame)frame.retain());
            return;
        }

        // 判断是否ping消息
        if (frame instanceof PingWebSocketFrame) {
            logger.info("get ping socket command");
            ctx.channel().write( new PongWebSocketFrame(frame.content().retain()));
            return;
        }

        // 文本内容
        if (frame instanceof TextWebSocketFrame){
            String body = ((TextWebSocketFrame) frame).text();
            if (!frame.isFinalFragment()){
                appendStr += body;
            }else {
                handleMsg(ctx, body);
            }
        }else if (frame instanceof ContinuationWebSocketFrame){
            String halfBody = ((ContinuationWebSocketFrame) frame).text();
            appendStr += halfBody;
            if (frame.isFinalFragment()){
                handleMsg(ctx, appendStr);
                appendStr = "";
            }
        }
    }

    private void handleMsg(ChannelHandlerContext ctx, String body){
        JSONObject jsonObject ;
        try {
            jsonObject = new JSONObject(body);
        }catch (Exception e){
            logger.error("get json error :{}",body);
            return;
        }
        String cmd = (String) jsonObject.get("command");
        if (cmd.equals("auth")){
            handleAuthLogic(ctx, jsonObject);
        }else if (cmd.equals("client_ping")){
            handleClientPingLogic(ctx, jsonObject);
        }
    }

    private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, DefaultFullHttpResponse res) {
        // 返回应答给客户端
        if (res.status().code() != 200) {
            ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(), CharsetUtil.UTF_8);
            res.content().writeBytes(buf);
            buf.release();
        }
        // 如果是非Keep-Alive,关闭连接
        ChannelFuture f = ctx.channel().writeAndFlush(res);
        if (!isKeepAlive(req) || res.status().code() != 200) {
            f.addListener(ChannelFutureListener.CLOSE);
        }
    }

    private static boolean isKeepAlive(FullHttpRequest req) {
        return false;
    }

    /** 客戶端登錄 */
    private void handleAuthLogic(ChannelHandlerContext ctx, JSONObject jsonObject){
        logger.debug("json:{}",jsonObject.toString());
        String userId = (String)jsonObject.get("from");
        Long clientTime = (Long)jsonObject.get("timestamp");
        String uniqueId = (String) jsonObject.get("uniqueId");

        Long currentTime = System.currentTimeMillis();
        Long diff = currentTime - clientTime;

        AuthRes authRes = new AuthRes();
        authRes.setCommand("auth");
        authRes.setFrom("sys");
        authRes.setTo(userId);
        authRes.setDiff_time(diff);

        Service2Controller<UserProfile> s2c = NettyManager.USER_SERVICE.getUserById(userId);
        UserProfile userProfile = s2c.getData();
//        UserProfile userProfile = new UserProfile();

        boolean shouldClose = false;
        if (userProfile == null){
            authRes.setResult("failed");
            authRes.setResson("user_not_exist");
            shouldClose = true;
        }else {
            authRes.setResult("ok");
            authRes.setResson("success");
            currentUserId = userId;
            // 保存当前user的所有链接chennel
//            NettyManager.addConnectMap(userId, ctx.channel());
            if (!NettyManager.channelGroupMap.containsKey(userId)){
                NettyManager.channelGroupMap.put(userId, new DefaultChannelGroup(GlobalEventExecutor.INSTANCE));
            }
            NettyManager.channelGroupMap.get(userId).add(ctx.channel());
            logger.info("connect map user size:{}  connect count:{}",NettyManager.channelGroupMap.size(),NettyManager.channelGroupMap.get(userId).size());

        }
        authRes.setTimestamp(currentTime);
        String resString = authRes.toString() + NettyManager.SEP;
        ctx.channel().writeAndFlush(new TextWebSocketFrame(resString));
        if (shouldClose){
            ctx.close();
        }
    }

    /** 客戶端ping */
    private void handleClientPingLogic(ChannelHandlerContext ctx, JSONObject jsonObject){
        Long clientTime = (Long)jsonObject.get("timestamp");
        Long currentTime = System.currentTimeMillis();
        long diffTime = Math.abs(currentTime - clientTime);

        if (diffTime < 30 * 1000){
            JsonObject object = new JsonObject();
            object.addProperty("command","client_ping_receive");
            String resString = object.toString() + NettyManager.SEP;
            ctx.channel().writeAndFlush(new TextWebSocketFrame(resString));
            logger.info("receive client ping command ,res:{}", resString);
        }
    }
}


代码中还有维持存储客户端连接的逻辑,一并记录,保存连接的容器结构是:
Map<String, ChannelGroup> channelGroupMap = new ConcurrentHashMap<>;
键为用户ID,值为当前用户的连接集合。在给某个用户发送数据,在相应地方调用channelGroupMap.get("userId").writeAndFlush()方法即可。

 

 

 

 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

使用netty时,如果链接异常关闭会打印对应的log信息,下面是别人的博客地址,记录一下。

tcp(socket)各类日志的打印场景

终止一个连接的正常方式是发送FIN。 在发送缓冲区中 所有排队数据都已发送之后才发送FIN,正常情况下没有任何数据丢失。

但我们有时也有可能发送一个RST报文段而不是F IN来中途关闭一个连接。这称为异常关闭 。 

进程关闭socket的默认方式是正常关闭,如果需要异常关闭,利用 SO_LINGER选项来控制。

异常关闭一个连接对应用程序来说有两个优点:

(1)丢弃任何待发的已经无意义的 数据,并立即发送RST报文段;

(2)RST的接收方利用关闭方式来 区分另一端执行的是异常关闭还是正常关闭。

值得注意的是RST报文段不会导致另一端产生任何响应,另一端根本不进行确认。收到RST的一方将终止该连接。程序行为如下:

阻塞模型下,内核无法主动通知应用层出错,只有应用层主动调用read()或者write()这样的IO系统调用时,内核才会利用出错来通知应用层对端RST。

非阻塞模型下,select或者epoll会返回sockfd可读,应用层对其进行读取时,read()会报错RST。 

游戏测试过程中发现某些socket错误经常出现,以下是测试游戏服务器时通常考虑的case. 
服务器端: 
1. 
Case:客户端程序正常运行的情况下,拔掉网线,杀掉客户端程序 
目的:模拟客户端死机、系统突然重启、网线松动或网络不通等情况 
结论:这种情况下服务器程序没有检测到任何异常,并最后等待“超时”才断开TCP连接

2. 
Case:客户端程序发送很多数据包后正常关闭Socket并exit进程(或不退出进程) 
目的:模拟客户端发送完消息后正常退出的情况 
结论:这种情况下服务器程序能够成功接收完所有消息,并最后收到“对端关闭”(Recv返回零)消息

3. 
Case:客户端程序发送很多数据包后不关闭Socket直接exit进程 
目的:模拟客户端程序退出而忘记关闭Socket的情况(比如通过Windows窗口的关闭图标退出进程,而没有捕获相应关闭事件做正常退出处理等) 
结论:这种情况下服务器程序能够收到部分TCP消息,然后收到“104: Connection reset by peer”(Linux下)或“10054: An existing connection was forcibly closed by the remote host”(Windows下)错误

4. 
Case:客户端程序发送很多数据包的过程中直接Kill进程 
目的:模拟客户端程序崩溃或非正常方式结束进程(比如Linux下”kill -9″或Windows的任务管理器杀死进程)的情况 
结论:这种情况下服务器程序很快收到“104: Connection reset by peer”(Linux下)或“10054: An existing connection was forcibly closed by the remote host”(Windows下)错误

5. 
Case:客户端程序发送很多数据包后正常关闭Socket并exit进程(或不退出进程) 
目的:模拟客户端正常关闭Socket后,服务器端在检查到TCP对端关闭前向客户端发送消息的情况 
结论:这种情况下服务器程序接收和发送部分TCP消息后,在Send消息时产生“32: Broken pipe”(Linux下)或“10053: An established connection was aborted by the software in your host machine”(Windows下)错误

总结: 
当TCP连接的进程在忘记关闭Socket而退出、程序崩溃、或非正常方式结束进程的情况下(Windows客户端),会导致TCP连接的对端进程产生“104: Connection reset by peer”(Linux下)或“10054: An existing connection was forcibly closed by the remote host”(Windows下)错误

当TCP连接的进程机器发生死机、系统突然重启、网线松动或网络不通等情况下,连接的对端进程可能检测不到任何异常,并最后等待“超时”才断开TCP连接

当TCP连接的进程正常关闭Socket时,对端进程在检查到TCP关闭事件之前仍然向TCP发送消息,则在Send消息时会产生“32: Broken pipe”(Linux下)或“10053: An established connection was aborted by the software in your host machine”(Windows下)错误

客户端 
1. 
服务器端已经close了Socket,客户端再发送数据 
目的:测试在TCP对端进程已经关闭Socket时,本端进程还未检测到连接关闭的情况下继续向对端发送消息 
结论:第一包可以发送成功,但第二包发送失败,错误码为“10053: An established connection was aborted by the software in your host machine”(Windows下)或“32: Broken pipe,同时收到SIGPIPE信号”(Linux下)错误

2. 
服务器端发送数据到TCP后close了Socket,客户端再发送一包数据,然后接收消息 
目的:测试在TCP对端进程发送数据后关闭Socket,本端进程还未检测到连接关闭的情况下发送一包消息,并接着接收消息 
结论:客户端能够成功发送第一包数据(这会导致服务器端发送一个RST包 <已抓包验证>),客户端再去Recv时,对于Windows和Linux程序有如下不同的表现: 
Windows客户端程序:Recv失败,错误码为“10053: An established connection was aborted by the software in your host machine” 
Linux客户端程序:能正常接收完所有消息包,最后收到正常的对端关闭消息(这一点与Window下不一样)

3. 
服务器端在TCP的接收缓冲区中还有未接收数据的情况下close了Socket,客户端再收包 
目的:测试在TCP的接收缓冲区中还有未接收数据的情况下关闭Socket时,对端进程是否正常 
结论:这种情况服务器端就会向对端发送RST包,而不是正常的FIN包(已经抓包证明),这就会导致客户端提前(RST包比正常数据包先被收到)收到“10054: An existing connection was forcibly closed by the remote host”(Windows下)或“104: Connection reset by peer”(Linux下)错误

总结: 
当TCP连接的对端进程已经关闭了Socket的情况下,本端进程再发送数据时,第一包可以发送成功(但会导致对端发送一个RST包过来): 
之后如果再继续发送数据会失败,错误码为“10053: An established connection was aborted by the software in your host machine”(Windows下)或“32: Broken pipe,同时收到SIGPIPE信号”(Linux下)错误; 
之后如果接收数据,则Windows下会报10053的错误,而Linux下则收到正常关闭消息

TCP连接的本端接收缓冲区中还有未接收数据的情况下close了Socket,则本端TCP会向对端发送RST包,而不是正常的FIN包,这就会导致对端进程提前(RST包比正常数据包先被收到)收到“10054: An existing connection was forcibly closed by the remote host”(Windows下)或“104: Connection reset by peer”(Linux下)错误
--------------------- 
作者:九嶷山 
来源:CSDN 
原文:https://blog.csdn.net/larry_zeng1/article/details/78982370 
版权声明:本文为博主原创文章,转载请附上博文链接!

转载于:https://www.cnblogs.com/sunkaikees/p/10238652.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值