【Netty】网络协议与BIO/NIO编程

简介: 做为Netty系列第一篇文章,简要介绍学习Netty需要掌握的计算机网络知识,面试和学习中的重点TCP和UDP两个协议,并实现BIO编程对不同协议(TCP/UDP)的开发方法,同时也详细介绍了NIO编程的开发步骤和开发方法供读者参考

脑图:

前言:做为Netty系列第一篇文章,简要介绍学习Netty需要掌握的计算机网络知识,面试和学习中的重点TCP和UDP两个协议,并实现BIO编程对不同协议(TCP/UDP)的开发方法,同时也详细介绍了NIO编程的开发步骤和开发方法以及供读者参考。

有了本篇的基础,相信读者对学习Netty的整体架构以及原理可以很快了解与上手。

image.gif编辑

一、网络协议

1、了解OSI七层模型

口诀:巫术忘传会飙鹰,简要说明一下图片内容,网络协议一般理论上分osi7层,实现上是tcp/ip4层互联通信模型

image.gif编辑

image.gif编辑

2、了解TCP

2.1 TCP三次握手示意图

image.gif编辑

2.2 为什么需要3次?
一、TCP
是面对连接的,所以需要双方都确认连接的建立。
第一次客户端请求建立连接。第
二次服务端应答客户端,并请求建立连接。第三次客户端针对服务端请求确认应答
二、为了初始化Sequence Number
的值,保证通信双方不乱序,TCP会用这个seq拼接数据。

2.3 TCP Flags
SYN:同步序号,用于建立连接
FIN:
finish标志,用于释放连接
ACK:确认序号标志

2.4 SYN超时怎么办?

SYN队列满后,通过tcp_ syncookies参数回发SYN Cookie
若为正常连接则Client
会回发SYN Cookie ,直接建立连接

2.5 建立连接后, Client出现故障怎么办?
向对方发送保活探测报文,
如果未收到响应则继续发送
尝试次数达到保活探测数仍未收到响应则中断连接

2.6 TCP的三次握手的漏洞 ——洪泛攻
三次握手中有一个第二次握手,服务端向客户端应道请求,应答请求是需要客户端IP
的,服务端是需要知道客户端IP的,攻击者就伪造这个IP,往服务器端狂发送第一次握手的内容,当然第一次握手中的客户端IP地址是伪造的,从而服务端忙于进行第二次握手但是第二次握手当然没有结果,所以导致服务器端被拖累,死机

解决方案:无效连接监控释放;延缓TCB分配方法;防火墙

2.7 TCP四次挥手示意图

image.gif编辑

次挥手即终止TCP连接,需要客户端和服务端总共发送4个包以确认连接的断开。在socket编程中,这一过程由客户端或服务端任一方执行close来触发

TCP连接是全双工的,因此每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭。

2.8 为什么会有TIME_ WAIT状态?
确保有足够的时间让对方收到ACK

避免新旧连接混淆

2.9 服务器出现大量CLOSE_ WAIT状态的原因?
对方关闭socket
连接我方忙于读或写没有及时关闭连接
检查代码,
特别是释放资源的代码
检查配置,
特别是处理请求的线程配置

2.10 TCP的滑动窗口
TCP使用滑动窗口做流量控制与乱序重排

3.了解HTTP

3.1 HTTP报文结构

image.gif编辑image.gif编辑

3.2 一次完整http请求的过

1、DNS域名解析(本地浏览器缓存、操作系统缓存或者DNS服务器)

2、三次握手建立 TCP 连接

3、客户端向服务器发送请求命令Get /www.xx.com/ http/1.1
4、客户端发送请求头信息

5、服务服务器应答器  Http/1.1 200 OK
6、返回响应头信息

7、服务器向客户端发送数据

8、服务器关闭 TCP 连接

3.3 HTTP状态码,五种可能的取值
1xx :指示信息
--表示请求已接收,继续处理
2xx :成功
--表示请求已被成功接收、理解、接受
3xx :重定向
--要完成请求必须进行更进一步的操作
4xx :客户端错误
--请求有语法错误或请求无法实现
5xx :服务器端错误
--服务器未能实现合法的请求

二、java原生网络编程-Linux网络IO模型

BIO进程会一直阻塞,直到数据拷贝完成  

image.gif编辑

NIO非阻塞IO通过进程反复调用IO函数(多次系统调用,并马上返回);在数据拷贝的过程中,进程是阻塞的;

