网络编程之BIO/NIO基础

朴灿烈づ我的快乐病毒、 2022-05-18 22:09 340阅读 0赞

什么是网络编程

网络编程是指编写运行在多个设备上(计算机)的程序, 通过网络进行数据交换. 比如现在流行的微服务, 把一个大的系统按照功能拆分多个微服务, 每个微服务都是一个独立的应用, 部署在不同的服务器上, 不同服务器上的微服务如何进行通信就是属于网络编程的范畴.

TCP、IP、HTTP、Socket的区别

网络模型(OSI)从下往上分为七层, 分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层. IP协议是属于网络层的协议, TCP是属于传输层的协议, HTTP是属于应用层的协议, Socket则是对TCP/IP协议的封装和应用.
这里写图片描述

网络编程三要素

网络编程三要素分别是IP、端口号、TCP/UDP协议. IP是每个设备在网络中的唯一标识, 端口号是每个程序在设备上的唯一标识, TCP/UDP是数据传输的协议.

(1)TCP协议和UDP协议的区别

  • TCP协议是面向连接的(三次握手), 数据安全, 速度慢.
  • UDP协议是面向无连接的, 数据不安全, 速度快.

(2)TCP协议的三次握手

在这里插入图片描述

  • 第一次握手:客户端向服务端发送一个连接报文(标识位SYN=1, 序列号为一个随机值).
  • 第二次握手:服务端收到客户端的连接报文后, 返回一个确认报文(标识位SYN=1和ACK=1, 序列号为一个随机值, 确认号为客户端的序列号 + 1).
  • 第三次握手:客户端收到服务端的确认报文后, 再向服务端发送一个确认报文(标识位ACK=1, 序列号值等于上一次收到的确认号, 确认号为上一次收到的序列号 + 1).

握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。断开连接时服务器和客户端均可以主动发起断开TCP连接的请求,断开过程需要经过“四次挥手”.

(3)TCP协议的四次挥手

在这里插入图片描述

  • 第一次挥手:客户端完成它的数据发送任务后, 向服务端发送一个终止报文, 表示自己不再发送数据. (标识位FIN=1, 序列号值等于上一次收到的确认号)
  • 第二次挥手:服务端收到客户端的终止报文后, 会向客户端返回一个确认报文.(标识位ACK=1, 序列号值等于上一次收到的确认号, 确认号为客户端的序列号 + 1)
  • 第三次挥手:服务端完成它的数据发送任务后, 会向客户端发送一个终止报文, 表示自己不再发送数据.(标识位FIN=1, 序列号值等于上一次收到的确认号, 确认号为第二次挥手的确认号 + 1)
  • 第四次挥手:客户端收到服务端的终止报文后, 会向服务端发送一个确认报文. (标识位ACK=1, 序列号值等于上一次收到的确认号, 确认号为上一次收到的序列号+1)

由于TCP连接是双向的, 因此每个方向都必须单独进行关闭. 当一方完成它的数据发送任务后就会发送一个FIN来终止这个方向的连接, 收到一个FIN只意味着这一方向上没有数据流动, 一个TCP连接在收到一个FIN后仍能发送数据. 首先进行关闭的一方将执行主动关闭, 而另一方执行被动关闭.

(4)为什么建立连接是三次握手, 而关闭连接却是四次握手呢?

这是因为服务端LISTEN状态下的SOCKET当收到SYN报文的连接请求后, 它可以把ACK和SYN放在一个报文里发送. 但关闭连接时, 当收到客户端的FIN报文通知时, 它仅仅表示客户端没有数据发送给服务端, 但未必服务端的数据都发送给客户端了, 所以服务端不会马上关闭SOCKET连接. 当服务端数据发送完毕后, 会发送给客户端一个FIN报文, 表示同意关闭连接, 所以这里的ACK报文和FIN报文是分开发送的.

BIO(Blocking IO)

