java开发过程中常用的代码库、工具类库
网络编程
netty
Netty封装了JDK的NIO,Netty是一个异步事件驱动的网络应用框架,用于快速开发可维护的高性能服务器和客户端TIO
是基于aio(nio2)的网络编程框架,和netty属于同类,但t-io更注重一线开发工程师的感受,提供了大量和业务相关的API。基于t-io来开发IM、TCP私有协议、RPC、游戏服务器端、推送服务、实时监控、物联网、UDP、Socket将会变得空前的简单。MINA
通讯框架
BIO:同步阻塞式IO,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
NIO:同步非阻塞式IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
Java AIO(NIO.2) : 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理
NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持
NIO采用轮询的方式,但是AIO不需要,AIO框架在windows下使用windows IOCP技术,在Linux下使用epoll多路复用IO技术模拟异步IO,向操作系统注册监听,操作系统数据准备好会主动通知应用程序(订阅、通知模式),不再需要selector轮询,由channel通道直接到操作系统注册监听。
NIO和AIO
NIO:会等数据准备好后,再交由应用进行处理,数据的读取/写入过程依然在应用线程中完成,只是将等待的时间剥离到单独的线程中去,节省数据准备时间,因为多路复用机制,Selector会得到复用,对于那些读写过程时间长的,NIO就不太适合。NIO适合读写连接多的小数据。数据量大的场景BIO也是很适合的。
AIO:读完(内核内存拷贝到用户内存)了系统再通知应用,使用回调函数,进行业务处理,AIO能够胜任读写过程长的任务。
NIO 同步非阻塞概念
Selector提供选择已经就绪的任务的能力:
Selector轮询注册在其上的Channel,如果某个Channel发生读写请求并且Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。(同步)
一个Selector可以同时轮询多个Channel,因为JDK使用了epoll()代替传统的select实现,所以没有最大连接句柄1024/2048的限制。所以,只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端。(非阻塞)
DelimiterBasedFrameDecoder
FixedLengthFrameDecoder
LineBasedFrameDecoder
遇到一个换行符,则认为是一个完整的报文LengthFieldBasedFrameDecoder
可以指定最大包长度、长度域大小与位置,new LengthFieldBasedFrameDecoder(1024,0,2),一个包最好不要超过2048比如LengthFieldBasedFrameDecoder,遇到错误包会进行丢弃,保证后续发送过来的正常包可以继续解析。比如指定包最大长度为1024,解析到的长度超过1024就会报错丢弃,如果有包头还可以initialBytesToStrip参数设置成0,在处理程序中校验包头。如果长度指定成65536,如果中间有包给的长度是错误的,比如设置成3万,就可能出现需要丢弃掉3万个字节后面的内容才可以继续正常解析。
如果想要保住万无一失,就必须增加包头、包尾、长度、MD5校验码,进行组合校验,可以保证业务包出错的情况还能比较正常的运行。一般情况下直接用框架自带的Decoder就够用了。
StringDecoder
、StringEncoder
通过数据库进行交互可能出现一个进程往数据库里写入了一条记录,通知另一个进程去读取。但是写入操作还未提交另一个进程就去读取数据了,读取到的数据为空,特别是在需要批量操作的场景容易出现。所以需要根据情况写入后进行提交操作,提交成功然后再通知另一个进程读取。
1 | @Transactional(propagation=Propagation.NEVER) // java spring ibatis 提交的代码 |
1 | <tx:annotation-driven transaction-manager="transactionManagerIT"/> |
或者直接通过配置
1 | <bean id="baseTransactionProxyIT" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean" abstract="true"> |
使用空闲时间发送心跳。无消息交互时就算空闲, 客户端一直不发心跳,服务端无法通过心跳判断什么时候需要断开连接。
1 | public IdleStateHandler(int readerIdleTimeSeconds, int writerIdleTimeSeconds, int allIdleTimeSeconds) { |
1 | f.channel().closeFuture().sync();//等待服务端关闭端口监听。主线程执行到这里就 wait 子线程结束,子线程才是真正监听和接受请求的,closeFuture()是开启了一个channel的监听器,负责监听channel是否关闭的状态,如果监听到channel关闭了,子线程才会释放 |
1 | // ==============================存在问题======================================================= |
serverBootstrap.bind(port).sync();
形式监听指定端口,不要用new InetSocketAddress(ip,port)
这种限制IP的形式readerIndex
会随着数据的读取而不断增加,所以保证每次decode读取一个完整包,如果不满足一个完整包就重置readerIndex
退出下次再进来读。一个线程读取多个连接的数据(考虑各个链接都只读取到部分数据的情况,数据处理线程需要考虑数据拼接)
每个连接对应一个读线程
多个线程随机读取不定连接的数据(考虑各个链接都只读取到部分数据的情况,数据处理线程需要考虑数据拼接)
不能直接使用read会阻塞线程,那后面的线程都无法执行了,可以先通过available判断是否可读。
任务分发可以建立一个主线程的阻塞队列,然后分发任务至子线程,子线程若处理完毕,则提交任务至主线程。
通过一个缓冲区就可以把同步转换成异步。
AIO(NIO2.0) 与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。其中的read/write方法,会返回一个带回调函数的对象,当执行完读取/写入操作后,直接调用回调函数。主要在java.nio.channels包下增加了下面四个异步通道:
AsynchronousSocketChannel 客户端异步socket
AsynchronousServerSocketChannel 服务器异步socket
AsynchronousFileChannel 用于文件异步读写
AsynchronousDatagramChannel UDP
BIO是一个连接一个线程。
NIO是一个请求一个线程。
AIO是一个有效请求一个线程。
先来个例子理解一下概念,以银行取款为例:
同步 : 自己亲自出马持银行卡到银行取钱(使用同步IO时,Java自己处理IO读写);
异步 : 委托一小弟拿银行卡到银行取钱,然后给你(使用异步IO时,Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS(银行卡和密码),OS需要支持异步IO操作API);
阻塞 : ATM排队取款,你只能等待(使用阻塞IO时,Java调用会一直阻塞到读写完成才返回);
非阻塞 : 柜台取款,取个号,然后坐在椅子上做其它事,等号广播会通知你办理,没到号你就不能去,你可以不断问大堂经理排到了没有,大堂经理如果说还没到你就不能去(使用非阻塞IO时,如果不能读写Java调用会马上返回,当IO事件分发器会通知可读写时再继续进行读写,不断循环直到读写完成)
epoll fd有一个私有的struct eventpoll,它记录哪一个fd注册到了epfd上。eventpoll 同样有一个等待队列,记录所有等待的线程。还有一个预备好的fd列表,这些fd可以进行读或写。
函数声明:int epoll_create(int size)
该函数生成一个epoll专用的文件描述符。它其实是在内核申请一空间,用来存放你想关注的socket fd上是否发生以及发生了什么事件。size就是你在这个epoll fd上能关注的最大socket fd数。创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
创建一个 epoll instance,实际上是创建了一个 eventpoll 实例,包含了红黑树以及一个双向链表。源码位置https://github.com/torvalds/linux/blob/master/fs/eventpoll.c#L177
红黑树的叶子节点都是 epitem 结构体。关于各项的解释,注释里已经说的比较清楚了。我们关心的应该是,当往这棵红黑树上添加、删除、修改节点的时候,我们从(用户态)程序代码中能操作的是一个 fd,即一个 socket 对应的 file descriptor,所以一个 epitem 实例与一个 socket fd 一一对应。另外还需要注意到的是 rdllink 这个变量,这个指向了上一步创建的 evnetpoll 实例中的成员变量 rdllist,也就是那个就绪链表。这里很重要,注意留意,后面会讲到。当然,我们还需要关注的是 event 这个变量,代表了我们针对这个 socket fd 关心的事件,比如 EPOLLIN、EPOLLOUT。通过上述的讲解应该大致明白了,当我们使用 socket() 或者 accept() 得到一个 socket fd 时,我们添加到这棵红黑树上的是一个结构体,与这个 socket fd 一一对应。
当我们通过 socket() 以及 accept() 获取到一个 socket 对象时,这个 socket 对象到底有哪些东西呢?可以看到,一个 socket 实例包含了一个 file 的指针,以及一个 socket_wq 变量。其中 socket_wq 中的 wait 表示等待队列,fasync_list 表示异步等待队列。
那么等待队列和异步等待队列中有什么呢?大致来说,等待队列和异步等待队列中存放的是关注这个 socket 上的事件的进程。区别是等待队列中的进程会处于阻塞状态,处于异步等待队列中的进程不会阻塞。
再简单总结一下收包以及触发的过程:
1 | 包从网卡进来 |
epoll触发:
上面其实提到了等待队列,每当我们创建一个 socket 后(无论是 socket()函数 还是 accept() 函数),socket 对象中会有一个进程的等待队列,表示某个或者某些进程在等待这个 socket 上的事件。
但是当我们往 epoll 红黑树上添加一个 epitem 节点(也就是一个 socket 对象,或者说一个 fd)后,实际上还会在这个 socket 对象的 wait queue 上注册一个 callback function,当这个 socket 上有事件发生后就会调用这个 callback function。这里与上面讲到的不太一样,并不会直接 wake up 一个等待进程,需要注意一下。
简单讲就是,这个 socket 在添加到这棵 epoll 树上时,会在这个 socket 的 wait queue 里注册一个回调函数,当有事件发生的时候再调用这个回调函数(而不是唤醒进程)。
很简单,这个回调函数会把这个 socket 添加到创建 epoll instance 时对应的 eventpoll 实例中的就绪链表上,也就是 rdllist 上,并唤醒 epoll_wait,通知 epoll 有 socket 就绪,并且已经放到了就绪链表中,然后应用层就会来遍历这个就绪链表,并拷贝到用户空间,开始后续的事件处理(read/write)。
所以这里其实就体现出与 select 的不同, epoll 把就绪的 socket 给缓存了下来,放到一个双向链表中,这样当唤醒进程后,进程就知道哪些 socket 就绪了,而 select 是进程被唤醒后只知道有 socket 就绪,但是不知道哪些 socket 就绪,所以 select 需要遍历所有的 socket。
另外,应用程序遍历这个就绪链表,由于就绪链表是位于内核空间,所以需要拷贝到用户空间,这里要注意一下,网上很多不靠谱的文章说用了共享内存,其实不是。由于这个就绪链表的数量是相对较少的,所以由内核拷贝这个就绪链表到用户空间,这个效率是较高的。
上面可以看到,这里确确实实是从内核复制 rdllist 到用户空间,非共享内存。应用程序调用 epoll_wait 返回后,开始遍历拷贝回来的内容,处理 socket 事件。
至此,从注册一个 file descriptor(socket fd) 到 epoll 红黑树,到这个 socket 上有数据包从网卡进来,再到如何触发 epoll,再到应用程序的用户空间,由应用程序开始 read/write 事件的整个过程就理顺了。
accept 的惊群效应。
先解释一下什么是惊群,如果一个 socket 上有多个进程在同时等待事件,当事件触发后,内核可能会唤醒多个或者所有在等待的进程,然而只会有一个进程成功获取该事件,其他进程都失败,这种情况就叫惊群,会一定程度浪费 cpu,影响性能。
流程:
epoll 在内核开辟了一块缓存,用来创建 eventpoll 对象,并返回一个 file descriptor 代表 epoll instance
这个 epoll instance 中创建了一颗红黑树以及一个就绪的双向链表(当然还有其他的成员)
红黑树用来缓存所有的 socket,支持 O(log(n)) 的插入和查找,减少后续与用户空间的交互
socket 就绪后,会回调一个回调函数(添加到 epoll instance 上时注册到 socket 的)
这个回调函数会把这个 socket 放到就绪链表,并唤醒 epoll_wait
应用程序拷贝就绪 socket 到用户空间,开始遍历处理就绪的 socket
如果有新的 socket,再添加到 epoll 红黑树上,重复这个过程
1 | // include/linux/rbtree.h |
函数声明:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
该函数用于控制某个epoll文件描述符上的事件,可以注册事件,修改事件,删除事件。
参数:
epfd:由 epoll_create 生成的epoll专用的文件描述符;
op:要进行的操作例如注册事件,可能的取值EPOLL_CTL_ADD 注册、EPOLL_CTL_MOD 修 改、EPOLL_CTL_DEL 删除
fd:关联的文件描述符;
event:指向epoll_event的指针;
如果调用成功返回0,不成功返回-1
int epoll_ctl(int epfd, intop, int fd, struct epoll_event*event);
epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
第一个参数是epoll_create()的返回值 ,
第二个参数表示动作,用三个宏来表示 :
EPOLL_CTL_ADD: 注册新的fd到epfd中;
EPOLL_CTL_MOD: 修改已经注册的fd的监听事件;
EPOLL_CTL_DEL: 从epfd中删除一个fd;
第三个参数 是需要监听的fd ,
第四个参数 是告诉内核需要监听什么事件
events可以是以下几个宏的集合:
EPOLLIN: 触发该事件,表示对应的文件描述符上有可读数据。(包括对端SOCKET正常关闭);
EPOLLOUT: 触发该事件,表示对应的文件描述符上可以写数据;
EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR: 表示对应的文件描述符发生错误;
EPOLLHUP: 表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT: 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。
函数声明:int epoll_wait(int epfd,struct epoll_event * events,int maxevents,int timeout)
该函数用于轮询I/O事件的发生;
参数:
epfd:由epoll_create 生成的epoll专用的文件描述符;
epoll_event:用于回传代处理事件的数组;
maxevents:每次能处理的事件数;
timeout:等待I/O事件发生的超时值(单位我也不太清楚);-1相当于阻塞,0相当于非阻塞。一般用-1即可
返回发生事件数。
epoll_wait运行的原理是
等侍注册在epfd上的socket fd的事件的发生,如果发生则将发生的sokct fd和事件类型放入到events数组中。
并 且将注册在epfd上的socket fd的事件类型给清空,所以如果下一个循环你还要关注这个socket fd的话,则需要用epoll_ctl(epfd,EPOLL_CTL_MOD,listenfd,&ev)来重新设置socket fd的事件类型。这时不用EPOLL_CTL_ADD,因为socket fd并未清空,只是事件类型清空。这一步非常重要。
epoll使用的资料网上一大把,EPOLLIN(读)监听事件的类型,大家一般使用起来一般没有什么疑问,无非是监听某个端口,一旦客户端连接有数据发送,它马上通知服务端有数据,一般用一个回调的读函数,从这个相关的socket接口读取数据就行了。但是有关EPOLLOUT(写)监听的使用,网上的资料却讲得不够明白,理解起来有点麻烦。因为监听一般都是被动操作,客户端有数据上来需要读写(被动的读操作,EPOLIN监听事件很好理解,但是服务器给客户发送数据是个主动的操作,写操作如何监听呢?
如果将客户端的socket接口都设置成 EPOLLIN | EPOLLOUT(读,写)两个操作都设置,那么这个写操作会一直监听,有点影响效率。经过查阅大量资料,我终于明白了EPOLLOUT(写)监听的使用场,一般说明主要有以下三种使用场景:
1: 对客户端socket只使用EPOLLIN(读)监听,不监听EPOLLOUT(写),写操作一般使用socket的send操作
2:客户端的socket初始化为EPOLLIN(读)监听,有数据需要发送时,对客户端的socket修改为EPOLLOUT(写)操作,这时EPOLL机制会回调发送数据的函数,发送完数据之后,再将客户端的socket修改为EPOLL(读)监听
3:对客户端socket使用EPOLLIN 和 EPOLLOUT两种操作,这样每一轮epoll_wait循环都会回调读,写函数,这种方式效率不是很好。
程序思路:
1 | for( ; ; ) |
epoll_wait ,timeout,详细使用epoll因为源码里对timeout不为0的情况下,还有些额外处理,引起其他耗时。如果使用epoll_wait()如果明确知道这次能取到东西下次直接把timeout设置为0,其实是可以稍微提升点性能的。
https://github.com/torvalds/linux/blob/master/fs/eventpoll.c#L1759
1 | SYSCALL_DEFINE4(epoll_wait, int, epfd, struct epoll_event __user *, events, |
epoll到底用没用到mmap?
select poll epoll 都是对poll机制的封装。epoll里面没有mmap相关代码。并没有用到内核态内存映射到用户态的技术。但是这个技术是存在的。dpdk,跟netmap绕过内核的tcp/ip协议栈,在用户态协议栈处理。减少中断,上下文切换等开销)就有用到内核态内存映射到用户态技术。要提升性能可以使用基于DPDK的Redis 回环口测试性能提高明显。
libevent:事件驱动,高性能;轻量级,专注于网络;跨平台,支持Windows、Linux、Mac Os等;支持多种 I/O多路复用技术,epoll、poll、dev/poll、select 和kqueue 等; 支持I/O,定时器和信号等事件;
mv与rename区别?
mv is a basic command line designed to do one thing and do it well (Unix philosophy) : move file(s) or directorie(s).
mv is a standard utility to move one or more files to a given target. It can be used to rename a file, if there’s only one file to move. If there are several, mv only works if the target is directory, and moves the files there.
So mv foo bar will either move the file foo to the directory bar (if it exists), or rename foo to bar (if bar doesn’t exist or isn’t a directory). mv foo1 foo2 bar will just move both files to directory bar, or complain if bar isn’t a directory.
mv will call the rename() C library function to move the files, and if that doesn’t work (they’re being moved to another filesystem), it will copy the files and remove the originals.
If all you have is mv and you want to rename multiple files, you’ll have to use a shell loop. There are a number of questions on that here on the site, see e.g. this, this, and others.
rename() only works on the same device, it just changes its name(or “moves” the name to another directory). rename() cannot move the file data from one location to another.
If you want to copy or move the file, you need to do it yourself:
复制文件的话sendfile也比较快,调用shell的mv方法不安全,性能也不高。
netty导致的too many files open、linux系统句柄超负荷?
升级netty版本,或升级springboot和cloud的版本试试。
重连的时候释放资源试试:
1 | this.workerGroup.shutdownGracefully(); |
基于HTTP服务器推送技术发展过程介绍,从早期的Comet模式到Websocket,一些比较好的实现框架比如DWR、Jetty、Netty等
WEB页面需要实时显示的场景:
“服务器推”技术在现实应用中有一些解决方案,本文将这些解决方案分为三类:一类需要在浏览器端安装插件,基于套接口传送信息,或是使用 RMI、CORBA 进行远程调用;而另一类则无须浏览器安装任何插件、基于 HTTP 长连接;还有一种就是Websocket目前最优方案
JAVA WEB设计模式的演变过程,以及MVC、MVP、MVVM概念介绍
Servlet+JSP(Java Server Page)。比较适合小项目。项目变大复杂后,后端各种复杂调用,各种代码掺杂在一起很难维护。JSP的代码维护性也会越来越差,还可以嵌套后端代码,如果嵌套了后端代码就导致前后端职责不明,维护成本越来越大。
MVC 是个非常好的协作模式,为了让 View 层更简单干脆,还可以选择 Velocity、Freemaker 等模板,问题点:
1、前端开发重度依赖开发环境。这种架构下,前后端协作有两种模式:一种是前端写 demo,写好后,让后端去套模板。淘宝早期包括现在依旧有大量业务线是这种模式。另一种协作模式是前端负责浏览器端的所有开发和服务器端的 View 层模板开发,支付宝是这种模式
2、前后端职责依旧纠缠不清。
Velocity 模板还是蛮强大的,变量、逻辑、宏等特性,依旧可以通过拿到的上下文变量来实现各种业务逻辑。这样,只要前端弱势一点,往往就会被后端要求在模板层写出不少业务代码。还有一个很大的灰色地带是 Controller,页面路由等功能本应该是前端最关注的,但却是由后端来实现。
经常会有人吐槽 Java,但 Java 在工程化开发方面真的做了大量思考和架构尝试。Java 蛮符合马云的一句话:让平凡人做非凡事。
例子:
spring mvc,spring boot,mybatis
package分层:
resources分层:
Controller只应该做自己应该做的事,它不应该处理业务相关的代码。Domain(service + dao)是一个相当复杂的层级,这里是业务的核心。dao文件夹和mappers文件夹都是数据层的一部分
我们在Controller层应该做的事是:
View(html)层是一直在变化的层级
java�ַ��������������ܣ�javaĬ���ַ�����Unicode����;����ʹ��System.out.println(Charset.defaultCharset());
�鿴jvmĬ�ϵ��ַ������ҵ�WIN10Ϊutf-8��
JVM���ַ�������ȡ���Dz���ϵͳĬ�ϵ��ַ�������һ������£�
软件公司技术选型方案
主要技术: