Netty笔记之六:Netty对websocket的支持

Netty笔记之六:Netty对websocket的支持,第1张

WebSocket是一种规范,是Html5规范的一部分,websocket解决什么问题呢?解决http协议的一些不足。我们知道,http协议是一种无状态的,基于请求响应模式的协议。

网页聊天的程序(基于http协议的),浏览器客户端发送一个数据,服务器接收到这个浏览器数据之后,如何将数据推送给其他的浏览器客户端呢?

这就涉及到服务器的推技术。早年为了实现这种服务器也可以像浏览器客户端推送消息的长连接需求,有很多方案,比如说最常用的采用一种轮询技术,就是客户端每隔一段时间,比如说2s或者3s向服务器发送请求,去请求服务器端是否还有信息没有响应给客户端,有就响应给客户端,当然没有响应就只是一种无用的请求。

这种长轮询技术的缺点有:

1)响应数据不是实时的,在下一次轮询请求的时候才会得到这个响应信息,只能说是准实时,而不是严格意义的实时。

2)大多数轮询请求的空轮询,造成大量的资源带宽的浪费,每次http请求携带了大量无用的头信息,而服务器端其实大多数都不关注这些头信息,而实际大多数情况下这些头信息都远远大于body信息,造成了资源的消耗。

拓展

比较新的技术去做轮询的效果是Comet。这种技术虽然可以双向通信,但依然需要反复发出请求。而且在Comet中,普遍采用的长链接,也会消耗服务器资源。

WebSocket一种在单个 TCP 连接上进行 全双工通讯 的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并被RFC7936所补充规范。WebSocket API也被W3C定为标准。

WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许 服务端主动向客户端推送数据 。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

websocket的出现就是解决了客户端与服务端的这种长连接问题,这种长连接是真正意义上的长连接。客户端与服务器一旦连接建立双方就是对等的实体,不再区分严格意义的客户端和服务端。长连接只有在初次建立的时候,客户端才会向服务端发送一些请求,这些请求包括请求头和请求体,一旦建立好连接之后,客户端和服务器只会发送数据本身而不需要再去发送请求头信息,这样大量减少了

网络带宽。websocket协议本身是构建在http协议之上的升级协议,客户端首先向服务器端去建立连接,这个连接本身就是http协议只是在头信息中包含了一些websocket协议的相关信息,一旦http连接建立之后,服务器端读到这些websocket协议的相关信息就将此协议升级成websocket协议。websocket协议也可以应用在非浏览器应用,只需要引入相关的websocket库就可以了。

HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。Websocket使用ws或wss的统一资源标志符,类似于HTTPS,其中wss表示在TLS之上的Websocket。如:

优点

浏览器页面向服务器发送消息,服务器将当前消息发送时间反馈给浏览器页面。

服务器端

服务器端初始化连接

WebSocketServerProtocolHandler :参数是访问路径,这边指定的是ws,服务客户端访问服务器的时候指定的url是: ws://localhost:8899/ws 。

它负责websocket握手以及处理控制框架(Close,Ping(心跳检检测request),Pong(心跳检测响应))。 文本和二进制数据帧被传递到管道中的下一个处理程序进行处理。

WebSocket规范中定义了6种类型的桢,netty为其提供了具体的对应的POJO实现。

WebSocketFrame:所有桢的父类,所谓桢就是WebSocket服务在建立的时候,在通道中处理的数据类型。本列子中客户端和服务器之间处理的是文本信息。所以范型参数是TextWebSocketFrame。

自定义Handler

页面

启动服务器,然后运行客户端页面,当客户端和服务器端连接建立的时候,服务器端执行 handlerAdded 回调方法,客户端执行 onopen 回调方法

服务器端控制台:

页面:

客户端发送消息,服务器端进行响应,

服务端控制台打印:

客户端也收到服务器端的响应:

打开开发者工具

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

参考技术

java web 服务器推送技术--comet4j

Comet:基于 HTTP 长连接的“服务器推”技术

Java最强书单推荐抓紧学习

01、入门

《Java 核心技术卷 1》

《Head First Java》

《鸟哥的 Linux 私房菜》

为什么要学 Linux 呢因为在实际的开发工作中项目基本上都要部署到 Llilux 环境下。Windows作为服务器的很少,除了慢没别的原因。

假如能够提前掌握一些 Linux 基本操作的话,不仅简历上是加分项,工作中更能快人一步。

《Maven 实战》

《Git 权威指南》

02、进阶

《Java 编程思想》

