JAVA网络编程有三种方式,IO也就是BIO,BIO的伪异步方式,和NIO,原理都是通过socket(套接字进行通信)
套接字:就是ip+port ip就是计算机的地址 在java中默认是本地ip 127.0.0.1,port是端口号,每一个应用程序都有自己的端口号。每一台电脑的ip都不一样,每一台电脑上不能同时存在两个端口相同的程序,这样就可以确保网络编程通信的准确性,在其他编程语言中也是一样。
首先我们先通过看几个deme 来实现网络编程,再来比较几种方式的不同。
IO:
server服务端,首先创建一个serverSocket 其实也就是socket,java中在服务端喜欢用serversocket来表示服务端的socket,ip是默认的端口号是9876,io方式中采用阻塞的方式,当客户端有连接进来后才会结束阻塞返回一个socket也就是客户端的socket,并启动一个线程来监听这个socket,接着会继续阻塞等待下一个客户端socket的到来
public class IOServer { //端口 private static final int port = 9876; public static void main(String[] args) { ServerSocket serverSocket = null; try { serverSocket = new ServerSocket(port); //阻塞 Socket socket = serverSocket.accept(); //创建一个新的线程来监听 new Thread(new IOServerHandler(socket) ).start();; } catch (IOException e) { e.printStackTrace(); }finally{ try { serverSocket.close(); } catch (IOException e) { e.printStackTrace(); } } } }
在来看一下IOServerHandler的代码,因为这个类是通过一个线程来监听的 所以一定要实现runnable接口,io是读写分离的,inputStream是读的流,outputstream是写的流,读到的和写出的都是通过byte数组的来读写的,数组的长度决定一次读的大小,当读到的返回值是-1时,表示没有数据可读,切记inputStream的read方法是阻塞的当读完数据的时候如果没有新的数据可读,程序不会往下走会阻塞到这里 直到读到新的数据,使用完之后记得关闭所有的流
public class IOServerHandler implements Runnable{ private Socket socket; public IOServerHandler(Socket socket){ this.socket = socket; } public void run() { InputStream in = null; OutputStream out = null; try { in = socket.getInputStream(); out = socket.getOutputStream(); //读取客户端的 消息 byte[] temp = new byte[1024]; String pri = ""; int length = 0; while(true){ length = in.read(temp); if(length == -1){ break; } pri = new String(temp,0,length); System.out.println(pri); String wri = "服务器收到信息。。"; out.write(wri.getBytes()); } } catch (IOException e) { e.printStackTrace(); }finally{ try { in.close(); } catch (IOException e) { e.printStackTrace(); } try { out.flush(); } catch (IOException e1) { e1.printStackTrace(); } try { out.close(); } catch (IOException e) { e.printStackTrace(); } try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } }
再来看客户端代码:客户端想要和服务端进行通信 一定要知道服务端的ip和端口号22,并创建连接,当new了一个socket之后,这个socket就会进入服务端的accept处,将这个socket返回到服务端,建立连接。流的方式和服务端得使用是一样的 读写分离
public class Client { private static final int port = 9876; private static final String host = "127.0.0.1"; public static void main(String[] args) { Socket socket = null; InputStream in = null; OutputStream out = null; try { socket = new Socket(host,port); in = socket.getInputStream(); out = socket.getOutputStream(); out.write(new String("连接到服务器。。。").getBytes()); byte[] temp = new byte[1024]; int length = 0; String pri = ""; while(true){ length = in.read(temp); if(length == -1){ break; } pri = new String(temp,0,length); System.out.println(pri); } } catch (Exception e) { e.printStackTrace(); } finally{ try { socket.close(); } catch (IOException e) { e.printStackTrace(); } try { in.close(); } catch (IOException e) { e.printStackTrace(); } try { out.flush(); } catch (IOException e) { e.printStackTrace(); } try { out.close(); } catch (IOException e) { e.printStackTrace(); } } } }
看完服务端和客户端的 代码之后,大家想必明白当有一个客户端进行连接的时候会创建一个新的线程,除非旧的连接断开线程才会结束,这样来说如果你的电脑最多只能承受1000个线程,那么你的服务器最多只能承载1000个用户。
伪异步的io网络编程是通过线程池的方式来管理线程,并可设置队列等待的线程数,相对的可以增加服务器的承载力,我们来看一下服务端代码:
和上面一样是创建一个serversocket 也是阻塞之后返回一个socket连接,不同的是这里并没有直接new一个线程,而是通过线程池来管理连接
public class PoolIOServer { private static final int port = 9876; public static void main(String[] args) { ServerSocket serverSocket = null; try { serverSocket = new ServerSocket(port); Socket socket = null; ServerHandlerExecutePool executePool = new ServerHandlerExecutePool(50,100); while(true){ socket = serverSocket.accept(); executePool.execute(new IOServerHandler(socket)); } } catch (IOException e) { e.printStackTrace(); } finally{ try { serverSocket.close(); } catch (IOException e) { e.printStackTrace(); } } } }
看一下线程池的代码:使用工厂模式来创建线程,方便对线程的管理
Runtime.getRuntime().availableProcessors() 是coreThreadSize表示的是核心的线程数,也就是我们cup的最大线程数,比如说电脑配置的4核8线程,maxPoolSize是我们设置得到最大线程数,120L是表示120秒的等待时间,当一个线程120秒没有活跃会自动断开给其他客户端腾出位置,secnds是秒的意思就是120的单位,queuesieze队列的大小,排队的大小
public class ServerHandlerExecutePool { private ExecutorService executor; public ServerHandlerExecutePool(int maxPoolSize,int queueSize){ //Runtime.getRuntime().availableProcessors()本地方法 cpu最大的线程数 //maxPoolSize 最大线程数 //120L保持活跃的时间 //TimeUnit.SECONDS 单位 //queueSize 队列的容量 this.executor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), maxPoolSize, 120L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(queueSize),new ExecuteThreadFactory("executePool")); } public void execute(Runnable task){ executor.execute(task); } }
可以显而易见相比较与第一种方式用线程池的方式管理 可以略微的提高性能
在看一下nio 的方式;首先我们 要了解nio的三个基础概念:
buffer(缓冲区)中定义了大量的put get flip 方法,position mark等参数,大大方便了对读写内容的操作
channel(通道)不存在inputstream和outputStream,直接通过channel就可以实现两端的通信
selector是选择器,每个channel会都会注册的奥selector中得到一个key会不断轮询找到可以进行通信的channel进行调用
通过channel提升了传输效率,通过selector可以大大利用起闲置的线程,能承载1000线程的电脑不在是支持1000个玩家,而是能支持1000个线程去并发。看一下服务器代码:
public class NioServer implements Runnable{ //buffer:缓冲区 本质是一个数组 private ByteBuffer readBuffer = ByteBuffer.allocate(1024); private ByteBuffer writeBuffer = ByteBuffer.allocate(1024); //多路复用器 private Selector selector; public NioServer(int port){ try{ //1.打开多路复用器 this.selector = Selector.open(); //2.打开服务器端的通道 ServerSocketChannel ssc = ServerSocketChannel.open(); //3.设置通道的阻塞模式 ssc.configureBlocking(false); //4.绑定地址 ssc.bind(new InetSocketAddress(port)); //5.把服务器通道注册到多路复用器 监听的状态 ssc.register(this.selector,SelectionKey.OP_ACCEPT); }catch(IOException e){ e.printStackTrace(); } } public static void main(String[] args) { new Thread(new NioServer(9876)).start();; } public void run() { while(true){ try { //1.必须让多路复用器 开始监听 this.selector.select(); //2.返回多路复用器所有注册的key selectedKeys()返回值是set Iterator<SelectionKey> it = this.selector.selectedKeys().iterator(); //3.遍历获取的key while(it.hasNext()){ //4.接收key的值 SelectionKey key = it.next(); //5.从容器中移除已经被选中的key 缓解压力删掉没有意义的key it.remove(); //6.验证操作 判断key是否有效 true是有效 if(key.isValid()){ //7.如果 为阻塞状态 if(key.isAcceptable()){ this.accept(key); } //8.如果是可读状态 if(key.isReadable()){ this.read(key); } //9.如果是可写状态 if(key.isWritable()){ this.write(key); } } } } catch (IOException e) { e.printStackTrace(); } } } private void write(SelectionKey key) { } private void read(SelectionKey key) { try{ //1.对缓冲区进行清空 this.readBuffer.clear(); //2.获取之前注册的socketchannel通道对象 SocketChannel sc = (SocketChannel)key.channel(); //3.从通道里获取数据放入缓冲区 int index = sc.read(this.readBuffer); if(index == -1){ key.channel().close(); key.cancel(); return; } }catch(IOException e){ e.printStackTrace(); } //读取readbuffer数据 然后打印到控制台 //4.由于sc通道里的数据到readbuffer容器中 ,所以readbuffer里面的position一定发生了变化 要进行复位 this.readBuffer.flip(); byte[] bytes = new byte[this.readBuffer.remaining()]; this.readBuffer.get(bytes); String body = new String(bytes).trim(); System.out.println("服务器读取数据:"+body); } /** * 监听阻塞状态方法执行 * @param key */ private void accept(SelectionKey key) { try { //1.由于目前是server端 ,那么一定是server端启动并且处于阻塞状态 所哟煀阻塞状态的key 一定是:serversocketChannel ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel(); //2.通过调用accept方法 返回一个具体的客户端连接句柄 SocketChannel sc = serverSocketChannel.accept(); //3.设置客户端通道为非阻塞 sc.configureBlocking(false); //4.设置当前获取的客户端连接状态为可读状态位 sc.register(this.selector,SelectionKey.OP_READ); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
通过上面的内容我们做出如下总结:
1.io是面向流 nio是面向缓冲
2.io是阻塞 nio是非阻塞
3.io 一个线程管理一个连接 nio一个线程管理多个连接(一个线程管理一个selector,一个selector管理所有通道),使nio可容纳的客户端更多
4.io是读写分离的 nio读写可以使用一个buffer 一个channel使nio传输速度更快
5.nio唯一的劣势,难度相比于io要大很多,但是过netty的架构来简化代码