netty系列之:中国加油

2024-02-24 15:58
文章标签 系列 中国 netty 加油

本文主要是介绍netty系列之:中国加油,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 简介
  • 场景规划
  • 启动Server
  • 启动客户端
  • 消息处理
  • 消息处理中的陷阱
  • 总结

简介

之前的系列文章中我们学到了netty的基本结构和工作原理,各位小伙伴一定按捺不住心中的喜悦,想要开始手写代码来体验这神奇的netty框架了,刚好最近东京奥运会,我们写一个netty的客户端和服务器为中国加油可好?

场景规划

那么我们今天要搭建什么样的系统呢?

首先要搭建一个server服务器,用来处理所有的netty客户的连接,并对客户端发送到服务器的消息进行处理。

还要搭建一个客户端,这个客户端负责和server服务器建立连接,并发送消息给server服务器。在今天的例子中,客户端在建立连接过后,会首先发送一个“中国”消息给服务器,然后服务器收到消息之后再返回一个”加油!“ 消息给客户端,然后客户端收到消息之后再发送一个“中国”消息给服务器… 以此往后,循环反复直到奥运结束!

我们知道客户端和服务器端进行消息处理都是通过handler来进行的,在handler里面,我们可以重写channelRead方法,这样在读取channel中的消息之后,就可以对消息进行处理了,然后将客户端和服务器端的handler配置在Bootstrap中启动就可以了,是不是很简单?一起来做一下吧。

启动Server

假设server端的handler叫做CheerUpServerHandler,我们使用ServerBootstrap构建两个EventLoopGroup来启动server,有看过本系列最前面文章的小伙伴可能知道,对于server端需要启动两个EventLoopGroup,一个bossGroup,一个workerGroup,这两个group是父子关系,bossGroup负责处理连接的相关问题,而workerGroup负责处理channel中的具体消息。

启动服务的代码千篇一律,如下所示:

 // Server配置//boss loopEventLoopGroup bossGroup = new NioEventLoopGroup(1);//worker loopEventLoopGroup workerGroup = new NioEventLoopGroup();final CheerUpServerHandler serverHandler = new CheerUpServerHandler();try {ServerBootstrap b = new ServerBootstrap();b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)// tcp/ip协议listen函数中的backlog参数,等待连接池的大小.option(ChannelOption.SO_BACKLOG, 100)//日志处理器.handler(new LoggingHandler(LogLevel.INFO)).childHandler(new ChannelInitializer<SocketChannel>() {@Override//初始化channel,添加handlerpublic void initChannel(SocketChannel ch) throws Exception {ChannelPipeline p = ch.pipeline();//日志处理器p.addLast(new LoggingHandler(LogLevel.INFO));p.addLast(serverHandler);}});// 启动服务器ChannelFuture f = b.bind(PORT).sync();// 等待channel关闭f.channel().closeFuture().sync();

不同的服务,启动服务器的代码基本都是一样的,这里我们需要注意这几点。

在ServerBootstrap中,我们加入了一个选项:ChannelOption.SO_BACKLOG,ChannelOption.SO_BACKLOG对应的是tcp/ip协议listen(int socketfd,int backlog)函数中的backlog参数,用来初始化服务端可连接队列,backlog参数指定了这个队列的大小。因为对于一个连接来说,处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接,多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,

另外我们还添加了两个LoggingHandler,一个是给handler添加的,一个是给childHandler添加的。LoggingHandler主要监控channel中的各种事件,然后输出对应的消息,非常好用。

比如在服务器启动的时候会输出下面的日志:

 [nioEventLoopGroup-2-1] INFO  i.n.handler.logging.LoggingHandler - [id: 0xd9b41ea4] REGISTERED[nioEventLoopGroup-2-1] INFO  i.n.handler.logging.LoggingHandler - [id: 0xd9b41ea4] BIND: 0.0.0.0/0.0.0.0:8007[nioEventLoopGroup-2-1] INFO  i.n.handler.logging.LoggingHandler - [id: 0xd9b41ea4, L:/0:0:0:0:0:0:0:0:8007] ACTIVE

这个日志是第一个LoggingHandler输出的,分别代表了服务器端的REGISTERED、BIND和ACTIVE事件。从输出我们可以看到,服务器本身绑定的是0.0.0.0:8007。

在客户端启动和服务器端建立连接的时候会输出下面的日志:

[nioEventLoopGroup-2-1] INFO  i.n.handler.logging.LoggingHandler - [id: 0x37a4ba9f, L:/0:0:0:0:0:0:0:0:8007] READ: [id: 0x6dcbae9c, L:/127.0.0.1:8007 - R:/127.0.0.1:54566]
[nioEventLoopGroup-2-1] INFO  i.n.handler.logging.LoggingHandler - [id: 0x37a4ba9f, L:/0:0:0:0:0:0:0:0:8007] READ COMPLETE

