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对象,指明需要连接的服务器的地址和端口号
②连接建立后,通过输出流向服务器端发送请求信息
③通过输入流获取服务器响应信息
④关闭响应资源
下面看一个简单的小例子:
服务器端响应工具类:
- publicclassServerHandlerimplementsRunnable{
- privateSocketsocket;
- publicServerHandler(Socketsocket){
- this.socket=socket;
- }
- @Override
- publicvoidrun(){
- BufferedReaderbufferedReader=null;
- PrintWriterprintWriter=null;
- try{
- bufferedReader=newBufferedReader(newInputStreamReader(socket.getInputStream()));
- printWriter=newPrintWriter(socket.getOutputStream(),true);
- while(true){
- Stringinfo=bufferedReader.readLine();
- if(info==null)
- break;
- System.out.println("客户端发送的消息:"+info);
- printWriter.println("服务器端响应了客户端请求....");
- }
- }catch(Exceptione){
- e.printStackTrace();
- }finally{
- if(bufferedReader!=null){
- try{
- bufferedReader.close();
- }catch(IOExceptione){
- e.printStackTrace();
- }
- }
- if(printWriter!=null){
- try{
- printWriter.close();
- }catch(Exceptione){
- e.printStackTrace();
- }
- }
- if(socket!=null){
- try{
- socket.close();
- }catch(IOExceptione){
- e.printStackTrace();
- }
- }
- socket=null;
- }
- }
- }
服务器端:
- publicclassServer{
- privatestaticintPORT=8379;
- publicstaticvoidmain(String[]args){
- ServerSocketserverSocket=null;
- try{
- serverSocket=newServerSocket(PORT);
- System.out.println("服务器端启动了....");
- //进行阻塞
- Socketsocket=serverSocket.accept();
- //启动一个线程来处理客户端请求
- newThread(newServerHandler(socket)).start();
- }catch(Exceptione){
- e.printStackTrace();
- }finally{
- if(serverSocket!=null){
- try{
- serverSocket.close();
- }catch(IOExceptione){
- e.printStackTrace();
- }
- }
- serverSocket=null;
- }
- }
- }
客户端:
- publicclassClient{
- privatestaticintPORT=8379;
- privatestaticStringIP="127.0.0.1";
- publicstaticvoidmain(String[]args){
- BufferedReaderbufferedReader=null;
- PrintWriterprintWriter=null;
- Socketsocket=null;
- try{
- socket=newSocket(IP,PORT);
- printWriter=newPrintWriter(socket.getOutputStream(),true);
- bufferedReader=newBufferedReader(newInputStreamReader(socket.getInputStream()));
- printWriter.println("客户端请求了服务器....");
- Stringresponse=bufferedReader.readLine();
- System.out.println("Client:"+response);
- }catch(Exceptione){
- e.printStackTrace();
- }finally{
- if(bufferedReader!=null){
- try{
- bufferedReader.close();
- }catch(IOExceptione){
- e.printStackTrace();
- }
- }
- if(printWriter!=null){
- try{
- printWriter.close();
- }catch(Exceptione){
- e.printStackTrace();
- }
- }
- if(socket!=null){
- try{
- socket.close();
- }catch(IOExceptione){
- e.printStackTrace();
- }
- }else{
- socket=null;
- }
- }
- }
- }
运行结果:
以上的代码有个问题,就是每次有客户端请求服务器端都会创建一个线程,当线程过多时,服务器端可能会宕机。解决这个问题,可以使用JDK提供的线程池(伪异步)。其它地方都不变,将服务器端的代码修改成如下即可:
- publicclassServer{
- privatestaticintPORT=8379;
- publicstaticvoidmain(String[]args){
- ServerSocketserverSocket=null;
- try{
- serverSocket=newServerSocket(PORT);
- System.out.println("服务器端启动了....");
- //进行阻塞
- Socketsocket=null;
- //启动一个线程来处理客户端请求
- //newThread(newServerHandler(socket)).start();
- <spanstyle="color:#ff0000;">HandlerExecutorPoolpool=newHandlerExecutorPool(50,1000);
- while(true){
- socket=serverSocket.accept();
- pool.execute(newServerHandler(socket));
- }</span>
- }catch(Exceptione){
- e.printStackTrace();
- }finally{
- if(serverSocket!=null){
- try{
- serverSocket.close();
- }catch(IOExceptione){
- e.printStackTrace();
- }
- }
- serverSocket=null;
- }
- }
- }
其中HandlerExecutorPool为自定义的线程池,代码如下:
- publicclassHandlerExecutorPool{
- privateExecutorServiceexecutor;
- publicHandlerExecutorPool(intmaxSize,intqueueSize){
- this.executor=newThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),maxSize,120L,TimeUnit.SECONDS,
- <spanstyle="white-space:pre"></span>newArrayBlockingQueue<>(queueSize));
- }
- publicvoidexecute(Runnabletask){
- executor.execute(task);
- }
- }
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:
- publicclassIntBufferTest{
- publicstaticvoidmain(String[]args){
- //1、基本操作
- //创建指定长度的缓冲区
- /*IntBufferbuffer=IntBuffer.allocate(10);
- buffer.put(11);//position位置:0->1
- buffer.put(5);//position位置:1->2
- buffer.put(32);//position位置:2->3
- System.out.println("未调用flip复位方法前的buffer:"+buffer);
- //把位置复位为0,也就是position位置由3->0
- buffer.flip();
- //比较未调用flip方法和调用之后buffer的limit可以发现,不进行复位操作的话,position的值为3,limit的值为10
- //因为缓冲区中已有11、5、32三个元素,也就意味着put()方法会使position向后递增1
- System.out.println("调用flip复位方法后的buffer:"+buffer);
- System.out.println("buffer容量为:"+buffer.capacity());
- System.out.println("buffer限制为:"+buffer.limit());
- System.out.println("获取下标为1的元素:"+buffer.get(1));
- System.out.println("调用get(index)方法后的buffer:"+buffer);//调用get(index)方法,不会改变position的值
- buffer.put(1,4);//将buffer位置为1的值替换为4,调用put(index,value)不会改变position的值
- System.out.println("调用put(index,value)方法后的buffer:"+buffer);
- for(inti=0;i<buffer.limit();i++){
- //调用get方法会使缓冲区的位置(position)向后递增一位
- System.out.print(buffer.get()+"\t");
- }
- System.out.println("\nbuffer对象遍历之后buffer为:"+buffer);*/
- //2、wrap方法的使用
- /*int[]arr=newint[]{1,2,3};
- IntBufferbuffer=IntBuffer.wrap(arr);
- System.out.println("wrap(arr)方法:"+buffer);
- //IntBuffer.wrap(array,postion,length)表示容量为array的长度,但是可操作的元素为位置postion到length的数组元素
- buffer=IntBuffer.wrap(arr,0,2);
- System.out.println("wrap(arr,0,2):"+buffer);*/
- //3、其他方法
- IntBufferbuffer=IntBuffer.allocate(10);
- int[]arr=newint[]{1,2,3};
- buffer.put(arr);
- System.out.println("调用put(arr)方法后的buffer:"+buffer);
- //一种复制方法,buffer1的pos、lim、cap与buffer的一样
- IntBufferbuffer1=buffer.duplicate();
- System.out.println("buffer1:"+buffer1);
- buffer.position(1);//将buffer的position设置为1,不建议使用。功能相当于flip()方法,但是从运行结果可以看出,lim依然等于10
- System.out.println("调用position()方法后的buffer:"+buffer);
- System.out.println("buffer的可读数据量:"+buffer.remaining());//计算出从pos到lim的长度
- int[]arr1=newint[buffer.remaining()];
- //将缓冲区的数据放入arr1中
- buffer.get(arr1);
- for(Integeri:arr1){
- System.out.print(Integer.toString(i)+",");
- }
- System.out.println();
- //比较flip()方法和position(index)方法的区别
- buffer1.flip();
- System.out.println("buffer1的可读数量:"+buffer1.remaining());
- arr1=newint[buffer1.remaining()];
- buffer1.get(arr1);
- for(Integeri:arr1){
- System.out.print(Integer.toString(i)+",");
- }
- }
- }
运行结果:
接下来是Buffer、Channel、Selector的一个入门的小例子:
Server端
- publicclassServerimplementsRunnable{
- privateSelectorselector;
- privateByteBufferbuffer=ByteBuffer.allocate(1024);
- publicServer(intport){
- try{
- //1打开多复用器
- selector=Selector.open();
- //2打开服务器通道
- ServerSocketChannelssc=ServerSocketChannel.open();
- //3设置服务器通道为非阻塞方式
- ssc.configureBlocking(false);
- //4绑定地址
- ssc.bind(newInetSocketAddress(port));
知识推荐
- vb.net判断网络状态
- 解决JSON.stringify()自动将中文转译成unicode的方法
- php中$this->是什么意思
- WEB前端学习有用的书籍
- web前端学习之css
- js获取某周、某月、下月、某季度的开始日期、结束日期及判断日期第几周
- F. PHP中利用PHPMailer配合QQ邮箱实现发邮件(最实用)
- http报文
- Node.js学习笔记(一)
- PHP时间戳和日期相互转换(文字有问题)
- 将视频转换为 HLS(HTTP Live Streaming) 协议格式文件
- 封装QtCore(这非Qt项目里使用QString,QJson,QFileInfo,QFile等类)
- web安全:通俗易懂,以实例讲述破解网站的原理及如何进行防护!如何让网站变得更安全。收藏
- 关于JQuery中get()方法请求php文件时出现的问题
- css中按钮的四种状态
- tomcat8.5基础 web.xml 抛出指定异常时,转到指定的页面
- 使用Multiplayer Networking做一个简单的多人游戏例子-1/2(换一种方法)
- 任务调度Quartz.NET