标签:图片 使用数组 针对 rgs 告诉 iocp 装饰者模式 contains 编程
网络编程的目的在于远程发送数据,发送接收数据就涉及到I/O的操作,这里因为涉及到比较底层字节和字符的操作,所以不可以使用java.nio.file.Files 操作文件。那就先说说I/O吧,I/O流分为字节流和字符流。字节即Byte,包含8位二进制数,一个二进制数就是1bit,中文名称叫位。字符即一个字母或者一个汉字。一个字母由一个字节组成,而汉字根据编码不同由2个或者3个组成。
Java I/O类的实现原理是装饰者模式,装饰者模式动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。
1.将被装饰者(Concrete Component)当做类中一个成员变量。
2.利用构造将被装饰者注入
然后引入I/O模型:
阻塞和非阻塞,描述的是结果的请求。
阻塞:在得到结果之前就一直呆在那,啥也不干,此时线程挂起,就如其名,线程被阻塞了。
非阻塞:如果没得到结果就返回,等一会再去请求,直到得到结果为止。
异步和同步,描述的是结果的发出,当调用方的请求进来。
同步:在没获取到结果前就不返回给调用方,如果调用方是阻塞的,那么调用方就会一直等着。如果调用方是非阻塞的,调用方就会先回去,等一会再来问问得到结果没。
异步:调用方一来,会直接返回,等执行完实际的逻辑后在通过回调函数把结果返回给调用方。
异步非阻塞
用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。
事实上就是,用户提交IO请求,然后直接返回,并且内核自动完成将数据从内核缓冲区复制到用户缓冲区,完成后再通知用户。
当然,内核通知我们以后我们还需要执行剩余的操作,但是我们的代码已经继续往下运行了,所以AIO采用了回调的机制,为每个socket注册一个回调事件或者是回调处理器,在处理器中完成数据的操作,也就是内核通知到用户的时候,会自动触发回调函数,完成剩余操作。 这样的方式就是异步的网络编程。
但是,想要让操作系统支持这样的功能并非易事,windows的IOCP可以支持AIO方式,但是Linux的AIO支持并不是很好。(所以Netty后来也取消了对AIO的支持)
IO多路复用 :使用IO多路复用器管理socket,由于每个socket是一个文件描述符,操作系统可以维护socket和它的连接状态,一般分为可连接,可读和可写等状态。
每当用户程序接受到socket请求,将请求托管给多路复用器进行监控,当程序对请求感兴趣的事件发生时,多路复用器以某种方式通知或是用户程序自己轮询请求,以便获取就绪的socket,然后只需使用一个线程进行轮询,多个线程处理就绪请求即可。
IO多路复用避免了每个socket请求都需要一个线程去处理,而是使用事件驱动的方式,让少数的线程去处理多数socket的IO请求。
Linux操作系统对IO多路复用提供了较好的支持,select,poll,epoll是Linux提供的支持IO多路复用的API。一般用户程序基于这个API去开发自己的IO复用模型。比如NIO的非阻塞模型,就是采用了IO多路复用的方式,是基于epoll实现的。
select方式主要是使用数组来存储socket描述符,系统将发生事件的描述符做标记,然后IO复用器在轮询描述符数组的时候,就可以知道哪些请求是就绪了的。缺点是数组的长度只能到1024,并且需要不断地在内核空间和用户空间之间拷贝数组。
poll方式不采用数组存储描述符,而是使用独立的数据结构来描述,并且使用id来表示描述符,能支持更多的请求数量,缺点和select方式有点类似,就是轮询的效率很低,并且需要拷贝数据。
当然,上述两种方法适合在请求总数较少,并且活跃请求数较多的情况,这种场景下他们的性能还是不错的。
epoll,epoll函数会在内核空间开辟一个特殊的数据结构,红黑树,树节点中存放的是一个socket描述符以及用户程序感兴趣的事件类型。同时epoll还会维护一个链表。用于存储已经就绪的socket描述符节点。由Linux内核完成对红黑树的维护,当事件到达时,内核将就绪的socket节点加入链表中,用户程序可以直接访问这个链表以便获取就绪的socket。
有了直接与文件交互的I/O类,那怎么样与网络交互呢?这里就引入Socket:
socket是操作系统提供的网络编程接口,他封装了对于TCP/IP协议栈的支持,用于进程间的通信,当有连接接入主机以后,操作系统自动为其分配一个socket套接字,套接字绑定着一个IP与端口号。通过socket接口,可以获取tcp连接的输入流和输出流,并且通过他们进行读取和写入此操作。
Java提供了net包用于socket编程,同时支持像Inetaddress,URL等工具类,使用socket绑定一个endpoint(ip+端口号),可以用于客户端的请求处理和发送,使用serversocket绑定本地ip和端口号,可以用于服务端接收TCP请求。
BIO编程模型
所谓BIO,就是Block IO,阻塞式的IO。这个阻塞主要发生在:ServerSocket接收请求时(accept()方法)、InputStream、OutputStream(输入输出流的读和写)都是阻塞的。这个可以在下面代码的调试中发现,比如在客户端接收服务器消息的输入流处打上断点,除非服务器发来消息,不然断点是一直停在这个地方的。也就是说这个线程在这时间是被阻塞的。
这里放一个BIO模型写的聊天室,对于Socket编程基础请移步:https://www.cnblogs.com/yiwangzhibujian/p/7107785.html
服务端:
1 /* 2 1. 功能实现:这个类的作用就像Acceptor。它有两个比较关键的全局变量,一个就是存储在线用户信息的Map,一个就是线程池。 3 这个类会监听端口,接收客户端的请求,然后为客户端分配工作线程。 4 还会提供一些常用的工具方法给每个工作线程调用,比如:发送消息、添加在线用户等。 5 */ 6 7 import java.io.*; 8 import java.net.*; 9 import java.util.Map; 10 import java.util.concurrent.*; 11 12 public class ChatServer { 13 private int DEFAULT_PORT = 8888; 14 /** 15 * 创建一个Map存储在线用户的信息。这个map可以统计在线用户、针对这些用户可以转发其他用户发送的消息 16 * 因为会有多个线程操作这个map,所以为了安全起见用ConcurrentHashMap 17 * 在这里key就是客户端的端口号,但在实际中肯定不会用端口号区分用户,如果是web的话一般用session。 18 * value是IO的Writer,用以存储客户端发送的消息 19 */ 20 private Map<Integer, Writer> map = new ConcurrentHashMap<>(); 21 /** 22 * 创建线程池,线程上限为10个,如果第11个客户端请求进来,服务器会接收但是不会去分配线程处理它。 23 * 前10个客户端的聊天记录,它看不见。当有一个客户端下线时,这第11个客户端就会被分配线程,服务器显示在线 24 * 大家可以把10再设置小一点,测试看看 25 * */ 26 private ExecutorService executorService = Executors.newFixedThreadPool(10); 27 //客户端连接时往map添加客户端 28 public void addClient(Socket socket) throws IOException { 29 if (socket != null) { 30 BufferedWriter writer = new BufferedWriter( 31 new OutputStreamWriter(socket.getOutputStream()) 32 ); 33 map.put(socket.getPort(), writer); 34 System.out.println("Client["+socket.getPort()+"]:Online"); 35 } 36 } 37 38 //断开连接时map里移除客户端 39 public void removeClient(Socket socket) throws Exception { 40 if (socket != null) { 41 if (map.containsKey(socket.getPort())) { 42 map.get(socket.getPort()).close(); 43 map.remove(socket.getPort()); 44 } 45 System.out.println("Client[" + socket.getPort() + "]Offline"); 46 } 47 } 48 49 //转发客户端消息,这个方法就是把消息发送给在线的其他的所有客户端 50 public void sendMessage(Socket socket, String msg) throws IOException { 51 //遍历在线客户端 52 for (Integer port : map.keySet()) { 53 //发送给在线的其他客户端 54 if (port != socket.getPort()) { 55 Writer writer = map.get(port); 56 writer.write(msg); 57 writer.flush(); 58 } 59 } 60 } 61 62 //接收客户端请求,并分配Handler去处理请求 63 public void start() { 64 try (ServerSocket serverSocket = new ServerSocket(DEFAULT_PORT)) { 65 System.out.println("Server Start,The Port is:"+DEFAULT_PORT); 66 while (true){ 67 //等待客户端连接 68 Socket socket=serverSocket.accept(); 69 //为客户端分配一个ChatHandler线程 70 executorService.execute(new ChatHandler(this, socket)); 71 } 72 } catch (IOException e) { 73 e.printStackTrace(); 74 } 75 } 76 77 public static void main(String[] args) { 78 ChatServer server=new ChatServer(); 79 server.start(); 80 } 81 }
服务端的ChatHandler:
标签:图片 使用数组 针对 rgs 告诉 iocp 装饰者模式 contains 编程
原文地址:https://www.cnblogs.com/RQfreefly/p/13544850.html