上面日志表示READ和READ COMPLETE两个事件,其中 L:/127.0.0.1:8007 - R:/127.0.0.1:54566 代表本地服务器的8007端口连接了客户端的54566端口。

对于第二个LoggingHandler来说,会输出一些具体的消息处理相关的消息。比如REGISTERED、ACTIVE、READ、WRITE、FLUSH、READ COMPLETE等事件,这里面就不一一列举了。

启动客户端

同样的,假设客户端的handler名称叫做ChinaClientHandler,那么可以类似启动server一样启动客户端,如下:

// 客户端的eventLoopEventLoopGroup group = new NioEventLoopGroup();try {Bootstrap b = new Bootstrap();b.group(group).channel(NioSocketChannel.class).option(ChannelOption.TCP_NODELAY, true).handler(new ChannelInitializer<SocketChannel>() {@Overridepublic void initChannel(SocketChannel ch) throws Exception {ChannelPipeline p = ch.pipeline();//添加日志处理器p.addLast(new LoggingHandler(LogLevel.INFO));p.addLast(new ChinaClientHandler());}});// 启动客户端ChannelFuture f = b.connect(HOST, PORT).sync();

客户端启动使用的是Bootstrap,我们同样为他配置了一个LoggingHandler,并添加了自定义的ChinaClientHandler。

消息处理

我们知道有两种handler,一种是inboundHandler,一种是outboundHandler,这里我们是要监控从socket读取数据的事件,所以这里客户端和服务器端的handler都继承自ChannelInboundHandlerAdapter即可。

消息处理的流程是客户端和服务器建立连接之后,会首先发送一个”中国“的消息给服务器。

客户端和服务器建立连接之后,会触发channelActive事件,所以在客户端的handler中就可以发送消息了:

    public void channelActive(ChannelHandlerContext ctx) {ctx.writeAndFlush("中国");}

服务器端在从channel中读取消息的时候会触发channelRead事件,所以服务器端的handler可以重写channelRead方法:

    public void channelRead(ChannelHandlerContext ctx, Object msg) {log.info("收到消息:{}",msg);ctx.writeAndFlush("加油!");}

然后客户端从channel中读取到"加油!"之后,再将”中国“写到channel中,所以客户端也需要重写方法channelRead:

    public void channelRead(ChannelHandlerContext ctx, Object msg) {ctx.writeAndFlush("中国");}

这样是不是就可以循环往复的进行下去了呢?

消息处理中的陷阱

事实上,当你执行上面代码你会发现,客户端确实将”中国“ 消息写入了channel,但是服务器端的channelRead并没有被触发。为什么呢?

研究发下,如果写入的对象是一个String,程序内部会有这样的错误,但是这个错误是隐藏的,你并不会在运行的程序输出中看到,所以对新手小伙伴还是很不友好的。这个错误就是:

DefaultChannelPromise@57f5c075(failure: java.lang.UnsupportedOperationException: unsupported message type: String (expected: ByteBuf, FileRegion))

从错误的信息可以看出,目前支持的消息类型有两种,分别是ByteBuf和FileRegion。

好了,我们将上面的消息类型改成ByteBuf试一试:

        message = Unpooled.buffer(ChinaClient.SIZE);message.writeBytes("中国".getBytes(StandardCharsets.UTF_8));public void channelActive(ChannelHandlerContext ctx) {log.info("可读字节:{},index:{}",message.readableBytes(),message.readerIndex());log.info("可写字节:{},index:{}",message.writableBytes(),message.writerIndex());ctx.writeAndFlush(message);}

上面我们定义了一个ByteBuf的全局message对象,并将其发送给server,然后在server端读取到消息之后,再发送一个ByteBuf的全局message对象给client,如此循环往复。

但是当你运行上面的程序之后会发现,服务器端确实收到了”中国“,客户端也确实收到了”加油!“,但是客户端后续发送的”中国“消息服务器端却收不到了,怎么回事呢?

我们知道ByteBuf有readableBytes、readerIndex、writableBytes、writerIndex、capacity和refCnt等属性,我们将这些属性在message发送前和发送之后进行对比:

在消息发送之前:

可读字节:6,readerIndex:0
可写字节:14,writerIndex:6
capacity:20,refCnt:1

在消息发送之后:

可读字节:6,readerIndex:0
可写字节:-6,writerIndex:6
capacity:0,refCnt:0

于是问题找到了,由于ByteBuf在处理过一次之后,refCnt变成了0,所以无法继续再次重复写入,怎么解决呢?

简单的办法就是每次发送的时候再重新new一个ByteBuf,这样就没有问题了。

