分享web开发知识

注册/登录|最近发布|今日推荐

主页 IT知识网页技术软件开发前端开发代码编程运营维护技术分享教程案例
当前位置:首页 > 运营维护

[转]网络编程基本概念(IO,NIO,AIO,Netty)

发布时间:2023-09-06 01:33责任编辑:苏小强关键词:暂无标签

1.阻塞IO—Socket

Socket又称“套接字”,应用程序通常通过“套接字”向网络发出请求或应答网络请求。

Socket和ServerSocket类库位于java.net包中。ServerSocket用于服务器端,Socket是建立网络连接时使用的。在连接成功时,应用程序两端都会产生一个Socket实例,操作这个实例,完成所需的会话。对于一个网络连接来说,套接字是平等的,不会因为在服务器端或在客户端而产生不同的级别。不管是ServerSocket还是Socket,它们的工作都是通过SocketImpl类及其子类完成的。

套接字的连接过程可以分为四个步骤:服务器监听、客户端请求服务器、服务器端连接确认、客户端连接确认并进行通信。

(1)服务器监听:服务器端套接字并不定位具体的客户端套接字,而是出于等待连接的状态,实时监控网络状态。

(2)客户端请求:客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述要连接的服务器端的套接字,指出服务器端的套接字的地址和端口号,然后向服务器端套接字提出连接请求。

(3)服务器端连接确认:当服务器端的套接字监听到或者说接收到客户端套接字的连接请求,它就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发送给客户端。

(4)客户端连接确认:一旦客户端确认了此描述,连接就建立好了,双方开始通信。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。

借用一下网上的Socket通信模型图片:

Socket通信步骤:

①创建ServerSocket和Socket

②打开连接到Socket的输入/输出流

③按照协议对Socket进行读写操作

④关闭输入输出流、关闭Socket

服务器端:

①创建ServerSocket对象,绑定监听端口

②通过accept()方法监听客户端请求

③建立连接后,通过输入流读取客户端发送的请求信息

④通过输出流向客户端发送响应信息

⑤关闭相关资源

客户端:

①创建Socket对象,指明需要连接的服务器的地址和端口号

②连接建立后,通过输出流向服务器端发送请求信息

③通过输入流获取服务器响应信息

④关闭响应资源

下面看一个简单的小例子:

服务器端响应工具类:

  1. publicclassServerHandlerimplementsRunnable{
  2. privateSocketsocket;
  3. publicServerHandler(Socketsocket){
  4. this.socket=socket;
  5. }
  6. @Override
  7. publicvoidrun(){
  8. BufferedReaderbufferedReader=null;
  9. PrintWriterprintWriter=null;
  10. try{
  11. bufferedReader=newBufferedReader(newInputStreamReader(socket.getInputStream()));
  12. printWriter=newPrintWriter(socket.getOutputStream(),true);
  13. while(true){
  14. Stringinfo=bufferedReader.readLine();
  15. if(info==null)
  16. break;
  17. System.out.println("客户端发送的消息:"+info);
  18. printWriter.println("服务器端响应了客户端请求....");
  19. }
  20. }catch(Exceptione){
  21. e.printStackTrace();
  22. }finally{
  23. if(bufferedReader!=null){
  24. try{
  25. bufferedReader.close();
  26. }catch(IOExceptione){
  27. e.printStackTrace();
  28. }
  29. }
  30. if(printWriter!=null){
  31. try{
  32. printWriter.close();
  33. }catch(Exceptione){
  34. e.printStackTrace();
  35. }
  36. }
  37. if(socket!=null){
  38. try{
  39. socket.close();
  40. }catch(IOExceptione){
  41. e.printStackTrace();
  42. }
  43. }
  44. socket=null;
  45. }
  46. }
  47. }

服务器端:

  1. publicclassServer{
  2. privatestaticintPORT=8379;
  3. publicstaticvoidmain(String[]args){
  4. ServerSocketserverSocket=null;
  5. try{
  6. serverSocket=newServerSocket(PORT);
  7. System.out.println("服务器端启动了....");
  8. //进行阻塞
  9. Socketsocket=serverSocket.accept();
  10. //启动一个线程来处理客户端请求
  11. newThread(newServerHandler(socket)).start();
  12. }catch(Exceptione){
  13. e.printStackTrace();
  14. }finally{
  15. if(serverSocket!=null){
  16. try{
  17. serverSocket.close();
  18. }catch(IOExceptione){
  19. e.printStackTrace();
  20. }
  21. }
  22. serverSocket=null;
  23. }
  24. }
  25. }