《Java编程思想》这本书确实没得说,质量很高,但需要放在 Java 入门后再去读,这样才能真正地去理解思想。

《Netty 实战》

无论是构建高性能的 Web、游戏服务器、推送系统、RPC 框架、消息中间件还是分布式大数据处理引擎,都离不开Netty,在整个行业中,Netty 广泛而成功的应用,使其成为了 Java 高性能网络编程的卓绝框架。

代码整洁之道》

软件的质量,不仅依赖于架构,更与代码质量息息相关。而代码的质量与其整洁度成正比关系,越整洁的代码,其质量毫无疑问的就会越高。

03、深入

《重构,改善既有代码的设计》

《重构,改善既有代码的设计》

《深入理解 Nginx》

《深入剖析 Tomcat》

《JDK 里的设计模式》

《深入浅出设计模式》

《设计模式之禅》

《Head First 设计模式》

《算法》

《大型网站系统与 Java 中间件实践》

《大型网站技术架构: 核心原理与案例分析》

《亿级流量网站架构核心技术》

04、学习方法

第一,善用搜索引擎。平常需要找资料,需要解决问题,如果自己一时半会没有方法的话,就去搜。

第二,学会提问。如果搜索引擎找不到答案的话,不要直接把问题抛到群里,抛给同事、领导,或者大牛,要先对问题梳理一下。

第三,善干总结和归纳。很多同学给我反馈,“二哥,怎么总是感觉记不住啊,学完就忘啊,有什么好的办法吗

C语言基础知识的方法:

了解数据结构和算法:C 语言是一种基础的编程语言,很多算法和数据结构都是通过 C 语言实现的。因此,学习数据结构和算法可以帮助加深对 C 语言的理解,并提高编程能力。

参加在线课程或培训班: 可以参加一些在线课程或培训班来系统地学习 C 语言的基础知识。例如在 Coursera、Udemy或者网易云课堂等平台上可以找到相关的课程。

学习示例代码:

阅读代码:首先需要仔细地阅读示例代码,了解代码的功能和实现方法。可以分析代码结构,查看变量和函数的命名规范、注释说明和代码格式等。

理解代码逻辑:在阅读代码的过程中,需要尝试理解代码的逻辑。可以通过画流程图或者思维导图来帮助理解代码的实现思路和算法。

实际运行代码:在阅读完示例代码之后,可以尝试将代码运行起来,并且对代码进行调试,了解代码的具体执行过程。可以通过调试器等工具来帮助理解代码的运行过程。

修改代码:尝试修改示例代码,添加新的功能或者改进原有的代码。通过修改代码来深入理解代码的实现思路和功能特性并且可以提高自己的编程能力。

参考其他资源:如果在阅读示例代码的过程中遇到了困难,可以通过查阅相关的资料来帮助理解。例如可以参考官方文档博客文章或者在线教程等。

1 应用在netty建连接的过程中做了耗时的事;

因此我先dump了应用的线程,看到一切正常,boss线程看起来非常空闲;

2 backlog太小;

首先问了下开发代码里有没有设置过backlog,开发告诉我没设置过,于是我翻了下netty的源码确认下默认值,看到backlog的默认值为读取自系统的/proc/sys/net/core/somaxconn,于是查了下系统的这个值,确认系统的这个值已经是调整过的,设置为了2048,然后确认了系统上的tcp_max_syn_backlog是4096,也就是最后work的会是2048(为什么是这样具体可见 这篇文章 ),也就是说应该是够的。

用ss -s观察连接的状况,看到的是synrecv是0,也印证了上面的不是backlog的问题。

到这一步就彻底傻眼了,不知道该用什么方法排查了,于是开始一堆的google,看到的各种说解决新建连接并发低的解决办法,除了调整backlog外,主要是以下两种:

1 关闭tcp_tw_recycle,尝试了(话说这里充分体现了”病急乱投医“的心态,其实我自己都不相信改这参数有用,但想着只是改下参数这种小代价的事,还是试试吧),没任何作用;

2 关闭window scaling ,也尝试了,一样没任何作用;

查到这个阶段觉得自己已经无法理解了,于是求助了厂内内核团队对网络这块比较精通的同学,然后又求助了“神”,“神“帮忙看了会后,说主要的问题是现在是每epollWait唤醒一次,只建了一个连接,这导致在大量新建连接请求并发的时候,效率不够高,因此我翻了下代码,发现我本机上看到的代码不是这样的,我本机上看到的netty代码在epollWait唤醒后,是会尝试一直去accept的,但这个应用使用的netty确实不是这样,于是查了下应用的jar包库,发现里面有两个版本的netty(一个是321Final,一个是363Final),321确实是每次epollWait后就处理一个,于是通知开发同学把321去掉,满心期待的认为应该是好了,等开发更新好了后,自己也确认了一次epollWait唤醒后会连续处理很多个建立连接的请求,但悲催的还是没解决问题,具体的netty在这块的改造感兴趣的同学可以看看NioServerSocketPipelineSink这个类(重点看select唤醒后)…

