程序员潇然 发表于 2022-10-10 14:22:55

网络模型简介以及阻塞和非阻塞编程示例(三)

一般情况下的文件读写,虽然之前的BIO较慢,但是大多数场景也基本没啥问题。

但是面对网络IO,动辄数万、十万、百万的并发来说,显然已经可以称之为不可用状态。

所以java很早前就推出了NIO,用来解决这个问题。

本文主要在重提一下阻塞的网络编程,然后在于NIO的实现进行对比。

通俗的说,网络编程就是把数据从一台计算机传输到另一台计算机,这中间需要经过各种中间设备(比如路由器、交换机),也会经过各种软件进行处理加工。
关于计算机网络的发展,可以参考专栏《计算机网络》`https://www.crazybytex.com/collection-2.html`

### 网络编程简介

看下百度百科介绍:

!(data/attachment/forum/202210/10/141356ujb6bztq67bvs5i5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")

众所周知,计算机网络的TCP/IP协议,已经是事实上的标准,所有的操作系统,都提供了底层支持,对于开发人员来说,就是基于操作系统提供的系统调用进行编程,然而TCP/IP协议,较为复杂,比较偏向于底层,所以有了后来的socket,简化了网络编程的复杂度。

### SOCKET编程

socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口。
不管是什么协议,从程序的角度看,就是一行行代码的实现,通过一系列代码的组合实现了某种功能,遵守了某些规则的程序。
如同我们平时写代码封装了类库,提供了一系列的接口可以被调用,socket编程的本质就是如此。
这样对于普通的开发来说就不需要过多的涉猎底层网络传输的内容,可以更加高效的专注于应用程序的开发。

从网络编程的视角看,一台计算机的结构大致如下:

!(data/attachment/forum/202210/10/141425t0rqb05bnk5bkrez.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")

现代计算机,肯定是都依赖操作系统的,而操作系统提供了两类接口:
人机交互,比如控制台输入,鼠标点击
编程接口,系统调用函数,供用户开发程序调用系统资源以及管理硬件

socket就是编程接口的一部分,用来网络编程。

tcp网络编程一个概要示意图如下

!(data/attachment/forum/202210/10/141516ozq3tq7qq7ycz3w1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")

```html
服务端和客户端初始化 socket,得到文件描述符;
服务端调用 bind,将绑定在 IP 地址和端口;
服务端调用 listen,进行监听;
服务端调用 accept,等待客户端连接;

客户端调用 connect,向服务器端的地址和端口发起连接请求;
服务端 accept 返回用于传输的 socket 的文件描述符;
客户端调用 write 写入数据;服务端调用 read 读取数据;
```

再详细点说的话,看下UNIX网络编程 接口:

```cpp
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
```

sockfd是由socket函数返回的套接口描述字,第二个参数是一个指向特定于协议的地址结构的指针,第三个参数是该地址结构的长度
调用bind 可以指定ip和端口号,也可以不指定

```cpp
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
```

sockfd是由socket函数返回的套接口描述字,第二、第三个参数分别是一个指向套接口地址结构的指针和该结构的大小。
套接口地址结构必须含有服务器的IP地址和端口号

如果是TCP套接字,调用connect 将会触发三次握手的过程。

```cpp
#include <sys/socket.h>
int listen(int sockfd, int backlog);
```

sockfd是bind之后的套接口描述字,第二个参数规定了内核应该为相应套接口排队的最大连接个数

当socket创建时,默认是主动套接字-也就是将会调用connect发起连接的套接字,调用listen那么将会转换为被动套接字,也就是接受指向该连接的套接字

```cpp
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
```

//返回:非负描述子——成功,-1——出错

参数sockfd是监听后的套接字,这个套接字用来监听一个端口,当有一个客户与服务器连接时,它使用一个与这个套接字关联的端口号

参数cliaddr和addrlen是一个结果参数,用来返回已连接客户的协议地址。
如果accept调用成功,那么其返回值是由内核自动生成的一个全新描述符,代表与客户端的连接

**socket是一种网络通信模型,与我们常说的同步异步 阻塞非阻塞式没有直接关系的**

因为他作为编程接口,底层都有各自的实现,不需要自己单独进行实现

!(data/attachment/forum/202210/10/141741ul4ggcbj44jjyt4i.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")

简化以下形式如下

!(data/attachment/forum/202210/10/141753wjmutz0lfkgucawf.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")

上图就是现在大家进行网络编程的一个通常的模型。
分为客户端和服务端,客户端与服务端建立连接之后,客户端发起请求,服务端返回响应。

对于java开发来说,一个简易的socket编程如下

```java
package com.crazybytex.hello.socket;


import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;


/**
* @description server
* @Author 本文作者 程序员潇然 疯狂的字节X https://www.crazybytex.com/
* @Date 11:59 2022/7/4
**/
public class SocketServer {
    public static void main(String[] args) throws IOException {
      ServerSocket serverSocket = null;
      Socket socket = null;
      Reader reader = null;
      OutputStream os = null;
      PrintWriter pw = null;
      try {
            //1、创建一个服务器端Socket,即ServerSocket,指定绑定的端口,并监听此端口
            //1024-65535的某个端口
            serverSocket = new ServerSocket(9527);
            //2、调用accept()方法开始监听,等待客户端的连接
            socket = serverSocket.accept();
            //3、获取输入流,并读取客户端信息
            reader = new InputStreamReader(socket.getInputStream());
            char chars[] = new char;
            int len;
            String strClient = "";
            while ((len = reader.read(chars)) != -1) {
                strClient = new String(chars, 0, len);
                System.out.println("Receive from client message=: " + strClient);
            }
            //关闭输入流
            socket.shutdownInput();
            //4、获取输出流,响应客户端的请求
            os = socket.getOutputStream();
            pw = new PrintWriter(os);
            pw.write("Hello client ,response from server");
            pw.flush();
            //5、关闭资源
            pw.close();
            os.close();
            reader.close();
            socket.close();
            serverSocket.close();
      } catch (IOException e) {
            e.printStackTrace();
      }


    }
}
```

```java
package com.crazybytex.hello.socket;


import java.io.*;
import java.net.Socket;


/**
* @description client
* @Author 本文作者 程序员潇然 疯狂的字节X https://www.crazybytex.com/
* @Date 11:59 2022/7/4
**/
public class SocketClient {


    public static void main(String[] args) {
      try {
            //1、创建客户端Socket,指定服务器地址和端口
            Socket socket = new Socket("127.0.0.1", 9527);
            //2、获取输出流,向服务器端发送信息
            //字节输出流
            OutputStream os = socket.getOutputStream();
            //将输出流包装成打印流
            PrintWriter pw = new PrintWriter(os);
            pw.write("hello server ,i'm client");
            pw.flush();
            socket.shutdownOutput();


            //3、获取输入流,并读取服务器端的响应信息
            InputStream is = socket.getInputStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            String info = null;
            while ((info = br.readLine()) != null) {
                System.out.println("Receive from server message=: " + info);
            }


            //4、关闭资源
            br.close();
            is.close();
            pw.close();
            os.close();
            socket.close();
      } catch (IOException e) {
            System.out.println(e);
      }
    }
}
```

!(data/attachment/forum/202210/10/142112zzczzq16z9ob96c9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")

!(data/attachment/forum/202210/10/142119wmn7clspmnesmffo.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")

!(data/attachment/forum/202210/10/142126yo2elxv0d3288k8z.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")

值得注意的是,如果在如图所示位置加断点

!(data/attachment/forum/202210/10/142144gcyucbkuwbk0gxc0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")

server启动后,并不会走到断点,当有客户端连接发送数据时,才会走到断点
这也说明了 accept方法是一个阻塞方法。

!(data/attachment/forum/202210/10/142202wotja7lyj2y3jldj.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")

对于java中的socket编程,核心概念如上图所示,基于请求响应模式
服务端监听端口,等待远程连接
客户端根据远程服务器的ip地址以及端口号,发起连接
建立连接后,基于流,进行请求和响应。

socket是应用层与传输层之间的编程接口,socket就是对底层函数的封装,java是面向对象的编程语言
所以java中的socket就是对这些方面的一个抽象

一种通俗的网络编程模式如下图所示,通道不仅限于InputStream 和OutputStream ,随着java的发展,随之而来的还有Channel
换句话说,大致的网络编程模型是基本固定的,而变化的是IO的具体处理

!(data/attachment/forum/202210/10/142218od0wud98cdwu0q1c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")



介绍了阻塞式的网络编程,下面展示一个非阻塞的。

示例代码gitee:

`https://gitee.com/crazybytex/java-code-fragment/tree/master/src/main/java/com/crazybytex/fragment/nio`

```java
package com.crazybytex.fragment.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;

/**
* @Author 本文作者 程序员潇然 疯狂的字节X https://www.crazybytex.com/
* @Date 2022/10/10 14:41
* @Description TODO
**/
public class Server {
    public static void main(String[] args) throws IOException, InterruptedException {

      //1.打开一个服务器通道
      ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

      //2.绑定对应的端口号
      serverSocketChannel.bind(new InetSocketAddress(8888));

      //3.通道默认是阻塞的,需要设置为非阻塞
      serverSocketChannel.configureBlocking(false);

      //4.创建选择器
      Selector selector = Selector.open();

      //5.将服务器通道注册到选择器上,并且指定注册监听事件为OP_ACCEPT
      serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
      System.out.println("服务端启动成功!");

      while (true) {
            //6.检查选择器是否有事件
            int select = selector.select(2000);
            if (select == 0) {
                continue;
            }

            //7.获取事件集合
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                //8.判断事件是否是客户端连接事件SelectionKey.isAcceptable()
                SelectionKey key = iterator.next();

                //9.得到客户端通道,并将通道注册到选择器上,并且指定监听事件为OP_READ
                if (key.isAcceptable()) {
                  SocketChannel socketChannel = serverSocketChannel.accept();
                  System.out.println("客户端已连接......" + socketChannel);

                  //必须设置通道为非阻塞, 因为selector需要轮询监听每个通道的事件
                  socketChannel.configureBlocking(false);

                  //并指定监听事件为OP_READ
                  socketChannel.register(selector, SelectionKey.OP_READ);
                }

                //10.判断是否是客户端读就绪事件SelectionKey.isReadable()
                if (key.isReadable()) {

                  //11.得到客户端通道,读取数据到缓冲区
                  SocketChannel socketChannel = (SocketChannel) key.channel();
                  ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                  int read = socketChannel.read(byteBuffer);
                  if (read > 0) {
                        System.out.println("客户端消息:" +
                              new String(byteBuffer.array(), 0, read,
                                        StandardCharsets.UTF_8));

                        //12.给客户端回写数据
                        socketChannel.write(ByteBuffer.wrap("收到消息了".getBytes(StandardCharsets.UTF_8)));
                        socketChannel.close();
                  }
                }

                //13.从集合中删除对应的事件, 因为防止二次处理.
                iterator.remove();
            }
      }
    }

}

```

```java
package com.crazybytex.fragment.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;

/**
* @Author 本文作者 程序员潇然 疯狂的字节X https://www.crazybytex.com/
* @Date 2022/10/10 14:41
* @Description TODO
**/
public class Client {
    public static void main(String[] args) throws IOException {
      //1.打开通道
      SocketChannel socketChannel = SocketChannel.open();

      //2.设置连接IP和端口号
      socketChannel.connect(new InetSocketAddress("127.0.0.1", 8888));

      //3.写数据
      socketChannel.write(ByteBuffer.wrap("服务端你好".getBytes(StandardCharsets.UTF_8)));

      //4.读取服务器写回的数据
      ByteBuffer readBuffer = ByteBuffer.allocate(1024);
      int read = socketChannel.read(readBuffer);
      System.out.println("服务端返回消息:" + new String(readBuffer.array(), 0, read, StandardCharsets.UTF_8));

      //5.释放资源
      socketChannel.close();


    }

}

```

先运行server

!(data/attachment/forum/202210/10/145046jxj5p85jwtooo225.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")

在启动客户端

!(data/attachment/forum/202210/10/145106fxcxkm2f79dtfd2a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")

再看下服务端

!(data/attachment/forum/202210/10/145116xl86ld54hz5ev765.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")

从执行结果看,也没有多大区别,但是实现方式完全变化了。

```html
ServerSocketChannel 绑定了本地的端口,并且注册到了Selector上
通过Selector可以获取事件集合,遍历这个集合可以获得就绪的事件
根据事件的类型,可以进行相关的处理逻辑。
```

不再是直接的通过socket建立连接后的单纯阻塞读取,演变成了基于事件驱动的模型。

抽象出来了Selector SelectionKey Channel 等概念,Channel 关联到Selector,注册绑定关注的SelectionKey(事件),当事件产生响应时,就会获得相应的回调(也就是进入了相关事件的处理逻辑),这样不需要关注过程,只需要写相关的处理逻辑即可。

这是过程就是事件注册与处理结果回调,这个与前面提到的IO多路复用是不是有点类似呢?

毕竟上层应用,不需要关注底层,只需要关注事件的注册以及相关事件的处理即可。

后续展开介绍网络编程模型。


!(data/attachment/forum/202206/16/141330jha7st9soow8772i.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "common_log.png")
`转载务必注明出处:程序员潇然,疯狂的字节X,https://crazybytex.com/thread-202-1-1.html `
页: [1]
查看完整版本: 网络模型简介以及阻塞和非阻塞编程示例(三)