精华内容
下载资源
问答
  • 既然涉及到网络通信,就不得不说一下多线程,同步异步相关的知识了.Netty的网络模型是多线程的Reactor模式,所有I/O请求都是异步调用,我们今天就来探讨一下一些基础概念和Java NIO的底层机制.为了节约你的时间,...

    上一篇文章我们主要讲解了Netty的Channel和Pipeline,了解到不同的Channel可以提供基于不同网络协议的通信处理.既然涉及到网络通信,就不得不说一下多线程,同步异步相关的知识了.Netty的网络模型是多线程的Reactor模式,所有I/O请求都是异步调用,我们今天就来探讨一下一些基础概念和Java NIO的底层机制.

    为了节约你的时间,本文主要内容如下:

    异步,阻塞的概念

    操作系统I/O的类型

    Java NIO的Linux底层实现

    异步,同步,阻塞,非阻塞

    同步和异步关注的是消息通信机制,所谓同步就是调用者进行调用后,在没有得到结果之前,该调用一直不会返回,但是一旦调用返回,就得到了返回值,同步就是指调用者主动等待调用结果;而异步则相反,执行调用之后直接返回,所以可能没有返回值,等到有返回值时,由被调用者通过状态,通知来通知调用者.异步就是指被调用者来通知调用者调用结果就绪*.所以,二者在消息通信机制上有所不同,一个是调用者检查调用结果是否就绪,一个是被调用者通知调用者结果就绪

    阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.阻塞调用是指在调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会继续执行.非阻塞调用是指在不能立刻得到结构之前,调用线程不会被挂起,还是可以执行其他事情.

    两组概念相互组合就有四种情况,分别是同步阻塞,同步非阻塞,异步阻塞,异步非阻塞.我们来举个例子来分别类比上诉四种情况.

    比如你要从网上下载一个1G的文件,按下下载按钮之后,如果你一直在电脑旁边,等待下载结束,这种情况就是同步阻塞;如果你不需要一直呆在电脑旁边,你可以去看一会书,但是你还是隔一段时间来查看一下下载进度,这种情况就是同步非阻塞;如果你一直在电脑旁边,但是下载器在下载结束之后会响起音乐来提醒你,这就是异步阻塞;但是如果你不呆在电脑旁边,去看书,下载器下载结束后响起音乐来提醒你,那么这种情况就是异步非阻塞.

    Unix的I/O类型

    知道上述两组概念之后,我们来看一下Unix下可用的5种I/O模型:

    阻塞I/O

    非阻塞I/O

    多路复用I/O

    信号驱动I/O

    异步I/O

    前4种都是同步,只有最后一种是异步I/O.需要注意的是Java NIO依赖于Unix系统的多路复用I/O,对于I/O操作来说,它是同步I/O,但是对于编程模型来说,它是异步网络调用.下面我们就以系统read的调用来介绍不同的I/O类型.

    当一个read发生时,它会经历两个阶段:

    1 等待数据准备

    2 将数据从内核内存空间拷贝到进程内存空间中

    不同的I/O类型,在这两个阶段中有不同的行为.但是由于这块内容比较多,而且多为表述性的知识,所以这里我们只给出几张图片来解释,具体解释大家可以参看这篇博文

    201742-netty-0_1280550787I2K8.gif

    201742-netty-0_128055089469yL.gif

    201742-netty-0_1280551028YEeQ.gif

    201742-netty-0_1280551287S777.gif

    201742-netty-0_1280551552NVgW.gif

    Java NIO的Linux底层实现

    我们都知道Netty通过JNI的方式提供了Native Socket Transport,为什么Netty要提供自己的Native版本的NIO呢?明明Java NIO底层也是基于epoll调用(最新的版本)的.这里,我们先不明说,大家想一想可能的情况.下列的源码都来自于OpenJDK-8u40-b25版本.

    open方法

    如果我们顺着Selector.open()方法一个类一个类的找下去,很容易就发现Selector的初始化是由DefaultSelectorProvider根据不同操作系统平台生成的不同的SelectorProvider,对于Linux系统,它会生成EPollSelectorProvider实例,而这个实例会生成EPollSelectorImpl作为最终的Selector实现.

    EpollArrayWapper将Linux的epoll相关系统调用封装成了native方法供EpollSelectorImpl使用.

    上述三个native方法就对应Linux下epoll相关的三个系统调用

    所以,我们会发现在EpollArrayWapper的构造函数中调用了epollCreate方法,创建了一个epoll的句柄.这样,Selector对象就算创造完毕了.

    register方法

    与open类似,ServerSocketChannel的register函数底层是调用了SelectorImpl类的register方法,这个SelectorImpl就是EPollSelectorImpl的父类.

    EpollSelectorImpl的相应的方法实现如下,它调用了EPollArrayWrapper的add方法,记录下Channel所对应的fd值,然后将ski添加到keys变量中.在EPollArrayWrapper中有一个byte数组eventLow记录所有的channel的fd值.

    我们会发现,调用register方法并没有涉及到EpollArrayWrapper中的native方法epollCtl的调用,这是因为他们将这个方法的调用推迟到Select方法中去了.

    Select方法

    和register方法类似,SelectorImpl中的select方法最终调用了其子类EpollSelectorImpl的doSelect方法

    由上述的代码,可以看到,EPollSelectorImpl先调用EPollArrayWapper的poll方法,然后在更新SelectedKeys.其中poll方法会先调用epollCtl来注册先前在register方法中保存的Channel的fd和感兴趣的事件类型,然后epollWait方法等待感兴趣事件的生成,导致线程阻塞.

    等待关注的事件产生之后(或在等待时间超过预先设置的最大时间),epollWait函数就会返回.select函数从阻塞状态恢复.

    selectedKeys方法

    我们先来看SelectorImpl中的selectedKeys方法.

    很奇怪啊,怎麽直接就返回publicSelectedKeys了,难道在select函数的执行过程中有修改过这个变量吗?

    publicSelectedKeys这个对象其实是selectedKeys变量的一份副本,你可以在SelectorImpl的构造函数中找到它们俩的关系,我们再回头看一下select中updateSelectedKeys方法.

    后记

    看到这里,详细大家都已经了解到了NIO的底层实现了吧.这里我想在说两个问题.

    一是为什么Netty自己又从新实现了一边native相关的NIO底层方法? 听听Netty的创始人是怎麽说的吧链接

    二是看这么多源码,花费这么多时间有什么作用呢?我感觉如果从非功利的角度来看,那么就是纯粹的希望了解的更多,有时候看完源码或在理解了底层原理之后,都会用一种恍然大悟的感觉,比如说AQS的原理.如果从目的性的角度来看,那么就是你知道底层原理之后,你的把握性就更强了,如果出了问题,你可以更快的找出来,并且解决.除此之外,你还可以按照具体的现实情况,以源码为模板在自己造轮子,实现一个更加符合你当前需求的版本.

    后续如果有时间,我希望好好了解一下epoll的操作系统级别的实现原理.

    关于图片和转载

    88x31.png

    本作品采用知识共享署名 4.0 国际许可协议进行许可。

    转载时请注明原文链接,图片在使用时请保留图片中的全部内容,可适当缩放并在引用处附上图片所在的文章链接。

    关于评论和留言

    如果对本文 Netty源码(三):I/O模型和Java NIO底层原理 的内容有疑问,请在下面的评论系统中留言,谢谢。

    展开全文
  • Java网络IO通信:Socket

    2019-09-18 02:30:48
    1. Socket通信原理 Socket的底层数据传输使用的是SocketStream,本质上还是属于IO流。Socket的通信分为客户端和服务器端。客户端主要向服务器端发送请求和接受返回值;服务器端用于接收客户端请求,处理请求及返回...

    1. Socket通信原理

    Socket的底层数据传输使用的是SocketStream,本质上还是属于IO流。Socket的通信分为客户端和服务器端。客户端主要向服务器端发送请求和接受返回值;服务器端用于接收客户端请求,处理请求及返回处理结果。下面从两个方面介绍其基本原理:

    1.1  服务器端的流程

    1)服务器端首先要启动监听。监听使用的类ServerSocket,创建该类的实例,然后调用bind方法绑定监听的IP地址和端口号。然后调用accept方法进行监听,此时IO阻塞一直到有请求。

    2)当服务器端监听到客户端的请求,则accept方法会返回一个Socket对象,这时候已经与客户端建立了Socket通道,使用Socket的getInputStream()方法获取通道输入流,通过输入流获取客户端发送的请求数据。

    3)获取客户端的请求数据后,服务器开始调用相应的方法等来处理。接着使用socket获取输出流getOutputStream(),将结果写回到客户端。

    4)最后一步,关闭输入输出流及socket通道。

    需要注意的是:使用socketinputstream读取数据都是祖阻塞的。

    1.2 客户端流程

     1)创建Socket对象,该对象绑定到服务器监听的IP地址和端口号。

     2)同样用getInoutStream和getOutputStream获取输入输出流接受数据和发送数据。

    总结:从上述流程可以看出,在服务器端只需要使用ServerSocket进行监听客户端请求,实际的通信是由Socket底层的输入输出流完成的。Socket很大的缺点是读取数据时会发生阻塞。

     

    2. Socket的实例

    在该实例中有详细的代码注解。

    服务器端代码的实现:

    package com.dd171290.socket;
    
    import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.net.ServerSocket;
    import java.net.Socket;
    
    public class UserServer {
    
    	
    	@SuppressWarnings("resource")
    	public static void main(String[] args) throws IOException {
    		//服务器端监听客户端的请求,使用的类是ServerSocket
    		ServerSocket serverSocket=new ServerSocket();
    		//绑定监听的ip地址和端口号
    		serverSocket.bind(new InetSocketAddress("localhost", 9920));
    		//开始监听客户端请求,此时IO阻塞,一直到有新的请求时被激活,为了能够处理多客户端发来的请求
    		//使用线程来处理每一次请求的业务
    		while(true){
    			Socket socket=serverSocket.accept();
    			new Thread(new ServiceDeal(socket)).start();
    		}
    	}
    }
    

    线程的业务处理类,通过反射实现目标类的调用:

    package com.dd171290.socket;
    
    
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.io.OutputStream;
    import java.io.PrintWriter;
    import java.lang.reflect.Method;
    import java.net.Socket;
    
    public class ServiceDeal implements Runnable {
    
    	Socket socket;
    	InputStream inputStream=null;
    	OutputStream outputStream=null;
    	public ServiceDeal(Socket socket) {
    		this.socket=socket;
    	}
    
    	public void run() {
    		//开始处理业务逻辑
    		
    		//先获取到客户端的请求输入流
    		try {
    			inputStream=socket.getInputStream();
    			BufferedReader reader=new BufferedReader(new InputStreamReader(inputStream));
    			//从网络中读取客户端发送过来的数据流
    			//使用socketinputstream读取数据都是祖阻塞的。
    			String request=reader.readLine();
    			
    			//获取到了请求数据后,开始调用相对应的业务逻辑处理,这里采用的是反射来处理业务并将结果放回给客户端。
    			//为了方便,这里约定用空格来分割不同的参数
    			String [] params=request.split(" ");
    			String className=params[0].trim();//服务端调用类所在包的路径
    			String method=params[1].trim();//调用方法的名字
    			String arg=params[2].trim();//默认传递一个参数
    			System.out.println("packages:"+className);
    			System.out.println("method:"+method);
    			System.out.println("args:"+arg);
    			
    			//获取Class对象
    			Class<?> service=Class.forName(className);
    			Object data=service.newInstance();
    			Method method1=service.getMethod(method, String.class);
    			Object result=method1.invoke(data, arg);
    			System.out.println("返回的结果:"+result);
    			
    			//将结果返回给客户端
    			System.out.println("开始向客户端返回结果!");
    			outputStream=socket.getOutputStream();
    			PrintWriter pWriter=new PrintWriter(outputStream);
    			pWriter.println(result.toString());
    			pWriter.flush();
    			System.out.println("向客户端发送数据结束!!");
    		} catch (Exception e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		} finally {
    			try {
    				outputStream.close();
    				inputStream.close();
    				if(socket!=null)
    				   socket.close();
    			} catch (IOException e) {
    				// TODO Auto-generated catch block
    				e.printStackTrace();
    			}
    		}
    	}
    
    }
    

    业务处理的目标类:

    package com.dd171290.socket;
    
    public class ReflectData {
    
    	public String welcome(String msg) {
    		return "welcome:"+msg;
    	}
    }
    

    客户端代码实现:

    package com.dd171290.socket;
    
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.io.PrintWriter;
    import java.net.Socket;
    import java.net.UnknownHostException;
    
    public class UserClient {
    
    	
    	public static void main(String[] args) throws Exception {
    		// 向客户端发送请求
    		Socket socket=new Socket("localhost", 9920);
    		//获取输出流,发送数据
    		PrintWriter pWriter=new PrintWriter(socket.getOutputStream());
    		String mSg="com.dd171290.socket.ReflectData welcome client!";
    		pWriter.println(mSg);
    		pWriter.flush();
    		
    		
    		//从服务器端读取返回的结果
    		BufferedReader bReader=new BufferedReader(new InputStreamReader(socket.getInputStream()));
    		String result=bReader.readLine();
    		System.out.println(result);
    	}
    	
    }
    

     

    先启动服务端,运行的结果为:

    packages:com.dd171290.socket.ReflectData
    method:welcome
    args:client!
    返回的结果:welcome:client!
    开始向客户端返回结果!
    向客户端发送数据结束!!

    客户端运行的结果:

    welcome:client!

     

    转载于:https://my.oschina.net/dd171290/blog/869706

    展开全文
  • 这学期开始接触了java网络通信这方面的知识,对于网络通信的感觉由原来的那种感觉不可思议慢慢的发生了改变,感觉网络通信并不是原来想象的那么神奇,虽然不知道最底层那些数据是以怎么样的一种方式去传送的,或者...
    [size=large]        这学期开始接触了java中网络通信这方面的知识,对于网络通信的感觉由原来的那种感觉不可思议慢慢的发生了改变,感觉网络通信并不是原来想象的那么神奇,虽然不知道最底层那些数据是以怎么样的一种方式去传送的,或者说现在并不了解网络通信最初原始的原理是怎么样的,这些自己也还没有动手去查资料了解。但基于java这种语言,用它已经建好的一系列类和方法,我们便可以很好的利用这些工具来开发一些网络通信这方面的东西了(自己也利用这些知识实现了网络五子棋,网络画图板、简单的聊天工具、和文件的传输)。[/size]

       [size=large]我们知道网络通信,既然是通信他就不再局限于一台机器上的数据传送,他实现了不同机器上的数据传输。现在开始介绍一下,java中时怎么去实现通信的。从简单入手、实现一个客服端和服务器之间的简单通信。
      
    客服端和服务器能实现连接和通信的条件:客服端的创建需要知道它所连接的服务器的ip和端口号。服务器的创建需要一个端口号。至于ip可能大家比较清楚,我们知道每一台其计算机都有一个独一无二的ip地址,之所以为什么?这个很容易理解每不同的ip对应一台计算机才能实现数据的准确传输,只要你ip弄对了就不会发生你想把数据发送到A但却却发送到B这样的问题。而端口号又是什么呢?用ip类比,ip相当于一台计算机,而端口号就相当于一台电脑上一个程序,你要给一台计算机上的某一个程序发送数据,不仅仅要知道这台计算机的在哪里,还要知道这个程序在哪里。即你要知道ip(计算机的位置),和端口号(程序的位置)。再做一个类比,ip相当于一个酒店名,而他端口号就是这个就酒店房间的房间号。这样只要有了这两个信息我们就能很容易的找到我们要找的房间。

    说了这么多,如何用java代码编程实现通信了。这个是这边文章所要讲的重点内容。
    我们知道java中就是利用一个个类实例化的一个个对象,并调用他们所特有的方法来实现完成一个软件的开发的。当然通信也不例外。

       服务器的创建就是实例化一个ServerSocket类的一个对象。如下我们定义一个创建服务器的方法。[/size]

      /**
    * 创建服务器的方法
    * @param port端口号
    */
    public void creat_server(int port){
    try {
    //实例化服务器对象
    ServerSocket server=new ServerSocket(port);
    System.out.println("服务器启动成功,等待客服端连接、、、");
    } catch (IOException e) {
    e.printStackTrace();
    }
       }


       [size=large] 这样一个非常简单的客户端和服务器就建立起来了。

       那么该如何去实现我们建立起来的客户端和服务器的数据传输呢?这个是通信的重点所在。

       如上面所讲有了独一无二的ip和特定的端口号我们能让客户端很容易的找它所要连接的服务器。那么怎么样让客户端和服务器连接起来呢?这个很简单,只要调用我们上面建好的服务器对象server调用它等待客户端连接的方法accept(),就行了.
       即Socket client=server.accept();

       这是一个会阻塞的方法,所谓的阻塞就是当程序运行到这段代码的时候会停在这边,即等待到有相应的客户端程序运行起来,这句代码下面的代码才能被执行。
       要是还不懂,做一个小测试。[/size]
      Public class Test{
      Public static void main(String [] args){
      try {
      //实例化服务器对象
    ServerSocket server=new ServerSocket(9090);
    System.out.println("服务器启动成功,等待客服端连接、、、");
    //等待客服端连接
    Socket client=server.accept();
       System.out.println("连接服务器成功了!!!");
      } catch (IOException e) {
    e.printStackTrace();
    }
      }
      }


    [size=large]   把这个程序运行起来,下面的输出语句并没有被打印出来,因为上面server.accept();这个方法阻塞住了代码的执行。想要让他执行起来,只要有相应的客户端程序运行起来就行。
      
       如在一个主函数中运行我们上面写的创建客户端的代码。就能在控制台看到我们打印出来的东西了。需要注意的是创建客户端的端口号要和服务器的端口号对应起来,也就是上面的9090。这边就不多做测试,留给你们自己动手。
      
       这样我们就实现了客户端和服务器之间的连接了。
       那么接下来就是讲讲他们之间的交流了。

       I/o流我们都知道。(但大多人都没有很好的去解析java中的流,包括本人,所以在学通信过程中很多由于流而产生的问题都没有去真正解决,或者是解决了但却不知道所以然)。
       其实java通信过程简单的说就是流之间传送数据的过程、流的输入和输出过程。
      
       那么我们就必须从客户端和服务器中获取输入输出流。这个很容易。
       从我们上面实例化的客户端client对象中调用它的方法便能获取流。
       即:InputStream in=client.getInputStream();
       OutputStream out=client.getOutputStream();
       服务器和客户端两端的流都是这样获取的。

    获取了流,那么下面就可以利用流来实现客户端和服务器之间的数据传输了。
    比如用客户端这边的输出流out执行这样一句代码:
       out.write(10);
       意思就是从客户端向服务器发送10这个整形数据。
    那么以之相应的是服务器端执行代码:
       int i=In.read();

    那么这样服务器就读取了客户端所发送过来的数据。这样是否能准确传送客户端和服务器之间的数据呢?我们下面来做个测试。

    服务器:[/size]
      
      Public class Test{
      Public static void main(String [] args){
       try {
       // 实例化服务器对象
       ServerSocket server= new ServerSocket(9090);
       // 等待客服端连接
       Socket client=server.accept();
       System.out.println("服务器启动成功,等待客服端连接、、、");
       InputStream in=client.getInputStream();
       OutputStream out=client.getOutputStream();
       int i=0;
       while(true){
       i=in.read();
       System.out.println("客户端发送过来的数据是:"+i);
       }
       } catch (IOException e) {
       e.printStackTrace();
       }
      
      }



    [size=large]客服端:[/size]
      
      Public class Test2{
      Public static void main(String [] args){
       try {
       // 实例化客服端对象
       Socket client= new Socket ("localhost",9090);
       InputStream in=client.getInputStream();
       OutputStream out=client.getOutputStream();
       int i=0;
       while(true){
       i++;
       out.write(i);
       Thread.sleep(1000);
       }
       } catch (UnknownHostException e) {
       e.printStackTrace();
       } catch (IOException e) {
       e.printStackTrace();
       } catch (InterruptedException e) {
       e.printStackTrace();
       }
      
      }



    [size=large] 先运行服务器,在运行客户端,这样我们就可以从控制台看到打印出来如下:
    [align=center] 客户端发送过来的数据是:1
    客户端发送过来的数据是:2
    客户端发送过来的数据是:3
    客户端发送过来的数据是:4
    。。。。。。。。
    客户端发送过来的数据是:254
    客户端发送过来的数据是:255
    客户端发送过来的数据是:0
    客户端发送过来的数据是:1
    客户端发送过来的数据是:2[/align] 为什么255后面不是256??
    即用out.write(256);
    单用int i=in.read();接收到的却是0;
       这个问题我实在做网络画图板的时候发现的。比如说要在客户端画上一条直线,在服务器上能把这条直线显示出来。那么就必须把直线的四个坐标点传过去。四个坐标点都是整形,坐标点有很多大于255的,所以用上面的方法就出现了这样的问题,当你四个坐标点都不大于255的时候直线在服务器上画出来是和客户端一模一样的,也就是数据正确传送了。但但出现大于255的时候两条直线就差别很大了。这究竟是为什么呢?
      
       下面给你解答:

       由于OutputStream和InputStream这两种流在传输过程中是以一个字节为单位的。一个字节8个位所以最大只能传送2^8-1=255;当超过255它就自动截取低8位,比如256写成二进制是100000000,它就截取了8个0,所以服务器那边接收到的就是0了。
       相信这样并不难理解。

       发现了是由于这样的原因造成的,那么怎么去解决这个问题呢?
    知道了原因去解决问题是不难的。

    我们在传送整形数据之间可以先计算他的字节数,先发送字节数(有几个字节)给服务器,然后将它拆分成一个个字节,用for循环再用out.write();去发送数据;在接收到先收到它的字节数,然后相应的用for循环来把一个个字节接收过来。在并起来,两者是一个逆过程。这个我们就要自己去实现几个方法:


      发送端:
      1.获取一个整形数据对应的二进制的字节数。
      2.获取一个整形数据对象的二进制字符串,不足8的倍速补零。
      这样我们就能截取一个个8位字节去发送了。

      接收端:
      1.根据获取的字节数来计算相应的整形数据。


       比如发送端发送256,那么接收到会接收到2个字节,第一个字节获取的整形数据是0,第二是1,但第二个要相应乘于2^8,也就是256,然后两者相加。这样就能获得了。相信这样并不难懂。要是有三个字节。那么获取的第三个整形数据要乘以2^16在加上前面两个字节对应的整形数据。


       以上方法我们是基于InputStream和OutputStream流加上我们对问题的理解自定义的能准确传送比较大的整形数据的方法。显然十分的麻烦。其实有更简单的方法,用java中写好的类和封装好的方法可以非常容易的实现大型整形数据的传送。(他的底层操作或许就是我上面所说的哦!!!!)
      
       Java中提供了数据流DataInputStream和DataOutputStream这两个数据流
    他们有writeInt(),和相应的readInt()方法;你写入一个什么样的整形他就会相应读取到什么整形数。在实例化数据流的对象时只要把我们上面获取的in和out流对象作为参数,便可以建立两者之间的联系了。
    如DaraOutputStream dataout=new DataOutputStream(out);这样就行了。具体测试就有兴趣的可以自己去测试一下。


       有了上面这些知识,去实现一个简单的网络画图板、网络五子棋、还有文件的传送和聊天工具就不难了、有兴趣的同学可以去实践一下。
      
    关于java网络通信编程这边就略作介绍这些。后面关于我在利用java网络通信编程在写自己小项目遇到的问题和我如何去解决在做介绍。[/size]
    展开全文
  • 既然涉及到网络通信,就不得不说一下多线程,同步异步相关的知识了.Netty的网络模型是多线程的 Reactor模式,所有I/O请求都是异步调用,我们今天就来探讨一下一些基础概念和Java NIO的底层机制.  为了节...

    上一篇文章我们主要讲解了Netty的 ChannelPipeline,了解到不同的 Channel可以提供基于不同网络协议的通信处理.既然涉及到网络通信,就不得不说一下多线程,同步异步相关的知识了.Netty的网络模型是多线程的 Reactor模式,所有I/O请求都是异步调用,我们今天就来探讨一下一些基础概念和Java NIO的底层机制.

     为了节约你的时间,本文主要内容如下:

    • 异步,阻塞的概念

    • 操作系统I/O的类型

    • Java NIO的Linux底层实现

    异步,同步,阻塞,非阻塞

    同步和异步关注的是消息通信机制,所谓同步就是调用者进行调用后,在没有得到结果之前,该调用一直不会返回,但是一旦调用返回,就得到了返回值,同步就是指调用者主动等待调用结果;而异步则相反,执行调用之后直接返回,所以可能没有返回值,等到有返回值时,由被调用者通过状态,通知来通知调用者.异步就是指被调用者来通知调用者调用结果就绪所以,二者在消息通信机制上有所不同,一个是调用者检查调用结果是否就绪,一个是被调用者通知调用者结果就绪*

    阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.阻塞调用是指在调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会继续执行.非阻塞调用是指在不能立刻得到结构之前,调用线程不会被挂起,还是可以执行其他事情.

     两组概念相互组合就有四种情况,分别是同步阻塞,同步非阻塞,异步阻塞,异步非阻塞.我们来举个例子来分别类比上诉四种情况.

     比如你要从网上下载一个1G的文件,按下下载按钮之后,如果你一直在电脑旁边,等待下载结束,这种情况就是同步阻塞;如果你不需要一直呆在电脑旁边,你可以去看一会书,但是你还是隔一段时间来查看一下下载进度,这种情况就是同步非阻塞;如果你一直在电脑旁边,但是下载器在下载结束之后会响起音乐来提醒你,这就是异步阻塞;但是如果你不呆在电脑旁边,去看书,下载器下载结束后响起音乐来提醒你,那么这种情况就是异步非阻塞.

    Unix的I/O类型

     知道上述两组概念之后,我们来看一下Unix下可用的5种I/O模型:

    • 阻塞I/O

    • 非阻塞I/O

    • 多路复用I/O

    • 信号驱动I/O

    • 异步I/O

       前4种都是同步,只有最后一种是异步I/O.需要注意的是Java NIO依赖于Unix系统的多路复用I/O,对于I/O操作来说,它是同步I/O,但是对于编程模型来说,它是异步网络调用.下面我们就以系统 read的调用来介绍不同的I/O类型.

       当一个 read发生时,它会经历两个阶段:

    • 1 等待数据准备

    • 2 将数据从内核内存空间拷贝到进程内存空间中

       不同的I/O类型,在这两个阶段中有不同的行为.但是由于这块内容比较多,而且多为表述性的知识,所以这里我们只给出几张图片来解释,具体解释大家可以参看这篇博文

    Blocking I/O

    NonBlocking I/O

    Multiplexing I/O

    Asynchronous I/O

    对比

    Java NIO的Linux底层实现

     我们都知道Netty通过JNI的方式提供了Native Socket Transport,为什么 Netty要提供自己的Native版本的NIO呢?明明Java NIO底层也是基于 epoll调用(最新的版本)的.这里,我们先不明说,大家想一想可能的情况.下列的源码都来自于OpenJDK-8u40-b25版本.

    open方法

     如果我们顺着 Selector.open()方法一个类一个类的找下去,很容易就发现 Selector的初始化是由 DefaultSelectorProvider根据不同操作系统平台生成的不同的 SelectorProvider,对于Linux系统,它会生成 EPollSelectorProvider实例,而这个实例会生成 EPollSelectorImpl作为最终的 Selector实现.

    
    
    1. class EPollSelectorImpl extends SelectorImpl
    2. {
    3. .....
    4. // The poll object
    5. EPollArrayWrapper pollWrapper;
    6. .....
    7. EPollSelectorImpl(SelectorProvider sp) throws IOException {
    8. .....
    9. pollWrapper = new EPollArrayWrapper();
    10. pollWrapper.initInterrupt(fd0, fd1);
    11. .....
    12. }
    13. .....
    14. }

    EpollArrayWapper将Linux的epoll相关系统调用封装成了native方法供 EpollSelectorImpl使用.

    
    
    1. private native int epollCreate();
    2. private native void epollCtl(int epfd, int opcode, int fd, int events);
    3. private native int epollWait(long pollAddress, int numfds, long timeout,
    4. int epfd) throws IOException;

     上述三个native方法就对应Linux下epoll相关的三个系统调用

    
    
    1. //创建一个epoll句柄,size是这个监听的数目的最大值.
    2. int epoll_create(int size);
    3. //事件注册函数,告诉内核epoll监听什么类型的事件,参数是感兴趣的事件类型,回调和监听的fd
    4. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    5. //等待事件的产生,类似于select调用,events参数用来从内核得到事件的集合
    6. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

     所以,我们会发现在 EpollArrayWapper的构造函数中调用了 epollCreate方法,创建了一个epoll的句柄.这样, Selector对象就算创造完毕了.

    register方法

     与 open类似, ServerSocketChannelregister函数底层是调用了 SelectorImpl类的 register方法,这个 SelectorImpl就是 EPollSelectorImpl的父类.

    
    
    1. protected final SelectionKey register(AbstractSelectableChannel ch,
    2. int ops,
    3. Object attachment)
    4. {
    5. if (!(ch instanceof SelChImpl))
    6. throw new IllegalSelectorException();
    7. //生成SelectorKey来存储到hashmap中,一共之后获取
    8. SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this);
    9. //attach用户想要存储的对象
    10. k.attach(attachment);
    11. //调用子类的implRegister方法
    12. synchronized (publicKeys) {
    13. implRegister(k);
    14. }
    15. //设置关注的option
    16. k.interestOps(ops);
    17. return k;
    18. }

    EpollSelectorImpl的相应的方法实现如下,它调用了 EPollArrayWrapperadd方法,记录下Channel所对应的fd值,然后将ski添加到 keys变量中.在 EPollArrayWrapper中有一个byte数组 eventLow记录所有的channel的fd值.

    
    
    1. protected void implRegister(SelectionKeyImpl ski) {
    2. if (closed)
    3. throw new ClosedSelectorException();
    4. SelChImpl ch = ski.channel;
    5. //获取Channel所对应的fd,因为在linux下socket会被当作一个文件,也会有fd
    6. int fd = Integer.valueOf(ch.getFDVal());
    7. fdToKey.put(fd, ski);
    8. //调用pollWrapper的add方法,将channel的fd添加到监控列表中
    9. pollWrapper.add(fd);
    10. //保存到HashSet中,keys是SelectorImpl的成员变量
    11. keys.add(ski);
    12. }

     我们会发现,调用 register方法并没有涉及到 EpollArrayWrapper中的native方法 epollCtl的调用,这是因为他们将这个方法的调用推迟到 Select方法中去了.

    Select方法

     和 register方法类似, SelectorImpl中的 select方法最终调用了其子类 EpollSelectorImpldoSelect方法

    
    
    1. protected int doSelect(long timeout) throws IOException {
    2. .....
    3. try {
    4. ....
    5. //调用了poll方法,底层调用了native的epollCtl和epollWait方法
    6. pollWrapper.poll(timeout);
    7. } finally {
    8. ....
    9. }
    10. ....
    11. //更新selectedKeys,为之后的selectedKeys函数做准备
    12. int numKeysUpdated = updateSelectedKeys();
    13. ....
    14. return numKeysUpdated;
    15. }

     由上述的代码,可以看到, EPollSelectorImpl先调用 EPollArrayWapperpoll方法,然后在更新 SelectedKeys.其中 poll方法会先调用 epollCtl来注册先前在 register方法中保存的Channel的fd和感兴趣的事件类型,然后 epollWait方法等待感兴趣事件的生成,导致线程阻塞.

    
    
    1. int poll(long timeout) throws IOException {
    2. updateRegistrations(); 先调用epollCtl,更新关注的事件类型
    3. 导致阻塞,等待事件产生
    4. updated = epollWait(pollArrayAddress, NUM_EPOLLEVENTS, timeout, epfd);
    5. .....
    6. return updated;
    7. }

     等待关注的事件产生之后(或在等待时间超过预先设置的最大时间), epollWait函数就会返回. select函数从阻塞状态恢复.

    selectedKeys方法

     我们先来看 SelectorImpl中的 selectedKeys方法.

    
    
    1. //是通过Util.ungrowableSet生成的,不能添加,只能减少
    2. private Set<SelectionKey> publicSelectedKeys;
    3. public Set<SelectionKey> selectedKeys() {
    4. ....
    5. return publicSelectedKeys;
    6. }

     很奇怪啊,怎麽直接就返回 publicSelectedKeys了,难道在 select函数的执行过程中有修改过这个变量吗?

    publicSelectedKeys这个对象其实是 selectedKeys变量的一份副本,你可以在 SelectorImpl的构造函数中找到它们俩的关系,我们再回头看一下 selectupdateSelectedKeys方法.

    
    
    1. private int updateSelectedKeys() {
    2. //更新了的keys的个数,或在说是产生的事件的个数
    3. int entries = pollWrapper.updated;
    4. int numKeysUpdated = 0;
    5. for (int i=0; i<entries; i++) {
    6. //对应的channel的fd
    7. int nextFD = pollWrapper.getDescriptor(i);
    8. //通过fd找到对应的SelectionKey
    9. SelectionKeyImpl ski = fdToKey.get(Integer.valueOf(nextFD));
    10. if (ski != null) {
    11. int rOps = pollWrapper.getEventOps(i);
    12. //更新selectedKey变量,并通知响应的channel来做响应的处理
    13. if (selectedKeys.contains(ski)) {
    14. if (ski.channel.translateAndSetReadyOps(rOps, ski)) {
    15. numKeysUpdated++;
    16. }
    17. } else {
    18. ski.channel.translateAndSetReadyOps(rOps, ski);
    19. if ((ski.nioReadyOps() & ski.nioInterestOps()) != 0) {
    20. selectedKeys.add(ski);
    21. numKeysUpdated++;
    22. }
    23. }
    24. }
    25. }
    26. return numKeysUpdated;
    27. }

    后记

     看到这里,详细大家都已经了解到了NIO的底层实现了吧.这里我想在说两个问题.

     一是为什么Netty自己又从新实现了一边native相关的NIO底层方法? 听听Netty的创始人是怎麽说的吧链接

     二是看这么多源码,花费这么多时间有什么作用呢?我感觉如果从非功利的角度来看,那么就是纯粹的希望了解的更多,有时候看完源码或在理解了底层原理之后,都会用一种恍然大悟的感觉,比如说 AQS的原理.如果从目的性的角度来看,那么就是你知道底层原理之后,你的把握性就更强了,如果出了问题,你可以更快的找出来,并且解决.除此之外,你还可以按照具体的现实情况,以源码为模板在自己造轮子,实现一个更加符合你当前需求的版本.

     后续如果有时间,我希望好好了解一下epoll的操作系统级别的实现原理.

    展开全文
  • 补充知识网络爬虫网络爬虫的简易实例robots.txt无线网络 网络爬虫 网络爬虫 (Web Crawler),也叫做网络蜘蛛 (Web Spider) 模拟人类使用浏览器操作页面的行为,对页面进行相关的操作 常用爬虫工具:Python 的 Scrapy...
  • Netty通信原理

    千次阅读 2018-09-10 22:01:29
    基于Java框架NIO,进行通信 是一个异步事件驱动的,网络应用程序框架 用于快速开发可维护的高性能协议服务器和客户端 极大地简化了TCP和UDP套接字服务器等网络编程 Dubbo底层通信 使用的netty框架,进行通信 ...
  • 要实现网络机器间的通讯,首先得来看看计算机系统网络通信的基本原理,在底层层面去看,网络通信需要做的就是将流从一台计算机传输到另外一台计算机 基于传输协议和网络IO来实现,其中传输协议比较出名的有tcp、udp...
  • 相对来说,比较系统地学习了基于TCP协议实现网络通信,也是计算机网络中重中之重,TCP/IP属于网络层,在java中,对该层的工作实现了封装,在编程中,就更加容易地去实现通信,而不用顾及底层的实现。当然,我们需要...
  • Java Socket原理

    2019-02-06 08:29:41
    主机A的应用程序要能和主机B的应用程序通信,必须通过Socket建立连接,而建立Socket连接必须由底层TCP/IP协议来建立TCP连接。建立TCP连接需要底层IP协议来寻找网络中的主机。虽然网络层的IP协议可以帮助我们根据IP...
  • 前言 本文主要从 select 和 epoll 系统调用入手,... Netty 底层通信机制是基于I/O多路复用模型构建的,简单一句话概括就是多路网络连接可以复用一个I/O线程,在 Java 层面也就是封装了其 NIO API,但是 JDK 底...
  • 前言  本文主要从 select 和 epoll 系统... Netty 底层通信机制是基于I/O多路复用模型构建的,简单一句话概括就是多路网络连接可以复用一个I/O线程,在 Java 层面也就是封装了其 NIO API,但是 JDK 底层基于...
  • 理解RPC底层实现原理

    2020-06-12 20:20:24
    涉及的知识点:动态代理,网络IO,序列化(RPC序列化框架(如hessian),JDK序列化的缺点),反射,网络协议(通过哪种协议来传输数据) 从单机到分布式 -> 分布式通信 ->最基本:二进制数据传输 TCP/IP #...
  • c++\c跨平台原理: c\c++ 编译后可以生成对应平台...Java跨平台原理: C\S OSI 7层参考模型 计算机之间的通信基础 计算机之间的连接方式 同种设备连接使用交叉线 同轴电缆 集线器(hub) 网桥 交换机 ...
  • 分布式架构网络通信

    2020-08-08 16:13:24
    要实现网络机器间的通讯,首先得来看看计算机系统网络通信的基本原理,在底层层面去看,网络通信需要做的就是将流从一台计算机传输到另外一台计算机,基于传输协议和网络IO来实现,其中传输协议比较出名的有tcp、udp...
  •   要实现网络机器间的通讯,首先得来看看计算机系统网络通信的基本原理,在底层层面去看,网络通信需要做的就是将流从一台计算机传输到另外一台计算机,基于传输协议和网络IO来实现,其中传输协议比较出名的有tcp...
  • 很多初学者一进入公司就开始使用Linux内核开发内核模块,无论是使用通信方式、内存接口还是设备接口,都是早已被淘汰的内容。因为他们通常直接在网络上搜索一些很早之前发布的内容来指导自己如何完成开发工作,但...
  • 要实现网络机器间的通讯,首先得来看看计算机系统网络通信的基本原理,在底层层面去看,网络通信需要做的就是将流从一台计算机传输到另外一台计算机,基于传输协议和网络IO来实现,其中传输协议比较出名的有tcp、udp...
  • 1.网络通信原理 计算机之间的网络通信通常由最高的应用层向低打包封装到底层(最底层为物理设备),通过物理介质传递到另一台计算机上后再层层拆包,值到最高的应用层。 网络通信中的OSI模型与TCP/IP参考模型 ...
  • 计算机系统网络通信的基本原理,在底层层面去看,网络通信需要做的就是将流从一台计算机传输到另外一台计算机,基于传输协议和网络IO来实现,其中传输协议比较出名的有 http、tcp、udp等等,http、tcp、udp都是在...
  • 从操作系统的底层原理开始讲解:浅显易懂地剖析高并发IO的底层 原理,并介绍如何让单体Java应用支持百万级的高并发;从传统的阻塞式OIO开 始,细致地解析Reactor高性能模式,介绍高性能网络开发的基础知识;从Java的...
  • 基本原理: 要实现网络机器间的通讯,首先得来看看计算机系统网络通信的基本原理,在底层层面去看,网络通信需要做的就是将流从一台计算机传输到另外一台计算机,基于传输协议和网络IO来实现,其中传输
  • java day07 网络编程

    2018-07-19 23:14:38
    1,找到对方的IP 2,数据要发送到对方指定的应用程序上。为了表示这些应用程序,所以...4,网络底层传输原理:数据封包----数据拆包 5,传输协议:  UDP:将数据及源和目的封装成数据包中,不需要建立连接。  ...
  • 了解java的socket编程与Linux Socket API之间的关系一、java网络编程1、socket原理socket通信就是通过IP和端口号将两台主机建立连接,提供通信。主机A的应用程序要能和服务器B进行通信,必须通过Socket建立连接,...
  • 但不可否认,作为即时通讯应用,心跳机制是其网络通信技术底层中非常重要的一环,有没有心跳机制、心跳机制的算法实现好坏,都将直接影响即时通讯应用在应用层的表现——比如:实时性、断网自愈能力、...
  • Java远程调用(一)基本原理

    千次阅读 2013-09-06 23:40:35
    最近项目开始服务化,稍微看了些...基本原理:在底层层面去看,就是将流从一台计算机传输到另外一台计算机,基于传输协议( http、tcp、udp等等)和网络IO( bio、nio、aio )来实现。 关键技术点: 1. 通信协议:
  • 前言Java里面的IO模型种类较多,主要包括BIO,NIO和AIO,每个IO模型都有不一样的地方,那么这些IO模型是如何演变呢,底层原理又是怎样的呢? 本文我们就来聊聊。BIOBIO全称是Blocking IO,是JDK1.4之前的传统IO...
  • 底层层面去看,网络通信需要做的就是将流从一台计算机传输到另外一台计算机,基于传输协议和网络IO来实现 传输协议 UDP: 广播协议,面向无连接,速度快,不安全 TCP: 面向连接的协议,速度不快,较为安全 ...

空空如也

空空如也

1 2 3 4 5 ... 8
收藏数 146
精华内容 58
关键字:

java底层网络通信原理

java 订阅