到这步,就彻底郁闷了,话说其实到这步的时候我已经被这个问题困扰了2天多了,于是只好继续google,发现又有提到打开syn cookies的建议,于是尝试了下,竟然真的work了,打开了这个参数后新建连接的并发请求轻松超过100+了。

到此以为已经解决了,但很快开发给我反馈,整个集群开启了这个参数后,连接确实是能建上了,但客户端出现了发了请求后,等不到任何响应的现象,当时还不确定服务器端到底有没有收到请求,于是只好又先关闭了这个参数(话说这个我到现在都不明白为什么这个参数打开后,会出现发请求没响应的现象,求高人解答)。

尽管还是没解决,但毕竟有进展,有的进展就是打开了syn cookies后连接就能建上,而syn cookies只有在backlog满了后才会生效,那也就是说还是backlog满了,从kern的日志也能确认syn cookies确实是work了,到这步就觉得诡异了,明明netty用的默认值就是somaxconn,而每秒新建40多个连接,且boss线程还很空闲的情况下显然不应该出现backlog满的现象,这个时候仔细看了下本机查看的netty代码,才发现我看的是netty 4的代码,而应用用的是netty 363,悲催,赶紧把netty 363的代码捞下来看了,才发现在363里backlog的默认值处理时不一样的,在363里默认值是50,不是netty写的默认值,是java本身,50那估计真的不一定够,于是就通知开发在代码里先强制设置下backlog为1000。

在开发改代码的过程中,还有一个怀疑点想确认,就是既然是backlog满了,为什么看到的synrecv会是0呢,于是再用netstat -na | grep [port] | grep SYN_RECV -c统计了下,结果发现值基本一直是64,java层面默认设置的是50,linux会将这个值调整为大于这个值的2的n次幂的值,那也就是64,好吧,看到这就彻底可以确定真的是因为backlog太小了造成的(只是话说我不明白为什么ss -s统计出来的synrecv会是0呢,求高人解答)。

等开发改完代码重新发布后,稍微增加了点引流测试了下,轻松支撑每秒200+,客户端建立连接后发请求获取响应也完全ok,问题到此宣告解决。

题外话: 这应用之所以会比较容易出现较多的synrecv,主要是因为手机网络通常是不太稳定的,另外一个原因是这种对外的都很容易带来攻击,而当时刚好这个应用前面的一个用来防syn flood的由于有bug临时关闭了,所以问题暴露的比较明显。

从这个折腾了4天的case的排查过程,大家可以看到其实如果一开始我仔细确认过应用用的netty版本和我本机看的代码是不一致的话,估计很快就会排查出原因就是backlog值太小造成的,所以说折腾了这么多天其实也是自己造成的,这个告诉自己,以后排查问题的时候一定要对出现问题的应用所在的环境更加清楚的确认。

ps: 最后再多啰嗦几句,从这个case还能看到的是netty的版本其实在细节上是一直在改进的,就像这个case里的不同版本的netty在处理连接事件唤醒上,还有backlog的默认值上,所以我一直很强调,对于需要存活很多年的软件而言,选择一个使用范围较广的开源软件是非常重要的,如果自己开发,也许短期能超越,但放到三年、五年这样的范围来看,通常是很难和开源软件去抗衡的(原因是商业公司没多少人会专注在一个领域做三五年的,而开源界这样的人实在是多,说实话,这种case看过太多),所以如果觉得你能做的比开源软件好,还不如去帮助已有的(当然,如果这个领域目前完全没有什么使用面较广的、靠谱的,那自己做一个开源是挺好的),软件的可持续发展能力(除非是一次性软件、做的玩的或就玩个一两年的)是非常非常重要的。

一、什么是Netty Netty是一个高性能 事件驱动、异步非堵塞的IO(NIO)Java开源框架,Jboss提供,用于建立TCP等底层的连接,基于Netty可以建立高性能的Http服务器,快速开发高性能、高可靠性的网络服务器和客户端程序。