BIO也叫同步阻塞IO, 对于每一个客户端的连接请求都会创建一个新线程来进行处理, 处理完成后线程销毁. 当一个线程调用IO流读写数据时,该线程被阻塞,直到读到数据,或数据完全写入, 该线程在此期间不能再干任何事情。
在这里插入图片描述

  1. public class Server {
  2. public static void main(String[] args) {
  3. ServerSocket server = null;
  4. try {
  5. server = new ServerSocket(12000);
  6. System.out.println("server start...");
  7. while(true){
  8. //进行阻塞,监听端口
  9. Socket socket = server.accept();
  10. //新建一个线程执行客户端的任务
  11. new Thread(new ServerHandler(socket)).start();
  12. }
  13. } catch (Exception e) {
  14. e.printStackTrace();
  15. } finally {
  16. if(server != null){
  17. try {
  18. server.close();
  19. } catch (IOException e) {
  20. e.printStackTrace();
  21. }
  22. }
  23. server = null;
  24. }
  25. }
  26. }
  27. public class ServerHandler implements Runnable{
  28. private Socket socket ;
  29. public ServerHandler(Socket socket){
  30. this.socket = socket;
  31. }
  32. @Override
  33. public void run() {
  34. BufferedReader in = null;
  35. PrintWriter out = null;
  36. try {
  37. in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
  38. out = new PrintWriter(this.socket.getOutputStream(), true);
  39. String content = null;
  40. while(true){
  41. content = in.readLine();
  42. if(content == null) break;
  43. System.out.println("Server :" + content);
  44. out.println("Server response");
  45. }
  46. } catch (Exception e) {
  47. e.printStackTrace();
  48. } finally {
  49. if(in != null){
  50. try {
  51. in.close();
  52. } catch (IOException e) {
  53. e.printStackTrace();
  54. }
  55. }
  56. if(out != null){
  57. try {
  58. out.close();
  59. } catch (Exception e) {
  60. e.printStackTrace();
  61. }
  62. }
  63. if(socket != null){
  64. try {
  65. socket.close();
  66. } catch (IOException e) {
  67. e.printStackTrace();
  68. }
  69. }
  70. socket = null;
  71. }
  72. }
  73. }
  74. public class Client {
  75. public static void main(String[] args) {
  76. Socket socket = null;
  77. BufferedReader in = null;
  78. PrintWriter out = null;
  79. try {
  80. socket = new Socket("127.0.0.1", 12000);
  81. in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
  82. out = new PrintWriter(socket.getOutputStream(), true);
  83. //向服务器端发送数据
  84. out.println("Client request");
  85. String response = in.readLine();
  86. System.out.println("Client: " + response);
  87. } catch (Exception e) {
  88. e.printStackTrace();
  89. } finally {
  90. if(in != null){
  91. try {
  92. in.close();
  93. } catch (IOException e) {
  94. e.printStackTrace();
  95. }
  96. }
  97. if(out != null){
  98. try {
  99. out.close();
  100. } catch (Exception e) {
  101. e.printStackTrace();
  102. }
  103. }
  104. if(socket != null){
  105. try {
  106. socket.close();
  107. } catch (IOException e) {
  108. e.printStackTrace();
  109. }
  110. }
  111. socket = null;
  112. }
  113. }
  114. }

伪异步IO

使用线程池来管理线程, 实现1个或多个线程处理N个客户端.

  1. public class Server {
  2. public static void main(String[] args) {
  3. ServerSocket server = null;
  4. try {
  5. server = new ServerSocket(12000);
  6. System.out.println("server start...");
  7. while(true){
  8. //进行阻塞,监听端口
  9. Socket socket = server.accept();
  10. //用线程池来管理线程
  11. ThreadPoolExecutor executor = new ThreadPoolExecutor(
  12. Runtime.getRuntime().availableProcessors(),
  13. 10,
  14. 120L,
  15. TimeUnit.SECONDS,
  16. new LinkedBlockingQueue<Runnable>(20)
  17. );
  18. //新建一个线程执行客户端的任务
  19. executor.execute(new Thread(new ServerHandler(socket)));
  20. }
  21. } catch (Exception e) {
  22. e.printStackTrace();
  23. } finally {
  24. if(server != null){
  25. try {
  26. server.close();
  27. } catch (IOException e) {
  28. e.printStackTrace();
  29. }
  30. }
  31. server = null;
  32. }
  33. }
  34. }

NIO(Non-Blocking IO)

NIO也叫同步非阻塞IO, NIO的服务端可以只启动一个专门的线程来处理所有的 IO 事件, 且不会被任何IO事件阻塞住.

