浅谈netty的诞生历程
1.如果要将一个数据报从一个源节点发送到另一个目的节点我们应该怎么办? ???最简单的方案是在源节点和目的节点中间建立一条安全可靠的通路,以后两个节点之间所有的通信都在这条通路上进行,我们只要保证这个通路上的每个节点都是正常运行的,那我们就能保证数据传输的准确性(虚电路)。
???随着节点的越来越多,这种方案的缺陷渐渐地展现出来:①维护一条安全可靠的通路的代价太大,需要确保每两个节点之间的消息都能够准确到达;②并且如果一个节点出问题了,那么和这个节点相关的通路都得重新规划,费时费力。
???怎么去解决呢?第一个问题,我们既然不想去保证每个节点之间都能发送成功,其实我们只要保证目的地最终的结果是正确的就可以,类似于分布式事务中从JTA和BASE模式的改变,最终一致性才是目的。第二个问题,其实如果一个节点出问题了,相隔的节点是最先知道的,如果我们把路径规划的工作放到中间节点上(也就是路由器),而中间节点也只需要关注我应该给哪个节点发送是最优的就可以了。这样就减轻了发送方的压力。就这样,TCP/IP协议诞生了。
2什么是TCP/IP协议? ???利用不可靠的ip协议实现可靠的传输。
???IP协议之所以是不可靠的是因为IP网络存在冲突丢包及传输错误甚至被恶意篡改的情况。
???简单来说,就是每两个节点之间采用ip协议(网络层)进行传输,但是因为不可靠,所以我们在上层,也就是传输层有自己的保证信息正确的机制,这就是TCP要做的事。
???那么TCP是怎么保证传输的可靠性的呢?
???首先要保证可靠性,那我们就得对每一条消息进行ack,也就是说source节点给dist端发送一个信息,要等到dist端进行ack确认收到了才能算是发送成功了,如果发送超时,就必须要有重传机制。
???当然,为了变得更加高效,更加可靠,tcp协议还包含很多东西:
???①首先就是我们熟悉的三次握手,四次挥手,用这个机制,tcp保证source节点和dist的可用性(可用性是指接收消息和发送消息都没有问题),具体怎么实现的,我们后面再说。
???②为了避免一个大的数据包重试的代价太大,tcp会将一个大的数据包分解成很多小的ip包,这样哪个包丢失了,我们重试哪个包。
???③如果一个包一个包的去发,然后在等ack是一件很low的事,因此tcp定义了一个滑动窗口协议,这个协议可以实现多个包同时发送,并能够实现流量控制,解决发送端发送消息和接收端处理消息速度有差异的问题,具体的实现我们后面说。
???④其他还有拥塞控制,积累确认,分组缓存,多种定时器等等为了可靠的传输而定的解决方案。
3.假如某一天你要实现两个进程之间通信的方案,你需要做什么? ???两个进程之间的通信,我们可以使用tcp协议,因此,我们需要先建立两个服务器之间的连接,哦对,我们应该先实现三次握手,然后发送接收消息的时候,我们要实现滑动窗口协议,我们还要进行流量控制,积累确认,分组缓存等等,记住,tcp只是一个协议,具体的实现是要我们来做的。怎么样,复杂不?为了解决这个问题,socket协议面世了。
4.什么是socket? ???为了让程序员们更加轻松,一个叫做Bill的人,看了看TCP/IP协议的RFC,然后自己把那些复杂的东西给实现了,他在tcp协议上面抽象了一层,将那些复杂的协议都给隐藏了起来,而我们基于他抽象的协议,我们可以很轻松的进行通信编程。这个抽象协议就是socket,意为插座,一个插头插进插座就建立了连接。
???那么这个协议究竟有多方便?我们看一下下面的伪代码
客户端:
client = new socket(ip,port)
conn = client.connect();
client .send();
client .receive();
conn.close();
???从上面我们可以看出我们客户端要做的事就是新建一个socketClient,然后建立连接,之后就可以从这个连接中发送消息,接收消息,其实在connect的过程中就进行了三次握手,在收发消息的过程中就进行了一系列控制操作,在close的过程中就进行了四次挥手,怎么样,bill良心吧…
服务端:
server = new socketServer();
server.bind(port);
server.listen();
while (true){
socket = server.accept();
new thread(new SocketHandler(socket)).start;
}class SocketHandler implements Runable{
Socket socket;
SocketHandler(Socket socket){
this.socket = socket;
}
public void run(){
socket.send(msg1);
socket.receive(msg2);
}
}
???服务端逻辑稍微复杂一点,就是新建一个socket服务端,然后占用一个端口,之后就在这个端口这个监听着,如果有客户端连接过来了那么就创建一个类似于socketClient一样的一个端点,去收发消息。
???这样就ok了吗?试想,在上面这个方案中,一个socket连接过来就要分配一个线程去处理这个socket之间的通信,那服务端的线程用完了怎么办?
5.伪异步io ???在jdk1.4之前,也就是nio推出之前,很多人因为阻塞式io所带来的巨大烦恼,已经在实践中采用了‘伪异步io’,就是将之前的阻塞式io的线程分配转换成了任务以及线程池的形式,这样一来解决了线程用完的尴尬,二来,后端开发人员完全可以将任务放到线程池的任务中,然后去处理别的事情,但是线程终归还是要消耗的,本质问题还是没有解决。(这个阶段并不是一个里程碑式的阶段,本质还是阻塞式io,因此代码就不写了 -_-)
6.linux 网络IO模型? ???试想,为了避免服务端每个连接都要消耗一个线程,我们是不是可以将一个线程当成多个线程来用?也就是说,在一个连接需要执行读写任务的时候,我们使用这个线程来工作,执行完之后,线程就回到线程池里,然后去执行其他的io操作,这样问题也许就解决了。
???但是因为io操作是涉及到操作系统内核的,这里就需要对操作系统的几个io模型有一些了解
???在linux内核有五种io模型:阻塞IO模型,非阻塞IO模型,多路复用IO模型,信号驱动IO模型,异步IO模型,因为NIO socket主要使用使用了多路复用IO模型,这里仅仅对这个模型进行一些简单的介绍:
???linux提供了select/poll,进程将一个或多个fd(文件描述符,linux对每一次读写都会返回一个fd)传递给select或者poll系统调用,阻塞在select上,这样select/poll可以侦测所有的这些fd是否就绪(采用顺序扫描),当有fd就绪时,就会回调一个函数,执行操作,这样通过一个select就可以管理所有的fd,并且只去处理就绪的fd,因此那些没有就绪的io就不会去占用线程,这是不是和我们上面的一个线程代替多个线程的思想是相同的?
???java在多路复用IO的基础上,在jdk1.4推出了NIO socket。
7.什么是NIOSocket? ???NIO官方称new io 但是大多数人都喜欢称之为非阻塞式io (主要是针对之前的阻塞式io来说的)。虽然是NIO,其实他也是支持阻塞式io的,还是需要根据自己的业务情况来定,毕竟NIO的API使用难度有点过于复杂。直接上伪代码吧,这样更清楚一点。
???对了,在这之前我们先解释几个概念:
???1.ByteBuffer:这是一个字节缓存区,和他类似的还有很多,比如CharBuffer,IntBuffer等等,在NIO库中,所有的数据都是通过缓冲区来处理的。他的本质其实就是一个数组,但是多提供了一些操作而已。
???2.Channel: 这是一个全双工通道,它能够更好的底层操作系统的api,因为底层的通道都是全双工的。
???3.selector:选择器(多路复用器)他提供了选择就绪io任务的能力,也就是说,通过它我们可以将就绪的io拿出来,然后通过一个线程组去执行。
服务端代码:
//初始化服务端通道,一个父通道
serverChannel = ServerSocketChannel.open();
serverChannel.bind(ip,port);
serverChannel.configBlocking(false);
selector = new selector();
//注册父通道的接收连接事件,当有客户端连接时,这个事件处于就绪状态。
selectKey = serverChannel.regist(selector,"ACCEPT");
//选择操作,将就绪状态的筛选出来标记,将取消状态的删除
selector.select();
//取出所有就绪状态的事件key
keySet = selector.selectKeys();
it = keySet.iterator();
//遍历事件key
while(it.hasNext()){
selectKey = it.next();
//通过key处理这次io事件
handleIO(selectKey);
}handleIO(selectKey){
//如果是被连接就绪状态,接收连接,注册读事件
if(selectKey.isAccept()){
serverChannel = key.channel();
channel = serverChannel.accept();
channel.registor(selector,"READ");
}
//如果是读就绪状态,读出数据,处理数据
if(selectKey.isReadable()){
channel = key.channel();
msg = channel.read();
handleMsg(msg);
}
}//往通道中写数据
write(channel);
???当然上面是一个简单的不能再简单的伪代码,我们只是在阐述一种思想,实际上我们要处理的比这多得多,比如说编解码,续读,续写等等。
客户端伪代码:
channel = SocketChannel.open();
channel.configBlocking(false);
selector = new selector();
isSuccess = channel.connection(ip,port);
//连接成功,注册读事件,连接失败,注册连接事件
if(isSuccess){
channel.registor(selector,"READ");
}else{
channel.registor(selector,"CONNECT")
}
//选择操作,将就绪状态的筛选出来标记,将取消状态的删除
selector.select();
//取出所有就绪状态的事件key
keySet = selector.selectKeys();
it = keySet.iterator();
//遍历事件key
while(it.hasNext()){
selectKey = it.next();
//通过key处理这次io事件
handleIO(selectKey);
}handleIO(selectKey){
//如果连接就绪状态,连接
if(selectKey.isConnetable()){
channel = key.channel();
isSuccess = channel.finishConnect();
if(isSuccess){
channel.registor(selector,"READ");
}else{
exit;
}
}
//如果是读就绪状态,读出数据,处理数据
if(selectKey.isReadable()){
channel = key.channel();
msg = channel.read();
handleMsg(msg);
}
}//往通道中写数据
write(channel);
???其实原理很简单,就是将所有的就绪事件取出来,然后用一个线程或者线程组去执行,执行完了就收回线程,然后再去轮询,再取出就绪状态的事件。。。这样就可以实现很高的线程利用率,实现高并发!
???那么,这样是不是就ok了呢?
???在实践中,人们渐渐发现了jdk原生NIO的缺陷,首先就是API太过于复杂,需要了解很多底层的东西才能开发一套高质量的nio框架,并且我们在生产过程中,很多业务还需要处理网络的闪断问题、客户端的重复接入问题、安全认证问题,消息编解码问题、半包读写问题等等,非常的麻烦。
???为了解决这些问题,很快就有很多人又封装了一层框架,在框架内部解决了这些不应该是业务程序员解决的问题,提供了一套简单易用的api,其中最成功的框架就是netty。
【浅谈netty的诞生历程】???至于netty的具体用法及其原理,我们下回分解。
推荐阅读
- 热闹中的孤独
- JAVA(抽象类与接口的区别&重载与重写&内存泄漏)
- 放屁有这三个特征的,请注意啦!这说明你的身体毒素太多
- 一个人的旅行,三亚
- 布丽吉特,人生绝对的赢家
- 慢慢的美丽
- 尽力
- 一个小故事,我的思考。
- 家乡的那条小河
- 《真与假的困惑》???|《真与假的困惑》??? ——致良知是一种伟大的力量