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

框架与中间件 框架与中间件 1656 人阅读 | 0 人回复

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

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

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

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

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

网络编程简介

看下百度百科介绍:

image.png

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

SOCKET编程

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

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

image.png

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

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

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

image.png

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

image.png

简化以下形式如下

image.png

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

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

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[1024];
            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();
        }


    }
}
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);
        }
    }
}

image.png

image.png

image.png

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

image.png

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

image.png

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

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

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

image.png

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

示例代码gitee:

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

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();
            }
        }
    }

}
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

image.png

在启动客户端

image.png

再看下服务端

image.png

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

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

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

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

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

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

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

common_log.png 转载务必注明出处:程序员潇然,疯狂的字节X,https://crazybytex.com/thread-202-1-1.html

关注下面的标签,发现更多相似文章

文章被以下专栏收录:

    黄小斜学Java

    疯狂的字节X

  • 目前专注于分享Java领域干货,公众号同步更新。原创以及收集整理,把最好的留下。
    包括但不限于JVM、计算机科学、算法、数据库、分布式、Spring全家桶、微服务、高并发、Docker容器、ELK、大数据等相关知识,一起进步,一起成长。
热门推荐
[若依]微服务springcloud版新建增添加一个
[md]若依框架是一个比较出名的后台管理系统,有多个不同版本。
[CXX1300] CMake '3.18.1' was not
[md][CXX1300] CMake '3.18.1' was not found in SDK, PATH, or
海康摄像头接入 wvp-GB28181-pro平台测试验
[md]### 简介 开箱即用的28181协议视频平台 `https://github.c