服务端和客户端各自维护一个Selector选择器, Selector会不断地轮询注册在其上的通道(Channel)是否发生事件, 只有事件发生时才会去执行相应的操作. 当客户端连接服务器时发生OP_CONNECT事件, 当服务端接收到客户端连接时发生OP_ACCEPT事件, 当有数据发送过来时发生OP_READ事件, 当要发送数据给对方时发生OP_WRITE事件.

在这里插入图片描述

Buffer

在BIO中, 数据直接读写到Stream对象中. 而在NIO中, 所有数据都是读写到Buffer中, 然后通过Channel传输. Buffer实质上是一个数组, 通常它是一个字节数组(ByteBuffer) ,也可以是其他类型的数组.

(1)成员变量

  • mark : s初始值为-1,用于备份当前的position;
  • position : 初始值为0, position表示当前可以写入或读取数据的位置,当写入或读取一个数据后,position移动到下一个位置
  • limit : 写模式下,limit表示最多能往Buffer里写多少数据,等于capacity值;读模式下,limit表示最多可以读取多少数据
  • capacity : 缓存数组大小

在这里插入图片描述

(2)成员方法

  • allocate(int capacity) : 创建指定长度的缓冲区
  • put(E e) : 添加一个元素
  • get() : 获取第一个元素
  • wrap(E[] array) : 将数组元素添加到缓冲中
  • clear() : 清除数据, 实际上数据没有被清除.
  • public final Buffer clear() {

    1. position = 0;
    2. limit = capacity;
    3. mark = -1;
    4. return this;
    5. }
  • flip() : Buffer有两种模式, 写模式和读模式. flip后Buffer从写模式变成读模式.

  • public final Buffer flip() {

    1. limit = position;
    2. position = 0;
    3. mark = -1;
    4. return this;
    5. }

(3)Buffer的实现类

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

(4)实例

  1. public static void main(String[] args) {
  2. //创建指定长度的缓冲区
  3. IntBuffer buf = IntBuffer.allocate(10);
  4. buf.put(13);
  5. buf.put(21);
  6. buf.put(35);
  7. System.out.println(buf);
  8. //从写模式变成读模式
  9. buf.flip();
  10. System.out.println(buf);
  11. //调用get方法会使position位置向后递增一位
  12. for (int i = 0; i < buf.limit(); i++) {
  13. System.out.print(buf.get() + "\t");
  14. }
  15. System.out.println("\n" + buf);
  16. //清除数据
  17. buf.clear();
  18. System.out.println(buf);
  19. }
  20. java.nio.HeapIntBuffer[pos=3 lim=10 cap=10]
  21. java.nio.HeapIntBuffer[pos=0 lim=3 cap=10]
  22. 13 21 35
  23. java.nio.HeapIntBuffer[pos=3 lim=3 cap=10]
  24. java.nio.HeapIntBuffer[pos=0 lim=10 cap=10]

Channel

NIO把它支持的I/O对象抽象为Channel, Channel又称为”通道”, 类似于BIO中的流(Stream), 但有锁区别:

  • 流是单向的,通道是双向的,可读可写
  • 流读写是阻塞的,通道可以阻塞也可以非阻塞
  • 流中的数据可以选择性的先读到缓存中,通道的数据总是要先读写到缓存中

在这里插入图片描述

(1)Channel的实现类

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

Selector

Selector选择器充当一个监听者, 会不断地轮询注册在其上的通道(Channel)是否发生事件, 如果某个通道发生了读写操作, 这个通道就处于就绪状态, 会被Selector轮询出来, 进行后续的IO操作.

(1)Selector监听的事件(SelectionKey)

  • OP_CONNECT : 客户端连接服务端事件
  • OP_ACCEPT : 服务端接收客户端连接事件
  • OP_READ : 读事件
  • OP_WRITE : 写事件