image.gif编辑

I/O多路复用:使用一个或多个固定线程来处理每一个 Socket

image.gif编辑

AIO当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者的输入输出操作。

image.gif编辑

主要区别:主要在等待数据和数据复制这两个时间段不同

image.gif编辑

三、Java原生网络编程-BIO编程

Socket是对TCP/IP协议的抽象,是操作系统对外开放的接口

编程中的Socket是应用层与TCP/IP协议族通信的中间软件抽象层,在设计模式中,Socket其实就是一个外观模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

以下

image.gif编辑

public class TCPServer {
    public static void main(String[] args) throws Exception {
        // 创建socket,并将socket绑定到port 65000端口
        ServerSocket ss = new ServerSocket(65000);
        // 死循环,使得socket-直等待并处理客户端发送过来的请求
        while (true) {
            // 监听65000端口,直到客户端返回连接信息后才返回
            Socket socket = ss.accept();
            // 获取客户端的请求信息后,执行相关业务逻辑
            new Calculator(socket).start();
        }
    }
}
public class TCPClient {
    public static void main(String[] args) throws Exception {
        // 创建socket,并指定连接的是本机的端口号host:为127.0.0.1;port为65000的服务器socket
        Socket socket = new Socket("127.0.0.1", 65000);
        // 获取输出流
        OutputStream os = socket.getOutputStream();
        // 获取输入流
        InputStream is = socket.getInputStream();
        // 将要传递给server的字符串参数转换成byte数组,并数组写入到输出流中
        os.write(new String("hello world").getBytes());
        byte[] buff = new byte[1024];
        // buff主要用来读取输入的内容,存成byte数组,ch主要用来获取读取数组的长度
        int ch = is.read(buff);
        // 将接收流的byte数组转换成字符串,这里是从服务端回发回来的字符串参数的长度
        String content = new String(buff, 0, ch);
        System.out.println(content);
        // 不要忘记关闭输入输出流以及socket
        is.close();
        os.close();
        socket.close();
    }
}
public class Calculator extends Thread {
    /**
     * 以socket为成员变量
     */
    private Socket socket;
    public Calculator(Socket socket) {
        this.socket = socket;
    }
    @Override
    public void run() {
        try {
            // 获取socket的输出流
            OutputStream outputStream = socket.getOutputStream();
            // 获取socket的输入流
            InputStream inputStream = socket.getInputStream();
            // buff主要用来读取输入的内容,存成byte数组,ch主要用来获取读取数组
            byte[] buff = new byte[1024];
            int ch = inputStream.read(buff);
            // 将接收流的byte数组转换成字符串,这里获取的内容是客户端发送过来的字节
            String content = new String(buff, 0, ch);
            System.out.println(content);
            // 往输出流里写入获得的字符串的长度,回发给客户端
            outputStream.write(String.valueOf(content.length()).getBytes());
            // 不要忘记关闭输入输出流以及socket
            inputStream.close();
            outputStream.close();
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

image.gif

public class UDPServer {
    public static void main(String[] args) throws Exception {
        // 服务端接受客户端发送的数据报port:65001 ,监听的端口号
        DatagramSocket socket = new DatagramSocket(65001);
        // 存储从客户端接受到的内容
        byte[] buff = new byte[100];
        DatagramPacket packet = new DatagramPacket(buff, buff.length);
        // 接受客户端发送过来的内容,并将内容封装进DatagramPacket对象中
        socket.receive(packet);
        // 从DatagramPacket对象 中获取到真正存储的数据
        byte[] data = packet.getData();
        // 将数据从二进制转换成字符串形式
        String content = new String(data, 0, packet.getLength());
        System.out.println(content);
        // 将要发送给客户端的数据转换成二进制
        byte[] sendedContent = String.valueOf(content.length()).getBytes();
        // 服务端给客户端发送数据报
        // 从DatagramPacket对象中获取到数据的来源地址与端口号
        DatagramPacket packetToClient = new DatagramPacket(sendedContent, sendedContent.length, packet.getAddress(), packet.getPort());
        // 发送数据给客户端
        socket.send(packetToClient);
    }
}
public class UDPClient {
    public static void main(String[] args) throws Exception {
        // 客户端发数据报给服务端
        DatagramSocket socket = new DatagramSocket();
        // 要发送给服务端的数据
        byte[] buf = "Hello World".getBytes();
        // 将IP地址封装成InetAddress对象
        InetAddress address = InetAddress.getByName("127.0.0.1");
        // 将要发送给服务端的数据封装成DatagramPacket对象需要填写上ip地址与端口号
        DatagramPacket packet = new DatagramPacket(buf, buf.length, address, 65001);
        // 发送数据给服务端
        socket.send(packet);
        // 客户端接受服务端发送过来的数据报
        byte[] data = new byte[100];
        // 创建DtagramPacket对象 用来存储服务端发送过来的数据
        DatagramPacket receivedPacket = new DatagramPacket(data, data.length);
        // 将接受到的数据存储到DatagramPacket对象中
        socket.receive(receivedPacket);
        // 将服务器端发送过来的数据取出来并打印到控制台
        String content = new String(receivedPacket.getData(), 0, receivedPacket.getLength());
        System.out.println(content);
    }
}

image.gif

四、Java原生网络编程-NIO编程

1、NIO开发步骤:

1、创建ServerSocketChannl,配置为非阻塞

2、绑定监听,配置tcp参数

3、创建IO线程,用于轮询多路复用器Selector
4、创建
Selector。注册ServerSocketChannl

5、启动IO线程

6、当轮询到处于就绪的Channel时,判断是否为OP_ACCEPT,调用accept接收新client
7、设置新
client为非zuse

8、注册SocketChannl

9、如果轮询的channelOP_READ,则继续读取

10、如果轮询的channelOP_WRITE,则继续发送

/**
 * 类说明:nio通信服务端
 */
public class NioServer {
    private static NioServerHandle nioServerHandle;
    public static void start() {
        if (nioServerHandle != null) {
            nioServerHandle.stop();
        }
        nioServerHandle = new NioServerHandle(Const.DEFAULT_PORT);
        new Thread(nioServerHandle, "Server").start();
    }
    public static void main(String[] args) {
        start();
    }
}
/**
 * 类说明:nio通信服务端处理器
 */
public class NioServerHandle implements Runnable {
    private Selector selector;
    private ServerSocketChannel serverChannel;
    private volatile boolean started;
    /**
     * 构造方法
     *
     * @param port 指定要监听的端口号
     */
    public NioServerHandle(int port) {
        try {
            // 创建选择器
            selector = Selector.open();
            // 打开监听通道
            serverChannel = ServerSocketChannel.open();
            // 如果为 true,则此通道将被置于阻塞模式;
            // 如果为 false,则此通道将被置于非阻塞模式
            // 开启非阻塞模式
            serverChannel.configureBlocking(false);
            serverChannel.socket().bind(new InetSocketAddress(port));
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);
            // 标记服务器已开启
            started = true;
            System.out.println("服务器已启动,端口号:" + port);
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }
    }
    public void stop() {
        started = false;
    }
    @Override
    public void run() {
        // 循环遍历selector
        while (started) {
            try {
                // 阻塞,只有当至少一个注册的事件发生的时候才会继续.
                selector.select();
                Set<SelectionKey> keys = selector.selectedKeys();
                Iterator<SelectionKey> it = keys.iterator();
                SelectionKey key = null;
                while (it.hasNext()) {
                    key = it.next();
                    it.remove();
                    try {
                        handleInput(key);
                    } catch (Exception e) {
                        if (key != null) {
                            key.cancel();
                            if (key.channel() != null) {
                                key.channel().close();
                            }
                        }
                    }
                }
            } catch (Throwable t) {
                t.printStackTrace();
            }
        }
        // selector关闭后会自动释放里面管理的资源
        if (selector != null) {
            try {
                selector.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    private void handleInput(SelectionKey key) throws IOException {
        if (key.isValid()) {
            // 处理新接入的请求消息
            if (key.isAcceptable()) {
                ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                SocketChannel sc = ssc.accept();
                System.out.println("=======建立连接===");
                sc.configureBlocking(false);
                sc.register(selector, SelectionKey.OP_READ);
            }
            // 读消息
            if (key.isReadable()) {
                System.out.println("======socket channel 数据准备完成," + "可以去读==读取=======");
                SocketChannel sc = (SocketChannel) key.channel();
                // 创建ByteBuffer,并开辟一个1M的缓冲区
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                // 读取请求码流,返回读取到的字节数
                int readBytes = sc.read(buffer);
                // 读取到字节,对字节进行编解码
                if (readBytes > 0) {
                    // 将缓冲区当前的limit设置为position,position=0,
                    // 用于后续对缓冲区的读取操作
                    buffer.flip();
                    // 根据缓冲区可读字节数创建字节数组
                    byte[] bytes = new byte[buffer.remaining()];
                    // 将缓冲区可读字节数组复制到新建的数组中
                    buffer.get(bytes);
                    String message = new String(bytes, "UTF-8");
                    System.out.println("服务器收到消息:" + message);
                    // 处理数据
                    String result = Const.response(message);
                    // 发送应答消息
                    doWrite(sc, result);
                }
                // 链路已经关闭,释放资源
                else if (readBytes < 0) {
                    key.cancel();
                    sc.close();
                }
            }
        }
    }
    /**
     * 发送应答消息
     */
    private void doWrite(SocketChannel channel, String response) throws IOException {
        // 将消息编码为字节数组
        byte[] bytes = response.getBytes();
        // 根据数组容量创建ByteBuffer
        ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
        // 将字节数组复制到缓冲区
        writeBuffer.put(bytes);
        // flip操作
        writeBuffer.flip();
        // 发送缓冲区的字节数组
        channel.write(writeBuffer);
    }
}
/**
 * 类说明:nio通信客户端
 */
public class NioClient {
    private static NioClientHandle nioClientHandle;
    public static void start() {
        if (nioClientHandle != null) {
            nioClientHandle.stop();
        }
        nioClientHandle = new NioClientHandle(Const.DEFAULT_SERVER_IP, Const.DEFAULT_PORT);
        new Thread(nioClientHandle, "Server").start();
    }
    /**
     * 向服务器发送消息
     */
    public static boolean sendMsg(String msg) throws Exception {
        nioClientHandle.sendMsg(msg);
        return true;
    }
    public static void main(String[] args) throws Exception {
        start();
        // 向服务器发送消息
        Scanner scanner = new Scanner(System.in);
        while (NioClient.sendMsg(scanner.next())) {
        }
    }
}
/**
 * 类说明:nio通信客户端处理器
 */
public class NioClientHandle implements Runnable {
    private String host;
    private int port;
    private volatile boolean started;
    // 选择器
    private Selector selector;
    // 管道
    private SocketChannel socketChannel;
    public NioClientHandle(String ip, int port) {
        this.host = ip;
        this.port = port;
        try {
            // 创建选择器
            this.selector = Selector.open();
            // 打开监听通道
            socketChannel = SocketChannel.open();
            // 如果为 true,则此通道将被置于阻塞模式;如果为 false,则此通道将被置于非阻塞模式;缺省为true
            socketChannel.configureBlocking(false);
            started = true;
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(-1);
        }
    }
    public void stop() {
        started = false;
    }
    @Override
    public void run() {
        // 连接服务器
        try {
            doConnect();
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(-1);
        }
        // 循环遍历selector
        while (started) {
            try {
                // 阻塞方法,当至少一个注册的事件发生的时候就会继续
                selector.select();
                // 获取当前有哪些事件可以使用
                Set<SelectionKey> keys = selector.selectedKeys();
                // 转换为迭代器
                Iterator<SelectionKey> it = keys.iterator();
                SelectionKey key = null;
                while (it.hasNext()) {
                    key = it.next();
                    // 我们必须首先将处理过的 SelectionKey 从选定的键集合中删除。
                    // 如果我们没有删除处理过的键,那么它仍然会在事件集合中以一个激活的键出现,这会导致我们尝试再次处理它。
                    it.remove();
                    try {
                        handleInput(key);
                    } catch (Exception e) {
                        if (key != null) {
                            key.cancel();
                            if (key.channel() != null) {
                                key.channel().close();
                            }
                        }
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
                System.exit(-1);
            }
        }
        if (selector != null) {
            try {
                selector.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    // 具体的事件处理方法
    private void handleInput(SelectionKey key) throws IOException {
        if (key.isValid()) {
            // 获得关心当前事件的channel
            SocketChannel sc = (SocketChannel) key.channel();
            // 处理连接就绪事件
            // 但是三次握手未必就成功了,所以需要等待握手完成和判断握手是否成功
            if (key.isConnectable()) {
                // finishConnect的主要作用就是确认通道连接已建立,
                // 方便后续IO操作(读写)不会因连接没建立而
                // 导致NotYetConnectedException异常。
                if (sc.finishConnect()) {
                    // 连接既然已经建立,当然就需要注册读事件,
                    // 写事件一般是不需要注册的
                    socketChannel.register(selector, SelectionKey.OP_READ);
                } else {
                    System.exit(-1);
                }
            }
            // 处理读事件,也就是当前有数据可读
            if (key.isReadable()) {
                // 创建ByteBuffer,并开辟一个1k的缓冲区
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                // 将通道的数据读取到缓冲区,read方法返回读取到的字节数
                int readBytes = sc.read(buffer);
                if (readBytes > 0) {
                    buffer.flip();
                    byte[] bytes = new byte[buffer.remaining()];
                    buffer.get(bytes);
                    String result = new String(bytes, "UTF-8");
                    System.out.println("客户端收到消息:" + result);
                }
                // 链路已经关闭,释放资源
                else if (readBytes < 0) {
                    key.cancel();
                    sc.close();
                }
            }
        }
    }
    /**
     * 进行连接
     */
    private void doConnect() throws IOException {
        // 如果此通道处于非阻塞模式,则调用此方法将启动非阻塞连接操作。
        // 如果连接马上建立成功,则此方法返回true。
        // 否则,此方法返回false,
        // 因此我们必须关注连接就绪事件,
        // 并通过调用finishConnect方法完成连接操作。
        if (socketChannel.connect(new InetSocketAddress(host, port))) {
            // 连接成功,关注读事件
            socketChannel.register(selector, SelectionKey.OP_READ);
        } else {
            socketChannel.register(selector, SelectionKey.OP_CONNECT);
        }
    }
    /**
     * 写数据对外暴露的API
     *
     * @param msg
     * @throws IOException
     */
    public void sendMsg(String msg) throws IOException {
        doWrite(socketChannel, msg);
    }
    /**
     * 写数据
     *
     * @param sc
     * @param request
     * @throws IOException
     */
    private void doWrite(SocketChannel sc, String request) throws IOException {
        byte[] bytes = request.getBytes();
        ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
        writeBuffer.put(bytes);
        writeBuffer.flip();
        sc.write(writeBuffer);
    }
}
public class Const {
    public static int DEFAULT_PORT = 12345;
    public static String DEFAULT_SERVER_IP = "127.0.0.1";
    public static String response(String msg) {
        return "Hello," + msg + ",Now is " + new java.util.Date(System.currentTimeMillis()).toString();
    }
}

image.gif

2、Java原生网络编程-BIO、NIO主要区别

阻塞与非阻塞IO

Java IO的各种流是阻塞的。这意味着,当一个线程调用read() write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。

Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

面向流与面向缓冲
BIO是面向流的,
NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

选择器(Selectors
Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。

相关文章
|
2月前
|
Java
如何理解网络阻塞 I/O:BIO
如何理解网络阻塞 I/O:BIO
|
7天前
|
监控 Java 开发者
深入理解 Java 网络编程和 NIO
【4月更文挑战第19天】Java网络编程基于Socket,但NIO(非阻塞I/O)提升了效率和性能。NIO特点是非阻塞模式、选择器机制和缓冲区,适合高并发场景。使用NIO涉及通道、选择器和事件处理,优点是高并发、资源利用率和可扩展性,但复杂度、错误处理和性能调优是挑战。开发者应根据需求选择是否使用NIO,并深入理解其原理。
|
存储 设计模式 网络协议
Netty网络框架(一)
Netty网络框架
34 1
|
2月前
|
移动开发 编解码 网络协议
用Java的BIO和NIO、Netty来实现HTTP服务器(三) 用Netty实现
用Java的BIO和NIO、Netty来实现HTTP服务器(三) 用Netty实现
|
2月前
|
网络协议 Java Linux
用Java来实现BIO和NIO模型的HTTP服务器(二) NIO的实现
用Java来实现BIO和NIO模型的HTTP服务器(二) NIO的实现
|
2月前
|
编解码 网络协议 Java
用Java的BIO和NIO、Netty实现HTTP服务器(一) BIO与绪论
用Java的BIO和NIO、Netty实现HTTP服务器(一) BIO与绪论
|
2月前
|
消息中间件 网络协议 Java
一文彻底理解BIO、NIO、AIO
一文彻底理解BIO、NIO、AIO
35 0
|
3月前
|
Java 应用服务中间件 Linux
java中的NIO,BIO,AIO
java中的NIO,BIO,AIO
17 0
|
1月前
|
存储 Java 数据处理
|
1月前
|
Java API
java中IO与NIO有什么不同
java中IO与NIO有什么不同