说说Java网络编程

2021-04-01 03:27

阅读:611

标签:图片   使用数组   针对   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 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 }
View Code

服务端的ChatHandler:

 

说说Java网络编程

标签:图片   使用数组   针对   rgs   告诉   iocp   装饰者模式   contains   编程   

原文地址:https://www.cnblogs.com/RQfreefly/p/13544850.html


评论


亲,登录后才可以留言!