-
java基础-netty详解
nio是net开发中最常被提起的点,而游戏服务器端对这个也是看的比较重。java底层提供了nio但是确实很少见有人直接用他,原因很简单,看netty或者mina的文章都可以看到原因,就是它比较难用,想实现很稳定的商用需要功底很深。
那么网络底层框架解决了这些问题,现在最主流的就是netty,最开始解除游戏行业的时候还是用的mina,mina实现的比较简单易上手,但是功能和灵活度欠缺。改用netty的时候刚好是主程职位,所有的工作都是我来做,所以有更深入了解netty的机会,但是也是被动的,因为公司要用就得研究的比较透啊,不然哪里敢使用。
一转眼使用了很多年了,也就是最开始转用netty的时候进行很深入的了解,net底层实现好了以后,几乎就不会再去管它了。一直都是花时间在游戏业务和团队管理上了。
接下来从经常被问到的一些主要问题分别讲起。
netty的结构?
1.AbstractBootstrap引导程序,无论是服务器端(ServerBootstrap)还是客户端(Bootstrap)都是用它来进行引导的。实例化一个这个对象、设置好内容、绑定端口,开启同步就OK了。
2.EventLoopGroup事件线程池组,它是组,这个组里面管理的就是EventLoop。这里需要两个线程池组,一个是boss一个是worker,boss线程池组个数与绑定的端口数相关,几个端口就设置几个,多了也没有用,它是用来管理外部连接事件的,连接成功创建了channel了就扔到worker里面去管理了。worker线程池组数量默认是核心数*2,它是管理真实连接的地方,当有任何事件的时候就会得到通知在线程池中处理channel的响应。
3.EventLoop事件线程池,线程池大家都可以理解,它里面管理一个执行线程池(但是像epollEventLoop其实是单线程运行的,也就是一个线程池只有一个线程)、一组channel、还有其他的锁;状态;任务队列等。最重要的是各种事件选择器(selector)是在这里面定义,后面会介绍各种selector的区别。
4.channel通道,简单的理解为socket连接的一种抽象,所有的连接都单独抽象成为通道放在EventLoop中管理,当哪个通道有事件了,就生成一个任务执行。worker通道建立是通过ChannelInitializer完成的。
5.ChannelInitializer通道初始化器,每个链接建立的时候,调用初始化器初始化一个channel,并且给channel构造后一个channelPipeline。
6.ChannelPipeline管道,一个channel有个管道,管道的作用就是管理一个责任链,任何事件的执行都是通过这个责任链一层一层执行的。每个责任链环节就是一个channelHandler。
7.ChannelHandler通道处理器,就是一个环节的处理,比如黑名单处理、拆包封包、编解码都是一个处理器。这个处理器是公用的,每个通道的没个环节都有一个ChannelHandlerContext。
8.ChannelHandlerContext通道处理器环境,就是记录在某个处理器环节的数据存放的,里面可以存放一些处理过或者待处理的属性。
所以netty的结构可以看做是
Bootstrap{
EventLoopGroup boss;
EventLoopGroup worker{
EventLoop{
Selector
ThreadPoolExecutor
Channel{
ChannelPipeline{
ChannelHandlerContext
}
}
}
}
}
channel、channelPipeline与channelContext的关系?
boss和work线程池的结构是什么样的?boss可以设置几个线程?
其实前面的netty的结构已经回答了这两个问题。
什么是多路复用,各种selector的区别,存在哪些问题?
多路复用是相对bio来说。bio中每个链路都是一个线程阻塞运行,是主动监听有任何信号就进行处理,而nio中是又一个selector来管理多个链路,selector监听某链路有信号才调用执行的被动处理。
selector实现主要有:
select-兼容性好
poll-实现类似select,没有连接数限制
epoll-没有连接数限制,效率高,高效率是因为它不只是返回信号,而且返回了哪些channel有什么信号,而不需要再去轮训一遍channel列表去找哪些channel有信号。
在linux操作系统下,epoll是最长被使用的选择器,但是它有一个臭名昭著的bug,在某些特殊情况下会导致selector线程唤醒,但是其实没有任务可以执行,这样它就一直轮训查找任务导致CPU到100%。netty解决的办法就是监听这种空轮训,若空轮训到达一定数量就考试重新建立这个selector。
什么是0拷贝?可能存在的问题。
0拷贝是指bytebuffer类型是直接应用的采用0拷贝特性。它的0拷贝是指操作系统的内核空间到用户空间之间不需要拷贝。
首先,分清哪些操作在用户空间,例如jvm里面的对象分配、复制、销毁都是用户空间,而所有io操作其实jvm只是给操作系统内核一些指令然后就等着操作系统完成,例如磁盘io操作、网络io操作,那么这就切换到内核态操作内核空间。
然后,减少拷贝操作就是减少时间,常规io操作都是内核读完复制信息到jvm的内存中等待jvm处理,处理完再往回写内核空间等待内核去写磁盘写网络,一来一回多两次内存复制。而0拷贝就是减少这两次内存复制的。
再然后,仅仅是减少了复制了么?当然不是,内核分配和读写这些内存数据其实远比用户态读写的更快。
最后,java的nio是如何实现0拷贝的。它是用过unsafe类来直接操作主内存的读写实现的这部分内存叫做堆外内存,而jvm内使用一个虚引对象用来对应这块直接内存进行管理。所以内核直接把网络缓冲区的内容直接写到这块内存或者从这个块内存写到网络缓冲区就可以,这样的速度非常块,而jvm内部使用这个虚引用对象直接读写内外内存的数据。
存在的问题:前面说了0拷贝是依靠对外内存实现的,那么问题就在于堆外内存的管理,若管理不好就会产生内存泄露,而之所以使用虚引用就是为了在虚引用被回收的时候可以通过回收方法对对堆外内存进行回收。而虚引用回收时是将这个回收方法放在一个队里里面等待jvm调用,而这个调用不一定被执行或者不一定被完全执行完。就会导致有部分堆外内存无法释放。所以netty的bytebuf对象外面会包裹一个探针,会有监控机制来随机检查探针是否正常来判断对象回收是否正常,一般出现问题都是因为没有调用回收犯法。
PS:注意,多路复用和0拷贝不是netty的特性,而是java的nio就已经提供了。
封包拆包怎么做的?
netty提供了三种封包拆包解码器来进行封包拆包,一般就够用了,也可以自己编程实现。
LengthFieldBasedFrameDecoder 头长度解码器
DelimiterBasedFrameDecoder 分隔符解码器
FixedLengthFrameDecoder 定长解码器
bytebuf池结构是什么样的?
池结构分为:arean、chunkList、chunk、page、subpage
PoolArena{
PoolChunkList{
PoolChunk{
PoolSubpage
}
}
}
如何做校验或者加密来保证不被修改,如何防止恶意重复发包?
可以在字节流的头部增加两个内容:1.整个协议内容的校验码放在协议头,可以对协议进行hash计算得到的结果放在这里,若内部被修改了则这个会对不上。2.协议头部放一个协议顺序号,每个客户端的序号自增,服务器端可以缓存最近10个序号,若重复发送就丢弃。
netty中tcp参数优化?
TCP_NODELAY,是否启用Nagle算法,该算法防止过多小数据包的发送,将小包合并后一起发送,看似挺有用,但是这会延迟协议的响应,所以游戏服务器一般都禁用。
SO_SNDBUF、SO_RCVBUF,tcp收发缓冲区大小,这个大小一般默认系统即可,若在网络层遇到瓶颈的时候可以适当放大,这个一般做压力测试的时候可以进行调优。
SO_BACKLOG,连接请求队列,当有瞬时有大量连接请求时,可以调整该队列,避免客户端连接直接被拒绝导致一直断线重连。
扩展TCP协议
创建连接,三次握手。客户端发送创建连接请求,服务器应答请求的同时发送创建连接请求,客户端应答请求,连接创建完毕。
断开连接,四次挥手。主动断开方发送断开请求并且保证不再发送协议,被断开方应答请求但是可能还有未发送完成的缓冲内容,被断开方缓冲区内容都发送完成后发送断开请求,主动段开方应答请求,自此断开连接。
之所以断开连接需要四步是因为被断开方不能同时就断开,因为还有一部分发送到一半,他发送完才能进行断开连接的请求。
弱网情况tcp的优点可能就真的成为短板,tcp协议的实现保证了收发的顺序和可靠性,这两个实现的原理就是将长协议拆分成有序的数据包,按照数据包的顺序进行发送,并且发送每一个数据包的时候都需要客户端进行确认应答,若超时未应答这进行重传,直到收到确认应答才发送下一个数据包,而这个超时时间会随着重试次数而递增。所以弱网的情况经常会发生多次超时,而如果协议比较大,分成多个数据包,每个数据包都超时重发多次,导致一条协议在网络传输上的超时就无法忍受了。解决办法:
1.尽量减小协议,比如大量的定义数据放在客户端。
2.减少阻塞协议的实现,有些协议必须等待收发完成而阻塞界面无法操作。但是大多数协议是不需要这个过程的。
3.有条件使用或者及其注重同步效率的,可以考虑使用udp协议。
TCP拥塞控制
网络io也是一种资源,资源也都不是无限度的,比如公司或者家里装网都会根据带宽收费的,带宽就是一种限制,链路上的路由、交换机的缓存大小也是限制。所以网络传输并不是可以无限量的发送(这里说的是同一时刻发送量,时间拉的足够长确实可以无限量),所以简单理解拥塞控制就是调整网络流量大小。
控制网络流量大小首先就是要知道控制的量,由于这个不只是本方网络状况的因素,也要看对方网络状况(我方带宽100M,对方带宽可能100M也可能是1M,而且现在移动网络应用的很多,所以这个值是不定的,一会高一会低)。所以这个控制量是不停的尝试和调整出来的。而确定这个值TCP的实现提供4个算法来结合使用(最初只有2种方法)。
首先明确,要确定这个值叫做拥塞窗口值 cwnd,它就是同时可以发送多少个数据包。另外有一个值ssthresh (slow start threshold)慢启动阈值,是协助cwnd使用的,这个值是一个转折点,即cwnd到达ssthresh的时候就要改变算法了。
阻塞控制的四个算法使用如下:
1.最开始,使用慢开始算法,最开始定义cwnd为1,即每次发送一个数据包,收到一个ack应答,cwnd加1即为2,则下次发送两个数据报收到两个ack应答,cwnd加2即为4,这样指数级增长。
2.cwnd达到ssthresh的时候,改用阻塞避免算法,该时猜测为可能要达到阻塞上限,cwnd增加速度从指数级改为线性的每次得到应答后增加为1,一直到有数据包超时重传再次进入到慢开始算法。
3.当有三个重复确认的时候,使用快速重传算法,为了避免数据包达到超时状态添加了快速重传算法,tcp发送数据包是有序的,当客户端收到数据包序号不连续的时候会重复发送ack应答不连续数据包前一个数据包的序号,当服务器方连续收到三个这样的重复确认则重发丢失的数据包,这样就在该数据包超时之前重传了,避免了超时的出现进入慢开始算法。
4.当用三个重复确认的时候,使用快速恢复算法调整cwnd和ssthresh值,三次重复确认可能网络就是有问题的,但是避免进入慢开始降低传输速度,所以这里进行调整cwnd和ssthresh都调整为当前cwnd的一半(有的也会调整成一半再加3其实就是加上三次重复亲确认的值),然后继续使用避免阻塞算法。
5.当有超时重传的包的时候,回到第一步使用慢开始算法重新来。
PS:其实整个过程就是一直在调整cwnd的大小,一直到维持比较稳定范围内,这个范围就是双方传输时的稳定值,小了网络利用不足,大了会产生大量丢包重传反倒影响TCP传输速率。一般在游戏中用到它的并不太多,要是游戏能把网络打满了,流量很客观,一般都是网络视频或者下载类应用考虑的多。
java中常见的网络异常
SocketTimeoutException 创建socket连接超时,如若设置了超时时间。
BindException:Address already in use: JVM_Bind 绑定的端口已经被使用,这个比较常见,例如服务重复启动,另外以前也碰到过一个问题就是创建socket连接的时候本地也要随机给一个端口,但是这个端口可能已经被占用(当时创建的是与MySQL的连接,而随机得到的本地端口号是绑定的游戏端口)所以也会报错,这个就是在操作系统层设置随机端口范围,避开常用端口和服务使用端口(一般都是用非常大的端口号)。
ConnectException: Connection refused: connect 创建socket连接的时候,IP地址或者端口号不能使用。原因多种,配置错了、网络不可达、有防火墙。
SocketException: Socket is closed 连接socket已经关闭了,但是在此对连接进行操作的异常,一般在操作的时候应该进行判断socket是否已关闭。
SocketException:Connection reset或者Connect reset by peer:Socket write error,连接socket被关闭(一般是指异常关闭对方检查连接没有关闭的情况)进行读或写的时候发生的异常。
原文:https://www.cnblogs.com/ijay/p/14513094.html