简单的NIO实例

  1. public class Server{
  2. //选择器(多路复用器)
  3. private Selector selector;
  4. //读取的缓冲区
  5. private ByteBuffer readBuf = ByteBuffer.allocate(1024);
  6. //写入的缓冲区
  7. private ByteBuffer writeBuf = ByteBuffer.allocate(1024);
  8. /**
  9. * 打开选择器,注册服务器通道
  10. */
  11. public Server(int port){
  12. try {
  13. //1 打开选择器
  14. this.selector = Selector.open();
  15. //2 打开服务器通道
  16. ServerSocketChannel ssc = ServerSocketChannel.open();
  17. //3 设置服务器通道为非阻塞模式
  18. ssc.configureBlocking(false);
  19. //4 绑定地址
  20. ssc.bind(new InetSocketAddress(port));
  21. // 5 把服务器通道注册到选择器上,并为该通道注册OP_ACCEPT事件.
  22. // 当该事件到达时,selector.select()会返回,否则selector.select()会一直阻塞
  23. ssc.register(this.selector, SelectionKey.OP_ACCEPT);
  24. System.out.println("Server start, port :" + port);
  25. } catch (IOException e) {
  26. e.printStackTrace();
  27. }
  28. }
  29. /**
  30. * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
  31. */
  32. public void listen() {
  33. while(true){
  34. try {
  35. //1 当注册的事件发生时,方法返回;否则,该方法会一直阻塞
  36. this.selector.select();
  37. //2 获得selector选中项的迭代器,选中的项为注册的事件
  38. Iterator<SelectionKey> keys = this.selector.selectedKeys().iterator();
  39. while(keys.hasNext()){
  40. SelectionKey key = keys.next();
  41. //删除已选的key,以防重复处理
  42. keys.remove();
  43. //OP_ACCEPT事件发生(接收到客户端的连接)
  44. if(key.isAcceptable()){
  45. ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
  46. //获得和客户端连接的通道
  47. SocketChannel sc = ssc.accept();
  48. sc.configureBlocking(false);
  49. //将SocketChannel注册到selector上,并设置读事件
  50. sc.register(this.selector, SelectionKey.OP_READ);
  51. }else if(key.isReadable()){ //OP_READ事件发生
  52. read(key);
  53. }else if(key.isWritable()){ //OP_WRITE事件发生
  54. write(key);
  55. }
  56. }
  57. } catch (IOException e) {
  58. e.printStackTrace();
  59. }
  60. }
  61. }
  62. /**
  63. * 读操作
  64. */
  65. private void read(SelectionKey key) {
  66. try {
  67. //清空缓冲区旧的数据
  68. this.readBuf.clear();
  69. //获取socket通道对象
  70. SocketChannel sc = (SocketChannel) key.channel();
  71. int count = sc.read(this.readBuf);
  72. if(count > 0){
  73. String msg = new String(readBuf.array());
  74. System.out.println("服务端收到信息:"+msg);
  75. //将SocketChannel注册到selector上,并设置写事件
  76. sc.register(this.selector, SelectionKey.OP_WRITE);
  77. }
  78. } catch (IOException e) {
  79. e.printStackTrace();
  80. }
  81. }
  82. /**
  83. * 写操作
  84. */
  85. private void write(SelectionKey key){
  86. try {
  87. //清空缓冲区旧的数据
  88. this.writeBuf.clear();
  89. //获取socket通道对象
  90. SocketChannel sc = (SocketChannel) key.channel();
  91. sc.write(ByteBuffer.wrap(new String("我是服务端,我已收到你的信息!").getBytes()));
  92. //将SocketChannel注册到selector上,并设置读事件
  93. sc.register(this.selector, SelectionKey.OP_READ);
  94. } catch (IOException e) {
  95. e.printStackTrace();
  96. }
  97. }
  98. public static void main(String[] args) {
  99. new Server(12000).listen();
  100. }
  101. }
  102. public class Client {
  103. //选择器(多路复用器)
  104. private Selector selector;
  105. //读取的缓冲区
  106. private ByteBuffer readBuf = ByteBuffer.allocate(1024);
  107. //写入的缓冲区
  108. private ByteBuffer writeBuf = ByteBuffer.allocate(1024);
  109. /**
  110. * 打开选择器,注册客户端通道
  111. */
  112. public Client(String ip,int port){
  113. try {
  114. //1 打开选择器
  115. this.selector = Selector.open();
  116. //2 获得一个Socket通道
  117. SocketChannel channel = SocketChannel.open();
  118. //3 设置通道为非阻塞
  119. channel.configureBlocking(false);
  120. //4 绑定服务器ip和port
  121. channel.connect(new InetSocketAddress(ip,port));
  122. //5 将客户端通道注册到选择器上,并为该通道注册OP_CONNECT事件
  123. channel.register(selector, SelectionKey.OP_CONNECT);
  124. } catch (IOException e) {
  125. e.printStackTrace();
  126. }
  127. }
  128. /**
  129. * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
  130. */
  131. public void listen() {
  132. while(true){
  133. try {
  134. //1 当注册的事件发生时,方法返回;否则,该方法会一直阻塞
  135. this.selector.select();
  136. //2 获得selector选中项的迭代器,选中的项为注册的事件
  137. Iterator<SelectionKey> keys = this.selector.selectedKeys().iterator();
  138. while(keys.hasNext()){
  139. SelectionKey key = keys.next();
  140. //删除已选的key,以防重复处理
  141. keys.remove();
  142. //OP_CONNECT事件发生(连接上服务器)
  143. if(key.isConnectable()){
  144. SocketChannel sc = (SocketChannel) key.channel();
  145. // 如果正在连接,则完成连接
  146. if(sc.isConnectionPending()){
  147. sc.finishConnect();
  148. }
  149. sc.configureBlocking(false);
  150. //将SocketChannel注册到selector上,并设置写事件
  151. sc.register(this.selector, SelectionKey.OP_WRITE);
  152. }else if(key.isReadable()){
  153. read(key);
  154. }else if(key.isWritable()){
  155. write(key);
  156. }
  157. }
  158. } catch (IOException e) {
  159. e.printStackTrace();
  160. }
  161. }
  162. }
  163. /**
  164. * 读操作
  165. */
  166. private void read(SelectionKey key) {
  167. try {
  168. //清空缓冲区旧的数据
  169. this.readBuf.clear();
  170. //获取socket通道对象
  171. SocketChannel sc = (SocketChannel) key.channel();
  172. int count = sc.read(this.readBuf);
  173. if(count > 0){
  174. String msg = new String(readBuf.array());
  175. System.out.println("客户端收到信息:"+msg);
  176. //将SocketChannel注册到selector上,并设置写事件
  177. sc.register(this.selector, SelectionKey.OP_WRITE);
  178. }
  179. } catch (IOException e) {
  180. e.printStackTrace();
  181. }
  182. }
  183. /**
  184. * 写操作
  185. */
  186. private void write(SelectionKey key){
  187. try {
  188. //清空缓冲区旧的数据
  189. this.writeBuf.clear();
  190. //获取socket通道对象
  191. SocketChannel sc = (SocketChannel) key.channel();
  192. sc.write(ByteBuffer.wrap(new String("我是Client,我先发条信息!").getBytes()));
  193. //将SocketChannel注册到selector上,并设置读事件
  194. sc.register(this.selector, SelectionKey.OP_READ);
  195. } catch (IOException e) {
  196. e.printStackTrace();
  197. }
  198. }
  199. public static void main(String[] args) {
  200. new Client("127.0.0.1",12000).listen();
  201. }
  202. }
  203. 结果:
  204. 客户端先发送一条消息
  205. 服务端收到客户端的消息,然后返回一条消息
  206. 客户端收到服务端的消息,再发送一条消息
  207. ...

BIO和NIO的区别

(1)BIO是面向流的, 而NIO是面向缓冲的.

(2)BIO是阻塞的,而NIO是非阻塞的.

  • 传统IO方式(BIO)在调用InputStream.read()/BufferedReader.readLine()方法时是阻塞的,它会一直等到数据到来或缓冲区已满或超时才会返回.
  • NIO通过向Selector注册读写事件, Selector不断轮询读写事件是否发生, 当读写事件发生后再去进行相应的处理.

(3)NIO的选择器允许一个单独的线程来监视多个输入通道.

发表评论

表情:
评论列表 (有 0 条评论,340人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Java基础网络编程

    一.网络概述: (1)计算机网路: 计算机网络,是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路连接起来,在网络操作系统,网络管理软件及网络通信协

    相关 Java基础网络编程

        Java基础之网络编程 基本概念 Socket 类:该类实现客户端套接字,套接字指的是两台设备之间通讯的端点。 构造函数与常用方法 客户端实现步骤--使用S