客户端:

  1. publicclassClient{
  2. privatestaticintPORT=8379;
  3. privatestaticStringIP="127.0.0.1";
  4. publicstaticvoidmain(String[]args){
  5. BufferedReaderbufferedReader=null;
  6. PrintWriterprintWriter=null;
  7. Socketsocket=null;
  8. try{
  9. socket=newSocket(IP,PORT);
  10. printWriter=newPrintWriter(socket.getOutputStream(),true);
  11. bufferedReader=newBufferedReader(newInputStreamReader(socket.getInputStream()));
  12. printWriter.println("客户端请求了服务器....");
  13. Stringresponse=bufferedReader.readLine();
  14. System.out.println("Client:"+response);
  15. }catch(Exceptione){
  16. e.printStackTrace();
  17. }finally{
  18. if(bufferedReader!=null){
  19. try{
  20. bufferedReader.close();
  21. }catch(IOExceptione){
  22. e.printStackTrace();
  23. }
  24. }
  25. if(printWriter!=null){
  26. try{
  27. printWriter.close();
  28. }catch(Exceptione){
  29. e.printStackTrace();
  30. }
  31. }
  32. if(socket!=null){
  33. try{
  34. socket.close();
  35. }catch(IOExceptione){
  36. e.printStackTrace();
  37. }
  38. }else{
  39. socket=null;
  40. }
  41. }
  42. }
  43. }

运行结果:


以上的代码有个问题,就是每次有客户端请求服务器端都会创建一个线程,当线程过多时,服务器端可能会宕机。解决这个问题,可以使用JDK提供的线程池(伪异步)。其它地方都不变,将服务器端的代码修改成如下即可:

  1. publicclassServer{
  2. privatestaticintPORT=8379;
  3. publicstaticvoidmain(String[]args){
  4. ServerSocketserverSocket=null;
  5. try{
  6. serverSocket=newServerSocket(PORT);
  7. System.out.println("服务器端启动了....");
  8. //进行阻塞
  9. Socketsocket=null;
  10. //启动一个线程来处理客户端请求
  11. //newThread(newServerHandler(socket)).start();
  12. <spanstyle="color:#ff0000;">HandlerExecutorPoolpool=newHandlerExecutorPool(50,1000);
  13. while(true){
  14. socket=serverSocket.accept();
  15. pool.execute(newServerHandler(socket));
  16. }</span>
  17. }catch(Exceptione){
  18. e.printStackTrace();
  19. }finally{
  20. if(serverSocket!=null){
  21. try{
  22. serverSocket.close();
  23. }catch(IOExceptione){
  24. e.printStackTrace();
  25. }
  26. }
  27. serverSocket=null;
  28. }
  29. }
  30. }

其中HandlerExecutorPool为自定义的线程池,代码如下:

  1. publicclassHandlerExecutorPool{
  2. privateExecutorServiceexecutor;
  3. publicHandlerExecutorPool(intmaxSize,intqueueSize){
  4. this.executor=newThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),maxSize,120L,TimeUnit.SECONDS,
  1. <spanstyle="white-space:pre"></span>newArrayBlockingQueue<>(queueSize));
  2. }
  3. publicvoidexecute(Runnabletask){
  4. executor.execute(task);
  5. }
  6. }

2.IO(BIO)与NIO的区别

其本质就是阻塞和非阻塞的区别。

阻塞概念:应用程序在获取网络数据的时候,如果网络传输数据很慢,那么程序就一直等着,直到传输完毕为止。

非阻塞概念:应用程序直接可以获取已经准备就绪的数据,无需等待。

IO为同步阻塞形式,NIO为同步非阻塞形式。NIO没有实现异步,在JDK1.7之后,升级了NIO库包,支持异步非阻塞通信模型,即NIO2.0(AIO)。

