原生JDK网络编程BIO编程
ServerSocker 负责绑定就IP地址,启动端口
那么Socker负责连接操作
一般在编程里面会起一个线程负责监听客户端的连接,当客户端发起一个连接以后,他会为每一个客户端新起一个线程,由线程对客户端进行应答,应答后就销毁线程;
这就就相当于每有一个客户端连接就需要新起一个线程;
缺点: 开销线程数量比较大;
在实际使用BIO编程中一般会使用线程池,用线程池来管理线程,每当有客户端请求之后,打包成一个任务,方放到想成里面去,这样就可以使用少量的线程,来处理大量的任务;这就是伪异步IO模型
缺点:当处理大任务需要花很多时间的时候,就有很多任务在队列里面排队;这也是他的一个弊端;
伪异步IO解决的问题就是节约线程数量的;
BIO标准模式代码
客户端代码
public class BioClient { public static void main(String[] args) throws InterruptedException, IOException { //通过构造函数创建Socket,并且连接指定地址和端口的服务端 Socket socket = new Socket(DEFAULT_SERVER_IP,DEFAULT_PORT); System.out.println("请输入请求消息:"); //启动读取服务端输出数据的线程 new ReadMsg(socket).start(); PrintWriter pw = null; //允许客户端在控制台输入数据,然后送往服务器 while(true){ pw = new PrintWriter(socket.getOutputStream()); pw.println(new Scanner(System.in).next()); pw.flush(); } } //读取服务端输出数据的线程 private static class ReadMsg extends Thread { Socket socket; public ReadMsg(Socket socket) { this.socket = socket; } @Override public void run() { //负责socket读写的输入流 try (BufferedReader br = new BufferedReader( new InputStreamReader(socket.getInputStream()))){ String line = null; //通过输入流读取服务端传输的数据 //如果已经读到输入流尾部,返回null,退出循环 //如果得到非空值,就将结果进行业务处理 while((line=br.readLine())!=null){ System.out.printf("%s\n",line); } } catch (SocketException e) { System.out.printf("%s\n", "服务器断开了你的连接"); } catch (Exception e) { e.printStackTrace(); } finally { clear(); } } //必要的资源清理工作 private void clear() { if (socket != null) try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } }
服务器端代码
public class BioServer { //服务器端必须 private static ServerSocket server; //线程池,处理每个客户端的请求 private static ExecutorService executorService = Executors.newFixedThreadPool(5); private static void start() throws IOException{ try{ //通过构造函数创建ServerSocket //如果端口合法且空闲,服务端就监听成功 server = new ServerSocket(DEFAULT_PORT); System.out.println("服务器已启动,端口号:" + DEFAULT_PORT); while(true){ Socket socket= server.accept(); System.out.println("有新的客户端连接----" ); //当有新的客户端接入时,打包成一个任务,投入线程池 executorService.execute(new BioServerHandler(socket)); } }finally{ if(server!=null){ server.close(); } } } public static void main(String[] args) throws IOException { start(); } }
BIO对于服务器来说,对于高并发的时候性能不是很让人满意的;
所以后面关于服务器的网络通讯人们就提出了NIO和AIO的模式;
一般BIO 在客户端编程里面会比较多,因为客户端不需要接收外来请求;
一般java开发是服务器程序;
原生JDK 网络编程AIO,
异步IO采用“订阅-通知”模式:即应用程序向操作系统注册IO监听,然后继续做自己的事情。当操作系统发生IO事件,并且准备好数据后,在主动通知应用程序,触发相应的函数;
在AIO中就两个核心类:
Asynchronous、
服务器端:最主要的工作就是接收客户端的连接,然后接收客户端发送的数据,然后把数据写给客户端;
在AIO模式里面当我调用之后我就不管了,然后由服务器处理好了以后就通知你;你开始找我要网络中的数据;中间的阻塞 不需要等待了
AIO 模式, 应用程序向操作系统发起一个注册,然后操作系统准备好了,通知你,你要的数据已经准备好了,什么时候操作系统准备好了 应用程序是不知道的;
在JDK 里提供了一个接口 CompletionHandler
completed(V result ,A attachment) 方法 处理数据
result 返回的结果
attachment 代表IO操作的时候附加的一个参数;
failed() 方法,当数据操作失败的时候你要怎么做? 你要告诉JDK 调用failed 这个方法;
AIO编程里面不管是 读,写,连接 都是异步操作;
BIO如果是服务器不要用BIO,如果是客户端可以使用BIO;
BIO是所有三种模型中编程最简单的应用;
服务器的话就是用AIO或者NIO, 但是linux 操作系统不支持AIO,
在linux 上编程,使用NIO;
经过测试得出。客户端和服务器端使用两种不同模式是可以连接上的
客户端方便我们开发,我们使用阻塞式IO,服务器端应对我们的高并发我们使用IO复用模型;
原生JDK网络编程- Buffer
重要属性 capacity : 表示我可以写的最大容量 position : 表示我已经写到了这个位置 limit :表示这个数组能写的范围,动态调节阀无论你怎么写 你都只能写到limit的位置
Buffer-Read
表示就是只能读到我写的范围,只能是position到limit的位置;
buffer.flip() 方法就是把写模式转化为读模式
Buffer 就可以理解为放在内存里面的一个数组
我们用的最多的是ByteBuffer,因为在网络上传输都是字节传输,
Buffer 是直接在堆上分配,和直接内存上分配;
原生JDK网络编程- NIO之Reactor模式
NIO 也被称为反应器,和spring的依赖倒转 有点像;
我们调用某个东西,一般是我们主动去发起调用,
但是在反应堆模式里面,我们一个具体的事件处理程序,他不是调用反应器,而是在反应器注册一个事件处理器,我们表示对某些事件感兴趣,有事件来了,应用程序通过事件处理器对某一个事件发生反应;这种控制逆转也被称为好莱坞法则;相当于你把你的名片给我,我有戏了我找你,你不要来找我;这也被称为反应器模式;
可以分为:
单线程反应器,
多线程反应器;
有一个主反应线器程和一个子的反应器线程
其他的工作线程和单线程一样;
在Netty 是用多线程反应器线程模式;
原生JDK网络编程- NIO
Buffer 最终是使用来和NIO使用的
重要概念
Selector:
Selector的英文含义是“选择器”,也可以称为为“轮询代理器”、“事件订阅器”、“channel容器管理机”都行。
事件订阅和Channel管理:
应用程序将向Selector对象注册需要它关注的Channel,以及具体的某一个Channel会对哪些IO事件感兴趣。Selector中也会维护一个“已经注册的Channel”的容器。
Channel : 他是一个抽象,是一个通道,是我们应用程序和操作系统交换事件传递内容的聚到;
意味着我们的应用程序可以向操作系统读数据,也可以向操作系统写数据; 当然写和读都是通过Buffer;
服务器端有一个:ServerScoketChannel
客户端有一个:SokcetChannel
还有一个数据包也就是UDP通道: DatagramChannel 通道
通道,被建立的一个应用程序和操作系统交互事件、传递内容的渠道(注意是连接到操作系统)。那么既然是和操作系统进行内容的传递,那么说明应用程序可以通过通道读取数据,也可以通过通道向操作系统写数据。
· 所有被Selector(选择器)注册的通道,只能是继承了SelectableChannel类的子类。
· ServerSocketChannel:应用服务器程序的监听通道。只有通过这个通道,应用程序才能向操作系统注册支持“多路复用IO”的端口监听。同时支持UDP协议和TCP协议。
· ScoketChannel:TCP Socket套接字的监听通道,一个Socket套接字对应了一个客户端IP:端口 到 服务器IP:端口的通信连接。
· DatagramChannel:UDP 数据报文的监听通道。
通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。
操作类型SelecttionKey :
JAVA NIO共定义了四种操作类型:OP_READ、OP_WRITE、OP_CONNECT、OP_ACCEPT(定义在SelectionKey中),分别对应读、写、请求连接、接受连接等网络Socket操作。ServerSocketChannel和SocketChannel可以注册自己感兴趣的操作类型,当对应操作类型的就绪条件满足时OS会通知channel,下表描述各种Channel允许注册的操作类型,Y表示允许注册,N表示不允许注册,其中服务器SocketChannel指由服务器ServerSocketChannel.accept()返回的对象。
OP_READ | OP_WRITE | OP_CONNECT | OP_ACCEPT | |
---|---|---|---|---|
服务器ServerSocketChannel | N | N | N | *Y* |
服务器SocketChannel | *Y* | *Y* | N | N |
客户端SocketChannel | *Y* | *Y* | *Y* | N |
服务器启动ServerSocketChannel,关注OP_ACCEPT事件。
客户端启动SocketChannel,连接服务器,关注OP_CONNECT事件。
服务器接受连接,启动一个服务器的SocketChannel,这个SocketChannel可以关注OP_READ、OP_WRITE事件,一般连接建立后会直接关注OP_READ事件。
客户端这边的客户端SocketChannel发现连接建立后,可以关注OP_READ、OP_WRITE事件,一般是需要客户端需要发送数据了才关注OP_READ事件。
连接建立后客户端与服务器端开始相互发送消息(读写),根据实际情况来关注OP_READ、OP_WRITE事件。
我们可以看看每个操作类型的就绪条件。
*操作类型* | *就绪条件及说明* |
---|---|
OP_READ | 当操作系统读缓冲区有数据可读时就绪。并非时刻都有数据可读,所以一般需要注册该操作,仅当有就绪时才发起读操作,有的放矢,避免浪费CPU。 |
OP_WRITE | 当操作系统写缓冲区有空闲空间时就绪。一般情况下写缓冲区都有空闲空间,小块数据直接写入即可,没必要注册该操作类型,否则该条件不断就绪浪费CPU;但如果是写密集型的任务,比如文件下载等,缓冲区很可能满,注册该操作类型就很有必要,同时注意写完后取消注册。 |
OP_CONNECT | 当SocketChannel.connect()请求连接成功后就绪。该操作只给客户端使用。 |
OP_ACCEPT | 当接收到一个客户端连接请求时就绪。该操作只给服务器使用。 |
NIO代码实现
客户端代码
public class NioClient { private static NioClientHandle nioClientHandle; public static void start(){ if(nioClientHandle !=null) nioClientHandle.stop(); nioClientHandle = new NioClientHandle(DEFAULT_SERVER_IP,DEFAULT_PORT); new Thread(nioClientHandle,"Client").start(); } //向服务器发送消息 public static boolean sendMsg(String msg) throws Exception{ nioClientHandle.sendMsg(msg); return true; } public static void main(String[] args) throws Exception { start(); Scanner scanner = new Scanner(System.in); while(NioClient.sendMsg(scanner.next())); } }
客户端处理器
public class NioClientHandle implements Runnable{ private String host; private int port; private Selector selector; private SocketChannel socketChannel; private volatile boolean started; public NioClientHandle(String ip, int port) { this.host = ip; this.port = port; try { //创建选择器 selector = Selector.open(); //打开通道 socketChannel = SocketChannel.open(); //如果为 true,则此通道将被置于阻塞模式; // 如果为 false,则此通道将被置于非阻塞模式 检测是否为阻塞模式 socketChannel.configureBlocking(false); started = true; } catch (IOException e) { e.printStackTrace(); } } public void stop(){ started = false; } @Override public void run() { /** * 在run方法里面 对事件使用选择器, * 拿到事件之后一一的做处理, 放到handleInput() 里面就我关注这个事件一一做相关的处理 *还有发送消息是在主程序里面发 * */ try { doConnect(); } catch (IOException e) { e.printStackTrace(); System.exit(1); } //循环遍历selector while(started){ try { //阻塞,只有当至少一个注册的事件发生的时候才会继续 selector.select(); //获取当前有哪些事件可以使用 Set<SelectionKey> keys = selector.selectedKeys(); //转换为迭代器 Iterator<SelectionKey> it = keys.iterator(); SelectionKey key = null; while(it.hasNext()){ key = it.next(); it.remove(); try { handleInput(key); } catch (IOException e) { e.printStackTrace(); if(key!=null){ key.cancel(); if(key.channel()!=null){ key.channel().close(); } } } } } catch (IOException e) { e.printStackTrace(); } } //selector关闭后会自动释放里面管理的资源 if(selector!=null){ try { selector.close(); } catch (IOException e) { e.printStackTrace(); } } } //具体的事件处理方法 private void handleInput(SelectionKey key) throws IOException{ if(key.isValid()){ //获得关心当前事件的channel SocketChannel sc = (SocketChannel)key.channel(); if(key.isConnectable()){//连接事件 如果是他关心的事件 if(sc.finishConnect()){} else{System.exit(1);} } //有数据可读事件 连接完成了就需要读消息 if(key.isReadable()){// 判断写事件; //创建ByteBuffer,并开辟一个1M的缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); //读取请求码流,返回读取到的字节数 int readBytes = sc.read(buffer); //读取到字节,对字节进行编解码 if(readBytes>0){ //实际读到的数据 //将缓冲区当前的limit设置为position,position=0, // 用于后续对缓冲区的读取操作 buffer.flip(); //根据缓冲区可读字节数创建字节数组 byte[] bytes = new byte[buffer.remaining()]; //将缓冲区可读字节数组复制到新建的数组中 buffer.get(bytes); String result = new String(bytes,"UTF-8"); System.out.println("accept message:"+result); }else if(readBytes<0){// 小于0 表示链路已经关闭了; key.cancel(); sc.close(); } } } } //发送消息 private void doWrite(SocketChannel channel,String request) throws IOException { //将消息编码为字节数组 byte[] bytes = request.getBytes(); //根据数组容量创建ByteBuffer ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length); //将字节数组复制到缓冲区 writeBuffer.put(bytes); //flip操作 writeBuffer.flip(); //发送缓冲区的字节数组 channel.write(writeBuffer); } private void doConnect() throws IOException { /*如果此通道处于非阻塞模式, 则调用此方法将启动非阻塞连接操作。 如果立即建立连接,就像本地连接可能发生的那样,则此方法返回true。 否则,此方法返回false, 稍后必须通过调用finishConnect方法完成连接操作。*/ if(socketChannel.connect(new InetSocketAddress(host,port))){} else{ //连接还未完成,所以注册连接就绪事件,向selector表示关注这个事件 socketChannel.register(selector,SelectionKey.OP_CONNECT); } } //写数据对外暴露的API public void sendMsg(String msg) throws Exception{ socketChannel.register(selector,SelectionKey.OP_READ); doWrite(socketChannel,msg); } }
服务器端代码
/** * 类说明:nio通信服务端 */ public class NioServer { private static NioServerHandle nioServerHandle; public static void start(){ if(nioServerHandle !=null) nioServerHandle.stop(); nioServerHandle = new NioServerHandle(DEFAULT_PORT); new Thread(nioServerHandle,"Server").start(); } public static void main(String[] args){ start(); } }
服务器端处理器
/** * 类说明:nio通信服务端处理器 */ public class NioServerHandle implements Runnable{ private Selector selector; private ServerSocketChannel serverChannel; private volatile boolean started; /** * 构造方法 * @param port 指定要监听的端口号 */ public NioServerHandle(int port) { try { selector = Selector.open(); serverChannel = ServerSocketChannel.open(); serverChannel.configureBlocking(false); // 指明服务器在哪个端口监听 serverChannel.socket().bind(new InetSocketAddress(port)); serverChannel.register(selector,SelectionKey.OP_ACCEPT); started = true; System.out.println("服务器已启动,端口号:"+port); } catch (IOException e) { e.printStackTrace(); } } public void stop(){ started = false; } @Override public void run() { //循环遍历selector while(started){ try{ //阻塞,只有当至少一个注册的事件发生的时候才会继续. selector.select(); Set<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> it = keys.iterator(); SelectionKey key = null; while(it.hasNext()){ key = it.next(); it.remove(); try{ handleInput(key); }catch(Exception e){ if(key != null){ key.cancel(); if(key.channel() != null){ key.channel().close(); } } } } }catch(Throwable t){ t.printStackTrace(); } } //selector关闭后会自动释放里面管理的资源 if(selector != null) try{ selector.close(); }catch (Exception e) { e.printStackTrace(); } } private void handleInput(SelectionKey key) throws IOException{ /** * 这里关注的不在是客户端的连接事件, * 而是关注客户端的接收连接的事件 * 需要新起一个socker */ if(key.isValid()){ //处理新接入的请求消息 if(key.isAcceptable()){ //获得关心当前事件的channel ServerSocketChannel ssc = (ServerSocketChannel)key.channel(); //通过ServerSocketChannel的accept创建SocketChannel实例 //完成该操作意味着完成TCP三次握手,TCP物理链路正式建立 。接收连接事件 SocketChannel sc = ssc.accept(); System.out.println("======socket channel 建立连接" ); //设置为非阻塞的 sc.configureBlocking(false); //连接已经完成了,可以开始关心读事件了 sc.register(selector,SelectionKey.OP_READ); } //读消息 if(key.isReadable()){ System.out.println("======socket channel 数据准备完成," + "可以去读==读取======="); SocketChannel sc = (SocketChannel) key.channel(); //创建ByteBuffer,并开辟一个1M的缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); //读取请求码流,返回读取到的字节数 int readBytes = sc.read(buffer); //读取到字节,对字节进行编解码 if(readBytes>0){ //将缓冲区当前的limit设置为position=0, // 用于后续对缓冲区的读取操作 buffer.flip(); //根据缓冲区可读字节数创建字节数组 byte[] bytes = new byte[buffer.remaining()]; //将缓冲区可读字节数组复制到新建的数组中 buffer.get(bytes); String message = new String(bytes,"UTF-8"); System.out.println("服务器收到消息:" + message); //处理数据 String result = response(message) ; //发送应答消息 doWrite(sc,result); } //链路已经关闭,释放资源 else if(readBytes<0){ key.cancel(); sc.close(); } } } } //发送应答消息 private void doWrite(SocketChannel channel,String response) throws IOException { //将消息编码为字节数组 byte[] bytes = response.getBytes(); //根据数组容量创建ByteBuffer ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length); //将字节数组复制到缓冲区 writeBuffer.put(bytes); //flip操作 writeBuffer.flip(); //发送缓冲区的字节数组 channel.write(writeBuffer); } }