但是每次都新建一个对象好像有点浪费空间,怎么办呢?既然refCnt变成了0,那么我们调用ByteBuf中的retain()方法增加refCnt不就行了?

答案就是这样,但是要注意,需要在发送之前调用retain()方法,如果是在消息被处理过后调用retain()会报异常。

总结

好了,运行上面的程序就可以一直给中国加油了,YYDS!

本文的例子可以参考:learn-netty4

本文已收录于 http://www.flydean.com/06-netty-cheerup-china/

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

这篇关于netty系列之:中国加油的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/742599

相关文章

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

跨国公司撤出在华研发中心的启示:中国IT产业的挑战与机遇

近日,IBM中国宣布撤出在华的两大研发中心,这一决定在IT行业引发了广泛的讨论和关注。跨国公司在华研发中心的撤出,不仅对众多IT从业者的职业发展带来了直接的冲击,也引发了人们对全球化背景下中国IT产业竞争力和未来发展方向的深思。面对这一突如其来的变化,我们应如何看待跨国公司的决策?中国IT人才又该如何应对?中国IT产业将何去何从?本文将围绕这些问题展开探讨。 跨国公司撤出的背景与

科研绘图系列:R语言扩展物种堆积图(Extended Stacked Barplot)

介绍 R语言的扩展物种堆积图是一种数据可视化工具,它不仅展示了物种的堆积结果,还整合了不同样本分组之间的差异性分析结果。这种图形表示方法能够直观地比较不同物种在各个分组中的显著性差异,为研究者提供了一种有效的数据解读方式。 加载R包 knitr::opts_chunk$set(warning = F, message = F)library(tidyverse)library(phyl

【生成模型系列(初级)】嵌入(Embedding)方程——自然语言处理的数学灵魂【通俗理解】

【通俗理解】嵌入(Embedding)方程——自然语言处理的数学灵魂 关键词提炼 #嵌入方程 #自然语言处理 #词向量 #机器学习 #神经网络 #向量空间模型 #Siri #Google翻译 #AlexNet 第一节:嵌入方程的类比与核心概念【尽可能通俗】 嵌入方程可以被看作是自然语言处理中的“翻译机”,它将文本中的单词或短语转换成计算机能够理解的数学形式,即向量。 正如翻译机将一种语言

从戴尔公司中国大饭店DTF大会,看科技外企如何在中国市场发展

【科技明说 | 科技热点关注】 2024戴尔科技峰会在8月如期举行,虽然因事未能抵达现场参加,我只是观看了网上在线直播,也未能采访到DTF现场重要与会者,但是通过数十年对戴尔的跟踪与观察,我觉得2024戴尔科技峰会给业界传递了6大重要信号。不妨简单聊聊:从戴尔公司中国大饭店DTF大会,看科技外企如何在中国市场发展? 1)退出中国的谣言不攻自破。 之前有不良媒体宣扬戴尔将退出中国的谣言,随着2

flume系列之:查看flume系统日志、查看统计flume日志类型、查看flume日志

遍历指定目录下多个文件查找指定内容 服务器系统日志会记录flume相关日志 cat /var/log/messages |grep -i oom 查找系统日志中关于flume的指定日志 import osdef search_string_in_files(directory, search_string):count = 0

GPT系列之:GPT-1,GPT-2,GPT-3详细解读

一、GPT1 论文:Improving Language Understanding by Generative Pre-Training 链接:https://cdn.openai.com/research-covers/languageunsupervised/language_understanding_paper.pdf 启发点:生成loss和微调loss同时作用,让下游任务来适应预训

Java基础回顾系列-第七天-高级编程之IO

Java基础回顾系列-第七天-高级编程之IO 文件操作字节流与字符流OutputStream字节输出流FileOutputStream InputStream字节输入流FileInputStream Writer字符输出流FileWriter Reader字符输入流字节流与字符流的区别转换流InputStreamReaderOutputStreamWriter 文件复制 字符编码内存操作流(

Java基础回顾系列-第五天-高级编程之API类库

Java基础回顾系列-第五天-高级编程之API类库 Java基础类库StringBufferStringBuilderStringCharSequence接口AutoCloseable接口RuntimeSystemCleaner对象克隆 数字操作类Math数学计算类Random随机数生成类BigInteger/BigDecimal大数字操作类 日期操作类DateSimpleDateForma

Java基础回顾系列-第三天-Lambda表达式

Java基础回顾系列-第三天-Lambda表达式 Lambda表达式方法引用引用静态方法引用实例化对象的方法引用特定类型的方法引用构造方法 内建函数式接口Function基础接口DoubleToIntFunction 类型转换接口Consumer消费型函数式接口Supplier供给型函数式接口Predicate断言型函数式接口 Stream API 该篇博文需重点了解:内建函数式