同步和异步:同步和异步一般是面向操作系统与应用程序对IO操作的层面上来区别的。①同步时,应用程序会直接参与IO读写操作,并且应用程序会直接阻塞到某一个方法上,直到数据准备就绪(BIO);或者采用轮询的策略实时检查数据的就绪状态,如果就绪则获取数据(NIO)。②异步时,则所有的IO读写操作都交给操作系统处理,与应用程序没有直接关系,应用程序并不关心IO读写,当操作系统完成IO读写操作时,会向应用程序发出通知,应用程序直接获取数据即可。

同步说的是Server服务端的执行方式,阻塞说的是具体的技术,接收数据的方式、状态(io、nio)。

3.NIO编程介绍

学习NIO编程,首先需要了解几个概念:

(1)Buffer(缓冲区)

Buffer是一个对象,它包含一些需要写入或者读取的数据。在NIO类库中加入Buffer对象,体现了新类库与原IO的一个重要区别。在面向流的IO中,可以直接将数据写入或读取到Stream对象中。在NIO类库中,所有的数据都是用缓冲区处理的(读写)。缓冲区实质上是一个数组,通常它是一个字节数组(ByteBuffer),也可以使用其他类型的数组。这个数组为缓冲区提供了访问数据的读写等操作属性,如位置、容量、上限等概念,具体的可以参考API文档。

Buffer类型:最常使用的是ByteBuffer,实际上每一种java基本类型都对应了一种缓存区(除了Boolean类型)。

①ByteBuffer②CharBuffer③ShortBuffer④IntBuffer⑤LongBuffer⑥FloatBuffer⑦DoubleBuffer

(2)Channel(管道、通道)

Channel就像自来水管道一样,网络数据通过Channel读取和写入,通道与流的不同之处在于通道是双向的,而流只能在一个方向上移动(一个流必须是InputStream或者OutputStream的子类),而通道可以用于读、写或者二者同时进行,最关键的是可以和多路复用器集合起来,有多种的状态位,方便多路复用器去识别。通道分为两大类:一类是用于网络读写的SelectableChannel,另一类是用于文件操作的FileChannel,我们使用的SocketChannel和ServerSocketChannel都是SelectableChannel的子类。

(3)Selector(选择器、多路复用器)

是NIO编程的基础,非常重要。多路复用器提供选择已经就绪的任务的能力。简单说,就是Selector会不断的轮询注册在其上的通道(Channel),如果某个通道发生了读写操作,这个通道就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以取得就绪的Channel集合,从而进行后续的IO操作。一个多路复用器(Selector)可以负责成千上万的通道(Channel),没有上限。这也是JDK使用了epoll代替传统的select实现,获得连接句柄(客户端)没有限制。那也就意味着我们只要一个线程负责Selector的轮询,就可以接入成千上万个客户端,这是JDK NIO库的巨大进步。

Selector线程类似一个管理者(Master),管理了成千上万个管道,然后轮询哪个管道的数据已经准备好了,通知CPU执行IO的读取或写入操作。

Selector模式:当IO事件(管道)注册到选择器以后,Selector会分配给每个管道一个key值,相当于标签。Selector选择器是以轮询的方式进行查找注册的所有IO事件(管道),当IO事件(管道)准备就绪后,Selector就会识别,会通过key值来找到相应的管道,进行相关的数据处理操作(从管道中读取或写入数据,写到缓冲区中)。每个管道都会对选择器进行注册不同的事件状态,以便选择器查找。

事件状态:

SelectionKey.OP_CONNECT

SelectionKey.OP_ACCEPT

SelectionKey.OP_READ

SelectionKey.OP_WRITE

NIO通信模型图解:

(虚线表示不直接相关联)

下面用代码来演示一下Buffer、Channel、Selector的使用。