etty框架是用在服务器端,客户端是嵌入式编程,通过自定义的tcp通信协议进行连接的,现在需求是这样的,我的服务器端只是用来和客户端进行通信,现在有第三方如微信端进行支付成功后在数据库里生成了一条数据,表示要往某个客户端发送指令,我尝试了两种方式:

1、微信端生成通讯指令后调用TCP端的接口(负责通讯程序和数据库交互的),在接口程序中通过定义Socket连到通讯程序服务器端,根据通道编号去发送,但是这种会导致服务器端的tcp客户端连接变得更多

2、直接在netty框架中定义了scheduleAtF

CIM(CROSS-IM) 一款面向开发者的 IM(即时通讯)系统;同时提供了一些组件帮助开发者构建一款属于自己可水平扩展的 IM 。

借助 CIM 你可以实现以下需求:

下面来看看具体的架构设计。

整体主要由以下模块组成:

cim-server

IM 服务端;用于接收 client 连接、消息透传、消息推送等功能。

支持集群部署。

cim-forward-route

消息路由服务器;用于处理消息路由、消息转发、用户登录、用户下线以及一些运营工具(获取在线用户数等)。

cim-client

IM 客户端;给用户使用的消息终端,一个命令即可启动并向其他人发起通讯(群聊、私聊);同时内置了一些常用命令方便使用。

整体的流程也比较简单,流程图如下:

所以当我们自己部署时需要以下步骤:

接下来重点看看具体的实现,比如群聊、私聊消息如何流转;IM 服务端负载均衡;服务如何注册发现等等。

IM 服务端

先来看看服务端;主要是实现客户端上下线、消息下发等功能。

首先是服务启动:

由于是在 SpringBoot 中搭建的,所以在应用启动时需要启动 Netty 服务。

从 pipline 中可以看出使用了 Protobuf 的编解码(具体报文在客户端中分析)。

注册发现

需要满足 IM 服务端的水平扩展需求,所以 cim-server 是需要将自身数据发布到注册中心的。

所以在应用启动成功后需要将自身数据注册到 Zookeeper 中。

最主要的目的就是将当前应用的 ip + cim-server-port+ http-port 注册上去。

上图是我在演示环境中注册的两个 cim-server 实例(由于在一台服务器,所以只是端口不同)。

这样在客户端(监听这个 Zookeeper 节点)就能实时的知道目前可用的服务信息。

登录

当客户端请求 cim-forward-route 中的登录接口(详见下文)做完业务验证(就相当于日常登录其他网站一样)之后,客户端会向服务端发起一个长连接,如之前的流程所示:

这时客户端会发送一个特殊报文,表明当前是登录信息。

服务端收到后就需要将该客户端的 userID 和当前 Channel 通道关系保存起来。

同时也缓存了用户的信息,也就是 userID 和 用户名。

离线

当客户端断线后也需要将刚才缓存的信息清除掉。

同时也需要调用 route 接口清除相关信息(具体接口看下文)。

IM 路由

从架构图中可以看出,路由层是非常重要的一环;它提供了一系列的 HTTP 服务承接了客户端和服务端。

目前主要是以下几个接口。

注册接口

由于每一个客户端都是需要登录才能使用的,所以第一步自然是注册。

这里就设计的比较简单,直接利用 Redis 来存储用户信息;用户信息也只有 ID 和 userName 而已。

只是为了方便查询在 Redis 中的 KV 又反过来存储了一份 VK,这样 ID 和 userName 都必须唯一。

登录接口

这里的登录和 cim-server 中的登录不一样,具有业务性质,

为了实现只能一个用户登录,使用了 Redis 中的 set 来保存登录信息;利用 userID 作为 key ,重复的登录就会写入失败。

获取一台可用的路由实例也比较简单:

当然要获取 Zookeeper 中的服务实例前自然是需要监听 cim-server 之前注册上去的那个节点。

具体代码如下:

也是在应用启动之后监听 Zookeeper 中的路由节点,一旦发生变化就会更新内部缓存。

群聊接口

这是一个真正发消息的接口,实现的效果就是其中一个客户端发消息,其余所有客户端都能收到!

流程肯定是客户端发送一条消息到服务端,服务端收到后在上文介绍的 SessionSocketHolder 中遍历所有 Channel(通道)然后下发消息即可。

服务端是单机倒也可以,但现在是集群设计。所以所有的客户端会根据之前的轮询算法分配到不同的 cim-server 实例中。

因此就需要路由层来发挥作用了。

路由接口收到消息后首先遍历出所有的客户端和服务实例的关系。

路由关系在 Redis 中的存放如下:

由于 Redis 单线程的特质,当数据量大时;一旦使用 keys 匹配所有 cim-route: 数据,会导致 Redis 不能处理其他请求。