以IntBuffer为例,讲解一下Buffer的常用API:

  1. publicclassIntBufferTest{
  2. publicstaticvoidmain(String[]args){
  3. //1、基本操作
  4. //创建指定长度的缓冲区
  5. /*IntBufferbuffer=IntBuffer.allocate(10);
  6. buffer.put(11);//position位置:0->1
  7. buffer.put(5);//position位置:1->2
  8. buffer.put(32);//position位置:2->3
  9. System.out.println("未调用flip复位方法前的buffer:"+buffer);
  10. //把位置复位为0,也就是position位置由3->0
  11. buffer.flip();
  12. //比较未调用flip方法和调用之后buffer的limit可以发现,不进行复位操作的话,position的值为3,limit的值为10
  13. //因为缓冲区中已有11、5、32三个元素,也就意味着put()方法会使position向后递增1
  14. System.out.println("调用flip复位方法后的buffer:"+buffer);
  15. System.out.println("buffer容量为:"+buffer.capacity());
  16. System.out.println("buffer限制为:"+buffer.limit());
  17. System.out.println("获取下标为1的元素:"+buffer.get(1));
  18. System.out.println("调用get(index)方法后的buffer:"+buffer);//调用get(index)方法,不会改变position的值
  19. buffer.put(1,4);//将buffer位置为1的值替换为4,调用put(index,value)不会改变position的值
  20. System.out.println("调用put(index,value)方法后的buffer:"+buffer);
  21. for(inti=0;i<buffer.limit();i++){
  22. //调用get方法会使缓冲区的位置(position)向后递增一位
  23. System.out.print(buffer.get()+"\t");
  24. }
  25. System.out.println("\nbuffer对象遍历之后buffer为:"+buffer);*/
  26. //2、wrap方法的使用
  27. /*int[]arr=newint[]{1,2,3};
  28. IntBufferbuffer=IntBuffer.wrap(arr);
  29. System.out.println("wrap(arr)方法:"+buffer);
  30. //IntBuffer.wrap(array,postion,length)表示容量为array的长度,但是可操作的元素为位置postion到length的数组元素
  31. buffer=IntBuffer.wrap(arr,0,2);
  32. System.out.println("wrap(arr,0,2):"+buffer);*/
  33. //3、其他方法
  34. IntBufferbuffer=IntBuffer.allocate(10);
  35. int[]arr=newint[]{1,2,3};
  36. buffer.put(arr);
  37. System.out.println("调用put(arr)方法后的buffer:"+buffer);
  38. //一种复制方法,buffer1的pos、lim、cap与buffer的一样
  39. IntBufferbuffer1=buffer.duplicate();
  40. System.out.println("buffer1:"+buffer1);
  41. buffer.position(1);//将buffer的position设置为1,不建议使用。功能相当于flip()方法,但是从运行结果可以看出,lim依然等于10
  42. System.out.println("调用position()方法后的buffer:"+buffer);
  43. System.out.println("buffer的可读数据量:"+buffer.remaining());//计算出从pos到lim的长度
  44. int[]arr1=newint[buffer.remaining()];
  45. //将缓冲区的数据放入arr1中
  46. buffer.get(arr1);
  47. for(Integeri:arr1){
  48. System.out.print(Integer.toString(i)+",");
  49. }
  50. System.out.println();
  51. //比较flip()方法和position(index)方法的区别
  52. buffer1.flip();
  53. System.out.println("buffer1的可读数量:"+buffer1.remaining());
  54. arr1=newint[buffer1.remaining()];
  55. buffer1.get(arr1);
  56. for(Integeri:arr1){
  57. System.out.print(Integer.toString(i)+",");
  58. }
  59. }
  60. }

运行结果:

接下来是Buffer、Channel、Selector的一个入门的小例子:

Server端

  1. publicclassServerimplementsRunnable{
  2. privateSelectorselector;
  3. privateByteBufferbuffer=ByteBuffer.allocate(1024);
  4. publicServer(intport){
  5. try{
  6. //1打开多复用器
  7. selector=Selector.open();
  8. //2打开服务器通道
  9. ServerSocketChannelssc=ServerSocketChannel.open();
  10. //3设置服务器通道为非阻塞方式
  11. ssc.configureBlocking(false);
  12. //4绑定地址
  13. ssc.bind(newInetSocketAddress(port));
我的编程学习网——分享web前端后端开发技术知识。 垃圾信息处理邮箱 tousu563@163.com 网站地图
icp备案号 闽ICP备2023006418号-8 不良信息举报平台 互联网安全管理备案 Copyright 2023 www.wodecom.cn All Rights Reserved