所以这里改为使用 scan 命令来遍历所有的 cim-route:。

接着会挨个调用每个客户端所在的服务端的 HTTP 接口用于推送消息。

在 cim-server 中的实现如下:

cim-server 收到消息后会在内部缓存中查询该 userID 的通道,接着只需要发消息即可。

在线用户接口

这是一个辅助接口,可以查询出当前在线用户信息。

实现也很简单,也就是查询之前保存 ”用户登录状态的那个去重 set “即可。

私聊接口

之所以说获取在线用户是一个辅助接口,其实就是用于辅助私聊使用的。

一般我们使用私聊的前提肯定得知道当前哪些用户在线,接着你才会知道你要和谁进行私聊。

类似于这样:

在我们这个场景中,私聊的前提就是需要获得在线用户的 userID。

所以私聊接口在收到消息后需要查询到接收者所在的 cim-server 实例信息,后续的步骤就和群聊一致了。调用接收者所在实例的 HTTP 接口下发信息。

只是群聊是遍历所有的在线用户,私聊只发送一个的区别。

下线接口

一旦客户端下线,我们就需要将之前存放在 Redis 中的一些信息删除掉(路由信息、登录状态)。

IM 客户端

客户端中的一些逻辑其实在上文已经谈到一些了。

登录

第一步也就是登录,需要在启动时调用 route 的登录接口,获得 cim-server 信息再创建连接。

登录过程中 route 接口会判断是否为重复登录,重复登录则会直接退出程序。

接下来是利用 route 接口返回的 cim-server 实例信息(ip+port)创建连接。

最后一步就是发送一个登录标志的信息到服务端,让它保持客户端和 Channel 的关系。

自定义协议

上文提到的一些登录报文、真正的消息报文这些其实都是在我们自定义协议中可以区别出来的。

由于是使用 Google Protocol Buffer 编解码,所以先看看原始格式。

其实这个协议中目前一共就三个字段:

目前主要是三种类型,分别对应不同的业务:

心跳

为了保持客户端和服务端的连接,每隔一段时间没有发送消息都需要自动的发送心跳。

目前的策略是每隔一分钟就是发送一个心跳包到服务端:

这样服务端每隔一分钟没有收到业务消息时就会收到 ping 的心跳包:

内置命令

客户端也内置了一些基本命令来方便使用。

比如输入 :q 就会退出客户端,同时会关闭一些系统资源。

当输入 :olu(onlineUser 的简写)就会去调用 route 的获取所有在线用户接口。

群聊

群聊的使用非常简单,只需要在控制台输入消息回车即可。

这时会去调用 route 的群聊接口。

私聊

私聊也是同理,但前提是需要触发关键字;使用 userId;;消息内容 这样的格式才会给某个用户发送消息,所以一般都需要先使用 :olu 命令获取所以在线用户才方便使用。

消息回调

为了满足一些定制需求,比如消息需要保存之类的。

所以在客户端收到消息之后会回调一个接口,在这个接口中可以自定义实现。

因此先创建了一个 caller 的 bean,这个 bean 中包含了一个 CustomMsgHandleListener 接口,需要自行处理只需要实现此接口即可。

自定义界面

由于我自己不怎么会写界面,但保不准有其他大牛会写。所以客户端中的群聊、私聊、获取在线用户、消息回调等业务(以及之后的业务)都是以接口形式提供。

也方便后面做页面集成,只需要调这些接口就行了;具体实现不用怎么关心。

cim 目前只是第一版,BUG 多,功能少(只拉了几个群友做了测试);不过后续还会接着完善,至少这一版会给那些没有相关经验的朋友带来一些思路。

欢迎工作一到五年的Java工程师朋友们加入Java程序员开发: 721575865

群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!

如果你指的是单机的话,不说Netty会怎么样,服务器都有可能直接崩溃掉,你的算一下,按平均每链接传输数据1K,100W链接大概数据量会在1G左右,G级服务器网卡也受不了的,我们在网络编程中对单机来讲,成功解决了C10K的问题,这种M级别的链接,可能暂时解决不了。对于如此大的并发,一般我们都是通过负载均衡的方式进行处理,如新浪微博,同时在线100W以上,通过约100多个节点处理,每个节点也就才10000并发左右。

DABAN RP主题是一个优秀的主题,极致后台体验,无插件,集成会员系统
网站模板库 » Netty笔记之六:Netty对websocket的支持

0条评论

发表评论

提供最优质的资源集合

立即查看 了解详情